feat: improve tui status line

This commit is contained in:
Peter Steinberger
2026-01-10 03:53:23 +01:00
parent 895cd06ecc
commit 8e63cd9a76

View File

@@ -152,6 +152,9 @@ export async function runTui(opts: TuiOptions) {
let autoMessageSent = false; let autoMessageSent = false;
let sessionInfo: SessionInfo = {}; let sessionInfo: SessionInfo = {};
let lastCtrlCAt = 0; let lastCtrlCAt = 0;
let activityStatus = "idle";
let connectionStatus = "connecting";
let statusTimeout: NodeJS.Timeout | null = null;
const client = new GatewayChatClient({ const client = new GatewayChatClient({
url: opts.url, url: opts.url,
@@ -218,6 +221,30 @@ export async function runTui(opts: TuiOptions) {
status.setText(theme.dim(text)); status.setText(theme.dim(text));
}; };
const renderStatus = () => {
const text = activityStatus
? `${connectionStatus} | ${activityStatus}`
: connectionStatus;
setStatus(text);
};
const setConnectionStatus = (text: string, ttlMs?: number) => {
connectionStatus = text;
renderStatus();
if (statusTimeout) clearTimeout(statusTimeout);
if (ttlMs && ttlMs > 0) {
statusTimeout = setTimeout(() => {
connectionStatus = isConnected ? "connected" : "disconnected";
renderStatus();
}, ttlMs);
}
};
const setActivityStatus = (text: string) => {
activityStatus = text;
renderStatus();
};
const updateFooter = () => { const updateFooter = () => {
const connection = isConnected ? "connected" : "disconnected"; const connection = isConnected ? "connected" : "disconnected";
const sessionKeyLabel = formatSessionKey(currentSessionKey); const sessionKeyLabel = formatSessionKey(currentSessionKey);
@@ -246,7 +273,7 @@ export async function runTui(opts: TuiOptions) {
const deliver = deliverDefault ? "on" : "off"; const deliver = deliverDefault ? "on" : "off";
footer.setText( footer.setText(
theme.dim( theme.dim(
`${connection} | agent ${agentLabel} | session ${sessionLabel} | model ${modelLabel} | think ${think} | verbose ${verbose}${reasoningLabel ? ` | ${reasoningLabel}` : ""} | ${tokens} | deliver ${deliver}`, `${connection} | agent ${agentLabel} | session ${sessionLabel} | ${modelLabel} | think ${think} | verbose ${verbose}${reasoningLabel ? ` | ${reasoningLabel}` : ""} | ${tokens} | deliver ${deliver}`,
), ),
); );
}; };
@@ -440,10 +467,10 @@ export async function runTui(opts: TuiOptions) {
sessionKey: currentSessionKey, sessionKey: currentSessionKey,
runId: activeChatRunId, runId: activeChatRunId,
}); });
setStatus("aborted"); setActivityStatus("aborted");
} catch (err) { } catch (err) {
chatLog.addSystem(`abort failed: ${String(err)}`); chatLog.addSystem(`abort failed: ${String(err)}`);
setStatus("abort failed"); setActivityStatus("abort failed");
} }
tui.requestRender(); tui.requestRender();
}; };
@@ -478,7 +505,7 @@ export async function runTui(opts: TuiOptions) {
}); });
if (!text) return; if (!text) return;
chatLog.updateAssistant(text, evt.runId); chatLog.updateAssistant(text, evt.runId);
setStatus("streaming"); setActivityStatus("streaming");
} }
if (evt.state === "final") { if (evt.state === "final") {
const text = extractTextFromMessage(evt.message, { const text = extractTextFromMessage(evt.message, {
@@ -487,17 +514,17 @@ export async function runTui(opts: TuiOptions) {
chatLog.finalizeAssistant(text || "(no output)", evt.runId); chatLog.finalizeAssistant(text || "(no output)", evt.runId);
noteFinalizedRun(evt.runId); noteFinalizedRun(evt.runId);
activeChatRunId = null; activeChatRunId = null;
setStatus("idle"); setActivityStatus("idle");
} }
if (evt.state === "aborted") { if (evt.state === "aborted") {
chatLog.addSystem("run aborted"); chatLog.addSystem("run aborted");
activeChatRunId = null; activeChatRunId = null;
setStatus("aborted"); setActivityStatus("aborted");
} }
if (evt.state === "error") { if (evt.state === "error") {
chatLog.addSystem(`run error: ${evt.errorMessage ?? "unknown"}`); chatLog.addSystem(`run error: ${evt.errorMessage ?? "unknown"}`);
activeChatRunId = null; activeChatRunId = null;
setStatus("error"); setActivityStatus("error");
} }
tui.requestRender(); tui.requestRender();
}; };
@@ -528,9 +555,9 @@ export async function runTui(opts: TuiOptions) {
} }
if (evt.stream === "lifecycle") { if (evt.stream === "lifecycle") {
const phase = typeof evt.data?.phase === "string" ? evt.data.phase : ""; const phase = typeof evt.data?.phase === "string" ? evt.data.phase : "";
if (phase === "start") setStatus("running"); if (phase === "start") setActivityStatus("running");
if (phase === "end") setStatus("idle"); if (phase === "end") setActivityStatus("idle");
if (phase === "error") setStatus("error"); if (phase === "error") setActivityStatus("error");
tui.requestRender(); tui.requestRender();
} }
}; };
@@ -895,7 +922,7 @@ export async function runTui(opts: TuiOptions) {
try { try {
chatLog.addUser(text); chatLog.addUser(text);
tui.requestRender(); tui.requestRender();
setStatus("sending"); setActivityStatus("sending");
const { runId } = await client.sendChat({ const { runId } = await client.sendChat({
sessionKey: currentSessionKey, sessionKey: currentSessionKey,
message: text, message: text,
@@ -904,10 +931,10 @@ export async function runTui(opts: TuiOptions) {
timeoutMs: opts.timeoutMs, timeoutMs: opts.timeoutMs,
}); });
activeChatRunId = runId; activeChatRunId = runId;
setStatus("waiting"); setActivityStatus("waiting");
} catch (err) { } catch (err) {
chatLog.addSystem(`send failed: ${String(err)}`); chatLog.addSystem(`send failed: ${String(err)}`);
setStatus("error"); setActivityStatus("error");
} }
tui.requestRender(); tui.requestRender();
}; };
@@ -933,7 +960,7 @@ export async function runTui(opts: TuiOptions) {
const now = Date.now(); const now = Date.now();
if (editor.getText().trim().length > 0) { if (editor.getText().trim().length > 0) {
editor.setText(""); editor.setText("");
setStatus("cleared input"); setActivityStatus("cleared input");
tui.requestRender(); tui.requestRender();
return; return;
} }
@@ -943,7 +970,7 @@ export async function runTui(opts: TuiOptions) {
process.exit(0); process.exit(0);
} }
lastCtrlCAt = now; lastCtrlCAt = now;
setStatus("press ctrl+c again to exit"); setActivityStatus("press ctrl+c again to exit");
tui.requestRender(); tui.requestRender();
}; };
editor.onCtrlD = () => { editor.onCtrlD = () => {
@@ -954,7 +981,7 @@ export async function runTui(opts: TuiOptions) {
editor.onCtrlO = () => { editor.onCtrlO = () => {
toolsExpanded = !toolsExpanded; toolsExpanded = !toolsExpanded;
chatLog.setToolsExpanded(toolsExpanded); chatLog.setToolsExpanded(toolsExpanded);
setStatus(toolsExpanded ? "tools expanded" : "tools collapsed"); setActivityStatus(toolsExpanded ? "tools expanded" : "tools collapsed");
tui.requestRender(); tui.requestRender();
}; };
editor.onCtrlL = () => { editor.onCtrlL = () => {
@@ -978,20 +1005,20 @@ export async function runTui(opts: TuiOptions) {
client.onConnected = () => { client.onConnected = () => {
isConnected = true; isConnected = true;
setStatus("connected"); setConnectionStatus("connected");
void (async () => { void (async () => {
await refreshAgents(); await refreshAgents();
updateHeader(); updateHeader();
if (!historyLoaded) { if (!historyLoaded) {
await loadHistory(); await loadHistory();
chatLog.addSystem("gateway connected"); setConnectionStatus("gateway connected", 4000);
tui.requestRender(); tui.requestRender();
if (!autoMessageSent && autoMessage) { if (!autoMessageSent && autoMessage) {
autoMessageSent = true; autoMessageSent = true;
await sendMessage(autoMessage); await sendMessage(autoMessage);
} }
} else { } else {
chatLog.addSystem("gateway reconnected"); setConnectionStatus("gateway reconnected", 4000);
} }
updateFooter(); updateFooter();
tui.requestRender(); tui.requestRender();
@@ -1000,23 +1027,24 @@ export async function runTui(opts: TuiOptions) {
client.onDisconnected = (reason) => { client.onDisconnected = (reason) => {
isConnected = false; isConnected = false;
chatLog.addSystem(`gateway disconnected: ${reason || "closed"}`); const reasonLabel = reason?.trim() ? reason.trim() : "closed";
setStatus("disconnected"); setConnectionStatus(`gateway disconnected: ${reasonLabel}`, 5000);
setActivityStatus("idle");
updateFooter(); updateFooter();
tui.requestRender(); tui.requestRender();
}; };
client.onGap = (info) => { client.onGap = (info) => {
chatLog.addSystem( setConnectionStatus(
`event gap: expected ${info.expected}, got ${info.received}`, `event gap: expected ${info.expected}, got ${info.received}`,
5000,
); );
tui.requestRender(); tui.requestRender();
}; };
updateHeader(); updateHeader();
setStatus("connecting"); setConnectionStatus("connecting");
updateFooter(); updateFooter();
chatLog.addSystem("connecting...");
tui.start(); tui.start();
client.start(); client.start();
} }