feat(tui): improve task tool display with subagent keybind hints and spinner animations (#15607)

This commit is contained in:
Dax 2026-03-01 12:46:10 -05:00 committed by GitHub
parent 6b7e6bde4d
commit 90270c615d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -7,6 +7,7 @@ import {
For,
Match,
on,
onMount,
Show,
Switch,
useContext,
@ -1323,6 +1324,8 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
return props.message.time.completed - user.time.created
})
const keybind = useKeybind()
return (
<>
<For each={props.parts}>
@ -1340,6 +1343,14 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
)
}}
</For>
<Show when={props.parts.some((x) => x.type === "tool" && x.tool === "task")}>
<box paddingTop={1} paddingLeft={3}>
<text fg={theme.text}>
{keybind.print("session_child_first")}
<span style={{ fg: theme.textMuted }}> view subagents</span>
</text>
</box>
</Show>
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
<box
border={["left"]}
@ -1609,6 +1620,7 @@ function InlineTool(props: {
iconColor?: RGBA
complete: any
pending: string
spinner?: boolean
children: JSX.Element
part: ToolPart
}) {
@ -1665,11 +1677,18 @@ function InlineTool(props: {
}
}}
>
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
</Show>
</text>
<Switch>
<Match when={props.spinner}>
<Spinner color={fg()} children={props.children} />
</Match>
<Match when={true}>
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
</Show>
</text>
</Match>
</Switch>
<Show when={error() && !denied()}>
<text fg={theme.error}>{error()}</text>
</Show>
@ -1836,6 +1855,7 @@ function Glob(props: ToolProps<typeof GlobTool>) {
function Read(props: ToolProps<typeof ReadTool>) {
const { theme } = useTheme()
const isRunning = createMemo(() => props.part.state.status === "running")
const loaded = createMemo(() => {
if (props.part.state.status !== "completed") return []
if (props.part.state.time.compacted) return []
@ -1845,7 +1865,13 @@ function Read(props: ToolProps<typeof ReadTool>) {
})
return (
<>
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
<InlineTool
icon="→"
pending="Reading file..."
complete={props.input.filePath}
spinner={isRunning()}
part={props.part}
>
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
</InlineTool>
<For each={loaded()}>
@ -1921,62 +1947,60 @@ function Task(props: ToolProps<typeof TaskTool>) {
const local = useLocal()
const sync = useSync()
onMount(() => {
if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length)
sync.session.sync(props.metadata.sessionId)
})
const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? [])
const tools = createMemo(() => {
const sessionID = props.metadata.sessionId
const msgs = sync.data.message[sessionID ?? ""] ?? []
return msgs.flatMap((msg) =>
return messages().flatMap((msg) =>
(sync.data.part[msg.id] ?? [])
.filter((part): part is ToolPart => part.type === "tool")
.map((part) => ({ tool: part.tool, state: part.state })),
)
})
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
const current = createMemo(() => tools().findLast((x) => (x.state as any).title))
const isRunning = createMemo(() => props.part.state.status === "running")
const duration = createMemo(() => {
const first = messages().find((x) => x.role === "user")?.time.created
const assistant = messages().findLast((x) => x.role === "assistant")?.time.completed
if (!first || !assistant) return 0
return assistant - first
})
return (
<Switch>
<Match when={props.input.description || props.input.subagent_type}>
<BlockTool
title={"# " + Locale.titlecase(props.input.subagent_type ?? "unknown") + " Task"}
onClick={
props.metadata.sessionId
? () => navigate({ type: "session", sessionID: props.metadata.sessionId! })
: undefined
}
part={props.part}
spinner={isRunning()}
>
<box>
<text style={{ fg: theme.textMuted }}>
{props.input.description} ({tools().length} toolcalls)
</text>
<Show when={current()}>
{(item) => {
const title = item().state.status === "completed" ? (item().state as any).title : ""
return (
<text style={{ fg: item().state.status === "error" ? theme.error : theme.textMuted }}>
{Locale.titlecase(item().tool)} {title}
</text>
)
}}
</Show>
</box>
<Show when={props.metadata.sessionId}>
<text fg={theme.text}>
{keybind.print("session_child_first")}
<span style={{ fg: theme.textMuted }}> view subagents</span>
</text>
</Show>
</BlockTool>
</Match>
<Match when={true}>
<InlineTool icon="#" pending="Delegating..." complete={props.input.subagent_type} part={props.part}>
{props.input.subagent_type} Task {props.input.description}
</InlineTool>
</Match>
</Switch>
<InlineTool
icon="≡"
spinner={isRunning()}
complete={props.input.description}
pending="Delegating..."
part={props.part}
>
{props.input.description}
<Show when={isRunning() && tools().length > 0}>
{" "}
· {tools().length} toolcalls
<Show fallback={"\n└ Running..."} when={current()}>
{(item) => {
const title = createMemo(() => (item().state as any).title)
return (
<>
{"\n"} {Locale.titlecase(item().tool)} {title()}
</>
)
}}
</Show>
</Show>
<Show when={duration() && props.part.state.status === "completed"}>
{"\n "}
{tools().length} toolcalls · {Locale.duration(duration())}
</Show>
</InlineTool>
)
}