UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,621 lines (1,603 loc) 78.1 kB
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