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;
}