fix: add TUI status spinner
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user