feat: improve tui status output
This commit is contained in:
@@ -96,14 +96,6 @@ export function getSlashCommands(): SlashCommand[] {
|
|||||||
(value) => ({ value, label: value }),
|
(value) => ({ value, label: value }),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "deliver",
|
|
||||||
description: "Toggle delivery of assistant replies",
|
|
||||||
getArgumentCompletions: (prefix) =>
|
|
||||||
TOGGLE.filter((v) => v.startsWith(prefix.toLowerCase())).map(
|
|
||||||
(value) => ({ value, label: value }),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ name: "abort", description: "Abort active run" },
|
{ name: "abort", description: "Abort active run" },
|
||||||
{ name: "new", description: "Reset the session" },
|
{ name: "new", description: "Reset the session" },
|
||||||
{ name: "reset", description: "Reset the session" },
|
{ name: "reset", description: "Reset the session" },
|
||||||
@@ -128,7 +120,6 @@ export function helpText(): string {
|
|||||||
"/elevated <on|off>",
|
"/elevated <on|off>",
|
||||||
"/elev <on|off>",
|
"/elev <on|off>",
|
||||||
"/activation <mention|always>",
|
"/activation <mention|always>",
|
||||||
"/deliver <on|off>",
|
|
||||||
"/new or /reset",
|
"/new or /reset",
|
||||||
"/abort",
|
"/abort",
|
||||||
"/settings",
|
"/settings",
|
||||||
|
|||||||
164
src/tui/tui.ts
164
src/tui/tui.ts
@@ -9,6 +9,7 @@ import {
|
|||||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { normalizeUsageDisplay } from "../auto-reply/thinking.js";
|
import { normalizeUsageDisplay } from "../auto-reply/thinking.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
|
import { formatAge } from "../infra/provider-summary.js";
|
||||||
import {
|
import {
|
||||||
buildAgentMainSessionKey,
|
buildAgentMainSessionKey,
|
||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
@@ -74,6 +75,30 @@ type AgentSummary = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GatewayStatusSummary = {
|
||||||
|
web?: { linked?: boolean; authAgeMs?: number | null };
|
||||||
|
heartbeatSeconds?: number;
|
||||||
|
providerSummary?: string[];
|
||||||
|
queuedSystemEvents?: string[];
|
||||||
|
sessions?: {
|
||||||
|
path?: string;
|
||||||
|
count?: number;
|
||||||
|
defaults?: { model?: string | null; contextTokens?: number | null };
|
||||||
|
recent?: Array<{
|
||||||
|
key: string;
|
||||||
|
kind?: string;
|
||||||
|
updatedAt?: number | null;
|
||||||
|
age?: number | null;
|
||||||
|
model?: string | null;
|
||||||
|
totalTokens?: number | null;
|
||||||
|
contextTokens?: number | null;
|
||||||
|
remainingTokens?: number | null;
|
||||||
|
percentUsed?: number | null;
|
||||||
|
flags?: string[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function extractTextBlocks(
|
function extractTextBlocks(
|
||||||
content: unknown,
|
content: unknown,
|
||||||
opts?: { includeThinking?: boolean },
|
opts?: { includeThinking?: boolean },
|
||||||
@@ -120,6 +145,29 @@ function formatTokens(total?: number | null, context?: number | null) {
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatContextUsageLine(params: {
|
||||||
|
total?: number | null;
|
||||||
|
context?: number | null;
|
||||||
|
remaining?: number | null;
|
||||||
|
percent?: number | null;
|
||||||
|
}) {
|
||||||
|
const totalLabel =
|
||||||
|
typeof params.total === "number" ? formatTokenCount(params.total) : "?";
|
||||||
|
const ctxLabel =
|
||||||
|
typeof params.context === "number" ? formatTokenCount(params.context) : "?";
|
||||||
|
const pct =
|
||||||
|
typeof params.percent === "number"
|
||||||
|
? Math.min(999, Math.round(params.percent))
|
||||||
|
: null;
|
||||||
|
const remainingLabel =
|
||||||
|
typeof params.remaining === "number"
|
||||||
|
? `${formatTokenCount(params.remaining)} left`
|
||||||
|
: null;
|
||||||
|
const pctLabel = pct !== null ? `${pct}%` : null;
|
||||||
|
const extra = [remainingLabel, pctLabel].filter(Boolean).join(", ");
|
||||||
|
return `tokens ${totalLabel}/${ctxLabel}${extra ? ` (${extra})` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
function asString(value: unknown, fallback = ""): string {
|
function asString(value: unknown, fallback = ""): string {
|
||||||
if (typeof value === "string") return value;
|
if (typeof value === "string") return value;
|
||||||
if (typeof value === "number" || typeof value === "boolean") {
|
if (typeof value === "number" || typeof value === "boolean") {
|
||||||
@@ -147,7 +195,7 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
let toolsExpanded = false;
|
let toolsExpanded = false;
|
||||||
let showThinking = false;
|
let showThinking = false;
|
||||||
let deliverDefault = Boolean(opts.deliver);
|
const deliverDefault = opts.deliver ?? true;
|
||||||
const autoMessage = opts.message?.trim();
|
const autoMessage = opts.message?.trim();
|
||||||
let autoMessageSent = false;
|
let autoMessageSent = false;
|
||||||
let sessionInfo: SessionInfo = {};
|
let sessionInfo: SessionInfo = {};
|
||||||
@@ -246,7 +294,6 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateFooter = () => {
|
const updateFooter = () => {
|
||||||
const connection = isConnected ? "connected" : "disconnected";
|
|
||||||
const sessionKeyLabel = formatSessionKey(currentSessionKey);
|
const sessionKeyLabel = formatSessionKey(currentSessionKey);
|
||||||
const sessionLabel = sessionInfo.displayName
|
const sessionLabel = sessionInfo.displayName
|
||||||
? `${sessionKeyLabel} (${sessionInfo.displayName})`
|
? `${sessionKeyLabel} (${sessionInfo.displayName})`
|
||||||
@@ -270,14 +317,89 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
: reasoning === "stream"
|
: reasoning === "stream"
|
||||||
? "reasoning:stream"
|
? "reasoning:stream"
|
||||||
: null;
|
: null;
|
||||||
const deliver = deliverDefault ? "on" : "off";
|
|
||||||
footer.setText(
|
footer.setText(
|
||||||
theme.dim(
|
theme.dim(
|
||||||
`${connection} | agent ${agentLabel} | session ${sessionLabel} | ${modelLabel} | think ${think} | verbose ${verbose}${reasoningLabel ? ` | ${reasoningLabel}` : ""} | ${tokens} | deliver ${deliver}`,
|
`agent ${agentLabel} | session ${sessionLabel} | ${modelLabel} | think ${think} | verbose ${verbose}${reasoningLabel ? ` | ${reasoningLabel}` : ""} | ${tokens}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatStatusSummary = (summary: GatewayStatusSummary) => {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push("Gateway status");
|
||||||
|
|
||||||
|
const webLinked = summary.web?.linked === true;
|
||||||
|
const authAge =
|
||||||
|
webLinked && typeof summary.web?.authAgeMs === "number"
|
||||||
|
? ` (last refreshed ${formatAge(summary.web.authAgeMs)})`
|
||||||
|
: "";
|
||||||
|
lines.push(`Web session: ${webLinked ? "linked" : "not linked"}${authAge}`);
|
||||||
|
|
||||||
|
const providerSummary = Array.isArray(summary.providerSummary)
|
||||||
|
? summary.providerSummary
|
||||||
|
: [];
|
||||||
|
if (providerSummary.length > 0) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push("System:");
|
||||||
|
for (const line of providerSummary) {
|
||||||
|
lines.push(` ${line}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof summary.heartbeatSeconds === "number") {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`Heartbeat: ${summary.heartbeatSeconds}s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionPath = summary.sessions?.path;
|
||||||
|
if (sessionPath) lines.push(`Session store: ${sessionPath}`);
|
||||||
|
|
||||||
|
const defaults = summary.sessions?.defaults;
|
||||||
|
const defaultModel = defaults?.model ?? "unknown";
|
||||||
|
const defaultCtx =
|
||||||
|
typeof defaults?.contextTokens === "number"
|
||||||
|
? ` (${formatTokenCount(defaults.contextTokens)} ctx)`
|
||||||
|
: "";
|
||||||
|
lines.push(`Default model: ${defaultModel}${defaultCtx}`);
|
||||||
|
|
||||||
|
const sessionCount = summary.sessions?.count ?? 0;
|
||||||
|
lines.push(`Active sessions: ${sessionCount}`);
|
||||||
|
|
||||||
|
const recent = Array.isArray(summary.sessions?.recent)
|
||||||
|
? summary.sessions?.recent
|
||||||
|
: [];
|
||||||
|
if (recent.length > 0) {
|
||||||
|
lines.push("Recent sessions:");
|
||||||
|
for (const entry of recent) {
|
||||||
|
const ageLabel =
|
||||||
|
typeof entry.age === "number" ? formatAge(entry.age) : "no activity";
|
||||||
|
const model = entry.model ?? "unknown";
|
||||||
|
const usage = formatContextUsageLine({
|
||||||
|
total: entry.totalTokens ?? null,
|
||||||
|
context: entry.contextTokens ?? null,
|
||||||
|
remaining: entry.remainingTokens ?? null,
|
||||||
|
percent: entry.percentUsed ?? null,
|
||||||
|
});
|
||||||
|
const flags = entry.flags?.length
|
||||||
|
? ` | flags: ${entry.flags.join(", ")}`
|
||||||
|
: "";
|
||||||
|
lines.push(
|
||||||
|
`- ${entry.key}${entry.kind ? ` [${entry.kind}]` : ""} | ${ageLabel} | model ${model} | ${usage}${flags}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queued = Array.isArray(summary.queuedSystemEvents)
|
||||||
|
? summary.queuedSystemEvents
|
||||||
|
: [];
|
||||||
|
if (queued.length > 0) {
|
||||||
|
const preview = queued.slice(0, 3).join(" | ");
|
||||||
|
lines.push(`Queued system events (${queued.length}): ${preview}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
const closeOverlay = () => {
|
const closeOverlay = () => {
|
||||||
overlay.clear();
|
overlay.clear();
|
||||||
tui.setFocus(editor);
|
tui.setFocus(editor);
|
||||||
@@ -675,12 +797,6 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
|
|
||||||
const openSettings = () => {
|
const openSettings = () => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
|
||||||
id: "deliver",
|
|
||||||
label: "Deliver replies",
|
|
||||||
currentValue: deliverDefault ? "on" : "off",
|
|
||||||
values: ["off", "on"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "tools",
|
id: "tools",
|
||||||
label: "Tool output",
|
label: "Tool output",
|
||||||
@@ -697,10 +813,6 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
const settings = createSettingsList(
|
const settings = createSettingsList(
|
||||||
items,
|
items,
|
||||||
(id, value) => {
|
(id, value) => {
|
||||||
if (id === "deliver") {
|
|
||||||
deliverDefault = value === "on";
|
|
||||||
updateFooter();
|
|
||||||
}
|
|
||||||
if (id === "tools") {
|
if (id === "tools") {
|
||||||
toolsExpanded = value === "expanded";
|
toolsExpanded = value === "expanded";
|
||||||
chatLog.setToolsExpanded(toolsExpanded);
|
chatLog.setToolsExpanded(toolsExpanded);
|
||||||
@@ -730,11 +842,16 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
case "status":
|
case "status":
|
||||||
try {
|
try {
|
||||||
const status = await client.getStatus();
|
const status = await client.getStatus();
|
||||||
chatLog.addSystem(
|
if (typeof status === "string") {
|
||||||
typeof status === "string"
|
chatLog.addSystem(status);
|
||||||
? status
|
break;
|
||||||
: JSON.stringify(status, null, 2),
|
}
|
||||||
);
|
if (status && typeof status === "object") {
|
||||||
|
const lines = formatStatusSummary(status as GatewayStatusSummary);
|
||||||
|
for (const line of lines) chatLog.addSystem(line);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
chatLog.addSystem("status: unknown response");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
chatLog.addSystem(`status failed: ${String(err)}`);
|
chatLog.addSystem(`status failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
@@ -880,15 +997,6 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
chatLog.addSystem(`activation failed: ${String(err)}`);
|
chatLog.addSystem(`activation failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "deliver":
|
|
||||||
if (!args) {
|
|
||||||
chatLog.addSystem("usage: /deliver <on|off>");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
deliverDefault = args === "on";
|
|
||||||
updateFooter();
|
|
||||||
chatLog.addSystem(`deliver ${deliverDefault ? "on" : "off"}`);
|
|
||||||
break;
|
|
||||||
case "new":
|
case "new":
|
||||||
case "reset":
|
case "reset":
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user