From fac66d4ddad5ba0ab5effdfc79cce7d2c825f0aa Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Sun, 18 Jan 2026 13:14:58 -0800 Subject: [PATCH] TUI: waiting shimmer helper + tests --- src/tui/tui-waiting.test.ts | 41 ++++++++++++++++++++++++++++++++ src/tui/tui-waiting.ts | 47 +++++++++++++++++++++++++++++++++++++ src/tui/tui.ts | 43 +++++++-------------------------- 3 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 src/tui/tui-waiting.test.ts create mode 100644 src/tui/tui-waiting.ts diff --git a/src/tui/tui-waiting.test.ts b/src/tui/tui-waiting.test.ts new file mode 100644 index 000000000..12a3bc6c9 --- /dev/null +++ b/src/tui/tui-waiting.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { buildWaitingStatusMessage, pickWaitingPhrase } from "./tui-waiting.js"; + +const theme = { + dim: (s: string) => `${s}`, + bold: (s: string) => `${s}`, + accentSoft: (s: string) => `${s}`, +} as any; + +describe("tui-waiting", () => { + it("pickWaitingPhrase rotates every 10 ticks", () => { + const phrases = ["a", "b", "c"]; + expect(pickWaitingPhrase(0, phrases)).toBe("a"); + expect(pickWaitingPhrase(9, phrases)).toBe("a"); + expect(pickWaitingPhrase(10, phrases)).toBe("b"); + expect(pickWaitingPhrase(20, phrases)).toBe("c"); + expect(pickWaitingPhrase(30, phrases)).toBe("a"); + }); + + it("buildWaitingStatusMessage includes shimmer markup and metadata", () => { + const msg = buildWaitingStatusMessage({ + theme, + tick: 1, + elapsed: "3s", + connectionStatus: "connected", + phrases: ["hello"], + }); + + expect(msg).toContain("connected"); + expect(msg).toContain("3s"); + // text is wrapped per-char; check it appears in order + expect(msg).toContain("h"); + expect(msg).toContain("e"); + expect(msg).toContain("l"); + expect(msg).toContain("o"); + // shimmer should contain both highlighted and dim parts + expect(msg).toContain(""); + expect(msg).toContain(""); + }); +}); diff --git a/src/tui/tui-waiting.ts b/src/tui/tui-waiting.ts new file mode 100644 index 000000000..25cfe8d60 --- /dev/null +++ b/src/tui/tui-waiting.ts @@ -0,0 +1,47 @@ +import type { ClawdbotTheme } from "./theme/theme.js"; + +export const defaultWaitingPhrases = [ + "flibbertigibbeting", + "kerfuffling", + "dillydallying", + "twiddling thumbs", + "noodling", + "bamboozling", + "moseying", + "hobnobbing", + "pondering", + "conjuring", +]; + +export function pickWaitingPhrase(tick: number, phrases = defaultWaitingPhrases) { + const idx = Math.floor(tick / 10) % phrases.length; + return phrases[idx] ?? phrases[0] ?? "waiting"; +} + +export function shimmerText(theme: ClawdbotTheme, text: string, tick: number) { + const width = 6; + const hi = (ch: string) => theme.bold(theme.accentSoft(ch)); + + const pos = tick % (text.length + width); + const start = Math.max(0, pos - width); + const end = Math.min(text.length - 1, pos); + + let out = ""; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + out += i >= start && i <= end ? hi(ch) : theme.dim(ch); + } + return out; +} + +export function buildWaitingStatusMessage(params: { + theme: ClawdbotTheme; + tick: number; + elapsed: string; + connectionStatus: string; + phrases?: string[]; +}) { + const phrase = pickWaitingPhrase(params.tick, params.phrases); + const cute = shimmerText(params.theme, `${phrase}…`, params.tick); + return `${cute} • ${params.elapsed} | ${params.connectionStatus}`; +} diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 2f81b9d5d..7bc41fe04 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -22,6 +22,7 @@ import { editorTheme, theme } from "./theme/theme.js"; import { createCommandHandlers } from "./tui-command-handlers.js"; import { createEventHandlers } from "./tui-event-handlers.js"; import { formatTokens } from "./tui-formatters.js"; +import { buildWaitingStatusMessage } from "./tui-waiting.js"; import { createOverlayHandlers } from "./tui-overlays.js"; import { createSessionActions } from "./tui-session-actions.js"; import type { @@ -286,49 +287,23 @@ export async function runTui(opts: TuiOptions) { statusContainer.addChild(statusLoader); }; - const waitingPhrases = [ - "flibbertigibbeting", - "kerfuffling", - "dillydallying", - "twiddling thumbs", - "noodling", - "bamboozling", - "moseying", - "hobnobbing", - "pondering", - "conjuring", - "vibing", - "clawding", - ]; - let waitingTick = 0; let waitingTimer: NodeJS.Timeout | null = null; - const shimmerWaitingText = (text: string, tick: number) => { - const width = 6; - const hi = (ch: string) => theme.bold(theme.accentSoft(ch)); - - const pos = tick % (text.length + width); - const start = Math.max(0, pos - width); - const end = Math.min(text.length - 1, pos); - - let out = ""; - for (let i = 0; i < text.length; i++) { - const ch = text[i]; - out += i >= start && i <= end ? hi(ch) : theme.dim(ch); - } - return out; - }; - const updateBusyStatusMessage = () => { if (!statusLoader || !statusStartedAt) return; const elapsed = formatElapsed(statusStartedAt); if (activityStatus === "waiting") { waitingTick++; - const phrase = waitingPhrases[Math.floor(waitingTick / 10) % waitingPhrases.length]; - const cute = shimmerWaitingText(`${phrase}…`, waitingTick); - statusLoader.setMessage(`${cute} • ${elapsed} | ${connectionStatus}`); + statusLoader.setMessage( + buildWaitingStatusMessage({ + theme, + tick: waitingTick, + elapsed, + connectionStatus, + }), + ); return; }