fix: improve subagent UI display and interruption handling (#330)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-12-21 00:09:12 -08:00
committed by GitHub
parent 90d84482ef
commit 0852ce26fe
14 changed files with 161 additions and 29 deletions

View File

@@ -252,6 +252,45 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
);
}
// Task tool (subagent) - show nicely formatted preview
if (t === "task") {
const subagentType =
typeof parsedArgs?.subagent_type === "string"
? parsedArgs.subagent_type
: "unknown";
const description =
typeof parsedArgs?.description === "string"
? parsedArgs.description
: "(no description)";
const prompt =
typeof parsedArgs?.prompt === "string"
? parsedArgs.prompt
: "(no prompt)";
const model =
typeof parsedArgs?.model === "string" ? parsedArgs.model : undefined;
// Truncate long prompts for preview (show first ~200 chars)
const maxPromptLength = 200;
const promptPreview =
prompt.length > maxPromptLength
? `${prompt.slice(0, maxPromptLength)}...`
: prompt;
return (
<Box flexDirection="column" paddingLeft={2}>
<Box flexDirection="row">
<Text bold>{subagentType}</Text>
<Text dimColor> · </Text>
<Text>{description}</Text>
</Box>
{model && <Text dimColor>Model: {model}</Text>}
<Box marginTop={1}>
<Text dimColor>{promptPreview}</Text>
</Box>
</Box>
);
}
// File edit previews: write/edit/multi_edit/replace/write_file/write_file_gemini
if (
(t === "write" ||
@@ -714,5 +753,6 @@ function getHeaderLabel(toolName: string): string {
if (t === "write_file" || t === "writefile") return "Write File";
if (t === "killbash") return "Kill Shell";
if (t === "bashoutput") return "Shell Output";
if (t === "task") return "Task";
return toolName;
}

View File

@@ -131,7 +131,7 @@ const AgentRow = memo(({ agent, isLast, expanded }: AgentRowProps) => {
<Text dimColor>{" ⎿ Done"}</Text>
) : agent.status === "error" ? (
<Text color={colors.subagent.error}>
{" ⎿ Error: "}
{" ⎿ "}
{agent.error}
</Text>
) : lastTool ? (
@@ -151,21 +151,27 @@ AgentRow.displayName = "AgentRow";
interface GroupHeaderProps {
count: number;
allCompleted: boolean;
hasErrors: boolean;
expanded: boolean;
}
const GroupHeader = memo(
({ count, allCompleted, expanded }: GroupHeaderProps) => {
({ count, allCompleted, hasErrors, expanded }: GroupHeaderProps) => {
const statusText = allCompleted
? `Ran ${count} subagent${count !== 1 ? "s" : ""}`
: `Running ${count} subagent${count !== 1 ? "s" : ""}`;
const hint = expanded ? "(ctrl+o to collapse)" : "(ctrl+o to expand)";
// Use error color for dot if any subagent errored
const dotColor = hasErrors
? colors.subagent.error
: colors.subagent.completed;
return (
<Box flexDirection="row">
{allCompleted ? (
<Text color={colors.subagent.completed}></Text>
<Text color={dotColor}></Text>
) : (
<BlinkDot color={colors.subagent.header} />
)}
@@ -200,12 +206,14 @@ export const SubagentGroupDisplay = memo(() => {
const allCompleted = agents.every(
(a) => a.status === "completed" || a.status === "error",
);
const hasErrors = agents.some((a) => a.status === "error");
return (
<Box flexDirection="column" marginTop={1}>
<GroupHeader
count={agents.length}
allCompleted={allCompleted}
hasErrors={hasErrors}
expanded={expanded}
/>
{agents.map((agent, index) => (

View File

@@ -87,7 +87,7 @@ const AgentRow = memo(({ agent, isLast }: AgentRowProps) => {
<Text dimColor>{" ⎿ Done"}</Text>
) : (
<Text color={colors.subagent.error}>
{" ⎿ Error: "}
{" ⎿ "}
{agent.error}
</Text>
)}
@@ -109,12 +109,18 @@ export const SubagentGroupStatic = memo(
}
const statusText = `Ran ${agents.length} subagent${agents.length !== 1 ? "s" : ""}`;
const hasErrors = agents.some((a) => a.status === "error");
// Use error color for dot if any subagent errored
const dotColor = hasErrors
? colors.subagent.error
: colors.subagent.completed;
return (
<Box flexDirection="column">
{/* Header */}
<Box flexDirection="row">
<Text color={colors.subagent.completed}></Text>
<Text color={dotColor}></Text>
<Text color={colors.subagent.header}> {statusText}</Text>
</Box>

View File

@@ -1,5 +1,6 @@
import { Box, Text } from "ink";
import { memo } from "react";
import { INTERRUPTED_BY_USER } from "../../constants";
import { clipToolReturn } from "../../tools/manager.js";
import { formatArgsDisplay } from "../helpers/formatArgsDisplay.js";
import {
@@ -46,8 +47,14 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
const argsText = line.argsText ?? "...";
// Task tool - handled by SubagentGroupDisplay, don't render here
// Exception: Cancelled/rejected Task tools should be rendered inline
// since they won't appear in SubagentGroupDisplay
if (isTaskTool(rawName)) {
return null;
const isCancelledOrRejected =
line.phase === "finished" && line.resultOk === false;
if (!isCancelledOrRejected) {
return null;
}
}
// Apply tool name remapping
@@ -103,14 +110,14 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
);
}
if (line.resultText === "Interrupted by user") {
if (line.resultText === INTERRUPTED_BY_USER) {
return (
<Box flexDirection="row">
<Box width={prefixWidth} flexShrink={0}>
<Text>{prefix}</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<Text color={colors.status.interrupt}>Interrupted by user</Text>
<Text color={colors.status.interrupt}>{INTERRUPTED_BY_USER}</Text>
</Box>
</Box>
);