Commit Graph
1096 Commits
Author SHA1 Message Date
Taylor HoandCarl b0d2b64094 fix(composer): restore card mint status in activity rail
Render the existing card-mint status chip inside the unified composer activity row so minting, ready, and failure states remain visible alongside live agent pills and typing.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:03 -07:00
Taylor HoandCarl 0d98a40a78 fix(composer): reconcile live-activity strip with the dock geometry from #3151
Post-rebase integration fixes after rebasing onto main, which landed the
composer dock geometry contract (PR #3151) in the same region this branch
reworks. The rebase kept main's dock architecture (reserved bottom rail +
ComposerActivityAccessory fade) with the branch's pill strip rendered
inside it; this commit cleans up the fallout:

- ChannelPane.tsx: re-add the useChannelWorkingAgentPubkeys import — the
  dock's reserved-rail toggle (composer-dock--with-activity) still needs
  the channel-level working-set check even though
  ChannelComposerActivityRow computes its own copy — and consolidate the
  gating into a single hasComposerBottomActivity expression
- ChannelComposerActivityAccessory.tsx: delete — main's channel-level
  wrapper from #3151 is fully superseded by ChannelComposerActivityRow
  rendered inside ComposerActivityAccessory
- AppShell.tsx / routes/root.tsx: move the temporary DebugHarnessMount
  from AppShell (999/1000 lines on main, so the mount no longer fits the
  file-size ratchet) to the root route via a RootComponent wrapper
- agents/debug/README.md + DebugHarnessMount.tsx: update the removal
  instructions and header comment for the new mount location

Validated with just desktop-check, 3,817 desktop unit tests, and the
channels / composer-overflow / threadpane-ultrawide e2e specs (88
passing).

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:03 -07:00
Taylor HoandCarl fb2a99598c fix(composer): lone typing group uses the full row width instead of truncating at the strip cap
- BotActivityBar.tsx: apply the typing slot's max-w-64 cap only when the
  group shares the strip with working-agent pills. The cap exists so a
  long "X, Y, and N more are typing" label can't inflate the strip's
  scroll extent alongside pills, but it was unconditional — as the
  strip's LONE item a two-name label ("Alpha Debug and Beta Debug are
  typing...") got cut off at 256px even with the row mostly empty. The
  lone item already shrinks with the container (min-w-0 through the
  slot chain), so it now consumes the row's free width and only
  ellipsizes when the container is genuinely narrow.
- channels.spec.ts: add "lone typing group uses the full row width"
  e2e regression — three typers push the label past the old cap, then
  asserts no truncation at full width (scrollWidth <= clientWidth) and
  graceful ellipsizing without scroll fades at a 200px row. Verified
  the test fails against the old unconditional cap.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:02 -07:00
Taylor HoandCarl e5b77b9b15 fix(composer): past-tense plan and thought headlines in the activity pill
- Normalize plan items in getActivityHeadline (agentSessionTranscriptPresentation.ts):
  the fallthrough previously returned the raw item title, so ACP plan
  updates headlined the pill as a bare "Plan" / "Plan updated" next to the
  past-tense tool verbs ("Read foo.ts", "Ran …") — now "Created plan" for
  the initial plan and "Updated plan" for update markers (keyed off isUpdate)
- Normalize thought items the same way: "Thinking" → "Thought", and the
  plan-titled thought that rendered "Planning" → "Planned"
- Assistant-message headlines (first line of the streamed reply) are
  deliberately untouched — they are content, not action labels
- Add test coverage for all four normalized cases

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:02 -07:00
Taylor HoandCarl 3669a0d280 feat(composer): morph the avatar between typing group and pill on partition flips
The one strip transition that can't be an in-place relabel — an agent's
first promotion from the merged typing group into a status pill — played
as exit-here/enter-there: the avatar dropped out of the overlapping
group while a new pill faded in beside it. Give both avatar homes a
matching motion layoutId so the avatar MORPHS from the group into the
new pill (geometry keeps the hop short: a new pill appends rightmost,
directly adjacent to the trailing typing slot).

- desktop/src/features/channels/ui/composerStripAvatarLayout.ts: new
  leaf module — ComposerStripAvatarScopeContext (per-strip namespace),
  useComposerStripAvatarLayoutId, and the shared morph spring. layoutIds
  are app-global and the channel strip + thread panel strip can be
  mounted at once, so ids bake in a per-strip scope string. Motion's own
  LayoutGroup id namespacing is deliberately NOT used: LayoutGroup
  re-measures every layout-animating member whenever any member updates,
  which turned each pill label ticker resize into a group-wide
  onLayoutAnimationStart storm that permanently held the pills' deferred
  label swaps (caught by the hover-freeze e2e test)
- desktop/src/features/channels/ui/BotActivityBar.tsx: strip provides
  the scope via React.useId(); the pill avatar is wrapped in a
  motion.div carrying the shared-element id (dropped under reduced
  motion)
- desktop/src/features/messages/ui/TypingIndicatorRow.tsx: avatar
  extracted into a TypingAvatar sub-component that reads the scope from
  context — no new props, and typing rows rendered outside a strip
  (context null) are unaffected
- ChannelComposerActivityRow.tsx / ChannelPane.tsx: no call-site wiring
  needed beyond the strip itself (context flows through the
  typingIndicator slot)

E2E: full pill/typing strip set passes (10/10 smoke), including the
hover-freeze membership test that failed under the LayoutGroup approach.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:02 -07:00
Taylor HoandCarl 2f132519c6 feat(composer): capability-based pill partition keeps pills through the turn-end gap
The composer strip's pill-vs-typing-group partition keyed on signal
source: any typing-fallback agent went to the merged typing group. An
agent whose observer turn just completed but who was still typing
therefore flipped partitions — its pill exited and a new typing-group
item entered at the trailing slot — and re-typing after a finished turn
mounted a group item instead of relighting the pill.

Partition on capability instead: an agent renders as a pill when there
is a session worth hovering and opening — an active observer turn, or a
headline-able transcript for the channel left behind by a prior turn.
Only an agent's first-ever activity in a channel can now start in the
typing group; every later working↔typing transition is an in-place pill
relabel (the "is typing…" override from cf6014de9).

- desktop/src/features/channels/ui/composerLiveActivity.ts: add pure
  partitionComposerWorkingAgents helper (injected getWorkingSource /
  getTranscript readers, mirrors deriveAgentWorkingOrder's style);
  pill-worthiness = source !== "typing" OR deriveActivityPillLabel
  non-null, so lifecycle-noise-only transcripts ("Turn started" seeds)
  still fold into the group
- desktop/src/features/channels/ui/ChannelComposerActivityRow.tsx:
  partition snapshot now subscribes to BOTH the working signal and the
  observer store (a transcript landing must be able to promote a typing
  agent); rename observerWorkingPubkeys → pillBotPubkeys to match the
  capability semantics
- desktop/src/features/channels/ui/composerLiveActivity.test.mjs: six
  new tests — observer always pills, transcript-less typing groups,
  turn-end gap keeps the pill, lifecycle-noise-only transcripts group,
  channel scoping, mixed-roster order preservation
- desktop/src/features/channels/ui/BotActivityBar.tsx +
  desktop/src/features/agents/debug/README.md: docs updated to the
  capability partition

E2E: shows-and-clears, composer-does-not-shift, typing-group-in-strip,
typing-avatars, and the pill hover-freeze suite all pass (8/8 smoke).

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:01 -07:00
Taylor HoandCarl bb1271fcaf fix(composer): scope slow reorder pacing to slot travel, not enter/exit
The activity pill slot's single top-level 0.9s transition
(PILL_REORDER_DURATION_S) applied to every animated value, so the
enter/exit fade+scale inherited the slow spring meant only for reorder
travel — pills faded/scaled in and out over 0.9s.

- Split AnimatedPillSlot's transition per value: `layout` keeps the
  0.9s reorder spring unchanged
- `opacity` keeps the 0.9s linear tween only while the slot is moving
  (the mid-move dip keyframes must still land with the layout spring);
  otherwise it uses a fast 0.18s ease-out for enter/exit
- `scale` always uses the fast tween — it only animates on enter/exit
- Add PILL_ENTER_EXIT_DURATION_S (0.18s) alongside the reorder constant

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:01 -07:00
Taylor HoandCarl 1b3b236555 feat(composer): relabel working pill to "is typing" in place while agent types
An observer-backed agent that started typing its reply mid-turn had the
typing signal swallowed entirely: computeAgentWorkingState drops a
channel's typing entry under observer precedence, so the pill kept
showing the stale last tool action and typing could only ever surface as
a separate item in the trailing typing group. Now the existing pill's
ticker swaps to "<name> is typing…" in the same slot and swaps back to
the last action headline when typing clears.

- desktop/src/features/agents/agentWorkingSignal.ts: add
  getAgentChannelTypingSince(pubkey, channelId) — a raw typing-registry
  read that deliberately bypasses the observer-precedence fold,
  returning the first-seen typing timestamp or null
- desktop/src/features/channels/ui/BotActivityBar.tsx:
  BotActivityAgentPill subscribes via useSyncExternalStore; while
  typing, the ticker keys on a dedicated TYPING_LABEL_ID and the label
  becomes "<name> is typing…" — same slot, avatar, and hover feed, no
  new strip item. Partition unchanged: typing-only agents still divert
  to the combined typing indicator group
- desktop/src/features/agents/agentWorkingSignal.test.mjs: three new
  tests pinning the getter (null cases, first-seen anchor lifecycle,
  and not-folded-under-observer-precedence)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:01 -07:00
Taylor HoandCarl 2dc5d4826d feat(composer): placement-aware pill resize guard for the activity strip
Replace the all-or-nothing hover freeze in the composer activity bar with
a guard that only pins pill widths when a resize would actually move the
open hover card.

- BotActivityBar.tsx: drop the blanket pinWidth/freezeLayout tied to bar
  hover; compute activeCardIndex (pill anchoring the open card) and pin
  width / freeze slot layout only for pills at or left of that index.
  The card renders side="top" align="start" — anchored to its pill's
  left edge — so only those pills can displace it by resizing
- Pills right of the anchor, and every pill when no card is open (even
  with the bar hovered), now resize freely with their label swaps and
  keep slot layout animation so neighbors ease instead of snapping
- Keep the membership/order freeze on bar hover unchanged — a pill
  exiting mid-hover would slide the strip under the cursor, which is
  independent of card placement
- Expose the membership hold via a data-hold attribute on the strip
  root, since the inline width pin is no longer a proxy for it
- channels.spec.ts: bar-hover test now waits on data-hold, asserts pills
  are NOT width-pinned during bar-only hover, and parks the cursor past
  the last pill (label swaps can legitimately grow a pill into the
  inter-pill gap); add "open card pins only its anchor pill and the
  pills left of it" covering both directions; add
  waitForSettledPillBoxes helper because pills mount with short generic
  labels and grow via the slot layout spring when seeded headlines land

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:00 -07:00
Taylor HoandCarl 1814d7d332 style(composer): rebalance activity pill spacing
- Tighten avatar-to-label gap inside each working-agent pill (gap-2 -> gap-1.5)
- Widen the gap between strip items (pills + typing indicator) for clearer separation (gap-1.5 -> gap-3)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:00 -07:00
Taylor HoandCarl c320cedfcb fix(composer): keep strip edge fades live when pill content grows after a resize
The overflow-fade hook resize-observes the scroller and its content
wrapper, but the wrapper was a min-w-0 flex item: under a narrow
container it collapsed to the scroller's content width, so the shrink-0
pill slots overflowed it invisibly. When a pill's deferred label ticker
swapped in a longer label AFTER the resize settled, scrollWidth grew
without resizing any observed element — no ResizeObserver callback, and
the edge fades stayed stale (missing) until a manual scroll. Caught by
the "narrow strip scrolls horizontally with edge fades" e2e spec, which
failed deterministically because label swaps are intentionally deferred
until slot layout settles.

- BotActivityBar.tsx: give the scroller's content wrapper min-w-max on a
  multi-item strip so its border box always tracks content size and every
  late growth fires the ResizeObserver; keep min-w-0 for the lone-item
  case so a single pill/typing group still shrinks to fit (shrinkToFit)
  instead of overflowing into scroll
- Side effect: pills no longer visually compress below their natural
  width inside a constrained strip (the collapsed wrapper was squeezing
  them); they keep readable widths and the strip scrolls as designed
- Document the min-w-max dependency at the ResizeObserver in
  useStripOverflowFades so the wrapper's sizing isn't "simplified" back
  into a stale-fade regression

Verified: all 13 activity/pill/typing specs in channels.spec.ts pass
(including the previously failing narrow-strip fade spec and the new
typing-inside-strip spec), plus both channel-composer-overflow specs.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:42:00 -07:00
Taylor HoandCarl 690cc8f753 feat(composer): terse tool-call headline for the activity pill
- Rewrite the tool branch of getActivityHeadline (agentSessionTranscriptPresentation.ts)
  to use the descriptor's terse action tier — verb + compact object, e.g.
  "Read foo.ts" — instead of "label · full preview" ("Read file ·
  src/agents/ui/foo.ts"), so the composer pill's 200px cap shows the
  informative part of the action instead of ellipsizing a long preview
- Basename path-like objects only (file-read / file-edit / skill-read);
  shell commands stay whole since they legitimately contain "/", and file
  edits reuse the already-basenamed fileEditSummary.filename
- Fall back to the previous "label · preview" format when a descriptor
  carries no action (older descriptors)
- Export the existing private basename() from agentSessionFileEditDiff.ts
  rather than adding a third copy
- Document the terse format on deriveActivityPillLabel (composerLiveActivity.ts);
  no logic change — the pill picks it up via getActivityHeadline, whose only
  production consumer is the pill
- Tests: update presentation expectations ("Send Message · abc" → "Sent abc"),
  add cases for file-read/file-edit basenaming, shell commands kept whole,
  and the no-action fallback; add a pill-level terse-headline case to
  composerLiveActivity.test.mjs

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:59 -07:00
Taylor HoandCarl 1bb36782af feat(composer): unify typing indicator into the pill strip and align pill styling
Typing group becomes a slot sibling of the working-agent pills:

- BotActivityBar.tsx: BotActivityComposerAction gains a `typingIndicator`
  prop rendered as the strip's trailing AnimatedPillSlot, so the typing
  group shares the scroller, edge fades, and layout/enter/exit animations
  with the pills; lone-item shrink logic now counts the typing slot
- ChannelComposerActivityRow.tsx: the row renders ONE strip hosting both
  groups instead of a pill strip beside a standalone TypingIndicatorRow
- ChannelPane.tsx / MessageThreadPanel.tsx: thread panel mirrors the same
  structure — ChannelPane builds the strip (thread typers included) and the
  panel's activity row just mounts it; `threadTypingPubkeys` prop removed
  from MessageThreadPanel; row height fixed at h-8.5 to match the channel
  row so the bottom-anchored composer never bumps
- TypingIndicatorRow.tsx: drop the now-unused "activity" variant — the
  strip's slot owns sizing/spacing, so only the default variant remains
- composer.css: corner-mask offset 2.5rem -> 2.625rem to match the fixed
  h-8.5 activity row below the composer in both surfaces
- channels.spec.ts: new e2e covering the typing group rendering inside the
  strip, trailing the pills, and scrolling under the edge fades

Align the "is working" pill with the "is typing" indicator:

- Remove the pill's border, background, shadow, and horizontal padding —
  hover/open states signal through text color only
- Match label typography (text-xs font-medium, was font-semibold) and the
  typing label's translate-y-px optical nudge; avatar-to-label gap-2
- Drop the labelClassName="font-semibold" overrides that compensated for
  the pill's old heavier weight (channel row + thread strip)

Also document in AGENTS.md that e2e reruns need `pnpm run build:e2e` — a
plain build omits the mock-bridge module and every spec fails at boot.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:59 -07:00
Taylor HoandCarl 33ede155c6 feat(composer): stable turn-start pill order and persistent action labels
Calm the working-agent pill strip while multiple agents stream: position
now encodes identity (stable for a whole turn), liveness stays on the
per-pill label tickers, and positional motion is reserved for membership
changes (an agent starting or finishing) — the transitions a builder
actually needs to notice. Grounded in the work-visibility and
completion-awareness JTBDs: transcript-recency ordering shuffled pills on
every agent event (cry-wolf motion) and buried quiet/stuck agents at the
scrolled-off end of the strip.

- composerLiveActivity.ts: replace deriveAgentActivityOrder (transcript
  recency, newest first) with deriveAgentWorkingOrder — sorts by
  channel-scoped turn anchorAt ascending (earliest worker left-most, new
  agents append right), quantized to whole seconds so retroactive
  clock-offset refinements cannot reorder the strip, stable roster-order
  tiebreak, no-anchor agents keep roster order at the end
- composerLiveActivity.ts: remove the 6s label decay
  (ACTIVITY_PILL_STALE_MS and the now/staleAfterMs params) — the last
  real action now persists through quiet stretches; the generic
  "<name> is working…" label only shows before the first action lands,
  since the last action is more informative than a placeholder while a
  turn is in progress
- BotActivityBar.tsx: order snapshot subscribes to agentWorkingSignal
  (turn anchors) instead of the observer store's transcript scan; drop
  the per-pill useNow(1000) staleness tick so pills no longer re-render
  every second; hover-freeze machinery (order/membership/width hold)
  unchanged and still guards membership changes under the cursor
- composerLiveActivity.test.mjs: six anchor-ordering tests (earliest
  first, append-on-later-start, channel scoping, unscoped
  earliest-across-channels, roster fallback, sub-second-shift stability);
  decay tests replaced with a headline-persists-regardless-of-age test
- channels.spec.ts: retarget the two hover-freeze e2e tests from recency
  flips to membership changes — a third agent (lyra) starting a turn
  mid-hover appends only after the hold releases, and a turn completing
  mid-bar-hover exits only after release
- e2eBridge.ts: __BUZZ_E2E_SEED_ACTIVE_TURNS__ accepts an explicit atMs
  so tests seed well-separated turn-start anchors deterministically
- channels.spec.ts: seedPillActivityMessage now takes the matching
  turnId — the old hardcoded id resurrected a phantom sibling turn that
  would have kept a completed agent "working" forever
- debugAgentHarness.ts / debug README: update decay-era comments to the
  hold-last-headline behavior

Verified: 3652 unit tests pass; 12 pill/activity/typing e2e tests pass
against a fresh build:e2e.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:59 -07:00
Taylor HoandCarl 2fe4b68091 fix(composer): pin activity strip edge fades to the container edges
- Bleed the pill strip's scroll viewport across the mounting row's px-5
  gutter (-mx-5 on the strip root, px-5 restored as scroller padding) so
  overflowing pills clip at the container's visual edge instead of the
  row's padded content box — previously a pill faded out mid-row, 20px
  shy of the edge, with a dead gutter after the gradient
- Keep at-rest alignment unchanged: the scroller's internal px-5 keeps
  the first pill on the row gutter in both mounting surfaces
  (ChannelComposerActivityRow and the thread panel toolbar row)
- Widen the edge fades from w-8 to w-12 so the gradient keeps a soft
  fade zone over the pills after spanning the 20px gutter
- Grow the pill label ticker viewport from h-3.5 to h-4 with a matching
  leading-4 line box so Inter's descenders ("g", "y") are no longer
  sheared off the label
- Extend the narrow-strip e2e test to assert both fades sit within 1px
  of the activity row's border-box edges, guarding the full-bleed
  geometry against regression

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:58 -07:00
Taylor HoandCarl 3e597d003b feat(composer): ship live activity by default and scroll the pill strip when narrow
Remove the composerLiveActivity preview flag so the live-activity hover
preview ships for everyone, and land the pill strip overflow behavior
for narrow containers.

Flag removal (ships by default):
- preview-features.json: drop the composerLiveActivity entry — absence
  from the manifest means stable/always-on; E2E preview seeding and the
  Settings > Experiments card both derive from the manifest, so they
  pick this up automatically
- BotActivityBar.tsx: remove the useFeatureEnabled gate and the legacy
  flag-off "View activity" popover item; the pill hover popover now
  always renders ComposerLiveActivityFeed
- Remove the now-dead openAgentSessionPubkey plumbing through
  BotActivityAgentPill / BotActivityComposerAction /
  ChannelComposerActivityRow / ChannelPane — it only backed the legacy
  item's active-session highlight
- Update stale flag references in ComposerLiveActivityFeed's doc
  comment, the agents debug harness README, and a channels.spec.ts
  comment

Pill strip overflow (BotActivityBar.tsx, ChannelComposerActivityRow.tsx):
- With several pills in a narrow container (thread panel toolbars
  especially), pills keep their natural label-truncated width and the
  strip scrolls horizontally — scrollbar hidden, edge gradient fades
  signalling clipped pills — instead of compressing every pill into an
  unreadable sliver
- New useStripOverflowFades hook tracks scroll position plus
  scroller/content resizes (ResizeObserver) so fades appear and
  disappear without a re-render triggering event
- A lone pill shrinks to fit (min-w-0) instead of scrolling — an edge
  fade over a single pill reads as a cut-off bug, not an affordance
- layoutScroll on the scroller keeps motion's slot layout animations
  measured relative to the scroll offset
- e2e: add "narrow strip scrolls horizontally with edge fades instead
  of compressing pills" and "lone pill shrinks to fit a narrow
  container without scroll fades"

Verified: tsc, biome check, desktop unit tests (3651 pass), and the
pill/strip/live-activity e2e subset. Two pre-existing channels.spec.ts
failures ("composer does not shift when the activity row mounts and
clears", "hovering the bar itself freezes pill order without opening a
card") reproduce identically on the parent commit with a clean tree —
unrelated to this change.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:57 -07:00
Taylor HoandCarl df164431c7 fix(desktop): align thread composer activity row with the full-width composer
The thread footer's activity/typing row was centered into an mx-auto
max-w-4xl column while the thread composer above it spans the full pane
width, so in a wide thread pane the typing indicator floated toward the
pane's center instead of hugging the composer's left gutter.

- MessageThreadPanel.tsx: drop the mx-auto/max-w-4xl wrapper so the row
  spans the same width as the composer above it
- Remove flex-1 from the toolbarExtraActions (agent pill strip) wrapper —
  it previously ate half the row and pushed the typing group to the
  midpoint whenever it mounted; the strip now sizes to its content,
  mirroring ChannelComposerActivityRow
- Make the typing indicator's leading padding conditional like the
  channel row: composer-edge padding only when the typing group leads
  the row, pl-0 when it follows pills so the flex gap is the whole
  spacing

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:57 -07:00
Taylor HoandCarl 442e08b4e0 feat(composer): collapse typing agents and humans into one indicator group
- ChannelComposerActivityRow.tsx: partition the channel's working set by
  signal source — observer-backed agents keep the interactive activity
  pills, while typing-fallback-only agents (no observer turn, nothing to
  hover or open) are diverted out of the pill strip and merged with the
  human typers into ONE combined TypingIndicatorRow group with an
  overlapping avatar set; channel-agent roster names are overlaid onto
  the profile lookup so roster-only agents never fall back to truncated
  pubkeys
- Fix row layout: the pill strip wrapper and typing row were both flex-1,
  splitting the row 50/50 and stranding the typing group at the halfway
  mark — the strip now sizes to content (still shrinkable) and the typing
  group takes the remainder; row gap set to gap-3, and the typing row's
  base px-4/sm:px-6 padding is zeroed next to pills (composer-edge
  alignment padding only applies when the group leads the row)
- TypingIndicatorRow.tsx: 4+ typers label now reads "X, Y, and N more are
  typing..." per spec (was "N others"); new optional labelClassName prop
  so the composer row can match the pills' font-semibold weight without
  changing the thread panel's indicator
- BotActivityBar.tsx: revert the passive typing pill variant and
  "is typing…" label — typing-only agents never reach the pill anymore;
  pill avatar bumped 18px → 20px with a uniform 3px surround
  (pl-0.75, h-7 minus 1px borders) and asymmetric pr-2 for label
  breathing room
- debug/README.md: document that the harness Typing toggle now renders
  through the combined typing indicator group

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:57 -07:00
Taylor HoandCarl fbdd4b2dcb feat(composer): label typing-fallback pills "is typing…" instead of "is working…"
- BotActivityBar.tsx: the generic decay label on a typing-fallback-only
  pill (working source "typing", no observer turn) now reads
  "<name> is typing…", matching the human typing indicator's vocabulary;
  observer-backed pills keep "<name> is working…"
- A fresh action headline still wins over both generic labels
- Both generic labels share the same ticker id, so a typing → observer
  source upgrade updates the text in place instead of playing the
  push-up swap

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:56 -07:00
Taylor HoandCarl ee7f183e26 feat(composer): freeze pill strip on hover, typing-fallback pills, faster label decay
Composer activity strip (BotActivityBar.tsx):
- Centralize hover popover state at the strip level (useStripHoverPopover):
  ONE active pill and ONE timer for the whole strip, replacing per-pill
  timers that raced each other (double-open on pill-to-pill travel, stuck
  cards when the cursor clipped an open card)
- Freeze pill order, membership, layout animation, and pill widths while
  the cursor is over the bar or a hover card is showing; queued reorders
  apply (animated) after the hold releases
- Pin the hovered pill's rendered width so label swaps can't shift
  neighboring pills under the cursor
- Open on keyboard focus only for :focus-visible, and preventDefault on
  the card's close auto-focus, fixing the re-open loop when Radix
  returned focus to the trigger
- Typing-fallback-only agents (working source "typing", no observer turn)
  now render a passive status pill: borderless, no max-width cap, no hover
  card, no click-through, not focusable — there is no transcript or
  session behind the typing signal to open

Pill label decay (composerLiveActivity.ts):
- Shorten ACTIVITY_PILL_STALE_MS 15s → 6s so pauses in the event stream
  cycle the pill back to the generic label promptly; tick 5s → 1s
- Generic label is now agent-named ("<name> is working…") instead of the
  bare "Working…"

Debug harness (features/agents/debug):
- Add per-agent "Typing" toggle simulating the basic "is typing"
  situation: a synthetic channel-scoped typing entry with no observer
  turn, exercising the typing-fallback path (useChannelActivityTyping →
  reportChannelBotTyping → passive pill); independent of Working/Progress
  so observer-over-typing precedence is testable; "End all turns" also
  clears typing
- useDebugHarnessTypingEntries appends the synthetic entries in
  useChannelActivityTyping (dev-only, reference-stable when idle)

Tests:
- channels.spec.ts: e2e coverage for hover-freeze — card freezes pill
  order with queued reorder applying on close, and bar-hover freeze
  without an open card
- composerLiveActivity.test.mjs: freshness fixtures updated for the 6s
  stale window

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:56 -07:00
Taylor HoandCarl facf4284b9 feat(composer): sync pill reorder motion and add per-action label ticker
- BotActivityBar.tsx: extract AnimatedPillSlot around each agent pill —
  reorders animate with a 0.9s spring (bounce 0.15, subtler overshoot) and
  an opacity dip via [1, 0.35, 1] keyframes running the same duration with
  linear easing, so the fade lands together with the layout switch instead
  of trailing it
- Label updates render as a clipped push-up ticker (h-3.5 overflow-hidden
  container; the new text pushes the old text up and out, 0.28s tween,
  instant under reduced motion); swaps are deferred while the pill's slot
  is mid layout animation (AnimatedPillSlot passes isMoving via render
  prop) and play once the spring settles, otherwise immediately
- Ticker keys on the backing transcript item id, not the label string: a
  genuinely new action animates, while streamed chunks that grow the same
  item (message first line, thought text) update text in place — fixes
  phantom re-animations of visually identical truncated text
- composerLiveActivity.ts: deriveActivityPillLabel now returns
  ActivityPillHeadline {id, label} and skips usage_update /
  available_commands_update lifecycle meta frames, which previously
  headlined the pill as bare "Usage"/"Commands" between real actions
- animations.css: .buzz-shimmer.truncate::before ellipsizes the glow
  overlay copy the same way as the base text, so the shimmer sweeps the
  ellipsis instead of revealing the clipped rightmost characters
- Pill max width raised from max-w-30 (120px) to max-w-36 (144px)
- channels.spec.ts: activity-indicator test now asserts the flag-ON
  reality (E2E seeds every preview feature, so composerLiveActivity always
  renders): hovering the pill opens the live-activity feed popover and
  clicking opens the agent session in the aux panel — replaces the legacy
  "View activity" item assertions, which only ever passed via a popover
  toggle race in the pre-pill trigger
- composerLiveActivity.test.mjs: object-shape assertions plus new cases —
  meta frames never headline, meta-only transcripts stay generic, and the
  headline id stays stable while a message streams (19 tests passing)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:56 -07:00
Taylor HoandCarl 8c1e29ae3b feat(composer): flatten agent activity UI into per-agent status pills
- BotActivityBar.tsx: replace the combined "agents working" trigger with one
  pill per working agent (avatar + latest action summary in a rounded-full
  chip, max-w-30 with truncation so pill widths stay uniform); extract
  BotActivityAgentPill with per-pill hover popover state (useHoverPopover)
  and remove the tab strip, tab selection state, and the 5-headline
  rotation interval
- Pill label shows the agent's single latest action headline and decays to
  a generic "Working…" state once activity goes quiet (15s staleness
  window, 5s useNow tick); deliberately no rotation between recent actions
- Hovering a pill shows the live activity feed as the popover surface
  itself (w-80 p-0, edge-to-edge, no inset bordered box); clicking the
  pill opens the agent's full runtime in the auxiliary panel
  (preventDefault stops Radix's composed trigger toggle from racing the
  hover/focus open state, which made click-to-open flaky)
- composerLiveActivity.ts: add deriveActivityPillLabel pure helper
  (channel-scoped spine-first scan + staleness decay,
  ACTIVITY_PILL_STALE_MS = 15s) and remove the now-unused
  resolveSelectedActivityAgent
- composerLiveActivity.test.mjs: replace resolveSelectedActivityAgent
  tests with 7 deriveActivityPillLabel cases (fresh headline wins,
  staleness decay, custom window, channel scoping, spine-over-metadata,
  metadata fallback, empty transcript)
- ComposerLiveActivityFeed.tsx: overlay button rounding rounded-lg →
  rounded-[inherit] so the hover tint follows the popover's rounded-xl
  clip when the feed renders edge-to-edge
- ChannelComposerActivityRow.tsx / ChannelPane.tsx: drop the removed
  variant prop (the toolbar variant was unused; both call sites rendered
  inline)
- channels.spec.ts: activity-indicator test now hovers the pill for the
  legacy preview popover and clicks the pill for direct aux-panel open
- Flag-off (composerLiveActivity disabled) hover popover keeps the legacy
  "View activity" item; pills stay h-7 inside the fixed h-8.5 activity row
  so the composer-shift guard keeps passing

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:55 -07:00
Taylor HoandCarl da349763d5 fix(composer): pin activity row height so composer doesn't shift on agent turns
- Extract the composer status strip into ChannelComposerActivityRow.tsx
  (new component hosting the inline bot-activity trigger and the typing
  indicator, plus the useChannelWorkingAgentPubkeys subscription). Also
  brings ChannelPane.tsx back under the 1000-line file-size ceiling
  (1006 -> 969).
- Change the row from min-h-8 to a fixed h-8.5 (34px). The inline
  bot-activity trigger is h-7 (28px), which plus the row's pb-1.5 (6px)
  made the active row 34px — 2px taller than the 32px idle minimum — so
  the bottom-anchored composer visibly bumped up whenever an agent turn
  or bot typing mounted the trigger, and dropped back when it cleared.
  34px matches the previous active-state height exactly: the working
  state renders identically and the idle state now reserves the same
  space. (Typing indicators alone are 20px of content, under the old
  minimum, which is why only the agent-activity trigger caused the
  shift.)
- channels.spec.ts: add "composer does not shift when the activity row
  mounts and clears" e2e test asserting the composer's y-position stays
  stable across trigger mount and clear. Verified via negative control:
  with min-h-8 restored the test fails with exactly the 2px delta.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:55 -07:00
Taylor HoandCarl bf67337a66 fix(composer): make live-activity preview subtree inert to keyboard and AT
Wrap the ManagedAgentSessionPanel subtree in a native inert container so
compact transcript rows (which can render keyboard-focusable message
links) are removed from pointer, tab-order, and assistive-tech
interaction — CSS pointer-events-none only neutralized the mouse. The
overlay open button and Last-live pill stay accessible.

Also extract deriveLastLiveAt and make it archive-aware: the Last-live
pill now takes the newest across the live transcript window, archived
channel events, and the active-turn anchor, so it can no longer disagree
with archived rows visible in the preview.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:55 -07:00
Taylor HoandCarl 9029063810 feat(composer): live activity preview for the selected working agent
Rebuild of the composer live-activity experiment (old draft PR #1575) on
current main primitives. Behind the new off-by-default
composerLiveActivity preview feature, the agents-working popover shows
one selected working agent's channel-scoped feed at a time:

- ComposerLiveActivityFeed reuses ManagedAgentSessionPanel with the
  compactPreview transcript variant (same primitive as the profile
  activity embed), so live/archive merging, projection, anchored
  scrolling, and idle handling stay owned by that surface.
- Agent switching via pill tabs (role=tablist) under the preview;
  selection resolves explicit tab > open session pane > first working
  agent, and falls through when the selected agent stops working.
- Whole preview is one click target with a Last-live recency pill;
  transcript rows underneath are inert, avoiding nested interactive
  controls inside a clickable shell.
- formatLastLiveLabel extracted from UserProfilePanelTabs into
  profile/lib/lastLiveLabel so both surfaces share the recency label.

With the experiment off, the popover renders the existing agent list
unchanged.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 16:41:54 -07:00
a7ea86cdcf fix(desktop): enable the content security policy (#4614)
This change enables a Tauri content security policy that limits
executable content to the packaged application and does not allow inline
scripts.

Relay, media, asset, and Tauri IPC schemes remain available for desktop
compatibility. The policy contains the impact of a future renderer
injection; it does not itself remove an injection bug.

## Testing

- `git diff --check origin/main...codex/security-desktop-csp`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:36:36 -07:00
eb6a37569d fix(desktop): enable message editing in Inbox (#2198)
### What changed?

Inbox detail now gives the current user's messages the same
ownership-gated Edit action as channel view. Editing reuses the existing
composer and mutation flow, preserves attachment metadata, and refreshes
structural overlays so the edited content appears immediately.

Foreign authors' messages remain non-editable, including grouped Inbox
conversations whose selected event is not the representative item.

| Own Inbox message exposes **Edit message**. | Saving the edit updates
the Inbox detail immediately. |
| --- | --- |
| ![Before: Edit message action in Inbox
detail](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/2198/inbox-edit-before.png)
| ![After: edited Inbox message
content](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/2198/inbox-edit-after.png)
|

### Why?

Inbox rows did not pass an edit handler into the shared message action
bar, so a user's own messages could be edited from channel view but not
from Inbox detail.

### How is it tested?

Desktop checks, unit tests, and the full local CI gate passed. The
focused Inbox Playwright regression passed 3 consecutive runs and covers
current-user edit/save, foreign and archived-channel denial, and
attachment preservation when a just-sent reply is edited before its
relay echo arrives.

Added tests:

-
[`inbox-edit.spec.ts`](https://github.com/block/buzz/blob/inbox-message-edit-action/desktop/tests/e2e/inbox-edit.spec.ts)
-
[`inboxViewHelpers.test.mjs`](https://github.com/block/buzz/blob/inbox-message-edit-action/desktop/src/features/home/lib/inboxViewHelpers.test.mjs)

*🤖 This PR was authored [with an
agent](buzz://message?channel=7f2d7e02-f4d5-4fb0-a426-0ca60ed3a1c3&id=c09ee04d18399b90296c3f932d22ab0377fa05f7e690ec7b08c36483ee633fbb).*

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Signed-off-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
2026-08-05 17:00:23 -06:00
Taylor HoandGitHub 005fe54d02 fix(desktop): outline the selected community (#4969)
**Category:** improvement
**User Impact:** Selected communities now use a clear offset outline
without tinting or covering their icon.

**Problem:** The selected community state replaced the icon surface with
an accent fill, obscuring image icons and changing the tile's content
treatment. Hover also changed the fill, text color, shape, and opacity,
making navigation states visually jumpy.

**Solution:** Preserve each community tile's neutral surface and content
while using a primary CSS outline for selection and a lighter outline
for hover. The transparent outline offset leaves the space around image
edges unpainted, and adjusted spacing prevents neighboring outlines from
colliding.

<img width="200" height="152" alt="Screen Recording 2026-08-05 at 3 23
32 PM"
src="https://github.com/user-attachments/assets/5c25b1c0-4be8-41c4-8f1d-ad0010310c92"
/>


<details>
<summary>File changes</summary>

**desktop/src/features/sidebar/ui/CommunityRail.tsx**
Replaces selected and hover fills with offset outlines, keeps icon
presentation stable across states, and adjusts rail and tooltip spacing
for the new outline geometry.

**desktop/tests/e2e/community-rail.spec.ts**
Covers the shared active/inactive surface, radius, text color, opacity,
and outline behavior, including hover invariants.

</details>

## Reproduction steps

1. Run the desktop app with two or more communities.
2. Give the active community an image icon.
3. Confirm the active icon keeps its original image and receives a 2px
primary outline with a transparent 2px gap.
4. Hover another community and confirm only a lighter outline appears;
its fill, text color, opacity, and corner radius remain unchanged.
5. Switch communities and confirm the outline follows the active
community.

## Screenshots

**Full desktop context**

![Selected community outline in the desktop
app](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4969/selected-community-full.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 22:44:07 +00:00
Taylor HoandGitHub 24c7995740 fix(desktop): clamp thread panel to channel surface (#4965)
**Category:** fix
**User Impact:** Expanded thread panels now stay fully visible within
the desktop channel area instead of being cut off.

**Problem:** The resize handler clamped the thread panel against the
full window width, even though the panel renders inside a narrower
channel surface. On a 1720px window, this allowed a 1160px requested
width where only 1111px could render, leaving persisted and visible
geometry out of sync.

**Solution:** Clamp resizing against the measured channel-surface width
so the stored width matches what the layout can render while preserving
the minimum 300px main pane.

<details>
<summary>File changes</summary>

**desktop/src/features/channels/ui/ChannelScreen.tsx**
Passes the measured channel-surface width into the thread-panel sizing
hook.

**desktop/src/shared/hooks/useThreadPanelWidth.ts**
Clamps drag-resize updates against the available channel width instead
of the full viewport.

**desktop/tests/e2e/threadpane-ultrawide.spec.ts**
Adds a 1720px regression proving the requested and rendered panel widths
match, while retaining the ultrawide expansion case.

</details>

### Reproduction steps

1. Open a channel thread in the desktop app at a 1720×900 window size.
2. Drag the thread panel's left resize handle toward the left edge to
expand it as far as possible.
3. Confirm the panel remains fully bounded inside the channel surface
and the main channel pane remains at least 300px wide.
4. Reload the channel and confirm the persisted expanded width renders
without clipping.

### Testing

- `pnpm --dir desktop build:e2e`
- `pnpm --dir desktop exec playwright test
tests/e2e/threadpane-ultrawide.spec.ts` — 2 passed
- Push hooks: `desktop-check` and `desktop-test` passed
- `git diff --check origin/main..HEAD`

### Screenshot

![Expanded thread panel remains bounded at
1720×900](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4965/threadpane-expanded-after-fix.png)

### Related issue

None found.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 15:40:40 -07:00
Taylor HoandGitHub cda33978b1 style(messages): increase username contrast (#4948)
**Category:** improvement
**User Impact:** Message usernames are now bolder, making it easier to
distinguish who said what at a glance.

**Problem:** Usernames and surrounding message metadata had too little
visual separation, which made message headers slower to scan.
**Solution:** Increase the shared message-author label from semibold to
bold while preserving its existing size, spacing, and interaction
behavior.

<details>
<summary>File changes</summary>

**desktop/src/features/messages/ui/MessageHeader.tsx**
Raises the shared message-author font weight so standard and system
message usernames gain consistent visual contrast.

</details>

## Reproduction steps

1. Open a channel containing messages from multiple people or agents.
2. Compare each message username with its timestamp and message body.
3. Confirm the username renders in bold while the surrounding typography
and layout remain unchanged.

## Screenshots

| Before | After |
| --- | --- |
| ![Message usernames
before](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4948/before-message-usernames.png)
| ![Message usernames
after](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4948/after-message-usernames.png)
|

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-05 15:39:07 -07:00
d42d60d64e fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)
Fixes #2381.

## What was broken

The message composer's paperclip accepts generic attachments — images,
videos, PDFs, archives, and any other supported file — but its tooltip
and accessible name still read **"Attach image"**. Sighted users might
reasonably believe the control is image-only, and screen-reader users
get an incomplete description of what the button does.

## The fix

Rename the accessible name and tooltip text on the generic composer
paperclip in `MessageComposerToolbar.tsx`:

- `aria-label` — `"Attach image"` → `"Attach file"`
- `<TooltipContent>` — `"Attach image"` → `"Attach file"`

Plus update the 12 affected Desktop e2e selectors across five spec files
to reference the new accessible name:

- `desktop/tests/e2e/file-attachment.spec.ts` (2 selectors)
- `desktop/tests/e2e/spoiler.spec.ts` (2)
- `desktop/tests/e2e/composer-image-draw.spec.ts` (2)
- `desktop/tests/e2e/image-attachment-gallery.spec.ts` (4)
- `desktop/tests/e2e/video-attachment.spec.ts` (2)

## Scope (per the issue)

The feedback screenshot dialog
(`desktop/src/features/settings/ui/SendFeedbackDialog.tsx`) is
**unchanged** — that dialog itself is image-only, so its "Attach image"
wording is accurate. This PR only touches the generic composer control.

## Test plan

- All **105** unit tests in
`desktop/src/features/messages/ui/*.test.mjs` pass locally.
- Verified no remaining `"Attach image"` string outside the
intentionally preserved feedback dialog:
  ```sh
  grep -rn '"Attach image"' desktop/
  # → only hits in SendFeedbackDialog.tsx
  ```
- The six e2e specs are only exercised in CI; the selector updates are
mechanical and verified by grep to reference the new a11y name.

## Blast radius

- **Files touched**: `MessageComposerToolbar.tsx` (two strings); five
e2e spec files (12 selector updates).
- **User-facing behaviour**: one tooltip + one screen-reader name
change; no functional or visual changes otherwise.
- **No API or state change.**

## Out of scope

- The feedback dialog's "Attach image" wording — kept per the issue's
own "Scope" guidance.
- Any i18n plumbing — Buzz Desktop doesn't currently localize these
strings.

Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Co-authored-by: Ravneet Arora <rarora@squareup.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 15:16:32 -07:00
2ea9385015 fix(reactions): support max-length custom emoji (#3833)
**Category:** fix
**User Impact:** Custom emoji with valid 64-character names can now be
used as reactions without errors.

**Problem:** Buzz accepted 64-character custom emoji names during
registration, but rejected them as reactions after the required
surrounding colons made the payload 66 characters. Validation also
differed between desktop, SDK, relay, and storage boundaries.

<img width="554" height="47" alt="image"
src="https://github.com/user-attachments/assets/4013452f-210e-4dd3-9003-f45ff3b28dc8"
/>

**Solution:** Keep the product limit at 64 ASCII characters for custom
emoji names, enforce it consistently when emoji sets are registered, and
allow only valid matching custom reaction payloads up to 66 characters.
Widen the reaction projection to preserve the wrapped payload while
retaining the existing 64-character limit for ordinary reactions.

<details>
<summary>File changes</summary>

**crates/buzz-sdk/src/builders.rs**
Defines the shared custom emoji boundaries and covers accepted
64-character and rejected 65-character shortcodes.

**crates/buzz-relay/src/handlers/ingest.rs**
Validates emoji-set shortcodes and permits 66-character reactions only
when they are valid colon-wrapped custom emoji with a matching tag.

**crates/buzz-db/src/event.rs**
Adds storage regression coverage for maximum-length custom emoji
reactions.

**crates/buzz-db/src/migration.rs**
Verifies the reaction column migration is applied correctly.

**desktop/src/shared/api/customEmoji.ts**
Enforces the existing 64-character shortcode maximum during desktop
normalization and registration/import.

**desktop/src/shared/api/customEmoji.test.mjs**
Covers the desktop shortcode boundary.

**migrations/0027_long_reaction_payloads.sql**
Widens stored reaction payloads to 66 characters for the two required
surrounding colons.

**schema/schema.sql**
Keeps the desired schema aligned with the migration.

</details>

## Reproduction Steps

1. Register or import a custom emoji whose ASCII shortcode is exactly 64
characters.
2. Select that emoji as a reaction to a message.
3. Confirm the reaction publishes, persists, and renders without an
error.
4. Attempt to register a 65-character shortcode and confirm it is
rejected.
5. Publish an ordinary or malformed reaction over 64 characters and
confirm the relay rejects it.

## Verification

- `cargo test -p buzz-sdk`: 243 passed
- `cargo test -p buzz-db`: 94 passed, 152 Postgres-required tests
ignored
- `pnpm test` in `desktop`: 3,859 passed
- `cargo test -p buzz-relay`: 795 passed, 9 existing
Postgres-unavailable failures, 35 ignored; new reaction boundary tests
pass directly
- `cargo fmt --all -- --check`
- `git diff --check`

Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f`

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-05 21:02:57 +00:00
719f9730d4 feat(desktop): allow leaving your final community (#3621)
**Category:** improvement
**User Impact:** People can leave their final Buzz community and return
to **Join or create a community** without losing their signed-in
identity.

**Problem:** Buzz Desktop blocked people from leaving when only one
community remained. Its existing remove action also changed local
configuration without ending relay membership.

**Solution:** Allow the final community to be left. Buzz now asks the
relay to end membership, removes the community locally only after
acceptance, and returns the person to the community selector while
keeping their identity signed in. If other communities remain, Buzz
switches to one of them. Relay rejection or timeout keeps the community
in place and shows an actionable retry error.

<details>
<summary>File changes</summary>

**desktop/src/features/communities/leaveCommunity.ts**
Adds signed kind 28936 publishing for active and inactive community
relays with actionable timeout handling.

**desktop/src/features/communities/leaveCommunity.test.mjs**
Covers event shape, relay selection, acceptance gating, rejection,
timeout messaging, and cleanup.

**desktop/src/features/communities/useCommunities.tsx**
Allows final-community removal and clears community-specific storage
without touching identity.

**desktop/src/features/communities/resolveCommunityRemoval.test.mjs**
Covers final, active, and inactive community removal state transitions.

**desktop/src/app/useCommunityNavigationTransitions.ts**
Gates local removal on relay acceptance and routes to a fallback
community or setup selector.

**desktop/src/app/AppShell.tsx**
Passes the asynchronous leave operation through shell entry points.

**desktop/src/features/communities/ui/EditCommunityDialog.tsx**
Replaces the local-only remove action with a pending-aware Leave
Community action that retains actionable errors.

**desktop/src/features/communities/ui/CommunitySwitcher.tsx**
Enables leaving the final community and carries the asynchronous
callback.

**desktop/src/features/sidebar/ui/AppSidebar.tsx**
Carries the asynchronous leave callback through sidebar props.

**desktop/src/features/sidebar/ui/CommunityRail.tsx**
Enables leaving the final community from rail settings.

**desktop/src/features/sidebar/ui/SidebarProfileCard.tsx**
Carries the asynchronous leave callback through profile community
settings.

**desktop/src/testing/e2eBridge.ts**
Teaches the mock relay to accept NIP-43 leave events.

**desktop/tests/e2e/community-rail.spec.ts**
Updates leave interactions and verifies final-community setup
navigation, storage cleanup, and identity preservation.

</details>

### Reproduction steps

1. Run Buzz Desktop with a signed-in identity and one joined community.
2. Open Community settings and choose **Leave Community**.
3. Confirm the app shows **Join or create a community** and the existing
identity remains signed in.
4. Repeat with two communities and confirm leaving the active one
switches cleanly to the remaining community.
5. Reject or withhold the relay `OK` response and confirm the community
remains configured with an actionable error in the dialog.

### Test plan

- `pnpm check`
- `pnpm build`
- `pnpm test` (3,913 passing)
- `pnpm build:e2e && pnpm exec playwright test
tests/e2e/community-rail.spec.ts --grep "final community"`


<img width="557" height="316" alt="image"
src="https://github.com/user-attachments/assets/b628182f-cba5-451d-ae4b-bee8d8dd19aa"
/>

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub14ndfusear8wdpe4kss8h7juc7wjk78atnqzf63zvppcpneknv4sq6x9370 <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-05 14:02:43 -07:00
7334ad1e16 fix(desktop): route macos notification clicks (#4799)
## Summary
- deliver macOS notifications through `UNUserNotificationCenter`
- route notification clicks to the referenced channel or thread through
the existing frontend activation path
- preserve click targets across cold startup and frontend remounts with
a small process-wide activation queue
- keep Linux notification activation behavior unchanged

## Architecture
A single `UNUserNotificationCenterDelegate` is installed during Tauri
setup. Each notification stores its navigation target in `userInfo`. On
click, Rust queues the target before emitting a wake-up event; the
frontend atomically drains the queue and dispatches the existing
notification action. The queue is the source of truth, which prevents
cold-start loss and duplicate delivery.

## Validation
Verified at `a81241611d617becf7640bee6fe56b5cdb4d0fab`:
- Biome format/check and lint
- TypeScript typecheck
- repository and desktop-Tauri `cargo fmt --check`
- repository and desktop-Tauri Clippy with `-D warnings`
- full pre-push desktop tests and Tauri workspace checks/tests
- desktop production build

Manual macOS validation passed: after explicitly ad-hoc signing the
local bundle with `xyz.block.buzz.app`, the operator confirmed real
Notification Center delivery and click navigation.

<details>
<summary>Local macOS test procedure</summary>

Tauri's generated ad-hoc signing identifier is not accepted by
`UNUserNotificationCenter`. Re-sign the local bundle with its bundle
identifier and keep other Buzz copies closed:

```bash
just desktop-release-build
APP="$HOME/.cache/cargo-target/aarch64-apple-darwin/release/bundle/macos/Buzz.app"
codesign --force --deep --sign - \
  --identifier xyz.block.buzz.app \
  --entitlements desktop/src-tauri/Entitlements.plist \
  "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
pkill -x buzz-desktop || true
open -n "$APP"
```

</details>

Buzz channel: `55e2bfca-1b38-48fb-9dc2-584d400501f3`

---------

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-05 13:29:41 -07:00
43cced308d feat(desktop): sync themes per community (#3653)
**Category:** new-feature
**User Impact:** Users can keep a distinct Appearance scheme for each
community and restore it on another desktop signed in with the same
identity.

**Problem:** A single global theme makes it harder to distinguish among
communities, and local-only preferences do not follow a user to another
device. **Solution:** Save each community's stable theme, accent, and
system-following selection as private encrypted relay state, backed by a
responsive local cache and guarded against switch races, invalid future
records, and relay failures.

<details>
<summary>File changes</summary>

**desktop/src/app/App.tsx**
Mounts the community-scoped theme controller inside the active community
lifecycle.

**desktop/src/features/settings/lib/appearanceScopeCopy.test.mjs**
Covers active-community and fallback labels used to explain Appearance
scope.

**desktop/src/features/settings/lib/appearanceScopeCopy.ts**
Builds a safe, trimmed label for the currently active community.

**desktop/src/features/settings/ui/SettingsPanels.tsx**
Clarifies which Appearance controls are per-community and which apply
globally when multiple communities exist.

**desktop/src/shared/constants/kinds.ts**
Defines the NIP-78 application-data event kind used for theme
preferences.

**desktop/src/shared/theme/CommunityThemeController.tsx**
Coordinates cached appearance, encrypted relay retrieval, live updates,
reconnect behavior, and safe community switching.

**desktop/src/shared/theme/ThemeProvider.tsx**
Exposes a single appearance application path so synchronized preferences
use the existing renderer and persistence behavior.

**desktop/src/shared/theme/communityThemePreference.test.mjs**
Covers contract validation, user/relay isolation, malformed records,
cache failures, and switch-race decisions.

**desktop/src/shared/theme/communityThemePreference.ts**
Defines the versioned stable preference contract, safe defaults, local
cache keys, and persistence guards.

**desktop/src/shared/theme/communityThemeSync.test.mjs**
Covers relay absence, unreadable records, unavailability, seeding
safety, and teardown of pending writes.

**desktop/src/shared/theme/communityThemeSync.ts**
Encrypts theme preferences to the user, publishes and retrieves NIP-78
state, and handles ordering and lifecycle safety.

</details>

### Reproduction steps

1. Join at least two communities and open **Settings → Appearance**.
2. Choose a different theme, accent, or system-following mode in each
community.
3. Switch between the communities and verify each one restores its own
scheme without overwriting the other.
4. Sign in on another desktop with the same Nostr identity, join the
same community, and verify its saved scheme is restored from that
community's relay.
5. Disconnect the relay, change Appearance, and verify the UI remains
responsive and the local fallback is retained.

### Screenshots / demos
<img width="1733" height="948" alt="image"
src="https://github.com/user-attachments/assets/5afeabaa-0def-482c-9b87-8a880ee0a467"
/>


<img width="700" height="412" alt="Screen Recording 2026-07-29 at 4 39
52 PM"
src="https://github.com/user-attachments/assets/d58da329-aec5-4324-a4b2-cbcb2702a81a"
/>

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-08-05 13:18:48 -07:00
6c40ce394f feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.

## Contract

Store the requested value (1–32) verbatim at every persistence and wire
boundary. Apply `effective = min(requested, harness_cap)` only at the
four enforcement points:

| Boundary | Implementation |
|---|---|
| Local spawn | `BUZZ_ACP_AGENTS` env var in child `Command` |
| Remote deploy | `launch.policy_env["BUZZ_ACP_AGENTS"]` + legacy
`parallelism` field |
| Restart badge | `SpawnConfigSnapshot.parallelism` stores effective
value; the diff surface displays what actually runs |
| UI copy | Amber hint when requested > cap; no `max` attribute, no
save-path clamp |

`BUZZ_ACP_AGENTS` is added to `RESERVED_ENV_KEYS` — the Desktop resolves
the effective value into `policy_env`; a user-supplied override in `env`
would bypass the cap and is silently stripped.

## Changes

**`managed_agents/parallelism.rs`** (new) — policy core:
- `OPENCLAW_MAX_PARALLELISM = 5`
- `harness_max_parallelism(command)` — keyed on
`normalize_command_identity` so path prefixes, `.exe` suffixes, and
other cosmetic differences are ignored
- `effective_parallelism(command, value)` — identity for uncapped
harnesses
- `acp_agents_value(command, parallelism)` — `env("BUZZ_ACP_AGENTS", …)`
helper

**`runtime.rs`** — spawn clamp: `BUZZ_ACP_AGENTS =
acp_agents_value(effective_command, record.parallelism)`

**`agents_deploy.rs`** — deploy egress clamp: `build_deploy_payload`
resolves `effective_parallelism` once from `descriptor.command`; both
`launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy top-level
`parallelism` field use that value — the two are always consistent
regardless of stale `record.agent_command` pins

**`spawn_snapshot.rs`** — `from_inputs` stores
`effective_parallelism(&descriptor.command, record.parallelism)` in the
`parallelism` field. Over-cap edits that don't change the pool (e.g. 10
→ 8, both clamp to 5 on OpenClaw) produce equal snapshots; cap crossings
(8 → 3) produce different snapshots.

**`AcpRuntimeCatalogEntry.max_parallelism: Option<u32>`** — derived from
the static definition command, not the probed `entry.command` (which may
be `null` for unavailable entries), so unavailable OpenClaw entries
still carry the cap. Propagated through all four catalog constructors
(builtin discovery, preset catalog construction, custom discovery,
custom-save response), IPC types
(`RawAcpRuntimeCatalogEntry.max_parallelism`), and the frontend catalog
type.

**UI** — `EditAgentAdvancedFields` and `PersonaAdvancedFields` show an
amber hint when `selectedRuntime.maxParallelism` is set and the current
value exceeds it. Cap and label come from the catalog entry — no
hardcoded 5 in TS. No `max` attribute on inputs; the input stays
`type="text"` with 1–32 copy.

**Docs** — `docs/remote-agents.md`: `BUZZ_ACP_AGENTS` moved from the
deliberately-non-reserved section to reserved; new contract documented.
`desktop/src/features/agents/AGENTS.md`: command-keyed execution policy
documented as the sanctioned second metadata source feeding the catalog
projection.

## Tests

**Rust** (`parallelism.rs`):
- `policy_table` — `harness_max_parallelism` and `effective_parallelism`
across all openclaw variants and uncapped harnesses
- `acp_agents_value_openclaw_above_cap_is_capped` — spawn-env seam
- `override_direction_*` — both override directions (openclaw runtime +
goose override; goose runtime + openclaw override)
- `summary_persona_inherited_*` — live persona wins over stale
`agent_command`
- `snapshot_export_carries_requested_definition_parallelism` — requested
value travels wire/sync unchanged

**Rust** (`spawn_snapshot/tests.rs`):
- `openclaw_above_cap_parallelism_snapshots_equal` — stored 10 vs 8,
both clamp to 5 → snapshots equal
- `openclaw_cap_crossing_parallelism_snapshots_differ` — 8 (clamps to 5)
vs 3 → snapshots differ

**Rust** (`discovery/presets.rs`):
- `openclaw_preset_unavailable_carries_max_parallelism` /
`openclaw_preset_available_carries_max_parallelism` — catalog metadata
present with `command: null` and with a resolved path

**Rust** (`agents_deploy.rs`):
- `launch_block_openclaw_over_cap_policy_env_is_capped` — direct
`launch.policy_env` seam
-
`deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped`
— stale `record.agent_command=goose`, live descriptor=openclaw: both
fields cap to 5
-
`deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped`
— stale `record.agent_command=openclaw`, live descriptor=goose: both
fields pass through requested
- `deploy_payload_json_explicit_openclaw_override_both_capped` —
explicit `agent_command_override=openclaw`: both fields cap to 5

**Rust** (`persona_events/stale_pin_tests.rs`):
- `apply_persona_snapshot_goose_to_custom_harness_drops_stale_goose_pin`
— custom-direction stale-pin drop (builtin pin → loaded custom harness
via `update_loaded_harness_registry`)

**TypeScript** (`agentParallelism.test.mjs`):
- `parallelismCapHint` — at/below cap (null), above cap (hint includes
label and cap value), singular form for cap=1, uncapped harness (null)

**TypeScript** (`tauri.test.mjs`):
- `fromRawAcpRuntimeCatalogEntry` round-trips `max_parallelism` →
`maxParallelism`; absent when `undefined`

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-05 16:09:14 -04:00
f2ce575b62 Fix media attachment actions (#4849)
## Summary

- Upload photos immediately while keeping videos queued for background
upload.
- Move image annotation and video spoiler actions to thumbnail hover
overlays.
- Preserve the image editor's existing Draw and Spoiler controls.

### Snapshots

#### Image annotation overlay

![Image annotation
overlay](https://raw.githubusercontent.com/block/buzz/87d7e1b1a0774ffef7a1a6cba03baffd63e11bd4/pr-4849--01-image-annotation-overlay.png)

#### Image editor controls

![Image editor
controls](https://raw.githubusercontent.com/block/buzz/87d7e1b1a0774ffef7a1a6cba03baffd63e11bd4/pr-4849--02-image-editor-controls.png)

## Testing

- `pnpm typecheck`
- `pnpm check`
- Focused attachment, drawing, and spoiler smoke tests
- Pre-push desktop tests (4,286 passing)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Honey <47c18026466e670a1618fd7de0ef32b9ff75d6e0b5ccf255d13c8c3d674ed115@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 12:17:48 -07:00
klopez4212andGitHub 2034e693a8 fix(desktop): remove join API token control (#4897)
## Summary

Remove the nonfunctional API-token option from the existing-community
join flow.

## Validation

- Focused Playwright join-flow coverage
- Add-community screenshot coverage

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-05 11:56:14 -07:00
014562c063 fix(desktop): allow shared agent mentions (#4913)
## Summary

- admit relay-discovered agents to autocomplete when their response
policy authorizes the viewer
- require authorization in the exact active stream/forum channel for
mentions, while keeping community-wide discovery for member invitation
- fail closed for relay-only agents in DMs and unresolved composer
contexts
- re-authorize cached autocomplete rows after policy/channel changes so
stale agent suggestions cannot leak back in
- preserve managed-agent behavior and explicitly reject stale
agent-marked channel members absent from both live directories

## Validation

- `pnpm --dir desktop test` — 4,288 passed
- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop check`
- `pnpm --dir desktop build:e2e`
- focused Playwright mention matrix — 12 passed
- focused Playwright member-invitation matrix — 2 passed
- pre-push hooks after rebase to current `origin/main` — desktop check
and 4,288 tests passed
- independent correctness/privacy re-review cleared with no remaining
blocker

## Related competing PRs

This supersedes or overlaps #2333, #3056, #4242, #4137, #2314, #4058,
and #2605. This version adds exact-channel authorization, fail-closed
DM/context handling, cached-row reauthorization, forum coverage,
outbound mention-tag coverage, explicit stale-member coverage, and
add-member discovery coverage.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 11:15:11 -07:00
efe1893dd3 fix(channels): restrict private-channel invitations (#4612)
This change requires an active owner or administrator for third-party
additions to private channels. The relay validator and transactional
database authority enforce the same rule, including removed-member
reactivation and role-change paths.

Idempotent self-target behavior remains available, while ordinary
members can no longer extend private-channel access to another identity.

## Testing

- `git diff --check
origin/main...codex/security-private-channel-invite-authority`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
2026-08-05 10:47:00 -07:00
25a9cf1be6 feat: paste composer text without formatting (#4801)
## Summary

- handle Cmd+Shift+V on macOS and Ctrl+Shift+V on Windows/Linux in the
message composer
- read plain text through the native Tauri/arboard clipboard path in
packaged builds, with a browser-only Clipboard API fallback
- re-enter ProseMirror's paste pipeline with populated `text/plain`
clipboard data so selection, undo, multiline behavior, and paste
observers remain intact
- cover both platform mappings with rendered composer E2E tests that
assert the native command path

## Testing

- `pnpm test` — 4,286 passed
- `pnpm check`
- `pnpm typecheck`
- `pnpm exec playwright test composer-selection-formatting.spec.ts
--project=smoke` — 26 passed
- `cargo check --manifest-path desktop/src-tauri/Cargo.toml --workspace
--all-targets --target aarch64-apple-darwin`
- `just desktop-tauri-test` — 2,206 core tests plus integration and
doc-test groups passed
- full pre-push hooks passed

## Manual verification

Physical packaged-app clipboard verification remains recommended on
macOS, Windows, and Linux. The automated E2E uses mocked Tauri IPC but
asserts the native `read_clipboard_text` command is invoked.

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-08-04 16:16:14 -07:00
a1d78f2959 feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues (#4695)
## Summary

Gives Buzz-hosted git entities the same "GitHub-style" chat experience
GitHub links already get: rich preview cards, real titles, and
click-through — except clicks navigate **in-app** to the Projects view
instead of a browser.

- **Spec**: `docs/buzz-entity-links.md` — link scheme, slices, and
deferred work (`buzz://project`, OS deep links, web routes).
- **Canonical `buzz://` deep links**: new
`desktop/src/shared/lib/entityLink.ts` with builders + strict parser for
`buzz://pr?id=…&owner=…&d=…`, `buzz://issue?…`, and
`buzz://repo?owner=…&d=…`, mirrored by a Rust module
(`crates/buzz-cli/src/links.rs`) with a shared golden-format test so the
two implementations can't drift.
- **Preview cards**: `linkPreview.ts` recognizes `buzz://` entity links
*and* HTTPS relay clone URLs (`{origin}/git/<pubkey>/<repo>`, the shape
agents paste today). Both normalize onto the canonical `buzz://` href,
so the two spellings of a repo dedupe to one `Buzz`-provider card
(`BuzzMark` logo) rendered by `link-preview-attachment.tsx`.
- **Title enrichment**: PR/issue cards fetch the real subject from the
relay event (`subject` tag or first content line) via
`useResolvedLinkPreviews.ts`; the cache is community-scoped and reset in
`resetCommunityState()`.
- **In-app navigation**: clicking a card or inline anchor (including
HTTPS relay clone URLs whose origin matches the active relay) routes to
the canonical `30617:<owner>:<d>` coordinate via `goProject()`
(`markdown/entityLinks.tsx`). **Merge dependency: #4671 must merge
first** — route resolution for `30617:` coordinates is implemented on
that branch (`feat/multi-repository-projects`). Entity-link and
external-anchor logic were extracted out of `markdown.tsx` to stay under
the file-size ratchet.
- **Agent side**: `buzz pr open`, `buzz issues create`, and `buzz repos
create` now return a ready-made `link` field (omitted when the relay
returns `accepted: false`), and `base_prompt.md` instructs agents to
paste it verbatim when announcing work.

## Test plan

- [x] Desktop unit tests: pass, including new `entityLink.test.mjs` and
`linkPreview.test.mjs` coverage (golden formats, malformed-link
rejection, clone-URL/`buzz://` dedupe, origin-gated anchor behavior,
label-must-win invariant, cache epoch)
- [x] Rust: `cargo test -p buzz-cli` golden-format test +
accepted/rejected link guard assertions, clippy + fmt clean
- [x] Biome + `tsc --noEmit` clean; pre-push hooks
(desktop-tauri-checks, rust-tests, desktop-test) pass
- [ ] Manual: paste a relay clone URL and a `buzz://pr` link in a
channel — verify one card each, real PR title, and in-app navigation to
the Projects view

Related: [#4671](https://github.com/block/buzz/pull/4671)

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:54:14 -04:00
e30db7028f feat(projects): support multiple repositories (#4671)
## Summary
- adopt the finalized NIP-MP project model so one project can enumerate
and switch between multiple NIP-34 repositories
- add project and repository navigation, activity summaries,
existing-repository attachment, and repository access-channel management
- preserve privacy-safe activation provenance for agent-authored
patches, pull requests, issues, and associated commits

## Test plan
- [x] Run desktop typecheck and unit tests
- [x] Run focused NIP-MP, repository access, and provenance tests
- [x] Run Rust formatting and desktop lint checks
- [x] Run the complete pre-push suite after merging current `main`
- [ ] Manually verify project creation, repository attachment,
switching, and access repair on staging
- [ ] Manually verify public-channel and private-agent origin labels on
newly created Git activity

Related: [#4695](https://github.com/block/buzz/pull/4695)

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-04 17:30:12 -04:00
65f7a10035 fix(desktop): wait for terminal frame before splash (#4781)
## Summary

- keep the first-open Buzz Term splash pending until the active PTY
delivers its first frame
- retrigger the splash effect when that readiness gate changes
- cover the real bootstrap path so startup latency cannot consume the
animation invisibly

## Verification

- Wes manually verified the first-open animation in the worktree
- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 4,195 passed
- `pnpm exec biome check src/features/terminal/TerminalBootstrap.tsx
src/features/terminal/TerminalSubstrate.tsx
src/features/terminal/TerminalBootstrap.test.mjs`
- pre-push hooks — branch skew, desktop check, and 4,195 desktop tests
passed

The repository-wide `pnpm --dir desktop check` still reports
pre-existing diagnostics in `personaCatalogRelay.test.mjs` and
`terminal.css`; the three changed files pass Biome directly.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 13:50:20 -07:00
8b8d86c5d2 fix(desktop): integer-align custom reaction emoji (#4779)
## Summary

- remove the fractional half-pixel translation from custom reaction
emoji
- preserve the existing 28px reaction pill, 14×14 glyph box, and
`object-fit: contain`
- add real-app Playwright coverage for integer centering and non-square
intrinsic dimensions

### Related issue

None found. Follow-up to the Buzz emoji-warp investigation.

### Testing

- `cd desktop && pnpm exec playwright test
tests/e2e/custom-emoji.spec.ts --project=smoke` (15 passed)
- `cd desktop && pnpm test` (4,171 passed)
- `cd desktop && pnpm lint` (passed; two pre-existing informational
`useTemplate` diagnostics)
- `cd desktop && pnpm typecheck` (passed)
- `cd desktop && pnpm exec biome check
src/features/messages/ui/MessageReactions.tsx
tests/e2e/custom-emoji.spec.ts` (passed)

Independent review also mutation-tested the regression coverage by
restoring the half-pixel transform and confirming the new test fails. No
after screenshot is included because the patch preserves dimensions and
fixes subpixel raster alignment; the real-app test asserts the mechanism
directly.

Validated at `bc95969b21b58d83b7f94de4ad25e499e52b35fb`.

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
2026-08-04 20:38:19 +00:00
ce3cf3cd25 Polish Huddle voice controls (#4694)
## Summary

- add a visible Stop control for interrupting agent speech
- make push-to-talk available by default while preserving manual mute
controls
- refine agent management, muted audio states, drawer layering, and
return navigation
- suppress duplicate notification sounds for Huddle messages

## Why

Huddles could trap users behind long agent speech, hide useful agent
controls, and leave temporary Huddle state visible after the call. The
drawer also regressed when the terminal substrate began painting behind
the rounded app surface.

## Validation

- `just desktop-ci`
- focused Huddle Playwright coverage for the drawer, speech
interruption, agent picker, and leave navigation

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-04 13:07:50 -07:00
5179726737 fix(local-archive): default both archive settings to enabled (#4750)
## Overview

Both local archive settings — "Archive my agents' observer frames" (kind
24200) and "Archive my agents' turn metrics" (kind 44200) — previously
defaulted to OFF in OSS builds, controlled by build-time env vars. This
had an irreversible cost: observer frames are ephemeral (not stored by
the relay), so any missed events are permanently unrecoverable. This PR
makes both settings default to enabled for all builds and removes the
build-time flag machinery entirely.

## What changed

### Rust

- `observer_archive_default_enabled()` — returns `true` unconditionally;
removed `option_env!("BUZZ_DESKTOP_BUILD_OBSERVER_ARCHIVE_DEFAULT")`
check and `nest_is_dev()` runtime fallback.
- `agent_metric_archive_default_enabled()` — returns `true`
unconditionally; removed
`option_env!("BUZZ_DESKTOP_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT")` check
and its OSS-build test.
- `build.rs` — removed both `rerun-if-env-changed` declarations
(`BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT`,
`BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT`) and the two baked-env
emitting blocks.

### Build / CI

- `Justfile` — removed `desktop-tauri-test-compiled-flags` recipe (the
dual-compile test machinery).
- `.github/workflows/ci.yml` — removed the "Desktop Tauri compiled-flag
verification" CI step.

### TypeScript

- `useObserverArchiveSeed.ts` — removed `observerArchiveDefaultEnabled`
dep from `ObserverArchiveSeedDeps` and the `policyOn` gate in
`reconcileObserverArchive`; the function now unconditionally calls
`mergeSaveSubscriptionKinds`.
- `useAgentMetricArchiveSeed.ts` — removed
`agentMetricArchiveDefaultEnabled` dep from `AgentMetricArchiveSeedDeps`
and the `defaultOn` flag-check path in `maybeSeed`; the
`hasExplicitChoice` guard is preserved as the sole gate against
re-seeding.
- `LocalArchiveSettingsCard.tsx` — removed `policy` prop,
`observerPolicy` state, and `observerArchiveDefaultEnabled` fetch from
`ObserverArchiveSection`; toggle is now always enabled (just `toggling`
disables it); removed the stale "Always on for internal builds" copy
branch; removed the `observerPolicy !== false` guard from
`handleObserverToggle`.
- `tauriArchive.ts` — updated JSDoc on both default-enabled functions to
reflect always-true.
- `e2eBridge.ts` — changed both mock defaults from `?? false` to `??
true` so E2E tests without an explicit mock override exercise the real
default behavior.

### Tests

- `useObserverArchiveSeed.test.mjs` — replaced `policyOn` dep with
direct merge dep; updated `test_oss_policy_off_no_merge` →
`test_reconcile_always_seeds_24200`; all cancellation, identity-switch,
and ordering tests adapted.
- `useAgentMetricArchiveSeed.test.mjs` — removed `defaultOn` dep and
`test_oss_build_does_not_seed`; updated
`test_internal_build_unset_seeds_*` → `test_default_enabled_*`;
`hasExplicitChoice` guard tests unchanged.

## Preservation of explicit opt-outs

Users who have previously toggled the setting off are unaffected:

- `useAgentMetricArchiveSeed` skips seeding when
`hasExplicitChoice(pubkey)` returns true (localStorage-persisted per
identity).
- Observer archive reconciliation now unconditionally calls
`mergeSaveSubscriptionKinds`, but a user who already deleted the
subscription can turn it off via the Settings toggle, which calls
`removeSaveSubscriptionKind` — this is the existing explicit opt-out
path, and the toggle is now always enabled (not locked by a policy
flag).

## Result

- No `BUZZ_BUILD_*_ARCHIVE_DEFAULT` /
`BUZZ_DESKTOP_BUILD_*_ARCHIVE_DEFAULT` references remain outside
CHANGELOG/history.
- Desktop node tests: 4168 pass, 0 fail.
- `just desktop-tauri-check`: clean.
- `just desktop-tauri-test`: all pass.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 16:02:34 -04:00
e5efd04705 fix(desktop): close reconnect gaps that previously required CMD+R (#4737)
> Opened by Brain (agent) on behalf of @wesbillman.

## Problem

Users report the desktop app doesn't reliably reconnect and can wedge in
states where only CMD+R (or a full restart) restores connectivity
(thread `c2205e2b` in #desktop-reconnecting).

Pinky's empirical light-switch matrix (real `buzz-relay`, SIGTERM/1012 +
SIGKILL × 1s/45s/3min, at `f18a9cb10`) passed 4/4 — the backoff state
machine recovers cleanly from ordinary relay loss. That isolates the
user-stuck states to four special cases a reload resets but the auto
flow never did.

## Fixes

| Gap | Change |
|---|---|
| **G1** — recovery rode solely on the backoff timer (max 30s),
throttled by WKWebView in occluded/background windows; nothing fired on
network return or wake | New `useRelayResumeTriggers`: `online`, window
focus, and visibility→visible call `preconnect()` when the session is
`reconnecting`/`stalled`, rate-limited to one attempt per 5s
(`relayResumeTriggerPolicy.ts`). Deliberately inert for the terminal
`disconnected` state. |
| **G2** — any AUTH `OK false` latched the session terminal forever,
though the relay also rejects for transient causes (duplicate-AUTH
"already authenticated" race, ±60s clock skew, fail-closed allowlist DB
errors) | New `AuthOkTracker` (`relayAuthPolicy.ts`): "already
authenticated" resolves as success; transient rejections retry with
normal backoff; latch only on `restricted:` or after 3 consecutive
rejections. |
| **G3** — an `auth-required:` CLOSED (REQ racing AUTH after reconnect)
permanently deleted the live subscription with no UI signal — frozen
channel while state reads "connected" | Reclassified `auth-required:` as
retryable in `relayClosedPolicy.ts`. Genuinely terminal classes
(`restricted:`, `invalid:`, …) still delete. Can't loop: a truly
unauthenticated session latches terminal at the connection level. |
| **G4** — `useRelayAutoHeal` observed the 2s-debounced connection hook,
so sub-2s flaps never triggered the heal even though `resetConnection`
had already rejected every in-flight query | Auto-heal now observes the
raw connection-state emitter. The existing 15s heal rate-limit still
guards against flap storms. |

Each fix is a colocated pure-policy module + unit tests, matching the
existing `relayReconnectPolicy`/`relayClosedPolicy` pattern.

## Validation

- Full desktop unit suite: **4151 pass, 0 fail** (at branch tip, `pnpm
-C desktop test`)
- `pnpm -C desktop typecheck` and `pnpm -C desktop check` clean
(file-size ratchet respected — `relayClientSession.ts` net −2 lines
despite the tracker wiring)
- Evidence trail: `RESEARCH/DESKTOP_RECONNECT_CMDR_GAP_AUDIT.md`
(audit), `RESEARCH/DESKTOP_RECONNECT_LIGHT_SWITCH_RESULTS.md` (Pinky's
matrix)

## Not covered / follow-ups

- Native macOS sleep-wake was not automated (would kill the harness
session); G1's focus trigger is the mechanism that covers wake in
practice, but a manual sleep-wake verification on a real build is
worthwhile.
- G3 terminal-CLOSED classes (`restricted:` etc.) still silently delete
subs with no UI signal — surfacing that is a separate UX decision.
- Stall-watchdog latency (60s idle + 10s check) left unchanged; G1
triggers largely mask it.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
2026-08-04 18:33:42 +00:00
cb4a73e17d Dock Buzz Term within channel workspace (#4724)
## Summary

- replace Buzz Term's full-app takeover with a resizable bottom dock
inside the channel content surface
- add a discoverable channel-header button plus hide and
maximize/restore controls
- create PTYs lazily and keep separate, persistent terminal workspaces
per channel
- capture immutable channel/thread context on every terminal session

## Multiple-channel behavior

The dock is a single surface, but its tabs are partitioned by channel.
Switching channels swaps to that channel's sessions without terminating
background PTYs; returning restores them. New tabs capture the currently
visible channel/thread context.

## Verification

At commit `7ca087f8e08c80528387684364a65bf4ccd6315f`:

- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 4,129 passed
- pre-push repository hooks — desktop check/test, Tauri checks, terminal
Rust suites all passed

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 11:10:47 -07:00
klopez4212andGitHub e1287c92cc Refine community invite links (#4734)
## Summary

- Separate direct invites from link sharing with a labeled divider.
- Show the generated invite URL inline with truncation and a copy
control.
- Use shared loading feedback and a restrained copy-status resize.

## Validation

- `pnpm -C desktop exec playwright test
tests/e2e/invite-link-copy.spec.ts
tests/e2e/invites-settings-screenshots.spec.ts` (7 passed)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 10:39:09 -07:00