UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,504 lines (1,488 loc) 101 kB
import { h as visibleWidth, m as stripAnsi } from "./entry.js"; import { i as buildAgentMainSessionKey, l as normalizeAgentId, u as normalizeMainKey, x as parseAgentSessionKey } from "./session-key-BGiG_JcT.js"; import { l as resolveDefaultAgentId } from "./agent-scope-RzK9Zcks.js"; import { V as VERSION, i as loadConfig } from "./config-B2kL1ciP.js"; import { kt as PROTOCOL_VERSION, t as GatewayClient } from "./client-CDjZdZtI.js"; import { i as ensureExplicitGatewayAuth, o as resolveExplicitGatewayAuth, t as buildGatewayConnectionDetails } from "./call-DXhJGwEy.js"; import { f as GATEWAY_CLIENT_CAPS, h as GATEWAY_CLIENT_NAMES, m as GATEWAY_CLIENT_MODES } from "./message-channel-CVHJDItx.js"; import { S as stripLeadingInboundMetadata } from "./sessions-BD5dyLxb.js"; import { _ as formatRawAssistantErrorForUi } from "./pi-embedded-helpers-NVEtaJwl.js"; import { c as normalizeUsageDisplay, r as listThinkingLevelLabels, t as formatThinkingLevels, u as resolveResponseUsageMode } from "./thinking-CJPPUYWd.js"; import { a as listChatCommandsForConfig, i as listChatCommands } from "./commands-registry-CRmMPJQ9.js"; import { n as resolveToolDisplay, t as formatToolDetail } from "./tool-display-Cs1uRaRV.js"; import { n as formatTimeAgo, t as formatRelativeTimestamp } from "./format-relative-TyajjYxu.js"; import { n as formatTokenCount } from "./usage-format-8m-0w7RC.js"; import { spawn } from "node:child_process"; import chalk from "chalk"; 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), assistantText: (text) => 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 baseSelectListTheme = { 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 selectListTheme = baseSelectListTheme; const filterableSelectListTheme = { ...baseSelectListTheme, 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 = { ...baseSelectListTheme, 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.assistantText(line) }); this.addChild(new Spacer(1)); this.addChild(this.body); } setText(text) { this.body.setText(text); } }; //#endregion //#region src/tui/tui-formatters.ts const REPLACEMENT_CHAR_RE = /\uFFFD/g; const MAX_TOKEN_CHARS = 32; const LONG_TOKEN_RE = /\S{33,}/g; const LONG_TOKEN_TEST_RE = /\S{33,}/; const BINARY_LINE_REPLACEMENT_THRESHOLD = 12; const URL_PREFIX_RE = /^(https?:\/\/|file:\/\/)/i; const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/; const FILE_LIKE_RE = /^[a-zA-Z0-9._-]+$/; function hasControlChars(text) { for (const char of text) { const code = char.charCodeAt(0); if (code <= 31 && code !== 9 && code !== 10 && code !== 13 || code >= 127 && code <= 159) return true; } return false; } function stripControlChars(text) { if (!hasControlChars(text)) return text; let sanitized = ""; for (const char of text) { const code = char.charCodeAt(0); if (!(code <= 31 && code !== 9 && code !== 10 && code !== 13) && !(code >= 127 && code <= 159)) sanitized += char; } return sanitized; } function chunkToken(token, maxChars) { if (token.length <= maxChars) return [token]; const chunks = []; for (let i = 0; i < token.length; i += maxChars) chunks.push(token.slice(i, i + maxChars)); return chunks; } function isCopySensitiveToken(token) { if (URL_PREFIX_RE.test(token)) return true; if (token.startsWith("/") || token.startsWith("~/") || token.startsWith("./") || token.startsWith("../")) return true; if (WINDOWS_DRIVE_RE.test(token) || token.startsWith("\\\\")) return true; if (token.includes("/") || token.includes("\\")) return true; return token.includes("_") && FILE_LIKE_RE.test(token); } function normalizeLongTokenForDisplay(token) { if (isCopySensitiveToken(token)) return token; return chunkToken(token, MAX_TOKEN_CHARS).join(" "); } function redactBinaryLikeLine(line) { const replacementCount = (line.match(REPLACEMENT_CHAR_RE) || []).length; if (replacementCount >= BINARY_LINE_REPLACEMENT_THRESHOLD && replacementCount * 2 >= line.length) return "[binary data omitted]"; return line; } function sanitizeRenderableText(text) { if (!text) return text; const hasAnsi = text.includes("\x1B"); const hasReplacementChars = text.includes("�"); const hasLongTokens = LONG_TOKEN_TEST_RE.test(text); const hasControls = hasControlChars(text); if (!hasAnsi && !hasReplacementChars && !hasLongTokens && !hasControls) return text; const withoutAnsi = hasAnsi ? stripAnsi(text) : text; const withoutControlChars = hasControls ? stripControlChars(withoutAnsi) : withoutAnsi; const redacted = hasReplacementChars ? withoutControlChars.split("\n").map((line) => redactBinaryLikeLine(line)).join("\n") : withoutControlChars; return LONG_TOKEN_TEST_RE.test(redacted) ? redacted.replace(LONG_TOKEN_RE, normalizeLongTokenForDisplay) : redacted; } function resolveFinalAssistantText(params) { const finalText = params.finalText ?? ""; if (finalText.trim()) return finalText; const streamedText = params.streamedText ?? ""; if (streamedText.trim()) return streamedText; return "(no output)"; } function composeThinkingAndContent(params) { const thinkingText = params.thinkingText?.trim() ?? ""; const contentText = params.contentText?.trim() ?? ""; const parts = []; if (params.showThinking && thinkingText) parts.push(`[thinking]\n${thinkingText}`); if (contentText) parts.push(contentText); return parts.join("\n\n").trim(); } /** * Extract ONLY thinking blocks from message content. * Model-agnostic: returns empty string if no thinking blocks exist. */ function extractThinkingFromMessage(message) { if (!message || typeof message !== "object") return ""; const content = message.content; if (typeof content === "string") return ""; if (!Array.isArray(content)) return ""; const parts = []; for (const block of content) { if (!block || typeof block !== "object") continue; const rec = block; if (rec.type === "thinking" && typeof rec.thinking === "string") parts.push(sanitizeRenderableText(rec.thinking)); } return parts.join("\n").trim(); } /** * Extract ONLY text content blocks from message (excludes thinking). * Model-agnostic: works for any model with text content blocks. */ function extractContentFromMessage(message) { if (!message || typeof message !== "object") return ""; const record = message; const content = record.content; if (typeof content === "string") return sanitizeRenderableText(content).trim(); if (!Array.isArray(content)) { if ((typeof record.stopReason === "string" ? record.stopReason : "") === "error") return formatRawAssistantErrorForUi(typeof record.errorMessage === "string" ? record.errorMessage : ""); return ""; } const parts = []; for (const block of content) { if (!block || typeof block !== "object") continue; const rec = block; if (rec.type === "text" && typeof rec.text === "string") parts.push(sanitizeRenderableText(rec.text)); } if (parts.length === 0) { if ((typeof record.stopReason === "string" ? record.stopReason : "") === "error") return formatRawAssistantErrorForUi(typeof record.errorMessage === "string" ? record.errorMessage : ""); } return parts.join("\n").trim(); } function extractTextBlocks(content, opts) { if (typeof content === "string") return sanitizeRenderableText(content).trim(); if (!Array.isArray(content)) return ""; const thinkingParts = []; const textParts = []; for (const block of content) { if (!block || typeof block !== "object") continue; const record = block; if (record.type === "text" && typeof record.text === "string") textParts.push(sanitizeRenderableText(record.text)); if (opts?.includeThinking && record.type === "thinking" && typeof record.thinking === "string") thinkingParts.push(sanitizeRenderableText(record.thinking)); } return composeThinkingAndContent({ thinkingText: thinkingParts.join("\n").trim(), contentText: textParts.join("\n").trim(), showThinking: opts?.includeThinking ?? false }); } function extractTextFromMessage(message, opts) { if (!message || typeof message !== "object") return ""; const record = message; const text = extractTextBlocks(record.content, opts); if (text) { if (record.role === "user") return stripLeadingInboundMetadata(text); return text; } if ((typeof record.stopReason === "string" ? record.stopReason : "") !== "error") return ""; return formatRawAssistantErrorForUi(typeof record.errorMessage === "string" ? record.errorMessage : ""); } function isCommandMessage(message) { if (!message || typeof message !== "object") return false; return message.command === true; } function formatTokens(total, context) { if (total == null && context == null) return "tokens ?"; const totalLabel = total == null ? "?" : formatTokenCount(total); if (context == null) return `tokens ${totalLabel}`; const pct = typeof total === "number" && context > 0 ? Math.min(999, Math.round(total / context * 100)) : null; return `tokens ${totalLabel}/${formatTokenCount(context)}${pct !== null ? ` (${pct}%)` : ""}`; } function formatContextUsageLine(params) { const totalLabel = typeof params.total === "number" ? formatTokenCount(params.total) : "?"; const ctxLabel = typeof params.context === "number" ? formatTokenCount(params.context) : "?"; const pct = typeof params.percent === "number" ? Math.min(999, Math.round(params.percent)) : null; const extra = [typeof params.remaining === "number" ? `${formatTokenCount(params.remaining)} left` : null, pct !== null ? `${pct}%` : null].filter(Boolean).join(", "); return `tokens ${totalLabel}/${ctxLabel}${extra ? ` (${extra})` : ""}`; } function asString(value, fallback = "") { if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); return fallback; } //#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 sanitizeRenderableText(detail); if (!args || typeof args !== "object") return ""; try { return sanitizeRenderableText(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(sanitizeRenderableText(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(maxComponents = 180) { super(); this.toolById = /* @__PURE__ */ new Map(); this.streamingRuns = /* @__PURE__ */ new Map(); this.toolsExpanded = false; this.maxComponents = Math.max(20, Math.floor(maxComponents)); } dropComponentReferences(component) { for (const [toolId, tool] of this.toolById.entries()) if (tool === component) this.toolById.delete(toolId); for (const [runId, message] of this.streamingRuns.entries()) if (message === component) this.streamingRuns.delete(runId); } pruneOverflow() { while (this.children.length > this.maxComponents) { const oldest = this.children[0]; if (!oldest) return; this.removeChild(oldest); this.dropComponentReferences(oldest); } } append(component) { this.addChild(component); this.pruneOverflow(); } clearAll() { this.clear(); this.toolById.clear(); this.streamingRuns.clear(); } addSystem(text) { this.append(new Spacer(1)); this.append(new Text(theme.system(text), 1, 0)); } addUser(text) { this.append(new UserMessageComponent(text)); } resolveRunId(runId) { return runId ?? "default"; } startAssistant(text, runId) { const component = new AssistantMessageComponent(text); this.streamingRuns.set(this.resolveRunId(runId), component); this.append(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.append(new AssistantMessageComponent(text)); } dropAssistant(runId) { const effectiveRunId = this.resolveRunId(runId); const existing = this.streamingRuns.get(effectiveRunId); if (!existing) return; this.removeChild(existing); this.streamingRuns.delete(effectiveRunId); } 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.append(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, caps: [GATEWAY_CLIENT_CAPS.TOOL_EVENTS], 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 = opts.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, reason) { return await this.client.request("sessions.reset", { key, ...reason ? { reason } : {} }); } 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 urlOverride = typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : void 0; const explicitAuth = resolveExplicitGatewayAuth({ token: opts.token, password: opts.password }); ensureExplicitGatewayAuth({ urlOverride, auth: explicitAuth, errorHint: "Fix: pass --token or --password when using --url." }); return { url: buildGatewayConnectionDetails({ config, ...urlOverride ? { url: urlOverride } : {} }).url, token: explicitAuth.token || (!urlOverride ? 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) : void 0), password: explicitAuth.password || (!urlOverride ? process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() || (typeof remote?.password === "string" && remote.password.trim().length > 0 ? remote.password.trim() : void 0) : void 0) }; } //#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 const ANSI_ESCAPE = String.fromCharCode(27); const ANSI_SGR_REGEX = new RegExp(`${ANSI_ESCAPE}\\[[0-9;]*m`, "g"); /** * A select list with a search input at the top for fuzzy filtering. */ var SearchableSelectList = class SearchableSelectList { static { this.DESCRIPTION_LAYOUT_MIN_WIDTH = 40; } static { this.DESCRIPTION_MIN_WIDTH = 12; } static { this.DESCRIPTION_SPACING_WIDTH = 2; } static { this.RIGHT_MARGIN_WIDTH = 2; } 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 rawLabel = this.getItemLabel(item); const rawDesc = item.description ?? ""; const label = stripAnsi(rawLabel).toLowerCase(); const desc = stripAnsi(rawDesc).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; } const searchText = item.searchText ?? ""; fuzzyCandidates.push({ item, searchTextLower: [ rawLabel, rawDesc, searchText ].map((value) => stripAnsi(value)).filter(Boolean).join(" ").toLowerCase() }); } scoredItems.sort(this.compareByScore); const fuzzyMatches = fuzzyFilterLower(fuzzyCandidates, q); return [...scoredItems.map((s) => s.item), ...fuzzyMatches.map((entry) => entry.item)]; } escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } getItemLabel(item) { return item.label || item.value; } splitAnsiParts(text) { const parts = []; ANSI_SGR_REGEX.lastIndex = 0; let lastIndex = 0; let match; while ((match = ANSI_SGR_REGEX.exec(text)) !== null) { if (match.index > lastIndex) parts.push({ text: text.slice(lastIndex, match.index), isAnsi: false }); parts.push({ text: match[0], isAnsi: true }); lastIndex = match.index + match[0].length; } if (lastIndex < text.length) parts.push({ text: text.slice(lastIndex), isAnsi: false }); return parts; } 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 parts = this.splitAnsiParts(text); for (const token of uniqueTokens) { const regex = this.getCachedRegex(token); const nextParts = []; for (const part of parts) { if (part.isAnsi) { nextParts.push(part); continue; } regex.lastIndex = 0; const replaced = part.text.replace(regex, (match) => this.theme.matchHighlight(match)); if (replaced === part.text) { nextParts.push(part); continue; } nextParts.push(...this.splitAnsiParts(replaced)); } parts = nextParts; } return parts.map((part) => part.text).join(""); } 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); const description = item.description; if (description) { const descriptionLayout = this.getDescriptionLayout(width, prefixWidth); if (descriptionLayout) { const truncatedValue = truncateToWidth(displayValue, descriptionLayout.maxValueWidth, ""); const valueText = this.highlightMatch(truncatedValue, query); const usedByValue = visibleWidth(valueText); const descriptionWidth = descriptionLayout.availableWidth - usedByValue - descriptionLayout.spacingWidth; if (descriptionWidth >= SearchableSelectList.DESCRIPTION_MIN_WIDTH) { const spacing = " ".repeat(descriptionLayout.spacingWidth); const truncatedDesc = truncateToWidth(description, descriptionWidth, ""); 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; } getDescriptionLayout(width, prefixWidth) { if (width <= SearchableSelectList.DESCRIPTION_LAYOUT_MIN_WIDTH) return null; const availableWidth = Math.max(1, width - prefixWidth - SearchableSelectList.RIGHT_MARGIN_WIDTH); const maxValueWidth = availableWidth - SearchableSelectList.DESCRIPTION_MIN_WIDTH - SearchableSelectList.DESCRIPTION_SPACING_WIDTH; if (maxValueWidth < 1) return null; return { availableWidth, maxValueWidth, spacingWidth: SearchableSelectList.DESCRIPTION_SPACING_WIDTH }; } 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 ${formatTimeAgo(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" ? formatTimeAgo(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, applySessionInfoFromPatch, noteLocalRunId, forgetLocalRunId } = 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 { const result = await client.patchSession({ key: state.currentSessionKey, model: item.value }); chatLog.addSystem(`model set to ${item.value}`); applySessionInfoFromPatch(result); 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 =