@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
1,621 lines (1,603 loc) • 78.1 kB
JavaScript
import { I as listThinkingLevelLabels, P as formatThinkingLevels, U as resolveResponseUsageMode, V as normalizeUsageDisplay } from "./pi-embedded-helpers-CC00lEFI.js";
import { u as resolveGatewayPort } from "./paths-BDd7_JUB.js";
import { H as parseAgentSessionKey, N as normalizeAgentId, P as normalizeMainKey, c as resolveDefaultAgentId, k as buildAgentMainSessionKey } from "./agent-scope-CrgUOY3f.js";
import { s as visibleWidth } from "./subsystem-46MXi6Ip.js";
import { M as VERSION, r as loadConfig } from "./config-qgIz1lbh.js";
import { m as GATEWAY_CLIENT_NAMES, p as GATEWAY_CLIENT_MODES } from "./message-channel-CQGWXVL4.js";
import { n as resolveToolDisplay, t as formatToolDetail } from "./tool-display-rIUh61kT.js";
import { a as extractContentFromMessage, c as formatContextUsageLine, d as resolveFinalAssistantText, i as composeThinkingAndContent, l as formatTokens, n as formatAge, o as extractTextFromMessage, p as formatTokenCount, r as asString, s as extractThinkingFromMessage, u as isCommandMessage, v as listChatCommands, y as listChatCommandsForConfig } from "./channel-summary-C8GoEKgH.js";
import { St as PROTOCOL_VERSION, t as GatewayClient } from "./client-zqMhLTAX.js";
import chalk from "chalk";
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { Box, CombinedAutocompleteProvider, Container, Editor, Input, Key, Loader, Markdown, ProcessTerminal, SelectList, SettingsList, Spacer, TUI, Text, getEditorKeybindings, isKeyRelease, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
import { highlight, supportsLanguage } from "cli-highlight";
//#region src/tui/commands.ts
const VERBOSE_LEVELS = ["on", "off"];
const REASONING_LEVELS = ["on", "off"];
const ELEVATED_LEVELS = [
"on",
"off",
"ask",
"full"
];
const ACTIVATION_LEVELS = ["mention", "always"];
const USAGE_FOOTER_LEVELS = [
"off",
"tokens",
"full"
];
const COMMAND_ALIASES = { elev: "elevated" };
function parseCommand(input) {
const trimmed = input.replace(/^\//, "").trim();
if (!trimmed) return {
name: "",
args: ""
};
const [name, ...rest] = trimmed.split(/\s+/);
const normalized = name.toLowerCase();
return {
name: COMMAND_ALIASES[normalized] ?? normalized,
args: rest.join(" ").trim()
};
}
function getSlashCommands(options = {}) {
const thinkLevels = listThinkingLevelLabels(options.provider, options.model);
const commands = [
{
name: "help",
description: "Show slash command help"
},
{
name: "status",
description: "Show gateway status summary"
},
{
name: "agent",
description: "Switch agent (or open picker)"
},
{
name: "agents",
description: "Open agent picker"
},
{
name: "session",
description: "Switch session (or open picker)"
},
{
name: "sessions",
description: "Open session picker"
},
{
name: "model",
description: "Set model (or open picker)"
},
{
name: "models",
description: "Open model picker"
},
{
name: "think",
description: "Set thinking level",
getArgumentCompletions: (prefix) => thinkLevels.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({
value,
label: value
}))
},
{
name: "verbose",
description: "Set verbose on/off",
getArgumentCompletions: (prefix) => VERBOSE_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({
value,
label: value
}))
},
{
name: "reasoning",
description: "Set reasoning on/off",
getArgumentCompletions: (prefix) => REASONING_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({
value,
label: value
}))
},
{
name: "usage",
description: "Toggle per-response usage line",
getArgumentCompletions: (prefix) => USAGE_FOOTER_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({
value,
label: value
}))
},
{
name: "elevated",
description: "Set elevated on/off/ask/full",
getArgumentCompletions: (prefix) => ELEVATED_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({
value,
label: value
}))
},
{
name: "elev",
description: "Alias for /elevated",
getArgumentCompletions: (prefix) => ELEVATED_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({
value,
label: value
}))
},
{
name: "activation",
description: "Set group activation",
getArgumentCompletions: (prefix) => ACTIVATION_LEVELS.filter((v) => v.startsWith(prefix.toLowerCase())).map((value) => ({
value,
label: value
}))
},
{
name: "abort",
description: "Abort active run"
},
{
name: "new",
description: "Reset the session"
},
{
name: "reset",
description: "Reset the session"
},
{
name: "settings",
description: "Open settings"
},
{
name: "exit",
description: "Exit the TUI"
},
{
name: "quit",
description: "Exit the TUI"
}
];
const seen = new Set(commands.map((command) => command.name));
const gatewayCommands = options.cfg ? listChatCommandsForConfig(options.cfg) : listChatCommands();
for (const command of gatewayCommands) {
const aliases = command.textAliases.length > 0 ? command.textAliases : [`/${command.key}`];
for (const alias of aliases) {
const name = alias.replace(/^\//, "").trim();
if (!name || seen.has(name)) continue;
seen.add(name);
commands.push({
name,
description: command.description
});
}
}
return commands;
}
function helpText(options = {}) {
return [
"Slash commands:",
"/help",
"/commands",
"/status",
"/agent <id> (or /agents)",
"/session <key> (or /sessions)",
"/model <provider/model> (or /models)",
`/think <${formatThinkingLevels(options.provider, options.model, "|")}>`,
"/verbose <on|off>",
"/reasoning <on|off>",
"/usage <off|tokens|full>",
"/elevated <on|off|ask|full>",
"/elev <on|off|ask|full>",
"/activation <mention|always>",
"/new or /reset",
"/abort",
"/settings",
"/exit"
].join("\n");
}
//#endregion
//#region src/tui/theme/syntax-theme.ts
/**
* Syntax highlighting theme for code blocks.
* Uses chalk functions to style different token types.
*/
function createSyntaxTheme(fallback) {
return {
keyword: chalk.hex("#C586C0"),
built_in: chalk.hex("#4EC9B0"),
type: chalk.hex("#4EC9B0"),
literal: chalk.hex("#569CD6"),
number: chalk.hex("#B5CEA8"),
string: chalk.hex("#CE9178"),
regexp: chalk.hex("#D16969"),
symbol: chalk.hex("#B5CEA8"),
class: chalk.hex("#4EC9B0"),
function: chalk.hex("#DCDCAA"),
title: chalk.hex("#DCDCAA"),
params: chalk.hex("#9CDCFE"),
comment: chalk.hex("#6A9955"),
doctag: chalk.hex("#608B4E"),
meta: chalk.hex("#9CDCFE"),
"meta-keyword": chalk.hex("#C586C0"),
"meta-string": chalk.hex("#CE9178"),
section: chalk.hex("#DCDCAA"),
tag: chalk.hex("#569CD6"),
name: chalk.hex("#9CDCFE"),
attr: chalk.hex("#9CDCFE"),
attribute: chalk.hex("#9CDCFE"),
variable: chalk.hex("#9CDCFE"),
bullet: chalk.hex("#D7BA7D"),
code: chalk.hex("#CE9178"),
emphasis: chalk.italic,
strong: chalk.bold,
formula: chalk.hex("#C586C0"),
link: chalk.hex("#4EC9B0"),
quote: chalk.hex("#6A9955"),
addition: chalk.hex("#B5CEA8"),
deletion: chalk.hex("#F44747"),
"selector-tag": chalk.hex("#D7BA7D"),
"selector-id": chalk.hex("#D7BA7D"),
"selector-class": chalk.hex("#D7BA7D"),
"selector-attr": chalk.hex("#D7BA7D"),
"selector-pseudo": chalk.hex("#D7BA7D"),
"template-tag": chalk.hex("#C586C0"),
"template-variable": chalk.hex("#9CDCFE"),
default: fallback
};
}
//#endregion
//#region src/tui/theme/theme.ts
const palette = {
text: "#E8E3D5",
dim: "#7B7F87",
accent: "#F6C453",
accentSoft: "#F2A65A",
border: "#3C414B",
userBg: "#2B2F36",
userText: "#F3EEE0",
systemText: "#9BA3B2",
toolPendingBg: "#1F2A2F",
toolSuccessBg: "#1E2D23",
toolErrorBg: "#2F1F1F",
toolTitle: "#F6C453",
toolOutput: "#E1DACB",
quote: "#8CC8FF",
quoteBorder: "#3B4D6B",
code: "#F0C987",
codeBlock: "#1E232A",
codeBorder: "#343A45",
link: "#7DD3A5",
error: "#F97066",
success: "#7DD3A5"
};
const fg = (hex) => (text) => chalk.hex(hex)(text);
const bg = (hex) => (text) => chalk.bgHex(hex)(text);
const syntaxTheme = createSyntaxTheme(fg(palette.code));
/**
* Highlight code with syntax coloring.
* Returns an array of lines with ANSI escape codes.
*/
function highlightCode(code, lang) {
try {
return highlight(code, {
language: lang && supportsLanguage(lang) ? lang : void 0,
theme: syntaxTheme,
ignoreIllegals: true
}).split("\n");
} catch {
return code.split("\n").map((line) => fg(palette.code)(line));
}
}
const theme = {
fg: fg(palette.text),
dim: fg(palette.dim),
accent: fg(palette.accent),
accentSoft: fg(palette.accentSoft),
success: fg(palette.success),
error: fg(palette.error),
header: (text) => chalk.bold(fg(palette.accent)(text)),
system: fg(palette.systemText),
userBg: bg(palette.userBg),
userText: fg(palette.userText),
toolTitle: fg(palette.toolTitle),
toolOutput: fg(palette.toolOutput),
toolPendingBg: bg(palette.toolPendingBg),
toolSuccessBg: bg(palette.toolSuccessBg),
toolErrorBg: bg(palette.toolErrorBg),
border: fg(palette.border),
bold: (text) => chalk.bold(text),
italic: (text) => chalk.italic(text)
};
const markdownTheme = {
heading: (text) => chalk.bold(fg(palette.accent)(text)),
link: (text) => fg(palette.link)(text),
linkUrl: (text) => chalk.dim(text),
code: (text) => fg(palette.code)(text),
codeBlock: (text) => fg(palette.code)(text),
codeBlockBorder: (text) => fg(palette.codeBorder)(text),
quote: (text) => fg(palette.quote)(text),
quoteBorder: (text) => fg(palette.quoteBorder)(text),
hr: (text) => fg(palette.border)(text),
listBullet: (text) => fg(palette.accentSoft)(text),
bold: (text) => chalk.bold(text),
italic: (text) => chalk.italic(text),
strikethrough: (text) => chalk.strikethrough(text),
underline: (text) => chalk.underline(text),
highlightCode
};
const selectListTheme = {
selectedPrefix: (text) => fg(palette.accent)(text),
selectedText: (text) => chalk.bold(fg(palette.accent)(text)),
description: (text) => fg(palette.dim)(text),
scrollInfo: (text) => fg(palette.dim)(text),
noMatch: (text) => fg(palette.dim)(text)
};
const filterableSelectListTheme = {
...selectListTheme,
filterLabel: (text) => fg(palette.dim)(text)
};
const settingsListTheme = {
label: (text, selected) => selected ? chalk.bold(fg(palette.accent)(text)) : fg(palette.text)(text),
value: (text, selected) => selected ? fg(palette.accentSoft)(text) : fg(palette.dim)(text),
description: (text) => fg(palette.systemText)(text),
cursor: fg(palette.accent)("→ "),
hint: (text) => fg(palette.dim)(text)
};
const editorTheme = {
borderColor: (text) => fg(palette.border)(text),
selectList: selectListTheme
};
const searchableSelectListTheme = {
selectedPrefix: (text) => fg(palette.accent)(text),
selectedText: (text) => chalk.bold(fg(palette.accent)(text)),
description: (text) => fg(palette.dim)(text),
scrollInfo: (text) => fg(palette.dim)(text),
noMatch: (text) => fg(palette.dim)(text),
searchPrompt: (text) => fg(palette.accentSoft)(text),
searchInput: (text) => fg(palette.text)(text),
matchHighlight: (text) => chalk.bold(fg(palette.accent)(text))
};
//#endregion
//#region src/tui/components/assistant-message.ts
var AssistantMessageComponent = class extends Container {
constructor(text) {
super();
this.body = new Markdown(text, 1, 0, markdownTheme, { color: (line) => theme.fg(line) });
this.addChild(new Spacer(1));
this.addChild(this.body);
}
setText(text) {
this.body.setText(text);
}
};
//#endregion
//#region src/tui/components/tool-execution.ts
const PREVIEW_LINES = 12;
function formatArgs(toolName, args) {
const detail = formatToolDetail(resolveToolDisplay({
name: toolName,
args
}));
if (detail) return detail;
if (!args || typeof args !== "object") return "";
try {
return JSON.stringify(args);
} catch {
return "";
}
}
function extractText(result) {
if (!result?.content) return "";
const lines = [];
for (const entry of result.content) if (entry.type === "text" && entry.text) lines.push(entry.text);
else if (entry.type === "image") {
const mime = entry.mimeType ?? "image";
const size = entry.bytes ? ` ${Math.round(entry.bytes / 1024)}kb` : "";
const omitted = entry.omitted ? " (omitted)" : "";
lines.push(`[${mime}${size}${omitted}]`);
}
return lines.join("\n").trim();
}
var ToolExecutionComponent = class extends Container {
constructor(toolName, args) {
super();
this.expanded = false;
this.isError = false;
this.isPartial = true;
this.toolName = toolName;
this.args = args;
this.box = new Box(1, 1, (line) => theme.toolPendingBg(line));
this.header = new Text("", 0, 0);
this.argsLine = new Text("", 0, 0);
this.output = new Markdown("", 0, 0, markdownTheme, { color: (line) => theme.toolOutput(line) });
this.addChild(new Spacer(1));
this.addChild(this.box);
this.box.addChild(this.header);
this.box.addChild(this.argsLine);
this.box.addChild(this.output);
this.refresh();
}
setArgs(args) {
this.args = args;
this.refresh();
}
setExpanded(expanded) {
this.expanded = expanded;
this.refresh();
}
setResult(result, opts) {
this.result = result;
this.isPartial = false;
this.isError = Boolean(opts?.isError);
this.refresh();
}
setPartialResult(result) {
this.result = result;
this.isPartial = true;
this.refresh();
}
refresh() {
const bg = this.isPartial ? theme.toolPendingBg : this.isError ? theme.toolErrorBg : theme.toolSuccessBg;
this.box.setBgFn((line) => bg(line));
const display = resolveToolDisplay({
name: this.toolName,
args: this.args
});
const title = `${display.emoji} ${display.label}${this.isPartial ? " (running)" : ""}`;
this.header.setText(theme.toolTitle(theme.bold(title)));
const argLine = formatArgs(this.toolName, this.args);
this.argsLine.setText(argLine ? theme.dim(argLine) : theme.dim(" "));
const text = extractText(this.result) || (this.isPartial ? "…" : "");
if (!this.expanded && text) {
const lines = text.split("\n");
const preview = lines.length > PREVIEW_LINES ? `${lines.slice(0, PREVIEW_LINES).join("\n")}\n…` : text;
this.output.setText(preview);
} else this.output.setText(text);
}
};
//#endregion
//#region src/tui/components/user-message.ts
var UserMessageComponent = class extends Container {
constructor(text) {
super();
this.body = new Markdown(text, 1, 1, markdownTheme, {
bgColor: (line) => theme.userBg(line),
color: (line) => theme.userText(line)
});
this.addChild(new Spacer(1));
this.addChild(this.body);
}
setText(text) {
this.body.setText(text);
}
};
//#endregion
//#region src/tui/components/chat-log.ts
var ChatLog = class extends Container {
constructor(..._args) {
super(..._args);
this.toolById = /* @__PURE__ */ new Map();
this.streamingRuns = /* @__PURE__ */ new Map();
this.toolsExpanded = false;
}
clearAll() {
this.clear();
this.toolById.clear();
this.streamingRuns.clear();
}
addSystem(text) {
this.addChild(new Spacer(1));
this.addChild(new Text(theme.system(text), 1, 0));
}
addUser(text) {
this.addChild(new UserMessageComponent(text));
}
resolveRunId(runId) {
return runId ?? "default";
}
startAssistant(text, runId) {
const component = new AssistantMessageComponent(text);
this.streamingRuns.set(this.resolveRunId(runId), component);
this.addChild(component);
return component;
}
updateAssistant(text, runId) {
const effectiveRunId = this.resolveRunId(runId);
const existing = this.streamingRuns.get(effectiveRunId);
if (!existing) {
this.startAssistant(text, runId);
return;
}
existing.setText(text);
}
finalizeAssistant(text, runId) {
const effectiveRunId = this.resolveRunId(runId);
const existing = this.streamingRuns.get(effectiveRunId);
if (existing) {
existing.setText(text);
this.streamingRuns.delete(effectiveRunId);
return;
}
this.addChild(new AssistantMessageComponent(text));
}
startTool(toolCallId, toolName, args) {
const existing = this.toolById.get(toolCallId);
if (existing) {
existing.setArgs(args);
return existing;
}
const component = new ToolExecutionComponent(toolName, args);
component.setExpanded(this.toolsExpanded);
this.toolById.set(toolCallId, component);
this.addChild(component);
return component;
}
updateToolArgs(toolCallId, args) {
const existing = this.toolById.get(toolCallId);
if (!existing) return;
existing.setArgs(args);
}
updateToolResult(toolCallId, result, opts) {
const existing = this.toolById.get(toolCallId);
if (!existing) return;
if (opts?.partial) {
existing.setPartialResult(result);
return;
}
existing.setResult(result, { isError: opts?.isError });
}
setToolsExpanded(expanded) {
this.toolsExpanded = expanded;
for (const tool of this.toolById.values()) tool.setExpanded(expanded);
}
};
//#endregion
//#region src/tui/components/custom-editor.ts
var CustomEditor = class extends Editor {
handleInput(data) {
if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) {
this.onAltEnter();
return;
}
if (matchesKey(data, Key.ctrl("l")) && this.onCtrlL) {
this.onCtrlL();
return;
}
if (matchesKey(data, Key.ctrl("o")) && this.onCtrlO) {
this.onCtrlO();
return;
}
if (matchesKey(data, Key.ctrl("p")) && this.onCtrlP) {
this.onCtrlP();
return;
}
if (matchesKey(data, Key.ctrl("g")) && this.onCtrlG) {
this.onCtrlG();
return;
}
if (matchesKey(data, Key.ctrl("t")) && this.onCtrlT) {
this.onCtrlT();
return;
}
if (matchesKey(data, Key.shift("tab")) && this.onShiftTab) {
this.onShiftTab();
return;
}
if (matchesKey(data, Key.escape) && this.onEscape && !this.isShowingAutocomplete()) {
this.onEscape();
return;
}
if (matchesKey(data, Key.ctrl("c")) && this.onCtrlC) {
this.onCtrlC();
return;
}
if (matchesKey(data, Key.ctrl("d"))) {
if (this.getText().length === 0 && this.onCtrlD) this.onCtrlD();
return;
}
super.handleInput(data);
}
};
//#endregion
//#region src/tui/gateway-chat.ts
var GatewayChatClient = class {
constructor(opts) {
const resolved = resolveGatewayConnection(opts);
this.connection = resolved;
this.readyPromise = new Promise((resolve) => {
this.resolveReady = resolve;
});
this.client = new GatewayClient({
url: resolved.url,
token: resolved.token,
password: resolved.password,
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
clientDisplayName: "openclaw-tui",
clientVersion: VERSION,
platform: process.platform,
mode: GATEWAY_CLIENT_MODES.UI,
instanceId: randomUUID(),
minProtocol: PROTOCOL_VERSION,
maxProtocol: PROTOCOL_VERSION,
onHelloOk: (hello) => {
this.hello = hello;
this.resolveReady?.();
this.onConnected?.();
},
onEvent: (evt) => {
this.onEvent?.({
event: evt.event,
payload: evt.payload,
seq: evt.seq
});
},
onClose: (_code, reason) => {
this.onDisconnected?.(reason);
},
onGap: (info) => {
this.onGap?.(info);
}
});
}
start() {
this.client.start();
}
stop() {
this.client.stop();
}
async waitForReady() {
await this.readyPromise;
}
async sendChat(opts) {
const runId = randomUUID();
await this.client.request("chat.send", {
sessionKey: opts.sessionKey,
message: opts.message,
thinking: opts.thinking,
deliver: opts.deliver,
timeoutMs: opts.timeoutMs,
idempotencyKey: runId
});
return { runId };
}
async abortChat(opts) {
return await this.client.request("chat.abort", {
sessionKey: opts.sessionKey,
runId: opts.runId
});
}
async loadHistory(opts) {
return await this.client.request("chat.history", {
sessionKey: opts.sessionKey,
limit: opts.limit
});
}
async listSessions(opts) {
return await this.client.request("sessions.list", {
limit: opts?.limit,
activeMinutes: opts?.activeMinutes,
includeGlobal: opts?.includeGlobal,
includeUnknown: opts?.includeUnknown,
includeDerivedTitles: opts?.includeDerivedTitles,
includeLastMessage: opts?.includeLastMessage,
agentId: opts?.agentId
});
}
async listAgents() {
return await this.client.request("agents.list", {});
}
async patchSession(opts) {
return await this.client.request("sessions.patch", opts);
}
async resetSession(key) {
return await this.client.request("sessions.reset", { key });
}
async getStatus() {
return await this.client.request("status");
}
async listModels() {
const res = await this.client.request("models.list");
return Array.isArray(res?.models) ? res.models : [];
}
};
function resolveGatewayConnection(opts) {
const config = loadConfig();
const isRemoteMode = config.gateway?.mode === "remote";
const remote = isRemoteMode ? config.gateway?.remote : void 0;
const authToken = config.gateway?.auth?.token;
const localPort = resolveGatewayPort(config);
return {
url: (typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : void 0) || (typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : void 0) || `ws://127.0.0.1:${localPort}`,
token: (typeof opts.token === "string" && opts.token.trim().length > 0 ? opts.token.trim() : void 0) || (isRemoteMode ? typeof remote?.token === "string" && remote.token.trim().length > 0 ? remote.token.trim() : void 0 : process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || (typeof authToken === "string" && authToken.trim().length > 0 ? authToken.trim() : void 0)),
password: (typeof opts.password === "string" && opts.password.trim().length > 0 ? opts.password.trim() : void 0) || process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || (typeof remote?.password === "string" && remote.password.trim().length > 0 ? remote.password.trim() : void 0)
};
}
//#endregion
//#region src/utils/time-format.ts
function formatRelativeTime(timestamp) {
const diff = Date.now() - timestamp;
const seconds = Math.floor(diff / 1e3);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) return "just now";
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days === 1) return "Yesterday";
if (days < 7) return `${days}d ago`;
return new Date(timestamp).toLocaleDateString(void 0, {
month: "short",
day: "numeric"
});
}
//#endregion
//#region src/tui/components/fuzzy-filter.ts
/**
* Shared fuzzy filtering utilities for select list components.
*/
/**
* Word boundary characters for matching.
*/
const WORD_BOUNDARY_CHARS = /[\s\-_./:#@]/;
/**
* Check if position is at a word boundary.
*/
function isWordBoundary(text, index) {
return index === 0 || WORD_BOUNDARY_CHARS.test(text[index - 1] ?? "");
}
/**
* Find index where query matches at a word boundary in text.
* Returns null if no match.
*/
function findWordBoundaryIndex(text, query) {
if (!query) return null;
const textLower = text.toLowerCase();
const queryLower = query.toLowerCase();
const maxIndex = textLower.length - queryLower.length;
if (maxIndex < 0) return null;
for (let i = 0; i <= maxIndex; i++) if (textLower.startsWith(queryLower, i) && isWordBoundary(textLower, i)) return i;
return null;
}
/**
* Fuzzy match with pre-lowercased inputs (avoids toLowerCase on every keystroke).
* Returns score (lower = better) or null if no match.
*/
function fuzzyMatchLower(queryLower, textLower) {
if (queryLower.length === 0) return 0;
if (queryLower.length > textLower.length) return null;
let queryIndex = 0;
let score = 0;
let lastMatchIndex = -1;
let consecutiveMatches = 0;
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) if (textLower[i] === queryLower[queryIndex]) {
const isAtWordBoundary = isWordBoundary(textLower, i);
if (lastMatchIndex === i - 1) {
consecutiveMatches++;
score -= consecutiveMatches * 5;
} else {
consecutiveMatches = 0;
if (lastMatchIndex >= 0) score += (i - lastMatchIndex - 1) * 2;
}
if (isAtWordBoundary) score -= 10;
score += i * .1;
lastMatchIndex = i;
queryIndex++;
}
return queryIndex < queryLower.length ? null : score;
}
/**
* Filter items using pre-lowercased searchTextLower field.
* Supports space-separated tokens (all must match).
*/
function fuzzyFilterLower(items, queryLower) {
const trimmed = queryLower.trim();
if (!trimmed) return items;
const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0);
if (tokens.length === 0) return items;
const results = [];
for (const item of items) {
const text = item.searchTextLower ?? "";
let totalScore = 0;
let allMatch = true;
for (const token of tokens) {
const score = fuzzyMatchLower(token, text);
if (score !== null) totalScore += score;
else {
allMatch = false;
break;
}
}
if (allMatch) results.push({
item,
score: totalScore
});
}
results.sort((a, b) => a.score - b.score);
return results.map((r) => r.item);
}
/**
* Prepare items for fuzzy filtering by pre-computing lowercase search text.
*/
function prepareSearchItems(items) {
return items.map((item) => {
const parts = [];
if (item.label) parts.push(item.label);
if (item.description) parts.push(item.description);
if (item.searchText) parts.push(item.searchText);
return {
...item,
searchTextLower: parts.join(" ").toLowerCase()
};
});
}
//#endregion
//#region src/tui/components/filterable-select-list.ts
/**
* Combines text input filtering with a select list.
* User types to filter, arrows/j/k to navigate, Enter to select, Escape to clear/cancel.
*/
var FilterableSelectList = class {
constructor(items, maxVisible, theme) {
this.filterText = "";
this.allItems = prepareSearchItems(items);
this.maxVisible = maxVisible;
this.theme = theme;
this.input = new Input();
this.selectList = new SelectList(this.allItems, maxVisible, theme);
}
applyFilter() {
const queryLower = this.filterText.toLowerCase();
if (!queryLower.trim()) {
this.selectList = new SelectList(this.allItems, this.maxVisible, this.theme);
return;
}
this.selectList = new SelectList(fuzzyFilterLower(this.allItems, queryLower), this.maxVisible, this.theme);
}
invalidate() {
this.input.invalidate();
this.selectList.invalidate();
}
render(width) {
const lines = [];
const filterLabel = this.theme.filterLabel("Filter: ");
const inputText = this.input.render(width - 8)[0] ?? "";
lines.push(filterLabel + inputText);
lines.push(chalk.dim("─".repeat(Math.max(0, width))));
const listLines = this.selectList.render(width);
lines.push(...listLines);
return lines;
}
handleInput(keyData) {
const allowVimNav = !this.filterText.trim();
if (matchesKey(keyData, "up") || matchesKey(keyData, "ctrl+p") || allowVimNav && keyData === "k") {
this.selectList.handleInput("\x1B[A");
return;
}
if (matchesKey(keyData, "down") || matchesKey(keyData, "ctrl+n") || allowVimNav && keyData === "j") {
this.selectList.handleInput("\x1B[B");
return;
}
if (matchesKey(keyData, "enter")) {
const selected = this.selectList.getSelectedItem();
if (selected) this.onSelect?.(selected);
return;
}
if (getEditorKeybindings().matches(keyData, "selectCancel")) {
if (this.filterText) {
this.filterText = "";
this.input.setValue("");
this.applyFilter();
} else this.onCancel?.();
return;
}
const prevValue = this.input.getValue();
this.input.handleInput(keyData);
const newValue = this.input.getValue();
if (newValue !== prevValue) {
this.filterText = newValue;
this.applyFilter();
}
}
getSelectedItem() {
return this.selectList.getSelectedItem();
}
getFilterText() {
return this.filterText;
}
};
//#endregion
//#region src/tui/components/searchable-select-list.ts
/**
* A select list with a search input at the top for fuzzy filtering.
*/
var SearchableSelectList = class {
constructor(items, maxVisible, theme) {
this.selectedIndex = 0;
this.regexCache = /* @__PURE__ */ new Map();
this.compareByScore = (a, b) => {
if (a.tier !== b.tier) return a.tier - b.tier;
if (a.score !== b.score) return a.score - b.score;
return this.getItemLabel(a.item).localeCompare(this.getItemLabel(b.item));
};
this.items = items;
this.filteredItems = items;
this.maxVisible = maxVisible;
this.theme = theme;
this.searchInput = new Input();
}
getCachedRegex(pattern) {
let regex = this.regexCache.get(pattern);
if (!regex) {
regex = new RegExp(this.escapeRegex(pattern), "gi");
this.regexCache.set(pattern, regex);
}
return regex;
}
updateFilter() {
const query = this.searchInput.getValue().trim();
if (!query) this.filteredItems = this.items;
else this.filteredItems = this.smartFilter(query);
this.selectedIndex = 0;
this.notifySelectionChange();
}
/**
* Smart filtering that prioritizes:
* 1. Exact substring match in label (highest priority)
* 2. Word-boundary prefix match in label
* 3. Exact substring in description
* 4. Fuzzy match (lowest priority)
*/
smartFilter(query) {
const q = query.toLowerCase();
const scoredItems = [];
const fuzzyCandidates = [];
for (const item of this.items) {
const label = item.label.toLowerCase();
const desc = (item.description ?? "").toLowerCase();
const labelIndex = label.indexOf(q);
if (labelIndex !== -1) {
scoredItems.push({
item,
tier: 0,
score: labelIndex
});
continue;
}
const wordBoundaryIndex = findWordBoundaryIndex(label, q);
if (wordBoundaryIndex !== null) {
scoredItems.push({
item,
tier: 1,
score: wordBoundaryIndex
});
continue;
}
const descIndex = desc.indexOf(q);
if (descIndex !== -1) {
scoredItems.push({
item,
tier: 2,
score: descIndex
});
continue;
}
fuzzyCandidates.push(item);
}
scoredItems.sort(this.compareByScore);
const fuzzyMatches = fuzzyFilterLower(prepareSearchItems(fuzzyCandidates), q);
return [...scoredItems.map((s) => s.item), ...fuzzyMatches];
}
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
getItemLabel(item) {
return item.label || item.value;
}
highlightMatch(text, query) {
const tokens = query.trim().split(/\s+/).map((token) => token.toLowerCase()).filter((token) => token.length > 0);
if (tokens.length === 0) return text;
const uniqueTokens = Array.from(new Set(tokens)).toSorted((a, b) => b.length - a.length);
let result = text;
for (const token of uniqueTokens) {
const regex = this.getCachedRegex(token);
result = result.replace(regex, (match) => this.theme.matchHighlight(match));
}
return result;
}
setSelectedIndex(index) {
this.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));
}
invalidate() {
this.searchInput.invalidate();
}
render(width) {
const lines = [];
const prompt = this.theme.searchPrompt("search: ");
const inputWidth = Math.max(1, width - visibleWidth(prompt));
const inputText = this.searchInput.render(inputWidth)[0] ?? "";
lines.push(`${prompt}${this.theme.searchInput(inputText)}`);
lines.push("");
const query = this.searchInput.getValue().trim();
if (this.filteredItems.length === 0) {
lines.push(this.theme.noMatch(" No matches"));
return lines;
}
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible));
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
for (let i = startIndex; i < endIndex; i++) {
const item = this.filteredItems[i];
if (!item) continue;
const isSelected = i === this.selectedIndex;
lines.push(this.renderItemLine(item, isSelected, width, query));
}
if (this.filteredItems.length > this.maxVisible) {
const scrollInfo = `${this.selectedIndex + 1}/${this.filteredItems.length}`;
lines.push(this.theme.scrollInfo(` ${scrollInfo}`));
}
return lines;
}
renderItemLine(item, isSelected, width, query) {
const prefix = isSelected ? "→ " : " ";
const prefixWidth = prefix.length;
const displayValue = this.getItemLabel(item);
if (item.description && width > 40) {
const truncatedValue = truncateToWidth(displayValue, Math.min(30, width - prefixWidth - 4), "");
const valueText = this.highlightMatch(truncatedValue, query);
const spacingWidth = Math.max(1, 32 - visibleWidth(valueText));
const spacing = " ".repeat(spacingWidth);
const remainingWidth = width - (prefixWidth + visibleWidth(valueText) + spacing.length) - 2;
if (remainingWidth > 10) {
const truncatedDesc = truncateToWidth(item.description, remainingWidth, "");
const highlightedDesc = this.highlightMatch(truncatedDesc, query);
const line = `${prefix}${valueText}${spacing}${isSelected ? highlightedDesc : this.theme.description(highlightedDesc)}`;
return isSelected ? this.theme.selectedText(line) : line;
}
}
const truncatedValue = truncateToWidth(displayValue, width - prefixWidth - 2, "");
const line = `${prefix}${this.highlightMatch(truncatedValue, query)}`;
return isSelected ? this.theme.selectedText(line) : line;
}
handleInput(keyData) {
if (isKeyRelease(keyData)) return;
const allowVimNav = !this.searchInput.getValue().trim();
if (matchesKey(keyData, "up") || matchesKey(keyData, "ctrl+p") || allowVimNav && keyData === "k") {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
this.notifySelectionChange();
return;
}
if (matchesKey(keyData, "down") || matchesKey(keyData, "ctrl+n") || allowVimNav && keyData === "j") {
this.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1);
this.notifySelectionChange();
return;
}
if (matchesKey(keyData, "enter")) {
const item = this.filteredItems[this.selectedIndex];
if (item && this.onSelect) this.onSelect(item);
return;
}
if (getEditorKeybindings().matches(keyData, "selectCancel")) {
if (this.onCancel) this.onCancel();
return;
}
const prevValue = this.searchInput.getValue();
this.searchInput.handleInput(keyData);
if (prevValue !== this.searchInput.getValue()) this.updateFilter();
}
notifySelectionChange() {
const item = this.filteredItems[this.selectedIndex];
if (item && this.onSelectionChange) this.onSelectionChange(item);
}
getSelectedItem() {
return this.filteredItems[this.selectedIndex] ?? null;
}
};
//#endregion
//#region src/tui/components/selectors.ts
function createSearchableSelectList(items, maxVisible = 7) {
return new SearchableSelectList(items, maxVisible, searchableSelectListTheme);
}
function createFilterableSelectList(items, maxVisible = 7) {
return new FilterableSelectList(items, maxVisible, filterableSelectListTheme);
}
function createSettingsList(items, onChange, onCancel, maxVisible = 7) {
return new SettingsList(items, maxVisible, settingsListTheme, onChange, onCancel);
}
//#endregion
//#region src/tui/tui-status-summary.ts
function formatStatusSummary(summary) {
const lines = [];
lines.push("Gateway status");
if (!summary.linkChannel) lines.push("Link channel: unknown");
else {
const linkLabel = summary.linkChannel.label ?? "Link channel";
const linked = summary.linkChannel.linked === true;
const authAge = linked && typeof summary.linkChannel.authAgeMs === "number" ? ` (last refreshed ${formatAge(summary.linkChannel.authAgeMs)})` : "";
lines.push(`${linkLabel}: ${linked ? "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}`);
}
const heartbeatAgents = summary.heartbeat?.agents ?? [];
if (heartbeatAgents.length > 0) {
const heartbeatParts = heartbeatAgents.map((agent) => {
const agentId = agent.agentId ?? "unknown";
if (!agent.enabled || !agent.everyMs) return `disabled (${agentId})`;
return `${agent.every ?? "unknown"} (${agentId})`;
});
lines.push("");
lines.push(`Heartbeat: ${heartbeatParts.join(", ")}`);
}
const sessionPaths = summary.sessions?.paths ?? [];
if (sessionPaths.length === 1) lines.push(`Session store: ${sessionPaths[0]}`);
else if (sessionPaths.length > 1) lines.push(`Session stores: ${sessionPaths.length}`);
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;
}
//#endregion
//#region src/tui/tui-command-handlers.ts
function createCommandHandlers(context) {
const { client, chatLog, tui, opts, state, deliverDefault, openOverlay, closeOverlay, refreshSessionInfo, loadHistory, setSession, refreshAgents, abortActive, setActivityStatus, formatSessionKey } = context;
const setAgent = async (id) => {
state.currentAgentId = normalizeAgentId(id);
await setSession("");
};
const openModelSelector = async () => {
try {
const models = await client.listModels();
if (models.length === 0) {
chatLog.addSystem("no models available");
tui.requestRender();
return;
}
const selector = createSearchableSelectList(models.map((model) => ({
value: `${model.provider}/${model.id}`,
label: `${model.provider}/${model.id}`,
description: model.name && model.name !== model.id ? model.name : ""
})), 9);
selector.onSelect = (item) => {
(async () => {
try {
await client.patchSession({
key: state.currentSessionKey,
model: item.value
});
chatLog.addSystem(`model set to ${item.value}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`model set failed: ${String(err)}`);
}
closeOverlay();
tui.requestRender();
})();
};
selector.onCancel = () => {
closeOverlay();
tui.requestRender();
};
openOverlay(selector);
tui.requestRender();
} catch (err) {
chatLog.addSystem(`model list failed: ${String(err)}`);
tui.requestRender();
}
};
const openAgentSelector = async () => {
await refreshAgents();
if (state.agents.length === 0) {
chatLog.addSystem("no agents found");
tui.requestRender();
return;
}
const selector = createSearchableSelectList(state.agents.map((agent) => ({
value: agent.id,
label: agent.name ? `${agent.id} (${agent.name})` : agent.id,
description: agent.id === state.agentDefaultId ? "default" : ""
})), 9);
selector.onSelect = (item) => {
(async () => {
closeOverlay();
await setAgent(item.value);
tui.requestRender();
})();
};
selector.onCancel = () => {
closeOverlay();
tui.requestRender();
};
openOverlay(selector);
tui.requestRender();
};
const openSessionSelector = async () => {
try {
const selector = createFilterableSelectList((await client.listSessions({
includeGlobal: false,
includeUnknown: false,
includeDerivedTitles: true,
includeLastMessage: true,
agentId: state.currentAgentId
})).sessions.map((session) => {
const title = session.derivedTitle ?? session.displayName;
const formattedKey = formatSessionKey(session.key);
const label = title && title !== formattedKey ? `${title} (${formattedKey})` : formattedKey;
const timePart = session.updatedAt ? formatRelativeTime(session.updatedAt) : "";
const preview = session.lastMessagePreview?.replace(/\s+/g, " ").trim();
const description = timePart && preview ? `${timePart} · ${preview}` : preview ?? timePart;
return {
value: session.key,
label,
description,
searchText: [
session.displayName,
session.label,
session.subject,
session.sessionId,
session.key,
session.lastMessagePreview
].filter(Boolean).join(" ")
};
}), 9);
selector.onSelect = (item) => {
(async () => {
closeOverlay();
await setSession(item.value);
tui.requestRender();
})();
};
selector.onCancel = () => {
closeOverlay();
tui.requestRender();
};
openOverlay(selector);
tui.requestRender();
} catch (err) {
chatLog.addSystem(`sessions list failed: ${String(err)}`);
tui.requestRender();
}
};
const openSettings = () => {
openOverlay(createSettingsList([{
id: "tools",
label: "Tool output",
currentValue: state.toolsExpanded ? "expanded" : "collapsed",
values: ["collapsed", "expanded"]
}, {
id: "thinking",
label: "Show thinking",
currentValue: state.showThinking ? "on" : "off",
values: ["off", "on"]
}], (id, value) => {
if (id === "tools") {
state.toolsExpanded = value === "expanded";
chatLog.setToolsExpanded(state.toolsExpanded);
}
if (id === "thinking") {
state.showThinking = value === "on";
loadHistory();
}
tui.requestRender();
}, () => {
closeOverlay();
tui.requestRender();
}));
tui.requestRender();
};
const handleCommand = async (raw) => {
const { name, args } = parseCommand(raw);
if (!name) return;
switch (name) {
case "help":
chatLog.addSystem(helpText({
provider: state.sessionInfo.modelProvider,
model: state.sessionInfo.model
}));
break;
case "status":
try {
const status = await client.getStatus();
if (typeof status === "string") {
chatLog.addSystem(status);
break;
}
if (status && typeof status === "object") {
const lines = formatStatusSummary(status);
for (const line of lines) chatLog.addSystem(line);
break;
}
chatLog.addSystem("status: unknown response");
} catch (err) {
chatLog.addSystem(`status failed: ${String(err)}`);
}
break;
case "agent":
if (!args) await openAgentSelector();
else await setAgent(args);
break;
case "agents":
await openAgentSelector();
break;
case "session":
if (!args) await openSessionSelector();
else await setSession(args);
break;
case "sessions":
await openSessionSelector();
break;
case "model":
if (!args) await openModelSelector();
else try {
await client.patchSession({
key: state.currentSessionKey,
model: args
});
chatLog.addSystem(`model set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`model set failed: ${String(err)}`);
}
break;
case "models":
await openModelSelector();
break;
case "think":
if (!args) {
const levels = formatThinkingLevels(state.sessionInfo.modelProvider, state.sessionInfo.model, "|");
chatLog.addSystem(`usage: /think <${levels}>`);
break;
}
try {
await client.patchSession({
key: state.currentSessionKey,
thinkingLevel: args
});
chatLog.addSystem(`thinking set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`think failed: ${String(err)}`);
}
break;
case "verbose":
if (!args) {
chatLog.addSystem("usage: /verbose <on|off>");
break;
}
try {
await client.patchSession({
key: state.currentSessionKey,
verboseLevel: args
});
chatLog.addSystem(`verbose set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`verbose failed: ${String(err)}`);
}
break;
case "reasoning":
if (!args) {
chatLog.addSystem("usage: /reasoning <on|off>");
break;
}
try {
await client.patchSession({
key: state.currentSessionKey,
reasoningLevel: args
});
chatLog.addSystem(`reasoning set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`reasoning failed: ${String(err)}`);
}
break;
case "usage": {
const normalized = args ? normalizeUsageDisplay(args) : void 0;
if (args && !normalized) {
chatLog.addSystem("usage: /usage <off|tokens|full>");
break;
}
const currentRaw = state.sessionInfo.responseUsage;
const current = resolveResponseUsageMode(currentRaw);
const next = normalized ?? (current === "off" ? "tokens" : current === "tokens" ? "full" : "off");
try {
await client.patchSession({
key: state.currentSessionKey,
responseUsage: next === "off" ? null : next
});
chatLog.addSystem(`usage footer: ${next}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`usage failed: ${String(err)}`);
}
break;
}
case "elevated":
if (!args) {
chatLog.addSystem("usage: /elevated <on|off|ask|full>");
break;
}
if (![
"on",
"off",
"ask",
"full"
].includes(args)) {
chatLog.addSystem("usage: /elevated <on|off|ask|full>");
break;
}
try {
await client.patchSession({
key: state.currentSessionKey,
elevatedLevel: args
});
chatLog.addSystem(`elevated set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`elevated failed: ${String(err)}`);
}
break;
case "activation":
if (!args) {
chatLog.addSystem("usage: /activation <mention|always>");
break;
}
try {
await client.patchSession({
key: state.currentSessionKey,
groupActivation: args === "always" ? "always" : "mention"
});
chatLog.addSystem(`activation set to ${args}`);
await refreshSessionInfo();
} catch (err) {
chatLog.addSystem(`activation failed: ${String(err)}`);
}
break;
case "new":
case "reset":
try {
state.sessionInfo.inputTokens = null;
state.sessionInfo.outputTokens = null;
state.sessionInfo.totalTokens = null;
tui.requestRender();
await client.resetSession(state.currentSessionKey);
chatLog.addSystem(`session ${state.currentSessionKey} reset`);
await loadHistory();
} catch (err) {
chatLog.addSystem(`reset failed: ${String(err)}`);
}
break;
case "abort":
await abortActive();
break;
case "settings":
openSettings();
break;
case "exit":
case "quit":
client.stop();
tui.stop();
process.exit(0);
break;
default:
await sendMessage(raw);
break;
}
tui.requestRender();
};
const sendMessage = async (text) => {
try {
chatLog.addUser(text);
tui.requestRender();
setActivityStatus("sending");
const { runId } = await client.sendChat({
sessionKey: state.currentSessionKey,
message: text,
thinking: opts.thinking,
deliver: deliverDefault,
timeoutMs: opts.timeoutMs
});
state.activeChatRunId = runId;
setActivityStatus("waiting");
} catch (err) {
chatLog.addSystem(`send failed: ${String(err)}`);
setActivityStatus("error");
}
tui.requestRender();
};
return {
handleCommand,
sendMessage,
openModelSelector,
openAgentSelector,
openSessionSelector,
openSettings,
setAgent
};
}
//#endregion
//#region src/tui/tui-stream-assembler.ts
var TuiStreamAssembler = class {
constructor() {
this.runs = /* @__PURE__ */ new Map();
}
getOrCreateRun(runId) {
let state = this.runs.get(runId);
if (!state) {
state = {
thinkingText: "",
contentText: "",
displayText: ""
};
this.runs.set(runId, state);
}
return state;
}
updateRunState(state, message, showThinking) {
const thinkingText = extractThinkingFromMessage(message);
const contentText = extractContentFromMessage(message);
if (thinkingText) state.thinkingText = thinkingText;
if (contentText) state.contentText = contentText;
state.displayText = composeThinkingAndContent({
thinkingText: state.thinkingText,
contentText: state.contentText,
showThinking
});
}
ingestDelta(runId, message, showThinking) {
const state = this.getOrCreateRun(runId);
const previousDisplayText = state.displayText;
this.updateRunState(state, message, showThinking);
if (!state.displayText || state.displayText === previousDisplayText) return null;
return state.displayText;
}
finalize(runId, message, showThinking) {
const state = this.getOrCreateRun(runId);
this.updateRunState(state, message, showThinking);
const finalComposed = state.displayText;
const finalText = resolveFinalAssistantText({
finalText: finalComposed,
streamedText: state.displayText
});
this.runs.delete(runId);
return finalText;
}
drop(runId) {
this.runs.delete(runId);
}
};
//#endregion
//#region src/tui/tui-event-handlers.ts
function createEventHandlers(context) {
const { chatLog, tui, state, setActivityStatus, refreshSessionInfo } = context;
const finalizedRuns = /* @__PURE__ */ new Map();
const sessionRuns = /* @__PURE__ */ new Map();
let streamAssembler = new TuiStreamAssembler();
let lastSessionKey = state.currentSessionKey;
const pruneRunMap = (runs) => {
if (runs.size