feat(tui): improve task tool display with subagent keybind hints and spinner animations (#15607)
This commit is contained in:
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user