diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 2134a0d..c2f069e 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -1,7 +1,6 @@ // src/cli/App.tsx import { existsSync, readFileSync, writeFileSync } from "node:fs"; - import { APIError, APIUserAbortError } from "@letta-ai/letta-client/core/error"; import type { AgentState, @@ -32,6 +31,7 @@ import { type AgentProvenance, createAgent } from "../agent/create"; import { sendMessageStream } from "../agent/message"; import { getModelDisplayName, getModelInfo } from "../agent/model"; import { SessionStats } from "../agent/stats"; +import { INTERRUPTED_BY_USER } from "../constants"; import type { ApprovalContext } from "../permissions/analyzer"; import { type PermissionMode, permissionMode } from "../permissions/mode"; import { @@ -144,6 +144,7 @@ import { import { clearCompletedSubagents, clearSubagentsByIds, + interruptActiveSubagents, } from "./helpers/subagentState"; import { getRandomThinkingVerb } from "./helpers/thinkingMessages"; import { @@ -2559,6 +2560,9 @@ export default function App({ (buffersRef.current.abortGeneration || 0) + 1; const toolsCancelled = markIncompleteToolsAsCancelled(buffersRef.current); + // Mark any running subagents as interrupted + interruptActiveSubagents(INTERRUPTED_BY_USER); + // Show interrupt feedback (yellow message if no tools were cancelled) if (!toolsCancelled) { appendError(INTERRUPT_MESSAGE, true); @@ -2605,6 +2609,9 @@ export default function App({ (buffersRef.current.abortGeneration || 0) + 1; const toolsCancelled = markIncompleteToolsAsCancelled(buffersRef.current); + // Mark any running subagents as interrupted + interruptActiveSubagents(INTERRUPTED_BY_USER); + // NOW abort the stream - interrupted flag is already set if (abortControllerRef.current) { abortControllerRef.current.abort(); diff --git a/src/cli/components/SubagentGroupDisplay.tsx b/src/cli/components/SubagentGroupDisplay.tsx index c7acd9a..605d2b1 100644 --- a/src/cli/components/SubagentGroupDisplay.tsx +++ b/src/cli/components/SubagentGroupDisplay.tsx @@ -7,7 +7,7 @@ * * Features: * - Real-time updates via useSyncExternalStore - * - Blinking dots for running agents + * - Single blinking dot in header while running * - Expand/collapse tool calls (ctrl+o) * - Shows "Running N subagents..." while active * @@ -64,24 +64,9 @@ interface AgentRowProps { const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => { const { treeChar, continueChar } = getTreeChars(isLast); const columns = useTerminalWidth(); - const gutterWidth = 7; // continueChar (3) + " ⎿ " (4) + const gutterWidth = 8; // indent (3) + continueChar (2) + status indent (3) const contentWidth = Math.max(0, columns - gutterWidth); - const getDotElement = () => { - switch (agent.status) { - case "pending": - return ; - case "running": - return ; - case "completed": - return ; - case "error": - return ; - default: - return ; - } - }; - const isRunning = agent.status === "pending" || agent.status === "running"; const stats = formatStats( agent.toolCalls.length, @@ -94,19 +79,30 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => { {/* Main row: tree char + description + type + model + stats */} - {treeChar} - {getDotElement()} - {agent.description} - · {agent.type.toLowerCase()} - {agent.model && · {agent.model}} - · {stats} + + + {" "} + {treeChar}{" "} + + {agent.description} + + {" · "} + {agent.type.toLowerCase()} + {agent.model ? ` · ${agent.model}` : ""} + {" · "} + {stats} + + {/* Subagent URL */} {agent.agentURL && ( - {continueChar} - {" ⎿ Subagent: "} + + {" "} + {continueChar} ⎿{" "} + + {"Subagent: "} {agent.agentURL} )} @@ -117,7 +113,10 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => { const formattedArgs = formatToolArgs(tc.args); return ( - {continueChar} + + {" "} + {continueChar} + {" "} {tc.name}({formattedArgs}) @@ -130,15 +129,21 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => { {agent.status === "completed" ? ( <> - {continueChar} - {" ⎿ Done"} + + {" "} + {continueChar} + + {" Done"} ) : agent.status === "error" ? ( <> - {continueChar} - {" ⎿ "} + + {" "} + {continueChar} + + {" "} @@ -149,16 +154,22 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => { ) : lastTool ? ( <> - {continueChar} + + {" "} + {continueChar} + - {" ⎿ "} + {" "} {lastTool.name} ) : ( <> - {continueChar} - {" ⎿ Starting..."} + + {" "} + {continueChar} + + {" Starting..."} )} diff --git a/src/cli/components/SubagentGroupStatic.tsx b/src/cli/components/SubagentGroupStatic.tsx index d49e6a8..a074b31 100644 --- a/src/cli/components/SubagentGroupStatic.tsx +++ b/src/cli/components/SubagentGroupStatic.tsx @@ -51,33 +51,39 @@ interface AgentRowProps { const AgentRow = memo(({ agent, isLast }: AgentRowProps) => { const { treeChar, continueChar } = getTreeChars(isLast); const columns = useTerminalWidth(); - const gutterWidth = 7; // continueChar (3) + " ⎿ " (4) + const gutterWidth = 8; // indent (3) + continueChar (2) + status indent (3) const contentWidth = Math.max(0, columns - gutterWidth); - const dotColor = - agent.status === "completed" - ? colors.subagent.completed - : colors.subagent.error; - const stats = formatStats(agent.toolCount, agent.totalTokens); return ( {/* Main row: tree char + description + type + model + stats */} - {treeChar} - - {agent.description} - · {agent.type.toLowerCase()} - {agent.model && · {agent.model}} - · {stats} + + + {" "} + {treeChar}{" "} + + {agent.description} + + {" · "} + {agent.type.toLowerCase()} + {agent.model ? ` · ${agent.model}` : ""} + {" · "} + {stats} + + {/* Subagent URL */} {agent.agentURL && ( - {continueChar} - {" ⎿ Subagent: "} + + {" "} + {continueChar} ⎿{" "} + + {"Subagent: "} {agent.agentURL} )} @@ -86,15 +92,21 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => { {agent.status === "completed" ? ( <> - {continueChar} - {" ⎿ Done"} + + {" "} + {continueChar} + + {" Done"} ) : ( <> - {continueChar} - {" ⎿ "} + + {" "} + {continueChar} + + {" "} diff --git a/src/cli/components/colors.ts b/src/cli/components/colors.ts index 76ddb5f..57daeb9 100644 --- a/src/cli/components/colors.ts +++ b/src/cli/components/colors.ts @@ -127,7 +127,7 @@ export const colors = { running: brandColors.statusWarning, completed: brandColors.statusSuccess, error: brandColors.statusError, - treeChar: brandColors.textDisabled, + treeChar: brandColors.textSecondary, hint: brandColors.textDisabled, }, diff --git a/src/cli/helpers/subagentDisplay.ts b/src/cli/helpers/subagentDisplay.ts index 66fe22e..72db8f0 100644 --- a/src/cli/helpers/subagentDisplay.ts +++ b/src/cli/helpers/subagentDisplay.ts @@ -43,6 +43,6 @@ export function getTreeChars(isLast: boolean): { } { return { treeChar: isLast ? "└─" : "├─", - continueChar: isLast ? " " : "│ ", + continueChar: isLast ? " " : "│ ", }; } diff --git a/src/cli/helpers/subagentState.ts b/src/cli/helpers/subagentState.ts index 51be0da..790d22a 100644 --- a/src/cli/helpers/subagentState.ts +++ b/src/cli/helpers/subagentState.ts @@ -274,6 +274,29 @@ export function hasActiveSubagents(): boolean { return false; } +/** + * Mark all running/pending subagents as interrupted + * Called when user presses ESC to interrupt execution + */ +export function interruptActiveSubagents(errorMessage: string): void { + let anyInterrupted = false; + for (const [id, agent] of store.agents.entries()) { + if (agent.status === "pending" || agent.status === "running") { + const updatedAgent: SubagentState = { + ...agent, + status: "error", + error: errorMessage, + durationMs: Date.now() - agent.startTime, + }; + store.agents.set(id, updatedAgent); + anyInterrupted = true; + } + } + if (anyInterrupted) { + notifyListeners(); + } +} + // ============================================================================ // React Integration (useSyncExternalStore compatible) // ============================================================================