Add spinner animation for Task tool (#11725)

This commit is contained in:
Dax
2026-02-01 22:51:55 -05:00
committed by GitHub
parent 8e985e0a75
commit 0dc80df6fd
3 changed files with 47 additions and 12 deletions

View File

@@ -10,7 +10,7 @@ import { useSDK } from "../context/sdk"
import { DialogSessionRename } from "./dialog-session-rename" import { DialogSessionRename } from "./dialog-session-rename"
import { useKV } from "../context/kv" import { useKV } from "../context/kv"
import { createDebouncedSignal } from "../util/signal" import { createDebouncedSignal } from "../util/signal"
import "opentui-spinner/solid" import { Spinner } from "./spinner"
export function DialogSessionList() { export function DialogSessionList() {
const dialog = useDialog() const dialog = useDialog()
@@ -32,8 +32,6 @@ export function DialogSessionList() {
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
const sessions = createMemo(() => searchResults() ?? sync.data.session) const sessions = createMemo(() => searchResults() ?? sync.data.session)
const options = createMemo(() => { const options = createMemo(() => {
@@ -56,11 +54,7 @@ export function DialogSessionList() {
value: x.id, value: x.id,
category, category,
footer: Locale.time(x.time.updated), footer: Locale.time(x.time.updated),
gutter: isWorking ? ( gutter: isWorking ? <Spinner /> : undefined,
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[]</text>}>
<spinner frames={spinnerFrames} interval={80} color={theme.primary} />
</Show>
) : undefined,
} }
}) })
}) })

View File

@@ -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>
)
}

View File

@@ -16,6 +16,7 @@ import path from "path"
import { useRoute, useRouteData } from "@tui/context/route" import { useRoute, useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { Spinner } from "@tui/component/spinner"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
import { import {
BoxRenderable, 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 { theme } = useTheme()
const renderer = useRenderer() const renderer = useRenderer()
const [hover, setHover] = createSignal(false) const [hover, setHover] = createSignal(false)
@@ -1582,9 +1589,16 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () =
props.onClick?.() props.onClick?.()
}} }}
> >
<text paddingLeft={3} fg={theme.textMuted}> <Show
{props.title} when={props.spinner}
</text> fallback={
<text paddingLeft={3} fg={theme.textMuted}>
{props.title}
</text>
}
>
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
</Show>
{props.children} {props.children}
<Show when={error()}> <Show when={error()}>
<text fg={theme.error}>{error()}</text> <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 current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
const isRunning = createMemo(() => props.part.state.status === "running")
return ( return (
<Switch> <Switch>
<Match when={props.input.description || props.input.subagent_type}> <Match when={props.input.description || props.input.subagent_type}>
@@ -1824,6 +1840,7 @@ function Task(props: ToolProps<typeof TaskTool>) {
: undefined : undefined
} }
part={props.part} part={props.part}
spinner={isRunning()}
> >
<box> <box>
<text style={{ fg: theme.textMuted }}> <text style={{ fg: theme.textMuted }}>