Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions webviews/src/agent-session/acpmux/continuationSpacing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,21 @@ test("estimated grid rows follow the Interface Size", () => {
g.getComputedStyle = saved.getComputedStyle;
}
});

test("the live line sits flush under a run of one-line rows: 22 apart top to top (#29965)", () => {
const run = row("run", "activity", {
items: [{ kind: "tool", text: "", tool: { id: "c", title: "ls", kind: "execute", status: "completed" } }],
} as Partial<AcpmuxRow>);
const next = row("next", "activity", {
items: [{ kind: "tool", text: "", tool: { id: "d", title: "cat x", kind: "execute", status: "completed" } }],
} as Partial<AcpmuxRow>);
const live = row("live", "thinking", { below: true } as Partial<AcpmuxRow>);
const layout = layoutConversation([run, next, live], 720);
expect(layout.heights[0]).toBe(22);
expect(layout.tops[1]! - layout.tops[0]!).toBe(22);
expect(layout.tops[2]! - layout.tops[1]!).toBe(22);
// The same live line right under its prompt still waits a block gap under it (grid.md 5.5).
const prompt = row("p", "user", { text: "Go" });
const waiting = layoutConversation([prompt, live], 720);
expect(waiting.tops[1]! - waiting.heights[0]!).toBe(TRANSCRIPT_BLOCK_GAP);
});
34 changes: 20 additions & 14 deletions webviews/src/agent-session/acpmux/conversation/conversation.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@
--cv-message-gap: 0px;
--cv-turn-gap: 0px;
--cv-continuation-gap: 0px;
--cv-tool-row-height: var(--grid-row);
/* One-line activity rows (tool runs, calls, notices, inbox and message lines) sit 22 apart top to
top, flush, as Codex lists them (design/chat-transcript.md "One-line row pitch", #29965; Leo,
2026-11-01: "doesn't make sense for these to be spaced differently"). #27580 drew them on the
28 list row with a 24 block gap between each, about 50 apart (Leo, 2026-11-02: "well now
there's sometimes too much spacing between tool calls"). */
--cv-tool-row-height: 22px;
--cv-glyph-slot: var(--grid-transcript-icon-box);
--cv-glyph-line: var(--grid-transcript-icon-box);
--cv-disclosure-slot: var(--grid-transcript-icon-box);
Expand All @@ -79,18 +84,18 @@
--cv-message-line: var(--cv-tool-row-height);
--cv-message-subhead: 22px; /* a sender's head inside that group */
--cv-live-gap: 10px; /* over the running turn's live line, inside its row */
--cv-disclosure-line-height: var(--grid-row);
--cv-disclosure-line-height: var(--cv-tool-row-height);
/* A status row between two replies (model.ts `isStatusRow`) sits in the middle of the gap (Leo,
2026-10-30: "see how claude code has those centered tool calls / id like more alignment too"):
the block gap on both sides already centres it, so the status gaps add nothing (model.ts
STATUS_GAP, STATUS_GAP_PLAIN). */
--cv-status-gap: 0px;
--cv-status-gap-plain: 0px;
--cv-status-gap-plain-over: 0px;
/* #30865's reply room and #30400's note gap on the grid: the stack's block gap is the room under a
reply and around a note, so neither adds its own. */
/* #30865's reply room on the grid: the stack's block gap is the room under a reply. A note in a
run of flush lines keeps --cv-note-gap (below) around it. */
--cv-reply-room: 0px;
--cv-note-gap: 0px;
--cv-note-gap: var(--grid-gap-block);
--cv-status-reply-tail: 0px;
--cv-tool-gap: var(--grid-gap);
/* One icon column for every transcript meta row (Leo, 2026-10-31: "YOU GUYS CANNOT FUCKIGN
Expand Down Expand Up @@ -1999,7 +2004,8 @@ pre.cv-math-source {
.cv-tools {
display: flex;
flex-direction: column;
gap: var(--grid-gap-block);
/* Flush lines, 22 apart (--cv-tool-row-height); a note keeps --cv-note-gap around it. */
gap: 0;
margin: 0;
font-family: var(--cv-font);
font-size: var(--cv-font-size);
Expand Down Expand Up @@ -2181,10 +2187,10 @@ pre.cv-math-source {
pointer-events: none;
}
/* A short interim note folded into a run of calls (turns.ts mergeShortNotes): the agent's words in
the run's quiet color, wrapped on the rows' 20px lines, with the plain status gap (12px, the
transcript's inner gap) to the tool rows above and below it, as a longer reply between two runs
has (Leo, 2026-11-01: "the thoughts in between tool calls probably want a bit more padding").
model.ts `activityStepHeight` estimates it with the same numbers (NOTE_LINE, NOTE_GAP). */
the run's quiet color, wrapped on the rows' 20px lines, with the block gap (--cv-note-gap, 24) to
the flush tool rows above and below it (Leo, 2026-11-01: "the thoughts in between tool calls
probably want a bit more padding"). model.ts `activityStepHeight` estimates it with the same
numbers (NOTE_LINE, noteGap). */
.cv-tool-note {
margin: var(--cv-note-gap) 0;
line-height: 20px;
Expand Down Expand Up @@ -2661,7 +2667,7 @@ button.cv-video-frames__frame:focus-visible {
.cv-tool-group {
display: flex;
flex-direction: column;
gap: var(--grid-gap-block);
gap: 0;
margin: 0;
padding-left: var(--grid-icon-box);
box-shadow: inset 1px 0 var(--cv-rule);
Expand Down Expand Up @@ -3167,7 +3173,7 @@ button.cv-video-frames__frame:focus-visible {
background: color-mix(in srgb, var(--cv-text) 6%, transparent);
}
.cv-received .cv-disclosure__toggle {
height: var(--grid-row);
height: var(--cv-tool-row-height);
box-sizing: border-box;
padding-block: 0;
padding-inline: 0;
Expand Down Expand Up @@ -5380,9 +5386,9 @@ article.acpmux-row:not(:has(> :not(.acpmux-row__gap))) {
.cv-tool-run {
display: flex;
flex-direction: column;
gap: var(--grid-gap-block);
/* Flush lines, 22 apart (--cv-tool-row-height); a note keeps --cv-note-gap around it. */
gap: 0;
}
.cv-tool-note { margin: 0; }
.cv-tool-label {
display: inline-flex;
max-width: 100%;
Expand Down
36 changes: 20 additions & 16 deletions webviews/src/agent-session/acpmux/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
footerAfter,
gridLength,
MARK_STEP,
rowMark,
gridScale,
restBand,
Expand Down Expand Up @@ -1094,7 +1095,14 @@ function replyHeight(reply: string): number {

/// The compact tool run rhythm (`conversation.css --cv-tool-row-height` and its run bottom gap).
const TOOL_RUN_TOP = 0;
const TOOL_ROW_PT = gridLength("row");
/// The running turn's live run line (conversation/turns.ts ACTIVITY_LINE), by value: turns.ts imports
/// this module.
const ACTIVITY_LINE_KIND = "activityLine";
/// A step row: a tool run, the live run line, or a one-line row (transcriptGrid.ts MARK_STEP).
const isStepRow = (row: AcpmuxRow) => row.kind === "activity" || row.kind === ACTIVITY_LINE_KIND || isLineRow(row);
/// One-line activity rows are 22 apart top to top (design/chat-transcript.md "One-line row pitch",
/// #29965), `--cv-tool-row-height`.
const TOOL_ROW_PT = 22;
const toolRowHeight = () => TOOL_ROW_PT * chromeScale;
const TOOL_RUN_BOTTOM = 0;
/// A one-line row that is not a tool run (`isLineRow`) draws one tool row's line, as a one-call run
Expand All @@ -1108,12 +1116,12 @@ const lineRow = () => LINE_FRAME + toolRowHeight();
/// A short interim note folded into a run (conversation.css `.cv-tool-note`): one 20px line with
/// the plain status gap above and below it.
const NOTE_LINE = 20;
/// The room around a note (#30400's --cv-note-gap): 0 on the grid, where the run's block gap between
/// its lines is the room (conversation.css `.cv-tool-note { margin: 0 }`, foldedRunSteps).
const NOTE_GAP = 0;
/// The room above and below a note (#30400's --cv-note-gap): the block gap, since a run's lines are
/// otherwise flush (conversation.css `.cv-tool-note`).
const noteGap = () => TRANSCRIPT_BLOCK_GAP * chromeScale;
function activityStepHeight(item: AcpmuxActivity): number {
const tool = item.tool;
if (!tool) return item.kind === "note" ? NOTE_LINE + 2 * NOTE_GAP : 26;
if (!tool) return item.kind === "note" ? NOTE_LINE + 2 * noteGap() : 26;
// A message to another agent (SendMessage, `acpmux send`, a sub-session's `send_message`) is one
// line on the tool rows' line (AgentMessageRow.tsx), as every one-line row is.
if (agentMessage(tool)) return toolRowHeight();
Expand Down Expand Up @@ -1163,27 +1171,23 @@ function activityStepHeight(item: AcpmuxActivity): number {
/// "when I'm seeing all of these agent messages back and forth, that's when it sort of, like, gets
/// confused").
function foldedRunSteps(row: AcpmuxRow, groups: readonly ToolGroup[]): number {
// `.cv-tools` and `.cv-tool-run` stack their children with the block gap (conversation.css).
// `.cv-tools` and `.cv-tool-run` stack their lines flush (conversation.css), 22 apart.
const stretches = runStretches(row, groups);
const children = stretches.reduce((count, stretch) => count + (stretch.fold ? 1 : stretch.groups.length), 0);
return stretches.reduce(
(height, stretch) =>
height +
(stretch.fold ? toolRowHeight() : stretch.groups.reduce((sum, group) => sum + foldedGroupHeight(group), 0)),
Math.max(0, children - 1) * TRANSCRIPT_BLOCK_GAP * chromeScale,
0,
);
}

/// An entry a folded run keeps on its own line: a group starts closed there (ToolGroupRow.tsx
/// `folded`), one line, as a sent message's row is (SentMessageLine.tsx); anything else is its calls.
function foldedGroupHeight(group: ToolGroup): number {
if (group.type === "group" || group.type === "send") return toolRowHeight();
// `.cv-tool-group` stacks its calls with the block gap too.
// `.cv-tool-group` lists its calls flush too.
const items = "items" in group ? group.items : [group.item];
return items.reduce(
(sum, item) => sum + activityStepHeight(item),
Math.max(0, items.length - 1) * TRANSCRIPT_BLOCK_GAP * chromeScale,
);
return items.reduce((sum, item) => sum + activityStepHeight(item), 0);
}

/// The prepared texts of those messages, for their estimates; cleared past 256, as a row's are.
Expand Down Expand Up @@ -1567,8 +1571,7 @@ function fallbackRowHeight(
}
const items = row.items ?? [];
const steps = items.length
? items.reduce((height, item) => height + activityStepHeight(item), 0) +
Math.max(0, items.length - 1) * TRANSCRIPT_BLOCK_GAP * chromeScale
? items.reduce((height, item) => height + activityStepHeight(item), 0)
: toolRowHeight();
return finish(TOOL_RUN_TOP + TOOL_RUN_BOTTOM + steps);
}
Expand Down Expand Up @@ -2108,7 +2111,8 @@ export function layoutConversation(
let prevDrawn = prevDrawnRow(rows, start);
for (let index = start; index < rows.length; index += 1) {
const row = rows[index]!;
marks[index] = rowMark(row);
// A step row (a tool run or a one-line row) joins the step row before it flush (transcriptGap).
marks[index] = isStepRow(row) ? MARK_STEP : rowMark(row);
messages.visit(row, index, previous?.rows[index]);
if (nextDrawn <= index) {
nextDrawn = index + 1;
Expand Down
18 changes: 9 additions & 9 deletions webviews/src/agent-session/acpmux/rowEstimates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
import { turnView } from "./conversation/turns";
import { COLLAPSED_PROMPT_LINES, PASTE_CHIP_HEIGHT, PROMPT_MORE_HEIGHT } from "./conversation/promptCollapse";
import { currentChatMetrics } from "./chatDisplay";
import { gridLength } from "./transcriptGrid";

const reply = (id: string, text: string) => ({ id, version: 1, at: 0, kind: "assistant", text });
// A footer carries its answer's text, for Copy (conversation/turns.ts), so the estimate never reads it.
Expand Down Expand Up @@ -278,7 +277,7 @@ test("another agent's message is one 22px tool line in the one-line rows' frame,
incoming: { kind: "message", message: { name: "cmux16/glass-lens", text, raw: text } },
});
const [short, long] = heightsOf([row("m1", "Draft is up."), row("m2", "Line of the report.\n".repeat(40))]);
expect(short).toBe(28);
expect(short).toBe(22);
expect(long).toBe(short);
});

Expand All @@ -295,14 +294,14 @@ test("a message this agent sent another one is one 22px tool line in the one-lin
},
};
const [height] = heightsOf([{ id: "s", version: 1, at: 0, kind: "activity", items: [send] }]);
expect(height).toBe(2 + 22 + 4);
expect(height).toBe(22);
});

test("a task notice is a tool row's line in the one-line rows' frame", () => {
const [height] = heightsOf([
{ id: "n", version: 1, at: 0, kind: "user", text: "Task done", incoming: { kind: "notice", text: "Task done" } },
]);
expect(height).toBe(28);
expect(height).toBe(22);
});

// A waiting steer's Send now leads the existing action row, so it adds no height to a text prompt.
Expand Down Expand Up @@ -416,8 +415,9 @@ test("a status row between two replies is estimated under the reply's kept room"
} else {
// No reply draws its own action row (a turn's copy lives in its footer). On the grid (grid.md
// 5.5) the stack's block gap above and below the run centres it, so neither row adds room: the
// run is its one 28 row and the reply is as tall as it is over a prompt.
expect(step).toBe(gridLength("row"));
// run is its one 22 line (design/chat-transcript.md "One-line row pitch") and the reply is as tall
// as it is over a prompt.
expect(step).toBe(22);
expect(reply).toBe(overPrompt!);
}
expect(alone).toBeGreaterThan(0);
Expand Down Expand Up @@ -478,9 +478,9 @@ test("a folded run with messages to other agents is estimated as the lines it dr
const run = (items: unknown[]) => [
{ id: "run", version: 1, at: 0, kind: "activity", settled: true, foldRun: true, items },
];
// On the grid a run adds no margin of its own (grid.md 5.5): its lines are 28 rows, stacked with the
// block gap `.cv-tools`, `.cv-tool-run` and `.cv-tool-group` draw between them (conversation.css).
const lines = (n: number) => n * gridLength("row") + (n - 1) * gridLength("gap-block");
// A run adds no margin of its own (grid.md 5.5): its lines are flush 22px lines, one apart top to top
// (design/chat-transcript.md "One-line row pitch", #29965).
const lines = (n: number) => n * 22;
// Plain work still folds to its one summary line.
expect(heightsOf(run([shell("a", "git status"), read]))[0]).toBe(lines(1));
// A send between two commands: a line each.
Expand Down
6 changes: 6 additions & 0 deletions webviews/src/agent-session/acpmux/transcriptGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export const MARK_MESSAGE = 2;
export const MARK_INDICATOR = 3;
/// A settled turn's footer: no height of its own, its action row laid in the gap under the reply.
export const MARK_FOOTER = 4;
/// A step row: a tool run or a one-line activity row (model.ts `isLineRow`). Consecutive step rows
/// sit flush, one 22px line apart top to top (design/chat-transcript.md "One-line row pitch", #29965);
/// the block gap is around a run of them only.
export const MARK_STEP = 5;

type MarkedRow = { kind: string; incoming?: unknown; received?: unknown; text?: string };
export function rowMark(row: MarkedRow): number {
Expand All @@ -46,6 +50,8 @@ export function transcriptGap(previous: number, next: number): number {
// Waiting for the first output: the indicator right under its prompt takes the block gap.
if (next === MARK_INDICATOR && previous === MARK_PROMPT) return TRANSCRIPT_BLOCK_GAP;
if (previous === MARK_PROMPT || next === MARK_PROMPT) return TRANSCRIPT_TURN_GAP;
// The live line under a run is one more line of it (#29965): flush too.
if (previous === MARK_STEP && (next === MARK_STEP || next === MARK_INDICATOR)) return 0;
return TRANSCRIPT_BLOCK_GAP;
}

Expand Down
11 changes: 9 additions & 2 deletions webviews/test/agent-pane-working-counts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,15 @@ await requireBrowserLane("agent-pane-working-counts.test.ts", async () => {
const box = await line(page).evaluate((node) => {
const label = node.querySelector(".cv-thinking")!.getBoundingClientRect();
const row = node.getBoundingClientRect();
const style = getComputedStyle(node);
const px = (name: string) => parseFloat(style.getPropertyValue(name));
// The tokens are var() chains, so resolve each through a probe's height.
const px = (name: string) => {
const probe = document.createElement("div");
probe.style.height = `var(${name})`;
node.append(probe);
const value = probe.getBoundingClientRect().height;
probe.remove();
return value;
};
return {
height: row.height,
labelLeft: label.left - row.left,
Expand Down
5 changes: 4 additions & 1 deletion webviews/test/browser/transcript-rhythm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ await requireBrowserLane("transcript-rhythm.test.ts", async () => {
toolIcon,
chevron,
copy,
gaps: [message.top - header.bottom, group.top - message.bottom, live.top - group.bottom],
gaps: [message.top - header.bottom, group.top - message.bottom],
// The live line is one more line of the tool run over it: flush (#29965).
liveUnderRun: live.top - group.bottom,
inner: paragraphs[1]!.top - paragraphs[0]!.bottom,
composerGap: composer.top - live.bottom,
floating: Boolean(document.querySelector(".acpmux-main[data-dock=float]")),
Expand All @@ -140,6 +142,7 @@ await requireBrowserLane("transcript-rhythm.test.ts", async () => {
if (state.actionMid !== undefined)
expect(Math.abs(state.actionMid - 12 * scale), "action glyph center in block gap").toBeLessThanOrEqual(0.5);
for (const gap of state.gaps) expect(Math.abs(gap - 24 * scale), "block gap").toBeLessThanOrEqual(0.5);
expect(Math.abs(state.liveUnderRun), "live line flush under its run").toBeLessThanOrEqual(0.5);
expect(Math.abs(state.inner - 12 * scale), "paragraph gap").toBeLessThanOrEqual(0.5);
// Over a floating stack the newest line rests the rest gap over it (model.ts REST_GAP, Leo
// 2026-11-01: "more space between composer and latest messages"); docked in flow, the
Expand Down
Loading