mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
e02cd6b62f51e966eb62389680decd29a2c548c5
2473 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e02cd6b62f |
refactor(desktop): extract the channel agent session surface from ChannelPane
**Category:** refactor The file-size ratchet grandfathers `ChannelPane.tsx` above the 1000-line cap but forbids growth, and `#6575` landed it at 1011 lines on main — so this branch's +35 became a hard gate failure on rebase rather than the +35 it was when the branch was cut. Extracting rather than trimming, and specifically extracting *this branch's* own additions rather than anything `#6575` just added: the agent-session branch of the auxiliary-surface chain moves into `ChannelAgentSessionSurface`, taking the cross-channel re-scoping rule, the cover-drawer-vs-split-pane choice, and its layout-prop resolution with it. `ChannelPane` keeps what it owns — the surface resolution, the shared `AnimatePresence` key, and the split pane's resize affordances, which it passes down as `wrapSplitPane`. No behaviour change: the same panel with the same props in both presentations. `ChannelPane.tsx` is now 1006 lines, back under the ratchet's recorded 1011. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
5aae5c5bfd |
test(desktop): supply the thread edit guard to the activity exclusivity harness
**Category:** test `#6575` made `requireThreadEditResolution` a required dependency of `useChannelAgentSessions`, which this branch's exclusivity harness predates — so after the rebase all five cases threw `requireThreadEditResolution is not a function` rather than exercising last-opened-wins. Production wiring was never affected; `ChannelScreen` supplies it from `useChannelPaneHandlers`. Rather than stub it silently, the harness takes it as an injectable so the guard's interaction with the replacement is itself covered: a refused open must leave the thread and its draft exactly as they were, with no half-applied replacement that cleared the thread before the guard turned the activity open away. Verified to fail with the guard's early return removed. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
32c9eaf248 |
test(desktop): cover Escape's yield to an in-progress edit in the cover drawer
**Category:** test `#6575` gave `FocusThreadDrawer` a `hasActiveEdit` prop so a capture-phase Escape claim yields to an edit composer inside the drawer instead of dismissing the surface and losing the draft. Rebasing this branch moved Escape ownership out of that file and into `CoverDrawer`, so the behaviour now lives behind `escapeYieldsToContent` — and `#6575`'s only regression test for it is an e2e case written against the thread drawer, which cannot see the agent drawer or the prop boundary itself. This adds jsdom coverage at the new seam: a press inside the drawer yields while content owns the key, closes when it does not, and a press from outside closes it either way, so the yield cannot wedge the drawer open. Verified to fail on the first case with the guard removed, while both negative controls keep passing. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
1ca346a52b |
fix(desktop): capture the cover drawer's opener once across a StrictMode replay
The drawer captured `document.activeElement` in a layout effect with an
empty dependency list. The app root is wrapped in `React.StrictMode`, which
in development replays every effect as setup -> cleanup -> setup, so by the
second setup the drawer had already focused itself and recorded *itself* as
the element it had covered. On a real close the deferred restore then
focused a node React had detached, which is a silent no-op that drops focus
to `<body>` -- keyboard-stranded, no visible focus ring, in the exact dev
build people use the feature in.
A one-shot ref guards the capture. Refs survive the replay, so the second
setup keeps the opener the first one recorded. The flag is deliberately not
reset in cleanup: the only cleanup it sees before a real close is the
simulated one, which is precisely what it exists to ignore.
The focus-slot claim stays unconditional. Re-claiming on the replayed setup
is what makes the first cleanup's deferred restore stand down, so the
coordinator was already behaving correctly here -- only the capture was
wrong.
The existing focus-handoff tests could not see this because they render
without StrictMode, so the new case renders under it via
`render(..., { reactStrictMode: true })`. Verified to fail at the previous
head (focus lands on `HTMLBodyElement` instead of the opener) and to pass
with the guard.
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
|
||
|
|
0c343c0607 |
test(desktop): promote the cover drawer preview into a reference screenshot spec
The throwaway spec used to compare presentation options has now paid for itself twice, so give it a permanent home next to the other activity screenshot specs and register it in the smoke project. One seeded turn with the shape a real investigation has: a `@mention` that starts it, thinking, three file reads, a shell command, a relay post, a step that failed, a plan, and an answer containing code. The prompt is framed the way the harness frames it — a `[Buzz event: ...]` section with `From:`/`Content:` lines — because `parsePromptText` reads the author pubkey and the user-visible text out of exactly that shape; a plain text prompt would render as an unattributed bubble and would not exercise the header the drawer exists to make readable. Three frames, all at the drawer's design width: the full window as it opens (the drawer against the sliver and the scrimmed channel, which a panel-only shot cannot show), then the expanded turn at its head and at its tail. Expanding matters — the transcript opens folded, so the default frame is a stack of one-line summaries that shows none of the turn's shape. Expanded it is taller than the drawer, hence two frames. The PNGs are the deliverable, not a pixel baseline, so the assertions are scoped to what this slice owns: the drawer covers, the panel is mounted inside it, there is no split resize handle, and the turn actually rendered. Transcript structure, grouping, and styling belong to the transcript variants being restyled in parallel and are asserted by their own specs; asserting them here would make the reference shots fail for reasons unrelated to the drawer. For the same reason the helpers reach for DOM contracts rather than transcript markup — the scroller is found by computed overflow, and folds are opened through the native `<details>` `open` property rather than by clicking a variant-owned toggle. Two things the helpers guard rather than assume. The expander asserts it found at least one collapsed row, so a moved fold cannot silently turn the expanded shots into duplicates of the collapsed one. And the scroll-to-head polls a value read two frames after the write: the panel is tail-anchored and expanding pins it back to the bottom, so an immediate read would report the write instead of the outcome and the shot would quietly become a second tail frame. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
5a6dc8704f |
fix(desktop): coordinate cover drawer focus with a shared slot claim
**Category:** fix **User Impact:** Opening a thread over agent activity (or activity over a thread) now leaves keyboard focus inside the drawer that just opened. Before, the outgoing drawer's deferred focus restore fired after the new drawer had already taken focus and pushed focus into the covered channel, which is `inert` — so focus landed on `<body>` with no visible focus ring and no keyboard path back into the visible drawer. **Problem:** Two review findings, one root cause. `CoverDrawer` decided at teardown whether to restore focus by calling a caller-supplied predicate that read thread view-mode state, so a presentation-only primitive was interpreting a specific caller's presentation, and the predicate could not see the replacement race at all. Separately, last-opened-wins was only demonstrated through the resolver's fixed thread-beats-agent priority; the e2e "exclusivity" test closed one drawer before opening the other and its final step opened nothing, so no test exercised a real open-over-open transition or asserted the replaced surface's URL param was cleared. **Solution:** Replace the predicate with a module-level focus slot: a drawer claims the slot when it captures focus and restores only if its claim is still current, so any successor's claim — from any source, with no mount ordering assumptions — invalidates the loser's pending restore. The thread view-mode switch releases the slot from the thread side, keeping that decision out of the primitive while leaving plain thread close identical. Document the resolver's priority as a stale-simultaneous-param safety net and test the open handlers directly for both orderings, which is where the real rule lives. <details> <summary>File changes</summary> **desktop/src/features/channels/lib/coverDrawerFocusSlot.ts** New single-slot coordinator: `claimCoverDrawerFocus`, `hasCoverDrawerFocusClaim`, `releaseCoverDrawerFocus`. A monotonic generation answers "was I superseded?" without any drawer naming its successor. **desktop/src/features/channels/ui/CoverDrawer.tsx** Drops the `shouldRestoreFocusOnClose` prop. Claims the slot on focus capture and gates the deferred restore on still holding it. **desktop/src/features/channels/ui/FocusThreadDrawer.tsx** No longer reads thread view-mode state to decide focus restore. **desktop/src/features/channels/ui/useThreadViewModeSwitch.ts** Releases the focus slot when the switch places focus itself. **desktop/src/features/channels/lib/channelAuxiliarySurface.ts** Documents the priority order as a safety net for stale/simultaneous params, not the product rule. **desktop/src/features/channels/ui/CoverDrawerFocusHandoff.test.mjs** Drives `CoverDrawer` in jsdom: plain close restores, a replaced drawer leaves focus with its successor, a chain of replacements keeps only the last, and a released slot leaves focus where the caller put it. **desktop/src/features/channels/lib/coverDrawerFocusSlot.test.mjs** Claim/supersede/release semantics, including that claims are never reused. **desktop/src/features/channels/ui/useChannelAgentSessionExclusivity.test.mjs** Tests the open handlers directly in both orderings, plus the breadcrumb that keeps the replaced thread recoverable. **desktop/tests/e2e/agent-activity-cover.spec.ts** Rewrites the exclusivity test as genuine open-over-open transitions with no closed intermediate: thread over activity via a `messageId` deep link, activity over thread via the thread composer's activity bar (the one ingress reachable while the channel is inert). Asserts the replaced param is cleared and exactly one overlay exists. </details> Note on the e2e focus assertions: they are positive checks, not the regression guard. Because the covered channel is `inert`, a wrongly-restored focus is silently refused and lands on `<body>` rather than visibly stealing focus — mutating the guard to a no-op still passes e2e. The discriminating assertions therefore live in `CoverDrawerFocusHandoff.test.mjs`, where disabling the guard fails 3 of 4 tests while the plain-close test keeps passing. Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
371933130e |
feat(desktop): cover the channel with the agent activity drawer
**Category:** feat **User Impact:** At wide viewports, opening an agent's activity inside a channel now covers the channel content area in a right-anchored drawer instead of squeezing into a ~380px split pane, so tool calls, diffs, and command output are readable without resizing. **Problem:** The agent session panel shared the thread's split `RightAuxiliaryPane`, which is too narrow for a transcript. The focus drawer that solves this for threads was welded to thread-specific breadcrumb and view-mode-toggle concerns, so activity could not reuse it. **Solution:** Extract the drawer surface into a presentation-only `CoverDrawer` and give activity its own thin wrapper. Activity always covers at wide viewports and offers no focus/split toggle; narrow, overlay, and single-panel presentations are unchanged for both surfaces. A single resolved auxiliary surface makes the two drawers mutually exclusive by construction, so last-opened wins and two drawers can never stack. Thread drawer behavior is unchanged. <details> <summary>File changes</summary> **desktop/src/features/channels/ui/CoverDrawer.tsx** New presentation-only drawer extracted from `FocusThreadDrawer`: motion, scrim, focus capture/restore, and an `ownsEscape` opt-out for content that already handles Escape itself. **desktop/src/features/channels/ui/AgentActivityDrawer.tsx** Activity's wrapper. Delegates Escape to the panel so the settings menu dismisses first. **desktop/src/features/channels/ui/FocusThreadDrawer.tsx** Reduced to a `CoverDrawer` wrapper plus its thread-specific focus-restore rule. Behavior unchanged. **desktop/src/features/channels/lib/channelAuxiliarySurface.ts** Resolves the one auxiliary surface and which surface, if any, covers — the structural guarantee that only one drawer exists. **desktop/src/features/channels/lib/agentSessionPanelPresentation.ts** Maps presentation to the panel's layout props. Suppresses the panel's own enter motion inside the drawer to avoid a double slide. **desktop/src/features/channels/lib/coverDrawerLayout.ts** Sliver width and travel distance, moved off the thread-specific module. **desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx** Accepts and forwards `enterMotion` (additive), which `AuxiliaryPanel` already supported but the panel never threaded through. **desktop/src/features/channels/ui/ChannelPane.tsx** Renders branches off the resolved surface instead of an implicit fall-through chain, and wraps the agent panel in the cover drawer. **desktop/tests/e2e/agent-activity-cover.spec.ts** Covers covering vs. splitting, drawer exclusivity, suppressed panel motion, Escape/scrim dismissal, and unchanged narrow presentation. **desktop/tests/e2e/activity-scope-label-screenshots.spec.ts** Lengthens the long-name fixture so the header truncation assertion still clamps at the wider cover-drawer width. </details> Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
91a5754ad5 |
fix(desktop): keep a reader's work block open when a later answer regroups it
A reader who opened a finished work block to read its steps was folded shut by
the agent posting another message — an event they did not cause and could not
predict.
Work blocks are derived, not stored. `groupConversationWorkBlocks` rebuilds them
every render and ids each one after its first step, and `findFinalAnswerId`
exempts only the LAST assistant message from the block. So a second assistant
message demotes the first: the earlier answer becomes work, and the runs on
either side of it merge.
frame 2 work-block:th:1[th:1,tool:1] msg:1 work-block:th:2[th:2,tool:2]
frame 3 work-block:th:1[th:1,tool:1,msg:1,th:2,tool:2] msg:2
`work-block:th:2` stops existing, React unmounts it, and the `useState` holding
the reader's expansion goes with it. The block they were reading is still on
screen — it is just inside a different block now — so this is not a case where
the intent has nowhere to live.
The choice therefore moves above the block, keyed by the STEP ids it was taken
on rather than by block id, since the steps are what the reader's intent was
actually about and they survive regrouping. Two rules follow from the merge:
- **An open choice wins over a folded one.** A merged block can carry both, and
the two are not symmetric: showing steps a reader did not ask for costs them a
scroll, hiding steps they did ask for loses what they were reading.
- **A choice is recorded against every step in the block, not just the first.**
Recording only the first leaves the absorbed block's stale `open` entry behind,
and since open wins the read, the merged block could never be folded again —
the reader's click would do nothing. Caught by mutation, not by inspection; the
first version of the test passed against it, so it now asserts the fold-back.
`useWorkBlockDisclosureStore` returns `null` rather than a no-op store when no
transcript provides one, and the block falls back to local state. A no-op default
would make a block mounted on its own silently swallow every click — how most of
this component's tests mount it — and local state is the correct behaviour in
isolation, where nothing is regrouping anything. A provider emits no DOM, so
`default`/`compactPreview` markup stays byte-identical (the byte-for-byte
baseline test covers this).
Verified by mutation, each applied and reverted in isolation:
- bypass the shared store (i.e. the pre-fix behaviour) — the new test fails on
the merge frame, `0 !== 1` open blocks, which is exactly the reader-visible
symptom.
- record the choice against only the first step — fails the fold-back
assertion, `1 !== 0`.
The test drives the real `AgentSessionTranscriptList` across the three frames so
the actual regrouping runs, and asserts the steps are on screen rather than
trusting a block that reports itself open.
Full desktop suite 5492 passing / 0 failing, 81 suites; `tsc --noEmit` clean.
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
|
||
|
|
d02437981a |
test(desktop): pin the plain next-turn gap and stop the 5-minute suite hang
Two review corrections to the previous commit, neither changing production
behaviour.
**The gap test was pinning the wrong sequence.** Its middle frames put a
`session/new` card between `turn_started` and `session_resolved`. That card
renders as a trailing `single` block, which moves `streamingItemId` off the
previous turn's work block on its own — so those frames pass against the old
block-walking code and prove nothing. Measured over the revert mutation:
SEQ A f1 turn_started only liveTurnId=turn-1 tail=tool:2 BUG
SEQ A f2 +session_resolved, no session/new liveTurnId=turn-1 tail=tool:2 BUG
SEQ B f1 +session/new liveTurnId=turn-1 tail=null masked
SEQ B f2 +session/new +session_resolved liveTurnId=turn-1 tail=null masked
The plain sequence — no session restart, which is the overwhelmingly common
path — now leads the frame list, and the restart sequence is kept as a
separately labelled path that also has to settle. Reverting either half of the
fix now fails on frame 1 of the plain sequence rather than on a frame that a
restart card was carrying.
**A five-minute gc timer was being waited out on every run.** The orphaned
suite's own `QueryClient` took React Query's default 300000ms `gcTime`. The one
test that reaches the message-bubble presenter's `useQuery` leaves a query
uncollected, so a 300s timer is armed at teardown and node:test waits it out
before exiting: the file's tests sum to ~2s, the wall was ~303s, and every test
passed, so nothing pointed at the cause. `gcTime: 0` takes the standalone file
from 303s to 7s, and the full desktop suite from 307s to 102s.
The shared rig (`AgentSessionWorkBlockTestRig`) creates a byte-identical client
and gets the same line. It is dormant today — only the relay-bubble test drives
a `useQuery`, and that test builds its own client — but the failure mode if it
ever arms is a silent five-minute hang with no failing assertion, which is
expensive to diagnose and free to prevent.
Verified with a negative control rather than on the passing tree alone: with the
orphaned file restored to its previous blob (
|
||
|
|
eeb2f702ac |
fix(desktop): guard the work-block pulse and stop a finished block going live
Two reader-visible bugs in the focus-mode work block, both found reviewing this PR rather than by a test. **The running step's pulse ignored reduced motion.** `WorkBlockRailGlyph` applied bare `animate-pulse`. `useWorkBlockMotionEnabled` skips the fold's height animation, but it cannot reach a keyframe animation applied by a utility class, and none of the app's 20 `prefers-reduced-motion: reduce` blocks matches `.animate-pulse` — every one is scoped to a `buzz-*`/`motion-*`/`t-skel-*` class. So a reader who asked for no motion got an indefinite pulse. Now `motion-safe:animate-pulse`, which this repo's Tailwind (4.3.0) compiles to the same declaration wrapped in `@media (prefers-reduced-motion: no-preference)` — and which matches every other pulse in this feature (`AgentStatusBadge`, `ManagedAgentRow`). That swap also repaired three assertions it would otherwise have disarmed. `motion-safe:animate-pulse` is a different class TOKEN, not `animate-pulse` plus a modifier, so the suite's `.animate-pulse` selectors would have stopped matching anything and passed unconditionally. Worse, they were already vacuous: all three ran after `settleToStepCount(0)`, i.e. on a folded rail with no rows, so "nothing pulses" held against a build where abandoned steps pulse forever — the exact Codex finding this PR answers. They now assert on rows that exist (expanded, or mid-settle) and match the exact class token, so neither a substring nor a missing element can fake a pass. **A finished block went live again between turns.** `lastTurnId` walked the display blocks for the newest `turn` block, but a turn that has only emitted setup lifecycle rows (`turn_started`, `session_resolved`) classifies to zero segments and so produces no block at all. For the whole gap between `turn_started` and the next turn's first renderable item, "newest turn with a block" was therefore the turn that had already ENDED, and its own trailing item became the streaming item: a settled 6-step block re-opened, dropped to its last three steps behind a previous-steps disclosure, then folded back. `turn_started` fires on every turn and the gap is real observer-stream latency, so this was every turn, not an edge case. Liveness now comes from the newest turn id in the item stream, which is why `buildConversationTurnMeta` takes `items`. A trailing item is also only reported as streaming when the live turn owns it — the same ownership rule `toolEntryState` already applies to an `executing` status, for the same reason: without it the newest block's tail holds a finished turn's block open regardless of whose turn it is. Tests, each mutation-checked in isolation so none of them is vacuous: - pulse guarded, asserted under BOTH preference values (the class must not depend on it) — reverting to the bare class fails exactly that test; dropping the `running` guard fails the three retargeted negatives. - the meta gap contract over the real 4-frame sequence built from a raw item stream through the real grouping, since the bug is precisely which turns do and do not produce a block — reverting either half of the fix fails it. - the rendered consequence with a 6-step block (above the live window, where the symptom is loudest): folded summary and no rail across the gap. Also fixed a fixture that named the wrong thing: "no turn at all" carried the shared helper's default `turnId: "turn-1"`, so it was asserting that a turn with no block reports no live turn — the opposite of the rule. Full desktop suite 5491 passing / 0 failing, 81 suites; `tsc --noEmit` clean. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
036bd38c3b |
test(desktop): split the work-block suite and prune the conversation harness
`AgentSessionWorkBlock.test.mjs` landed at 1,146 lines. Under today's rule tables that passes, but only because the desktop ratchet's script roots allowlist `.ts`/`.tsx` and silently skip `.mjs` — the gap #6736 (`ce1f1d427`) closes. Measured rather than assumed: with that rule table cherry-picked, this file is the one remaining violation on C, and because `allowedLineCount` grandfathers a base that already exceeds the max, whatever count C lands with becomes the file's permanent ceiling on main. 1,146 is not a ceiling worth inheriting for a file that exists to cover a single component. Split on the seam the file already had, at `// -- Orphaned work --`, where the subject changes from live-vs-finished *policy* to what an individual *row* is: - `AgentSessionWorkBlockTestRig.mjs` (311) — jsdom lifecycle, the item fixtures, `settle()`, `renderBlock()`. - `AgentSessionWorkBlock.test.mjs` (352) — Live, Finished, fold animation, reader choice, rail glyph states. - `AgentSessionWorkBlock.orphaned.test.mjs` (528) — orphaned work, the per-kind rail presentation, streaming re-render cost. One rig, not a copy per file. The two suites run in separate processes (node's runner is one process per file), so a second copy of the jsdom and `matchMedia` setup could not *collide* — it would drift, and a drifted ambient pin fails a fixture for a reason that has nothing to do with the markup under test. That trap already cost this suite family two commits (`2b7b0baf6`, `c91819706`). The split forces one real behavioural change. `prefersReducedMotion` was a module-level `let` the reduced-motion test assigned directly; ESM bindings are read-only in importers, so once it lives in the rig that assignment cannot work. It goes through `setPrefersReducedMotion(value)`, with the rig's `afterEach` still resetting it. Proved non-vacuous by stubbing the setter to a no-op: that fails exactly one test — the reduced-motion one — and restoring it passes. Without that check the test would have been asserting against a flag nothing sets, which is precisely the way a split can silently disarm a test. Behaviour is otherwise preserved, checked rather than assumed: the 30 test titles across the two files are an exact set match with the 30 in the single file, and every body is byte-identical apart from the setter call. Harness prune, in the same revision because it is the same debt: with the four `<details>` disclosure tests gone, `export const domWindow = dom.window` had no consumer anywhere in `desktop/src` — it existed only for `new domWindow.Event("toggle")`. Dropping just the `export` leaves an unused local that Biome flags (`lint/correctness/noUnusedVariables`), so the declaration and its now-false doc comment both go (576 -> 571). `TRIGGER_TITLE` only loses its `export`; it is still used inside the harness. Ratchet with #6736's rules cherry-picked, run in the same shell as `git rev-parse HEAD`: - base `fd2e01799` (the current PR base, which is what CI resolves — `resolveBaseRef` returns `HEAD^1` under `GITHUB_ACTIONS`): exit 0. - base `merge-base(origin/main)` = `db5617dd1`, which is what it resolves to locally and would resolve to if C retargets: exit 0. - negative control at the pre-split tree, same rules, same base: exit 1, `AgentSessionWorkBlock.test.mjs: new -> 1146 lines (allowed 1000)`. The gate is measuring the thing this commit fixes. Both suites green together, 30/30. `conversation` + `conversationChrome` still green. `tsc --noEmit` clean, `pnpm check` findings identical to main's baseline. The split shape, the seam and the reduced-motion proof are dev-01's, handed over as a verified patch (`OUTBOX/C_WORKBLOCK_SPLIT_HARNESS_PRUNE.patch`, sha256 6cfbccc9…) rather than as a commit in this checkout. I read the seam, reconstructed the tree from `fe57b7ac7` + that patch to confirm it is exactly what was proposed, and re-ran the title/body comparison, the setter mutant, both ratchet bases and the negative control here. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
fe57b7ac7b |
fix(desktop): settle a work block whose running step has no live session
A tool item's `executing`/`pending` status is written when the step starts and never revised if the agent dies first, so an abandoned step keeps it forever. On the activity feed that was a stale row label. In the work block this commit introduces it is a MODE: one `running` entry makes `summarizeWorkBlock` report `isActive`, which suppresses the folded summary line, holds the rail open and pulses a bullet. Scrolling back to a crashed turn therefore showed the reader live work indefinitely (Codex on #6536, discussion_r3848214880 — not stale, and worse here than on the card it was filed against). Status alone cannot answer "is this step happening?", so the block is given the missing half: `AgentSessionTranscriptTurnMeta.liveTurnId`, published by the list from the same display blocks it already reads for `streamingItemId`. `toolEntryState` treats `executing`/`pending` as `running` only when a session owns that step's turn. A turn id, not a boolean. "Some turn is live" is not the question: an agent that crashed during turn 1 and is now working on turn 2 IS live, yet turn 1's abandoned step is no more running than before — a global flag would keep it spinning in exactly the case a restarted agent makes common. The comparison is explicit (`liveTurnId !== null && ...`) so an item with no turn id cannot match a null live turn by `null === null`. An abandoned step is reported as `settled`, not as a new state and not as `failed`, per core-02: - We do not know it failed — only that nobody finished it — so it must not count toward the folded line's `N failed`. It folds to a neutral `N steps`. - No third entry state and no new glyph. It renders as the neutral step it is, with the same muted detail the list already shows for it outside a block. A visible "interrupted" marker would be a design addition, not a bug fix. `liveTurnId` is read from the display blocks rather than from the active-turn store, because the store's turn ids and the transcript's are populated by different paths and a mismatch would silently gate every step off — the mirror image of this bug. It is the last *turn* block, not the last block: a compaction notice arrives as a `single` block after the turn it belongs to, so reading the final block's kind would report no live turn at all. `projectWorkBlockEntries` takes the option as a required argument rather than defaulting it, so a future caller that has not thought about liveness cannot silently get the spins-forever behaviour back. Non-vacuous by mutation, each mutant run in isolation against the unit suite: - gate removed (always `running`): 4 failures. - gate inverted (`executing` never `running`): 5 — this is the fix that would have "passed" the bug report while breaking live work. - truthiness instead of turn ownership (any live turn resurrects an old abandoned step): 1. - `item.turnId === liveTurnId` without the null guard: 1. - abandoned step reported `failed` instead of `settled`: 4. - `liveTurnId` forced null in the meta builder: 2. - `lastTurnId` reading only the final block: 1. Measured in a real browser as well, not only JSDOM, because the failure mode is presentational and animation-dependent — a unit test can assert class names while the rendered rail is still wrong (the interim-note and relay-send bugs on this branch were both invisible to the unit suite). Two seeded scenarios in the preview harness, driving the real component through the cover drawer under Buzz Dark: - agent panic mid-step (the terminal `crates/buzz-acp` actually emits): folded label `3 steps`, rail collapses to 0 rows, `getAnimations` reports 0 infinite animations in the block, and opened the rail reads `["settled","settled","settled"]`. - the same events with no panic: no folded line, rail open, states `["settled","settled","running"]`, and exactly 1 infinite animation — the running bullet really does pulse. Both browser scenarios were mutation-checked too: removing the gate fails the first and passes the second, inverting it fails the second and passes the first, so neither can be satisfied by a one-sided fix. (Those two specs are not in this commit — they need Slice A's `conversation` variant pin to be reachable, which is not on this branch.) The row outside a block is untouched: `buildCompactToolSummary` still derives its own `running` from status, and no file under `activityRenderClasses/` or `agentSessionToolSummary.ts` is modified here. Verified at this tree: full desktop suite 5486 passing / 0 failing, `tsc --noEmit` clean, `pnpm check` findings identical to main's baseline (2 warnings + 2 infos, all pre-existing). Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
ac794843a6 |
feat(desktop): fold focus-mode agent work into one transcript block
Focus mode gave every thought and tool step its own row and its own
disclosure, so a turn that did real work scrolled its answer off screen
behind a stack of chrome. This adopts berd's shipping transcript model:
within a turn, everything between the prompt and the answer that is
thinking, a tool step, or an interim agent note becomes one "work block"
— a thin rail of small glyph bullets that folds to a single "N steps"
line when the work is done.
Grouping is variant-aware AT THE LIST BOUNDARY rather than inside the
grouping module. `TranscriptDisplayBlockView` runs the additive
work-block transform only for `conversation`, so `default` and
`compactPreview` walk main's segments on the identical code path they
always did. That makes Slice B's byte-for-byte baseline fixture hold BY
CONSTRUCTION rather than by assertion, which is the property worth having
here: no future edit to the block can regress the other two variants
without first moving this branch.
Behaviour, mirroring berd:
- Live: the block is open and the rail IS the status, so there is no
header line to restate it. The last three steps show in true arrival
order; older ones go behind an "N previous steps" disclosure at the top
of the rail, so a long run cannot push the answer out of view.
- Finished: folds to "N steps" with a chevron that rotates on open.
- The fold ANIMATES. `<details>` cannot do this — its content is either
laid out or not, with no intermediate height — so the collapse would
snap. A block that was live when it mounted renders open for a paint
and then settles closed, giving the height tween a start state that was
actually painted; a block already finished on mount (scrollback) never
had a rail on screen and closes immediately, so the animation stays
meaningful instead of firing on every mount.
- Reader choice wins: once the reader toggles a block, policy stops
opening and closing it.
One deliberate departure from berd, per core-02: a finished block holding
a failure folds to "N steps · 1 failed". A bare count is the one thing a
reader cannot distinguish a clean run from a broken one by, and a fold
that hides a failure behind a neutral number invites them not to open it.
The rail bullet itself stays muted, per berd — failure is a glyph shape,
not a colour, so one bad step does not read as an alarm across the run.
Items are projected ONCE into rail entries, and the glyph, the body and
the folded line's counts all read that projection. Two independent
classifications of the same item is precisely how the headline and the
chain eligibility drifted apart on the abandoned tool-chain card, so both
render sites are exhaustive switches over the entry and neither asks
`item.type` again.
The projection is closed in the TYPE SYSTEM, not only by convention. An
earlier revision made the entry a product of independent fields
(`{ item, kind, state }`) with a catch-all `return "tool"`, which left two
things wrong that no test could see: a future `TranscriptItem` variant
would silently wear a wrench, and impossible pairs like
`{ kind: "note", item: <thought> }` stayed representable — so the body
switch still had to re-check `item.type` and render `""` on a mismatch it
could not otherwise handle. Meaning was therefore still derived in two
places.
Now:
- `WorkBlockItem` is the closed union of items a block admits, and
`isWorkItem` is a type guard, so the membership decision is made once
and every later stage receives the narrowed type. `admittedWorkItems`
returns the narrowed array rather than a boolean because an `.every()`
guard cannot narrow the array it tested.
- `WorkBlockEntry` is a discriminated union pairing each kind with its own
item type, and fixing `state: "settled"` on the prose kinds. Both classes
of impossible entry are now unrepresentable rather than defended against.
- `projectWorkBlockEntry` switches exhaustively over `WorkBlockItem` with
no default, so admitting a new item type without deciding how it renders
is a compile error (`TS2366: Function lacks ending return statement`),
not a wrench.
- The body switch takes the whole entry, so narrowing on `kind` narrows
`item` too. The `item.type === "thought" ? item.text : ""` fallbacks are
gone because there is no longer a mismatch to fall back from.
Verified by compiling three mutants, each of which now fails `tsc` where
before it type-checked: admitting `plan` to `WorkBlockItem` without a
projection case (TS2366), projecting a thought as a note (TS2322 on
`item`), and giving a thought `state: "failed"` (TS2322 on `state`). The
runtime kind/item pairing is also asserted in
`agentSessionWorkBlockGrouping.test.mjs`, because types are stripped at
runtime and swapping the two prose branches by hand is the easy mistake —
that mutant fails the test.
The projection also fixes an ordering bug the old split invited: a tool
carrying a stale `isError` from a retry while the new attempt executes
reads as `running`, not `failed`, so a live block cannot fold its own
count to "N steps · 1 failed" while the work is still in flight.
**Interim notes suppress the identity row.** #6720 gives every
conversation-variant assistant message a 20px avatar + name row, which is
right for the turn's answer. A rail note is the same item type, so
routing it through that presenter would render a fully attributed agent
turn nested inside a muted step row — the agent apparently replying twice,
once inside the work it was doing. Notes render through a dedicated rail
prose body instead, keeping markdown and the focus code-block recipe by
providing the same `CodeBlockVariantContext` value the presenter would.
Done on this side rather than by reaching into #6720, so that PR keeps one
rule for what a message looks like. Notes share the thought's speech
bubble, matching berd's `progress` entry: both are the agent talking.
- `useControlledDisclosure` is deleted rather than reused. Its entire
reason to exist was the `<details>` echo trap — `<details>` fires
`toggle` for programmatic `open` changes indistinguishably from clicks,
so a policy-driven open echoes back looking like reader intent. This
block's trigger is a `<button>`, where the only thing that can call the
handler is a real click. Keeping the guard would have been dead code
masquerading as load-bearing. Nothing else in the tree imported it.
- berd brightens rail prose with `usePrimaryText={open}`; here the
brightening is unconditional. A closed block unmounts its rows rather
than dimming them, so there is no state in which rail prose is on
screen and not in an open block — the flag's false branch would be
unreachable. A test records that reasoning so the divergence is not
mistaken for an oversight.
dev-01 flagged that `turnSegmentItems` does not know `work-block`, so a
thought followed only by block content would never settle its duration.
Tracing it: the meta is built from pre-transform display blocks, so no
`work-block` segment can reach that function — the reported bug cannot
fire. But the trace turned up something worse. `ConversationThought` was
the only reader of `thoughtDurationSecondsById` and
`formatThoughtDisclosureLabel`, and this commit deletes it, leaving ~100
lines of production code and ~200 lines of tests that only tested each
other. Fixing a bug in code with no reader would have preserved the
illusion that focus mode still shows "Thought for Ns" somewhere.
So the duration map, its label formatter and `elapsedSeconds` are gone.
`AgentSessionTranscriptTurnMeta` narrows to the one field that still has
a consumer: `streamingItemId`, which the work block needs because a
thought or note streaming in carries no status of its own. Its tests are
rewritten around what that hint must actually get right — the tail of a
live turn, skipping setup, expanding a summary segment to its last leaf,
and reporting nothing at all when the turn is idle.
Three further notes on translation rather than transcription:
- berd's bullet masks the spine with `bg-card` and its BOT-1599 note
warns off `bg-background`. The rule is "mask with the surface the
transcript is drawn on"; in Buzz that surface is the cover drawer,
which is literally `bg-background`. The two tokens are NOT
interchangeable here: `[data-buzz-content-surface]` locally overrides
`--background` to `--buzz-content-dark` while `--card` keeps the theme
value, so in Buzz Dark `bg-card` paints the bullet rgb(36,41,46) over an
rgb(26,26,26) drawer — the exact BOT-1599 failure. Light mode matches
under either class, so light-mode evidence alone would not have caught
it. Copying the class would have followed the letter of berd's note
against its point.
- Reduced motion is read via `matchMedia` — the way `TerminalSubstrate`
reads it — not motion's `useReducedMotion`, which resolves the query
once per process and caches it. That cache made the preference
untestable (the assertion turned on module load order, not on the
setting) and ignored a mid-session change.
- The memo takes the entry SPREAD into props, not the entry object, and
its boundary is the step BODY rather than the whole row. The projection
is rebuilt whenever the item array changes, so entry objects are fresh
on every append and a memo keyed on one would never hit; spread, the
compared props are `item` (reference-stable) plus two strings. Spreading
also keeps the union intact, so the body switch still narrows `item`
from `kind`. The row stays outside because the glyph depends on
`isLast`, which changes for the previous last row on every append.
`ConversationThought` is deleted here rather than in dev-01's restyle,
per core-02's sequencing ruling: this is the commit that replaces it, so
reasoning stays visible in focus mode at every commit. Its four
disclosure-keyed tests are deleted rather than adapted — they assert a
`<details>` that no longer exists on that path. `default`/
`compactPreview` thought rendering is untouched.
Non-vacuous by mutation testing, each mutant run in isolation:
- note falls through to the tool kind: 4 failures.
- note routed through the message presenter: 2. Note given the wrench: 1.
- `entryState` checking failure before running: 1.
- rail bullet tinted on failure: 1. Prose muted instead of primary: 1.
- memo keyed on the projected entry object: 2 — this is the bug the
projection actually introduced, caught by the pre-existing streaming
cost tests before it shipped.
- streaming hint forced to null: 4. Summary segments not expanded when
finding the tail: 1.
- windowing keyed off `open` instead of the reader's choice: 3. Policy
already holds live blocks open, so this switches windowing off in
exactly the case it exists for.
- reduced-motion preference forced false: 1.
- rail bubble suppression removed (the bug above): the relay-step test
stops passing.
An earlier mutant also caught a test lying: the block-level "echo" test
passed with the guard removed, because the trigger is a `<button>` and
nothing was listening for `toggle` at all. It now asserts the structural
reason the trap cannot apply plus a repeated fold→reopen→fold cycle,
which is what a recorded echo would actually have disabled.
Relay posts are plain rail steps. A `buzz messages send` step classifies
as `renderClass: "message"`, so it renders through `CompactMessageSummary`
— 28px avatar, bordered speech bubble, timestamp, delivery-receipt
button. Correct in the activity feed, where a posted message is a
destination to open; on a muted rail it makes the agent appear to reply
in the middle of its own work. This is the same failure the interim-note
case avoids, reached by a different route: there the item IS an assistant
message, here it is a tool call that merely classifies as one. Caught by
looking at the seeded browser preview, not by a unit test — the JSDOM
suite was green while the rendered rail was wrong.
Suppressed with an explicit presentation signal
(`useIsInsideWorkBlockRail`, default false) rather than by reading the
transcript variant, because `conversation` alone is not the condition:
the same relay step rendered OUTSIDE a block in that variant should keep
its bubble. Both halves of that branch are now pinned by tests, so
suppressing the bubble everywhere cannot pass. Defaulting to false keeps
`default`/`compactPreview` markup byte-identical.
Rebased onto #6720 at `e8709554a` per core-02's B → #6720 → C ordering.
Verified at this tree: full desktop suite 5472 passing / 0 failing,
`tsc --noEmit` clean, `pnpm check` findings identical to main's baseline
(2 warnings + 2 infos, all pre-existing), px-text/pubkey-truncation/
file-size gates clean.
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
|
||
|
|
fd2e017998 |
fix(desktop): hide the decorative identity avatar and split the conversation suite
Two review findings on #6720. **P2 (Carl, CHANGES_REQUESTED): the identity row announced the agent twice.** `UserAvatar` names itself — an `<img alt="${displayName} avatar">` or its fallback initials — and the row puts the same name in visible text immediately after it, so assistive tech read the identity twice for every agent turn in the transcript. Wrap the avatar in an `aria-hidden` span so the visible name is the row's single accessible identity. Marked at this call site rather than by teaching the shared `UserAvatar` a decorative mode: other rows pair an avatar with adjacent name text and would want the same treatment, but changing the shared component's accessible name affects all 45 of its call sites and is not this PR's scope. Verified in Chromium's real accessibility tree over CDP, not from the DOM attributes, through the channel-opened flow: pre-fix role "image", name "Observer Agent avatar", ignored false post-fix ignored true, ignoredReasons ["ariaHiddenSubtree"] Geometry is unchanged across the two (row 114x20, avatar 20x20, label 90x16, 4px gap), so moving `shrink-0` onto the wrapper is not a visual change. The new test is non-vacuous by two mutations: removing the decorative marking fails it and only it, and re-adding a name-bearing `aria-label` outside the hidden subtree fails its escape assertion with the offending element named. **P1 (Codex): the conversation test file exceeded the 1000-line ceiling.** It reached 1253 lines. It slipped past `file-size-check` because that script only scans `.ts`/`.tsx` under `src/features`, so the gate I reported as green never covered this file — the rule in AGENTS.md plainly does. Split into: AgentSessionTranscriptList.conversationHarness.mjs 574 (shared) AgentSessionTranscriptList.conversation.test.mjs 521 AgentSessionTranscriptList.conversationChrome.test.mjs 274 The shared jsdom setup, order-sensitive TZ/locale pins, fixtures and render helpers live in a non-test harness module, following the existing `observedUnreadTestHarness.mjs` precedent — duplicating those pins into two files is exactly how they drift. Test bodies are moved verbatim; a name-by-name diff against the previous head confirms zero tests lost and exactly one added. Gate script untouched, per the routing of its `.mjs` coverage gap to a separate follow-up. Full desktop suite 5432 passing / 0 failing, including the byte-for-byte `default`/`compactPreview` fixture. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
e8709554a3 |
fix(desktop): resolve the focus identity row agent through the profiles lookup
The conversation variant's identity row read the raw `agentAvatarUrl` and `agentName` props. The primary channel-opened flow passes `agentAvatarUrl: null`, because `ChannelAgentSessionAgent` (useChannelAgentSessions.ts:21-29) carries no avatar field at all — so every channel-opened focus session showed initials in the identity row while the panel header directly above it showed the real profile avatar. The two disagreed about the same agent on the same surface. Resolve profile-first out of the `profiles` lookup the panel already hands down, exactly as `ToolItem` (ToolItem.tsx:45-52) and the panel header (AgentSessionThreadPanel.tsx:243-249) already do. The props stay as the fallback rather than being replaced: a locally managed agent can hold an avatar its relay profile never published. Found by ss-bugs-02's bug pass on #6720. Three tests: the profile avatar wins over a null prop (the channel path), the profile display name wins over a stale prop name, and the caller's avatar survives when the lookup has none. Proven non-vacuous by three mutations — reverting the resolution fails exactly the two resolution tests, dropping the `?? agentAvatarUrl` fallback fails only the fallback test, and removing the `LoadedImageStub` fails both avatar tests. That stub is load-bearing, not masking: Radix `AvatarImage` renders nothing until its own preloader reports `loaded`, and jsdom never fetches, so both avatar assertions would otherwise have passed vacuously against the initials fallback — the very bug under test. It only affects avatars that have a url, so the byte-for-byte `default`/`compactPreview` baseline fixture stays byte-identical. Verified empirically through the real channel-opened flow in a Chromium build, not just jsdom. Pre-fix the row rendered `hasImg: false` with an "OA" initials fallback while the header showed the real image; post-fix it renders the decoded `<img>` and the resolved name. That probe harness is untracked and not in this commit. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
4818d1cbbb |
fix(desktop): apply the focus code recipe to fenced human prompts too
Quality review caught a real bug in the previous commit. The `CodeBlockVariantContext` provider was mounted inside `MessageActivity`, which only handles assistant items — `UserMessageBubble` returns before it. A fence inside a human prompt therefore kept the legacy 16px muted frame, nested inside the new 12px prompt bubble. The recipe is a property of the *surface*, not of a role: a fenced block in a prompt should read the same as one in a reply. The provider moves up to `AgentSessionTranscriptList`, alongside the variant and turn-meta providers, so every descendant markdown render inherits it. It still renders no DOM, so `default`/`compactPreview` markup stays byte-identical and the baseline fixture is unchanged. Added a fenced-prompt test, which the existing fenced test could not catch because it only rendered an assistant item. Proven non-vacuous two ways: pinning the boundary provider to `default` fails both fenced tests, and re-adding a role-scoped `default` provider around only the user bubble's markdown fails the new prompt test alone. Also renamed `channel-message code blocks are untouched by the focus recipe` to `the default transcript variant keeps the legacy code chrome`. It renders the `default` transcript variant, not a channel message row, so the old name claimed a contract it did not prove. AGENTS.md gains the `assert.equal(el, null)` note: that form serializes the matched element's whole subtree to build a failure diff and OOMs the runner instead of printing the message, which made a genuine failure unreadable. Use `assert.ok(x === null)`. Verified at this tree: full desktop suite 5428 passing / 0 failing, `tsc --noEmit` clean, `biome check` at main's baseline, px-text / pubkey-truncation / file-size clean. Screenshots captured light and dark by temporarily pinning `transcriptVariant="conversation"` in `AgentSessionThreadPanel`; that pin is dev-00's slice and is not in this commit. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
f36b699220 |
style(desktop): bring the conversation variant closer to berd's recipes
dev-00's design-delta note measured the conversation variant against berd's shipping chat surface and named three divergences that make the focus view read as "not berd" regardless of how the rest is arranged. This takes all three literally, on Buzz tokens rather than berd's. **Agent identity row.** The largest gap was that agent prose carried no attribution at all, so a reply read as unowned body text in a full-cover view. berd labels every agent turn with a 20px round avatar plus the name at `text-xs`, `mb-0.5`, `gap-1` (`MessageBubble.tsx:961-981`). Added for the `conversation` variant only — the other variants' markup is pinned byte-for-byte by the baseline fixture, and a test asserts the row does not leak into them so a failure names the cause instead of printing a markup diff. **Prompt bubble.** berd's user turn is a soft fill with no border at all, `px-4 py-2`, and a 12px radius (`MessageBubble.tsx:990`). berd's `rounded-sm` is 12px on its own scale (`globals.css --radius-sm: 12px`), not Tailwind's stock 2px; Buzz's `rounded-xl` is the exact equivalent, so the radius drops from 16px rather than collapsing to a hairline. The cap moves from `max-w-[85%]` to a fixed 640px measure mirroring berd's `--chat-user-message-max-width` (`globals.css:615`): a percentage cap re-wraps the prompt on every resize of the cover, while a fixed measure holds one stable line length. **Fenced code.** berd puts the language in a real header row above the frame with the copy action opposite it, and frames the code at a 10px radius on the page background behind a subtle border, with no shadow (`ai-elements/code-block.tsx:379`, `:395`, `:528-529`). Buzz's `rounded-lg` is `--radius: 0.625rem` — exactly berd's `rounded-[0.625rem]`. The markdown renderer is shared with channel messages, so this recipe is opt-in: `CodeBlockVariantContext` is read at render time by `MarkdownCodeBlock` and provided by `MessageActivity`. A prop would have to thread through `createMarkdownComponents`, whose component map must stay module-stable and whose parsed-node cache keys on a variant string — the same reason `VideoReviewMarkdownContext` already exists. A context provider renders no DOM, so `MessageActivity` can wrap unconditionally and `default`/`compactPreview` markup stays byte-identical. A companion test renders the same fenced block through `default` and asserts the original chrome (16px radius, muted fill, `pr-12`, absolutely-positioned copy button) is untouched. Fenced blocks need `ThemeProvider` and `TooltipProvider` to mount, so those tests use a separate `renderTranscriptWithCodeChrome` helper; the byte-for-byte fixture keeps rendering through the exact tree it was captured with. Both new code-block tests were proven non-vacuous by mutation: forcing the provider to `default` everywhere fails only the focus-header test, and forcing `focusProse` everywhere fails only the channel-message guard. That guard is written as `assert.ok(x === null)` rather than `assert.equal(x, null)` — on failure the latter serializes the matched jsdom element and its subtree to build a diff, which exhausts memory instead of printing the message. Verified at this tree: full desktop suite 5427 passing / 0 failing, `tsc --noEmit` clean, `biome check` at main's baseline, px-text / pubkey-truncation / file-size checks clean. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
5e3faf443a |
fix(desktop): never show a trigger title as the prompt author
The conversation variant's author row resolved its name through
`resolveUserLabel({ fallbackName: item.title })`, whose last fallback is
the prompt item's title. That title describes the *trigger* that started
the turn — "@Mention", "Prompt", "Buzz event", or any title-cased Buzz
event kind — and is never an identity. On `default`/`compactPreview` it
only seeds avatar initials, so it was harmless; the conversation author
row promotes it to displayed text, so an unresolved sender was rendered
as if "@Mention" were their name.
Identity resolution for the displayed author now stops at the profile:
display name, then NIP-05 handle, then the truncated pubkey. A truncated
pubkey is terse but true, and it keeps the utterance attributed in a
full-cover view where an unlabelled bubble would read as orphaned. The
title stays available as trigger chrome in the footer.
The avatar seed keeps the old chain verbatim, so `default` and
`compactPreview` markup is untouched — the byte-for-byte fixture still
passes unchanged.
The existing author-row test was asserting the bug: its expectation came
from `item.title`, with no profile in the lookup. It now supplies a
profile and asserts the resolved display name, and the fixture's prompt
title is the realistic "@Mention" rather than a person's name so no test
can silently depend on a title reading as an identity again. Two new
tests cover the unresolved sender (truncated pubkey, and an explicit
assertion that no trigger word appears) and NIP-05-only resolution.
Non-vacuous: reverting the fix fails the unresolved-sender test — and
only that test — with `actual: '@Mention'`.
Verified at this tree: full desktop suite 5381 passing / 0 failing,
typecheck clean, `pnpm check` findings identical to main's baseline,
file-size ratchet clean. Transcript file also green under UTC/de-DE and
Asia/Tokyo/ja-JP.
Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
|
||
|
|
c91819706e |
fix(desktop): pin the transcript baseline fixture's locale too
Pinning TZ fixed one ambient input to the captured-markup fixture and missed a second one. The session-boundary divider formats its date with a bare `toLocaleString()` (`AgentSessionTranscriptChrome.tsx`), which is locale-sensitive as well as zone-sensitive: the fixture holds "6/14/2026, 7:05:00 PM", and under `LANG=de-DE` the same render produces "14.6.2026, 19:05:00". Node takes its default locale from LANG/LC_ALL, so the byte-for-byte comparison failed on any machine with a non-US locale — the same class of bug as the timezone one, one variable over, and the TZ guard passed happily while it happened. Assigning `process.env.LANG` at runtime does not fix this: node resolves its default locale once at startup, so unlike `TZ` (which `Date` reads lazily) the assignment is silently ineffective. Pinning the locale means overriding the two formatting surfaces the render path reaches — `Intl.DateTimeFormat` constructed without an explicit locale, and `Date.prototype.toLocale*`, which does not route through `Intl.DateTimeFormat` and needs its own patch. `Intl.DateTimeFormat` must be replaced with a plain function, not an arrow: the render path calls it with `new`, and an arrow function is not a constructor. The guard assertion now covers locale and `toLocaleString` output alongside the zone, so a future drift fails with a specific message instead of a 12KB markup diff. Verified: the de-DE failure reproduces before this change, and the suite passes after it under TZ/LANG combinations UTC/en-US, UTC/de-DE, America/New_York/ja-JP, Asia/Tokyo/de-DE, and UTC with LANG unset. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
2b7b0baf68 |
fix(desktop): pin the transcript baseline fixture to UTC
The captured-markup fixture embeds formatted timestamps, because `formatTranscriptTimestampTitle` renders in the ambient zone. It was captured at UTC-7, so the byte-for-byte comparison failed in CI (UTC) for a reason that had nothing to do with markup: "at 12:00:01 PM" vs "at 7:00:01 PM". This is why Desktop Core failed on the previous two heads. Pin `process.env.TZ = "UTC"` before the transcript modules are imported — their `Intl.DateTimeFormat` instances are module-level constants that resolve the zone once at construction — and recapture the baseline from pre-change main under that zone. A guard assertion in the coverage test now fails with a specific message if the ambient zone is not UTC, rather than dumping a 12KB markup diff. Verified passing under TZ=UTC, TZ=America/New_York, and TZ=Asia/Tokyo. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
63c928fc8c |
fix(desktop): address review on the conversation transcript variant
Review follow-ups on the `conversation` transcript variant. Variant selection is now caller-pinned only. The 640px width heuristic and its module are gone: an automatic wide-pane mode is a separate product decision, and swapping the whole transcript presentation as a reader drags a resize handle across a threshold is not a decision to make implicitly. Slice A's cover host pins `conversation` explicitly. The thought disclosure no longer records the browser's programmatic toggle echo as a reader choice. `<details>` fires `toggle` when React changes `open`, so auto-opening a streaming thought was pinning it open forever and reasoning never receded once the agent acted. Only a toggle that disagrees with the last rendered state can have come from the reader. The regression test injects the echo and then rerenders with a following item, and it is the only test that fails when the guard is removed; a second test proves a genuine reader toggle still wins. Thought completion at the next leaf item of any kind — tool call or plan update, not just the next assistant message — is now documented as intended rather than incidental, with a thought -> tool -> message test pinning it. The byte-for-byte fixture now covers the contract it claims. The baseline transcript contains every renderable item kind (prompt with context and setup, assistant message, thought, plan, tool, lifecycle status, error, permission) across two sessions so a session boundary is forced, and both legacy variants were recaptured from pre-change main. A companion test asserts the baseline really renders each kind, so a fixture that silently stopped covering one cannot keep passing while protecting nothing. Also adds the plan-in-place rerender assertion: same plan id with advanced entries retains the same card node while the progress text changes. Known follow-up: Slice C is not merged yet, so its shared controlled-details hook does not exist on origin and the echo guard here is a local copy with the same semantics, flagged in a comment for replacement on rebase. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
7cec752d7a |
feat(desktop): add conversation transcript variant for focus-mode agent sessions
**Category:** feature **User Impact:** In the wide/standalone agent session panel the observer transcript now reads like a conversation — the human's prompt as a right-aligned bubble, the agent's reply as unboxed prose at a comfortable measure, reasoning behind a "Thinking…/Thought for Ns" disclosure, the plan as a live checklist, and session/status noise receded to quiet dividers. **Problem:** The transcript had two presentations — the dense `default` activity feed and `compactPreview` — both tuned for a narrow panel. Read at full width they are a scan surface, not something you read a turn in. **Solution:** A third, purely additive `conversation` variant, threaded through `ManagedAgentSessionPanel` and `AgentSessionThreadPanel` as an optional prop with a width-based default. Tool items deliberately route through the same `ToolActivity` presenter as `default` so the parallel tool-chain-card work merges cleanly, and the drawer/shell is untouched. The byte-for-byte guarantee for the existing variants is enforced by a captured-markup fixture, not by inspection: the baseline was produced by mounting the same transcript on unmodified code, and the test fails on any drift in `default` or `compactPreview` output. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com> |
||
|
|
db5617dd15 |
fix(desktop): emit singular mention feed category so alerts route correctly (#6665)
## Problem Mentions — and thread replies that @-mention you — played the **Needs action** sound instead of the **@Mentions** sound. Reported by @morgmart: "I set Needs action to a different sound and it's the only one I ever hear." ## Root cause Two vocabularies got conflated in #475: - **Section / filter vocabulary** (plural): `mentions`, `needs_action`, `activity`, `agent_activity` — the shape of `FeedSections`, the `--types` filter, and the agent-facing CLI docs. - **Per-item category vocabulary** (singular for mention): `mention`, `needs_action`, `activity`, `agent_activity` — the `FeedItemCategory` contract in `desktop/src/shared/api/types.ts`, unchanged since #12. The Tauri feed builder reused the filter string `"mentions"` as each mention item's `category`. Only one word differs between the vocabularies, so only mentions broke. Every frontend consumer compares against the singular, so real mentions never matched and fell through to the resolver's `needs_action` fallback. The E2E mock bridge emits the singular form, so tests never saw the drift. ### Symptoms this fixes (all from the one mislabel) - Mentions and mentioning thread replies played the Needs-action sound - Mention notifications used the Needs-action title format - Mentions in muted channels were suppressed (the mute-bypass never fired) - Inbox / Home feed labelled mentions "Channel update" - Channel activity popover's mentions list was always empty ## Fix **Fix the owner, not the symptoms.** `FeedItemInfo.category` becomes a `FeedItemCategory` enum whose serde form is exactly the TS union, so a misspelled category can't compile at the producer. A serialization test pins each variant to its wire string. **Frontend:** `slotForFeedKind` maps every known category explicitly. The `needs_action` fallback for unknown categories is **kept on purpose** — a contract drift should cost the user the wrong sound, not a missed alert — but it now `console.warn`s so the drift is visible to developers instead of masquerading as intended behavior. `e2eBridge.ts` and `tauri.ts` now derive the category type from `types.ts` instead of retyping it. Not touched: the plural `--types` filter and `FeedSections` keys. Those are the section vocabulary and are correct as-is. ## Verification - `just ci` green (file-size ratchet, Rust/Tauri/desktop/mobile tests, desktop + web builds) - New tests: 2 Rust (`feed_item_category_serializes_to_frontend_contract`, `feed_item_from_event_carries_singular_mention_category`), 3 TS in `sound.test.mjs` incl. one that feeds the old `"mentions"` string and asserts fallback + warning - **Runtime, dev build against the production relay:** controlled test from an agent identity into a test channel — - mention in channel → @Mentions sound, inbox shows "Mentioned in" ✅ (was Needs-action) - thread reply with mention → @Mentions sound, once ✅ (was Needs-action) - plain thread reply in the channel being viewed → silent, as designed ✅ ## Reviewers - @tlongwell-block — #475 introduced the plural category; please confirm it wasn't intentional - @wesbillman — owner of the original `FeedItemCategory` contract (#12) and most of the feed builder - @taylorkmho — owner of the sound-slot model and resolver (#968); the fallback-with-warning shape is the part to weigh in on - cc @klopez4212 --------- Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
01091c15a1 |
fix(mobile): recover stale and shuffled messages (#6691)
## Summary - refetch mounted mobile thread replies after relay reconnect, preserving the previous reply list during recovery - auto-dispose route-scoped relay reply caches so reopening a thread queries current relay state - invalidate live replies through both the channel-window and legacy websocket-history paths - preserve optimistic-reply confirmation when the route closes before its deferred cleanup - stabilize rapid same-second messages using desktop's existing split contract: channel timelines render `(created_at ASC, id DESC)` while threads render `(created_at ASC, id ASC)` - retain late live rows after a channel window is exhausted instead of dropping same-second tail messages Closes #4404. Closes #4830. Closes #6204. ## Context The broad all-channel/all-DM stale-session defect reported in #4402 is already addressed on current `main` by #4372 and #3053. Two distinct mobile gaps remained: 1. `threadRepliesProvider` was a process-lifetime one-shot query, so replies missed while the socket was stale remained absent after reconnect or after closing and reopening the thread. 2. Mobile had inconsistent timestamp-only and event-id ordering across channel producers. Rapid messages routinely share Nostr's one-second timestamp, so later hydration/live reconciliation could reshuffle them. Desktop deliberately has two render contracts: channel windows reverse the relay's composite order to `(created_at ASC, id DESC)`, while thread replies use `(created_at ASC, id ASC)`. This consolidates the current-main portions of #4831 and #3243 rather than reviving stale overlapping branches. ## Validation Exact pushed head: `be92d9542c6cd1342733bdc5e8359664b511ce02` - focused channel-provider/window/thread suites: 48/48 passed - incident regression: a mounted thread misses a reply while disconnected, reconnects, and renders the recovered reply - route regression: closing and reopening a thread performs a fresh authoritative query - websocket fallback regression: live reply invalidates the mounted thread even without the channel-window path - disposal regression: optimistic confirmation survives provider disposal between rebuild and deferred cleanup - ordering regressions: channel window/live, websocket fallback, optimistic sends, deep links, both pagination paths, and thread merges preserve their desktop-compatible same-second order - boundary regression: exhausted windows admit late same-second live rows without weakening open-page cursor boundaries - independent adversarial review: no production blocker; source contract verified across all producers and relay cursor semantics unchanged - pre-push Mobile lane passed at exact head, including analysis, file-size/branch checks, and full Flutter suite: 1,675/1,675 passed - `git diff --check` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |
||
|
|
26f4c3ed30 |
feat(mobile): browse and join open channels (#6243)
Mobile previously exposed no way to browse or join channels. Users can now browse and join eligible open channels from the Home quick-actions menu. The public directory loads on demand when Browse channels opens, while the existing kind 9021 join path refreshes membership after success. | Browse channels | Join channel | | --- | --- | | <img width="320" alt="Browse channels" src="https://github.com/user-attachments/assets/f12c46c8-8bf4-487d-8a45-7bec63b16028" /> | <img width="320" alt="Join channel" src="https://github.com/user-attachments/assets/d6306ca7-9ab8-4f5a-8131-7f9b2a76d653" /> | ### How is it tested? Manually tested (see screenshots) and added tests: - [`channels_provider_test.dart`](https://github.com/block/buzz/blob/main/mobile/test/features/channels/channels_provider_test.dart) covers access filtering, independently paginated membership and directory queries, relay-capped pages, repeated-page termination, hard page caps, on-demand directory loading, load failures, retry, and cached-channel retention. - [`channels_page_test.dart`](https://github.com/block/buzz/blob/main/mobile/test/features/channels/channels_page_test.dart) covers browse eligibility, loading and retry states, quick-action layout, and scrolling and joining from a 500-channel directory. - [`search_page_test.dart`](https://github.com/block/buzz/blob/main/mobile/test/features/search/search_page_test.dart) covers discoverable open-channel results without presenting unknown membership counts as zero. Local validation: - `just mobile-check` - `just mobile-test` (1,560 tests) - full pre-push gate --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> |
||
|
|
0e69b3fd7c |
show mention counts in channel notifications (#6696)
**Category:** fix **User Impact:** Stream and forum channels now show an accessible numeric badge for unread mentions while mention chips remain clear in every theme. **Problem:** Mention notifications contributed to the app and Dock badge, but inactive stream and forum rows only became bold, making it difficult to see where multiple mentions were waiting. Mention styling and generic destructive colors could also lose contrast or visual meaning in some themes. **Solution:** Use the same app-badge projection for non-DM channel mention counts, while preserving regular unread bolding, thread activity dots, DM counts, and manual unread behavior. Dedicated notification and opaque mention-highlight tokens keep the new treatments stable and readable across syntax themes. <details> <summary>File changes</summary> **desktop/src/features/channels/useUnreadChannels.ts** Projects app-badge-eligible mention and broadcast counts into stream and forum channel rows while retaining DM-specific counting and manual-unread semantics. **desktop/src/features/sidebar/ui/SidebarSection.tsx** Renders an accessible numeric notification pill on inactive non-DM channels and preserves the thread activity dot fallback. **desktop/src/shared/styles/globals/markdown.css** Applies the shared opaque yellow highlight to human and agent mention chips, including hover treatment. **desktop/src/shared/styles/globals/theme.css** Adds fixed notification and mention-highlight tokens with theme-independent contrast. **desktop/tailwind.config.js** Exposes the notification token pair through semantic Tailwind utilities. **desktop/tests/e2e/badge.spec.ts** Covers aggregated mention counts, broadcasts, unchanged unread tiers, exact accessible text, and badge contrast under an adversarial theme. **desktop/tests/e2e/mentions.spec.ts** Covers human and agent mention styling, hover behavior, dark mode, and WCAG text contrast. </details> ## Reproduction steps 1. Open a stream or forum channel, then navigate to another channel. 2. Receive two messages that mention you in the inactive channel. 3. Confirm the inactive row is bold and shows a red `2` pill matching the two notifications added to the app or Dock badge. 4. Receive a regular channel message and confirm the row only becomes bold, without a numeric pill. 5. Receive a reply in an interested thread and confirm the channel retains its activity dot instead of a mention count. 6. Switch between light and dark themes and confirm human and agent mention chips remain yellow with near-black readable text, including on hover. ## Screenshots Screenshots are posted in the PR discussion using immutable repository-hosted image URLs. Signed-off-by: tulsi <tulsi@block.xyz> |
||
|
|
2f13e30e88 |
fix(desktop): hide selection formatting tray on composer right-click (#6683)
## Summary Right-clicking a text selection in the message composer left the selection formatting tray floating over the native context menu. This suppresses the tray for the duration of the right-click interaction. ## Changes - `SelectionFormattingTray.tsx`: a `contextmenu` listener on the editor DOM sets a suppression ref, cancels any queued rAF reposition, and hides the tray. Suppression clears on the next left-click `pointerdown` or `keydown` in the editor, which reschedules a normal position update. - `scheduleUpdate`/`updatePosition` both honor the suppression ref, so editor `selectionUpdate`/`transaction`/`focus` events fired during the right-click can't bring the tray back. - Extracted `cancelScheduledUpdate` to replace the duplicated rAF-cancel logic, and reset suppression on editor change / cleanup. - E2E coverage in `composer-selection-formatting.spec.ts`: double-click to select, assert the tray shows, right-click and assert the tray hides *and* that `contextmenu` is not `defaultPrevented` (the native menu still opens), then re-select and assert the tray returns. ## Testing `just` pre-push gate ran green: `desktop-check`, `desktop-typecheck`, `desktop-test` (5397 passing), `file-size-check`. ## Demo https://github.com/user-attachments/assets/2fdfced9-6cd6-4eb2-a6df-c03164ab1c42 Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
72ba987c36 |
fix(desktop): stabilize members dialog scrolling (#6670)
## Summary - Keep virtualized member rows measurable by removing `content-visibility: auto` from the measured row subtree. - Use the member card's 60px baseline as the virtualizer estimate while retaining deferred rendering for eager search and archived-member lists. - Cover large rosters with a regression test that checks stable scroll extent across the list and verifies the final member remains reachable. ### Related issue None found. ### Testing - `just ci` - `pnpm -C desktop build:e2e` - `pnpm -C desktop exec playwright test tests/e2e/channels.spec.ts --grep 'members sidebar virtualizes large channel rosters' --repeat-each=5` #### Before https://github.com/user-attachments/assets/a5fcc040-6872-4200-bcc3-7b4197a4dd23 #### After https://github.com/user-attachments/assets/f2c97f2f-3719-4c2a-b17a-2450c6c70a55 Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
17af15effa |
fix(desktop): keep member runtime status off the UI thread (#6445)
## Summary - skip managed-agent runtime discovery when the members sidebar has no local managed bots - run runtime listing disk, process, and mutex work on Tauri’s blocking pool - preserve local managed-bot status and Start/Stop behavior with positive and negative E2E coverage Opening Add people in a human-only channel could invoke synchronous native runtime discovery before the sidebar painted, leaving the macOS app beachballed. Human invites do not depend on that data. ## Why I have seen slowness opening this dialog in the UI https://github.com/user-attachments/assets/1955eb5e-ee47-4edf-8e5c-606d11ffbc25 ### Related issue Related overlap: #4851 is a broader managed-agent lifecycle change that includes a similar native offload. This draft is intentionally limited to the sidebar critical path and adds the human-only query gate. ### Testing I have verified the pause in the video goes away after this change. - `just ci` - `just desktop-check` - `just desktop-test` (5,241 passed) - `just desktop-tauri-fmt-check` - `just desktop-tauri-clippy` - `just desktop-tauri-test` (2,702 passed; 18 ignored) - focused Playwright: human-only sidebar skips runtime discovery - focused Playwright: local managed bot retains status and Stop/Start controls No visual styling changed, so screenshots are not applicable. Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
2d280376ad |
perf(desktop): persist channel heads, collapse thread reads and reply sends (#6572)
## Summary
Lands the build-now items from the desktop latency plan
(#ui-performance-deep-dive) as one change. Every perceived-latency hot
path a user hits on launch, channel open, thread open, and reply send
drops one or more round trips.
**A1 — persisted channel heads (the big one).** Native WAL SQLite cache
(`desktop/src-tauri/src/channel_head_cache.rs`) keyed by `{pubkey,
relayUrl}` scope, 32 rows/scope LRU, 1 MiB per-row drop cap,
schema-version reset, corrupt-row tolerance, checkpointed on shutdown.
Three blocking-pool commands: `channel_head_cache_load` / `_store` /
`_clear`. On the renderer side, `CommunityQueryProvider` kicks off
hydration of up to 12 heads when it constructs the query client — the
app, splash and relay preconnect mount immediately; only
`useChannelMessagesQuery` awaits the seed (`channelHeadHydration`), then
consumes a one-shot hydrated gate so a hydrated channel pays **zero**
`get_channel_window` calls on mount and exactly **one** on the
post-subscription refresh, whose response replaces page zero wholesale.
That refresh fires whether live-subscription setup succeeds or fails,
and is sequenced behind hydration so it is always a distinct
authoritative fetch (see Review follow-ups). Bounds-only persisted heads
(zero rows) are not hydrated and take the cold loading path. The
timeline loading latch recognizes native-hydrated rows as restart-safe
so they paint immediately instead of holding a skeleton. The cache is a
paint accelerator only — the relay response is always authoritative.
Replaces the legacy localStorage `messageSnapshot.ts` (removed, -401
lines).
Kill switch: `VITE_BUZZ_CHANNEL_HEAD_CACHE=off` at build time or
`localStorage["buzz-channel-head-cache"] = "off"` at runtime. Cache is
cleared on community removal and scoped per identity, so a replaced
signer never sees the previous identity's rows.
**B1 — thread aux in one response.** Relay thread filters accept
`include_aux`; the bridge appends the same authorized two-hop
reactions/edits/deletions closure a channel window gets
(`build_aux_query` shared with the window path). Renderer
`useThreadReplies` drops its two follow-up aux fetches. `next_cursor` is
computed from reply-kind rows only since aux rows are unpaged.
Documented in `docs/bridge-channel-window.md`. Thread queries keep
`staleTime: 0` (`bcfe04e2f`): an earlier revision raised it to 30s,
which CI's `thread-unread.spec.ts` caught — once the user leaves a
channel, the live subscription stops feeding that thread's cache, so a
reopen must always take the (now single) authoritative read.
**B2 — cached root on reply send.** `send_channel_message` gains
`root_event_id`; when the renderer already holds the parent (channel or
thread cache) it passes the NIP-10 root, and native signs without the
relay round trip that `resolve_thread_ref` used to make. Strict hex
parse; `root_event_id` requires `parent_event_id`; absent root falls
back to the existing relay resolution. The renderer never sends a
guessed root.
**B4** general HTTP pool idle 10s→300s, max idle per host 1→2. **B5**
relay preconnect fires as soon as identity is ready instead of waiting
for `requestIdleCallback`. One e2e test (`relay-reconnect.spec.ts`
"service restart close resets accumulated backoff") had been relying on
the idle-callback batching to skip past its own seeded dial failures
before the channel list painted; `8133d70bb` makes it wait for the
connected state instead (test-only, still fails with the 1012 backoff
reset disabled). **B6** profile freshness 60s→10 min (both the in-memory
entry check and the query `staleTime`). Tradeoff: another user's
display-name/avatar edit can take up to 10 min to propagate to a client
that already holds their profile (relay reconnect refetches
`users-batch` but resolves from the still-fresh per-pubkey entry); your
own edits still evict the entry immediately (`evictUsersBatchEntries` in
`useUpdateProfileMutation`).
### Related issue
Follows #6456/#6457/#6459/#6460 (already merged). #6455 is the
measurement instrument and is intentionally not folded in. No duplicate
PR found.
### Review follow-ups
Addressing Carl's reviews
[5001114109](https://github.com/block/buzz/pull/6572#pullrequestreview-5001114109)
and
[5002596542](https://github.com/block/buzz/pull/6572#pullrequestreview-5002596542),
each pushed as new commits (no rebase):
- `4f06b7770` fix(desktop): mount app while channel heads hydrate;
always revalidate — provider no longer gates children on the cache load;
`refreshAfterSubscribe` runs on subscribe failure too; bounds-only heads
skipped at seed; seed merges into an existing window store. +3 tests.
- `35834cb31` fix(relay): drain aux closure hops across the page clamp —
`query_all_pages` walks the `(created_at, id)` keyset via
`until`/`before_id` until a short page (`AUX_PAGE_LIMIT` =
`DEFAULT_MAX_PAGE_LIMIT`, `AUX_MAX_PAGES` = 64 warn+truncate) so
one-shot `limit: 1000` newest-first no longer drops the oldest
edits/deletions. +3 tests; `docs/bridge-channel-window.md` updated.
- `db21b0531` merge of `origin/main` `e23632941` (#6558, #6312 — no
overlap).
- `5a5566c0f` fix(desktop): sequence post-subscribe refresh behind
channel head hydration — `refreshChannelWindowMessages` awaits
`channelHeadHydration()` and, for a hydration-seeded query (`data !==
undefined && dataUpdatedAt === 0`), the in-flight snapshot fetch before
invalidating. Without this, a subscription that settles before the
SQLite load invalidated a data-less in-flight query; TanStack dedupes
that onto the existing fetch (`query-core` `fetch()` only cancels when
`state.data` exists), which returned the seeded snapshot — 0
authoritative fetches. Regression test reproduces Carl's exact ordering
(fails at `35834cb31` with 0 calls), plus a cold-channel guard that the
fix does not double-fetch.
- `b129231c8` fix(desktop): let concurrent post-hydration refreshes
share one window fetch — found independently by Max and Wren reviewing
`5a5566c0f`: subscribe settlement + reconnect both wake on the same
snapshot promise and both invalidate; the second (default
`cancelRefetch: true`) cancelled and replaced the first authoritative
fetch (3 queryFn calls, not 2, and the cancelled Tauri invoke still hits
the relay). The seeded branch now invalidates with `cancelRefetch:
false` so a second waker joins the in-flight fetch; cold/warm keep the
default (`test_canceled_stale_fetch_cannot_overwrite_catch_up_window`
relies on it). Concurrent regression test fails at `5a5566c0f` with 3.
### Testing
At `b129231c8` (PR head; verified in one shell with `git rev-parse HEAD`
= `b129231c8`): `pnpm check`, `tsc --noEmit`, desktop unit 5,393 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.
At `5a5566c0f`: `pnpm check`, `tsc --noEmit`, desktop unit 5,392 / 0,
Playwright `boot-splash` + `channel-head-restart` + `relay-reconnect` +
`relay-reconnect-affordance` + `thread-unread` 34 / 34 on a fresh
`build:e2e`, pre-push hooks green.
At `35834cb31`: desktop unit 5,390 / 0; `cargo test -p buzz-relay --lib`
910 / 0; fmt + clippy `-D warnings` clean; Playwright 32 / 32 (same
specs minus affordance); GitHub CI green on every job except Smoke (3)
(unrelated project-review row-count + messaging timing flake, per Carl)
and Unit Tests (sherpa cache skeleton, below).
Earlier, all at `8133d70bb` (this PR head is `0c492366d` =
|
||
|
|
0720f5380c |
Downgrade desktop Huddles to audio protocol v2 (#6610)
## Summary - negotiate Huddle audio protocol v2 on desktop - decode the released one-byte peer-index prefix - retain roster-driven playout resets and document the missing v3 epoch fence ## Testing - `just desktop-tauri-fmt-check` - `just desktop-tauri-clippy` - `just desktop-tauri-test` Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
e236329413 |
Polish Huddle participant interactions (#6312)
## Summary - arrange Huddle participants in a responsive, equal-weight cluster with spring enter/exit motion and a `+N` overflow - spotlight tapped participants over a blurred call surface, with a roster for hidden participants and no self-avatar action - add selection haptics across full-screen and drawer controls, including both end-call buttons <img width="1080" height="2424" alt="Screenshot_20260819-151448" src="https://github.com/user-attachments/assets/00b7fdca-2304-4788-9952-e07224798513" /> <img width="1080" height="2424" alt="Screenshot_20260819-151422" src="https://github.com/user-attachments/assets/a0cfc861-0519-44ff-bb56-4c983ed6344c" /> ## Validation - `just mobile-check` - focused participant, drawer-control, and full-screen end-call widget tests - Huddle-focused widget suite (15 tests) - full mobile Flutter suite (1,538 tests) ## Dependency Built on #6056 and contains only the follow-up interaction work. Merge after #6056 lands. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz> |
||
|
|
4baccd5394 |
Downgrade mobile Huddles to audio protocol v2 (#6558)
## Summary - downgrade Mobile Huddle authentication and native media configuration from protocol v3 to the currently deployed relay's v2 contract - restore the released one-byte relay peer prefix while retaining later reconnect, roster, and playout-reset reliability fixes - update Android, iOS, protocol documentation, and focused tests together Protocol v2 does not carry v3's occupancy epoch on audio frames, so it cannot fence the narrow delayed-packet/peer-index-reuse race. This is an intentional compatibility tradeoff until the relay v3 rollout is ready. ### Related issue None found. ### Testing - `just mobile-check` - `just mobile-test` — 1,661 tests passed - Android debug build installed and launched on Pixel 10 as `xyz.block.buzz.mobile.sprout_mobile_profile_settings`; foreground process verified - signed iOS Release build installed and launched on iPhone as `com.buzz.buzzMobile`; running process verified A live two-device Huddle audio call remains a manual verification step. Signed-off-by: kenny lopez <klopez4212@gmail.com>mobile-v0.14.0-rc.2 |
||
|
|
040b203f73 |
perf(desktop): make the Projects surface render-cheap (#6460)
Diagnostic profiling on a large community (101 issues, 258 PRs) showed Projects tab switches taking 2.5–3.6s, dominated by single React commits of 0.5–1.5s and per-render recomputation — fetch work was already off the main thread; the cost was building the UI. ### Measured: tab click → painted, per tab | tab | before | after | |---|---|---:| | projects | 3,608ms | 320–580ms | | repositories | 3,126–3,534ms | 310–410ms | | tasks | 395–1,101ms | ~115ms | | reviews | 322–2,603ms | ~96ms | | activity | 597–741ms | ~148ms | Single-commit ceiling dropped from 1,541ms to ≤200ms (growth steps 25–40ms). Fixes in profiled-cost order: - **Profile popover body mounts only while open.** `UserProfilePopover` carried seven query subscriptions plus interaction hooks per instance even when closed; grids mount hundreds (five per card in people stacks, one per row author) — measured **~40ms per card**, the dominant share of the 1.2s card-tab commits. The always-mounted shell is now just the Radix root + trigger; trigger markup, hover timing, and keyboard handling are unchanged, and hover/tooltip event continuity is preserved because the trigger never remounts. - **Incremental row mounting.** The first 12 cards / 30 rows render in the first commit; the rest stream in 36–60-per-frame low-priority transitions. Grouped lists trim across group boundaries via a pure, tested slicer; the mounted count survives in-place refetches. - **Activity feed**: was rebuilt unmemoized on every render, markdown-flattening every issue/PR/comment body in the community just to sort and keep 30 items (~360+ flattens per render on the measured community). Now memoized, and bodies stay raw until after the sort+slice — 30 flattens, once per data change. - **Contribution graph** (always-visible rail, so every tab paid for it): ~180 day cells each wrapped in a Radix tooltip with per-cell Intl date formatting per render. Now memoized, cells precomputed once per data change, native `title` tooltips. (The activity-bar segments keep their styled Radix tooltips — pinned by an existing spec.) - **Rows/cards memoized with identity-stable props**: per-row selection arrays were rebuilt per row per render (O(n²) — 258 PRs × 258-item arrays each render) and are now hoisted and shared; people arrays derive inside the memoized cards; the rail's stat walk over every issue/PR is memoized. - **`content-visibility: auto`** on cards and rows so offscreen entries skip layout and paint; **tab switches run in a React transition** so the click stays responsive while the new tree mounts. Remaining known cost (out of scope): cold-entry data readiness — the work-item and activity queries ship thousands of events to compute counts (2–4s on a large community; see the fan-lifecycle PR). The structural fix is a relay-side aggregate; tracked as follow-up. --------- Signed-off-by: Max Lampert <maxwell@squareup.com> |
||
|
|
f99532585a |
refactor(acp): clarify agent prompt sections (#6501)
## Why The ACP prompt puts a machine-specific Workspace prefix before static base guidance and labels the user-facing agent instruction layer as the generic System section. Because the cwd varies by launch and worktree, leading with it reduces reusable prompt-prefix stability. `[Workspace]` was added in [PR #1194](https://github.com/block/buzz/pull/1194) as a defensive fix after a broken `~/.sprout` → `~/.buzz` migration caused agents to scan `$HOME` and trigger macOS TCC prompts. This change retains that grounding while shrinking it to the current working directory and moving dynamic environment context after the static Base prompt. ## What - Emit the prompt in Base → Workspace → Agent Instructions order - Reduce Workspace to `Current working directory: <absolute path>` - Resolve cwd as an absolute native-platform path and preserve Windows drive/UNC paths instead of checking for a leading `/` - Emit Agent Instructions for persona and standalone agent instructions across modern and legacy ACP paths - Preserve parsing for archived observer frames that used System or the former Workspace-before-Base order, and align the persona catalog label ## Risk Assessment Medium-low — this changes prompt framing for every newly created agent session. Existing archived observer frames remain parseable, and execution still uses the same ACP working directory. Cwd resolution now fails clearly instead of substituting `/` when the process directory cannot be resolved. ## References - https://github.com/block/buzz/pull/1103 - https://github.com/block/buzz/pull/1194 Generated with Codex --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> |
||
|
|
8c0f42e187 |
Add mobile Huddles voice MVP (#6056)
## Summary - add foreground mobile Huddles on Android and iOS with native Opus capture/playback, mute, speaker routing, participants, lifecycle, and minimized drawer UI - keep mobile Huddle cards and roster state live, including ended rooms, relay-resolved profiles, and agents - broadcast desktop agent TTS through the existing Huddle audio protocol ## Scope Foreground human-to-human voice MVP only. Agent setup/transcripts, background calling, recording, and advanced device controls remain out of scope. ## Validation - `just mobile-check` - `just mobile-test` — 1,500 passed - `just desktop-check` and `just desktop-test` — 4,957 passed - desktop typecheck, strict Clippy, and Tauri tests — 2,445 passed, 15 ignored - mobile worktree identity contract checks - physical Pixel/iPhone behavior reviewed during development --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>mobile-v0.14.0-rc.1 |
||
|
|
a2d8be5efa |
feat(desktop-messages): keep agents addressed across messages (#6315)
**Category:** new-feature **User Impact:** Users can keep selected agents addressed across consecutive messages without retyping their handles. **Problem:** Repeated conversations with agents require manually typing the same mentions on every turn, which adds friction and makes recipients easy to omit. **Solution:** The composer can now keep agents automatically addressed per channel, either from the mention controls or after a successful inline mention. Addressed agents remain visible in the toolbar, apply to channel threads, survive send failures safely, and never cross community boundaries. ## Changes <details> <summary>File changes</summary> **desktop/src-tauri/src/events/message_tags.rs** Preserves the automatic-address marker on validated mention reference tags. **desktop/src/features/channels/ui/ChannelPane.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/communities/useCommunityInit.ts** Clears composer audience state when the active community changes. **desktop/src/features/forum/ui/ForumComposer.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/forum/ui/ForumComposerAutocompletes.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/home/ui/InboxDetailPane.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/messages/lib/agentAddressMention.d.mts** Defines helpers and types for marked automatic-address mention tags. **desktop/src/features/messages/lib/agentAddressMention.mjs** Defines helpers and types for marked automatic-address mention tags. **desktop/src/features/messages/lib/agentAddressMention.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/applyEditTagOverlay.mjs** Preserves automatic-address metadata when edited message tags are overlaid. **desktop/src/features/messages/lib/applyEditTagOverlay.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts** Stores the preference that keeps explicitly mentioned agents addressed for later messages. **desktop/src/features/messages/lib/extractMentionPersonas.ts** Separates persona recipients from the composer mention orchestration. **desktop/src/features/messages/lib/persistentAgentAudience.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/persistentAgentAudience.ts** Maintains bounded, in-memory, channel-scoped automatic agent audiences. **desktop/src/features/messages/lib/useMentionSelection.ts** Centralizes mention picker selection state and agent-first selection behavior. **desktop/src/features/messages/lib/useMentions.ts** Exposes explicit picker origins and selection controls while preserving inline mention behavior. **desktop/src/features/messages/ui/ComposerAddressControls.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/ComposerAddressControls.tsx** Renders compact addressed-agent avatars and the automatic-mention management entry point. **desktop/src/features/messages/ui/MentionAutocomplete.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/MentionAutocomplete.tsx** Adds automatic-mention controls and options to the existing mention picker. **desktop/src/features/messages/ui/MessageAgentAddressPrefix.tsx** Shows which agents were automatically addressed on a sent message. **desktop/src/features/messages/ui/MessageComposer.tsx** Integrates automatic audiences, picker controls, accessible feedback, shortcuts, and send behavior. **desktop/src/features/messages/ui/MessageComposer.types.ts** Defines the simplified channel audience context shared by composer hosts. **desktop/src/features/messages/ui/MessageComposerToolbar.tsx** Places automatic-address controls in the composer toolbar without crowding narrow layouts. **desktop/src/features/messages/ui/MessageRow.tsx** Displays automatic-address metadata alongside sent message content. **desktop/src/features/messages/ui/MessageThreadPanel.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/persistentAgentAudienceHosts.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useAddressMentionPulse.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useAddressMentionPulse.ts** Provides success and failure animation signals for addressed-agent controls. **desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts** Coordinates adding, removing, and announcing automatically addressed agents. **desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts** Implements the platform-aware shortcut for toggling automatic addressing. **desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts** Promotes successfully sent inline agent mentions and provides a single undoable notification. **desktop/src/features/messages/ui/useComposerMentionPicker.ts** Opens the mention picker without rewriting the current draft. **desktop/src/features/messages/ui/useMentionSendFlow.helpers.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts** Merges automatic and inline recipients, marks outgoing tags, and restores failed sends safely. **desktop/src/features/messages/ui/useMentionSendFlow.ts** Merges automatic and inline recipients, marks outgoing tags, and restores failed sends safely. **desktop/src/features/messages/ui/usePersistentAgentMentionHydration.ts** Removes the prior draft-text hydration approach now that automatic audiences stay at composer ingress. **desktop/src/features/settings/ui/AgentsSettingsPanel.tsx** Replaces the old global behavior with explicit composer-level automatic-mention controls. **desktop/src/features/settings/ui/PreventSleepSettingsCard.tsx** Replaces the old global behavior with explicit composer-level automatic-mention controls. **desktop/src/shared/lib/keyboard-shortcuts.ts** Defines the user-facing automatic-address keyboard shortcut label. **desktop/src/shared/ui/VideoReviewCommentMarkdown.tsx** Allows automatic-address prefixes to compose with video review timecodes. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. </details> ## Reproduction Steps 1. Open a channel with one or more agents and open the mention picker from the composer. 2. Select an agent for automatic mentions, then send several messages without retyping the handle; confirm the agent remains in the composer control and receives each message. 3. Mention another agent inline, send successfully, and confirm the agent becomes automatically addressed; use the notification's Undo action to reverse it. 4. Open a thread in the same channel and confirm the same addressed agents are available there. 5. Remove an agent from the composer control and confirm later messages stop addressing it. 6. Switch communities and confirm addressed agents do not carry into the other community. ## Screenshots All states below use the dark Buzz theme with a selected lilac accent. ### Addressed composer Selected agents stay visible at the composer ingress without adding handles to the draft.  ### Open mention menu The @ ingress opens the existing mention menu and shows which agents are already addressed.  ### Mention options The inline options pane controls whether a successful one-time agent mention carries into later messages.  ### Agent settings The same preference is available in **Settings → Agents → Conversations**.  --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
723affe5d1 |
fix(desktop): remove Buzz entity link previews (#6512)
**Category:** improvement **User Impact:** Buzz-native project, repository, issue, and pull request links now appear once as compact inline chips, with their details available on hover. **Problem:** Buzz-native entity links rendered both an inline chip and a standalone preview card, repeating the same metadata and adding visual noise to conversations. **Solution:** Exclude Buzz-native links from the shared standalone-preview extractor while leaving entity parsing intact for chip tooltips and preserving external web previews and attachment cards. <details> <summary>File changes</summary> **desktop/src/shared/lib/linkPreview.ts** Stops Buzz-native preview candidates after parsing, including same-relay git clone URLs that normalize to repository entities, while allowing external URLs through the existing snapshot path. **desktop/src/shared/lib/linkPreview.test.mjs** Covers project, repository, issue, pull request, markdown-labeled, same-relay clone, and mixed external-link extraction behavior. **desktop/src/shared/ui/markdown/useMessageLinkPreviews.test.mjs** Confirms sent messages no longer merge a standalone Buzz entity card while external sender snapshots still render. </details> ## Reproduction steps 1. Open a desktop channel containing a `buzz://project`, `buzz://repo`, `buzz://issue`, or `buzz://pr` link. 2. Confirm the link renders as an inline entity chip without a second standalone Buzz card below the message. 3. Hover the chip and confirm its entity metadata remains available. 4. Post an external HTTPS link and confirm its web preview still renders. 5. Paste a same-relay `/git/<owner>/<repo>` clone URL and confirm it uses the repository chip without a duplicate card. ## Screenshots | Before | After | | --- | --- | | Inline chip plus redundant standalone Project card | Inline chip is now the sole presentation | |  |  | **After — rich metadata stays available on hover**  ## Verification At commit `3fa74cdd342ac1f6721b7d56a7f111af31e0e6e9`: - focused link-preview + Markdown unit suites — 119/119 passed - targeted registered smoke E2E — 8/8 passed, including labeled same-relay clone metadata, ordinary-link presentation, and in-app navigation - `cd desktop && pnpm exec tsc --noEmit` — passed - `git diff --check origin/main...HEAD` — passed - pre-push hooks — desktop check, TypeScript, and full desktop unit suite passed --------- Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz> |
||
|
|
074561233e |
fix(composer): preserve caret when inserting mentions mid-message (#6531)
**Category:** fix **User Impact:** Users can insert mentions earlier in a draft and continue typing without the caret corrupting the rest of the message. **Problem:** Caret correction ran after every document change, so typing a mention before existing text repeatedly advanced across the mention separator and interleaved spaces into the draft. **Solution:** Limit correction to the autocomplete settlement it was designed for, with transaction-level and browser-level regression coverage for known and unregistered mentions. <details> <summary>File changes</summary> **desktop/src/features/messages/lib/mentionHighlightExtension.ts** Restricts trailing-space caret advancement to an armed autocomplete settlement instead of every document change. **desktop/src/features/messages/lib/mentionHighlightExtension.test.mjs** Exercises the real ProseMirror plugin state and verifies mid-draft mention typing, unknown tokens, end-of-message typing, and completed-mention separators. **desktop/tests/e2e/mentions.spec.ts** Reproduces the reported composer workflow in Chromium and covers the same corruption path for an unregistered `@token`. </details> ## Reproduction steps 1. Open a channel and enter `hello world` in the composer. 2. Move the caret between `hello` and ` world`. 3. Type ` @bo`, select `bob` from autocomplete, and continue typing `abc`. 4. Confirm the composer reads `hello @bob abc world` with the caret after `abc`. 5. Repeat with an unregistered token such as ` @zzq` and confirm the existing text remains intact. ## Before / After | Before | After | | --- | --- | | Typing after a mid-draft mention walks the caret through the existing message. | Continued typing stays after the inserted mention. | |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Mongo <5398c5fd039b963ce132b3e078e7c4af097dd997517bb5e14c2682fe68c25197@buzz.block.builderlab.xyz> |
||
|
|
f794216737 |
chore(deps): update rust crate async-trait to v0.1.92 (#6094)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [async-trait](https://redirect.github.com/dtolnay/async-trait) | dependencies | patch | `0.1.91` → `0.1.92` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>dtolnay/async-trait (async-trait)</summary> ### [`v0.1.92`](https://redirect.github.com/dtolnay/async-trait/releases/tag/0.1.92) [Compare Source](https://redirect.github.com/dtolnay/async-trait/compare/0.1.91...0.1.92) - Resolve double\_must\_use clippy lint in generated code ([#​303](https://redirect.github.com/dtolnay/async-trait/issues/303)) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yOS41IiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
2d93ea0955 |
chore(deps): update dependency sonner to v2.0.8 (#6093)
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [sonner](https://sonner.emilkowal.ski/) ([source](https://redirect.github.com/emilkowalski/sonner)) | [`2.0.7` → `2.0.8`](https://renovatebot.com/diffs/npm/sonner/2.0.7/2.0.8) |  |  | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>emilkowalski/sonner (sonner)</summary> ### [`v2.0.8`](https://redirect.github.com/emilkowalski/sonner/compare/v2.0.7...ecce1841c55e4a72dfe139a8992b56498660125e) [Compare Source](https://redirect.github.com/emilkowalski/sonner/compare/v2.0.7...v2.0.8) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4yOS41IiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
9390e11c9b |
chore(deps): update rust crate http-body-util to v0.1.4 (#5452)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [http-body-util](https://redirect.github.com/hyperium/http-body) | dependencies | patch | `0.1.3` → `0.1.5` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>hyperium/http-body (http-body-util)</summary> ### [`v0.1.5`](https://redirect.github.com/hyperium/http-body/compare/http-body-util-v0.1.4...http-body-util-v0.1.5) [Compare Source](https://redirect.github.com/hyperium/http-body/compare/http-body-util-v0.1.4...http-body-util-v0.1.5) ### [`v0.1.4`](https://redirect.github.com/hyperium/http-body/releases/tag/http-body-util-v0.1.4) [Compare Source](https://redirect.github.com/hyperium/http-body/compare/http-body-util-v0.1.3...http-body-util-v0.1.4) #### What's Changed - Add `Fused` body combinator that always returns `None` once completed. - Add `BodyExt::into_stream()` to convert a body into a `Stream`. - Add `Full::into_inner()` to get the full `Buf`. - Add `InspectFrame` and `InspectErr` combinators. </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
1a0a27d358 |
chore(deps): update rust crate http to v1.4.2 (#5451)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [http](https://redirect.github.com/hyperium/http) | dependencies | patch | `1.4.0` → `1.4.2` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>hyperium/http (http)</summary> ### [`v1.4.2`](https://redirect.github.com/hyperium/http/blob/HEAD/CHANGELOG.md#142-June-8-2026) [Compare Source](https://redirect.github.com/hyperium/http/compare/v1.4.1...v1.4.2) - Fix `uri::Builder` to allow `"*"` as the path when scheme and authority are also set, used in HTTP/2 requests. - Fix `Uri` to properly reject `DEL` characters. ### [`v1.4.1`](https://redirect.github.com/hyperium/http/blob/HEAD/CHANGELOG.md#141-May-25-2026) [Compare Source](https://redirect.github.com/hyperium/http/compare/v1.4.0...v1.4.1) - Fix `PathAndQuery::from_static()` and `from_shared()` to reject inputs that do not start with `/`. - Fix `Extend` for `HeaderMap` to clamp max size hint and not overflow. - Fix `header::IntoIter` that could use-after-free if the generic value type could panic on drop. - Fix `header::{IterMut, ValuesIterMut}` to not violate stacked borrows. </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
d874d67c11 |
chore(deps): update rust crate futures-util to v0.3.33 (#5448)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [futures-util](https://rust-lang.github.io/futures-rs) ([source](https://redirect.github.com/rust-lang/futures-rs)) | dependencies | patch | `0.3.32` → `0.3.34` | | [futures-util](https://rust-lang.github.io/futures-rs) ([source](https://redirect.github.com/rust-lang/futures-rs)) | workspace.dependencies | patch | `0.3.32` → `0.3.34` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>rust-lang/futures-rs (futures-util)</summary> ### [`v0.3.34`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0334---2026-08-11) [Compare Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.33...0.3.34) - Preserve cloned waker identity. ([#​3032](https://redirect.github.com/rust-lang/futures-rs/issues/3032)) - Updato `syn` to 3. ([#​3028](https://redirect.github.com/rust-lang/futures-rs/issues/3028)) ### [`v0.3.33`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0333---2026-07-18) [Compare Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33) - Fix `ReadLine`'s soundness issue regarding to exception safety. ([#​3020](https://redirect.github.com/rust-lang/futures-rs/issues/3020)) - Fix unsound `Send` impl for `IterPinRef` and `Iter`. ([#​3003](https://redirect.github.com/rust-lang/futures-rs/issues/3003)) - Fix stacked borrows violation in `compat01as03` implementation. ([#​3012](https://redirect.github.com/rust-lang/futures-rs/issues/3012)) - Fix memory leak in `FuturesUnordered::IntoIter`. ([#​3005](https://redirect.github.com/rust-lang/futures-rs/issues/3005)) - Add `portable-atomic-alloc` feature and use it in `FuturesUnordered`. ([#​3007](https://redirect.github.com/rust-lang/futures-rs/issues/3007)) - Re-export `alloc::task::Wake`. ([#​3010](https://redirect.github.com/rust-lang/futures-rs/issues/3010)) - Update `spin` to 0.12. ([#​3014](https://redirect.github.com/rust-lang/futures-rs/issues/3014)) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMzkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
2cef92df67 |
chore(deps): update rust crate futures to v0.3.33 (#5445)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [futures](https://rust-lang.github.io/futures-rs) ([source](https://redirect.github.com/rust-lang/futures-rs)) | dev-dependencies | patch | `0.3.32` → `0.3.34` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Release Notes <details> <summary>rust-lang/futures-rs (futures)</summary> ### [`v0.3.34`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0334---2026-08-11) [Compare Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.33...0.3.34) - Preserve cloned waker identity. ([#​3032](https://redirect.github.com/rust-lang/futures-rs/issues/3032)) - Updato `syn` to 3. ([#​3028](https://redirect.github.com/rust-lang/futures-rs/issues/3028)) ### [`v0.3.33`](https://redirect.github.com/rust-lang/futures-rs/blob/HEAD/CHANGELOG.md#0333---2026-07-18) [Compare Source](https://redirect.github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33) - Fix `ReadLine`'s soundness issue regarding to exception safety. ([#​3020](https://redirect.github.com/rust-lang/futures-rs/issues/3020)) - Fix unsound `Send` impl for `IterPinRef` and `Iter`. ([#​3003](https://redirect.github.com/rust-lang/futures-rs/issues/3003)) - Fix stacked borrows violation in `compat01as03` implementation. ([#​3012](https://redirect.github.com/rust-lang/futures-rs/issues/3012)) - Fix memory leak in `FuturesUnordered::IntoIter`. ([#​3005](https://redirect.github.com/rust-lang/futures-rs/issues/3005)) - Add `portable-atomic-alloc` feature and use it in `FuturesUnordered`. ([#​3007](https://redirect.github.com/rust-lang/futures-rs/issues/3007)) - Re-export `alloc::task::Wake`. ([#​3010](https://redirect.github.com/rust-lang/futures-rs/issues/3010)) - Update `spin` to 0.12. ([#​3014](https://redirect.github.com/rust-lang/futures-rs/issues/3014)) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjkuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
f84511c169 |
chore(deps): update dependency @tauri-apps/api to v2.11.1 (#5444)
This PR contains the following updates:
| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@tauri-apps/api](https://redirect.github.com/tauri-apps/tauri) |
[`2.11.0` →
`2.11.1`](https://renovatebot.com/diffs/npm/@tauri-apps%2fapi/2.11.0/2.11.1)
|

|

|
---
> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/1) for more information.
---
### Release Notes
<details>
<summary>tauri-apps/tauri (@​tauri-apps/api)</summary>
###
[`v2.11.1`](https://redirect.github.com/tauri-apps/tauri/releases/tag/%40tauri-apps/api-v2.11.1):
@​tauri-apps/api v2.11.1
[Compare
Source](https://redirect.github.com/tauri-apps/tauri/compare/@tauri-apps/api-v2.11.0...@tauri-apps/api-v2.11.1)
<details>
<summary><em><h4>PNPM Audit</h4></em></summary>
```
No known vulnerabilities found
```
</details>
#### \[2.11.1]
##### Enhancements
-
[`916782601`](
|
||
|
|
9008a4d172 |
chore(deps): update ubuntu:24.04 docker digest to 561618e (#5442)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ubuntu](https://hub.docker.com/_/ubuntu) ([source](https://git.launchpad.net/cloud-images/+oci/ubuntu-base)) | container | digest | `4fbb8e6` → `561618e` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/1) for more information. --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/block/buzz). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMi4wIiwidXBkYXRlZEluVmVyIjoiNDQuMjkuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
694d804b24 |
chore(deps): update swatinem/rust-cache digest to 6323deb (#5441)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [Swatinem/rust-cache](https://redirect.github.com/Swatinem/rust-cache)
([changelog](
|
||
|
|
97b1fee5c3 |
fix(desktop): restore true zoom by scaling the root rem (#6514)
## Summary Follow-up to #5644. Cmd +/- had become a text-only zoom: type scaled while rem-based padding, gaps, widths, avatars, and controls stayed frozen, which produced cramped layouts (see [#buzz-frontend thread](buzz://message?channel=a410ffde-c61f-416a-96e0-c296b5f5ecc9&id=1a758115cf07b00c097f6e988553908c045165325a57637519cfa7ed9c9accec)). Root cause: #5644 introduced a virtual typography rem so the **Font size** preference could change text without moving layout — a good decoupling — but it also routed **Cmd +/- zoom** through that same px-valued token and pinned the real root at 16px. One decision ("freeze layout") was applied to two dials that shouldn't share it. This PR gives each dial one owner and lets CSS compose them: | Control | Changes | How | |---|---|---| | **Cmd +/- zoom** | Everything — true zoom | Scales the real `<html>` font-size again (`useWebviewZoomShortcuts`) | | **Font size preference** | Text only | Sets `data-font-size`; `typography.css` maps it to a unitless `--buzz-type-scale`, mirroring how density already works | `--buzz-type-rem` becomes `calc(1rem * var(--buzz-type-scale))` — rem-relative, so it rides on zoom automatically. Resulting text px = `16 × zoom × scale × token-ratio`. The 13 / 14 / 15px conversation contract is unchanged at default zoom. Density and the type ramp from #5644 are untouched. The preference module no longer does px math or knows about zoom; the zoom hook no longer imports the preference module. Net deletion in production code. ## Validation - `pnpm test` — 5,308 desktop unit tests - `pnpm check:px-text`, `tsc --noEmit`, biome - Playwright: `top-chrome-zoom-clearance.spec.ts` (native-chrome clearance stays fixed under root zoom), `inbox-refactor-screenshots.spec.ts` (zoomed row padding now asserts `4.4px` instead of the frozen `4px`), and both `profile.spec.ts` zoom tests (composed zoom × preference, cross-window storage reset) - Before/after screenshots at 140% zoom in the comment below --------- Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
eb484387d5 |
chore(desktop): drop unused ORIGINAL_CONTENT from empty-edit-delete spec (#6517)
`biome check` fails with `lint/correctness/noUnusedVariables` on `ORIGINAL_CONTENT` in `desktop/tests/e2e/empty-edit-delete.spec.ts`, which fails `pnpm check` (Desktop Core) for **every PR touching desktop paths** — e.g. it currently blocks #6460. It presumably landed while Desktop Core was path-skipped on the introducing PR. One-line removal; the constant has no remaining references (the assertions use `RENDERED_ORIGINAL_CONTENT`). Signed-off-by: Max Lampert <maxwell@squareup.com> |