fix: smooth TUI waiting shimmer (#1196) (thanks @vignesh07)

This commit is contained in:
Peter Steinberger
2026-01-18 22:37:36 +00:00
parent e85d2dff97
commit e7e34c442e
4 changed files with 38 additions and 37 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.clawd.bot
### Changes ### Changes
- Dependencies: update core + plugin deps (grammy, vitest, openai, Microsoft agents hosting, etc.). - 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. - 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 ### Fixes
- Configure: hide OpenRouter auto routing model from the model picker. (#1182) — thanks @zerone0x. - Configure: hide OpenRouter auto routing model from the model picker. (#1182) — thanks @zerone0x.

View File

@@ -94,7 +94,7 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("- Read: Read file contents"); expect(prompt).toContain("- Read: Read file contents");
expect(prompt).toContain("- Exec: Run shell commands"); expect(prompt).toContain("- Exec: Run shell commands");
expect(prompt).toContain( expect(prompt).toContain(
"Use `Read` to load the SKILL.md at the location listed for that skill.", "read its SKILL.md at <location> with `Read`",
); );
expect(prompt).toContain("Clawdbot docs: /tmp/clawd/docs"); expect(prompt).toContain("Clawdbot docs: /tmp/clawd/docs");
expect(prompt).toContain( expect(prompt).toContain(
@@ -188,7 +188,7 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("## Skills"); expect(prompt).toContain("## Skills");
expect(prompt).toContain( expect(prompt).toContain(
"Use `read` to load the SKILL.md at the location listed for that skill.", "read its SKILL.md at <location> with `read`",
); );
}); });

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import path from "node:path"; 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 type { SessionEntry } from "../../config/sessions.js";
import * as sessions from "../../config/sessions.js"; import * as sessions from "../../config/sessions.js";
import type { TypingMode } from "../../config/types.js"; import type { TypingMode } from "../../config/types.js";
@@ -44,6 +44,10 @@ vi.mock("./queue.js", async () => {
import { runReplyAgent } from "./agent-runner.js"; import { runReplyAgent } from "./agent-runner.js";
beforeEach(() => {
runEmbeddedPiAgentMock.mockReset();
});
function createMinimalRun(params?: { function createMinimalRun(params?: {
opts?: GetReplyOptions; opts?: GetReplyOptions;
resolvedVerboseLevel?: "off" | "on"; resolvedVerboseLevel?: "off" | "on";
@@ -137,18 +141,12 @@ describe("runReplyAgent typing (heartbeat)", () => {
await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
await fs.writeFile(transcriptPath, "ok", "utf-8"); await fs.writeFile(transcriptPath, "ok", "utf-8");
runEmbeddedPiAgentMock runEmbeddedPiAgentMock.mockRejectedValueOnce(
.mockImplementationOnce(async () => { new Error(
throw new Error(
'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}',
),
); );
})
.mockImplementationOnce(async () => ({
payloads: [{ text: "ok" }],
meta: {},
}));
const callsBefore = runEmbeddedPiAgentMock.mock.calls.length;
const { run } = createMinimalRun({ const { run } = createMinimalRun({
sessionEntry, sessionEntry,
sessionStore, sessionStore,
@@ -157,9 +155,11 @@ describe("runReplyAgent typing (heartbeat)", () => {
}); });
const res = await run(); const res = await run();
expect(runEmbeddedPiAgentMock.mock.calls.length - callsBefore).toBe(2); expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1);
const payload = Array.isArray(res) ? res[0] : res; 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); expect(sessionStore.main.sessionId).not.toBe(sessionId);
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));
@@ -188,8 +188,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); await fs.mkdir(path.dirname(transcriptPath), { recursive: true });
await fs.writeFile(transcriptPath, "ok", "utf-8"); await fs.writeFile(transcriptPath, "ok", "utf-8");
runEmbeddedPiAgentMock runEmbeddedPiAgentMock.mockResolvedValueOnce({
.mockImplementationOnce(async () => ({
payloads: [{ text: "Context overflow: prompt too large", isError: true }], payloads: [{ text: "Context overflow: prompt too large", isError: true }],
meta: { meta: {
durationMs: 1, durationMs: 1,
@@ -199,13 +198,8 @@ describe("runReplyAgent typing (heartbeat)", () => {
'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', '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({ const { run } = createMinimalRun({
sessionEntry, sessionEntry,
sessionStore, sessionStore,
@@ -214,9 +208,11 @@ describe("runReplyAgent typing (heartbeat)", () => {
}); });
const res = await run(); const res = await run();
expect(runEmbeddedPiAgentMock.mock.calls.length - callsBefore).toBe(2); expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1);
const payload = Array.isArray(res) ? res[0] : res; 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); expect(sessionStore.main.sessionId).not.toBe(sessionId);
const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8"));

View File

@@ -361,10 +361,14 @@ export async function runTui(opts: TuiOptions) {
statusStartedAt = Date.now(); statusStartedAt = Date.now();
} }
ensureStatusLoader(); ensureStatusLoader();
updateBusyStatusMessage(); if (activityStatus === "waiting") {
stopStatusTimer();
startWaitingTimer();
} else {
stopWaitingTimer();
startStatusTimer(); startStatusTimer();
if (activityStatus === "waiting") startWaitingTimer(); }
else stopWaitingTimer(); updateBusyStatusMessage();
} else { } else {
statusStartedAt = null; statusStartedAt = null;
stopStatusTimer(); stopStatusTimer();