TUI: render sending and waiting indicators immediately

This commit is contained in:
Vignesh Natarajan 2026-02-21 19:28:42 -08:00
parent 68b92e80f7
commit 68cb4fc8a1
3 changed files with 53 additions and 1 deletions

View File

@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Cron/Gateway: keep `cron.list` and `cron.status` responsive during startup catch-up by avoiding a long-held cron lock while missed jobs execute. (#23106) Thanks @jayleekr.
- Gateway/Config reload: compare array-valued config paths structurally during diffing so unchanged `memory.qmd.paths` and `memory.qmd.scope.rules` no longer trigger false restart-required reloads. (#23185) Thanks @rex05ai.
- TUI/Input: enable multiline-paste burst coalescing on macOS Terminal.app and iTerm so pasted blocks no longer submit line-by-line as separate messages. (#18809) Thanks @fwends.
- TUI/Status: request immediate renders after setting `sending`/`waiting` activity states so in-flight runs always show visible progress indicators instead of appearing idle until completion. (#21549) Thanks @13Guinness.
- Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane.
- Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize.
- Gateway/Pairing: treat operator.admin pairing tokens as satisfying operator.write requests so legacy devices stop looping through scope-upgrade prompts introduced in 2026.2.19. (#23125, #23006) Thanks @vignesh07.

View File

@ -2,6 +2,55 @@ import { describe, expect, it, vi } from "vitest";
import { createCommandHandlers } from "./tui-command-handlers.js";
describe("tui command handlers", () => {
it("renders the sending indicator before chat.send resolves", async () => {
let resolveSend: ((value: { runId: string }) => void) | null = null;
const sendChat = vi.fn(
() =>
new Promise<{ runId: string }>((resolve) => {
resolveSend = resolve;
}),
);
const addUser = vi.fn();
const requestRender = vi.fn();
const setActivityStatus = vi.fn();
const { handleCommand } = createCommandHandlers({
client: { sendChat } as never,
chatLog: { addUser, addSystem: vi.fn() } as never,
tui: { requestRender } as never,
opts: {},
state: {
currentSessionKey: "agent:main:main",
activeChatRunId: null,
sessionInfo: {},
} as never,
deliverDefault: false,
openOverlay: vi.fn(),
closeOverlay: vi.fn(),
refreshSessionInfo: vi.fn(),
loadHistory: vi.fn(),
setSession: vi.fn(),
refreshAgents: vi.fn(),
abortActive: vi.fn(),
setActivityStatus,
formatSessionKey: vi.fn(),
applySessionInfoFromPatch: vi.fn(),
noteLocalRunId: vi.fn(),
});
const pending = handleCommand("/context");
await Promise.resolve();
expect(setActivityStatus).toHaveBeenCalledWith("sending");
const sendingOrder = setActivityStatus.mock.invocationCallOrder[0] ?? 0;
const renderOrders = requestRender.mock.invocationCallOrder;
expect(renderOrders.some((order) => order > sendingOrder)).toBe(true);
resolveSend?.({ runId: "r1" });
await pending;
expect(setActivityStatus).toHaveBeenCalledWith("waiting");
});
it("forwards unknown slash commands to the gateway", async () => {
const sendChat = vi.fn().mockResolvedValue({ runId: "r1" });
const addUser = vi.fn();

View File

@ -470,6 +470,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
noteLocalRunId(runId);
state.activeChatRunId = runId;
setActivityStatus("sending");
tui.requestRender();
await client.sendChat({
sessionKey: state.currentSessionKey,
message: text,
@ -479,6 +480,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
runId,
});
setActivityStatus("waiting");
tui.requestRender();
} catch (err) {
if (state.activeChatRunId) {
forgetLocalRunId?.(state.activeChatRunId);
@ -486,8 +488,8 @@ export function createCommandHandlers(context: CommandHandlerContext) {
state.activeChatRunId = null;
chatLog.addSystem(`send failed: ${String(err)}`);
setActivityStatus("error");
tui.requestRender();
}
tui.requestRender();
};
return {