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
2 changes: 1 addition & 1 deletion webviews/scripts/design-tokens-baseline.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
{"file":"agent-session/acpmux/conversation/conversation.css","kind":"spacing","property":"padding-block","values":{"3px":2,"2px 6px":1,"2px 4px":1}}
{"file":"agent-session/acpmux/conversation/conversation.css","kind":"spacing","property":"padding-bottom","values":{"var(--cv-th-pad-bottom, 1.75px)":1}}
{"file":"agent-session/acpmux/conversation/conversation.css","kind":"spacing","property":"padding-inline","values":{"18px 4px":1,"0 2px":1,"6px":1}}
{"file":"agent-session/acpmux/conversation/conversation.css","kind":"spacing","property":"padding-left","values":{"var(--cv-list-indent, 28px)":2,"22px":1,"21px":1,"14.5px":1,"22.5px":3,"20px":1}}
{"file":"agent-session/acpmux/conversation/conversation.css","kind":"spacing","property":"padding-left","values":{"var(--cv-list-indent, 28px)":2,"22px":1,"21px":1,"14.5px":1,"22.5px":2,"20px":1}}
{"file":"agent-session/acpmux/conversation/conversation.css","kind":"spacing","property":"padding-right","values":{"10px":1,"2px":1}}
{"file":"agent-session/acpmux/conversation/conversation.css","kind":"spacing","property":"padding-top","values":{"var(--cv-th-pad-top, 11px)":1,"calc(var(--cv-live-gap) + 2px)":1,"10px":1,"3px":2}}
{"file":"agent-session/acpmux/dictation.css","kind":"height","property":"height","values":{"18px":1,"2px":1,"calc(2px + var(--level, 0) * 16px)":1}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// l10n-allow-file: gallery fixtures (sample prompts, thoughts, commands and replies), not shipped UI.
// A turn organized as Claude Code desktop draws one (Leo, 2026-10-29: "we want to have that
// capabilitiy of rendering like claude code desktop has stuff organized."): a paragraph, a
// question asked and answered (the answered QuestionCard), a plan of three steps, then a thought
// and five shell steps under "Ran 5 commands", opened, and the prose after them. Drawn through the
// real transcript (TurnRows.tsx, ToolGroupRow.tsx), so the step bodies and the question and plan
// cards show here as they change. The arms are the opened group's looks
// (`data-tool-steps-look`, conversation.css `.cv-steps`): card (the default), tree and zebra.
import answeredCollapsed from "../../../../../Packages/Shared/CmuxAgentQuestion/Sources/CmuxAgentQuestion/Fixtures/answered-collapsed.json";
import { agentPaneEntry, type AgentPaneVariant } from "../../../gallery/format";
import { activity, assistant, chat, row, summary, thought, tool, user } from "../../../gallery/fixtures/acpmux";
import type { Play } from "../../../gallery/play";
import type { AcpmuxActivity, AcpmuxPlanEntry } from "../model";
import type { AgentQuestion } from "../question/model";
import { QUESTION_RECORD } from "../question/record";

const MINUTE = 60_000;
const at = (minutes: number) => Date.UTC(2026, 9, 29, 9, 0, 0) + minutes * MINUTE;

/// A finished shell step with Claude's Bash description, its command and what it printed.
const step = (description: string, command: string, output: string, minute: number, exitCode = 0): AcpmuxActivity =>
tool(command, "execute", "completed", {
description,
command,
output,
exitCode,
startedAt: at(minute),
endedAt: at(minute) + 1_400,
});

const todo = (content: string, status: AcpmuxPlanEntry["status"]): AcpmuxPlanEntry => ({ content, status });

const LEAD =
"The fetch helper retries every failure the same way, so a 404 waits through three backoffs before it fails. " +
"I should read how the helper decides to retry, find who calls it, and check whether the tests already cover a client error.\n\n" +
"If the retry loop has no status check, the fix belongs there rather than in each caller.";

function turn() {
return [
user("Make the fetch helper stop retrying client errors", 9),
assistant(
"The helper lives in `src/net/fetchWithRetry.ts` and every network call in the app goes through it. " +
"Before I change how it retries, one question about authentication, since the retry rules differ for it.",
8.8,
),
row(QUESTION_RECORD, 8.6, { question: answeredCollapsed as unknown as AgentQuestion }),
row("plan", 8.4, {
plan: [
todo("Find where the helper decides to retry", "completed"),
todo("Stop retrying 4xx responses", "in_progress"),
todo("Cover it with a test", "pending"),
],
}),
activity(
[
thought(LEAD, { startedAt: at(1), endedAt: at(1) + 6_000 }),
step(
"Found the retry loop and its status check",
"rg -n 'retry' src/net/fetchWithRetry.ts",
"12:export async function fetchWithRetry(url, options = {}) {\n18: for (let attempt = 0; attempt < retries; attempt++) {\n24: if (!response.ok) await delay(backoff(attempt));\n",
2,
),
step(
"Listed every caller of fetchWithRetry",
"rg -l fetchWithRetry src",
"src/net/fetchWithRetry.ts\nsrc/api/orders.ts\nsrc/api/profile.ts\nsrc/api/search.ts\n",
3,
),
thought(
"None of the callers handle a 404 themselves, so a client error waiting through every backoff is what the reader sees as a slow page. The check belongs in the loop.",
{ startedAt: at(4), endedAt: at(4) + 3_000 },
),
step(
"Read the retry tests",
"sed -n '1,60p' src/net/fetchWithRetry.test.ts",
"describe('fetchWithRetry', () => {\n it('retries a 503 three times', async () => {\n",
5,
),
step("Ran the retry tests before the change", "bun test src/net", "3 pass\n0 fail\n", 6),
step("Checked the branch is clean", "git status --short", "", 7),
],
8.2,
{ id: "organization-run" },
),
assistant(
"The loop retries any response that isn't ok. I'll stop it at 4xx statuses other than 408 and 429, which are worth another try, then add a test for a 404.",
7.8,
),
summary(7.7, { status: "completed", durationMs: 78_000, toolCount: 5 }),
];
}

/// Opens the ended turn's "Worked for" line, then "Ran 5 commands", as the reference shows it.
const openRun: Play = async (ctx) => {
const worked = () => ctx.document.querySelector<HTMLElement>(".cv-worked");
await ctx.waitFor(() => worked() ?? ctx.document.querySelector(".cv-tool[aria-expanded]"), { capMs: 10_000 });
if (worked()?.getAttribute("aria-expanded") === "false") {
await ctx.click({ selector: ".cv-worked" });
await ctx.waitFor(() => worked()?.getAttribute("aria-expanded") === "true", { capMs: 10_000 });
}
const line = () =>
[...ctx.document.querySelectorAll<HTMLButtonElement>("button.cv-tool[aria-expanded]")].find((node) =>
/5/.test(node.textContent ?? ""),
);
await ctx.waitFor(line, { capMs: 10_000 });
if (line()?.getAttribute("aria-expanded") === "false") await ctx.click({ role: "button", name: /5/ });
await ctx.waitFor(() => ctx.document.querySelector(".cv-steps"), { capMs: 10_000 });
};

const variant = (look: "card" | "tree" | "zebra", note: string): AgentPaneVariant => ({
note,
snapshot: chat(turn(), { title: "Stop retrying client errors" }),
documentAttributes: { "data-tool-steps-look": look },
play: openRun,
});

export default agentPaneEntry({
id: "agent-pane.claude-desktop-organization",
title: "Claude desktop organization",
area: "Agent pane",
height: 900,
widths: { narrow: 520, normal: 760, wide: 900 },
covers: [
"agent-session/acpmux/conversation/ToolGroupRow.tsx",
"agent-session/acpmux/conversation/ThoughtRow.tsx#StepThought",
"agent-session/acpmux/conversation/toolGroups.ts",
],
variants: {
card: variant(
"card",
"Selected default: one rounded group in the cards' ring; the leading thought in full, then a row per step with its disclosure right after its words, hairlines between.",
),
tree: variant("tree", "Alternative: the earlier tree guide with the glyphs back, hairlines between steps."),
zebra: variant("zebra", "Alternative: no ring or hairlines; every other step on the cards' fill."),
},
});
56 changes: 56 additions & 0 deletions webviews/src/agent-session/acpmux/conversation/ThoughtRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,59 @@ export const ThoughtRow = memo(function ThoughtRow({
</>
);
});

/// A thought as one step of an opened group (ToolGroupRow.tsx), as Claude Code desktop lists a
/// run's: closed, its words on one line cut with an ellipsis and the disclosure right after them;
/// open, its whole text in that place with the disclosure at the top of the row's end. The thought
/// that led into the run (`lead`) starts open; later ones start closed. The reader's choice is kept
/// by `id` with ThoughtRow's, so a thought that drew on its own keeps it once a call joins it to the
/// group. Find in Chat opens one that holds its match.
export function StepThought({ item, id, lead = false }: { item: AcpmuxActivity; id: string; lead?: boolean }) {
const t = useT();
const text = item.text.trim();
// Read from the kept choices on every draw, so the row follows its `id` even when React reuses it.
const [, redraw] = useState(0);
const open = choices.get(id) ?? lead;
const bodyId = useId();
const choose = (next: boolean) => {
choices.set(id, next);
redraw((count) => count + 1);
};
useFindReveal(
useMemo(() => [item], [item]),
() => choose(true),
);
// One line of its words, its headings' marks left out (thoughtPreview), its breaks as spaces.
const line = useMemo(
() => (open ? "" : text.split("\n").map(thoughtPreview).filter(Boolean).join(" ")),
[open, text],
);
return (
<>
<button
type="button"
className={`cv-tool is-toggle cv-thought cv-step-thought${open ? " is-open" : ""}`}
aria-expanded={open}
aria-controls={open ? bodyId : undefined}
aria-label={open ? t("tools.thought") : undefined}
onClick={() => choose(!open)}
>
{!open && (
<span className="cv-tool__text cv-thought__preview" dir="auto" data-find-skip>
{line}
</span>
)}
<ChevronRight
size={14}
strokeWidth={1.2}
className={`cv-tool__chevron cv-rotor${open ? " is-open" : " is-hover"}`}
/>
</button>
{open && (
<div id={bodyId} className="cv-thought__text cv-step-thought__body selectable" dir="auto">
<ThoughtText text={text} />
</div>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,8 @@ for (const [kind, toolKind] of GROUP_KINDS.slice(1))
test(`an open ${kind} group shows every call`, async () => {
const items = calls(toolKind);
const container = await render(<ToolGroupRow kind={kind} items={items} disclosureKey={`all-${kind}`} />);
expect(container.querySelectorAll(".cv-tool-group > .cv-tool:not(.cv-tool-group__more)")).toHaveLength(
items.length,
);
// One step row per call, each between the group's hairlines (ToolGroupRow.tsx Step).
expect(container.querySelectorAll(".cv-steps > .cv-step > .cv-tool")).toHaveLength(items.length);
expect(container.textContent).not.toMatch(/\+\d+ more/);
for (const item of items) expect(container.textContent).toContain(item.text);
});
Expand Down
Loading
Loading