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
Original file line number Diff line number Diff line change
@@ -0,0 +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.
65 changes: 31 additions & 34 deletions packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,6 @@ export class AgentsViewMode implements Component, Focusable {
private deleteConfirmTimer: ReturnType<typeof setTimeout> | undefined;
private workingIconFrame = 0;
private rows: AgentsViewRow[] = [];
private allRows: AgentsViewRow[] = [];
private lastListedSummaries: SessionSummary[] = [];
private lastVisibleSummaries: SessionSummary[] = [];
private savedSessions: AgentConnectionSavedSessionInfo[] = [];
Expand Down Expand Up @@ -960,7 +959,7 @@ export class AgentsViewMode implements Component, Focusable {
if (!this.replyTarget && this.editor.getText().length === 0) {
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;
}
}
Expand Down Expand Up @@ -1254,6 +1253,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;
Expand Down Expand Up @@ -1303,15 +1304,14 @@ 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,
this.scopeKey,
computeRecursiveRollups(this.unifiedRecords, this.unifiedIndex),
this.anchorSessionId,
);
this.rows = compactSessionRows(this.allRows);
const index =
selectedIdentity === undefined ? -1 : this.rows.findIndex((row) => row.identity === selectedIdentity);
if (index >= 0) {
Expand Down Expand Up @@ -1391,6 +1391,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;
Expand All @@ -1411,7 +1415,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);
Expand Down Expand Up @@ -1455,7 +1459,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;
}
Expand Down Expand Up @@ -2170,15 +2174,14 @@ 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,
this.scopeKey,
computeRecursiveRollups(this.unifiedRecords, this.unifiedIndex),
this.anchorSessionId,
);
this.rows = compactSessionRows(this.allRows);
this.applyPendingAncestorExpansion();
this.restoreSelection();
this.ui.requestRender();
Expand Down Expand Up @@ -2309,10 +2312,11 @@ export class AgentsViewMode implements Component, Focusable {
this.selectedActiveSessionId = undefined;
return;
}
const selectedIdentity = this.selectedRowIdentity ?? this.persistentState.selectedRowIdentity;
const resolution = resolveAgentsViewSelectionState(
this.rows,
this.selectedIndex,
this.selectedRowIdentity ?? this.persistentState.selectedRowIdentity,
selectedIdentity,
Comment thread
cursor[bot] marked this conversation as resolved.
this.selectedSessionKey ?? this.persistentState.selectedSessionKey,
);
this.selectedIndex = resolution.index;
Expand Down Expand Up @@ -2471,28 +2475,21 @@ 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`;
}

private renderSessionRows(width: number, maxRows: number): string[] {
if (maxRows <= 0) return [];
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" });
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) {
Expand All @@ -2517,14 +2514,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.fg("muted", truncateToWidth(`${sectionTitle(item.section)} (${counts[item.section]})`, width));
}
Expand All @@ -2545,6 +2534,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) ?? "";
Expand Down Expand Up @@ -2574,7 +2567,6 @@ export class AgentsViewMode implements Component, Focusable {
cells.push(theme.fg("dim", details));
return markRow(formatTableCell(cells.join(" "), width));
}

// Spawn-code rows are read-only context. They render deemphasized — muted
// text on a panel background (applied in finalizeRenderedLine) so the program
// reads as one quiet segmented block rather than competing with agent rows.
Expand Down Expand Up @@ -2661,7 +2653,18 @@ 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`;
const selected = this.rows[this.selectedIndex];
// 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")}/${keyText("app.agents.open")} ${rightAction}`,
this.scopeRootSummary ? `${keyText("app.agents.back")} parent` : undefined,
`${keyText("app.agents.new")} new`,
]
.filter((hint): hint is string => hint !== undefined)
.join(" ");
return truncateToWidth(theme.fg("muted", hints), width);
}

Expand Down Expand Up @@ -2729,14 +2732,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[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/coding-agent/test/agent-traces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function writeLedgerOutboxEntry(agentDir: string, ledgerFile: string, uploadedBy
}

async function advanceTimersUntil(condition: () => boolean): Promise<void> {
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();
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading
Loading