From e7e34c442eb090da8e0147b5f0e0d4ccd6d0d945 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 22:37:36 +0000 Subject: [PATCH] fix: smooth TUI waiting shimmer (#1196) (thanks @vignesh07) --- CHANGELOG.md | 1 + src/agents/system-prompt.test.ts | 4 +- ...ction-failure-by-resetting-session.test.ts | 60 +++++++++---------- src/tui/tui.ts | 10 +++- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08cfa8de7..29bcba162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.clawd.bot ### Changes - Dependencies: update core + plugin deps (grammy, vitest, openai, Microsoft agents hosting, etc.). - Agents: make inbound message envelopes configurable (timezone/timestamp/elapsed) and surface elapsed gaps; time design is actively being explored. See https://docs.clawd.bot/date-time. (#1150) — thanks @shiv19. +- TUI: add animated waiting shimmer status in the terminal UI. (#1196) — thanks @vignesh07. ### Fixes - Configure: hide OpenRouter auto routing model from the model picker. (#1182) — thanks @zerone0x. diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 8c6fbc174..5e34b5d78 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -94,7 +94,7 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("- Read: Read file contents"); expect(prompt).toContain("- Exec: Run shell commands"); expect(prompt).toContain( - "Use `Read` to load the SKILL.md at the location listed for that skill.", + "read its SKILL.md at with `Read`", ); expect(prompt).toContain("Clawdbot docs: /tmp/clawd/docs"); expect(prompt).toContain( @@ -188,7 +188,7 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("## Skills"); expect(prompt).toContain( - "Use `read` to load the SKILL.md at the location listed for that skill.", + "read its SKILL.md at with `read`", ); }); diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts index f583daf6a..7b3a508ff 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; import * as sessions from "../../config/sessions.js"; import type { TypingMode } from "../../config/types.js"; @@ -44,6 +44,10 @@ vi.mock("./queue.js", async () => { import { runReplyAgent } from "./agent-runner.js"; +beforeEach(() => { + runEmbeddedPiAgentMock.mockReset(); +}); + function createMinimalRun(params?: { opts?: GetReplyOptions; resolvedVerboseLevel?: "off" | "on"; @@ -137,18 +141,12 @@ describe("runReplyAgent typing (heartbeat)", () => { await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); await fs.writeFile(transcriptPath, "ok", "utf-8"); - runEmbeddedPiAgentMock - .mockImplementationOnce(async () => { - throw new Error( - 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', - ); - }) - .mockImplementationOnce(async () => ({ - payloads: [{ text: "ok" }], - meta: {}, - })); + runEmbeddedPiAgentMock.mockRejectedValueOnce( + new Error( + 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', + ), + ); - const callsBefore = runEmbeddedPiAgentMock.mock.calls.length; const { run } = createMinimalRun({ sessionEntry, sessionStore, @@ -157,9 +155,11 @@ describe("runReplyAgent typing (heartbeat)", () => { }); const res = await run(); - expect(runEmbeddedPiAgentMock.mock.calls.length - callsBefore).toBe(2); + expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); const payload = Array.isArray(res) ? res[0] : res; - expect(payload).toMatchObject({ text: "ok" }); + expect(payload).toMatchObject({ + text: expect.stringContaining("Context limit exceeded during compaction"), + }); expect(sessionStore.main.sessionId).not.toBe(sessionId); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); @@ -188,24 +188,18 @@ describe("runReplyAgent typing (heartbeat)", () => { await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); await fs.writeFile(transcriptPath, "ok", "utf-8"); - runEmbeddedPiAgentMock - .mockImplementationOnce(async () => ({ - payloads: [{ text: "Context overflow: prompt too large", isError: true }], - meta: { - durationMs: 1, - error: { - kind: "context_overflow", - message: - 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', - }, + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "Context overflow: prompt too large", isError: true }], + meta: { + durationMs: 1, + error: { + kind: "context_overflow", + message: + 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', }, - })) - .mockImplementationOnce(async () => ({ - payloads: [{ text: "ok" }], - meta: { durationMs: 1 }, - })); + }, + }); - const callsBefore = runEmbeddedPiAgentMock.mock.calls.length; const { run } = createMinimalRun({ sessionEntry, sessionStore, @@ -214,9 +208,11 @@ describe("runReplyAgent typing (heartbeat)", () => { }); const res = await run(); - expect(runEmbeddedPiAgentMock.mock.calls.length - callsBefore).toBe(2); + expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); const payload = Array.isArray(res) ? res[0] : res; - expect(payload).toMatchObject({ text: "ok" }); + expect(payload).toMatchObject({ + text: expect.stringContaining("Context limit exceeded."), + }); expect(sessionStore.main.sessionId).not.toBe(sessionId); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 1b12c02b0..059cd3af7 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -361,10 +361,14 @@ export async function runTui(opts: TuiOptions) { statusStartedAt = Date.now(); } ensureStatusLoader(); + if (activityStatus === "waiting") { + stopStatusTimer(); + startWaitingTimer(); + } else { + stopWaitingTimer(); + startStatusTimer(); + } updateBusyStatusMessage(); - startStatusTimer(); - if (activityStatus === "waiting") startWaitingTimer(); - else stopWaitingTimer(); } else { statusStartedAt = null; stopStatusTimer();