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.
|
- 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.
|
- 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: 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`).
|
- 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.
|
- 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.
|
- 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 { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -53,6 +60,9 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
let activityStatus = "idle";
|
let activityStatus = "idle";
|
||||||
let connectionStatus = "connecting";
|
let connectionStatus = "connecting";
|
||||||
let statusTimeout: NodeJS.Timeout | null = null;
|
let statusTimeout: NodeJS.Timeout | null = null;
|
||||||
|
let statusTimer: NodeJS.Timeout | null = null;
|
||||||
|
let statusStartedAt: number | null = null;
|
||||||
|
let lastActivityStatus = activityStatus;
|
||||||
|
|
||||||
const state: TuiStateAccess = {
|
const state: TuiStateAccess = {
|
||||||
get agentDefaultId() {
|
get agentDefaultId() {
|
||||||
@@ -178,14 +188,14 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const header = new Text("", 1, 0);
|
const header = new Text("", 1, 0);
|
||||||
const status = new Text("", 1, 0);
|
const statusContainer = new Container();
|
||||||
const footer = new Text("", 1, 0);
|
const footer = new Text("", 1, 0);
|
||||||
const chatLog = new ChatLog();
|
const chatLog = new ChatLog();
|
||||||
const editor = new CustomEditor(editorTheme);
|
const editor = new CustomEditor(editorTheme);
|
||||||
const root = new Container();
|
const root = new Container();
|
||||||
root.addChild(header);
|
root.addChild(header);
|
||||||
root.addChild(chatLog);
|
root.addChild(chatLog);
|
||||||
root.addChild(status);
|
root.addChild(statusContainer);
|
||||||
root.addChild(footer);
|
root.addChild(footer);
|
||||||
root.addChild(editor);
|
root.addChild(editor);
|
||||||
|
|
||||||
@@ -242,13 +252,79 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setStatus = (text: string) => {
|
const busyStates = new Set(["sending", "waiting", "streaming", "running"]);
|
||||||
status.setText(theme.dim(text));
|
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 renderStatus = () => {
|
||||||
const text = activityStatus ? `${connectionStatus} | ${activityStatus}` : connectionStatus;
|
const isBusy = busyStates.has(activityStatus);
|
||||||
setStatus(text);
|
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) => {
|
const setConnectionStatus = (text: string, ttlMs?: number) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user