Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
81 changes: 77 additions & 4 deletions packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,23 @@ export interface AgentsViewModeOptions {
verbose?: boolean;
}

type AgentsViewRunResult = { type: "exit" } | { type: "open"; summary: SessionSummary; subagent?: SessionSummary };
type AgentsViewRunResult =
| { type: "exit" }
| {
type: "open";
summary: SessionSummary;
subagent?: SessionSummary;
// Session ids of every ancestor that must be expanded to reveal the
// opened subagent, from the root agent down to its immediate parent.
subagentAncestorSessionIds?: string[];
};
type AgentsViewPersistentState = {
selectedRowIdentity?: string;
selectedSessionKey?: AgentsViewSelectionKey;
// Session ids of the ancestor chain to re-expand when returning to a subagent.
// Resolved to live row identities on first build, then cleared; kept by
// sessionId so it survives a parent's active→persisted identity flip.
pendingExpandedAncestorSessionIds?: string[];
statusMessage?: string;
initialPromptsSent?: boolean;
// Gathered once and reused across agents-view instances so the notices survive
Expand Down Expand Up @@ -232,8 +245,17 @@ export async function runAgentsViewMode(options: AgentsViewModeOptions): Promise
if (result.type === "exit") {
return;
}
persistentState.selectedRowIdentity = getSummaryIdentity(result.summary);
persistentState.selectedSessionKey = getAgentsViewSelectionKey(result.summary);
if (result.subagent) {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
// Returning from a subagent reopens the agents view with every ancestor
// list expanded and that subagent reselected.
persistentState.selectedRowIdentity = getSummaryIdentity(result.subagent);
persistentState.selectedSessionKey = getAgentsViewSelectionKey(result.subagent);
persistentState.pendingExpandedAncestorSessionIds = result.subagentAncestorSessionIds ?? [];
} else {
persistentState.selectedRowIdentity = getSummaryIdentity(result.summary);
persistentState.selectedSessionKey = getAgentsViewSelectionKey(result.summary);
persistentState.pendingExpandedAncestorSessionIds = undefined;
}

let opened: { connection: DaemonAgentConnection; summary: SessionSummary } | undefined;
try {
Expand Down Expand Up @@ -706,6 +728,32 @@ class AgentsViewMode implements Component, Focusable {
this.ui.requestRender();
}

/**
* Resolve the persisted ancestor breadcrumb (session ids) to live row
* identities and expand them, so returning to a subagent reopens its whole
* chain. Identity-keyed from here on, matching the rest of the expansion
* lifecycle; runs once then clears.
*/
private applyPendingAncestorExpansion(): void {
const sessionIds = this.persistentState.pendingExpandedAncestorSessionIds;
if (!sessionIds || sessionIds.length === 0) {
this.persistentState.pendingExpandedAncestorSessionIds = undefined;
return;
}
this.persistentState.pendingExpandedAncestorSessionIds = undefined;
const wanted = new Set(sessionIds);
let added = false;
for (const row of this.rows) {
if (wanted.has(row.summary.sessionId) && !this.expandedSubagentParents.has(row.identity)) {
this.expandedSubagentParents.add(row.identity);
added = true;
}
}
if (added) {
this.rebuildRows();
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

/** Expanded subagent lists collapse back to their summary row once selection leaves them. */
private collapseSubagentListsOutsideSelection(): void {
if (this.expandedSubagentParents.size === 0) {
Expand Down Expand Up @@ -1061,7 +1109,31 @@ class AgentsViewMode implements Component, Focusable {
this.setStatusMessage("Cannot open subagent without its parent agent");
return;
}
this.finish({ type: "open", summary: root.summary, subagent: row.summary });
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
this.finish({
type: "open",
summary: root.summary,
subagent: row.summary,
subagentAncestorSessionIds: this.collectSubagentAncestorSessionIds(row),
});
}

Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
/**
* Session ids of every ancestor of a subagent row, root-most first. Keyed by
* sessionId rather than row identity so the breadcrumb survives a parent's
* active→persisted identity flip while the subagent is open.
*/
private collectSubagentAncestorSessionIds(row: AgentsViewRow): string[] {
const ancestors: string[] = [];
let parentIdentity = row.parentIdentity;
while (parentIdentity !== undefined) {
const parent = this.rows.find((candidate) => candidate.identity === parentIdentity);
if (!parent) {
break;
}
ancestors.unshift(parent.summary.sessionId);
parentIdentity = parent.parentIdentity;
}
return ancestors;
}

/**
Expand Down Expand Up @@ -1485,6 +1557,7 @@ class AgentsViewMode implements Component, Focusable {
this.expandedSubagentParents,
this.programShownParents,
);
this.applyPendingAncestorExpansion();
this.restoreSelection();
this.ui.requestRender();
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ export class ChildAgentDetailComponent implements Component, Focusable {
private toolsExpanded = false;
private killConfirmExpiresAt = 0;
private killConfirmTimer: ReturnType<typeof setTimeout> | undefined;
private backHintLabel = "back to chat";

onCancel?: () => void;
onToggleToolsExpanded?: () => void;
Expand All @@ -613,6 +614,10 @@ export class ChildAgentDetailComponent implements Component, Focusable {
this.rebuildTranscriptComponents();
}

setBackHintLabel(label: string): void {
this.backHintLabel = label;
}

setToolsExpanded(expanded: boolean): void {
this.toolsExpanded = expanded;
for (const component of this.transcriptComponents) {
Expand Down Expand Up @@ -848,7 +853,7 @@ export class ChildAgentDetailComponent implements Component, Focusable {
? theme.fg("error", `${keyText("app.agents.delete", { primaryOnly: true }).trim()} again to stop`)
: keyAction("app.agents.delete", "stop", { primaryOnly: true });
return hintLine(
[keyAction("app.agents.back", "back to chat", { primaryOnly: true }), expandAction, stopAction],
[keyAction("app.agents.back", this.backHintLabel, { primaryOnly: true }), expandAction, stopAction],
width,
);
}
Expand Down
30 changes: 24 additions & 6 deletions packages/coding-agent/src/modes/interactive/interactive-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ export class InteractiveMode {
private childAgentNodes: ChildAgentInspectorNode[] = [];
private childAgentDetailNodeId: string | undefined;
private childAgentPanelMode: "detail" | undefined;
private enteredSessionViaSubagentDetail = false;

// Tool output expansion state
private toolOutputExpanded = false;
Expand Down Expand Up @@ -686,7 +687,13 @@ export class InteractiveMode {
getHideThinkingBlock: () => this.hideThinkingBlock,
getHiddenThinkingLabel: () => this.hiddenThinkingLabel,
});
this.childAgentDetail.onCancel = () => this.closeChildAgentPanel({ selectNodeId: this.childAgentDetailNodeId });
this.childAgentDetail.onCancel = () => {
if (this.options.returnToAgentsView && this.enteredSessionViaSubagentDetail) {
void this.returnToAgentsView();
return;
}
this.closeChildAgentPanel({ selectNodeId: this.childAgentDetailNodeId });
};
this.childAgentDetail.onToggleToolsExpanded = () => this.toggleToolOutputExpansion();
this.childAgentDetail.onKill = (nodeId) => void this.killChildAgent(nodeId);
this.widgetContainerAbove = new Container();
Expand Down Expand Up @@ -978,11 +985,18 @@ export class InteractiveMode {

// Jump straight into a subagent's read-only detail view when the agents
// view opened this session targeting one of its subagents.
if (this.options.initialSubagentNodeId && !this.openChildAgentDetail(this.options.initialSubagentNodeId)) {
// The subagent can finish and get released between the agents view
// listing it and this session attaching; say so instead of silently
// landing in the parent chat.
this.showStatus("Subagent already finished; showing its parent session");
if (this.options.initialSubagentNodeId) {
if (this.openChildAgentDetail(this.options.initialSubagentNodeId)) {
if (this.options.returnToAgentsView) {
this.enteredSessionViaSubagentDetail = true;
this.childAgentDetail.setBackHintLabel("back to agents");
}
} else {
// The subagent can finish and get released between the agents view
// listing it and this session attaching; say so instead of silently
// landing in the parent chat.
this.showStatus("Subagent already finished; showing its parent session");
}
}

// Set up theme file watcher
Expand Down Expand Up @@ -3105,6 +3119,8 @@ export class InteractiveMode {
// Re-render queued previews cleared on panel entry; the queue may still hold messages.
this.updatePendingMessagesDisplay();
this.childAgentDetailNodeId = undefined;
this.enteredSessionViaSubagentDetail = false;
this.childAgentDetail.setBackHintLabel("back to chat");
this.childAgentDetail.setNode(undefined);
}
this.childAgentPanelMode = undefined;
Expand Down Expand Up @@ -4652,6 +4668,8 @@ export class InteractiveMode {
private closeChildAgentPanel(options: { selectNodeId?: string } = {}): void {
this.childAgentPanelMode = undefined;
this.childAgentDetailNodeId = undefined;
this.enteredSessionViaSubagentDetail = false;
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
this.childAgentDetail.setBackHintLabel("back to chat");
this.childAgentDetail.setNode(undefined);
this.childAgentSummary.setHidden(false);
this.restoreMainAgentView();
Expand Down
14 changes: 12 additions & 2 deletions packages/coding-agent/test/daemon-list-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@ describe("formatSessionListTable", () => {
const table = stripAnsi(
formatSessionListTable(
[
makeSummary({ name: "sleep", id: "019e71ec-e08a-75a9-b573-fc10e9f8380f", lifecycle: "archived", activity: "idle" }),
makeSummary({
name: "sleep",
id: "019e71ec-e08a-75a9-b573-fc10e9f8380f",
lifecycle: "archived",
activity: "idle",
}),
makeSummary({ name: "tool", id: "ccccddddeeee", lifecycle: "live", activity: "working" }),
makeSummary({ name: "crash", id: "019e71ec-e08a-75a9-b573-abcdef123456", lifecycle: "archived", activity: "idle" }),
makeSummary({
name: "crash",
id: "019e71ec-e08a-75a9-b573-abcdef123456",
lifecycle: "archived",
activity: "idle",
}),
makeSummary({ name: "idle", id: "bbbbccccdddd", lifecycle: "live", activity: "idle" }),
makeSummary({ name: "model", id: "ddddeeeeffff", lifecycle: "live", activity: "working" }),
makeSummary({
Expand Down
Loading