From e6a20bd285611cb84657ac5ddd572c378d347786 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 9 Sep 2026 11:46:10 +0200 Subject: [PATCH 1/9] move the subagent expand affordance onto the summary line --- .../res-1327-expand-arrow-summary-line.md | 1 + .../src/modes/agents-view/agents-view-mode.ts | 40 ++++--------- .../test/agents-view-mode.test.ts | 60 ++++++++++++------- 3 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md diff --git a/packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md b/packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md new file mode 100644 index 0000000000..8653768cca --- /dev/null +++ b/packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md @@ -0,0 +1 @@ +- Changed the agents view subagent expand/collapse control: the arrow now sits on the always-visible subagent summary line instead of hiding on the session row. diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index 1433d107b3..39fc37d0f6 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -980,7 +980,7 @@ export class AgentsViewMode implements Component, Focusable { } if (this.keybindings.matches(data, "app.agents.expand")) { const row = this.rows[this.selectedIndex]; - if (row && row.descendantCount > 0) this.toggleSubagentList(row); + if (row && (row.kind === "subagent-summary" || row.descendantCount > 0)) this.toggleSubagentList(row); return; } } @@ -1320,7 +1320,7 @@ export class AgentsViewMode implements Component, Focusable { computeRecursiveRollups(this.unifiedRecords, this.unifiedIndex), this.anchorSessionId, ); - this.rows = compactSessionRows(this.allRows); + this.rows = this.allRows; const index = selectedIdentity === undefined ? -1 : this.rows.findIndex((row) => row.identity === selectedIdentity); if (index >= 0) { @@ -1400,6 +1400,10 @@ export class AgentsViewMode implements Component, Focusable { if (!row?.selectable || this.isPendingDeleteRow(row)) { return; } + if (row.kind === "subagent-summary") { + this.toggleSubagentList(row); + return; + } if (row.kind === "subagent") { this.openSelectedSubagent(row); return; @@ -1420,7 +1424,7 @@ export class AgentsViewMode implements Component, Focusable { } private toggleSubagentList(row: AgentsViewRow): void { - const target = row.identity; + const target = row.kind === "subagent-summary" ? (row.parentIdentity ?? row.identity) : row.identity; if (this.expandedSubagentParents.has(target)) { this.expandedSubagentParents.delete(target); this.programShownParents.delete(target); @@ -2187,7 +2191,7 @@ export class AgentsViewMode implements Component, Focusable { computeRecursiveRollups(this.unifiedRecords, this.unifiedIndex), this.anchorSessionId, ); - this.rows = compactSessionRows(this.allRows); + this.rows = this.allRows; this.applyPendingAncestorExpansion(); this.restoreSelection(); this.ui.requestRender(); @@ -2496,13 +2500,6 @@ export class AgentsViewMode implements Component, Focusable { displayItems.push({ type: "heading", section }); for (const row of getDisplayRowsForSection(this.rows, section)) { displayItems.push({ type: "row", row }); - if ( - (row.kind === "agent" || row.kind === "subagent") && - row.runningSubagentCount > 0 && - !this.expandedSubagentParents.has(row.identity) - ) { - displayItems.push({ type: "running-subagents", row }); - } } } if (displayItems.length === 0) { @@ -2525,14 +2522,6 @@ export class AgentsViewMode implements Component, Focusable { const sliceStart = selectedDisplayIndex >= start + contentRows ? selectedDisplayIndex - contentRows + 1 : start; const lines = displayItems.slice(sliceStart, sliceStart + contentRows).map((item) => { if (item.type === "spacer") return ""; - if (item.type === "running-subagents") { - const count = item.row.runningSubagentCount; - const indent = " ".repeat(item.row.depth + 1); - return theme.fg( - "success", - truncateToWidth(`${indent}${count} subagent${count === 1 ? "" : "s"} running`, width), - ); - } if (item.type === "heading") { return theme.bold(truncateToWidth(`${sectionTitle(item.section)} (${counts[item.section]})`, width)); } @@ -2552,6 +2541,10 @@ export class AgentsViewMode implements Component, Focusable { const selected = row.selectable && row.identity === this.rows[this.selectedIndex]?.identity; const markRow = (line: string): string => (selected ? `${SELECTED_ROW_MARKER}${line}` : line); if (row.kind === "subagent-code") return this.renderCodeRow(row); + if (row.kind === "subagent-summary") { + const indent = " ".repeat(row.depth); + return markRow(formatTableCell(`${indent}${row.expanded ? "▾" : "▸"} ${row.title}`, width)); + } const pendingDelete = row.kind === "agent" && this.isPendingDeleteRow(row); const pendingKill = row.kind === "subagent" && this.isPendingKillSubagentRow(row); const details = layout.details.get(row.identity) ?? ""; @@ -2565,10 +2558,9 @@ export class AgentsViewMode implements Component, Focusable { return markRow(formatTableCell(theme.fg("error", title), width)); } const icon = this.formatRowIcon(row.section, this.getRowIcon(row.section)); - const expand = row.descendantCount > 0 ? (this.expandedSubagentParents.has(row.identity) ? "▾" : "▸") : " "; const badge = formatHeartbeatBadge(row.heartbeat); const heartbeat = badge ? `${theme.fg((row.heartbeat?.activeCount ?? 0) > 0 ? "error" : "dim", badge)} ` : ""; - const title = `${" ".repeat(row.depth)}${icon}${expand} ${heartbeat}${styleRowTitle(row)}`; + const title = `${" ".repeat(row.depth)}${icon} ${heartbeat}${styleRowTitle(row)}`; const status = row.summary.statusLabel !== undefined || row.summary.lastHeardFromAt !== undefined ? row.statusLabel @@ -2764,14 +2756,8 @@ export class AgentsViewMode implements Component, Focusable { type DisplayItem = | { type: "spacer" } | { type: "heading"; section: AgentsViewSection } - | { type: "running-subagents"; row: AgentsViewRow } | { type: "row"; row: AgentsViewRow }; -// Summary rows fold into the running-subagents display items. -function compactSessionRows(rows: readonly AgentsViewRow[]): AgentsViewRow[] { - return rows.filter((row) => row.kind !== "subagent-summary"); -} - // Nested rows (subagent summaries and expanded subagents) always render in // their top-level agent's section block, regardless of their own section. function getDisplayRowsForSection(rows: readonly AgentsViewRow[], section: AgentsViewSection): AgentsViewRow[] { diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index 243dfbd125..8f82e9b1e7 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -691,7 +691,7 @@ describe("AgentsViewMode", () => { expect( expandedRows.find((row) => row.kind === "agent" && row.summary.sessionId === "root-session")?.identity, ).toBe("file:/tmp/root.jsonl"); - expect(expandedRows.some((row) => row.kind === "subagent-summary")).toBe(false); + expect(expandedRows.some((row) => row.kind === "subagent-summary")).toBe(true); expect(expandedRows.some((row) => row.kind === "subagent" && row.summary.sessionId === "child-session")).toBe( true, ); @@ -854,7 +854,7 @@ describe("AgentsViewMode", () => { expect(lines.some((line) => line.startsWith("Idle"))).toBe(true); expect(lines.join("\n")).not.toMatch(/show program|#sub|\$agent|↑in|↓out/); const rows = Reflect.get(view, "rows") as AgentsViewRow[]; - expect(rows.filter((row) => row.kind === "subagent-summary")).toHaveLength(0); + expect(rows.filter((row) => row.kind === "subagent-summary")).toHaveLength(1); for (const line of rendered) { expect(invoke("finalizeRenderedLine", view, line, 120)).not.toContain("\x1b[48"); } @@ -922,7 +922,7 @@ describe("AgentsViewMode", () => { try { Reflect.set(view, "lastListedSummaries", [parent, child]); invoke("reconcileCatalogs", view); - expect(rows().map((row) => row.kind)).toEqual(["agent"]); + expect(rows().map((row) => row.kind)).toEqual(["agent", "subagent-summary"]); const finish = vi.fn(); Reflect.set(view, "finish", finish); invoke("openSelected", view); @@ -935,7 +935,6 @@ describe("AgentsViewMode", () => { invoke("cycleProgramForSelected", view); expect(rows().some((row) => row.kind === "subagent-code" && row.code === child.spawnCode)).toBe(true); expect(rows().some((row) => row.kind === "subagent" && row.summary.sessionId === child.sessionId)).toBe(true); - expect(rows().some((row) => row.kind === "subagent-summary")).toBe(false); invoke("cycleProgramForSelected", view); expect(rows().some((row) => row.kind === "subagent-code")).toBe(false); } finally { @@ -993,7 +992,7 @@ describe("AgentsViewMode", () => { } }); - it("shows running-subagent counts only while collapsed and work remains", () => { + it("puts the expand affordance on the subagent summary line instead of the session row", () => { const parent = summary({ sessionName: "parent" }); const child = summary({ id: "child", @@ -1012,37 +1011,52 @@ describe("AgentsViewMode", () => { sessionId: "child-session-2", sessionFile: "/tmp/child-2.jsonl", }; + const childless = summary({ + id: "solo", + activeSessionId: "solo", + sessionId: "solo-session", + sessionName: "solo", + }); const view = new AgentsViewMode({ config: {}, uiServices: createUiServices() }, {}); const rows = () => Reflect.get(view, "rows") as AgentsViewRow[]; - const lines = () => (invoke("renderSessionRows", view, 120, 20) as string[]).map(stripAnsi); + const renderRow = (row: AgentsViewRow) => + (invoke("renderRow", view, row, 120) as string).replace("\0agents-view-selected-row\0", ""); + const summaryRow = () => rows().find((row) => row.kind === "subagent-summary")!; try { - Reflect.set(view, "lastListedSummaries", [parent, child, secondChild]); + Reflect.set(view, "lastListedSummaries", [parent, child, secondChild, childless]); invoke("reconcileCatalogs", view); - expect(rows()).toHaveLength(1); - expect(invoke("renderRow", view, rows()[0], 120)).toContain("▸"); - const collapsed = lines(); - const parentIndex = collapsed.findIndex((line) => line.includes("parent")); - expect(collapsed[parentIndex + 1]).toBe(" 2 subagents running"); - invoke("moveSelection", view, 1); - expect(Reflect.get(view, "selectedIndex")).toBe(0); - view.handleInput("\x1b[1;3C"); - expect(rows().map((row) => row.kind)).toEqual(["agent", "subagent", "subagent"]); - expect(lines().join("\n")).not.toContain("subagents running"); - expect(invoke("renderRow", view, rows()[0], 120)).toContain("▾"); + Reflect.set(view, "selectedIndex", -1); + expect(rows().map((row) => row.kind)).toEqual(["agent", "subagent-summary", "agent"]); + // Session rows carry no arrow; the summary line is the visible control. + for (const row of rows().filter((r) => r.kind === "agent")) { + expect(stripAnsi(renderRow(row))).not.toMatch(/[▸▾]/); + } + const collapsedLine = renderRow(summaryRow()); + expect(stripAnsi(collapsedLine).trimEnd()).toBe(" ▸ 2 subagents running"); + // Normal foreground: no dim/success styling on the summary line. + expect(collapsedLine).toBe(stripAnsi(collapsedLine)); + // Expand from the summary row itself (keybinding unchanged). + Reflect.set(view, "selectedIndex", 1); view.handleInput("\x1b[1;3C"); - expect(rows()).toHaveLength(1); - expect(lines()).toContain(" 2 subagents running"); + expect(rows().map((row) => row.kind)).toEqual(["agent", "subagent-summary", "subagent", "subagent", "agent"]); + expect(stripAnsi(renderRow(summaryRow())).trimEnd()).toBe(" ▾ 2 subagents running"); + // Enter on the summary row collapses it again. + invoke("openSelected", view); + expect(rows().some((row) => row.kind === "subagent")).toBe(false); + expect(stripAnsi(renderRow(summaryRow()))).toContain("▸ 2 subagents running"); const idleChild = { ...child, activity: "idle", isStreaming: false }; - Reflect.set(view, "lastListedSummaries", [parent, idleChild, secondChild]); + Reflect.set(view, "lastListedSummaries", [parent, idleChild, secondChild, childless]); invoke("reconcileCatalogs", view); - expect(lines()).toContain(" 1 subagent running"); + expect(stripAnsi(renderRow(summaryRow()))).toContain("▸ 1 subagent running"); Reflect.set(view, "lastListedSummaries", [ parent, idleChild, { ...secondChild, activity: "idle", isStreaming: false }, + childless, ]); invoke("reconcileCatalogs", view); - expect(lines().join("\n")).not.toMatch(/subagents? running/); + // Finished subagents keep a visible, expandable summary line. + expect(stripAnsi(renderRow(summaryRow()))).toContain("▸ 2 subagents"); } finally { stopThemeWatcher(); } From d86cadfc934f74fabf2ed1c7dddc7e03eb5686b1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 9 Sep 2026 12:06:08 +0200 Subject: [PATCH 2/9] own the summary row cleanly: parent-backed actions panel, session-only expansion keys --- .../src/modes/agents-view/agents-view-mode.ts | 9 +++- .../test/agents-view-mode.test.ts | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index 39fc37d0f6..4e4557aef4 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -1269,6 +1269,8 @@ export class AgentsViewMode implements Component, Focusable { while (added) { added = false; for (const row of this.rows) { + // Summary/code rows reuse their parent's summary; only session rows own expansion keys. + if (row.kind !== "agent" && row.kind !== "subagent") continue; if (wanted.has(row.summary.sessionId) && !this.expandedSubagentParents.has(row.identity)) { this.expandedSubagentParents.add(row.identity); added = true; @@ -2576,7 +2578,12 @@ export class AgentsViewMode implements Component, Focusable { } private renderActions(width: number): string[] { - const row = this.rows[this.selectedIndex]; + const selected = this.rows[this.selectedIndex]; + // The summary row is an expansion control; its details are the owning row's. + const row = + selected?.kind === "subagent-summary" + ? this.rows.find((candidate) => candidate.identity === selected.parentIdentity) + : selected; const actions = [ `${keyText("tui.select.confirm")} open ${keyText("app.agents.open")} open ${keyText("app.agents.new")} new`, `${keyText("app.agents.expand")} expand/collapse subagents ${keyText("app.agents.program")} program`, diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index 8f82e9b1e7..d5688bf392 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -704,6 +704,31 @@ describe("AgentsViewMode", () => { expect(collapsedView.expandedSubagentParents.size).toBe(0); }); + it("records only session-row identities when re-expanding pending ancestors", () => { + const parent = summary({ sessionName: "parent" }); + const child = summary({ + id: "child", + activeSessionId: "child", + sessionId: "child-session", + sessionFile: "/tmp/child.jsonl", + runtimeKind: "subagent", + parentActiveSessionId: parent.activeSessionId, + }); + const view = new AgentsViewMode({ config: {}, uiServices: createUiServices() }, {}); + try { + Reflect.set(view, "lastListedSummaries", [parent, child]); + invoke("reconcileCatalogs", view); + const persistentState = Reflect.get(view, "persistentState") as AgentsViewPersistentState; + persistentState.pendingExpandedAncestorSessionIds = [parent.sessionId]; + invoke("applyPendingAncestorExpansion", view); + const expanded = Reflect.get(view, "expandedSubagentParents") as Set; + expect(expanded).toEqual(new Set(["file:/tmp/scope.jsonl"])); + expect((Reflect.get(view, "rows") as AgentsViewRow[]).some((row) => row.kind === "subagent")).toBe(true); + } finally { + stopThemeWatcher(); + } + }); + it("toggles subagent list expansion from the parent row", () => { const expandedSubagentParents = new Set(["root-row"]); const programShownParents = new Set(["root-row"]); @@ -976,12 +1001,30 @@ describe("AgentsViewMode", () => { try { Reflect.set(view, "lastListedSummaries", [ summary({ sessionName: "parent", usage: { inputTokens: 1234, outputTokens: 56, cost: 1.23 } }), + summary({ + id: "child", + activeSessionId: "child", + sessionId: "child-session", + sessionFile: "/tmp/child.jsonl", + runtimeKind: "subagent", + parentActiveSessionId: "scope-active", + usage: { inputTokens: 10, outputTokens: 2, cost: 0.5 }, + }), ]); invoke("reconcileCatalogs", view); + // The summary row is an expansion control: its actions show the parent's data. + const builtRows = Reflect.get(view, "rows") as AgentsViewRow[]; + Reflect.set( + view, + "selectedIndex", + builtRows.findIndex((row) => row.kind === "subagent-summary"), + ); view.handleInput("?"); const actions = (invoke("renderSessionRows", view, 120, 20) as string[]).map(stripAnsi).join("\n"); + expect(actions).toContain("parent"); expect(actions).toContain("1234 in"); expect(actions).toContain("$1.23"); + expect(actions).toContain("$1.73 including subagents"); view.handleInput("p"); expect(Reflect.get(view, "showActions")).toBe(false); const rows = (invoke("renderSessionRows", view, 120, 20) as string[]).map(stripAnsi).join("\n"); From 799047e58a93bc1e667f91e5bd30e220281a4b43 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 10 Sep 2026 11:35:05 +0200 Subject: [PATCH 3/9] collapse the rows/allRows dual field left by the summary-row refactor --- .../src/modes/agents-view/agents-view-mode.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index 4e4557aef4..de802037ff 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -665,7 +665,6 @@ export class AgentsViewMode implements Component, Focusable { private deleteConfirmTimer: ReturnType | undefined; private workingIconFrame = 0; private rows: AgentsViewRow[] = []; - private allRows: AgentsViewRow[] = []; private showActions = false; private lastListedSummaries: SessionSummary[] = []; private lastVisibleSummaries: SessionSummary[] = []; @@ -1314,7 +1313,7 @@ export class AgentsViewMode implements Component, Focusable { /** Rebuild rows from the last fetched summaries, keeping selection on the same row. */ private rebuildRows(): void { const selectedIdentity = this.rows[this.selectedIndex]?.identity; - this.allRows = buildAgentsViewRows( + this.rows = buildAgentsViewRows( this.getFilteredRecords(), this.expandedSubagentParents, this.programShownParents, @@ -1322,7 +1321,6 @@ export class AgentsViewMode implements Component, Focusable { computeRecursiveRollups(this.unifiedRecords, this.unifiedIndex), this.anchorSessionId, ); - this.rows = this.allRows; const index = selectedIdentity === undefined ? -1 : this.rows.findIndex((row) => row.identity === selectedIdentity); if (index >= 0) { @@ -1470,7 +1468,7 @@ export class AgentsViewMode implements Component, Focusable { /** Whether any subagent under the given agent identity carries spawn code. */ private targetHasSpawnCode(target: string): boolean { - for (const row of this.allRows) { + for (const row of this.rows) { if (row.parentIdentity !== target) { continue; } @@ -2185,7 +2183,7 @@ export class AgentsViewMode implements Component, Focusable { } } this.scopedRecords = scopeToSessionSubtree(this.unifiedRecords, this.scopeKey, this.unifiedIndex); - this.allRows = buildAgentsViewRows( + this.rows = buildAgentsViewRows( this.getFilteredRecords(), this.expandedSubagentParents, this.programShownParents, @@ -2193,7 +2191,6 @@ export class AgentsViewMode implements Component, Focusable { computeRecursiveRollups(this.unifiedRecords, this.unifiedIndex), this.anchorSessionId, ); - this.rows = this.allRows; this.applyPendingAncestorExpansion(); this.restoreSelection(); this.ui.requestRender(); @@ -2486,7 +2483,7 @@ export class AgentsViewMode implements Component, Focusable { } private getAgentCountsText(): string { - const counts = countRowsBySection(this.allRows); + const counts = countRowsBySection(this.rows); return `${counts.running} running, ${counts.idle} idle, ${counts.inactive} inactive`; } @@ -2495,7 +2492,7 @@ export class AgentsViewMode implements Component, Focusable { if (this.showActions) return this.renderActions(width).slice(0, maxRows); const layout = buildCompactAgentsViewLayout(this.rows, width); const displayItems: DisplayItem[] = []; - const counts = countRowsBySection(this.allRows.length > 0 ? this.allRows : this.rows); + const counts = countRowsBySection(this.rows); for (const section of ["running", "idle", "inactive"] as const) { if (counts[section] === 0) continue; if (displayItems.length > 0) displayItems.push({ type: "spacer" }); From 3414dcf4ca6b603eacf177b1a532f18ed2e94d2f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Sep 2026 21:01:39 +0200 Subject: [PATCH 4/9] show contextual arrow hints in the agents view tray The tray now names what the arrows do on the selected row: open, or expand/collapse on a subagent summary line, plus a parent hint only inside an agent scope. The ? actions hint is gone from the tray; the overlay itself stays reachable. --- .../res-1327-expand-arrow-summary-line.md | 1 + .../src/modes/agents-view/agents-view-mode.ts | 14 ++++++++- .../test/agents-view-mode.test.ts | 30 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md b/packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md index 8653768cca..50b2d1aeb4 100644 --- a/packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md +++ b/packages/coding-agent/.changes/res-1327-expand-arrow-summary-line.md @@ -1 +1,2 @@ - Changed the agents view subagent expand/collapse control: the arrow now sits on the always-visible subagent summary line instead of hiding on the session row. +- Changed the agents view hint tray to describe the arrow keys in context — `→ open`, `→ expand`/`→ collapse` on a subagent summary line, and `← parent` only inside an agent scope — in place of the `?` actions hint. diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index de802037ff..23ce06be56 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -2688,7 +2688,19 @@ export class AgentsViewMode implements Component, Focusable { if (this.replyTarget) { return truncateToWidth(theme.fg("muted", this.renderReplyComposerHints()), width); } - const hints = `${keyText("tui.select.up")}/${keyText("tui.select.down")} navigate ${keyText("tui.select.confirm")} open ${keyText("app.agents.new")} new ${keyText("app.shortcuts")} actions`; + const selected = this.rows[this.selectedIndex]; + // Right toggles the list on a summary row and opens everywhere else; Left + // only has a parent scope to return to below the root view. + const rightAction = selected?.kind === "subagent-summary" ? (selected.expanded ? "collapse" : "expand") : "open"; + const hints = [ + `${keyText("tui.select.up")}/${keyText("tui.select.down")} navigate`, + `${keyText("tui.select.confirm")} open`, + `${keyText("app.agents.new")} new`, + `${keyText("app.agents.open")} ${rightAction}`, + this.scopeRootSummary ? `${keyText("app.agents.back")} parent` : undefined, + ] + .filter((hint): hint is string => hint !== undefined) + .join(" "); return truncateToWidth(theme.fg("muted", hints), width); } diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index d5688bf392..926fe49aab 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -1105,6 +1105,36 @@ describe("AgentsViewMode", () => { } }); + it("adapts the tray hints to the selected row and the scope", () => { + const parent = summary({ sessionName: "parent" }); + const child = summary({ + id: "child", + activeSessionId: "child", + sessionId: "child-session", + sessionFile: "/tmp/child.jsonl", + runtimeKind: "subagent", + parentActiveSessionId: parent.activeSessionId, + }); + const view = new AgentsViewMode({ config: {}, uiServices: createUiServices() }, {}); + const hints = () => stripAnsi(invoke("renderHints", view, 200) as string); + try { + Reflect.set(view, "lastListedSummaries", [parent, child]); + invoke("reconcileCatalogs", view); + Reflect.set(view, "selectedIndex", 0); + expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → open"); + // Right toggles the summary row, so its hint follows the expansion state. + Reflect.set(view, "selectedIndex", 1); + expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → expand"); + view.handleInput("\x1b[C"); + expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → collapse"); + // Only a scoped view has a parent to return to. + Reflect.set(view, "scopeRootSummary", parent); + expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → collapse ← parent"); + } finally { + stopThemeWatcher(); + } + }); + it("dims a paused-only heartbeat badge and keeps active badges in the error color", () => { const job = (status: "active" | "paused") => ({ job: { From 35b56a706b4c8507899adfcf0815c91e0a201980 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Sep 2026 21:06:59 +0200 Subject: [PATCH 5/9] fix(agents-view): render the Enter hint verb from the selected row --- .../coding-agent/src/modes/agents-view/agents-view-mode.ts | 6 +++--- packages/coding-agent/test/agents-view-mode.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index 23ce06be56..a09b561ce1 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -2689,12 +2689,12 @@ export class AgentsViewMode implements Component, Focusable { return truncateToWidth(theme.fg("muted", this.renderReplyComposerHints()), width); } const selected = this.rows[this.selectedIndex]; - // Right toggles the list on a summary row and opens everywhere else; Left - // only has a parent scope to return to below the root view. + // Enter and Right both toggle the list on a summary row and open everywhere + // else; Left only has a parent scope to return to below the root view. const rightAction = selected?.kind === "subagent-summary" ? (selected.expanded ? "collapse" : "expand") : "open"; const hints = [ `${keyText("tui.select.up")}/${keyText("tui.select.down")} navigate`, - `${keyText("tui.select.confirm")} open`, + `${keyText("tui.select.confirm")} ${rightAction}`, `${keyText("app.agents.new")} new`, `${keyText("app.agents.open")} ${rightAction}`, this.scopeRootSummary ? `${keyText("app.agents.back")} parent` : undefined, diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index 926fe49aab..10d267596a 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -1124,12 +1124,12 @@ describe("AgentsViewMode", () => { expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → open"); // Right toggles the summary row, so its hint follows the expansion state. Reflect.set(view, "selectedIndex", 1); - expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → expand"); + expect(hints()).toBe("↑/↓ navigate Enter expand Ctrl+N new → expand"); view.handleInput("\x1b[C"); - expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → collapse"); + expect(hints()).toBe("↑/↓ navigate Enter collapse Ctrl+N new → collapse"); // Only a scoped view has a parent to return to. Reflect.set(view, "scopeRootSummary", parent); - expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → collapse ← parent"); + expect(hints()).toBe("↑/↓ navigate Enter collapse Ctrl+N new → collapse ← parent"); } finally { stopThemeWatcher(); } From 6ac6b08f21445826197591f7da6fcb303c7a5ef6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Sep 2026 21:16:42 +0200 Subject: [PATCH 6/9] fix(agents-view): merge the Enter and Right hints into one entry --- .../src/modes/agents-view/agents-view-mode.ts | 3 +-- packages/coding-agent/test/agents-view-mode.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index a09b561ce1..b3d2bf3db0 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -2694,9 +2694,8 @@ export class AgentsViewMode implements Component, Focusable { const rightAction = selected?.kind === "subagent-summary" ? (selected.expanded ? "collapse" : "expand") : "open"; const hints = [ `${keyText("tui.select.up")}/${keyText("tui.select.down")} navigate`, - `${keyText("tui.select.confirm")} ${rightAction}`, + `${keyText("tui.select.confirm")}/${keyText("app.agents.open")} ${rightAction}`, `${keyText("app.agents.new")} new`, - `${keyText("app.agents.open")} ${rightAction}`, this.scopeRootSummary ? `${keyText("app.agents.back")} parent` : undefined, ] .filter((hint): hint is string => hint !== undefined) diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index 10d267596a..65a3082b73 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -1121,15 +1121,15 @@ describe("AgentsViewMode", () => { Reflect.set(view, "lastListedSummaries", [parent, child]); invoke("reconcileCatalogs", view); Reflect.set(view, "selectedIndex", 0); - expect(hints()).toBe("↑/↓ navigate Enter open Ctrl+N new → open"); + expect(hints()).toBe("↑/↓ navigate Enter/→ open Ctrl+N new"); // Right toggles the summary row, so its hint follows the expansion state. Reflect.set(view, "selectedIndex", 1); - expect(hints()).toBe("↑/↓ navigate Enter expand Ctrl+N new → expand"); + expect(hints()).toBe("↑/↓ navigate Enter/→ expand Ctrl+N new"); view.handleInput("\x1b[C"); - expect(hints()).toBe("↑/↓ navigate Enter collapse Ctrl+N new → collapse"); + expect(hints()).toBe("↑/↓ navigate Enter/→ collapse Ctrl+N new"); // Only a scoped view has a parent to return to. Reflect.set(view, "scopeRootSummary", parent); - expect(hints()).toBe("↑/↓ navigate Enter collapse Ctrl+N new → collapse ← parent"); + expect(hints()).toBe("↑/↓ navigate Enter/→ collapse Ctrl+N new ← parent"); } finally { stopThemeWatcher(); } From 3fe438c8c922b79f4ffaadb614df491c235634f8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 11 Sep 2026 21:21:43 +0200 Subject: [PATCH 7/9] fix(agents-view): group the navigation hints before the new-session hint --- packages/coding-agent/src/modes/agents-view/agents-view-mode.ts | 2 +- packages/coding-agent/test/agents-view-mode.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index b3d2bf3db0..3554438e02 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -2695,8 +2695,8 @@ export class AgentsViewMode implements Component, Focusable { const hints = [ `${keyText("tui.select.up")}/${keyText("tui.select.down")} navigate`, `${keyText("tui.select.confirm")}/${keyText("app.agents.open")} ${rightAction}`, - `${keyText("app.agents.new")} new`, this.scopeRootSummary ? `${keyText("app.agents.back")} parent` : undefined, + `${keyText("app.agents.new")} new`, ] .filter((hint): hint is string => hint !== undefined) .join(" "); diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index 65a3082b73..05d21c805e 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -1129,7 +1129,7 @@ describe("AgentsViewMode", () => { expect(hints()).toBe("↑/↓ navigate Enter/→ collapse Ctrl+N new"); // Only a scoped view has a parent to return to. Reflect.set(view, "scopeRootSummary", parent); - expect(hints()).toBe("↑/↓ navigate Enter/→ collapse Ctrl+N new ← parent"); + expect(hints()).toBe("↑/↓ navigate Enter/→ collapse ← parent Ctrl+N new"); } finally { stopThemeWatcher(); } From 1dc8dd47ad4aeb48cf8c2c79cdf47247a20f1eab Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Fri, 11 Sep 2026 20:41:05 -0700 Subject: [PATCH 8/9] test: stabilize agent trace upload timing --- packages/coding-agent/test/agent-traces.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/test/agent-traces.test.ts b/packages/coding-agent/test/agent-traces.test.ts index 8d73ad9655..5169bacee0 100644 --- a/packages/coding-agent/test/agent-traces.test.ts +++ b/packages/coding-agent/test/agent-traces.test.ts @@ -116,7 +116,7 @@ function writeLedgerOutboxEntry(agentDir: string, ledgerFile: string, uploadedBy } async function advanceTimersUntil(condition: () => boolean): Promise { - for (let step = 0; step < 200 && !condition(); step += 1) { + for (let step = 0; step < 1_000 && !condition(); step += 1) { await stat(new URL(import.meta.url)); if (!condition() && vi.getTimerCount() > 0) { await vi.advanceTimersToNextTimerAsync(); @@ -410,6 +410,9 @@ describe("agent trace upload", () => { sessionManager.appendMessage(createAssistantMessage("hi")); await advanceTimersUntil(() => calls.length === 1); expect(calls[0].url).toBe("https://api.example.test/api/v1/agent-traces/sessions/listener-session"); + const sessionFile = sessionManager.getSessionFile()!; + const signature = await stat(sessionFile); + await advanceTimersUntil(() => readOutboxEntry(tempDir, sessionFile)?.size === signature.size); }); it("coalesces new content that persists during an in-flight upload into one follow-up upload", async () => { From 9254a97667e09cec9e346bf7f8a3885ff4628cb3 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Fri, 11 Sep 2026 23:37:14 -0700 Subject: [PATCH 9/9] fix: preserve summary selection across identity changes --- .../src/modes/agents-view/agents-view-mode.ts | 9 --------- .../src/modes/agents-view/agents-view-state.ts | 9 +++++++-- packages/coding-agent/test/agents-view-mode.test.ts | 6 ++++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts index d57ddbe387..a2a96dce06 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-mode.ts @@ -2313,15 +2313,6 @@ export class AgentsViewMode implements Component, Focusable { return; } const selectedIdentity = this.selectedRowIdentity ?? this.persistentState.selectedRowIdentity; - const exactIndex = - selectedIdentity === undefined - ? -1 - : this.rows.findIndex((row) => row.selectable && row.identity === selectedIdentity); - if (exactIndex >= 0) { - this.selectedIndex = exactIndex; - this.syncSelectedRowState(); - return; - } const resolution = resolveAgentsViewSelectionState( this.rows, this.selectedIndex, diff --git a/packages/coding-agent/src/modes/agents-view/agents-view-state.ts b/packages/coding-agent/src/modes/agents-view/agents-view-state.ts index 85d9c93ff6..3db4ff780f 100644 --- a/packages/coding-agent/src/modes/agents-view/agents-view-state.ts +++ b/packages/coding-agent/src/modes/agents-view/agents-view-state.ts @@ -748,6 +748,9 @@ export function resolveAgentsViewSelectionIndex( ): number { const findSelectable = (predicate: (row: AgentsViewRow) => boolean): number => rows.findIndex((row) => row.selectable && predicate(row)); + const selectedSyntheticKind = identity?.startsWith("subagents:") ? "subagent-summary" : undefined; + const preservesSelectedKind = (row: AgentsViewRow): boolean => + selectedSyntheticKind === undefined || row.kind === selectedSyntheticKind; if (identity !== undefined) { const index = findSelectable((row) => row.identity === identity); @@ -759,7 +762,9 @@ export function resolveAgentsViewSelectionIndex( } if (key?.activeSessionId !== undefined) { const activeSessionId = key.activeSessionId; - const index = findSelectable((row) => (row.summary.activeSessionId ?? row.summary.id) === activeSessionId); + const index = findSelectable( + (row) => preservesSelectedKind(row) && (row.summary.activeSessionId ?? row.summary.id) === activeSessionId, + ); if (index >= 0) { return index; } @@ -772,7 +777,7 @@ export function resolveAgentsViewSelectionIndex( } if (key?.sessionId !== undefined) { const sessionId = key.sessionId; - return findSelectable((row) => row.summary.sessionId === sessionId); + return findSelectable((row) => preservesSelectedKind(row) && row.summary.sessionId === sessionId); } return -1; } diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index bb896088e0..25bd430004 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -730,7 +730,7 @@ describe("AgentsViewMode", () => { }); it("keeps a subagent summary selected across roster refreshes", () => { - const parent = summary({ sessionName: "parent" }); + const parent = summary({ sessionName: "parent", sessionFile: undefined }); const child = summary({ id: "child", activeSessionId: "child", @@ -749,11 +749,13 @@ describe("AgentsViewMode", () => { return rows[Reflect.get(view, "selectedIndex") as number]; }; expect(selectedRow()?.kind).toBe("subagent-summary"); + const provisionalIdentity = selectedRow()?.identity; - Reflect.set(view, "lastListedSummaries", [{ ...parent, summary: "Updated status" }, child]); + Reflect.set(view, "lastListedSummaries", [{ ...parent, sessionFile: "/tmp/parent.jsonl" }, child]); invoke("reconcileCatalogs", view); expect(selectedRow()?.kind).toBe("subagent-summary"); + expect(selectedRow()?.identity).not.toBe(provisionalIdentity); } finally { stopThemeWatcher(); }