fix: subagent tree styling (#505)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 <BlinkDot color={colors.subagent.running} />;
|
||||
case "running":
|
||||
return <BlinkDot color={colors.subagent.running} />;
|
||||
case "completed":
|
||||
return <Text color={colors.subagent.completed}>●</Text>;
|
||||
case "error":
|
||||
return <Text color={colors.subagent.error}>●</Text>;
|
||||
default:
|
||||
return <Text>●</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
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) => {
|
||||
<Box flexDirection="column">
|
||||
{/* Main row: tree char + description + type + model + stats */}
|
||||
<Box flexDirection="row">
|
||||
<Text color={colors.subagent.treeChar}>{treeChar} </Text>
|
||||
{getDotElement()}
|
||||
<Text> {agent.description}</Text>
|
||||
<Text dimColor> · {agent.type.toLowerCase()}</Text>
|
||||
{agent.model && <Text dimColor> · {agent.model}</Text>}
|
||||
<Text dimColor> · {stats}</Text>
|
||||
<Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{treeChar}{" "}
|
||||
</Text>
|
||||
<Text bold>{agent.description}</Text>
|
||||
<Text dimColor>
|
||||
{" · "}
|
||||
{agent.type.toLowerCase()}
|
||||
{agent.model ? ` · ${agent.model}` : ""}
|
||||
{" · "}
|
||||
{stats}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Subagent URL */}
|
||||
{agent.agentURL && (
|
||||
<Box flexDirection="row">
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text dimColor>{" ⎿ Subagent: "}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar} ⎿{" "}
|
||||
</Text>
|
||||
<Text dimColor>{"Subagent: "}</Text>
|
||||
<Text dimColor>{agent.agentURL}</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -117,7 +113,10 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => {
|
||||
const formattedArgs = formatToolArgs(tc.args);
|
||||
return (
|
||||
<Box key={tc.id} flexDirection="row">
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar}
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{" "}
|
||||
{tc.name}({formattedArgs})
|
||||
@@ -130,15 +129,21 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => {
|
||||
<Box flexDirection="row">
|
||||
{agent.status === "completed" ? (
|
||||
<>
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text dimColor>{" ⎿ Done"}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar}
|
||||
</Text>
|
||||
<Text dimColor>{" Done"}</Text>
|
||||
</>
|
||||
) : agent.status === "error" ? (
|
||||
<>
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text dimColor>{" ⎿ "}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar}
|
||||
</Text>
|
||||
<Text dimColor>{" "}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
@@ -149,16 +154,22 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => {
|
||||
</>
|
||||
) : lastTool ? (
|
||||
<>
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar}
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
{" ⎿ "}
|
||||
{" "}
|
||||
{lastTool.name}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text dimColor>{" ⎿ Starting..."}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar}
|
||||
</Text>
|
||||
<Text dimColor>{" Starting..."}</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -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 (
|
||||
<Box flexDirection="column">
|
||||
{/* Main row: tree char + description + type + model + stats */}
|
||||
<Box flexDirection="row">
|
||||
<Text color={colors.subagent.treeChar}>{treeChar} </Text>
|
||||
<Text color={dotColor}>●</Text>
|
||||
<Text> {agent.description}</Text>
|
||||
<Text dimColor> · {agent.type.toLowerCase()}</Text>
|
||||
{agent.model && <Text dimColor> · {agent.model}</Text>}
|
||||
<Text dimColor> · {stats}</Text>
|
||||
<Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{treeChar}{" "}
|
||||
</Text>
|
||||
<Text bold>{agent.description}</Text>
|
||||
<Text dimColor>
|
||||
{" · "}
|
||||
{agent.type.toLowerCase()}
|
||||
{agent.model ? ` · ${agent.model}` : ""}
|
||||
{" · "}
|
||||
{stats}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Subagent URL */}
|
||||
{agent.agentURL && (
|
||||
<Box flexDirection="row">
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text dimColor>{" ⎿ Subagent: "}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar} ⎿{" "}
|
||||
</Text>
|
||||
<Text dimColor>{"Subagent: "}</Text>
|
||||
<Text dimColor>{agent.agentURL}</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -86,15 +92,21 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
|
||||
<Box flexDirection="row">
|
||||
{agent.status === "completed" ? (
|
||||
<>
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text dimColor>{" ⎿ Done"}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar}
|
||||
</Text>
|
||||
<Text dimColor>{" Done"}</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>
|
||||
<Text color={colors.subagent.treeChar}>{continueChar}</Text>
|
||||
<Text dimColor>{" ⎿ "}</Text>
|
||||
<Text color={colors.subagent.treeChar}>
|
||||
{" "}
|
||||
{continueChar}
|
||||
</Text>
|
||||
<Text dimColor>{" "}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
|
||||
@@ -43,6 +43,6 @@ export function getTreeChars(isLast: boolean): {
|
||||
} {
|
||||
return {
|
||||
treeChar: isLast ? "└─" : "├─",
|
||||
continueChar: isLast ? " " : "│ ",
|
||||
continueChar: isLast ? " " : "│ ",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user