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

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