mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
cb8118e9cee2b48bfa9fdeb4fe76ef80bac082ae
2184
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb8118e9ce |
fix(composer): partition thread agent typing by activity
Keep first-time thread-only agent typers in the combined typing group until channel-scoped observer activity provides a real session to preview. Preserve the truthful typing label after promotion and cover the full group-to-pill transition. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
bd92fcc049 |
test(desktop): await thread scroll anchor
Wait until the focus-thread body has a real middle-scroll range and a visible non-root anchor before switching layouts. This removes the suite-order race that left the captured anchor outside the split viewport. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
f68693654b |
test(composer): seed activity pills from observer turns and drop animation sleeps
The composer pill strip only renders observer-backed agents — typing-only agents fold into the combined typing indicator group by design — but the activity-indicator and no-shift e2e tests still summoned the pill via mock typing events, so the pill never mounted and both tests failed at the first toBeVisible. - Rewrite 'shows and clears activity indicators' on __BUZZ_E2E_SEED_ACTIVE_TURNS__: assert the typing-only fold-in first, then the observer-turn promotion (typing indicator clears as the pill mounts), the channel-scoped aux panel with the live transcript, turn_completed clearing the pill, and a fresh turn re-mounting it. - Rewrite 'composer does not shift' on observer-turn seeds and extend it: the fixed-row contract now covers pill mount, pill clear, AND the combined typing group mounting — all within ±0.5px of the idle composer top, which would catch the reported 2px drift. - Make the bar-hover freeze test deterministic: the strip sizes to its content, so 'the bar's empty right side' could land on the trailing pill and open a hover card. Park the cursor in the inter-pill gap instead and wait for the hold's width-pin style before seeding the reorder. - Replace both 1200ms animation sleeps (0.9s spring + 180ms grace left only ~120ms margin on CI) with state-based assertions: the pill label committing the seeded message text proves the store update reached the strip before the frozen-order assertions run. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
06b60e682d |
fix(mobile): merge relay recounts with locally seen thread replies (#4633)
## Summary - Keep mobile thread reply badges current by merging relay recounts with replies observed locally. - Retain replies in the local channel store while continuing to filter them from the main timeline. - Match the thread summary behavior already used on desktop, including the reply count, latest reply time, and participant avatars. ## Why On mobile, the "N replies" badge under a channel message can stall at a stale count or remain missing after a reply arrives. This makes the badge unreliable and can cause people to miss replies. The badge has two inputs: best-effort recounts from the relay and replies the client sees arrive. Mobile previously let any positive relay recount override the local view, while also discarding replies from its local message store. A delayed or lost recount, or a reply received after the recount, could therefore leave the badge behind. This change combines both inputs by using the higher reply count, the later last-reply time, and a merged participant list. Relay timestamps have one-second precision, so equal timestamps do not prove that a recount included a locally observed reply. Comparing counts preserves that reply instead of trusting recency alone. Desktop already uses this merge behavior. ## Validation At commit `4e3356636f5ad62e8f07910af305c532186c6c08` with a clean worktree: - `flutter test` for mobile: 1105 passed, 1 skipped - `flutter analyze` for mobile: no issues found - Reverting the merge so a positive relay recount shadows local replies fails 4 of the new tests, including the same-second and reply-after-recount cases. Restoring the store-level reply drop fails both new provider tests. Added tests: - [`timeline_message_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/features/channels/timeline_message_test.dart), covering relay-only recounts, a reply newer than the recount, a reply in the same second as the recount, a lost recount, a zero recount, nested replies at the root and at the reply they answer, a deleted reply, and participant merging and capping. - [`channel_messages_provider_test.dart`](https://github.com/block/buzz/tree/main/mobile/test/features/channels/channel_messages_provider_test.dart), covering a live reply reaching the store while staying out of the main timeline, and a reply newer than the relay recount raising the badge. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: npub12uu53ml9upy7ww9apmtv6vm0u8xlcldx7znsjvwgsr7uvy5g0kssw943ca <573948efe5e049e738bd0ed6cd336fe1cdfc7da6f0a70931c880fdc612887da1@buzz.block.builderlab.xyz> |
||
|
|
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. | | --- | --- | |  |  | ### 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> |
||
|
|
e14fff74d0 |
relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
## Problem On SIGTERM the relay sends every live WebSocket a **1012 Service Restart** close frame via `ConnectionManager::drain_all()` — all in the same instant (`main.rs` shutdown task → `state.rs::drain_all`). On a pod holding thousands of sessions, that makes every client reconnect simultaneously: the thundering-herd reconnect behind the DB pool-timeout bursts observed on each rolling deploy. Client-side jitter can't fix this — the desktop client *resets* its backoff to base on a 1012 and reconnects with only ±25% jitter (`relayClientSession.ts`), so the spread has to come from the server. ## Change Add `BUZZ_DRAIN_JITTER_MS` (default `0` = unchanged behavior). The two paths are kept **deliberately separate** so the default is byte-for-byte the previously shipped shutdown: - **Jitter off (`0`/unset, the default):** the original synchronous, all-at-once `drain_all()` runs unchanged — queue the 1012 on each connection's control channel, cancel, return. No new machinery on the default path. - **Jitter on (`> 0`):** a separate async `drain_all_jittered(jitter_ms)` spreads each connection's restart close over an independent uniform delay in **`[1, jitter_ms]`**. Each delayed close travels a dedicated `RestartClose` channel; the writer flushes the 1012 frame and **acknowledges the flush over a oneshot**, so drain waits for confirmed delivery (up to `RESTART_CLOSE_ACK_TIMEOUT` = 5s) rather than assuming it, falling back to cancellation if the channel is full/closed or the ack times out. The drain future is **owned and awaited** by the shutdown task, and the 30s hard-drain backstop is aborted only after a clean drain — so a clean roll exits `0`. The two methods can be unified and the old one dropped later once the jittered path is proven for all cases. - **`config.rs`** — `drain_jitter_ms`: non-negative parse, clamped to `MAX_DRAIN_JITTER_MS` = **20s** (leaving 10s of the 30s budget for flush). Junk fails loudly at startup; **empty/whitespace-only is treated as unset (jitter off)** so a `BUZZ_DRAIN_JITTER_MS=""` kill switch does not crashloop the relay (matches the sibling env vars in this file). - **`state.rs`** — `drain_all()` (unchanged synchronous default) + `drain_all_jittered()` (jittered + flush-ack). Both set the sticky `draining` flag before the first await. A registration that lands mid-shutdown always self-signals via the **immediate** control-frame + cancel path — jitter smears already-established sockets, not late arrivals. - **`main.rs`** — shutdown task dispatches: `drain_jitter_ms == 0` → `drain_all()`, else `drain_all_jittered(...).await`. ## Safety - **Default off is the currently-committed path.** With jitter unset/0 the shutdown runs the original synchronous `drain_all()` — no restart channel, no ack wait. Safe to deploy dark and dial up. - **Shutdown-boundary race preserved.** Sticky flag set before any await; a late registration self-signals its close with no jitter. - **Owned + backstopped.** The jittered drain future is awaited; the 30s hard-drain `process::exit(1)` remains the ceiling. `MAX_DRAIN_JITTER_MS` (20s) + `RESTART_CLOSE_ACK_TIMEOUT` (5s) = 25s, inside the 30s budget; 5s pre-sleep + 25s = 30s against `terminationGracePeriodSeconds: 60`. ## Known behavior to note (not a blocker, flagged from review) On a **successful** flush the jittered path deliberately does not cancel the connection token — teardown then depends on the client echoing our Close, or on process exit. Compliant clients echo; a silent client rides to the 30s hard exit. The default (jitter-off) path cancels deterministically as before. ## Tests - `config::tests::drain_jitter_defaults_off_and_rejects_junk` — default off, `20000`, clamp `60000`→`20000`, explicit `0`, junk `"soon"` fails, **empty `""` and whitespace-only treated as off**. - `state::tests::drain_all_is_immediate` — default path queues frame + cancels synchronously. - `state::tests::drain_all_sends_restart_close_and_cancels_every_conn`, `drain_all_full_control_buffer_still_cancels`, `register_after_drain_self_signals_restart_close_and_cancel`. - `state::tests::drain_all_jittered_defers_close_until_within_jitter_window` (paused time). - `state::tests::drain_all_jittered_waits_for_writer_acknowledgement_without_cancelling`. - `state::tests::drain_all_jittered_cancels_when_restart_channel_is_full_or_closed`. - `state::tests::drain_all_jittered_cancels_when_flush_ack_times_out` (paused time — the 5s ack-timeout fallback). Validation at `46c690940`: `cargo fmt -p buzz-relay --check`, `cargo clippy -p buzz-relay --all-targets -- -D warnings`, and the drain/config unit suite all clean. Local live SIGTERM test with a real relay process + 200 NIP-42-authenticated sockets — see the PR comment for the before/after distribution and exit codes. ## Rollout Ship with default `0`, then set `BUZZ_DRAIN_JITTER_MS` (e.g. 10000–20000) on bb-block first, watch the roll-window pool-timeout metric, then bb-public. `""` is a safe kill switch. Complements the preStop `sleep` (stops routing before close). --------- Signed-off-by: npub1srl70fhzyu3fsnahl06vw2czvqc2w3ds37hyzvjnk8ve8f03ngcqg9le2w <80ffe7a6e22722984fb7fbf4c72b026030a745b08fae413253b1d993a5f19a30@buzz.block.builderlab.xyz> Signed-off-by: npub128x7j3pwgm4vs8yra3c42fcgcwcvh94g3luwzkqa376du2q6l0esqcrwch <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz> Signed-off-by: Brad Seiler <seiler@squareup.com> Co-authored-by: npub1srl70fhzyu3fsnahl06vw2czvqc2w3ds37hyzvjnk8ve8f03ngcqg9le2w <80ffe7a6e22722984fb7fbf4c72b026030a745b08fae413253b1d993a5f19a30@buzz.block.builderlab.xyz> Co-authored-by: npub128x7j3pwgm4vs8yra3c42fcgcwcvh94g3luwzkqa376du2q6l0esqcrwch <51cde9442e46eac81c83ec71552708c3b0cb96a88ff8e1581d8fb4de281afbf3@buzz.block.builderlab.xyz> |
||
|
|
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**  --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
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  ### Related issue None found. Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
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 | | --- | --- | |  |  | Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ed4b3e7afa |
fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
Provider `context_length_exceeded` 400s permanently wedged agent sessions: the turn errored, the oversized history persisted in the in-memory session, and the usage baseline stayed frozen at the last successful sub-threshold reading (failed requests report no usage), so the preflight handoff gate never fired again — every later prompt failed identically until an agent restart. The byte-truncation fallback never intervened because it is a request-body limiter (`estimated_bytes`), not a context-window defence; at context-window scale it is a measured no-op. This adds the reactive recovery path: - **Typed classification.** `AgentError::LlmContextExceeded` is classified at both non-success provider terminals — the shared `post()` (Anthropic, OpenAI, Databricks) and `openrouter_post()` — on `status == 400` plus a context-window body match, so ordinary 400s stay terminal. - **Forced handoff.** A context-400 forces a summarize-handoff that bypasses `should_handoff()` and `BUZZ_AGENT_MAX_HANDOFFS`, bounded by its own per-turn budget (`MAX_CONTEXT_RECOVERIES_PER_RUN = 3`). - **Shrink ladder.** The summarize prompt budget halves from the observed rejected history size — not from `max_context_tokens`, the number the provider just contradicted — rung to rung, with a 4096-byte floor. A summarize call rejected for the same reason takes the next rung instead of re-sticking. At the floor (overflow dominated by unshrinkable frame: system prompt, tool schemas, live prompt) recovery is refused and the provider error surfaces clearly instead of self-healing. - **Baseline reset.** The stale usage baseline is cleared when a request fails, so the preflight gate cannot stay frozen sub-threshold on retries. Named behavior changes: 1. **Anthropic and OpenRouter errors now carry the `(model)` stamp.** Provider arms return their `Result` into the central error mapper instead of early-returning past it, making the code match its documented single-convergence contract at that mapper. 2. **`max_rounds` now counts completions the loop acts on.** A request rejected with a context-400 that is then successfully recovered refunds its round before the retry, paired 1:1 with a consumed recovery rung, so the round cap is neither weakened nor able to drop a recovered turn unanswered. Related: #4805 — the complementary proactive fix (per-session handoff-cap kill switch that let sessions grow to the provider wall). #4805 prevents reaching the wall; this PR recovers at it. --------- Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
ccdaa16161 |
docs(persona-pack): fix stale desktop import instructions (#4500)
## Summary The desktop app no longer imports persona packs the way the docs described. `PERSONA_PACK_SPEC.md` and the `meadow-core` example still pointed users at a `.zip` import through "My Teams / My Agents → Import" and a future "Install Pack" button — none of that exists anymore. The app only imports agent/team **snapshots** (`.agent.json`/`.agent.png`, `.team.json`/`.team.png`), and a persona-pack `.zip` is explicitly rejected. ## Fix Updated both docs to describe the current import paths (Agents / Agent teams sections, snapshot files only) and added a note that persona packs and desktop snapshots are separate, non-interchangeable formats today. Fixes #4468 --------- Signed-off-by: SomSamantray <92726151+SomSamantray@users.noreply.github.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> |
||
|
|
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> |
||
|
|
05150c1188 |
feat(mobile): sync themes per community (#3767)
**Category:** new-feature **User Impact:** Mobile now keeps each community’s appearance in sync with desktop, including theme, accent, and system-mode preference. **Problem:** Appearance choices were device-local, so the same account could look different between desktop and mobile. Live sync could also stop after the relay closed a subscription. **Solution:** Store each community’s encrypted appearance preference on its relay using the shared desktop wire contract, restore it from a local identity-scoped cache, and apply replacement events live. Closed subscriptions now recover with guarded backoff and fetch the latest preference so no update is lost during the gap. <details> <summary>File changes</summary> **mobile/lib/app.dart** Connects community appearance state to the authenticated app lifecycle. **mobile/lib/features/settings/accent_picker_page.dart** Aligns mobile accent choices and selection behavior with the shared catalog. **mobile/lib/features/settings/settings_page/appearance_section.dart** Clarifies the active appearance and hides accent controls when the Buzz theme owns its neutral accent. **mobile/lib/features/settings/theme_picker_page.dart** Persists catalog theme choices through the community-scoped provider. **mobile/lib/shared/theme/accent_colors.dart** Matches desktop’s accent catalog and wire values. **mobile/lib/shared/theme/buzz_theme.dart** Keeps Buzz visually neutral without discarding the user’s stored accent for other themes. **mobile/lib/shared/theme/community_theme_preference.dart** Defines and validates the versioned desktop-compatible appearance payload. **mobile/lib/shared/theme/community_theme_provider.dart** Coordinates cache-first appearance loading with account and community changes. **mobile/lib/shared/theme/community_theme_sync.dart** Adds encrypted NIP-78 relay persistence, live replacement handling, deterministic ordering, safe seeding, and resilient subscription recovery. **mobile/lib/shared/theme/theme.dart** Exports the community appearance modules. **mobile/test/features/settings/theme_picker_page_test.dart** Covers the updated settings behavior. **mobile/test/shared/crypto/nip44_interop_test.dart** Proves Dart decrypts a desktop-produced nostr-rs NIP-44 v2 preference. **mobile/test/shared/theme/buzz_theme_test.dart** Covers Buzz’s neutral rendering and stored-accent restoration. **mobile/test/shared/theme/community_theme_preference_test.dart** Covers wire parsing, validation, migration, and future-version handling. **mobile/test/shared/theme/community_theme_sync_test.dart** Covers cache/relay lifecycle, replacement ordering, switching races, absence-only seeding, and closed-subscription recovery. </details> ## Reproduction steps 1. Sign into desktop and mobile with the same account and join the same community relay. 2. On desktop, choose a distinctive non-Buzz theme and accent; mobile should update without a local toggle. 3. Restart mobile and confirm it restores the same appearance. 4. Change the mobile theme and accent and confirm desktop follows. 5. Leave mobile idle or backgrounded through a relay reconnect, then change desktop again; mobile should resubscribe and catch up automatically. 6. Switch communities and confirm each community restores only its own appearance. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
6df7eba24d |
fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
`BUZZ_AGENT_MAX_HANDOFFS` compared against the session-cumulative `handoff_count` (persisted across prompts). After N handoffs a long-lived session hit the cap permanently: `maybe_handoff()` returned `Skipped` on every subsequent prompt, the 16 MiB byte-truncation fallback never bound before a 1M-token provider wall, and the session wedged on the first 400 with no recovery path. Thufir's session log shows 8 days of cap-forced truncation before the first `context_length_exceeded` 400. The fix replaces the session-level cap comparison with a local `handoff_attempts` counter constructed at the start of `run()` and passed into `maybe_handoff()`. The counter resets on every `session/prompt` turn so `BUZZ_AGENT_MAX_HANDOFFS` caps compaction loops within a single turn while allowing unbounded compactions across a session's lifetime. The session-cumulative `handoff_count` is retained for log context only and is not reset. Steer-driven rounds share the per-turn budget automatically since steers inject into the running `run()` loop, not a new call. - Move `handoff_attempts` increment to before `summarize()` so failed, empty, and cancelled summarize calls each consume one budget slot — the cap cannot be bypassed by a repeatedly-failing summarizer - Upgrade cap-forced `Skipped` from `INFO` to `WARN`; add structured fields for `session_id`, attempt count, projected tokens, and threshold so the cap→wall pairing is attributable per session - Document `max_handoffs` in `config.rs` as a per-`session/prompt`-turn bound - Three new behavioral regression tests: per-turn reset proven across two separate turns; within-turn cap proven via multi-round tool-call turn; failed summarize proven to burn the attempt budget Note: this is the proactive half of the context-window fix. The reactive `context_length_exceeded` 400 recovery path is owned by Sami's branch (`buzz-ctxfix-sami`, Tyler's crew); this PR is intended to land after that one. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>mobile-v0.8.0-rc.3 |
||
|
|
0c6842931b |
Fix mobile message timeline bounce (#4862)
## Summary Stop repeated follow-latest scrolling after layout changes in channels and DMs. ## Validation - `flutter analyze lib/features/channels/channel_detail_page.dart` - `flutter test test/features/channels/channel_detail_page_test.dart` - Full mobile pre-push suite --------- 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> |
||
|
|
27b51144f3 |
Polish mobile bottom sheets and profile cards (#4911)
## Summary - standardize mobile sheets with shared spacing, close controls, action tiles, haptics, and motion - add uniform native concentric corners on iOS 26+ while preserving the Android sheet shape - refresh profile actions/status and normalize membership and huddle timeline spacing ## Validation - `just mobile-check` - `just mobile-test` (1,165 tests) - signed iPhone Release build and device install - Android debug build and Pixel 10 install ## Snapshots ### Channel actions  ### Profile card  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
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 editor controls  ## 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> |
||
|
|
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> |
||
|
|
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> |