Two skeleton flashes made switching feel clunky:
- Every switch: the deferred timeline snapshot lags the live one for a
frame or two, and that gap painted the full skeleton to mask the
previous channel's rows — a visible skeleton flash on every switch.
The gap is a render-pipeline artifact, not a loading state: it now
renders plain background (new "blank" body surface). Blank blocks the
same behaviors as the skeleton (autoscroll, jumps, unread pill,
scrollback), only the visual differs. Cold loads keep the skeleton.
- Stale revisits: the loading latch reset on every switch, so re-entering
a channel past the messages staleTime refetched on mount with the
unsettled skeleton branch active — a skeleton over a fully cached
timeline for the whole relay round-trip. Channels that settled once
this session now render their cached rows stale-while-revalidate
(session registry, reset via resetCommunityState like every other
community-scoped singleton).
The revisit spec forces staleness by shifting Date.now past the 5-minute
window and asserts the skeleton never mounts over cached rows; the
switch-gap behavior is covered by the timelineSnapshot unit suite.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Entering a channel whose thread-summary facepiles hydrate after first
paint (profiles resolving late) visibly shifted the timeline: every row
above a summary slid ~3px per summary a beat after content painted.
The summary button was inline-flex, so it participated in its wrapper's
line box via its baseline — and that baseline moves when the avatar's
content resolves (Radix fallback text appearing after its delay, or the
avatar image landing). Make the button a block-level flex row so the
wrapper height is padding + button height regardless of avatar state,
and render the facepile fallback immediately (fallbackDelayMs=0): the
participant starts with no image URL, so the default 200ms delay only
left a blank hole where the initials belonged.
Regression spec pins the wrapper height from first paint through
hydration and the root row's position after the facepile renders, with
the users-batch response deliberately delayed past first paint.
Signed-off-by: Max Lampert <maxwell@squareup.com>
The render-pending marker was moved onto MessageTimeline's always-mounted
wrapper for the switch tracer, which is gone. Tested whether the surviving
benchmark still needs it: reverted the file to main, rebuilt, re-ran all
three scenarios. Medians are unchanged within run-to-run noise —
general->deep-history 395.3 -> 394.3 baseline, 515.4 -> 520.4 at 10k
deep-history->general 173.3 -> 179.3 baseline, 314.9 -> 321.4 at 10k
general->projects 142.1 -> 141.7 baseline, 164.4 -> 169.8 at 10k
projects->general 171.0 -> 179.6 baseline, 282.8 -> 290.5 at 10k
— and membership scaling stays monotonic in every direction. The move was
not load-bearing.
Reverting it removes this PR's last write to the document-wide
data-render-pending namespace that review flagged as the one surviving piece
of the tracer's shape, and it removes an untestable risk: release-smoke.spec.ts
asserts that marker against a local relay, and those assertions cannot run
here (no relay), so a marker change in that file would have shipped unverified.
The PR no longer touches MessageTimeline at all.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind review found the guard-refusal branch untested, and a refutation pass
disagreed. The refutation tested the wrong mutation: DELETING the
`allow()` call fails three ordering tests, so it looked covered. Consulting
the guard and discarding its answer passed all ten.
That is the branch that stops a channel switch while a thread edit is
unsaved, so a regression there would have shipped silently. Two tests now
pin it — refused, and refused-while-forced — and the ignore-the-answer
mutation fails both.
Also removes residue the same review found in the tracer teardown:
- a stranded `append_switch_perf_log` arm in the e2e mock bridge, for a
Tauri command deleted two commits ago
- MessageTimeline's marker comment, which justified its placement by a
tracer that no longer exists; its readers are now the perf benchmark and
the e2e readiness specs
- ChannelScreenLoadingFallback's `data-render-pending` wrapper, added
solely for the tracer. Measured benchmark switches are warm, so the lazy
pane never suspends there and nothing asserts the marker. Reverted to
main rather than left as an unowned production DOM node.
Signed-off-by: Max Lampert <maxwell@squareup.com>
The instrument was over half this PR and the source of nearly every review
finding, and the benchmark that actually validates the perf work does not
depend on it: member-heavy-switch.perf.ts times click -> ready itself with
performance.now() and DOM readiness, and never reads a tracer export.
Its remaining value was a dev console line, against a failure mode of
reporting a number that never happened — which is worse than reporting
nothing, because it sends someone chasing a regression that does not exist.
The live baselines it produced are already recorded in the PR description
and stand on their own; they justified the fixes, and the fixes are the
deliverable.
Removed: channelSwitchPerf.ts and its tests, useChannelSwitchTraceMarks,
switch-settle-after-paint.spec.ts, the fetch attribution in the window
queryFn, the trace hooks in commitGuardedNavigation/useAppNavigation, and
the community-init reset.
Kept, because the perf fixes build on them:
- member-heavy-switch.perf.ts plus the inflateChannelMembers bridge knob —
the high-membership benchmark
- useChannelTimelineLoading — the per-channel loading latch
- commitGuardedNavigation — the shared guard/no-op commit flow
- the Projects hydration marker the harness gates readiness on
- routing Pulse note actions through goChannel, so that DM entry goes
through the same navigation guard as every other one
Signed-off-by: Max Lampert <maxwell@squareup.com>
The instrument was accumulating defects faster than it was answering
questions. Nearly every review finding lived in a seam that existed only
because the tracer inferred more than it could observe: a starvation
heartbeat, a per-seam guard matrix, DOM-attribute sniffing with a commit
generation, click anchors threaded by hand through nine async call sites,
and an on-disk JSONL sink with rotation and byte caps. Each fix added a
seam, and the next review found the guard that seam was missing.
The perf work these numbers support matters more than the numbers, so the
instrument is now scoped to what it can measure without inference:
- click (or navigation) -> route commit -> settled paint, as a console line
plus User Timing marks/measures. That is what the Performance panel and
the perf harness read.
- the message-window fetch when it starts inside that interval.
- one drop rule per unmeasurable condition (hidden window, timeout,
superseded, left the surface), each printing why.
Removed: the Rust perf-log sink and its git-revision baking, the click-anchor
threading and every call site that existed only to carry it, the starvation
heartbeat, the roster-fetch attribution and its sequence machinery, and the
commit-generation burst gate.
Scope bounds are now stated in the module doc rather than inferred: callers
that await before navigating are measured from the navigation, a bounded
frame wait replaces the 5s render deadline, and a sample past that bound is
flagged rather than dropped.
Net: 2231 lines removed, 462 added.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Cycle-1 adversarial review findings.
- The starvation heartbeat rescheduled forever for a trace that was never
settled, dropped, or replaced. It hung the unit suite (a test stubs rAF as
setTimeout, so the timer kept node's loop alive) and leaked a frame callback
per frame in production. Bounded to the trace's own liveness budget.
- `routeStillOnChannel` read `location.pathname`, but the app uses hash
history, so it answered false for every real channel route and degraded
route-exit handling to an unconditional abandon.
- Supersession and community reset discarded traces with no record, censoring
the impatient-second-click case — precisely the slow switches worth seeing.
`community-reset` was a declared reason that nothing emitted.
- Drop accounting had no test coverage at all; deleting it left the suite
green. Covered now, verified by reverting each guard.
- `settleChannelSwitchTrace` was not idempotent: a second call re-stamped the
fetch-attribution bound and spawned a duplicate wait chain.
- `anchorDiscarded` reached the console but never the record it documents.
- NewMessageScreen back-dated the anchor across the message publish, so send
latency was reported as switch latency.
- The canceled-fetch test restated the queryFn's reconcile-then-attribute
ordering instead of exercising it; inverting production left it green. The
ordering now lives in `reconcileAndAttributeChannelWindow`, which the test
drives directly.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Three review findings against the tracer's honesty contract:
- The readiness -> paint frame was unguarded. `awaitDeferredCommit` checks
age, frame gap, and visibility on every frame it drives, but the one rAF
it queues before `record()` checked neither age nor gap, so a suspension
in that seam (App Nap fires no visibilitychange) recorded the whole
absence as a clean switch.
- DM actions await `open_dm` before `goChannel`, so that relay round-trip
sat outside the measurement; Pulse's note actions used raw `navigate` and
produced no measurement at all. Callers now capture the click anchor and
hand it to `goChannel`, and Pulse routes through `goChannel`.
- A `navigate()` rejection left the trace it opened active, so an untraced
re-entry could settle the failed attempt. Cancellation is by trace
identity, so a newer same-channel attempt is never erased.
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 18 (Opus xhigh) finding: 26ee8982b moved the
measure's origin to the input event's timeStamp but left the start mark
at handler-dispatch time, so the Performance panel showed the measure
beginning before its own start mark by the input delay — two different
switch durations from one instrument. The mark now carries
startTime: startedAt, pinned by asserting mark and measure share the
anchor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 17 (Fable high) finding: startedAt sampled
performance.now() at click-handler dispatch, silently excluding input
delay — a click queued behind a long task under-reported by the whole
queueing time, unbounded, in exactly the contention regime the tracer
exists to expose. begin() now anchors at the dispatching event's
timeStamp when one is present (window.event is set only during
synchronous dispatch, so stale timestamps cannot leak in from async
continuations; min() guards skewed clocks).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 16 (Opus high) P3s:
- A same-task unmount-then-renavigate to the same channel left the exit
cleanup's scheduled abandon pending, and its microtask killed the
freshly opened trace (silent lost sample). beginChannelSwitchTrace
now revokes any pending abandon for its channel.
- The delayed-chunk spec's timing-budget guard hit expect(null) with a
raw matcher error when a truncated record had already cleared the
start mark — the one case its message exists for. Boolean form now
covers the null.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 15 (Fable xhigh) finding: a clean measure recorded
behind the held chunk — the exact regression under test — clears the
start mark and nulls elapsedSinceClick, so the timing-budget assertion
failed first with a 'rerun, not a tracer bug' message stating the
opposite of the truth. The contract assertion now runs first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 14 (Opus medium) P3s:
- settleChannelSwitchTrace guarded window but then read document
(visibilityState, querySelector) unguarded — the document guard in
ensureVisibilityWatcher was dead protection. The settle entry gate now
covers both globals.
- The concurrent boundary test folded the rotated generation in behind
if-let, so a regression where rotation never fires under contention
would pass green with all 32 lines in the live file. The read is now
unconditional, and the boundary comment's arithmetic is corrected
(rotation triggers before the 23rd append).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 13 (Sonnet xhigh) hardening: leavesChannelSurface
used a /channels/ prefix check, so sibling routes (forum posts) counted
as staying on the channel surface and kept a live trace alive. No
currently reachable path turns that into a wrong record — every
re-entry overwrites or drops the trace — but the invariant from
9b462328d should hold structurally, not incidentally. Anything other
than the exact message-view route now drops the active trace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 12 (Opus xhigh) findings, both reproduced:
- P1: the round-9 abort gate destructured { signal } in the members
queryFn — merely reading that getter sets React Query's
abortSignalConsumed, switching the roster query to cancel-and-revert
when its last observer unsubscribes mid-fetch. Interrupted switches
discarded rosters that previously landed in cache (the Tauri call
cannot be cancelled, so the work was paid and thrown away), and
A->B->A warm switches repaid a full roster fetch. Replaced with a
per-channel supersession token (openChannelMembersFetch) checked by
traceChannelMembersFetch — stale fetches stay out of the one-shot
slot without touching the signal. Sequences reset with community
state.
- P2: lastFrameAt started null, so the settle-entry -> first-frame
window skipped the starvation guard: a no-visibilitychange suspension
there recorded a truncated measure inflated by the whole stall
(bounded only by the 35s age guard). The gap clock is now seeded at
settle entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 10 (Fable high) finding: the specs' settle polls
accepted a legitimately truncated measure — the tracer honestly hitting
its 5s render-wait deadline on a slow box — and then failed the painted-
rows assertion with a message blaming the tracer. Both tests now poll
for clean measures only and fail truncated-only runs with an explicit
harness-timing message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 9 (Opus high) findings:
- P2: the members queryFn attributed unconditionally, so a roster fetch
superseded by a live join/leave invalidation could claim the trace's
one-shot membersFetch slot with a stale count and duration. It now
checks signal.throwIfAborted() before attributing — same rule as the
window fetch's reconcile abort gate.
- P2: a permanently dead sink (unwritable log dir, stale directory at
the log path) was indistinguishable from no traced switches: console
lines kept flowing while every append failed into .catch(() => {}).
The first persistence failure now warns once.
- P3: during a suspension that fires no visibilitychange, the deferred
render-pending marker stays latched, so the settle wait recorded a
truncated measure inflated by the whole stall (bounded only by the
35s age guard). A single inter-frame gap beyond 3s — beyond any real
main-thread stall — now drops the sample.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 7 (Opus medium) findings:
- P2: a trace begun while the channel route was still resolving never
mounted ChannelScreen, so no route-exit cleanup existed; leaving for
Home and history-backing into the channel within 30s settled the
stale trace with the time spent away. Any committed non-channel
navigation and any history traversal now drop the active trace at the
navigation layer (dropActiveChannelSwitchTrace), which covers traces
no component ever owned. Same-channel navigations keep the live
trace.
- The delayed-chunk spec now asserts no CLEAN measure while the chunk
is held (the tracer honestly emits a truncated one if its 5s deadline
passes) and guards its own timing budget explicitly so a slow CI box
fails with the real reason.
- The rotation-degradation test blocks rotation with a non-empty
directory instead of chmod, so it also holds when tests run as root
(containers), and now runs on Windows too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 6 (Sonnet xhigh) finding: a transient lock on the
rotated generation (AV/EDR or an editor, chiefly Windows) failed the
whole append — and since the live file stays oversized, every later
append re-entered the same failing branch, silently dropping every
record until the lock cleared. Rotation is now best-effort: on failure
the line appends unrotated and the size cap re-applies once a later
rotation succeeds. Unix regression locks the directory and asserts the
line survives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 5 (Fable medium) finding: a longtask trailing switch
N is delivered by the PerformanceObserver in a later task and landed in
switch N+1's freshly reset array, inflating its longtask lines. Drain
takeRecords() and discard before each sample's reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 4 (Opus xhigh) findings:
- A not-pending frame landing past the wait deadline is rAF starvation
(system suspend, App Nap — no visibilitychange), not render time: a
fast settle followed by a stall recorded up to 34s as a clean switch
under the click-anchored 35s age guard. Such frames now drop.
- Visibility accounting now spans the whole trace: one module-level
visibilitychange watcher timestamps transitions, and any transition
since the click (not just during the settle wait) drops the trace —
a window minimized while the fetch was in flight no longer records
its absence as switch time. Replaces the per-settle listener.
- begin() now clears the previous switch's settled mark and measure
too, so a consumer polling the User Timing buffer mid-switch can
never read the prior switch's entries as the current one's.
- The goChannel comment no longer claims every channel navigation
funnels through it: Pulse startDm navigates to the channel route
directly and is untraced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 3 (Sonnet) finding: beginChannelSwitchTrace marked
unconditionally but only record() cleared, so every abandoned/dropped
trace (forum visits, route exits, hidden-window and frame-starved
drops) leaked a permanent buzz:channel-switch:start entry. Clearing
the previous start mark at begin bounds the buffer to one entry no
matter how the prior trace ended.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Blind-review round 2 (Opus) findings:
- P1: rAF suspends in hidden windows, so a queued settle fired only on
the user's return and charged the whole absence to the switch as a
clean record (reproduced at runtime). Settle now drops when the window
is already hidden, poisons the wait on visibilitychange, and drops any
trace older than the entry timeout plus the render wait — nothing
legitimate can reach that age.
- The JSONL append is a single write_all: writeln! issues two write
syscalls and the lock is process-local while the log path is not, so a
second process sharing the log dir could interleave mid-line.
- The User Timing buffer keeps only the latest switch's mark/measure —
desktop sessions run for weeks and the buffer is never GC'd.
- The route-commit mark moved to a layout effect so it stamps commit
time, not first-paint time.
- Comment honesty: the member-heavy harness now states that mock-mode
IPC hands live objects (parse cost not exercised) and that the
Projects hydration marker guards cold/invalidated samples only; the
goChannel trace-anchor comment describes what the same-channel guard
actually skips; the Rust concurrency test's byte arithmetic corrected.
- Longtask sampling drains PerformanceObserver.takeRecords() at sample
time so a longtask ending just before the resolve frame isn't lost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Address the three findings from the 2026-08-24 re-review of #6455, plus
one finding from a follow-up adversarial review:
- The switch trace now opens inside commitGuardedNavigation, only after
the navigation guard accepts — a refused click can no longer leave an
orphan trace that a later history navigation would settle with
inflated time. The commit flow is extracted and dependency-injected;
new unit tests pin guard ordering and the refusal→history-back case.
- The perf-log 4 KiB cap now bounds bytes on disk: one byte is reserved
for the newline writeln! appends. New boundary test measures the file.
- The channel↔Projects benchmark gates readiness on a new
data-projects-hydrating marker (driven by the surface's query fan via
isLoading, so disabled queries never wedge it) instead of the shell
header, verifies member inflation on both inflated channels, uses a
prefixed ready-selector for general, and reports each switch direction
as its own median so a one-leg regression cannot hide.
- A settle wait that hits its 5s deadline while the render is still
pending now records with settleWaitTruncated instead of posing as an
honest settled paint — the >deadline tail is what the tracer exists
to expose. Also removes the dead ChannelSwitchFetchTrace type.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Address the three findings from the 2026-08-24 review of #6455:
- The lazy ChannelPane's Suspense fallback now carries the
data-render-pending marker, so a switch trace can no longer settle
while the pane chunk is suspended. New Playwright regression holds
the chunk and asserts no measure lands until rows paint.
- Route-exit trace abandonment is deferred one microtask and canceled
by the effect re-setup, so StrictMode's dev-only effect replay no
longer kills a just-opened trace. New jsdom regressions run the real
react-dom dev replay; the settle spec also passes against the Vite
dev runtime.
- The perf-log 4 KiB cap is enforced on the final serialized line, and
BUZZ_PERF_LOG_LABEL is truncated to 128 bytes at a char boundary, so
a runaway label can neither inflate the sink nor kill every record.
Also extracts the timeline-loading latch + trace marks from
ChannelScreen into useChannelTimelineLoading (file-size ratchet).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
Add an inflateChannelMembers mock-bridge knob (channel name → target
member count; synthetic hex-pubkey members appended once, on first
channel read) and a member-heavy-switch perf spec that measures warm
switch wall time and longtasks for channel↔channel and
channel↔Projects at baseline / 2k / 10k members per channel, holding
message volume constant at 150 rows. Method mirrors
warm-switch-markdown.perf.ts: in-page click + rAF polling, 4x CPU
throttle, medians over 16 switches after an untimed warmup round-trip.
Run from desktop/:
pnpm build:e2e
npx playwright test --config=playwright.perf.config.ts member-heavy-switch.perf.ts
Signed-off-by: Max Lampert <maxwell@squareup.com>
Add a single-active-trace instrument for channel navigation: goChannel
opens the trace, ChannelScreen marks route commit and settles it after
the timeline loading latch clears (double rAF so the frame painted).
The two relay fetches that can sit on the switch path — the message
window and the member roster — are attributed to the trace when they
run during it; cache-served switches log "cache".
Each switch emits one [switch-perf] console line plus User Timing
marks/measures (buzz:channel-switch:*) so before/after comparisons work
identically in a dev build, the Performance panel, and Playwright perf
specs.
Signed-off-by: Max Lampert <maxwell@squareup.com>
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side
This is Part 1 of the channel-first Projects stack. Part 2 contains
project creation and model foundations.
## Testing
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings`
- `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test
ignored
- full pre-push gate passed
## Post-Deploy Monitoring & Validation
- validate project-home agent context and project-aware CLI commands
against a staging relay
- healthy signals: project context matches the active channel, explicit
repo coordinates remain stable, and normal channels receive no project
block
- failure signals: cross-channel project context, duplicate project
creation, or commands targeting an unrelated repository; mitigate by
reverting this PR
---------
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
## Why
PR #6503 bundled a broader context compiler with several prompt-shape
and telemetry changes, making it difficult to isolate whether paired
XML-like top-level boundaries improve agent behavior. This keeps the
follow-up experiment focused on framing only.
## What
- Replace top-level standing and per-turn bracket headers with paired
tags while preserving inner section content and event/message framing
- Carry only metadata already present in dynamic headers as attributes,
such as thread counts, truncation, event type, and batch count
- Preserve every section body verbatim so the model executes the exact
bytes reviewed in Desktop
- Keep routing metadata, event content, batching, and delivery behavior
unchanged
- Teach Desktop diagnostics to parse the new framing while retaining
compatibility with archived bracket-framed sessions; ambiguous paired
boundaries render as the complete captured prompt
## Risk Assessment
Medium — the change is limited to agent prompt framing and diagnostic
parsing, but it affects both standing context and every dynamic agent
turn. Bodies and delivery behavior remain unchanged, and compatibility
parsing remains for prior sessions.
## References
- Follow-up to #6503, scoped only to the paired-tag framing hypothesis
## Update — 2026-08-24
- Addressed review feedback by removing entity encoding/decoding from
the execution and diagnostic paths.
- Added regressions for literal closing tags, placeholders,
entity-looking text, ampersands, and authored boundary whitespace.
- Verified `cargo test -p buzz-acp` (805 unit + 9 integration), Desktop
tests (5,412 before the main sync; 5,439 in the pre-push gate), Desktop
typechecking, and Tauri clippy/tests.
- Live local relay startup and message ingestion passed. The live ACP
reply step is blocked locally because installed Goose 1.0.23 no longer
provides the documented `goose acp` command and no alternate ACP adapter
is installed.
---
**Update Aug 25, 12:13 EDT:** Extended paired tags to per-turn top-level
sections.
- Converted context, conversation/thread context, event batches, and
steer/interrupt boundaries without changing their inner bodies.
- Preserved existing header metadata as attributes only; added no new
prompt metadata.
- Verified 806 Buzz ACP unit tests plus 9 integration tests, 5,441
Desktop tests, Desktop typechecking and formatting, and all differential
pre-push lanes.
---
**Update Aug 25, 13:58 EDT:** Addressed framing review feedback.
- Updated operational base instructions to reference the emitted
`<context>` section and added a regression guard against stale
`[Context]` instructions.
- Restored batched steer/interrupt cardinality in Desktop diagnostic
titles with focused coverage for both paths.
- Verified 807 Buzz ACP unit tests plus 9 integration tests, 5,442
Desktop tests, Desktop typechecking and formatting, and every
differential pre-push lane including Tauri clippy/tests.
---
**Update Aug 25, 15:37 EDT:** Hardened legacy transcript compatibility.
- Semantic standing-context parsing now requires paired framing at the
start and complete input consumption.
- Historical bracket-framed personas keep literal balanced tag examples
in their original section; malformed or mixed current framing displays
as the complete captured prompt.
- Added regressions for the reported legacy persona case and trailing
unframed text; verified 60 focused parser tests, 5,444 full Desktop
tests, formatting, and typechecking.
---
**Update Aug 25, 16:31 EDT:** Preserved slash-command prompt block
boundaries.
- Desktop now sets aside a valid connector-facing slash-command block
only when the following block begins recognized paired framing, parses
the semantic blocks normally, and retains the command as visible Prompt
metadata.
- Added an end-to-end transcript regression using the production
three-block ACP shape, including the user bubble, author pubkey, event
ID, and grouped prompt context.
- Verified 113 combined transcript tests, 5,445 full Desktop tests,
formatting, typechecking, and all applicable pre-push lanes.
Generated with Codex
---------
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
## Summary
- preserve string Nostr `sig` values in normal `messages get`, `messages
thread`, `messages search`, and `feed get` output
- omit absent or non-string signatures instead of fabricating empty
values
- keep compact output at exactly `{id, content, created_at}` and update
CLI/managed-agent contract docs
## Validation
- `cargo test -p buzz-cli --lib` (367 passed at final head
`a2498faa0e07f90efc0ab0165606ba3b048327c7`)
- release candidate live-relay matrix: 49/49 normal events independently
recomputed to their NIP-01 IDs and passed BIP-340 verification; all four
compact commands preserved exact three-key output and ordering
- `git diff origin/main...HEAD --check`
- independent adversarial review: clear, no findings
## Compatibility
The normal JSON change is additive for valid relay events. In-repo
consumers use tolerant field lookup. Compact output is unchanged and
locked by regressions for both message and feed formatting.
Generated by Carl, an automated contributor working with Wes.
---------
Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
## Summary
- Stop replaying the selected-agent pulse when a remembered agent
remains addressed across a send.
- Preserve the existing pulse for genuine selections and shake feedback
for failed sends.
### Related issue
Reported in Buzz:
buzz://message?channel=2745947d-f905-4f31-8a3f-fc4485d3c36c&id=0cffab376186be9cc4e7f29027352cc1c7941aead48b3fed948f36181f4a59af
### Testing
- `cd desktop && pnpm test` (5,556 passed)
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm check`
- `cd desktop && pnpm build:e2e`
- `cd desktop && pnpm exec playwright test
tests/e2e/persistent-agent-audience.spec.ts --grep 'always-mentioned
agents remain selected|failed always-mentioned send'` (2 passed)
This changes animation timing only; the regression is covered by
Playwright assertions on the pulse and shake versions, while a static
screenshot is unchanged.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
## Summary
- Treat the sidebar temp-channel icon as a fallback behind working,
mute, numeric unread, and thread-unread indicators.
- Keep the header affordance and the sidebar temp icon when it is the
only row status.
- Extend the ephemeral-channel browser scenario across working, idle,
collapsed-sidebar, and unread states.
## Before / After
| Before: five temp icons compete with duration and unread states |
After: all five duration and unread states remain clear |
| --- | --- |
| 
| 
|
## Testing
- `node --import ./test-loader.mjs --experimental-strip-types --test
"src/**/*.test.mjs"` (5,556 passed)
- `tsc --noEmit -p desktop/tsconfig.json`
- `biome check desktop/src/features/sidebar/ui/SidebarSection.tsx
desktop/tests/e2e/channels.spec.ts`
- `vite build --mode e2e`
- Isolated Playwright run of `create ephemeral stream shows sidebar and
header affordances` (1 passed)
## Duplicate check
No matching open PR or issue found for `ephemeral sidebar icon`.
Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
## Summary
Pasting a link while composer text is selected now turns that text into
a hyperlink instead of replacing it — the behavior people expect from
every other editor.
- `resolveExactLinkPaste` recognizes a clipboard payload that is exactly
one supported link (bare or `<…>`-wrapped `http(s)`, or a Buzz deep
link, canonicalized through the existing composer-link attribute
resolver) and returns the href to apply.
- The paste handler applies that href as a `link` mark over the current
selection, collapses the caret to the end of the newly linked range,
clears stored marks so subsequent typing isn't linked, and refocuses the
editor.
Edge cases handled:
- **No link mark available / selection can't carry marks** — falls
through to the existing replace-with-node path rather than silently
dropping the paste.
- **Selection contains a composer message-link chip** — keeps the old
replacement behavior instead of nesting a link over a chip.
- **Whitespace in the clipboard payload** — `unwrapExactHttpLink` now
rejects any text containing whitespace, so multi-line or "text plus URL"
pastes are not treated as an exact link.
- **Empty selection (caret paste)** — unchanged.
- **Idempotent re-paste** of an already-wrapped link is consumed rather
than doubling up.
## Testing
`desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs`
gains coverage for exact-link resolution (per-shape accept/reject cases,
Buzz canonicalization) and for the paste handler: linking selected text,
canonicalizing Buzz links over a selection, idempotent wrapped-link
paste, fall-through when the selection can't carry marks, all-selection
collapse, chip-containing selections, and preserved caret-paste
behavior.
Full pre-push gate ran green on this branch: `file-size-check`,
`desktop-check`, `desktop-typecheck`, and `desktop-test` (5425 tests, 0
failures).
## Demo
Pasting an `https://` URL over selected composer text turns the selected
words into a hyperlink (instead of replacing them); text typed
immediately after is not linked:
https://github.com/user-attachments/assets/bbc93574-dd9d-4b1b-bad0-53307b6e887c
---------
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.20
- **Frozen main:** `8471049c430073474939336dfc6aa98272bc8762`
- **Reviewed candidate:** `95154bee4034ca7a40b33095c2ddbde8c9aa1614`
- **Previous desktop release:** `desktop-v0.5.19`
- **Proposed immutable tag:** `desktop-v0.5.20`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary
- Supersedes #1913 with a KLIPY-hosted URL implementation.
- Adds KLIPY GIF search and trending results to desktop message and
forum composers.
- Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL
and media metadata (no imeta tag, since relays only accept hash-backed
local `/media/` entries).
- Aligns the Emoji/GIF picker with Buzz's standard segmented control,
theme surfaces, and motion behavior.
## Relay-to-provider boundary
- The relay proxies KLIPY search/share so the `BUZZ_KLIPY_API_KEY` never
reaches the desktop; the key stays server-side behind a redacted `Debug`
impl.
- The dedicated GIF `reqwest` client sets `redirect::Policy::none()`.
Because the API key rides in the request path, following a provider
`3xx` could replay a key-bearing URL to an attacker-chosen host (an
SSRF/key-disclosure primitive). With redirects disabled, a `3xx` returns
as a non-success status that the handlers map to a generic `502`; the
`Location` target is never read or forwarded.
- Admission reuses the established NIP-98, membership, replay, and
per-pubkey rate-limit gates, with an upstream response-size cap and
allowlisting so KLIPY error bodies never cross the relay boundary.
## Accessibility
- Under `prefers-reduced-motion: reduce`, the picker grid renders a
static provider poster (a normalized `jpg` asset) instead of the
animated preview, or a named static placeholder when no poster is
available. It reacts to preference changes while mounted.
`no-preference` keeps the animated preview.
- Selected GIFs carry their title through `ImetaMedia`'s `displayLabel`,
so the composer thumbnail, preview dialog, editor, lightbox, and remove
control all derive one non-empty accessible name instead of an empty
`Attachment ` label. Ordinary hashed uploads keep their existing
hash-derived names.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
**Category:** fix
**User Impact:** Disabling automatic agent mentions now keeps one-time
agent mentions out of the next message draft.
**Problem:** A successfully sent inline agent mention was treated as
eligible for post-send restoration even when **Automatically mention
agents** was disabled, so the agent immediately reappeared in the
composer. **Solution:** Gate the send-success restoration path on the
live preference while leaving explicitly pinned agents and enabled
automatic mentions unchanged.
<details>
<summary>File changes</summary>
**desktop/src/features/messages/ui/MessageComposer.tsx**
Skips the automatic post-send mention restoration path when the
preference is disabled.
**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Reproduces disabling the toggle before sending an inline agent mention
and verifies the next composer is empty while the outgoing mention still
reaches the agent.
</details>
## Reproduction steps
1. Enable **Automatically mention agents** in the composer mention
options.
2. Disable it again.
3. Compose and send `@Agent test` using the inline mention picker.
4. Confirm the message still mentions the agent, but the cleared
composer does not repopulate `@Agent`.
5. Re-enable the preference and repeat; confirm the mention is restored
for the next message.
## Testing
At `a179a2d00a6ac5cf0bc4a1c7c1ef3d5dfa9d0875`:
- Full desktop unit suite: 5,501 passed.
- Desktop check and typecheck passed in the pre-push hooks.
- Desktop file-size ratchet passed in the pre-push hooks.
- Full `persistent-agent-audience.spec.ts` Playwright file: 17 passed,
including preference off (empty composer), preference on (restored
mention), and the existing address-undo journey.
## Visual validation
| Automatic mentions off | Automatic mentions on |
| --- | --- |
| After sending a one-time mention, the outgoing message is preserved
and the next composer stays empty. | After sending, the agent remains
addressed and is restored in the next composer. |
| 
| 
|
<details>
<summary>Original regression</summary>
With automatic mentions disabled, sending `@Alia test` still repopulated
`@Alia` in the cleared composer.

</details>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
## Buzz Desktop release v0.5.19
- **Frozen main:** `ee6ca5fa28bce04dfecb6717de65b08a57f2ac47`
- **Reviewed candidate:** `ab691bcdaeccaa6698a2199beaba3f6e93daae81`
- **Previous desktop release:** `desktop-v0.5.18`
- **Proposed immutable tag:** `desktop-v0.5.19`
This PR may be **squash merged** after the Desktop Release Candidate
check and all protected-branch checks pass. Merging authorizes
publication of the exact reviewed candidate; later or unrelated changes
on `main` cannot alter it.
The checked-in changelog accounts for every non-merge commit in the
release range. The Desktop tag points to the reviewed candidate commit,
not the later squash commit. Publication remains bound to that immutable
candidate tag.
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Release Automation <release-automation@users.noreply.github.com>
## Summary
- replace the generated Nest's unconditional human author/sign-off rules
with portable guidance that separates authorship, material
co-authorship, DCO certification, and cryptographic signing
- defer attribution to repository-local policy, forbid inferred or
guessed identities, and require inspection of every outgoing commit
- bump the Nest template version so existing installations refresh, with
regression coverage for fresh generation and upgrade preservation
### Related issue
None found. Related runtime identity work exists in #6177, but this PR
is intentionally limited to the generated Nest guidance and its refresh
behavior.
### Testing
- `bin/just desktop-tauri-clippy`
- `bin/just desktop-tauri-test`
- `bin/just file-size-check`
- `git diff --check`
- pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`,
and `desktop-tauri-checks`
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
**Category:** fix
**User Impact:** Users can place `@Agent` mentions inline anywhere in a
message—even when that agent is also persistently addressed—so separate
agents can receive separate instructions in the same message.
**Problem:** Selecting an agent from the composer mention picker could
turn the selection into persistent addressing instead of leaving an
inline `@Agent` at the cursor. That made persistent addressing and
inline composition mutually exclusive: once the persistent behavior took
over, users lost the clear `@Agent A do X, @Agent B do Y` message
structure they previously had.
**Solution:** Make inline mentioning and persistent addressing
independent behaviors:
- Ordinary mention selections always insert `@Agent` inline at the
current cursor position.
- This remains true when the agent is already persistently addressed;
the persistent audience never blocks or replaces an inline mention.
- If **Automatically mention agents** is enabled, a successfully sent
inline mention may additionally make that agent persistent for later
messages. Existing saved preferences remain respected.
- The dedicated automatic-mention control and primary+Shift+Enter
shortcut continue to add or remove persistent addressing directly.
- For users without a saved preference, **Automatically mention agents**
now defaults off.
<details>
<summary>File changes</summary>
**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Updates preference coverage for the default-off behavior while
preserving explicit saved choices.
**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Defaults automatic post-send persistence off when no valid preference
exists.
**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Verifies ordinary picker selections insert inline mentions without
changing or pulsing the persistent audience, including when the selected
agent is already persistently addressed.
**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**
Keeps ordinary mention selection on the existing inline insertion path
while preserving the separate persistent-address controls and shortcut.
**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Covers separately targeted inline instructions to two agents, signed
outgoing recipients, default-off persistence, and explicit opt-in
persistence scenarios.
</details>
## Reproduction steps
### Inline mentions without persistence
1. Open a channel with at least two available agents and leave
**Automatically mention agents** disabled.
2. Use the composer mention picker to insert the first agent, type an
instruction, then insert a second agent and type a different
instruction.
3. Confirm the draft reads like `@Agent A review this, @Agent B test
that` and neither agent appears in the persistent addressed-agent
controls.
4. Send the message and confirm both agents are recipients while neither
remains persistently addressed for the next draft.
### Inline mentions with persistence
1. Enable **Automatically mention agents**, then mention an agent inline
and send successfully.
2. Confirm that agent becomes persistently addressed for later messages.
3. In a new draft, select the same agent from the mention picker again.
4. Confirm a new inline `@Agent` is inserted at the cursor while the
agent remains persistently addressed.
5. Confirm the dedicated automatic-mention control and
primary+Shift+Enter shortcut can still add or remove persistent
addressing directly.
## Demo
- **Before:** Persistent addressing could consume an ordinary picker
selection, preventing users from placing that agent inline in the
message.
- **After:** Ordinary selection always produces an inline `@Agent`;
persistence is a separate optional behavior that can coexist with inline
mentions.
### Related issue
N/A — reported through the Buzz feature room.
### Testing
At `a4579c0a666f3644abe44271614186d7b9356bf5`:
- Post-push hooks passed desktop check, desktop typecheck, and the full
desktop unit suite.
- The two persistence-focused Playwright smoke scenarios passed after
explicitly opting into **Automatically mention agents**.
- The one-time multi-agent targeting and persistent-address shortcut
Playwright scenarios passed on the feature changes before the final
rebase; CI is validating the rebased branch.
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
**Category:** improvement
**User Impact:** Agent addressing now stays visible, editable, and
consistent across messages, so users can keep talking to the same agents
without rebuilding the audience for every send.
**Problem:** Agent addressing was transient and easy to lose track of.
Inline mention text, persistent audience state, restored drafts, and
sent-message rendering could drift apart, making it unclear who would
receive the next message.
**Solution:** The composer now treats addressed agents as a persistent
audience represented by ordinary editable mention chips and a
synchronized compact avatar tray. Eligible agent mentions can become
persistent immediately, addressing survives sends without flashing
empty, and removing or explicitly unpinning an agent updates that state
without disturbing unrelated draft text. `Cmd+Shift+M` / `Ctrl+Shift+M`
provides a fast way to toggle the current recipient or select the best
eligible agent.
- Promotes eligible inline agent mentions into persistent addressing
when the preference is enabled; human mentions remain unchanged.
- Keeps inline chips, restored drafts, persistent recipient state, and
the avatar tray synchronized.
- Clears an agent's persistent address when its final chip is removed,
while explicit unpinning preserves unrelated draft text.
- Restores persistent mentions optimistically during send and restores
the full draft on failure, avoiding an empty-composer flash.
- Deduplicates inline mentions and tag-backed address prefixes in sent
messages.
- Toggles the current addressed agent with `Cmd+Shift+M` /
`Ctrl+Shift+M`, or selects the best eligible agent when none is
addressed.
- Ranks shortcut selection by recent explicit stream mentions, then
active/member/runnable/stable fallbacks; DM participant fan-out does not
affect recency.
- Adds focused avatar feedback for entry/exit, send bounce, and failure
shake, with reduced-motion and narrow-layout support.
- Applies the same behavior to main and thread composers while
preserving draft restoration, channel switching, and send-race
correctness.
### Screenshots
#### Light theme
The compact tray keeps persistent recipients visible alongside editable
mention chips.

#### Dark theme
Persistent addressing remains clear and legible in the dark composer
treatment.

### Reproduction steps
1. Open a channel with an eligible agent and enable persistent agent
addressing.
2. Insert an agent mention and confirm it appears as a normal editable
chip while the compact avatar tray reflects the same persistent
recipient.
3. Send several messages and confirm the agent remains addressed, the
mention restores without an empty-composer flash, and sent messages do
not duplicate the addressed agent.
4. Remove the agent's final mention chip and confirm its persistent
address clears. Add unrelated draft text, explicitly unpin the agent,
and confirm that text remains intact.
5. Press `Cmd+Shift+M` on macOS or `Ctrl+Shift+M` on Windows/Linux.
Confirm it toggles the current addressed agent or, when none is
selected, chooses the most relevant eligible agent.
6. Verify recent explicit channel/thread mentions influence shortcut
selection, while DM participant fan-out does not.
7. Force a failed send and confirm the addressed avatars shake and the
full draft is restored; confirm successful sends use the subtler bounce
feedback.
8. Repeat in the thread composer, after switching channels, in dark
theme, with reduced motion enabled, and at a narrow composer width.
<details>
<summary>File changes</summary>
**desktop/src/features/channels/ui/ChannelPane.tsx**
Feeds recent explicit agent mentions into the composer for contextual
shortcut selection.
**desktop/src/features/messages/lib/getVisibleAgentAddressPubkeys.ts**
and tests
Show tag-backed addressed agents only when they are not already
represented by inline mention text.
**desktop/src/features/messages/lib/mentionCandidates.ts**,
**mentionRanking.ts**, **mentionSuggestionMapping.ts**,
**recentMentionPubkeys.ts**, and tests
Model agent eligibility and activity, derive explicit mention recency,
and rank the best shortcut recipient.
**desktop/src/features/messages/lib/useActiveAgentPubkeys.ts**,
**useDefaultAgentSuggestion.ts**, and **useMentions.ts**
Expose active-agent and recent-recipient context to mention consumers.
**desktop/src/features/messages/ui/ComposerAddressControls.tsx** and
tests
Render the compact persistent-recipient tray with enter, exit, send,
failure, layout, and reduced-motion states.
**desktop/src/features/messages/ui/MessageComposer.tsx** and
**MessageComposer.types.ts**
Connect editable mention text, persistent addressing, shortcut
selection, draft restoration, and send feedback.
**desktop/src/features/messages/ui/MessageRow.tsx**
Avoids duplicating addressed agents already rendered as inline mentions.
**desktop/src/features/messages/ui/MessageThreadPanel.tsx**
Brings the same recency and persistent-addressing behavior to thread
composers.
**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**,
**useAutoPinMentionedAgents.ts**, **useAlwaysAddressShortcut.ts**,
**useMentionSendFlow.ts**, and tests
Synchronize mention chips with persistent audience state, promote
eligible mentions, handle shortcut toggling/default selection, and
preserve recipients through send success or failure.
**desktop/src/features/messages/ui/useComposerPasteHandler.ts** and
**useMentionSendFlow.types.ts**
Keep the composer implementation focused by extracting paste and
send-flow boundaries.
**desktop/src/shared/lib/keyboard-shortcuts.ts**
Documents `Cmd+Shift+M` / `Ctrl+Shift+M` as the agent-address shortcut.
**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Exercises persistence, selection, removal, sends, failures, restored
drafts, themes, reduced motion, and narrow layouts.
</details>
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
## Summary
- navigate across image occurrences that are currently rendered inside
the nearest explicit open-thread DOM scope
- preserve rendered DOM order, duplicate occurrence identity,
hidden-spoiler exclusion, preview-image entry parity, and the exact
thumbnail return target
- keep adjacent timeline messages and unrelated surfaces as separate
galleries
- make Copy and Download follow the current gallery item, including when
a link-preview image opens the gallery first and navigation reaches
Markdown media
## Scope contract
This is intentionally a **rendered-media gallery only**. Gallery
membership comes from mounted image triggers under the nearest explicit
thread scope. Collapsed or unloaded descendants are excluded until the
thread UI renders them. There is no canonical thread fetch, descendant
projection, depth override, or synchronous message reparse in this
change.
`ChannelPane.tsx` remains unchanged from `main`;
`useSearchHighlightProps` and both timeline/thread prop spreads are
preserved.
## Verification
- rebased onto `origin/main` at
`113a33b7e49b7173ee1767c49ef2f49c63803034`
- `git diff --check`
- differential file-size check
- full Desktop check (Biome plus text/pubkey policy checks; only
pre-existing informational diagnostics)
- Desktop TypeScript typecheck
- Desktop production build and rebuilt E2E build
- full Desktop JS units: `5,476/5,476` passed
- full Desktop Tauri workspace tests passed (one documented native
performance test ignored)
- rebuilt image gallery Playwright spec: `14/14` passed
- search-highlight smoke cases: `5/5` passed
- reduced diff: 6 files, `+417/-41`; production `+78/-41`, E2E `+339/-0`
The four added E2E cases cover cross-message thread navigation and
return targeting, rendered-only exclusion of collapsed descendants,
preview-first current-item actions, and adjacent-message isolation.
---------
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
**Category:** fix
**User Impact:** Mention chips now follow the active theme without a
fixed yellow fill and remain readable across every supported theme.
**Problem:** PR #6696 added an opaque yellow treatment to every human
and agent mention. Removing that fixed color exposed another issue: some
theme accent colors do not provide WCAG AA contrast when also used as
normal-size mention text.
**Solution:** Remove the fixed yellow treatment and keep the active
theme accent as a subtle translucent chip background. Mention labels use
a light/dark semantic foreground selected for readable contrast,
including hover states.
<details>
<summary>File changes</summary>
**desktop/src/shared/styles/globals/markdown.css**
Removes the fixed-yellow treatment, retains theme-derived accent
backgrounds, and applies an AA-safe semantic foreground to human and
agent mention labels in resting and hover states.
**desktop/src/shared/styles/globals/theme.css**
Removes the obsolete fixed-yellow tokens and defines light/dark mention
foreground values.
**desktop/tests/e2e/mentions.spec.ts**
Adds rendered contrast coverage for resting and hover states across
every supported syntax theme, compositing translucent chip and ancestor
surfaces before checking the 4.5:1 threshold.
</details>
## Reproduction steps
1. Open a message containing a human or agent mention.
2. Confirm its subtle background follows the active theme accent rather
than using yellow.
3. Hover the mention and confirm the accent background strengthens while
the label remains readable.
4. Switch between default, light, dark, and custom syntax themes and
confirm mention labels remain legible.
This is a focused visual rollback of the mention treatment introduced by
#6696. The channel notification badge changes from that PR remain
intact.
---------
Signed-off-by: tulsi <tulsi@block.xyz>
**Category:** fix
**User Impact:** Search terms are now highlighted in yellow in Cmd+F
results and in the message opened from a result.
**Problem:** Search returned relevant messages, but users still had to
reread each preview and destination message to discover where the query
appeared. Search navigation could also lose or misapply highlighting
during rapid typing, repeated navigation, thread opening, and forum
navigation.
**Solution:** Render match-focused previews with a shared yellow
treatment, carry the result query through navigation, and apply it only
to the clicked destination. The implementation binds highlights to the
debounced result set, supports token and prefix matching, and handles
channels, threads, forums, diffs, and wave messages consistently.
<details>
<summary>File changes</summary>
**desktop/src/app/AppShell.tsx**
Carries the active result query into search-hit navigation so the
destination can preserve the user's context.
**desktop/src/app/navigation/searchHitEventCache.ts**
Adds a bounded, one-shot cache for result queries and navigation IDs
alongside cached search-hit events.
**desktop/src/app/navigation/searchHitNavigation.test.mjs**
Covers query retention, one-shot consumption, repeated same-route
activations, forum posts, forced routing, and cancellation.
**desktop/src/app/navigation/searchHitNavigation.ts**
Associates each search-result activation with a unique navigation ID and
forwards it to channel or forum destinations.
**desktop/src/app/navigation/useAppNavigation.ts**
Extends channel and forum navigation to carry search navigation state
without changing normal navigation behavior.
**desktop/src/app/routes/ChannelRouteScreen.tsx**
Consumes search highlight state after navigation and retains it for the
selected message while clearing it on community context changes.
**desktop/src/app/routes/channels.$channelId.posts.$postId.tsx**
Validates and forwards forum search-navigation IDs.
**desktop/src/app/routes/channels.$channelId.tsx**
Validates and forwards channel search-navigation IDs.
**desktop/src/features/channels/ui/ChannelPane.tsx**
Passes the selected result ID and query into the main timeline and open
thread panel.
**desktop/src/features/channels/ui/ChannelPane.types.ts**
Defines the destination-highlight contract for channel panes.
**desktop/src/features/channels/ui/ChannelScreen.tsx**
Routes destination highlighting to either forum content or the channel
timeline.
**desktop/src/features/channels/ui/ChannelScreen.types.ts**
Defines route-level search-highlight inputs.
**desktop/src/features/channels/ui/ForumChannelContent.tsx**
Forwards search context into expanded forum threads.
**desktop/src/features/forum/ui/ForumThreadPanel.tsx**
Highlights the matching text in the selected forum post or reply.
**desktop/src/features/forum/ui/ForumView.tsx**
Carries selected-result context from forum routing into the thread
panel.
**desktop/src/features/messages/ui/DiffMessage.tsx**
Forwards the query into diff descriptions and rendered diff content.
**desktop/src/features/messages/ui/DiffViewer.tsx**
Provides a highlighted match excerpt for structured diffs and highlights
fallback raw diff text.
**desktop/src/features/messages/ui/MessageRow.tsx**
Passes destination queries into normal Markdown, diff, and wave message
renderers.
**desktop/src/features/messages/ui/MessageThreadPanel.tsx**
Applies highlighting to the selected thread head or reply without
tinting unrelated messages.
**desktop/src/features/messages/ui/WaveMessageAttachment.tsx**
Highlights matches in wave-message fallback text.
**desktop/src/features/search/lib/searchMatch.test.mjs**
Covers case-insensitive, literal, multi-term, prefix, one-character, and
late-preview matches.
**desktop/src/features/search/lib/searchMatch.ts**
Centralizes token extraction, case-insensitive match splitting, and
match-focused preview generation.
**desktop/src/features/search/ui/HighlightedSearchText.tsx**
Adds the reusable accessible mark renderer used by result and
specialized message surfaces.
**desktop/src/features/search/ui/TopbarSearch.tsx**
Highlights result previews, centers excerpts around matches, strips
search operators, and hides stale results during debounce transitions.
**desktop/src/features/sidebar/ui/AppSidebar.types.ts**
Updates the sidebar search callback contract to include the result
query.
**desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx**
Passes query-aware result selection through the pinned search entry
point.
**desktop/src/shared/lib/rehypeSearchHighlight.ts**
Uses the shared matcher to mark every matching Markdown text segment
while leaving code blocks untouched.
**desktop/src/shared/lib/searchHighlightStyle.ts**
Defines one yellow highlight treatment for light and dark themes.
**desktop/src/shared/ui/markdown/nodeCache.test.mjs**
Verifies all Markdown matches, one-character scoped searches, code
exclusion, and transient cache behavior.
**desktop/src/shared/ui/markdown/nodeCache.ts**
Enables transient highlighting for one-character scoped searches without
polluting the Markdown cache.
**desktop/tests/e2e/smoke.spec.ts**
Exercises result and destination highlighting, same-route forum
activation, and stale-query suppression end to end.
</details>
## Reproduction steps
1. Open a channel containing a longer message and press Cmd+F.
2. Search for a word that appears later in the message, such as
`mentions`.
3. Confirm each matching result shows the word in yellow and keeps the
match visible in its preview.
4. Open a result and confirm the same term is highlighted in yellow in
the destination message.
5. Repeat with a thread reply or forum post, and quickly change the
query to confirm stale results are not selectable.
## Demo
- **Before:** Search results returned the correct message but offered no
visual indication of where the query matched; opening the message also
showed no match treatment.
- **After:** Every matching term is marked in yellow in the result
preview and in the exact channel, thread, or forum message opened from
that result.
---------
Signed-off-by: tulsi <tulsi@block.xyz>
## Summary
- Replace the decorative lightbox zoom icons with accessible Zoom out
and Zoom in buttons.
- Route button clicks through the existing 1x–3x, 5% stepped zoom state
without dismissing the viewer, including clicks on SVG icon descendants.
- Extract the zoom toolbar so `markdown.tsx` remains below the
repository file-size threshold.
### Testing
- `pnpm build:e2e`
- `pnpm exec playwright test image-attachment-gallery.spec.ts
--project=smoke` — 10 passed
- `pnpm typecheck`
- Full desktop unit suite — 5,397 passed
- Pre-push checks — file-size gate, desktop checks, typecheck, and
desktop tests passed
- `git diff --check`
### UI evidence
The rendered lightbox flow is covered by the gallery smoke test: Zoom
out is disabled at 100%, Zoom in changes the value to 105%, Zoom out
returns it to 100%, and the dialog remains open throughout. Existing
gallery, keyboard, spoiler, context-menu, and close-path assertions also
pass.
---------
Signed-off-by: sanic <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: sanic <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz>
Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
## Summary
- wait for TipTap's mount lifecycle before reading the selection
formatting tray's editor DOM
- detach and reattach DOM listeners across editor view unmounts and
remounts
- cover mounted and pre-mount editor view access
## Testing
- `just ci`
- `pnpm build:e2e`
- `pnpm exec playwright test
tests/e2e/composer-selection-formatting.spec.ts --project=smoke --grep
"right-clicking selected composer text"`
- Native Builderlab staging: reproduced the pre-mount crash on main,
relaunched with the fix, and verified the composer mounts without the
TipTap error
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
- ignore mobile room tone when deciding whether human speech should
interrupt agent audio
- show a subtle waiting animation while an agent prepares a response,
then restore its avatar when speech begins
- render agents from Huddle membership before their audio peer starts
transmitting
## Testing
- `just ci`
- signed Release build installed and launched on a physical iPhone
- live Huddle voice and agent-membership behavior verified on device
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>