Commit Graph

3 Commits

Author SHA1 Message Date
ss-dev-00
90f526be62 feat(desktop): fold focus-mode agent work into one transcript block
Slice C. 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 the
same 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 —
no future edit to the block can regress the other two variants without first
moving that branch.

Behaviour, mirroring berd:

- Live: the block is open and the rail IS the status, so there is no header
  line restating it. The last three steps show in arrival order; older ones go
  behind an "N previous steps" disclosure, 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 — its content is either laid out or
  not, with no intermediate height — so 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.
- Reader choice wins: once the reader toggles a block, policy stops opening and
  closing it.

One deliberate departure from berd: 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 —
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 how a headline and chain eligibility drifted apart on an
earlier attempt. The projection is closed in the type system rather than by
convention, so a future `TranscriptItem` variant cannot silently wear a wrench
and impossible pairs are not representable.

Four defects fixed on top, each mutation-checked in isolation:

- **A reader's open block was folded shut by the agent posting again.** Work
  blocks are derived and ided after their first step, and only the LAST
  assistant message is exempt from the block — so a second answer demotes the
  first, the runs on either side merge, the old block unmounts, and the
  `useState` holding the reader's expansion goes with it. The choice now lives
  above the block, keyed by the STEP ids it was taken on, since the steps
  survive regrouping. An open choice beats a folded one (hiding steps a reader
  asked for loses what they were reading; showing extra costs a scroll), and
  the choice is recorded against every step in the block — recording only the
  first leaves the absorbed block's stale `open` entry behind and, since open
  wins, the merged block could never be folded again.
- **A finished block went live again between turns.** `lastTurnId` walked the
  display blocks, but a turn that has only emitted setup lifecycle rows
  classifies to zero segments and produces no block — so for the whole gap
  between `turn_started` and the next turn's first renderable item, "newest
  turn with a block" was the turn that had already ended, and its trailing item
  became the streaming item: a settled 6-step block re-opened, dropped to its
  last three steps, then folded back. Every turn, not an edge case. Liveness
  now comes from the newest turn id in the item stream, and a trailing item is
  only reported as streaming when the live turn owns it.
- **The running step's pulse ignored reduced motion.** `WorkBlockRailGlyph`
  used bare `animate-pulse`, which no `prefers-reduced-motion` block in the app
  matches — all 20 are scoped to `buzz-*`/`motion-*`/`t-skel-*`. Now
  `motion-safe:animate-pulse`. That swap also repaired three assertions it
  would otherwise have disarmed: they matched `.animate-pulse` as a substring
  and, worse, all three ran on a folded rail with no rows, so "nothing pulses"
  held against a build where abandoned steps pulse forever.
- **A work block whose running step had no live session never settled.**

Two test-hygiene fixes worth keeping: a gap test was pinning a sequence whose
`session/new` card moved `streamingItemId` on its own, so those frames passed
against the pre-fix code and proved nothing (the plain no-restart path now
leads); and the orphaned suite inherited React Query's default 300000ms
`gcTime`, arming a five-minute timer at teardown that node:test waited out —
`gcTime: 0` takes the full desktop suite from 307s to 102s, verified against a
negative control rather than on the passing tree alone.

Squashed from ss-dev-02/tool-chain-cards (#6536) at 91a5754ad.

Co-authored-by: ss-dev-02 <9e916f802f7932c38e630ac6b4726f5db7ca326c0d98e9bab495309eef13fe5a@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
2026-08-26 15:28:48 -07:00
ss-dev-00
cd2037e051 feat(desktop): add conversation transcript variant for focus-mode agent sessions
Slice B. A third, purely additive `conversation` transcript variant for the
full-cover reading surface: 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.

The transcript previously had two presentations — the dense `default` activity
feed and `compactPreview` — both tuned for a 380px panel. Read at full width
they are a scan surface, not something you read a turn in.

Threaded through `ManagedAgentSessionPanel` and `AgentSessionThreadPanel` as
an optional prop. Tool items deliberately route through the same
`ToolActivity` presenter as `default`, which is what lets the tool-chain card
work in slice C land on top without conflict.

The byte-for-byte guarantee for the existing two variants is enforced by a
captured-markup fixture rather than by inspection: the baseline was produced
by mounting the same transcript on unmodified code, so the test fails on any
drift in `default` or `compactPreview` output.

Three defects found and fixed while building it, each with a
non-vacuous test:

- The author row resolved its name through `resolveUserLabel`'s
  `fallbackName: item.title`, whose value describes the *trigger* that started
  the turn ("@Mention", "Prompt", a title-cased Buzz event kind) and is never
  an identity. Harmless where it only seeded avatar initials; the conversation
  author row promotes it to displayed text, so an unresolved sender rendered
  as if "@Mention" were their name. Identity now stops at the profile —
  display name, NIP-05 handle, truncated pubkey — and the title stays as
  trigger chrome in the footer. The existing test was asserting the bug.
- The captured-markup fixture embeds formatted timestamps, so it was
  zone-sensitive; pinning `TZ` then exposed that it is locale-sensitive too.
  Assigning `process.env.LANG` at runtime does not fix it — node resolves its
  default locale once at startup, unlike `TZ`, which `Date` reads lazily. Both
  formatting surfaces the render path reaches are overridden, including
  `Date.prototype.toLocale*`, which does not route through
  `Intl.DateTimeFormat`. `Intl.DateTimeFormat` must be replaced with a plain
  function, not an arrow: the render path calls it with `new`.

Squashed from ss-dev-01/conversation-variant (#6538) at 5e3faf443.

Co-authored-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>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
2026-08-26 15:26:27 -07:00
Taylor Ho
654b6c374b feat(profile): embed live activity feed in profile aux panel (#1380)
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
2026-07-03 15:40:29 +00:00