summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2026-02-01 22:51:55 -0500
committerGitHub <[email protected]>2026-02-01 22:51:55 -0500
commit0dc80df6fd1f4c89cebf9ed20cd9d9291f996236 (patch)
treed0d2a3124595512f1eb82ae173da5a998fc8338f
parent8e985e0a75ca5f2cb859434fe82dee7ea81cb59f (diff)
downloadopencode-0dc80df6fd1f4c89cebf9ed20cd9d9291f996236.tar.gz
opencode-0dc80df6fd1f4c89cebf9ed20cd9d9291f996236.zip
Add spinner animation for Task tool (#11725)
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx10
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/spinner.tsx24
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx25
3 files changed, 47 insertions, 12 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
index 85c174c1d..775969bfc 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
@@ -10,7 +10,7 @@ import { useSDK } from "../context/sdk"
import { DialogSessionRename } from "./dialog-session-rename"
import { useKV } from "../context/kv"
import { createDebouncedSignal } from "../util/signal"
-import "opentui-spinner/solid"
+import { Spinner } from "./spinner"
export function DialogSessionList() {
const dialog = useDialog()
@@ -32,8 +32,6 @@ export function DialogSessionList() {
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
- const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
-
const sessions = createMemo(() => searchResults() ?? sync.data.session)
const options = createMemo(() => {
@@ -56,11 +54,7 @@ export function DialogSessionList() {
value: x.id,
category,
footer: Locale.time(x.time.updated),
- gutter: isWorking ? (
- <Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
- <spinner frames={spinnerFrames} interval={80} color={theme.primary} />
- </Show>
- ) : undefined,
+ gutter: isWorking ? <Spinner /> : undefined,
}
})
})
diff --git a/packages/opencode/src/cli/cmd/tui/component/spinner.tsx b/packages/opencode/src/cli/cmd/tui/component/spinner.tsx
new file mode 100644
index 000000000..8dc545550
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/spinner.tsx
@@ -0,0 +1,24 @@
+import { Show } from "solid-js"
+import { useTheme } from "../context/theme"
+import { useKV } from "../context/kv"
+import type { JSX } from "@opentui/solid"
+import type { RGBA } from "@opentui/core"
+import "opentui-spinner/solid"
+
+const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+
+export function Spinner(props: { children?: JSX.Element; color?: RGBA }) {
+ const { theme } = useTheme()
+ const kv = useKV()
+ const color = () => props.color ?? theme.textMuted
+ return (
+ <Show when={kv.get("animations_enabled", true)} fallback={<text fg={color()}>⋯ {props.children}</text>}>
+ <box flexDirection="row" gap={1}>
+ <spinner frames={frames} interval={80} color={color()} />
+ <Show when={props.children}>
+ <text fg={color()}>{props.children}</text>
+ </Show>
+ </box>
+ </Show>
+ )
+}
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 209469bad..8316d112c 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -16,6 +16,7 @@ import path from "path"
import { useRoute, useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border"
+import { Spinner } from "@tui/component/spinner"
import { useTheme } from "@tui/context/theme"
import {
BoxRenderable,
@@ -1559,7 +1560,13 @@ function InlineTool(props: {
)
}
-function BlockTool(props: { title: string; children: JSX.Element; onClick?: () => void; part?: ToolPart }) {
+function BlockTool(props: {
+ title: string
+ children: JSX.Element
+ onClick?: () => void
+ part?: ToolPart
+ spinner?: boolean
+}) {
const { theme } = useTheme()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
@@ -1582,9 +1589,16 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () =
props.onClick?.()
}}
>
- <text paddingLeft={3} fg={theme.textMuted}>
- {props.title}
- </text>
+ <Show
+ when={props.spinner}
+ fallback={
+ <text paddingLeft={3} fg={theme.textMuted}>
+ {props.title}
+ </text>
+ }
+ >
+ <Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
+ </Show>
{props.children}
<Show when={error()}>
<text fg={theme.error}>{error()}</text>
@@ -1813,6 +1827,8 @@ function Task(props: ToolProps<typeof TaskTool>) {
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
+ const isRunning = createMemo(() => props.part.state.status === "running")
+
return (
<Switch>
<Match when={props.input.description || props.input.subagent_type}>
@@ -1824,6 +1840,7 @@ function Task(props: ToolProps<typeof TaskTool>) {
: undefined
}
part={props.part}
+ spinner={isRunning()}
>
<box>
<text style={{ fg: theme.textMuted }}>