diff --git a/webviews/src/agent-session/acpmux/conversation/ChevronAfterLabel.gallery.ts b/webviews/src/agent-session/acpmux/conversation/ChevronAfterLabel.gallery.ts new file mode 100644 index 000000000000..c3a1f3ed6b3d --- /dev/null +++ b/webviews/src/agent-session/acpmux/conversation/ChevronAfterLabel.gallery.ts @@ -0,0 +1,139 @@ +// l10n-allow-file: gallery fixtures (sample lanes, messages and commands), not shipped UI. +// Every transcript disclosure chevron right after its label's words, the way ChatGPT draws "Worked +// for 11s ⌄" (Leo, 2026-11-03: "this how chatgpt does their chevrons"): small, muted, pointing down +// while closed and up while open, never parked at the row's far edge. The variants try the glyph's +// size and its gap after the words (conversation.css --cv-chevron-glyph and --cv-chevron-gap); the +// selected one is the default. A narrow width shows a cut label keeping its chevron after the +// ellipsis. test/transcript-chevron-after-label.test.ts measures the shipped rows. +import { agentPaneEntry, type AgentPaneVariant } from "../../../gallery/format"; +import type { Play } from "../../../gallery/play"; +import { + SESSION_ID, + activity, + assistant, + chat, + row, + session, + summary, + thought, + tool, + user, +} from "../../../gallery/fixtures/acpmux"; +import type { AcpmuxActivity } from "../model"; +import { BACKGROUND_TURN, backgroundTurnRowId } from "./backgroundTurns"; + +const run = (description: string, command: string): AcpmuxActivity => + tool(command, "execute", "completed", { description, command, output: "ok\n", exitCode: 0 }); + +const SENDER = "lane-box-2/cloud-promote"; +const RECEIVED = + "backend.yml is green at 4f2a91c, the candidate is ready to promote whenever the production window opens again."; + +const snapshot = () => { + const base = chat( + [ + user("Promote the candidate once backend.yml is green on the lane.", 20), + activity( + [ + thought("Check the lane's last backend.yml run before dispatching anything."), + run("Read the lane's last run", "gh run list -w backend.yml -L 1"), + run("Read the candidate's sha", "git rev-parse --short HEAD"), + ], + 19.9, + ), + assistant("The lane's run is still going; I'll wait for it and then promote.", 19.8), + summary(19.7, { status: "completed", durationMs: 11_000, toolCount: 2 }), + user(RECEIVED, 12, { + incoming: { + kind: "message", + message: { name: SENDER, text: RECEIVED, raw: RECEIVED, sessionId: "s-lane", harness: "claude" }, + }, + untyped: true, + }), + row(BACKGROUND_TURN, 8, { + id: backgroundTurnRowId("chevrons-scheduled"), + background: { id: "chevrons-scheduled", trigger: "scheduled", running: false, quiet: true }, + }), + activity([run("Read the lane's last run", "gh run list -w backend.yml -L 1")], 7.9, { + inBackground: "chevrons-scheduled", + }), + assistant("Nothing new yet.", 7.8, { inBackground: "chevrons-scheduled" }), + activity( + [ + run("Check backend.yml dispatch inputs", "gh workflow view backend.yml --yaml"), + run("Dispatch production promote of the candidate", "gh workflow run backend.yml -f env=production"), + ], + 4, + { id: "chevrons-run" }, + ), + ], + { + title: "cloud-promote", + sessions: [ + session({ sessionId: SESSION_ID, title: "cloud-promote", name: "cloud-promote" }), + session({ sessionId: "s-lane", title: SENDER, name: SENDER }), + ], + }, + ); + return { ...base, summary: { ...base.summary!, family: "claude" } }; +}; + +const STYLE_ID = "gallery-chevron-after-label"; + +/// Sets the chevron's glyph size and gap, then opens the last "Ran 2 commands" (its steps show the +/// hover chevron after their labels). +const look = + (glyph?: string, gap?: string): Play => + async (ctx) => { + ctx.document.getElementById(STYLE_ID)?.remove(); + if (glyph || gap) { + const style = ctx.document.createElement("style"); + style.id = STYLE_ID; + style.textContent = `:root { ${glyph ? `--cv-chevron-glyph: ${glyph};` : ""} ${gap ? `--cv-chevron-gap: ${gap};` : ""} }`; + ctx.document.head.append(style); + } + const line = () => + [...ctx.document.querySelectorAll("button.cv-tool[aria-expanded]")].filter((node) => + /Ran 2 commands/.test(node.textContent ?? ""), + )[0]; + await ctx.waitFor(line, { capMs: 10_000 }); + if (line()?.getAttribute("aria-expanded") === "false") line()!.click(); + await ctx.waitFor(() => ctx.document.querySelector(".cv-steps"), { capMs: 10_000 }); + }; + +const variants: Record = { + chatgpt: { + note: 'Selected default: a 12px glyph whose ink starts 5px after the words, as ChatGPT\'s "Worked for 11s ⌄". Down while closed, up while open.', + snapshot: snapshot(), + play: look(), + }, + tight: { + note: "A 10px glyph 3px after the words: reads as part of the label, but small at 1x.", + snapshot: snapshot(), + play: look("10px", "3px"), + }, + spaced: { + note: "A 12px glyph a full word space (8px) after the words: clearly its own control, a little detached.", + snapshot: snapshot(), + play: look("12px", "8px"), + }, + large: { + note: "A 14px glyph 5px after the words: the tool glyphs' size, heavier than ChatGPT's.", + snapshot: snapshot(), + play: look("14px", "5px"), + }, +}; + +export default agentPaneEntry({ + id: "agent-pane.transcript-chevron-after-label", + title: "Transcript chevrons: right after the label, pointing down, the ChatGPT way", + area: "Agent pane", + height: 640, + widths: { narrow: 420, normal: 760, wide: 900 }, + covers: [ + "agent-session/acpmux/conversation/conversation.css", + "agent-session/acpmux/conversation/TranscriptActivityRow.css", + "agent-session/acpmux/subagents/DisclosureRow.css", + ], + variants, +}); diff --git a/webviews/src/agent-session/acpmux/conversation/RowGrammar.test.tsx b/webviews/src/agent-session/acpmux/conversation/RowGrammar.test.tsx index 86e1dfebd363..c90b7aada2fa 100644 --- a/webviews/src/agent-session/acpmux/conversation/RowGrammar.test.tsx +++ b/webviews/src/agent-session/acpmux/conversation/RowGrammar.test.tsx @@ -288,15 +288,23 @@ test("an opened send shows its message once, under the row, not again on the lin } }); -test("the trailing column and chevron use the shared flex row contract", () => { +test("the chevron follows the words and the trailing column ends the line", () => { + // Leo, 2026-11-03: "this how chatgpt does their chevrons". In the transcript a row's chevron + // sits right after its words and its status or duration ends the line + // (test/transcript-chevron-after-label.test.ts measures it in Chromium). const css = readFileSync(new URL("./conversation.css", import.meta.url), "utf8"); - const block = (selector: string) => - new RegExp(`(?:^|\\n)${selector.replace(/[.]/g, "\\.")} \\{([^}]*)\\}`).exec(css)?.[1] ?? ""; - expect(block(".cv-tool")).toContain("display: flex"); + const block = (selector: string) => { + const at = css.indexOf(`\n${selector} {`); + return at < 0 ? "" : css.slice(at, css.indexOf("}", at)); + }; + expect(css).toMatch(/\n\.cv-tool \{[^}]*display: flex/); expect(block(".cv-tool__chevron")).toContain("order: 3"); + expect(block(".acpmux-row .cv-tool > .cv-tool__trail")).toContain("order: 5"); + expect(block(".acpmux-row .cv-tool > .cv-tool__trail")).toContain("margin-inline-start: auto"); expect(css).toContain(".cv-tool:not(.cv-agentmsg__line) > .cv-command__meta,"); - expect(css).toContain(" order: 4;"); - // Message rows use the same shared flex sentence, with the trailing status outside it. + // A message line's sentence joins the line in the transcript, so the chevron can precede Copy; + // outside it (Home's run ledger) the sentence keeps the free width. + expect(block(".acpmux-row .cv-agentmsg__line > .cv-agentmsg__sentence")).toContain("display: contents"); const agentCss = readFileSync(new URL("./AgentMessageRow.css", import.meta.url), "utf8"); const sentence = /\.cv-agentmsg__sentence \{([^}]*)\}/.exec(agentCss)?.[1] ?? ""; expect(sentence).toContain("flex: 1 1 0"); diff --git a/webviews/src/agent-session/acpmux/conversation/SentMessageLine.tsx b/webviews/src/agent-session/acpmux/conversation/SentMessageLine.tsx index 09bd32347e9b..3a1ccc1b6c85 100644 --- a/webviews/src/agent-session/acpmux/conversation/SentMessageLine.tsx +++ b/webviews/src/agent-session/acpmux/conversation/SentMessageLine.tsx @@ -235,7 +235,7 @@ export function SentMessageLine({ row's is (MessageCard.tsx); its click never toggles the row. */} {text && ( // oxlint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events - event.stopPropagation()}> + event.stopPropagation()}> )} diff --git a/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.css b/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.css index 64ab2591d48d..1b10039f22fd 100644 --- a/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.css +++ b/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.css @@ -1,4 +1,6 @@ -/* Activity rows share the transcript tool grid: icon, sentence, trailing status, chevron. */ +/* Activity rows share the transcript tool grid: icon, sentence, the chevron right after the words + (ChatGPT's "Worked for 11s ⌄"; Leo, 2026-11-03: "this how chatgpt does their chevrons"), then the + trailing status at the line's end. */ .cv-agent-activity-row { display: flex; min-width: 0; @@ -19,7 +21,7 @@ } .cv-agent-activity__sentence { display: flex; - flex: 1 1 auto; + flex: 0 1 auto; min-width: 0; align-items: center; order: 1; @@ -34,13 +36,13 @@ text-overflow: ellipsis; white-space: nowrap; } -.cv-agent-activity__trail { - order: 3; +.cv-agent-activity__control > .cv-agent-activity__trail { + order: 4; flex: none; margin-inline-start: auto; } -.cv-agent-activity__chevron { - order: 4; +.cv-agent-activity__control > .cv-agent-activity__chevron { + order: 2; flex: none; cursor: default; } diff --git a/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.tsx b/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.tsx index ec46b11b53c3..c6c727a74b64 100644 --- a/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.tsx +++ b/webviews/src/agent-session/acpmux/conversation/TranscriptActivityRow.tsx @@ -43,11 +43,8 @@ export function TranscriptActivityRow({ {children} {meta !== undefined && {meta}} -