Skip to content

Commit 90270c6

Browse files
authored
feat(tui): improve task tool display with subagent keybind hints and spinner animations (#15607)
1 parent 6b7e6bd commit 90270c6

1 file changed

Lines changed: 75 additions & 51 deletions

File tree

  • packages/opencode/src/cli/cmd/tui/routes/session

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 75 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
For,
88
Match,
99
on,
10+
onMount,
1011
Show,
1112
Switch,
1213
useContext,
@@ -1323,6 +1324,8 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
13231324
return props.message.time.completed - user.time.created
13241325
})
13251326

1327+
const keybind = useKeybind()
1328+
13261329
return (
13271330
<>
13281331
<For each={props.parts}>
@@ -1340,6 +1343,14 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
13401343
)
13411344
}}
13421345
</For>
1346+
<Show when={props.parts.some((x) => x.type === "tool" && x.tool === "task")}>
1347+
<box paddingTop={1} paddingLeft={3}>
1348+
<text fg={theme.text}>
1349+
{keybind.print("session_child_first")}
1350+
<span style={{ fg: theme.textMuted }}> view subagents</span>
1351+
</text>
1352+
</box>
1353+
</Show>
13431354
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
13441355
<box
13451356
border={["left"]}
@@ -1609,6 +1620,7 @@ function InlineTool(props: {
16091620
iconColor?: RGBA
16101621
complete: any
16111622
pending: string
1623+
spinner?: boolean
16121624
children: JSX.Element
16131625
part: ToolPart
16141626
}) {
@@ -1665,11 +1677,18 @@ function InlineTool(props: {
16651677
}
16661678
}}
16671679
>
1668-
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
1669-
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
1670-
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
1671-
</Show>
1672-
</text>
1680+
<Switch>
1681+
<Match when={props.spinner}>
1682+
<Spinner color={fg()} children={props.children} />
1683+
</Match>
1684+
<Match when={true}>
1685+
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
1686+
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
1687+
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
1688+
</Show>
1689+
</text>
1690+
</Match>
1691+
</Switch>
16731692
<Show when={error() && !denied()}>
16741693
<text fg={theme.error}>{error()}</text>
16751694
</Show>
@@ -1836,6 +1855,7 @@ function Glob(props: ToolProps<typeof GlobTool>) {
18361855

18371856
function Read(props: ToolProps<typeof ReadTool>) {
18381857
const { theme } = useTheme()
1858+
const isRunning = createMemo(() => props.part.state.status === "running")
18391859
const loaded = createMemo(() => {
18401860
if (props.part.state.status !== "completed") return []
18411861
if (props.part.state.time.compacted) return []
@@ -1845,7 +1865,13 @@ function Read(props: ToolProps<typeof ReadTool>) {
18451865
})
18461866
return (
18471867
<>
1848-
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
1868+
<InlineTool
1869+
icon="→"
1870+
pending="Reading file..."
1871+
complete={props.input.filePath}
1872+
spinner={isRunning()}
1873+
part={props.part}
1874+
>
18491875
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
18501876
</InlineTool>
18511877
<For each={loaded()}>
@@ -1921,62 +1947,60 @@ function Task(props: ToolProps<typeof TaskTool>) {
19211947
const local = useLocal()
19221948
const sync = useSync()
19231949

1950+
onMount(() => {
1951+
if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length)
1952+
sync.session.sync(props.metadata.sessionId)
1953+
})
1954+
1955+
const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? [])
1956+
19241957
const tools = createMemo(() => {
1925-
const sessionID = props.metadata.sessionId
1926-
const msgs = sync.data.message[sessionID ?? ""] ?? []
1927-
return msgs.flatMap((msg) =>
1958+
return messages().flatMap((msg) =>
19281959
(sync.data.part[msg.id] ?? [])
19291960
.filter((part): part is ToolPart => part.type === "tool")
19301961
.map((part) => ({ tool: part.tool, state: part.state })),
19311962
)
19321963
})
19331964

1934-
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
1965+
const current = createMemo(() => tools().findLast((x) => (x.state as any).title))
19351966

19361967
const isRunning = createMemo(() => props.part.state.status === "running")
19371968

1969+
const duration = createMemo(() => {
1970+
const first = messages().find((x) => x.role === "user")?.time.created
1971+
const assistant = messages().findLast((x) => x.role === "assistant")?.time.completed
1972+
if (!first || !assistant) return 0
1973+
return assistant - first
1974+
})
1975+
19381976
return (
1939-
<Switch>
1940-
<Match when={props.input.description || props.input.subagent_type}>
1941-
<BlockTool
1942-
title={"# " + Locale.titlecase(props.input.subagent_type ?? "unknown") + " Task"}
1943-
onClick={
1944-
props.metadata.sessionId
1945-
? () => navigate({ type: "session", sessionID: props.metadata.sessionId! })
1946-
: undefined
1947-
}
1948-
part={props.part}
1949-
spinner={isRunning()}
1950-
>
1951-
<box>
1952-
<text style={{ fg: theme.textMuted }}>
1953-
{props.input.description} ({tools().length} toolcalls)
1954-
</text>
1955-
<Show when={current()}>
1956-
{(item) => {
1957-
const title = item().state.status === "completed" ? (item().state as any).title : ""
1958-
return (
1959-
<text style={{ fg: item().state.status === "error" ? theme.error : theme.textMuted }}>
1960-
{Locale.titlecase(item().tool)} {title}
1961-
</text>
1962-
)
1963-
}}
1964-
</Show>
1965-
</box>
1966-
<Show when={props.metadata.sessionId}>
1967-
<text fg={theme.text}>
1968-
{keybind.print("session_child_first")}
1969-
<span style={{ fg: theme.textMuted }}> view subagents</span>
1970-
</text>
1971-
</Show>
1972-
</BlockTool>
1973-
</Match>
1974-
<Match when={true}>
1975-
<InlineTool icon="#" pending="Delegating..." complete={props.input.subagent_type} part={props.part}>
1976-
{props.input.subagent_type} Task {props.input.description}
1977-
</InlineTool>
1978-
</Match>
1979-
</Switch>
1977+
<InlineTool
1978+
icon="≡"
1979+
spinner={isRunning()}
1980+
complete={props.input.description}
1981+
pending="Delegating..."
1982+
part={props.part}
1983+
>
1984+
{props.input.description}
1985+
<Show when={isRunning() && tools().length > 0}>
1986+
{" "}
1987+
· {tools().length} toolcalls
1988+
<Show fallback={"\n└ Running..."} when={current()}>
1989+
{(item) => {
1990+
const title = createMemo(() => (item().state as any).title)
1991+
return (
1992+
<>
1993+
{"\n"}{Locale.titlecase(item().tool)} {title()}
1994+
</>
1995+
)
1996+
}}
1997+
</Show>
1998+
</Show>
1999+
<Show when={duration() && props.part.state.status === "completed"}>
2000+
{"\n "}
2001+
{tools().length} toolcalls · {Locale.duration(duration())}
2002+
</Show>
2003+
</InlineTool>
19802004
)
19812005
}
19822006

0 commit comments

Comments
 (0)