From 54cbbe30a6272186cc25e316508763d13ca91c2d Mon Sep 17 00:00:00 2001 From: minhn4 Date: Wed, 29 Jul 2026 15:44:16 +0700 Subject: [PATCH 1/3] feat(tui): swap spinners to cli-spinners sand and aesthetic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared Spinner component (thinking/tool rows, startup loading, session-list dialogs) now animates cli-spinners' 35-frame "sand" braille set instead of "dots". The busy footer spinner replaces the Knight Rider block scanner with the "aesthetic" ▰▱ ramp at 80ms, keeping the agent accent color for filled blocks and a dimmed variant (deriveInactiveColor) for unfilled ones. ui/spinner.ts is untouched. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 7 ++++ DIVERGENCES.md | 2 ++ packages/tui/src/component/prompt/index.tsx | 34 +++++++++--------- packages/tui/src/component/spinner.tsx | 39 ++++++++++++++++++++- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f65e4b57..1b7f93b9af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ file is the human-readable summary of what changed and why. ## Unreleased +### TUI + +- **New spinners** — the in-progress tool/thinking spinner is now cli-spinners + "sand" (falling braille dots) and the busy footer spinner next to + `esc interrupt` is now cli-spinners "aesthetic" (a `▰▱` progress ramp), + still tinted with the active agent's accent color. + ### CodeGraph - **`codev codegraph enable` wires the CodeGraph MCP server** — for standalone diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 1b6d43516c..4bd3d4e125 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -120,6 +120,8 @@ from the squash message first, or use a plain merge. | `packages/tui/src/component/prompt/index.tsx` | The busy-state footer arm (`status().type !== "idle"`) also renders `props.hint` after the `esc interrupt` text, gated on `status().type !== "retry"` | The hidden-sidebar hint (see the `routes/session/index.tsx` hint row) only rendered in the footer `Switch`'s idle fallback arm, so it vanished for the whole duration of a running turn — exactly when todos are being created. Retry keeps its `space-between` error row uncluttered. | Keep the `{props.hint}` line in the busy `Match` arm (additive); take upstream's surrounding changes. | | `packages/tui/src/component/prompt/index.tsx` | Removed the editor open-file label from the footer's right-side group and lifted the state behind it out of `Prompt`: the local `dismissedEditorSelectionKey` signal and the `editorContext`/`editorPath`/`editorSelectionLabel`/`editorFileLabel`/`editorFileLabelDisplay`/`editorContextLabelState` memos are gone, replaced by `useEditorLabel()` (fork-only `src/context/editor-label.tsx`) — `selection()` feeds the `prompt.editor_context.clear` `enabled` predicate and the submit-time editor part, `state()` replaces `editor.labelState()` there, and `dismiss()` replaces the local `dismissEditorContext`. The pure helpers `hasEditorRangeSelection`/`getEditorRangeLabel` moved to that context module and are imported back for `formatEditorContext`. | The label now lives in the sidebar footer (see its row above). The dismissal state had to move with it: the sidebar must label the *effective* selection — the one this file actually attaches on submit — or it would advertise context the prompt has dismissed and won't send. | Keep the deletions, the `useEditorLabel` import + its three call sites, and the helper import from `../../context/editor-label`; take upstream's surrounding changes. If upstream reworks the editor-context label, apply the change to `editorContextFileLabel` in the fork-only sidebar module rather than restoring the footer render. | | `packages/tui/src/routes/session/index.tsx`, `packages/tui/src/component/prompt/index.tsx` | Left accent border removed entirely: the user-message block's `border={["left"]}`/`borderColor` (agent accent)/`customBorderChars` props and the prompt input box's two bordered boxes (main box with its `bottomLeft: "╹"` char, plus the height-1 `╹` terminator row) are replaced with `paddingLeft={1}`; the now-unused `borderHighlight` memo and the `tint`/`SplitBorder` imports are removed from the prompt (the `highlight`/`agentMetaAlpha` memos stay — they still color the agent name label); the fork's in-scrollbox banner box gains `paddingLeft={1}` so the banner/tips left edge lines up with the prompt/message blocks' background edge (the removed border glyph used to occupy that aligned column) | No vertical accent line next to sent messages or the prompt — removed rather than recolored to the background because background-colored glyphs render oddly on some terminals (e.g. PowerShell). `paddingLeft={1}` keeps the same 1-column offset, so text alignment with other transcript blocks is unchanged. | Keep our `paddingLeft={1}` boxes and the `borderHighlight`/`tint`/`SplitBorder` removals; take upstream's surrounding changes. If upstream adds new accent-colored left borders to these blocks, replace them with `paddingLeft={1}` the same way. | +| `packages/tui/src/component/spinner.tsx` | `SPINNER_FRAMES` swapped from cli-spinners "dots" (10 braille frames) to cli-spinners "sand" (35 braille frames, falling-sand animation); interval stays 80 ms | Fork visual identity for the shared `Spinner` component (in-progress tool/thinking rows, startup loading, session-list dialogs). | Keep our frames array; take upstream's surrounding changes. | +| `packages/tui/src/component/prompt/index.tsx` | Busy footer spinner is cli-spinners "aesthetic" instead of the Knight Rider block scanner: module-level `FOOTER_FRAMES` (`▰▱` ramp, 7 chars, 8 frames), `spinnerDef` keeps the agent-color resolution but returns those frames plus a per-char color fn (`▰` → agent color, `▱` → `deriveInactiveColor(color, 0.3)`) instead of `createFrames`/`createColors` from `ui/spinner`, and the `` interval is 80 ms (was 40). `ui/spinner.ts` itself is untouched; only its `deriveInactiveColor` import remains. | Fork visual identity for the busy indicator next to `esc interrupt`; the agent accent color still drives it. | Keep `FOOTER_FRAMES`, our `spinnerDef` body, the `deriveInactiveColor` import, and the 80 ms interval; take upstream's surrounding changes. If upstream reworks `spinnerDef`, re-apply the aesthetic frames + per-char color fn on top. | | `packages/tui/src/app.tsx` | Mounted `` (fork-only `./context/editor-label`) immediately inside `` | The effective-editor-selection state is shared by the prompt and the sidebar footer, so it has to be provided above both. | Keep the import and the wrapper; take upstream's surrounding provider changes. If upstream moves `EditorContextProvider`, keep ours directly inside it. | | `packages/app/index.html` | `` → "CoDev Code" | Web UI tab title. | Keep our title. | | `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/layout-new.tsx` | Dropped the `<TabsInfoPopup />` mount and its import from both layouts (added upstream in v1.17.18; since v1.18.3 upstream itself removed the sibling `HelpButton`, so nothing is imported from `help-button` anymore) | `TabsInfoPopup` is a dev-channel-only "Introducing Tabs" promo whose `muted autoplay loop` `<video>` mounts on every layout render. The fork's Windows e2e runs on GPU-less GitHub-hosted runners (software GPU — see the runner-swap row), where continuous video decode wedges the GPU command buffer (`kTransientFailure`), making the whole app-e2e suite flaky and ~3–5× slower. It never ships (release builds use a non-`dev` `CODEV_CHANNEL`), so unmounting it costs users nothing. | Keep ours: don't re-add the `<TabsInfoPopup />` mount or restore it to the import. | diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index ba0a3cfd38..bbfb5555af 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -42,7 +42,7 @@ import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v import { Locale } from "../../util/locale" import { errorMessage } from "../../util/error" import { formatDuration } from "../../util/format" -import { createColors, createFrames } from "../../ui/spinner" +import { deriveInactiveColor } from "../../ui/spinner" import { useDialog } from "../../ui/dialog" import { DialogProvider as DialogProviderConnect } from "../dialog-provider" import { useConnected } from "../use-connected" @@ -62,6 +62,18 @@ import { useLocation } from "../../context/location" registerOpencodeSpinner() +// "aesthetic" from sindresorhus/cli-spinners (MIT) +const FOOTER_FRAMES = [ + "▰▱▱▱▱▱▱", + "▰▰▱▱▱▱▱", + "▰▰▰▱▱▱▱", + "▰▰▰▰▱▱▱", + "▰▰▰▰▰▱▱", + "▰▰▰▰▰▰▱", + "▰▰▰▰▰▰▰", + "▰▱▱▱▱▱▱", +] + export type PromptProps = { sessionID?: string visible?: boolean @@ -1319,21 +1331,11 @@ export function Prompt(props: PromptProps) { ? (local.agent.list().find((a) => a.name === lastUserMessage()?.agent) ?? local.agent.current()) : local.agent.current() const color = agent ? local.agent.color(agent.name) : theme.border + const inactive = deriveInactiveColor(color, 0.3) return { - frames: createFrames({ - color, - style: "blocks", - inactiveFactor: 0.6, - // enableFading: false, - minAlpha: 0.3, - }), - color: createColors({ - color, - style: "blocks", - inactiveFactor: 0.6, - // enableFading: false, - minAlpha: 0.3, - }), + frames: FOOTER_FRAMES, + color: (frameIndex: number, charIndex: number) => + FOOTER_FRAMES[frameIndex][charIndex] === "▰" ? color : inactive, } }) const maxHeight = createMemo(() => tuiConfig.prompt?.max_height ?? Math.max(6, Math.floor(dimensions().height / 3))) @@ -1498,7 +1500,7 @@ export function Prompt(props: PromptProps) { <box flexShrink={0} flexDirection="row" gap={1}> <box marginLeft={1}> <Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}> - <spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} /> + <spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={80} /> </Show> </box> <box flexDirection="row" gap={1} flexShrink={0}> diff --git a/packages/tui/src/component/spinner.tsx b/packages/tui/src/component/spinner.tsx index f648a605a6..afd549af04 100644 --- a/packages/tui/src/component/spinner.tsx +++ b/packages/tui/src/component/spinner.tsx @@ -7,7 +7,44 @@ import { registerOpencodeSpinner } from "./register-spinner" registerOpencodeSpinner() -export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] +// "sand" from sindresorhus/cli-spinners (MIT) +export const SPINNER_FRAMES = [ + "⠁", + "⠂", + "⠄", + "⡀", + "⡈", + "⡐", + "⡠", + "⣀", + "⣁", + "⣂", + "⣄", + "⣌", + "⣔", + "⣤", + "⣥", + "⣦", + "⣮", + "⣶", + "⣷", + "⣿", + "⡿", + "⠿", + "⢟", + "⠟", + "⡛", + "⠛", + "⠫", + "⢋", + "⠋", + "⠍", + "⡉", + "⠉", + "⠑", + "⠡", + "⢁", +] export function Spinner(props: { children?: JSX.Element; color?: RGBA }) { const { theme } = useTheme() From 3ec6679950db07254020d95d65f55383b0478557 Mon Sep 17 00:00:00 2001 From: minhn4 <nguyenm.cuny@gmail.com> Date: Wed, 29 Jul 2026 15:44:56 +0700 Subject: [PATCH 2/3] docs: link spinner changelog entry to PR Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b7f93b9af..9b15e12882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ file is the human-readable summary of what changed and why. "sand" (falling braille dots) and the busy footer spinner next to `esc interrupt` is now cli-spinners "aesthetic" (a `▰▱` progress ramp), still tinted with the active agent's accent color. + ([#129](https://github.com/quickbeard/codev-code/pull/129)) ### CodeGraph From 9ade36fa8edf257a9a6bf54c99bb4ce094e3f150 Mon Sep 17 00:00:00 2001 From: minhn4 <nguyenm.cuny@gmail.com> Date: Wed, 29 Jul 2026 15:50:10 +0700 Subject: [PATCH 3/3] feat(tui): slow the footer spinner to 120ms Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- DIVERGENCES.md | 2 +- packages/tui/src/component/prompt/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 4bd3d4e125..bfb7bcaddf 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -121,7 +121,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/component/prompt/index.tsx` | Removed the editor open-file label from the footer's right-side group and lifted the state behind it out of `Prompt`: the local `dismissedEditorSelectionKey` signal and the `editorContext`/`editorPath`/`editorSelectionLabel`/`editorFileLabel`/`editorFileLabelDisplay`/`editorContextLabelState` memos are gone, replaced by `useEditorLabel()` (fork-only `src/context/editor-label.tsx`) — `selection()` feeds the `prompt.editor_context.clear` `enabled` predicate and the submit-time editor part, `state()` replaces `editor.labelState()` there, and `dismiss()` replaces the local `dismissEditorContext`. The pure helpers `hasEditorRangeSelection`/`getEditorRangeLabel` moved to that context module and are imported back for `formatEditorContext`. | The label now lives in the sidebar footer (see its row above). The dismissal state had to move with it: the sidebar must label the *effective* selection — the one this file actually attaches on submit — or it would advertise context the prompt has dismissed and won't send. | Keep the deletions, the `useEditorLabel` import + its three call sites, and the helper import from `../../context/editor-label`; take upstream's surrounding changes. If upstream reworks the editor-context label, apply the change to `editorContextFileLabel` in the fork-only sidebar module rather than restoring the footer render. | | `packages/tui/src/routes/session/index.tsx`, `packages/tui/src/component/prompt/index.tsx` | Left accent border removed entirely: the user-message block's `border={["left"]}`/`borderColor` (agent accent)/`customBorderChars` props and the prompt input box's two bordered boxes (main box with its `bottomLeft: "╹"` char, plus the height-1 `╹` terminator row) are replaced with `paddingLeft={1}`; the now-unused `borderHighlight` memo and the `tint`/`SplitBorder` imports are removed from the prompt (the `highlight`/`agentMetaAlpha` memos stay — they still color the agent name label); the fork's in-scrollbox banner box gains `paddingLeft={1}` so the banner/tips left edge lines up with the prompt/message blocks' background edge (the removed border glyph used to occupy that aligned column) | No vertical accent line next to sent messages or the prompt — removed rather than recolored to the background because background-colored glyphs render oddly on some terminals (e.g. PowerShell). `paddingLeft={1}` keeps the same 1-column offset, so text alignment with other transcript blocks is unchanged. | Keep our `paddingLeft={1}` boxes and the `borderHighlight`/`tint`/`SplitBorder` removals; take upstream's surrounding changes. If upstream adds new accent-colored left borders to these blocks, replace them with `paddingLeft={1}` the same way. | | `packages/tui/src/component/spinner.tsx` | `SPINNER_FRAMES` swapped from cli-spinners "dots" (10 braille frames) to cli-spinners "sand" (35 braille frames, falling-sand animation); interval stays 80 ms | Fork visual identity for the shared `Spinner` component (in-progress tool/thinking rows, startup loading, session-list dialogs). | Keep our frames array; take upstream's surrounding changes. | -| `packages/tui/src/component/prompt/index.tsx` | Busy footer spinner is cli-spinners "aesthetic" instead of the Knight Rider block scanner: module-level `FOOTER_FRAMES` (`▰▱` ramp, 7 chars, 8 frames), `spinnerDef` keeps the agent-color resolution but returns those frames plus a per-char color fn (`▰` → agent color, `▱` → `deriveInactiveColor(color, 0.3)`) instead of `createFrames`/`createColors` from `ui/spinner`, and the `<spinner>` interval is 80 ms (was 40). `ui/spinner.ts` itself is untouched; only its `deriveInactiveColor` import remains. | Fork visual identity for the busy indicator next to `esc interrupt`; the agent accent color still drives it. | Keep `FOOTER_FRAMES`, our `spinnerDef` body, the `deriveInactiveColor` import, and the 80 ms interval; take upstream's surrounding changes. If upstream reworks `spinnerDef`, re-apply the aesthetic frames + per-char color fn on top. | +| `packages/tui/src/component/prompt/index.tsx` | Busy footer spinner is cli-spinners "aesthetic" instead of the Knight Rider block scanner: module-level `FOOTER_FRAMES` (`▰▱` ramp, 7 chars, 8 frames), `spinnerDef` keeps the agent-color resolution but returns those frames plus a per-char color fn (`▰` → agent color, `▱` → `deriveInactiveColor(color, 0.3)`) instead of `createFrames`/`createColors` from `ui/spinner`, and the `<spinner>` interval is 120 ms (was 40; cli-spinners' stock 80 felt too fast). `ui/spinner.ts` itself is untouched; only its `deriveInactiveColor` import remains. | Fork visual identity for the busy indicator next to `esc interrupt`; the agent accent color still drives it. | Keep `FOOTER_FRAMES`, our `spinnerDef` body, the `deriveInactiveColor` import, and the 120 ms interval; take upstream's surrounding changes. If upstream reworks `spinnerDef`, re-apply the aesthetic frames + per-char color fn on top. | | `packages/tui/src/app.tsx` | Mounted `<EditorLabelProvider>` (fork-only `./context/editor-label`) immediately inside `<EditorContextProvider>` | The effective-editor-selection state is shared by the prompt and the sidebar footer, so it has to be provided above both. | Keep the import and the wrapper; take upstream's surrounding provider changes. If upstream moves `EditorContextProvider`, keep ours directly inside it. | | `packages/app/index.html` | `<title>` → "CoDev Code" | Web UI tab title. | Keep our title. | | `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/layout-new.tsx` | Dropped the `<TabsInfoPopup />` mount and its import from both layouts (added upstream in v1.17.18; since v1.18.3 upstream itself removed the sibling `HelpButton`, so nothing is imported from `help-button` anymore) | `TabsInfoPopup` is a dev-channel-only "Introducing Tabs" promo whose `muted autoplay loop` `<video>` mounts on every layout render. The fork's Windows e2e runs on GPU-less GitHub-hosted runners (software GPU — see the runner-swap row), where continuous video decode wedges the GPU command buffer (`kTransientFailure`), making the whole app-e2e suite flaky and ~3–5× slower. It never ships (release builds use a non-`dev` `CODEV_CHANNEL`), so unmounting it costs users nothing. | Keep ours: don't re-add the `<TabsInfoPopup />` mount or restore it to the import. | diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index bbfb5555af..c97538af97 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -1500,7 +1500,7 @@ export function Prompt(props: PromptProps) { <box flexShrink={0} flexDirection="row" gap={1}> <box marginLeft={1}> <Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}> - <spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={80} /> + <spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={120} /> </Show> </box> <box flexDirection="row" gap={1} flexShrink={0}>