fix: add TUI status spinner

This commit is contained in:
Peter Steinberger
2026-01-15 02:11:12 +00:00
parent 7904a14af1
commit 6ffd7111a6
2 changed files with 84 additions and 7 deletions

View File

@@ -23,6 +23,7 @@
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.

View File

@@ -1,4 +1,11 @@
import { CombinedAutocompleteProvider, Container, ProcessTerminal, Text, TUI } from "@mariozechner/pi-tui";
import {
CombinedAutocompleteProvider,
Container,
Loader,
ProcessTerminal,
Text,
TUI,
} from "@mariozechner/pi-tui";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { loadConfig } from "../config/config.js";
import {
@@ -53,6 +60,9 @@ export async function runTui(opts: TuiOptions) {
let activityStatus = "idle";
let connectionStatus = "connecting";
let statusTimeout: NodeJS.Timeout | null = null;
let statusTimer: NodeJS.Timeout | null = null;
let statusStartedAt: number | null = null;
let lastActivityStatus = activityStatus;
const state: TuiStateAccess = {
get agentDefaultId() {
@@ -178,14 +188,14 @@ export async function runTui(opts: TuiOptions) {
});
const header = new Text("", 1, 0);
const status = new Text("", 1, 0);
const statusContainer = new Container();
const footer = new Text("", 1, 0);
const chatLog = new ChatLog();
const editor = new CustomEditor(editorTheme);
const root = new Container();
root.addChild(header);
root.addChild(chatLog);
root.addChild(status);
root.addChild(statusContainer);
root.addChild(footer);
root.addChild(editor);
@@ -242,13 +252,79 @@ export async function runTui(opts: TuiOptions) {
);
};
const setStatus = (text: string) => {
status.setText(theme.dim(text));
const busyStates = new Set(["sending", "waiting", "streaming", "running"]);
let statusText: Text | null = null;
let statusLoader: Loader | null = null;
const formatElapsed = (startMs: number) => {
const totalSeconds = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
if (totalSeconds < 60) return `${totalSeconds}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}m ${seconds}s`;
};
const ensureStatusText = () => {
if (statusText) return;
statusContainer.clear();
statusLoader?.stop();
statusLoader = null;
statusText = new Text("", 1, 0);
statusContainer.addChild(statusText);
};
const ensureStatusLoader = () => {
if (statusLoader) return;
statusContainer.clear();
statusText = null;
statusLoader = new Loader(
tui,
(spinner) => theme.accent(spinner),
(text) => theme.bold(theme.accentSoft(text)),
"",
);
statusContainer.addChild(statusLoader);
};
const updateBusyStatusMessage = () => {
if (!statusLoader || !statusStartedAt) return;
const elapsed = formatElapsed(statusStartedAt);
statusLoader.setMessage(`${activityStatus}${elapsed} | ${connectionStatus}`);
};
const startStatusTimer = () => {
if (statusTimer) return;
statusTimer = setInterval(() => {
if (!busyStates.has(activityStatus)) return;
updateBusyStatusMessage();
}, 1000);
};
const stopStatusTimer = () => {
if (!statusTimer) return;
clearInterval(statusTimer);
statusTimer = null;
};
const renderStatus = () => {
const text = activityStatus ? `${connectionStatus} | ${activityStatus}` : connectionStatus;
setStatus(text);
const isBusy = busyStates.has(activityStatus);
if (isBusy) {
if (!statusStartedAt || lastActivityStatus !== activityStatus) {
statusStartedAt = Date.now();
}
ensureStatusLoader();
updateBusyStatusMessage();
startStatusTimer();
} else {
statusStartedAt = null;
stopStatusTimer();
statusLoader?.stop();
statusLoader = null;
ensureStatusText();
const text = activityStatus ? `${connectionStatus} | ${activityStatus}` : connectionStatus;
statusText?.setText(theme.dim(text));
}
lastActivityStatus = activityStatus;
};
const setConnectionStatus = (text: string, ttlMs?: number) => {