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: 2 additions & 0 deletions webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,7 @@ const ThinkingRow = memo(
sendTo={row.doingSendTo}
sendSession={row.doingSendSession}
draft={row.doingDraft}
done={row.doingDone}
untimed={row.untimed}
quietKey={row.quietKey}
below={row.below}
Expand All @@ -1402,6 +1403,7 @@ const ThinkingRow = memo(
a.row.doingSendTo === b.row.doingSendTo &&
a.row.doingSendSession === b.row.doingSendSession &&
a.row.doingDraft === b.row.doingDraft &&
a.row.doingDone === b.row.doingDone &&
a.row.untimed === b.row.untimed &&
a.row.quietKey === b.row.quietKey &&
a.row.below === b.row.below &&
Expand Down
22 changes: 14 additions & 8 deletions webviews/src/agent-session/acpmux/conversation/Thinking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ function useQuiet(key: string | undefined, ms = QUIET_MS): boolean {
/// `doing`, `doingAt`, `doingShell`, `draft`, `untimed`, `quietKey`: the Codex-style live line at the
/// bottom of a running turn (conversation/turns.ts `codexLiveTurn`), the turn's one live block, which
/// each step replaces in place: "Thinking 18s" (the turn's time, and its tokens when the harness
/// reports them), or the call in flight ("Running bun test 13s", or what it says it does, "Run the
/// web tests 13s"), its time from the call's start, or "Writing a command 9s" while the agent still
/// writes the command (`draft`). All shimmer. While text streams (`quietKey` moves) the line keeps
/// reports them), the call in flight ("Running bun test 13s", or what it says it does, "Run the
/// web tests 13s"), the latest finished step until the next starts (`done`, "Ran grep … 15s"), or
/// "Writing a command 9s" while the agent still writes the command (`draft`). One clock and one
/// token count for all of them, the turn's, so the line just keeps going. All shimmer. While text streams (`quietKey` moves) the line keeps
/// its height but draws nothing.
export function Thinking({
status,
Expand All @@ -57,6 +58,7 @@ export function Thinking({
sendTo,
sendSession,
draft = false,
done = false,
untimed = false,
quietKey,
below = false,
Expand All @@ -74,6 +76,8 @@ export function Thinking({
sendTo?: string;
sendSession?: string;
draft?: boolean;
/// `doing` is the latest step, finished ("Ran grep …"): the line holds it until the next one.
done?: boolean;
untimed?: boolean;
quietKey?: string;
/// The turn's last row (conversation/turns.ts): as tall as the footer that replaces it.
Expand All @@ -84,7 +88,9 @@ export function Thinking({
} = {}) {
const t = useT();
const waiting = useContext(WaitingOnContext);
const from = doing !== undefined ? doingAt : untimed ? undefined : since;
// The turn's time, from the prompt, whatever the line names: it keeps counting across steps and
// never starts again at a call (Leo, 2026-11-01: "it should just keep going").
const from = untimed ? undefined : since;
const timed = !status && !waiting && from !== undefined;
const time = useTicking(timed, now);
const quiet = useQuiet(status || waiting || doing !== undefined ? undefined : quietKey);
Expand All @@ -97,18 +103,18 @@ export function Thinking({
? t("send.sending", { target: sendName })
: doingShell
? doing
? t("tools.runningCommand", { command: doing })
? t(done ? "tools.ranCommand" : "tools.runningCommand", { command: doing })
: draft
? t("tools.writingCommand")
: t("tools.runningUnnamed")
: t(done ? "tools.ranUnnamed" : "tools.runningUnnamed")
: doing);
const elapsed = timed ? formatDuration(Math.max(0, time - from)) : undefined;
// The turn's generated output tokens so far (WorkingFor.tsx), on the line while it times the turn; and, with the
// running tray off, the work still going in the background, each count opening what it counts
// (App.tsx counts nothing while the tray lists it).
const counts = useContext(WorkingCountsContext);
const tokens = counts?.tokens;
const showTokens = Boolean(tokens) && timed && doing === undefined;
const showTokens = Boolean(tokens) && timed;
const parts = counts && !status && !waiting ? workingParts(t, counts) : [];
return (
<div className={`cv-worked${below ? " is-below" : ""}${quiet ? "" : " is-quiet"}`} data-find-skip>
Expand Down Expand Up @@ -154,7 +160,7 @@ export function Thinking({
</span>
))}
{/* Keyed by the call, so each command's hint waits its moment again. */}
{command && !waiting && doing !== undefined && <BackgroundHint key={doingAt} />}
{command && !done && !waiting && doing !== undefined && <BackgroundHint key={doingAt} />}
{status && onCancel && (
<button type="button" className="cv-thinking__cancel" onClick={onCancel}>
{t("switch.cancelPrompt")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2475,7 +2475,13 @@ export default agentPaneEntry({
note: "Opened tool results read as their content, not the JSON a harness wrapped them in: one-line JSON pretty-printed, an MCP text list as Markdown, a result envelope and Codex's output as their text, a JSON file kept as printed.",
height: 1100,
snapshot: chat(
[user("Stop the watcher, check the issues and run the tests.", 1), activity(ENVELOPE_TOOLS, 0.8)],
// The reply under the calls keeps every call a row: with nothing after it, the live line
// would hold the last finished call instead.
[
user("Stop the watcher, check the issues and run the tests.", 1),
activity(ENVELOPE_TOOLS, 0.8),
assistant("The watcher is stopped and the tests pass.", 0.5),
],
{ isWorking: true },
),
play: async (ctx) => {
Expand Down
66 changes: 64 additions & 2 deletions webviews/src/agent-session/acpmux/conversation/turns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { QUESTION_RECORD } from "../question/record";
import type { Translate } from "../i18n";
import { editedPaths, isBareShellName, toolLabel } from "../toolPaths";
import { shortCommand } from "./shortCommand";
import { awaitsPermission, groupId, isDraftCall, isRunning, toolGroups } from "./toolGroups";
import { awaitsPermission, groupId, isDraftCall, isFailed as callFailed, isRunning, toolGroups } from "./toolGroups";
import { acpmuxMcpSends } from "./acpmuxMcpSends";
import { acpmuxSendCalls } from "./acpmuxSends";
import { subsessionCall } from "./subsessionCalls";
Expand Down Expand Up @@ -975,13 +975,14 @@ function liveTurn(
function codexLiveTurn(
user: AcpmuxRow,
drawn: AcpmuxRow[],
rows: AcpmuxRow[],
runs: AcpmuxRow[],
last: AcpmuxRow | undefined,
named: LineCall | undefined,
at: number,
clockOffset: number,
sessionStatus: boolean,
): AcpmuxRow[] {
let rows = runs;
// The one live block sits at the turn's bottom, as the ChatGPT and Codex apps draw it: no
// "Working for" over the turn (with steers it stood above all of them, far from the line; Leo,
// 2026-10-21). Between steps it reads "Thinking 18s", timed from the prompt.
Expand All @@ -1005,6 +1006,22 @@ function codexLiveTurn(
} else if (last.kind === "assistant") line.quietKey = `${last.id}:${last.version}`;
const answering = last.kind === "assistant";
const plan = sessionStatus ? [...rows].reverse().find((row) => row.kind === "plan" && row.plan?.length) : undefined;
// With a todo list and no answer streaming, the activity line under the list is the live line
// (below), which names the plan's step, so no call leaves its run for it.
if (!named && !answering && !plan) {
// Between calls the line keeps the latest finished step ("Ran grep …") instead of a separate
// "Thinking" under it, shimmering and timed from the prompt like any step: the line is the
// turn's current activity and just keeps going (Leo, 2026-11-01: "this should be replacing
// the thinking area and its shimmer across things", "like the thinking or whatever it should
// just keep going"). The step leaves its run while the line names it, so it reads once.
const lifted = liftedStep(rows, last);
if (lifted) {
rows = lifted.rows;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A completed latest step disappears when a todo list is present: liftedStep removes it, but the plan branch discards the populated line and renders a new activityLine. Preserve the lifted line's label and completion state in this branch, so the live line still says the completed step.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/conversation/turns.ts, line 998:

<comment>A completed latest step disappears when a todo list is present: `liftedStep` removes it, but the plan branch discards the populated `line` and renders a new `activityLine`. Preserve the lifted line's label and completion state in this branch, so the live line still says the completed step.</comment>

<file context>
@@ -986,6 +987,20 @@ function codexLiveTurn(
+    // just keep going"). The step leaves its run while the line names it, so it reads once.
+    const lifted = liftedStep(rows, last);
+    if (lifted) {
+      rows = lifted.rows;
+      line.doing = lifted.doing;
+      line.doingShell = lifted.shell;
</file context>

line.doing = lifted.doing;
line.doingShell = lifted.shell;
line.doingDone = true;
}
}
if (!plan) return [...rows, line];
const work = rows.filter((row) => row !== plan);
// The answer streams under the list, the line under both.
Expand All @@ -1025,6 +1042,51 @@ function codexLiveTurn(
];
}

/// The latest run's last finished call, lifted out of the run for the live line to name: a command
/// as its short line ("Ran grep …"), any other call as its row names it. Only a plain step: a failed
/// call keeps its mark in the run, a command sent to the background keeps its state there, and
/// messages, sub-sessions, render cards and edits keep their own rows. Nil when the latest step is anything else.
function liftedStep(
rows: AcpmuxRow[],
last: AcpmuxRow,
): { rows: AcpmuxRow[]; doing: string; shell: boolean } | undefined {
if (last.kind !== "activity") return undefined;
// A run that a short note joined to an earlier one keeps the earlier one's id (mergeShortNotes).
const at = rows.findIndex((row) => row.id === last.id || row.joined?.includes(last.id));
const run = rows[at];
if (!run?.foldRun) return undefined;
const items = run.items ?? [];
// Thoughts after it are the agent thinking about it; the line still names the call.
let index = items.length - 1;
while (index >= 0 && !items[index]!.tool) index -= 1;
const item = items[index];
const call = item?.tool;
if (
!call ||
isRunning(call) ||
call.background !== undefined ||
callFailed(call) ||
isEditTool(call) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Completed delete and move calls are lifted out of the run even though they are file-change rows that must remain visible. Extend this exclusion to delete and move so their own rows are retained like other edit calls.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/conversation/turns.ts, line 1049:

<comment>Completed `delete` and `move` calls are lifted out of the run even though they are file-change rows that must remain visible. Extend this exclusion to `delete` and `move` so their own rows are retained like other edit calls.</comment>

<file context>
@@ -1008,6 +1023,49 @@ function codexLiveTurn(
+    isRunning(call) ||
+    call.background !== undefined ||
+    callFailed(call) ||
+    isEditTool(call) ||
+    renderCall(call) ||
+    subsessionCall(call) ||
</file context>
Suggested change
isEditTool(call) ||
(isEditTool(call) || call.kind === "delete" || call.kind === "move") ||

renderCall(call) ||
subsessionCall(call) ||
agentMessage(call) ||
acpmuxSendCalls(call) ||
acpmuxMcpSends(call)
)
return undefined;
const shell =
call.kind === "execute" && !call.description && (Boolean(call.command) || isBareShellName(call, call.title));
const doing = shell ? (call.command ? shortCommand(call.command) : "") : runningLabel(call, item!.text);
const kept = items.filter((_, position) => position !== index);
const next = rows.slice();
if (kept.length) next[at] = { ...run, items: kept, version: run.version * LIFTED_STATES + 1 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This encoding can reuse a prior version on the live-to-ended transition, so React may skip the completed run when its activity version changes from 1 to 2 in the same snapshot. Use a distinct state offset for lifted runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/conversation/turns.ts, line 1065:

<comment>This encoding can reuse a prior version on the live-to-ended transition, so React may skip the completed run when its activity version changes from 1 to 2 in the same snapshot. Use a distinct state offset for lifted runs.</comment>

<file context>
@@ -1062,13 +1062,13 @@ function liftedStep(
   const kept = items.filter((_, position) => position !== index);
   const next = rows.slice();
-  if (kept.length) next[at] = { ...run, items: kept, version: run.version * RUN_STATES + LIFTED };
+  if (kept.length) next[at] = { ...run, items: kept, version: run.version * LIFTED_STATES + 1 };
   else next.splice(at, 1);
   return { rows: next, doing, shell };
</file context>
Suggested change
if (kept.length) next[at] = { ...run, items: kept, version: run.version * LIFTED_STATES + 1 };
if (kept.length) next[at] = { ...run, items: kept, version: run.version * RUN_STATES + RUN_STATES };

else next.splice(at, 1);
return { rows: next, doing, shell };
}
/// A run with its last call on the live line differs from the same run whole. (Not a sixth
/// `RUN_STATES`: the minimap's edit-card versions are pinned to five.)
const LIFTED_STATES = 2;

/// The newest call still running in a live turn's latest row.
function runningCall(
row: AcpmuxRow,
Expand Down
3 changes: 3 additions & 0 deletions webviews/src/agent-session/acpmux/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ export type AcpmuxRow = {
/// behind, by id and version, while that text still moves.
doingAt?: number;
doingShell?: boolean;
/// `doing` names the latest step, finished, which the live line holds until the next step
/// (conversation/turns.ts `liftedStep`): "Ran grep …".
doingDone?: boolean;
/// The call in flight sends another agent a message (conversation/turns.ts `sendingTo`: a shell
/// `acpmux send`, a sub-session's `send_message`): the line reads "Sending message to
/// <doingSendTo>", and the sub-session `doingSendSession` by its name once the list has it.
Expand Down
5 changes: 2 additions & 3 deletions webviews/test/agent-pane-peer-chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,9 @@ await requireBrowserLane("agent-pane-peer-chat.test.ts", async () => {
const row = page.locator(".cv-thinking", { hasText: "Run make" });
await row.first().waitFor();
const named = await row.count();
// The call ends: between steps the live line times the turn from the prompt ("Thinking 5s"),
// moved onto this computer's clock.
// The call ends: the live line holds it until the next step and times the turn from the
// prompt ("Run make 5s"), moved onto this computer's clock, as it did while the call ran.
send(3, "tool_call_update", { toolCallId: "t1", status: "completed" });
await row.first().waitFor({ state: "detached" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This no longer waits for any post-completion state: .cv-worked__time already exists while Run make is running, so the assertion can read the pre-completion DOM and miss a continuity regression. Wait for an observable completed state (for example, make the fixture render distinct Running/Ran labels and await Ran, or send a next call and assert the completed call becomes its row).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/test/agent-pane-peer-chat.test.ts, line 394:

<comment>This no longer waits for any post-completion state: `.cv-worked__time` already exists while `Run make` is running, so the assertion can read the pre-completion DOM and miss a continuity regression. Wait for an observable completed state (for example, make the fixture render distinct `Running`/`Ran` labels and await `Ran`, or send a next call and assert the completed call becomes its row).</comment>

<file context>
@@ -388,10 +388,9 @@ await requireBrowserLane("agent-pane-peer-chat.test.ts", async () => {
+      // prompt ("Run make 5s"), moved onto this computer's clock, as it did while the call ran.
       send(3, "tool_call_update", { toolCallId: "t1", status: "completed" });
-      await row.first().waitFor({ state: "detached" });
       const time = page.locator(".cv-worked.is-below .cv-worked__time");
       await time.waitFor();
       const label = (await time.textContent()) ?? "";
</file context>

const time = page.locator(".cv-worked.is-below .cv-worked__time");
await time.waitFor();
const label = (await time.textContent()) ?? "";
Expand Down
15 changes: 14 additions & 1 deletion webviews/test/agent-pane-question-records.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,25 @@ await requireBrowserLane("agent-pane-question-records.test.ts", async () => {
const live = page.locator(".cv-worked.is-below .cv-thinking", { hasText: "Read auth.ts" });
await live.waitFor({ timeout: 5_000 });
const line = await live.boundingBox();
// Done, it is the read's own row, under the record too.
// Done, the line holds it until the next step (Leo, 2026-11-01: "it should just keep going");
// once the next call runs, the read is its own row, under the record too.
push([
"in",
"tool_call_update",
update({ sessionUpdate: "tool_call_update", toolCallId: "read-auth", status: "completed" }),
]);
push([
"in",
"tool_call",
update({
sessionUpdate: "tool_call",
toolCallId: "read-routes",
title: "Read src/routes.ts",
kind: "read",
status: "in_progress",
rawInput: { file_path: "src/routes.ts" },
}),
]);
const after = page.locator(".cv-tool__text", { hasText: "Read src/auth.ts" }).first();
await after.waitFor({ timeout: 5_000 });
const [record, read] = await Promise.all([
Expand Down
7 changes: 4 additions & 3 deletions webviews/test/agent-pane-stream-jitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@
await ended(page, marker);
await rest(page);
const frames = await stop(page);
expect({ drops: drops(frames), atEnd: (await fromEnd(page)) <= 4 }).toEqual({ drops: [], atEnd: true });

Check failure on line 481 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-check (5)

error: expect(received).toEqual(expected)

{ "atEnd": true, - "drops": [], + "drops": [ + "5266ms: -316.8 -> -293.3", + ], } - Expected - 1 + Received + 3 at <anonymous> (/home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:481:75)

Check failure on line 481 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-check (5)

error: expect(received).toEqual(expected)

{ "atEnd": true, - "drops": [], + "drops": [ + "5197ms: -312.2 -> -293.3", + ], } - Expected - 1 + Received + 3 at <anonymous> (/home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:481:75)

Check failure on line 481 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-check (5)

error: expect(received).toEqual(expected)

{ "atEnd": true, - "drops": [], + "drops": [ + "6041ms: -311.0 -> -293.3", + ], } - Expected - 1 + Received + 3 at <anonymous> (/home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:481:75)

Check failure on line 481 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-base-check (5)

error: expect(received).toEqual(expected)

{ "atEnd": true, - "drops": [], + "drops": [ + "5269ms: -315.0 -> -293.3", + ], } - Expected - 1 + Received + 3 at <anonymous> (/home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:481:75)

Check failure on line 481 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-base-check (5)

error: expect(received).toEqual(expected)

{ "atEnd": true, - "drops": [], + "drops": [ + "5201ms: -312.3 -> -293.3", + ], } - Expected - 1 + Received + 3 at <anonymous> (/home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:481:75)
await page.close();
},
);
Expand Down Expand Up @@ -558,10 +558,11 @@
if (!scenario.toolEnding) expect(Math.abs(moved)).toBeLessThanOrEqual(1);
else {
// A finished turn folds its tool rows into "Worked for" above the reply (80e6a34d0b9c,
// on purpose): the tool row that ran after the reply leaves from under it, so the reply
// drops by what went from under it, once, in the settle's frame, and rests there.
// on purpose): whatever ran after the reply leaves from under it, so the reply drops by
// what went from under it, once, in the settle's frame, and rests there. The live line
// holds the finished call until the turn ends (Leo, 2026-11-01: "it should just keep

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This allowance removes the only assertion that the tool-ending fixture actually contributes a tool row. A regression that omits or fails to fold that row can still satisfy the remaining geometry equality; assert the live/finished row content separately while allowing zero height change.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/test/agent-pane-stream-jitter.test.ts, line 563:

<comment>This allowance removes the only assertion that the tool-ending fixture actually contributes a tool row. A regression that omits or fails to fold that row can still satisfy the remaining geometry equality; assert the live/finished row content separately while allowing zero height change.</comment>

<file context>
@@ -558,10 +558,11 @@ await requireBrowserLane("agent-pane-stream-jitter.test.ts", async () => {
-          // drops by what went from under it, once, in the settle's frame, and rests there.
+          // on purpose): whatever ran after the reply leaves from under it, so the reply drops by
+          // what went from under it, once, in the settle's frame, and rests there. The live line
+          // holds the finished call until the turn ends (Leo, 2026-11-01: "it should just keep
+          // going"), and the footer takes the line's place at its height, so that may be nothing.
           const left = before!.under! - after!.under!;
</file context>

// going"), and the footer takes the line's place at its height, so that may be nothing.
const left = before!.under! - after!.under!;
expect(left).toBeGreaterThan(20);
expect(Math.abs(moved - left)).toBeLessThanOrEqual(1);
const rested = measured.filter((frame) => frame.settled).map((frame) => frame.messageBottom!);
expect(Math.max(...rested) - Math.min(...rested)).toBeLessThanOrEqual(1);
Expand Down Expand Up @@ -640,7 +641,7 @@
for (let line = 1; line <= 5; line++) {
await page.keyboard.type(`Line ${line} of a draft`);
await page.keyboard.press("Shift+Enter");
await painted(page, 3);

Check failure on line 644 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-check (5)

error: evaluate: Execution context was destroyed, most likely because of a navigation

at /home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:644:15
}
await painted(page, 12);
await sample(page, marker);
Expand All @@ -650,7 +651,7 @@
// fixed-wait: stream() paces the reply's chunks as acpmux sends them; the cadence is the behavior.
await stream(`q-${turn}`, "then write the changelog line for it", `Next grown marker ${turn}`, 30);
endTurn();
await page.getByText(`Next grown marker ${turn}`).waitFor();

Check failure on line 654 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-check (5)

TimeoutError: waitFor: Timeout 30000ms exceeded.

- waiting for getByText('Next grown marker 1041') to be visible at /home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:654:57
await painted(page, 40);
const frames = await stop(page);
expect(frames.filter((frame) => frame.marker !== undefined).length).toBeGreaterThan(5);
Expand Down Expand Up @@ -748,7 +749,7 @@
const resting = await gapAbove(page);
await watchGap(page, resting);
unqueue("queue_removed", `q-${turn}`);
const after = await closed(page);

Check failure on line 752 in webviews/test/agent-pane-stream-jitter.test.ts

View workflow job for this annotation

GitHub Actions / web / react-apps-check (5)

error: evaluate: Execution context was destroyed, most likely because of a navigation.

at /home/runner/_work/cmux/cmux/webviews/test/agent-pane-stream-jitter.test.ts:752:27
toolDone(`t-${turn}`);
endTurn();
// The last row rests on the stack's top again, a card's height lower, well before a second:
Expand Down
13 changes: 7 additions & 6 deletions webviews/test/agent-pane-subagent-groups-open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ await requireBrowserLane("agent-pane-subagent-groups-open.test.ts", async () =>
"agent_message_chunk",
update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } }),
];
// The read that ends the live turn is its live line ("Read app.ts"), so pages wait on the prompt.
const live: Step[] = [...start, spawned("a1", "Reviewer"), spawned("a2", "Tester"), read("r1", "src/app.ts")];
// Finished chats, their group at the same place as LIVE's.
const done = (answer: string): Step[] => [
Expand Down Expand Up @@ -227,7 +228,7 @@ await requireBrowserLane("agent-pane-subagent-groups-open.test.ts", async () =>
// A cold dev server's first load can stall in CI: load it here, again if it stalls.
for (let attempt = 1; ; attempt++) {
try {
await (await open(LIVE, "Read src/app.ts")).close();
await (await open(LIVE, "Review the branch with a few agents")).close();
break;
} catch (error) {
if (attempt === 3) throw error;
Expand All @@ -241,7 +242,7 @@ await requireBrowserLane("agent-pane-subagent-groups-open.test.ts", async () =>
});

test("a group of running agents starts open; a finished chat's starts closed", async () => {
const page = await open(LIVE, "Read src/app.ts");
const page = await open(LIVE, "Review the branch with a few agents");
await lines(page).first().waitFor({ timeout: 5_000 });
await expectGroups(page, { open: ["true"], lines: 2 });
await page.close();
Expand All @@ -251,7 +252,7 @@ await requireBrowserLane("agent-pane-subagent-groups-open.test.ts", async () =>
});

test("a group the person closes stays closed as its agents work; a later group opens on its own", async () => {
const page = await open(LIVE, "Read src/app.ts");
const page = await open(LIVE, "Review the branch with a few agents");
await lines(page).first().waitFor({ timeout: 5_000 });
await groups(page).first().click();
await expectGroups(page, { open: ["false"], lines: 0 });
Expand Down Expand Up @@ -283,20 +284,20 @@ await requireBrowserLane("agent-pane-subagent-groups-open.test.ts", async () =>
});

test("a running group the person closed stays closed after a switch away and back", async () => {
const page = await open(LIVE, "Read src/app.ts");
const page = await open(LIVE, "Review the branch with a few agents");
await lines(page).first().waitFor({ timeout: 5_000 });
await groups(page).first().click();
await expectGroups(page, { open: ["false"], lines: 0 });
await select(page, DONE);
await page.getByText("Both reviews are in.").waitFor({ timeout: 10_000 });
await select(page, LIVE);
await page.getByText("Read src/app.ts").waitFor({ timeout: 10_000 });
await page.getByText("Read app.ts").first().waitFor({ timeout: 10_000 });
await expectGroups(page, { open: ["false"], lines: 0 });
await page.close();
});

test("an earlier turn's agent a cancel left running opens nothing when the next turn works", async () => {
const page = await open(STALE, "Read src/tests.ts");
const page = await open(STALE, "Now fix the tests");
await expectGroups(page, { open: ["false"], lines: 0 });
await page.close();
});
Expand Down
24 changes: 23 additions & 1 deletion webviews/test/browser/transcript-follow.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const view = createRoot(root);
// "Worked for" lines open and close as the pane's do: a click toggles the turn in `expanded`.
let shown: AcpmuxRow[] = [];
let rawRows = false;
// A running turn as the pane draws it by default (`__showLiveRows`): the Codex-style runs and the one
// live line at the bottom (conversation/turns.ts codexLiveTurn).
let live = false;
let expanded = new Set<string>();
// Prompts "sent" from the pane's composer (App's `chat.send`), which put the view at its latest message.
let sends = 0;
Expand All @@ -57,7 +60,13 @@ let mediaMount: { root: ReturnType<typeof createRoot>; host: HTMLElement } | und
const draw = () =>
view.render(
<VirtualTranscript
rows={rawRows ? shown : turnView(shown, expanded)}
rows={
rawRows
? shown
: live
? turnView(shown, expanded, { working: true, liveRuns: true, lineBelow: true })
: turnView(shown, expanded)
}
sessionId={sessionId}
onToggleActivity={(id) => {
expanded = toggleTurnFold(shown, expanded, id);
Expand All @@ -74,6 +83,7 @@ const draw = () =>
);
(window as unknown as { __showRows: (rows: AcpmuxRow[]) => void }).__showRows = (rows) => {
rawRows = false;
live = false;
shown = rows;
draw();
};
Expand Down Expand Up @@ -168,6 +178,18 @@ const draw = () =>
shown = rows;
draw();
};
// The pane's English strings, for a test that reads the words a row draws (the rest read keys).
(window as unknown as { __loadStrings: () => Promise<void> }).__loadStrings = async () => {
globalThis.__cmuxPaneStrings ??= (await import("../../src/agent-session/acpmux/generated/strings.json")).default;
const { setPaneLanguage } = await import("../../src/agent-session/acpmux/i18n");
setPaneLanguage("en");
};
(window as unknown as { __showLiveRows: (rows: AcpmuxRow[]) => void }).__showLiveRows = (rows) => {
rawRows = false;
live = true;
shown = rows;
flushSync(draw);
};
(window as unknown as { __showRowsSync: (rows: AcpmuxRow[]) => void }).__showRowsSync = (rows) => {
shown = rows;
flushSync(draw);
Expand Down
Loading
Loading