From 0b1c9e15cf5377f4e6285d73f9a8b84442162e9d Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Sun, 6 Sep 2026 21:51:17 +0100 Subject: [PATCH] fix(web): quiet structured transcript metadata --- web/src/structured.test.ts | 36 ++++++++++++++++++++++++++++++++++++ web/src/structured.ts | 9 ++++++++- web/src/styles.css | 18 ++++++++++-------- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/web/src/structured.test.ts b/web/src/structured.test.ts index e675f57..f2309ac 100644 --- a/web/src/structured.test.ts +++ b/web/src/structured.test.ts @@ -115,6 +115,42 @@ describe("structured connection: data flow", () => { conn.dispose(); }); + it("shows turn details only while that footer is the conversation's final item", () => { + const ws = new FakeSocket(); + const conn = mount({ sockets: [ws] }); + const turnMeta = (id: string, elapsedMs: number) => + row(id, "notice", { + maestroTurnMeta: { elapsedMs, contextTokens: 12_000, outcome: "completed", model: "codex" }, + }); + ws.open(); + ws.emit({ + type: "transcript_snapshot", + snapshot: { + cursor: 4, + rows: [ + row("user:1", "user", { content: { type: "text", text: "first" } }), + turnMeta("turn:1", 1_000), + row("user:2", "user", { content: { type: "text", text: "second" } }), + turnMeta("turn:2", 2_000), + ], + }, + }); + + expect(conn.el.querySelectorAll(".st-turn-meta")).toHaveLength(1); + expect(conn.el.querySelector(".st-turn-meta")?.textContent).toContain("2s"); + + ws.emit({ + type: "projection_update", + transcript: { + type: "append", + row: row("user:3", "user", { content: { type: "text", text: "third" } }), + }, + state: idleState, + }); + expect(conn.el.querySelector(".st-turn-meta")).toBeNull(); + conn.dispose(); + }); + it("keeps state-only plan rows out of the transcript", () => { const ws = new FakeSocket(); const conn = mount({ sockets: [ws] }); diff --git a/web/src/structured.ts b/web/src/structured.ts index 1ef8961..8a7edd5 100644 --- a/web/src/structured.ts +++ b/web/src/structured.ts @@ -340,7 +340,14 @@ export function createStructuredConnection( rowEls.set(row.id, rowEl); children.push(rowEl); } - rowsEl.replaceChildren(...children); + // A completed turn's footer is useful as the conversation's current + // tail, but repeating duration/model/token summaries throughout history + // makes them read like transcript content. Keep only a footer that is the + // final visible item; appending the next turn naturally retires it. + const visibleChildren = children.filter( + (child, index) => !child.classList.contains("st-turn-meta") || index === children.length - 1, + ); + rowsEl.replaceChildren(...visibleChildren); if (restoreToolFocus && focusedRowId) { rowEls.get(focusedRowId)?.querySelector(".st-tool-head")?.focus(); } else if (restoreGroupFocus && focusedGroupId) { diff --git a/web/src/styles.css b/web/src/styles.css index 618edb5..ea97bae 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -2549,19 +2549,27 @@ input.leaf-rename:focus { transition: opacity var(--dur-fast) var(--ease-out); } -.st-copyable:hover, .st-copyable:focus-visible { background: var(--surface-2); outline: none; } -.st-copyable:hover::after, .st-copyable:focus-visible::after, .st-copyable[data-copy-state="copied"]::after, .st-copyable[data-copy-state="error"]::after { opacity: 1; } +@media (hover: hover) { + .st-copyable:hover { + background: var(--surface-2); + } + + .st-copyable:hover::after { + opacity: 1; + } +} + .st-copyable[data-copy-state="copied"]::after { content: "copied"; color: var(--live); @@ -3126,12 +3134,6 @@ input.leaf-rename:focus { } } -@media (hover: none) { - .st-copyable::after { - opacity: 0.7; - } -} - /* ---------- modal ---------- */ .modal-backdrop {