diff options
| author | Dax <[email protected]> | 2026-03-01 12:46:10 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-01 17:46:10 +0000 |
| commit | 90270c615d47c3c6b47c3ef0f551e9e57664f2c4 (patch) | |
| tree | d46f5edb4fa290db8f42116343b826319ca10c03 | |
| parent | 6b7e6bde4d88b97ca99525f1bc40c33e48ba97b4 (diff) | |
| download | opencode-90270c615d47c3c6b47c3ef0f551e9e57664f2c4.tar.gz opencode-90270c615d47c3c6b47c3ef0f551e9e57664f2c4.zip | |
feat(tui): improve task tool display with subagent keybind hints and spinner animations (#15607)
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 126 |
1 files changed, 75 insertions, 51 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 314018367..68f6796cd 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -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> ) } |
