@xopcai/xopc 0.0.28 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/agents-CkgFSiCY.js +216 -0
- package/dist/gateway/static/root/assets/agents-CkgFSiCY.js.map +1 -0
- package/dist/gateway/static/root/assets/{apps-page-Co95hLOJ.js → apps-page-Bmq19MS-.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-Co95hLOJ.js.map → apps-page-Bmq19MS-.js.map} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js +9 -0
- package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js.map +1 -0
- package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js +2 -0
- package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js.map +1 -0
- package/dist/gateway/static/root/assets/{cron-utils-BmzF4m1y.js → cron-utils-N1PqD2DB.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-utils-BmzF4m1y.js.map → cron-utils-N1PqD2DB.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-Dn-ufXyc.js → dist--p2HQ2QF.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-Dn-ufXyc.js.map → dist--p2HQ2QF.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-BZ8xQ74_.js → extension-debug-page-DwHCB_6T.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-BZ8xQ74_.js.map → extension-debug-page-DwHCB_6T.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-BlNgKxwW.js → extension-page-BsYwQIex.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-BlNgKxwW.js.map → extension-page-BsYwQIex.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-CWTdW_oY.js → extension-settings-page-nsisEgjB.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-CWTdW_oY.js.map → extension-settings-page-nsisEgjB.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-CR8zUHGR.js +4734 -0
- package/dist/gateway/static/root/assets/{index-lV8FGWlt.js.map → index-CR8zUHGR.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-Dnfha4O2.css +1 -0
- package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js +2 -0
- package/dist/gateway/static/root/assets/{logs-page-DG31RpvG.js.map → logs-page-CQwdV_Xw.js.map} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js +2 -0
- package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js.map +1 -0
- package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js +2 -0
- package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js.map +1 -0
- package/dist/gateway/static/root/assets/skills-page-Clg8deH0.js +3 -0
- package/dist/gateway/static/root/assets/{skills-page-lb7vYtlP.js.map → skills-page-Clg8deH0.js.map} +1 -1
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/src/agent/lifecycle/hook-handler.d.ts +2 -0
- package/dist/src/agent/lifecycle/hook-handler.js +24 -0
- package/dist/src/agent/lifecycle/hook-handler.js.map +1 -1
- package/dist/src/agent/messaging/command-handler.js +10 -2
- package/dist/src/agent/messaging/command-handler.js.map +1 -1
- package/dist/src/agent/service/process-direct-streaming.js +77 -20
- package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
- package/dist/src/agent/service.d.ts +15 -0
- package/dist/src/agent/service.js +21 -1
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/channels/index.js +2 -2
- package/dist/src/channels/manager.js +2 -2
- package/dist/src/cli/agent-chat-log-level-preset.d.ts +3 -2
- package/dist/src/cli/agent-chat-log-level-preset.js +6 -3
- package/dist/src/cli/agent-chat-log-level-preset.js.map +1 -1
- package/dist/src/cli/index.js +4 -3
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/config/schema.js +5 -2
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/extensions/hooks.js +5 -1
- package/dist/src/extensions/hooks.js.map +1 -1
- package/dist/src/extensions/loader.d.ts +1 -0
- package/dist/src/extensions/loader.js +3 -1
- package/dist/src/extensions/loader.js.map +1 -1
- package/dist/src/extensions/sdk/index.d.ts +1 -1
- package/dist/src/extensions/sdk/index.js.map +1 -1
- package/dist/src/extensions/types/core.d.ts +8 -0
- package/dist/src/extensions/types/hooks.d.ts +16 -1
- package/dist/src/extensions/types/hooks.js +1 -0
- package/dist/src/extensions/types/hooks.js.map +1 -1
- package/dist/src/gateway/agents-admin.d.ts +19 -1
- package/dist/src/gateway/agents-admin.js +164 -3
- package/dist/src/gateway/agents-admin.js.map +1 -1
- package/dist/src/gateway/hono/app.js +1 -0
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/routes/agents.js +59 -5
- package/dist/src/gateway/hono/routes/agents.js.map +1 -1
- package/dist/src/gateway/hono/routes/config.js +2 -2
- package/dist/src/gateway/hono/routes/config.js.map +1 -1
- package/dist/src/gateway/hono/routes/public-gateway.js +1 -0
- package/dist/src/gateway/hono/routes/public-gateway.js.map +1 -1
- package/dist/src/gateway/hono/routes/sessions.js +17 -0
- package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
- package/dist/src/gateway/service.d.ts +2 -0
- package/dist/src/gateway/service.js +31 -4
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/session/client-history.d.ts +21 -0
- package/dist/src/session/client-history.js +89 -0
- package/dist/src/session/client-history.js.map +1 -0
- package/dist/src/session/index.d.ts +1 -0
- package/dist/src/session/index.js +2 -1
- package/dist/src/session/manager.d.ts +2 -0
- package/dist/src/session/manager.js +5 -0
- package/dist/src/session/manager.js.map +1 -1
- package/dist/src/session/thinking-resolve.js +1 -1
- package/dist/src/session/thinking-resolve.js.map +1 -1
- package/dist/src/tui/backends/embedded-backend.d.ts +1 -1
- package/dist/src/tui/backends/embedded-backend.js +15 -2
- package/dist/src/tui/backends/embedded-backend.js.map +1 -1
- package/dist/src/tui/backends/gateway-sse-backend.d.ts +4 -0
- package/dist/src/tui/backends/gateway-sse-backend.js +34 -4
- package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -1
- package/dist/src/tui/chat-history.d.ts +4 -0
- package/dist/src/tui/chat-history.js +29 -0
- package/dist/src/tui/chat-history.js.map +1 -0
- package/dist/src/tui/components/chat-log.d.ts +3 -1
- package/dist/src/tui/components/chat-log.js +17 -3
- package/dist/src/tui/components/chat-log.js.map +1 -1
- package/dist/src/tui/components/custom-editor.d.ts +1 -0
- package/dist/src/tui/components/custom-editor.js +8 -2
- package/dist/src/tui/components/custom-editor.js.map +1 -1
- package/dist/src/tui/components/fuzzy-filter.d.ts +17 -0
- package/dist/src/tui/components/fuzzy-filter.js +85 -0
- package/dist/src/tui/components/fuzzy-filter.js.map +1 -0
- package/dist/src/tui/components/searchable-select-list.d.ts +39 -0
- package/dist/src/tui/components/searchable-select-list.js +257 -0
- package/dist/src/tui/components/searchable-select-list.js.map +1 -0
- package/dist/src/tui/theme.d.ts +2 -0
- package/dist/src/tui/theme.js +7 -1
- package/dist/src/tui/theme.js.map +1 -1
- package/dist/src/tui/tui-agent-events.d.ts +7 -0
- package/dist/src/tui/tui-agent-events.js +103 -0
- package/dist/src/tui/tui-agent-events.js.map +1 -0
- package/dist/src/tui/tui-backend.d.ts +8 -12
- package/dist/src/tui/tui-commands.d.ts +23 -0
- package/dist/src/tui/tui-commands.js +165 -0
- package/dist/src/tui/tui-commands.js.map +1 -0
- package/dist/src/tui/tui-lifecycle.d.ts +26 -0
- package/dist/src/tui/tui-lifecycle.js +57 -0
- package/dist/src/tui/tui-lifecycle.js.map +1 -0
- package/dist/src/tui/tui-local-shell.d.ts +28 -0
- package/dist/src/tui/tui-local-shell.js +147 -0
- package/dist/src/tui/tui-local-shell.js.map +1 -0
- package/dist/src/tui/tui-overlays.d.ts +8 -0
- package/dist/src/tui/tui-overlays.js +22 -0
- package/dist/src/tui/tui-overlays.js.map +1 -0
- package/dist/src/tui/tui-picker-overlay.d.ts +26 -0
- package/dist/src/tui/tui-picker-overlay.js +69 -0
- package/dist/src/tui/tui-picker-overlay.js.map +1 -0
- package/dist/src/tui/tui-stdio-filter.d.ts +17 -0
- package/dist/src/tui/tui-stdio-filter.js +96 -0
- package/dist/src/tui/tui-stdio-filter.js.map +1 -0
- package/dist/src/tui/tui-submit.d.ts +25 -0
- package/dist/src/tui/tui-submit.js +102 -0
- package/dist/src/tui/tui-submit.js.map +1 -0
- package/dist/src/tui/tui-suspend.d.ts +10 -0
- package/dist/src/tui/tui-suspend.js +18 -0
- package/dist/src/tui/tui-suspend.js.map +1 -0
- package/dist/src/tui/tui-types.d.ts +1 -0
- package/dist/src/tui/tui-types.js.map +1 -1
- package/dist/src/tui/tui.d.ts +2 -0
- package/dist/src/tui/tui.js +175 -312
- package/dist/src/tui/tui.js.map +1 -1
- package/package.json +2 -6
- package/dist/gateway/static/root/assets/agents-DplaQYS2.js +0 -216
- package/dist/gateway/static/root/assets/agents-DplaQYS2.js.map +0 -1
- package/dist/gateway/static/root/assets/channels-settings-CkfSST0k.js +0 -9
- package/dist/gateway/static/root/assets/channels-settings-CkfSST0k.js.map +0 -1
- package/dist/gateway/static/root/assets/cron-page-D9q6KqL8.js +0 -2
- package/dist/gateway/static/root/assets/cron-page-D9q6KqL8.js.map +0 -1
- package/dist/gateway/static/root/assets/index-OT4cGzon.css +0 -1
- package/dist/gateway/static/root/assets/index-lV8FGWlt.js +0 -4734
- package/dist/gateway/static/root/assets/logs-page-DG31RpvG.js +0 -2
- package/dist/gateway/static/root/assets/sessions-page-CdmjxDEM.js +0 -2
- package/dist/gateway/static/root/assets/sessions-page-CdmjxDEM.js.map +0 -1
- package/dist/gateway/static/root/assets/settings-page-DU2XLf5s.js +0 -2
- package/dist/gateway/static/root/assets/settings-page-DU2XLf5s.js.map +0 -1
- package/dist/gateway/static/root/assets/skills-page-lb7vYtlP.js +0 -3
|
@@ -12,7 +12,7 @@ async function resolveEffectiveThinkingLevel(sessionConfigStore, sessionKey, req
|
|
|
12
12
|
if (fromSession !== void 0) return fromSession;
|
|
13
13
|
return agentDefault ?? FALLBACK;
|
|
14
14
|
}
|
|
15
|
-
const REASONING_FALLBACK = "
|
|
15
|
+
const REASONING_FALLBACK = "stream";
|
|
16
16
|
/**
|
|
17
17
|
* Session override > agent default (`agents.defaults.reasoningDefault`).
|
|
18
18
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thinking-resolve.js","names":[],"sources":["../../../src/session/thinking-resolve.ts"],"sourcesContent":["/**\n * Resolve effective thinking level: request override > session store > agent default.\n */\n\nimport type { ThinkingLevel } from '@mariozechner/pi-agent-core';\nimport type { SessionConfigStore } from './config-store.js';\nimport { resolveThinkingLevel, resolveReasoningLevel } from './config-store.js';\nimport {\n normalizeThinkLevel,\n type ThinkLevel,\n type ReasoningLevel,\n} from '../agent/transcript/thinking-types.js';\n\nconst FALLBACK: ThinkingLevel = 'medium';\n\n/**\n * @param requestOverride - Raw value from HTTP/API (e.g. Web pill); wins over persisted session when valid.\n */\nexport async function resolveEffectiveThinkingLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n requestOverride?: string | null,\n agentDefault?: ThinkLevel,\n): Promise<ThinkingLevel> {\n const fromRequest = normalizeThinkLevel(requestOverride ?? undefined);\n if (fromRequest !== undefined) {\n return fromRequest as ThinkingLevel;\n }\n\n const fromSession = await resolveThinkingLevel(sessionConfigStore, sessionKey, agentDefault);\n if (fromSession !== undefined) {\n return fromSession as ThinkingLevel;\n }\n\n const def = agentDefault ?? FALLBACK;\n return def as ThinkingLevel;\n}\n\nconst REASONING_FALLBACK: ReasoningLevel = '
|
|
1
|
+
{"version":3,"file":"thinking-resolve.js","names":[],"sources":["../../../src/session/thinking-resolve.ts"],"sourcesContent":["/**\n * Resolve effective thinking level: request override > session store > agent default.\n */\n\nimport type { ThinkingLevel } from '@mariozechner/pi-agent-core';\nimport type { SessionConfigStore } from './config-store.js';\nimport { resolveThinkingLevel, resolveReasoningLevel } from './config-store.js';\nimport {\n normalizeThinkLevel,\n type ThinkLevel,\n type ReasoningLevel,\n} from '../agent/transcript/thinking-types.js';\n\nconst FALLBACK: ThinkingLevel = 'medium';\n\n/**\n * @param requestOverride - Raw value from HTTP/API (e.g. Web pill); wins over persisted session when valid.\n */\nexport async function resolveEffectiveThinkingLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n requestOverride?: string | null,\n agentDefault?: ThinkLevel,\n): Promise<ThinkingLevel> {\n const fromRequest = normalizeThinkLevel(requestOverride ?? undefined);\n if (fromRequest !== undefined) {\n return fromRequest as ThinkingLevel;\n }\n\n const fromSession = await resolveThinkingLevel(sessionConfigStore, sessionKey, agentDefault);\n if (fromSession !== undefined) {\n return fromSession as ThinkingLevel;\n }\n\n const def = agentDefault ?? FALLBACK;\n return def as ThinkingLevel;\n}\n\nconst REASONING_FALLBACK: ReasoningLevel = 'stream';\n\n/**\n * Session override > agent default (`agents.defaults.reasoningDefault`).\n */\nexport async function resolveEffectiveReasoningLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n agentDefault?: ReasoningLevel,\n): Promise<ReasoningLevel> {\n const def = agentDefault ?? REASONING_FALLBACK;\n const resolved = await resolveReasoningLevel(sessionConfigStore, sessionKey, def);\n return resolved ?? def;\n}\n"],"mappings":";;;AAaA,MAAM,WAA0B;;;;AAKhC,eAAsB,8BACpB,oBACA,YACA,iBACA,cACwB;CACxB,MAAM,cAAc,oBAAoB,mBAAmB,KAAA,EAAU;AACrE,KAAI,gBAAgB,KAAA,EAClB,QAAO;CAGT,MAAM,cAAc,MAAM,qBAAqB,oBAAoB,YAAY,aAAa;AAC5F,KAAI,gBAAgB,KAAA,EAClB,QAAO;AAIT,QADY,gBAAgB;;AAI9B,MAAM,qBAAqC;;;;AAK3C,eAAsB,+BACpB,oBACA,YACA,cACyB;CACzB,MAAM,MAAM,gBAAgB;AAE5B,QAAO,MADgB,sBAAsB,oBAAoB,YAAY,IAAI,IAC9D"}
|
|
@@ -6,6 +6,7 @@ import { getAllProviders, getModelsByProvider, init_providers } from "../../prov
|
|
|
6
6
|
import { MessageBus, MessageBusShutdownError } from "../../infra/bus/queue.js";
|
|
7
7
|
import "../../infra/bus/index.js";
|
|
8
8
|
import { prependEnvelopeTimestamp } from "../../channels/envelope-timestamp.js";
|
|
9
|
+
import { messagesToClientHistory } from "../../session/client-history.js";
|
|
9
10
|
import { AgentService } from "../../agent/service.js";
|
|
10
11
|
import "../../agent/index.js";
|
|
11
12
|
import "../../config/index.js";
|
|
@@ -109,8 +110,20 @@ var EmbeddedBackend = class {
|
|
|
109
110
|
}
|
|
110
111
|
return { ok: false };
|
|
111
112
|
}
|
|
112
|
-
async loadHistory(
|
|
113
|
-
return { messages: [] };
|
|
113
|
+
async loadHistory(opts) {
|
|
114
|
+
if (!this.agent) return { messages: [] };
|
|
115
|
+
try {
|
|
116
|
+
const detail = await this.agent.loadSessionDetail(opts.sessionKey);
|
|
117
|
+
if (!detail) return { messages: [] };
|
|
118
|
+
return { messages: messagesToClientHistory(detail.messages, { limit: opts.limit }) };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
121
|
+
log.warn({
|
|
122
|
+
err: error,
|
|
123
|
+
errorMessage
|
|
124
|
+
}, `Embedded loadHistory failed: ${errorMessage}`);
|
|
125
|
+
return { messages: [] };
|
|
126
|
+
}
|
|
114
127
|
}
|
|
115
128
|
async listSessions() {
|
|
116
129
|
return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embedded-backend.js","names":[],"sources":["../../../../src/tui/backends/embedded-backend.ts"],"sourcesContent":["import { AgentService } from '../../agent/index.js';\nimport { prependEnvelopeTimestamp } from '../../channels/envelope-timestamp.js';\nimport { loadConfig, getWorkspacePath } from '../../config/index.js';\nimport { MessageBus, MessageBusShutdownError } from '../../infra/bus/index.js';\nimport { getAllProviders, getModelsByProvider } from '../../providers/index.js';\nimport { createLogger } from '../../utils/logger.js';\nimport type {\n ChatSendOptions,\n HistoryMessage,\n TuiBackend,\n TuiEvent,\n TuiModelChoice,\n TuiSessionItem,\n} from '../tui-backend.js';\nimport type { SessionInfo } from '../tui-types.js';\n\nconst log = createLogger('TUI:Embedded');\n\n/**\n * TUI backend that runs the agent in-process (no gateway required).\n *\n * Wraps `AgentService` directly and emits TuiEvents by observing the\n * `MessageBus` output stream.\n */\nexport class EmbeddedBackend implements TuiBackend {\n private bus: MessageBus;\n private agent: AgentService | null = null;\n private running = false;\n private chatAbort: AbortController | null = null;\n\n onEvent?: (evt: TuiEvent) => void;\n onConnected?: () => void;\n onDisconnected?: (reason: string) => void;\n\n constructor() {\n this.bus = new MessageBus();\n }\n\n get connectionLabel(): string {\n return 'local embedded';\n }\n\n start(): void {\n if (this.running) return;\n this.running = true;\n\n const config = loadConfig();\n const workspace = getWorkspacePath(config);\n const modelConfig = config.agents?.defaults?.model;\n const modelId = typeof modelConfig === 'string' ? modelConfig : modelConfig?.primary;\n\n this.agent = new AgentService(this.bus, {\n workspace: workspace ?? process.cwd(),\n model: modelId,\n config,\n });\n\n this.agent.start().catch((err) => {\n const errorMessage = err instanceof Error ? err.message : String(err);\n log.error({ err, errorMessage }, `Embedded agent failed: ${errorMessage}`);\n this.onDisconnected?.(errorMessage);\n });\n\n // Process outbound messages in background\n this.processOutbound();\n\n // Signal ready\n queueMicrotask(() => this.onConnected?.());\n }\n\n stop(): void {\n this.running = false;\n this.bus.shutdown();\n void this.agent?.stop();\n this.agent = null;\n }\n\n async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {\n if (!this.agent) throw new Error('Agent not started');\n\n const runId = crypto.randomUUID();\n this.chatAbort?.abort();\n this.chatAbort = new AbortController();\n const signal = this.chatAbort.signal;\n\n this.onEvent?.({ event: 'status', data: { status: 'started', runId } });\n\n // Run the stream in background so the TUI event loop stays responsive.\n void (async () => {\n try {\n // Prepend envelope timestamp so the model knows the current date/time,\n // matching the behavior of channel pipelines (Telegram, Weixin, etc.).\n // Skip for slash commands — parseSlashCommand requires lines starting with '/'.\n const messageForAgent = opts.message.trimStart().startsWith('/')\n ? opts.message\n : prependEnvelopeTimestamp(opts.message);\n\n const stream = this.agent!.processDirectStreaming(\n messageForAgent,\n opts.sessionKey,\n undefined,\n opts.thinking,\n { signal },\n );\n\n for await (const event of stream) {\n if (signal.aborted) break;\n this.onEvent?.({ event: event.type, data: event });\n }\n\n if (!signal.aborted) {\n this.onEvent?.({\n event: 'result',\n data: { ok: true },\n });\n }\n } catch (error) {\n if (signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n this.onEvent?.({ event: 'error', data: { content: errorMessage } });\n }\n })();\n\n return { runId };\n }\n\n async abortChat(_opts: { sessionKey: string; runId: string }): Promise<{ ok: boolean }> {\n if (this.chatAbort) {\n this.chatAbort.abort();\n this.chatAbort = null;\n return { ok: true };\n }\n return { ok: false };\n }\n\n async loadHistory(
|
|
1
|
+
{"version":3,"file":"embedded-backend.js","names":[],"sources":["../../../../src/tui/backends/embedded-backend.ts"],"sourcesContent":["import { AgentService } from '../../agent/index.js';\nimport { messagesToClientHistory } from '../../session/client-history.js';\nimport { prependEnvelopeTimestamp } from '../../channels/envelope-timestamp.js';\nimport { loadConfig, getWorkspacePath } from '../../config/index.js';\nimport { MessageBus, MessageBusShutdownError } from '../../infra/bus/index.js';\nimport { getAllProviders, getModelsByProvider } from '../../providers/index.js';\nimport { createLogger } from '../../utils/logger.js';\nimport type {\n ChatSendOptions,\n HistoryMessage,\n TuiBackend,\n TuiEvent,\n TuiModelChoice,\n TuiSessionItem,\n} from '../tui-backend.js';\nimport type { SessionInfo } from '../tui-types.js';\n\nconst log = createLogger('TUI:Embedded');\n\n/**\n * TUI backend that runs the agent in-process (no gateway required).\n *\n * Wraps `AgentService` directly and emits TuiEvents by observing the\n * `MessageBus` output stream.\n */\nexport class EmbeddedBackend implements TuiBackend {\n private bus: MessageBus;\n private agent: AgentService | null = null;\n private running = false;\n private chatAbort: AbortController | null = null;\n\n onEvent?: (evt: TuiEvent) => void;\n onConnected?: () => void;\n onDisconnected?: (reason: string) => void;\n\n constructor() {\n this.bus = new MessageBus();\n }\n\n get connectionLabel(): string {\n return 'local embedded';\n }\n\n start(): void {\n if (this.running) return;\n this.running = true;\n\n const config = loadConfig();\n const workspace = getWorkspacePath(config);\n const modelConfig = config.agents?.defaults?.model;\n const modelId = typeof modelConfig === 'string' ? modelConfig : modelConfig?.primary;\n\n this.agent = new AgentService(this.bus, {\n workspace: workspace ?? process.cwd(),\n model: modelId,\n config,\n });\n\n this.agent.start().catch((err) => {\n const errorMessage = err instanceof Error ? err.message : String(err);\n log.error({ err, errorMessage }, `Embedded agent failed: ${errorMessage}`);\n this.onDisconnected?.(errorMessage);\n });\n\n // Process outbound messages in background\n this.processOutbound();\n\n // Signal ready\n queueMicrotask(() => this.onConnected?.());\n }\n\n stop(): void {\n this.running = false;\n this.bus.shutdown();\n void this.agent?.stop();\n this.agent = null;\n }\n\n async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {\n if (!this.agent) throw new Error('Agent not started');\n\n const runId = crypto.randomUUID();\n this.chatAbort?.abort();\n this.chatAbort = new AbortController();\n const signal = this.chatAbort.signal;\n\n this.onEvent?.({ event: 'status', data: { status: 'started', runId } });\n\n // Run the stream in background so the TUI event loop stays responsive.\n void (async () => {\n try {\n // Prepend envelope timestamp so the model knows the current date/time,\n // matching the behavior of channel pipelines (Telegram, Weixin, etc.).\n // Skip for slash commands — parseSlashCommand requires lines starting with '/'.\n const messageForAgent = opts.message.trimStart().startsWith('/')\n ? opts.message\n : prependEnvelopeTimestamp(opts.message);\n\n const stream = this.agent!.processDirectStreaming(\n messageForAgent,\n opts.sessionKey,\n undefined,\n opts.thinking,\n { signal },\n );\n\n for await (const event of stream) {\n if (signal.aborted) break;\n this.onEvent?.({ event: event.type, data: event });\n }\n\n if (!signal.aborted) {\n this.onEvent?.({\n event: 'result',\n data: { ok: true },\n });\n }\n } catch (error) {\n if (signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n this.onEvent?.({ event: 'error', data: { content: errorMessage } });\n }\n })();\n\n return { runId };\n }\n\n async abortChat(_opts: { sessionKey: string; runId: string }): Promise<{ ok: boolean }> {\n if (this.chatAbort) {\n this.chatAbort.abort();\n this.chatAbort = null;\n return { ok: true };\n }\n return { ok: false };\n }\n\n async loadHistory(opts: {\n sessionKey: string;\n limit?: number;\n }): Promise<{ messages: HistoryMessage[] }> {\n if (!this.agent) {\n return { messages: [] };\n }\n try {\n const detail = await this.agent.loadSessionDetail(opts.sessionKey);\n if (!detail) {\n return { messages: [] };\n }\n return {\n messages: messagesToClientHistory(detail.messages, { limit: opts.limit }),\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Embedded loadHistory failed: ${errorMessage}`);\n return { messages: [] };\n }\n }\n\n async listSessions(): Promise<TuiSessionItem[]> {\n return [];\n }\n\n async getSessionInfo(_sessionKey: string): Promise<SessionInfo> {\n const config = loadConfig();\n const modelConfig = config.agents?.defaults?.model;\n const model = typeof modelConfig === 'string' ? modelConfig : modelConfig?.primary;\n return { model: model ?? undefined };\n }\n\n async listModels(): Promise<TuiModelChoice[]> {\n const choices: TuiModelChoice[] = [];\n for (const provider of getAllProviders()) {\n for (const model of getModelsByProvider(provider)) {\n choices.push({\n id: model.id,\n name: model.name ?? model.id,\n provider,\n });\n }\n }\n return choices;\n }\n\n async resetSession(_sessionKey: string): Promise<void> {\n // Restart agent for a clean session\n this.stop();\n this.bus = new MessageBus();\n this.start();\n }\n\n async patchSession(\n _sessionKey: string,\n _patch: Record<string, unknown>,\n ): Promise<void> {\n // Not supported in embedded mode\n }\n\n private processOutbound(): void {\n void (async () => {\n while (this.running) {\n try {\n const msg = await this.bus.consumeOutbound();\n log.debug({ channel: msg.channel, chatId: msg.chat_id }, 'Outbound message');\n } catch (error) {\n if (error instanceof MessageBusShutdownError) break;\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Outbound processor failed: ${errorMessage}`);\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n }\n })();\n }\n}\n"],"mappings":"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"}
|
|
@@ -19,6 +19,10 @@ export declare class GatewaySseBackend implements TuiBackend {
|
|
|
19
19
|
onEvent?: (evt: TuiEvent) => void;
|
|
20
20
|
onConnected?: () => void;
|
|
21
21
|
onDisconnected?: (reason: string) => void;
|
|
22
|
+
onGap?: (info: {
|
|
23
|
+
expected: number;
|
|
24
|
+
received: number;
|
|
25
|
+
}) => void;
|
|
22
26
|
constructor(opts: GatewaySSEOptions);
|
|
23
27
|
get connectionLabel(): string;
|
|
24
28
|
start(): void;
|
|
@@ -32,6 +32,7 @@ var GatewaySseBackend = class {
|
|
|
32
32
|
onEvent;
|
|
33
33
|
onConnected;
|
|
34
34
|
onDisconnected;
|
|
35
|
+
onGap;
|
|
35
36
|
constructor(opts) {
|
|
36
37
|
this.baseUrl = opts.url.replace(/\/+$/, "");
|
|
37
38
|
this.token = opts.token;
|
|
@@ -53,6 +54,13 @@ var GatewaySseBackend = class {
|
|
|
53
54
|
this.chatAbort = new AbortController();
|
|
54
55
|
const signal = this.chatAbort.signal;
|
|
55
56
|
const runId = crypto.randomUUID();
|
|
57
|
+
this.onEvent?.({
|
|
58
|
+
event: "status",
|
|
59
|
+
data: {
|
|
60
|
+
status: "started",
|
|
61
|
+
runId
|
|
62
|
+
}
|
|
63
|
+
});
|
|
56
64
|
(async () => {
|
|
57
65
|
try {
|
|
58
66
|
const res = await gatewayFetch(this.baseUrl, "/api/agent", this.token, {
|
|
@@ -121,9 +129,10 @@ var GatewaySseBackend = class {
|
|
|
121
129
|
}
|
|
122
130
|
async loadHistory(opts) {
|
|
123
131
|
try {
|
|
124
|
-
const params = new URLSearchParams(
|
|
132
|
+
const params = new URLSearchParams();
|
|
125
133
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
126
|
-
const
|
|
134
|
+
const qs = params.toString();
|
|
135
|
+
const res = await gatewayFetch(this.baseUrl, `/api/sessions/${encodeURIComponent(opts.sessionKey)}/messages${qs ? `?${qs}` : ""}`, this.token);
|
|
127
136
|
if (!res.ok) return { messages: [] };
|
|
128
137
|
return { messages: (await res.json()).payload?.messages ?? [] };
|
|
129
138
|
} catch (error) {
|
|
@@ -139,7 +148,13 @@ var GatewaySseBackend = class {
|
|
|
139
148
|
try {
|
|
140
149
|
const res = await gatewayFetch(this.baseUrl, "/api/sessions", this.token);
|
|
141
150
|
if (!res.ok) return [];
|
|
142
|
-
return (await res.json()).
|
|
151
|
+
return ((await res.json()).items ?? []).map((s) => ({
|
|
152
|
+
key: s.key,
|
|
153
|
+
displayName: s.name,
|
|
154
|
+
updatedAt: s.updatedAt ? Date.parse(s.updatedAt) : void 0,
|
|
155
|
+
totalTokens: s.estimatedTokens ?? null,
|
|
156
|
+
model: typeof s.customData?.model === "string" ? s.customData.model : typeof s.customData?.modelRef === "string" ? s.customData.modelRef : null
|
|
157
|
+
}));
|
|
143
158
|
} catch {
|
|
144
159
|
return [];
|
|
145
160
|
}
|
|
@@ -148,7 +163,14 @@ var GatewaySseBackend = class {
|
|
|
148
163
|
try {
|
|
149
164
|
const res = await gatewayFetch(this.baseUrl, `/api/sessions/${encodeURIComponent(sessionKey)}`, this.token);
|
|
150
165
|
if (!res.ok) return {};
|
|
151
|
-
|
|
166
|
+
const s = (await res.json()).session;
|
|
167
|
+
if (!s) return {};
|
|
168
|
+
return {
|
|
169
|
+
displayName: s.name,
|
|
170
|
+
totalTokens: s.estimatedTokens ?? void 0,
|
|
171
|
+
model: typeof s.customData?.model === "string" ? s.customData.model : typeof s.customData?.modelRef === "string" ? s.customData.modelRef : void 0,
|
|
172
|
+
modelProvider: typeof s.customData?.modelProvider === "string" ? s.customData.modelProvider : void 0
|
|
173
|
+
};
|
|
152
174
|
} catch {
|
|
153
175
|
return {};
|
|
154
176
|
}
|
|
@@ -190,6 +212,14 @@ var GatewaySseBackend = class {
|
|
|
190
212
|
this.onConnected?.();
|
|
191
213
|
await consumeSSEStream(res.body, (sseEvent) => {
|
|
192
214
|
if (sseEvent.event === "connected") return;
|
|
215
|
+
if (sseEvent.event === "gap") {
|
|
216
|
+
const gapData = parseSSEData(sseEvent.data);
|
|
217
|
+
if (gapData && typeof gapData.expected === "number" && typeof gapData.received === "number") this.onGap?.({
|
|
218
|
+
expected: gapData.expected,
|
|
219
|
+
received: gapData.received
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
193
223
|
const data = parseSSEData(sseEvent.data);
|
|
194
224
|
if (data !== null) this.onEvent?.({
|
|
195
225
|
event: sseEvent.event,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-sse-backend.js","names":[],"sources":["../../../../src/tui/backends/gateway-sse-backend.ts"],"sourcesContent":["import { prependEnvelopeTimestamp } from '../../channels/envelope-timestamp.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { consumeSSEStream, parseSSEData } from '../sse-consumer.js';\nimport type {\n ChatSendOptions,\n HistoryMessage,\n TuiBackend,\n TuiEvent,\n TuiModelChoice,\n TuiSessionItem,\n} from '../tui-backend.js';\nimport type { SessionInfo } from '../tui-types.js';\n\nconst log = createLogger('TUI:GatewaySSE');\n\ninterface GatewaySSEOptions {\n url: string;\n token?: string;\n}\n\n/** Fetch wrapper that adds auth headers. */\nasync function gatewayFetch(\n baseUrl: string,\n path: string,\n token: string | undefined,\n init?: RequestInit,\n): Promise<Response> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n ...(init?.headers as Record<string, string> | undefined),\n };\n return fetch(`${baseUrl}${path}`, { ...init, headers });\n}\n\n/**\n * TUI backend that communicates with a running xopc gateway via HTTP + SSE.\n *\n * - Agent streaming: `POST /api/agent` with `Accept: text/event-stream`\n * - Broadcast events: `GET /api/events` via long-lived SSE\n * - REST calls for sessions, models, etc.\n */\nexport class GatewaySseBackend implements TuiBackend {\n private readonly baseUrl: string;\n private readonly token: string | undefined;\n private eventAbort: AbortController | null = null;\n private chatAbort: AbortController | null = null;\n\n onEvent?: (evt: TuiEvent) => void;\n onConnected?: () => void;\n onDisconnected?: (reason: string) => void;\n\n constructor(opts: GatewaySSEOptions) {\n this.baseUrl = opts.url.replace(/\\/+$/, '');\n this.token = opts.token;\n }\n\n get connectionLabel(): string {\n return this.baseUrl;\n }\n\n start(): void {\n this.startEventStream();\n }\n\n stop(): void {\n this.eventAbort?.abort();\n this.eventAbort = null;\n this.chatAbort?.abort();\n this.chatAbort = null;\n }\n\n // ── Agent chat (POST /api/agent → SSE response body) ──\n\n async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {\n this.chatAbort?.abort();\n this.chatAbort = new AbortController();\n const signal = this.chatAbort.signal;\n const runId = crypto.randomUUID();\n\n // Fire-and-forget: run the HTTP request + SSE consumption in background\n // so the TUI event loop stays responsive for keyboard input.\n void (async () => {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/agent', this.token, {\n method: 'POST',\n headers: { Accept: 'text/event-stream' },\n body: JSON.stringify({\n // Prepend envelope timestamp for regular messages so the model knows\n // the current date/time. Skip for slash commands — parseSlashCommand\n // requires lines starting with '/'.\n message: opts.message.trimStart().startsWith('/')\n ? opts.message\n : prependEnvelopeTimestamp(opts.message),\n channel: 'webchat',\n sessionKey: opts.sessionKey,\n thinking: opts.thinking,\n }),\n signal,\n });\n\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as { error?: { message?: string } };\n this.onEvent?.({\n event: 'error',\n data: { content: body.error?.message ?? `Gateway error: ${res.status}` },\n });\n return;\n }\n\n const contentType = res.headers.get('Content-Type') ?? '';\n\n if (contentType.includes('text/event-stream') && res.body) {\n await consumeSSEStream(\n res.body,\n (sseEvent) => {\n if (signal.aborted) return;\n const data = parseSSEData<Record<string, unknown>>(sseEvent.data);\n if (!data) return;\n this.onEvent?.({ event: sseEvent.event, data });\n },\n signal,\n );\n } else {\n const json = (await res.json()) as { ok?: boolean; payload?: { content?: string } };\n if (json.ok && json.payload?.content) {\n this.onEvent?.({\n event: 'token',\n data: { content: json.payload.content },\n });\n this.onEvent?.({ event: 'result', data: { ok: true } });\n }\n }\n } catch (error) {\n if (signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n this.onEvent?.({ event: 'error', data: { content: errorMessage } });\n }\n })();\n\n return { runId };\n }\n\n async abortChat(opts: { sessionKey: string; runId: string }): Promise<{ ok: boolean }> {\n this.chatAbort?.abort();\n this.chatAbort = null;\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/agent/abort', this.token, {\n method: 'POST',\n body: JSON.stringify({ runId: opts.runId }),\n });\n const json = (await res.json()) as { ok?: boolean };\n return { ok: json.ok ?? false };\n } catch {\n return { ok: false };\n }\n }\n\n // ── REST helpers ──\n\n async loadHistory(opts: {\n sessionKey: string;\n limit?: number;\n }): Promise<{ messages: HistoryMessage[] }> {\n try {\n const params = new URLSearchParams({ key: opts.sessionKey });\n if (opts.limit) params.set('limit', String(opts.limit));\n const res = await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(opts.sessionKey)}/messages?${params}`,\n this.token,\n );\n if (!res.ok) return { messages: [] };\n const json = (await res.json()) as { ok?: boolean; payload?: { messages?: HistoryMessage[] } };\n return { messages: json.payload?.messages ?? [] };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Failed to load history: ${errorMessage}`);\n return { messages: [] };\n }\n }\n\n async listSessions(): Promise<TuiSessionItem[]> {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/sessions', this.token);\n if (!res.ok) return [];\n const json = (await res.json()) as {\n ok?: boolean;\n payload?: { sessions?: TuiSessionItem[] };\n };\n return json.payload?.sessions ?? [];\n } catch {\n return [];\n }\n }\n\n async getSessionInfo(sessionKey: string): Promise<SessionInfo> {\n try {\n const res = await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n );\n if (!res.ok) return {};\n const json = (await res.json()) as { ok?: boolean; payload?: SessionInfo };\n return json.payload ?? {};\n } catch {\n return {};\n }\n }\n\n async listModels(): Promise<TuiModelChoice[]> {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/models', this.token);\n if (!res.ok) return [];\n const json = (await res.json()) as {\n ok?: boolean;\n payload?: { models?: TuiModelChoice[] };\n };\n return json.payload?.models ?? [];\n } catch {\n return [];\n }\n }\n\n async resetSession(sessionKey: string): Promise<void> {\n await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n { method: 'DELETE' },\n ).catch(() => {});\n }\n\n async patchSession(sessionKey: string, patch: Record<string, unknown>): Promise<void> {\n await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n { method: 'PATCH', body: JSON.stringify(patch) },\n ).catch(() => {});\n }\n\n // ── Broadcast SSE (GET /api/events) ──\n\n private startEventStream(): void {\n this.eventAbort?.abort();\n this.eventAbort = new AbortController();\n\n const url = new URL(`${this.baseUrl}/api/events`);\n if (this.token) url.searchParams.set('token', this.token);\n\n const connect = async () => {\n try {\n const res = await fetch(url.toString(), {\n signal: this.eventAbort!.signal,\n headers: { Accept: 'text/event-stream' },\n });\n\n if (!res.ok || !res.body) {\n this.onDisconnected?.(`event stream error: ${res.status}`);\n this.scheduleReconnect();\n return;\n }\n\n this.onConnected?.();\n\n await consumeSSEStream(\n res.body,\n (sseEvent) => {\n if (sseEvent.event === 'connected') return;\n const data = parseSSEData(sseEvent.data);\n if (data !== null) {\n this.onEvent?.({ event: sseEvent.event, data });\n }\n },\n this.eventAbort!.signal,\n );\n\n // Stream ended normally\n if (!this.eventAbort?.signal.aborted) {\n this.onDisconnected?.('stream closed');\n this.scheduleReconnect();\n }\n } catch (error) {\n if (this.eventAbort?.signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Event stream failed: ${errorMessage}`);\n this.onDisconnected?.(errorMessage);\n this.scheduleReconnect();\n }\n };\n\n void connect();\n }\n\n private scheduleReconnect(): void {\n if (this.eventAbort?.signal.aborted) return;\n setTimeout(() => {\n if (!this.eventAbort?.signal.aborted) {\n this.startEventStream();\n }\n }, 3000);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gateway-sse-backend.js","names":[],"sources":["../../../../src/tui/backends/gateway-sse-backend.ts"],"sourcesContent":["import { prependEnvelopeTimestamp } from '../../channels/envelope-timestamp.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { consumeSSEStream, parseSSEData } from '../sse-consumer.js';\nimport type {\n ChatSendOptions,\n HistoryMessage,\n TuiBackend,\n TuiEvent,\n TuiModelChoice,\n TuiSessionItem,\n} from '../tui-backend.js';\nimport type { SessionInfo } from '../tui-types.js';\n\nconst log = createLogger('TUI:GatewaySSE');\n\ninterface GatewaySSEOptions {\n url: string;\n token?: string;\n}\n\n/** Fetch wrapper that adds auth headers. */\nasync function gatewayFetch(\n baseUrl: string,\n path: string,\n token: string | undefined,\n init?: RequestInit,\n): Promise<Response> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n ...(init?.headers as Record<string, string> | undefined),\n };\n return fetch(`${baseUrl}${path}`, { ...init, headers });\n}\n\n/**\n * TUI backend that communicates with a running xopc gateway via HTTP + SSE.\n *\n * - Agent streaming: `POST /api/agent` with `Accept: text/event-stream`\n * - Broadcast events: `GET /api/events` via long-lived SSE\n * - REST calls for sessions, models, etc.\n */\nexport class GatewaySseBackend implements TuiBackend {\n private readonly baseUrl: string;\n private readonly token: string | undefined;\n private eventAbort: AbortController | null = null;\n private chatAbort: AbortController | null = null;\n\n onEvent?: (evt: TuiEvent) => void;\n onConnected?: () => void;\n onDisconnected?: (reason: string) => void;\n onGap?: (info: { expected: number; received: number }) => void;\n\n constructor(opts: GatewaySSEOptions) {\n this.baseUrl = opts.url.replace(/\\/+$/, '');\n this.token = opts.token;\n }\n\n get connectionLabel(): string {\n return this.baseUrl;\n }\n\n start(): void {\n this.startEventStream();\n }\n\n stop(): void {\n this.eventAbort?.abort();\n this.eventAbort = null;\n this.chatAbort?.abort();\n this.chatAbort = null;\n }\n\n // ── Agent chat (POST /api/agent → SSE response body) ──\n\n async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {\n this.chatAbort?.abort();\n this.chatAbort = new AbortController();\n const signal = this.chatAbort.signal;\n const runId = crypto.randomUUID();\n\n // Match EmbeddedBackend: set activeRunId before any token/tool events so TUI state stays on one\n // runId (avoids assistant under \"default\" and tools under the real uuid).\n this.onEvent?.({ event: 'status', data: { status: 'started', runId } });\n\n // Fire-and-forget: run the HTTP request + SSE consumption in background\n // so the TUI event loop stays responsive for keyboard input.\n void (async () => {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/agent', this.token, {\n method: 'POST',\n headers: { Accept: 'text/event-stream' },\n body: JSON.stringify({\n // Prepend envelope timestamp for regular messages so the model knows\n // the current date/time. Skip for slash commands — parseSlashCommand\n // requires lines starting with '/'.\n message: opts.message.trimStart().startsWith('/')\n ? opts.message\n : prependEnvelopeTimestamp(opts.message),\n channel: 'webchat',\n sessionKey: opts.sessionKey,\n thinking: opts.thinking,\n }),\n signal,\n });\n\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as { error?: { message?: string } };\n this.onEvent?.({\n event: 'error',\n data: { content: body.error?.message ?? `Gateway error: ${res.status}` },\n });\n return;\n }\n\n const contentType = res.headers.get('Content-Type') ?? '';\n\n if (contentType.includes('text/event-stream') && res.body) {\n await consumeSSEStream(\n res.body,\n (sseEvent) => {\n if (signal.aborted) return;\n const data = parseSSEData<Record<string, unknown>>(sseEvent.data);\n if (!data) return;\n this.onEvent?.({ event: sseEvent.event, data });\n },\n signal,\n );\n } else {\n const json = (await res.json()) as { ok?: boolean; payload?: { content?: string } };\n if (json.ok && json.payload?.content) {\n this.onEvent?.({\n event: 'token',\n data: { content: json.payload.content },\n });\n this.onEvent?.({ event: 'result', data: { ok: true } });\n }\n }\n } catch (error) {\n if (signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n this.onEvent?.({ event: 'error', data: { content: errorMessage } });\n }\n })();\n\n return { runId };\n }\n\n async abortChat(opts: { sessionKey: string; runId: string }): Promise<{ ok: boolean }> {\n this.chatAbort?.abort();\n this.chatAbort = null;\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/agent/abort', this.token, {\n method: 'POST',\n body: JSON.stringify({ runId: opts.runId }),\n });\n const json = (await res.json()) as { ok?: boolean };\n return { ok: json.ok ?? false };\n } catch {\n return { ok: false };\n }\n }\n\n // ── REST helpers ──\n\n async loadHistory(opts: {\n sessionKey: string;\n limit?: number;\n }): Promise<{ messages: HistoryMessage[] }> {\n try {\n const params = new URLSearchParams();\n if (opts.limit) params.set('limit', String(opts.limit));\n const qs = params.toString();\n const res = await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(opts.sessionKey)}/messages${qs ? `?${qs}` : ''}`,\n this.token,\n );\n if (!res.ok) return { messages: [] };\n const json = (await res.json()) as { ok?: boolean; payload?: { messages?: HistoryMessage[] } };\n return { messages: json.payload?.messages ?? [] };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Failed to load history: ${errorMessage}`);\n return { messages: [] };\n }\n }\n\n async listSessions(): Promise<TuiSessionItem[]> {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/sessions', this.token);\n if (!res.ok) return [];\n const json = (await res.json()) as {\n items?: Array<{\n key: string;\n name?: string;\n updatedAt?: string;\n estimatedTokens?: number;\n customData?: Record<string, unknown>;\n }>;\n };\n return (json.items ?? []).map((s) => ({\n key: s.key,\n displayName: s.name,\n updatedAt: s.updatedAt ? Date.parse(s.updatedAt) : undefined,\n totalTokens: s.estimatedTokens ?? null,\n model:\n typeof s.customData?.model === 'string'\n ? s.customData.model\n : typeof s.customData?.modelRef === 'string'\n ? s.customData.modelRef\n : null,\n }));\n } catch {\n return [];\n }\n }\n\n async getSessionInfo(sessionKey: string): Promise<SessionInfo> {\n try {\n const res = await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n );\n if (!res.ok) return {};\n const json = (await res.json()) as {\n session?: {\n name?: string;\n estimatedTokens?: number;\n customData?: Record<string, unknown>;\n };\n };\n const s = json.session;\n if (!s) return {};\n return {\n displayName: s.name,\n totalTokens: s.estimatedTokens ?? undefined,\n model:\n typeof s.customData?.model === 'string'\n ? s.customData.model\n : typeof s.customData?.modelRef === 'string'\n ? s.customData.modelRef\n : undefined,\n modelProvider:\n typeof s.customData?.modelProvider === 'string' ? s.customData.modelProvider : undefined,\n };\n } catch {\n return {};\n }\n }\n\n async listModels(): Promise<TuiModelChoice[]> {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/models', this.token);\n if (!res.ok) return [];\n const json = (await res.json()) as {\n ok?: boolean;\n payload?: { models?: TuiModelChoice[] };\n };\n return json.payload?.models ?? [];\n } catch {\n return [];\n }\n }\n\n async resetSession(sessionKey: string): Promise<void> {\n await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n { method: 'DELETE' },\n ).catch(() => {});\n }\n\n async patchSession(sessionKey: string, patch: Record<string, unknown>): Promise<void> {\n await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n { method: 'PATCH', body: JSON.stringify(patch) },\n ).catch(() => {});\n }\n\n // ── Broadcast SSE (GET /api/events) ──\n\n private startEventStream(): void {\n this.eventAbort?.abort();\n this.eventAbort = new AbortController();\n\n const url = new URL(`${this.baseUrl}/api/events`);\n if (this.token) url.searchParams.set('token', this.token);\n\n const connect = async () => {\n try {\n const res = await fetch(url.toString(), {\n signal: this.eventAbort!.signal,\n headers: { Accept: 'text/event-stream' },\n });\n\n if (!res.ok || !res.body) {\n this.onDisconnected?.(`event stream error: ${res.status}`);\n this.scheduleReconnect();\n return;\n }\n\n this.onConnected?.();\n\n await consumeSSEStream(\n res.body,\n (sseEvent) => {\n if (sseEvent.event === 'connected') return;\n if (sseEvent.event === 'gap') {\n const gapData = parseSSEData(sseEvent.data) as {\n expected?: unknown;\n received?: unknown;\n } | null;\n if (\n gapData &&\n typeof gapData.expected === 'number' &&\n typeof gapData.received === 'number'\n ) {\n this.onGap?.({ expected: gapData.expected, received: gapData.received });\n }\n return;\n }\n const data = parseSSEData(sseEvent.data);\n if (data !== null) {\n this.onEvent?.({ event: sseEvent.event, data });\n }\n },\n this.eventAbort!.signal,\n );\n\n // Stream ended normally\n if (!this.eventAbort?.signal.aborted) {\n this.onDisconnected?.('stream closed');\n this.scheduleReconnect();\n }\n } catch (error) {\n if (this.eventAbort?.signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Event stream failed: ${errorMessage}`);\n this.onDisconnected?.(errorMessage);\n this.scheduleReconnect();\n }\n };\n\n void connect();\n }\n\n private scheduleReconnect(): void {\n if (this.eventAbort?.signal.aborted) return;\n setTimeout(() => {\n if (!this.eventAbort?.signal.aborted) {\n this.startEventStream();\n }\n }, 3000);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HistoryMessage } from './tui-backend.js';
|
|
2
|
+
import { ChatLog } from './components/chat-log.js';
|
|
3
|
+
/** Replay persisted transcript into the scroll log (synthetic run ids per assistant row). */
|
|
4
|
+
export declare function appendHistoryToChatLog(chatLog: ChatLog, messages: HistoryMessage[], toolsExpanded: boolean): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/tui/chat-history.ts
|
|
2
|
+
/** Replay persisted transcript into the scroll log (synthetic run ids per assistant row). */
|
|
3
|
+
function appendHistoryToChatLog(chatLog, messages, toolsExpanded) {
|
|
4
|
+
chatLog.setToolsExpanded(toolsExpanded);
|
|
5
|
+
messages.forEach((hm, idx) => {
|
|
6
|
+
const runId = `history:${idx}`;
|
|
7
|
+
if (hm.role === "user") {
|
|
8
|
+
chatLog.addUser(hm.content);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (hm.role === "system") {
|
|
12
|
+
chatLog.addSystem(hm.content);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const tools = hm.toolCalls ?? [];
|
|
16
|
+
for (let t = 0; t < tools.length; t++) {
|
|
17
|
+
const tc = tools[t];
|
|
18
|
+
const tid = `history:${idx}:t:${t}`;
|
|
19
|
+
chatLog.startTool(tid, tc.name, tc.args ?? {}, runId);
|
|
20
|
+
if (tc.result !== void 0) chatLog.updateToolResult(tid, tc.result, tc.isError ?? false);
|
|
21
|
+
}
|
|
22
|
+
const body = hm.content.trim() ? hm.content : " ";
|
|
23
|
+
chatLog.finalizeAssistant(body, runId);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { appendHistoryToChatLog };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=chat-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-history.js","names":[],"sources":["../../../src/tui/chat-history.ts"],"sourcesContent":["import type { HistoryMessage } from './tui-backend.js';\nimport { ChatLog } from './components/chat-log.js';\n\n/** Replay persisted transcript into the scroll log (synthetic run ids per assistant row). */\nexport function appendHistoryToChatLog(\n chatLog: ChatLog,\n messages: HistoryMessage[],\n toolsExpanded: boolean,\n): void {\n chatLog.setToolsExpanded(toolsExpanded);\n\n messages.forEach((hm, idx) => {\n const runId = `history:${idx}`;\n\n if (hm.role === 'user') {\n chatLog.addUser(hm.content);\n return;\n }\n\n if (hm.role === 'system') {\n chatLog.addSystem(hm.content);\n return;\n }\n\n const tools = hm.toolCalls ?? [];\n for (let t = 0; t < tools.length; t++) {\n const tc = tools[t]!;\n const tid = `history:${idx}:t:${t}`;\n chatLog.startTool(tid, tc.name, tc.args ?? {}, runId);\n if (tc.result !== undefined) {\n chatLog.updateToolResult(tid, tc.result, tc.isError ?? false);\n }\n }\n\n const body = hm.content.trim() ? hm.content : ' ';\n chatLog.finalizeAssistant(body, runId);\n });\n}\n"],"mappings":";;AAIA,SAAgB,uBACd,SACA,UACA,eACM;AACN,SAAQ,iBAAiB,cAAc;AAEvC,UAAS,SAAS,IAAI,QAAQ;EAC5B,MAAM,QAAQ,WAAW;AAEzB,MAAI,GAAG,SAAS,QAAQ;AACtB,WAAQ,QAAQ,GAAG,QAAQ;AAC3B;;AAGF,MAAI,GAAG,SAAS,UAAU;AACxB,WAAQ,UAAU,GAAG,QAAQ;AAC7B;;EAGF,MAAM,QAAQ,GAAG,aAAa,EAAE;AAChC,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,MAAM,KAAK,MAAM;GACjB,MAAM,MAAM,WAAW,IAAI,KAAK;AAChC,WAAQ,UAAU,KAAK,GAAG,MAAM,GAAG,QAAQ,EAAE,EAAE,MAAM;AACrD,OAAI,GAAG,WAAW,KAAA,EAChB,SAAQ,iBAAiB,KAAK,GAAG,QAAQ,GAAG,WAAW,MAAM;;EAIjE,MAAM,OAAO,GAAG,QAAQ,MAAM,GAAG,GAAG,UAAU;AAC9C,UAAQ,kBAAkB,MAAM,MAAM;GACtC"}
|
|
@@ -2,6 +2,8 @@ import { Container } from '@mariozechner/pi-tui';
|
|
|
2
2
|
export declare class ChatLog extends Container {
|
|
3
3
|
private toolById;
|
|
4
4
|
private streamingRuns;
|
|
5
|
+
/** After finalizeAssistant, late tool_start can still arrive; keep the bubble to insert tools above. */
|
|
6
|
+
private assistantAnchorByRunId;
|
|
5
7
|
private toolsExpanded;
|
|
6
8
|
private pruneOverflow;
|
|
7
9
|
private dropReferences;
|
|
@@ -13,7 +15,7 @@ export declare class ChatLog extends Container {
|
|
|
13
15
|
updateAssistant(text: string, runId: string): void;
|
|
14
16
|
finalizeAssistant(text: string, runId: string): void;
|
|
15
17
|
dropAssistant(runId: string): void;
|
|
16
|
-
startTool(toolCallId: string, toolName: string, args: unknown): void;
|
|
18
|
+
startTool(toolCallId: string, toolName: string, args: unknown, runId: string): void;
|
|
17
19
|
updateToolResult(toolCallId: string, result: string, isError: boolean): void;
|
|
18
20
|
setToolsExpanded(expanded: boolean): void;
|
|
19
21
|
}
|
|
@@ -8,6 +8,8 @@ const MAX_COMPONENTS = 180;
|
|
|
8
8
|
var ChatLog = class extends Container {
|
|
9
9
|
toolById = /* @__PURE__ */ new Map();
|
|
10
10
|
streamingRuns = /* @__PURE__ */ new Map();
|
|
11
|
+
/** After finalizeAssistant, late tool_start can still arrive; keep the bubble to insert tools above. */
|
|
12
|
+
assistantAnchorByRunId = /* @__PURE__ */ new Map();
|
|
11
13
|
toolsExpanded = false;
|
|
12
14
|
pruneOverflow() {
|
|
13
15
|
while (this.children.length > MAX_COMPONENTS) {
|
|
@@ -20,6 +22,7 @@ var ChatLog = class extends Container {
|
|
|
20
22
|
dropReferences(component) {
|
|
21
23
|
for (const [id, tool] of this.toolById.entries()) if (tool === component) this.toolById.delete(id);
|
|
22
24
|
for (const [runId, msg] of this.streamingRuns.entries()) if (msg === component) this.streamingRuns.delete(runId);
|
|
25
|
+
for (const [runId, msg] of this.assistantAnchorByRunId.entries()) if (msg === component) this.assistantAnchorByRunId.delete(runId);
|
|
23
26
|
}
|
|
24
27
|
append(component) {
|
|
25
28
|
this.addChild(component);
|
|
@@ -29,6 +32,7 @@ var ChatLog = class extends Container {
|
|
|
29
32
|
this.clear();
|
|
30
33
|
this.toolById.clear();
|
|
31
34
|
this.streamingRuns.clear();
|
|
35
|
+
this.assistantAnchorByRunId.clear();
|
|
32
36
|
}
|
|
33
37
|
addSystem(text) {
|
|
34
38
|
const entry = new Container();
|
|
@@ -37,6 +41,7 @@ var ChatLog = class extends Container {
|
|
|
37
41
|
this.append(entry);
|
|
38
42
|
}
|
|
39
43
|
addUser(text) {
|
|
44
|
+
this.assistantAnchorByRunId.clear();
|
|
40
45
|
this.append(new UserMessageComponent(text));
|
|
41
46
|
}
|
|
42
47
|
startAssistant(text, runId) {
|
|
@@ -62,9 +67,12 @@ var ChatLog = class extends Container {
|
|
|
62
67
|
if (existing) {
|
|
63
68
|
existing.setText(text);
|
|
64
69
|
this.streamingRuns.delete(runId);
|
|
70
|
+
this.assistantAnchorByRunId.set(runId, existing);
|
|
65
71
|
return;
|
|
66
72
|
}
|
|
67
|
-
|
|
73
|
+
const legacy = new AssistantMessageComponent(text);
|
|
74
|
+
this.append(legacy);
|
|
75
|
+
if (text.trim()) this.assistantAnchorByRunId.set(runId, legacy);
|
|
68
76
|
}
|
|
69
77
|
dropAssistant(runId) {
|
|
70
78
|
const existing = this.streamingRuns.get(runId);
|
|
@@ -72,7 +80,7 @@ var ChatLog = class extends Container {
|
|
|
72
80
|
this.removeChild(existing);
|
|
73
81
|
this.streamingRuns.delete(runId);
|
|
74
82
|
}
|
|
75
|
-
startTool(toolCallId, toolName, args) {
|
|
83
|
+
startTool(toolCallId, toolName, args, runId) {
|
|
76
84
|
const existing = this.toolById.get(toolCallId);
|
|
77
85
|
if (existing) {
|
|
78
86
|
existing.setArgs(args);
|
|
@@ -81,7 +89,13 @@ var ChatLog = class extends Container {
|
|
|
81
89
|
const component = new ToolExecutionComponent(toolName, args);
|
|
82
90
|
component.setExpanded(this.toolsExpanded);
|
|
83
91
|
this.toolById.set(toolCallId, component);
|
|
84
|
-
this.
|
|
92
|
+
const assistant = this.streamingRuns.get(runId) ?? this.assistantAnchorByRunId.get(runId);
|
|
93
|
+
if (assistant) {
|
|
94
|
+
this.removeChild(assistant);
|
|
95
|
+
this.addChild(component);
|
|
96
|
+
this.addChild(assistant);
|
|
97
|
+
} else this.addChild(component);
|
|
98
|
+
this.pruneOverflow();
|
|
85
99
|
}
|
|
86
100
|
updateToolResult(toolCallId, result, isError) {
|
|
87
101
|
const existing = this.toolById.get(toolCallId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-log.js","names":[],"sources":["../../../../src/tui/components/chat-log.ts"],"sourcesContent":["import type { Component } from '@mariozechner/pi-tui';\nimport { Container, Spacer, Text } from '@mariozechner/pi-tui';\n\nimport { theme } from '../theme.js';\nimport { AssistantMessageComponent } from './assistant-message.js';\nimport { ToolExecutionComponent } from './tool-execution.js';\nimport { UserMessageComponent } from './user-message.js';\n\nconst MAX_COMPONENTS = 180;\n\nexport class ChatLog extends Container {\n private toolById = new Map<string, ToolExecutionComponent>();\n private streamingRuns = new Map<string, AssistantMessageComponent>();\n private toolsExpanded = false;\n\n private pruneOverflow(): void {\n while (this.children.length > MAX_COMPONENTS) {\n const oldest = this.children[0];\n if (!oldest) return;\n this.removeChild(oldest);\n this.dropReferences(oldest);\n }\n }\n\n private dropReferences(component: Component): void {\n for (const [id, tool] of this.toolById.entries()) {\n if (tool === component) this.toolById.delete(id);\n }\n for (const [runId, msg] of this.streamingRuns.entries()) {\n if (msg === component) this.streamingRuns.delete(runId);\n }\n }\n\n private append(component: Component): void {\n this.addChild(component);\n this.pruneOverflow();\n }\n\n clearAll(): void {\n this.clear();\n this.toolById.clear();\n this.streamingRuns.clear();\n }\n\n addSystem(text: string): void {\n const entry = new Container();\n entry.addChild(new Spacer(1));\n entry.addChild(new Text(theme.system(text), 1, 0));\n this.append(entry);\n }\n\n addUser(text: string): void {\n this.append(new UserMessageComponent(text));\n }\n\n startAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (existing) {\n existing.setText(text);\n return;\n }\n const component = new AssistantMessageComponent(text);\n this.streamingRuns.set(runId, component);\n this.append(component);\n }\n\n updateAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (!existing) {\n this.startAssistant(text, runId);\n return;\n }\n existing.setText(text);\n }\n\n finalizeAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (existing) {\n existing.setText(text);\n this.streamingRuns.delete(runId);\n return;\n }\n this.append(
|
|
1
|
+
{"version":3,"file":"chat-log.js","names":[],"sources":["../../../../src/tui/components/chat-log.ts"],"sourcesContent":["import type { Component } from '@mariozechner/pi-tui';\nimport { Container, Spacer, Text } from '@mariozechner/pi-tui';\n\nimport { theme } from '../theme.js';\nimport { AssistantMessageComponent } from './assistant-message.js';\nimport { ToolExecutionComponent } from './tool-execution.js';\nimport { UserMessageComponent } from './user-message.js';\n\nconst MAX_COMPONENTS = 180;\n\nexport class ChatLog extends Container {\n private toolById = new Map<string, ToolExecutionComponent>();\n private streamingRuns = new Map<string, AssistantMessageComponent>();\n /** After finalizeAssistant, late tool_start can still arrive; keep the bubble to insert tools above. */\n private assistantAnchorByRunId = new Map<string, AssistantMessageComponent>();\n private toolsExpanded = false;\n\n private pruneOverflow(): void {\n while (this.children.length > MAX_COMPONENTS) {\n const oldest = this.children[0];\n if (!oldest) return;\n this.removeChild(oldest);\n this.dropReferences(oldest);\n }\n }\n\n private dropReferences(component: Component): void {\n for (const [id, tool] of this.toolById.entries()) {\n if (tool === component) this.toolById.delete(id);\n }\n for (const [runId, msg] of this.streamingRuns.entries()) {\n if (msg === component) this.streamingRuns.delete(runId);\n }\n for (const [runId, msg] of this.assistantAnchorByRunId.entries()) {\n if (msg === component) this.assistantAnchorByRunId.delete(runId);\n }\n }\n\n private append(component: Component): void {\n this.addChild(component);\n this.pruneOverflow();\n }\n\n clearAll(): void {\n this.clear();\n this.toolById.clear();\n this.streamingRuns.clear();\n this.assistantAnchorByRunId.clear();\n }\n\n addSystem(text: string): void {\n const entry = new Container();\n entry.addChild(new Spacer(1));\n entry.addChild(new Text(theme.system(text), 1, 0));\n this.append(entry);\n }\n\n addUser(text: string): void {\n this.assistantAnchorByRunId.clear();\n this.append(new UserMessageComponent(text));\n }\n\n startAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (existing) {\n existing.setText(text);\n return;\n }\n const component = new AssistantMessageComponent(text);\n this.streamingRuns.set(runId, component);\n this.append(component);\n }\n\n updateAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (!existing) {\n this.startAssistant(text, runId);\n return;\n }\n existing.setText(text);\n }\n\n finalizeAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (existing) {\n existing.setText(text);\n this.streamingRuns.delete(runId);\n this.assistantAnchorByRunId.set(runId, existing);\n return;\n }\n const legacy = new AssistantMessageComponent(text);\n this.append(legacy);\n if (text.trim()) {\n this.assistantAnchorByRunId.set(runId, legacy);\n }\n }\n\n dropAssistant(runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (!existing) return;\n this.removeChild(existing);\n this.streamingRuns.delete(runId);\n }\n\n startTool(toolCallId: string, toolName: string, args: unknown, runId: string): void {\n const existing = this.toolById.get(toolCallId);\n if (existing) {\n existing.setArgs(args);\n return;\n }\n const component = new ToolExecutionComponent(toolName, args);\n component.setExpanded(this.toolsExpanded);\n this.toolById.set(toolCallId, component);\n\n const assistant =\n this.streamingRuns.get(runId) ?? this.assistantAnchorByRunId.get(runId);\n if (assistant) {\n // Streamed assistant text is updated in place from the start of the turn; tools\n // arrive later from SSE but should appear above the conversational reply (like the web UI).\n this.removeChild(assistant);\n this.addChild(component);\n this.addChild(assistant);\n } else {\n this.addChild(component);\n }\n this.pruneOverflow();\n }\n\n updateToolResult(toolCallId: string, result: string, isError: boolean): void {\n const existing = this.toolById.get(toolCallId);\n if (!existing) return;\n existing.setResult(result, isError);\n }\n\n setToolsExpanded(expanded: boolean): void {\n this.toolsExpanded = expanded;\n for (const tool of this.toolById.values()) {\n tool.setExpanded(expanded);\n }\n }\n}\n"],"mappings":";;;;;;AAQA,MAAM,iBAAiB;AAEvB,IAAa,UAAb,cAA6B,UAAU;CACrC,2BAAmB,IAAI,KAAqC;CAC5D,gCAAwB,IAAI,KAAwC;;CAEpE,yCAAiC,IAAI,KAAwC;CAC7E,gBAAwB;CAExB,gBAA8B;AAC5B,SAAO,KAAK,SAAS,SAAS,gBAAgB;GAC5C,MAAM,SAAS,KAAK,SAAS;AAC7B,OAAI,CAAC,OAAQ;AACb,QAAK,YAAY,OAAO;AACxB,QAAK,eAAe,OAAO;;;CAI/B,eAAuB,WAA4B;AACjD,OAAK,MAAM,CAAC,IAAI,SAAS,KAAK,SAAS,SAAS,CAC9C,KAAI,SAAS,UAAW,MAAK,SAAS,OAAO,GAAG;AAElD,OAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,cAAc,SAAS,CACrD,KAAI,QAAQ,UAAW,MAAK,cAAc,OAAO,MAAM;AAEzD,OAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,uBAAuB,SAAS,CAC9D,KAAI,QAAQ,UAAW,MAAK,uBAAuB,OAAO,MAAM;;CAIpE,OAAe,WAA4B;AACzC,OAAK,SAAS,UAAU;AACxB,OAAK,eAAe;;CAGtB,WAAiB;AACf,OAAK,OAAO;AACZ,OAAK,SAAS,OAAO;AACrB,OAAK,cAAc,OAAO;AAC1B,OAAK,uBAAuB,OAAO;;CAGrC,UAAU,MAAoB;EAC5B,MAAM,QAAQ,IAAI,WAAW;AAC7B,QAAM,SAAS,IAAI,OAAO,EAAE,CAAC;AAC7B,QAAM,SAAS,IAAI,KAAK,MAAM,OAAO,KAAK,EAAE,GAAG,EAAE,CAAC;AAClD,OAAK,OAAO,MAAM;;CAGpB,QAAQ,MAAoB;AAC1B,OAAK,uBAAuB,OAAO;AACnC,OAAK,OAAO,IAAI,qBAAqB,KAAK,CAAC;;CAG7C,eAAe,MAAc,OAAqB;EAChD,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,UAAU;AACZ,YAAS,QAAQ,KAAK;AACtB;;EAEF,MAAM,YAAY,IAAI,0BAA0B,KAAK;AACrD,OAAK,cAAc,IAAI,OAAO,UAAU;AACxC,OAAK,OAAO,UAAU;;CAGxB,gBAAgB,MAAc,OAAqB;EACjD,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,CAAC,UAAU;AACb,QAAK,eAAe,MAAM,MAAM;AAChC;;AAEF,WAAS,QAAQ,KAAK;;CAGxB,kBAAkB,MAAc,OAAqB;EACnD,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,UAAU;AACZ,YAAS,QAAQ,KAAK;AACtB,QAAK,cAAc,OAAO,MAAM;AAChC,QAAK,uBAAuB,IAAI,OAAO,SAAS;AAChD;;EAEF,MAAM,SAAS,IAAI,0BAA0B,KAAK;AAClD,OAAK,OAAO,OAAO;AACnB,MAAI,KAAK,MAAM,CACb,MAAK,uBAAuB,IAAI,OAAO,OAAO;;CAIlD,cAAc,OAAqB;EACjC,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,CAAC,SAAU;AACf,OAAK,YAAY,SAAS;AAC1B,OAAK,cAAc,OAAO,MAAM;;CAGlC,UAAU,YAAoB,UAAkB,MAAe,OAAqB;EAClF,MAAM,WAAW,KAAK,SAAS,IAAI,WAAW;AAC9C,MAAI,UAAU;AACZ,YAAS,QAAQ,KAAK;AACtB;;EAEF,MAAM,YAAY,IAAI,uBAAuB,UAAU,KAAK;AAC5D,YAAU,YAAY,KAAK,cAAc;AACzC,OAAK,SAAS,IAAI,YAAY,UAAU;EAExC,MAAM,YACJ,KAAK,cAAc,IAAI,MAAM,IAAI,KAAK,uBAAuB,IAAI,MAAM;AACzE,MAAI,WAAW;AAGb,QAAK,YAAY,UAAU;AAC3B,QAAK,SAAS,UAAU;AACxB,QAAK,SAAS,UAAU;QAExB,MAAK,SAAS,UAAU;AAE1B,OAAK,eAAe;;CAGtB,iBAAiB,YAAoB,QAAgB,SAAwB;EAC3E,MAAM,WAAW,KAAK,SAAS,IAAI,WAAW;AAC9C,MAAI,CAAC,SAAU;AACf,WAAS,UAAU,QAAQ,QAAQ;;CAGrC,iBAAiB,UAAyB;AACxC,OAAK,gBAAgB;AACrB,OAAK,MAAM,QAAQ,KAAK,SAAS,QAAQ,CACvC,MAAK,YAAY,SAAS"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Editor, Key, matchesKey } from "@mariozechner/pi-tui";
|
|
1
|
+
import { Editor, Key, getKeybindings, matchesKey, parseKey } from "@mariozechner/pi-tui";
|
|
2
2
|
//#region src/tui/components/custom-editor.ts
|
|
3
3
|
/**
|
|
4
4
|
* Extended editor with additional key bindings for the TUI.
|
|
@@ -8,13 +8,19 @@ var CustomEditor = class extends Editor {
|
|
|
8
8
|
onCtrlC;
|
|
9
9
|
onCtrlD;
|
|
10
10
|
onCtrlL;
|
|
11
|
+
onCtrlP;
|
|
11
12
|
onCtrlO;
|
|
12
13
|
onCtrlT;
|
|
13
14
|
handleInput(data) {
|
|
15
|
+
const kb = getKeybindings();
|
|
14
16
|
if (matchesKey(data, Key.ctrl("l")) && this.onCtrlL) {
|
|
15
17
|
this.onCtrlL();
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
20
|
+
if (matchesKey(data, Key.ctrl("p")) && this.onCtrlP) {
|
|
21
|
+
this.onCtrlP();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
18
24
|
if (matchesKey(data, Key.ctrl("o")) && this.onCtrlO) {
|
|
19
25
|
this.onCtrlO();
|
|
20
26
|
return;
|
|
@@ -27,7 +33,7 @@ var CustomEditor = class extends Editor {
|
|
|
27
33
|
this.onEscape();
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
30
|
-
if (matchesKey(data, Key.ctrl("c"))
|
|
36
|
+
if (this.onCtrlC && (data === "" || parseKey(data) === "ctrl+c" || matchesKey(data, Key.ctrl("c")) || kb.matches(data, "tui.input.copy"))) {
|
|
31
37
|
this.onCtrlC();
|
|
32
38
|
return;
|
|
33
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-editor.js","names":[],"sources":["../../../../src/tui/components/custom-editor.ts"],"sourcesContent":["import { Editor, Key, matchesKey } from '@mariozechner/pi-tui';\n\n/**\n * Extended editor with additional key bindings for the TUI.\n */\nexport class CustomEditor extends Editor {\n onEscape?: () => void;\n onCtrlC?: () => void;\n onCtrlD?: () => void;\n onCtrlL?: () => void;\n onCtrlO?: () => void;\n onCtrlT?: () => void;\n\n handleInput(data: string): void {\n if (matchesKey(data, Key.ctrl('l')) && this.onCtrlL) {\n this.onCtrlL();\n return;\n }\n if (matchesKey(data, Key.ctrl('o')) && this.onCtrlO) {\n this.onCtrlO();\n return;\n }\n if (matchesKey(data, Key.ctrl('t')) && this.onCtrlT) {\n this.onCtrlT();\n return;\n }\n if (matchesKey(data, Key.escape) && this.onEscape && !this.isShowingAutocomplete()) {\n this.onEscape();\n return;\n }\n if (matchesKey(data, Key.ctrl('c'))
|
|
1
|
+
{"version":3,"file":"custom-editor.js","names":[],"sources":["../../../../src/tui/components/custom-editor.ts"],"sourcesContent":["import { Editor, getKeybindings, Key, matchesKey, parseKey } from '@mariozechner/pi-tui';\n\n/**\n * Extended editor with additional key bindings for the TUI.\n */\nexport class CustomEditor extends Editor {\n onEscape?: () => void;\n onCtrlC?: () => void;\n onCtrlD?: () => void;\n onCtrlL?: () => void;\n onCtrlP?: () => void;\n onCtrlO?: () => void;\n onCtrlT?: () => void;\n\n handleInput(data: string): void {\n const kb = getKeybindings();\n if (matchesKey(data, Key.ctrl('l')) && this.onCtrlL) {\n this.onCtrlL();\n return;\n }\n if (matchesKey(data, Key.ctrl('p')) && this.onCtrlP) {\n this.onCtrlP();\n return;\n }\n if (matchesKey(data, Key.ctrl('o')) && this.onCtrlO) {\n this.onCtrlO();\n return;\n }\n if (matchesKey(data, Key.ctrl('t')) && this.onCtrlT) {\n this.onCtrlT();\n return;\n }\n if (matchesKey(data, Key.escape) && this.onEscape && !this.isShowingAutocomplete()) {\n this.onEscape();\n return;\n }\n // Match all encodings pi-tui uses for Ctrl+C (incl. Kitty / modifyOtherKeys on macOS).\n // Base Editor treats \"tui.input.copy\" as a no-op — falling through swallows the key.\n if (\n this.onCtrlC &&\n (data === '\\x03' ||\n parseKey(data) === 'ctrl+c' ||\n matchesKey(data, Key.ctrl('c')) ||\n kb.matches(data, 'tui.input.copy'))\n ) {\n this.onCtrlC();\n return;\n }\n if (matchesKey(data, Key.ctrl('d'))) {\n if (this.getText().length === 0 && this.onCtrlD) {\n this.onCtrlD();\n }\n return;\n }\n super.handleInput(data);\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,eAAb,cAAkC,OAAO;CACvC;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,MAAoB;EAC9B,MAAM,KAAK,gBAAgB;AAC3B,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,OAAO,IAAI,KAAK,YAAY,CAAC,KAAK,uBAAuB,EAAE;AAClF,QAAK,UAAU;AACf;;AAIF,MACE,KAAK,YACJ,SAAS,OACR,SAAS,KAAK,KAAK,YACnB,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAC/B,GAAG,QAAQ,MAAM,iBAAiB,GACpC;AACA,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,EAAE;AACnC,OAAI,KAAK,SAAS,CAAC,WAAW,KAAK,KAAK,QACtC,MAAK,SAAS;AAEhB;;AAEF,QAAM,YAAY,KAAK"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuzzy filtering for searchable select lists (openclaw-style).
|
|
3
|
+
*/
|
|
4
|
+
export declare function normalizeLowercaseStringOrEmpty(value: string): string;
|
|
5
|
+
export declare function isWordBoundary(text: string, index: number): boolean;
|
|
6
|
+
export declare function findWordBoundaryIndex(text: string, query: string): number | null;
|
|
7
|
+
export declare function fuzzyMatchLower(queryLower: string, textLower: string): number | null;
|
|
8
|
+
export declare function fuzzyFilterLower<T extends {
|
|
9
|
+
searchTextLower?: string;
|
|
10
|
+
}>(items: T[], queryLower: string): T[];
|
|
11
|
+
export declare function prepareSearchItems<T extends {
|
|
12
|
+
label?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
searchText?: string;
|
|
15
|
+
}>(items: T[]): (T & {
|
|
16
|
+
searchTextLower: string;
|
|
17
|
+
})[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//#region src/tui/components/fuzzy-filter.ts
|
|
2
|
+
/**
|
|
3
|
+
* Fuzzy filtering for searchable select lists (openclaw-style).
|
|
4
|
+
*/
|
|
5
|
+
function normalizeLowercaseStringOrEmpty(value) {
|
|
6
|
+
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
7
|
+
}
|
|
8
|
+
const WORD_BOUNDARY_CHARS = /[\s\-_./:#@]/;
|
|
9
|
+
function isWordBoundary(text, index) {
|
|
10
|
+
return index === 0 || WORD_BOUNDARY_CHARS.test(text[index - 1] ?? "");
|
|
11
|
+
}
|
|
12
|
+
function findWordBoundaryIndex(text, query) {
|
|
13
|
+
if (!query) return null;
|
|
14
|
+
const textLower = normalizeLowercaseStringOrEmpty(text);
|
|
15
|
+
const queryLower = normalizeLowercaseStringOrEmpty(query);
|
|
16
|
+
const maxIndex = textLower.length - queryLower.length;
|
|
17
|
+
if (maxIndex < 0) return null;
|
|
18
|
+
for (let i = 0; i <= maxIndex; i++) if (textLower.startsWith(queryLower, i) && isWordBoundary(textLower, i)) return i;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function fuzzyMatchLower(queryLower, textLower) {
|
|
22
|
+
if (queryLower.length === 0) return 0;
|
|
23
|
+
if (queryLower.length > textLower.length) return null;
|
|
24
|
+
let queryIndex = 0;
|
|
25
|
+
let score = 0;
|
|
26
|
+
let lastMatchIndex = -1;
|
|
27
|
+
let consecutiveMatches = 0;
|
|
28
|
+
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) if (textLower[i] === queryLower[queryIndex]) {
|
|
29
|
+
const atWordBoundary = isWordBoundary(textLower, i);
|
|
30
|
+
if (lastMatchIndex === i - 1) {
|
|
31
|
+
consecutiveMatches++;
|
|
32
|
+
score -= consecutiveMatches * 5;
|
|
33
|
+
} else {
|
|
34
|
+
consecutiveMatches = 0;
|
|
35
|
+
if (lastMatchIndex >= 0) score += (i - lastMatchIndex - 1) * 2;
|
|
36
|
+
}
|
|
37
|
+
if (atWordBoundary) score -= 10;
|
|
38
|
+
score += i * .1;
|
|
39
|
+
lastMatchIndex = i;
|
|
40
|
+
queryIndex++;
|
|
41
|
+
}
|
|
42
|
+
return queryIndex < queryLower.length ? null : score;
|
|
43
|
+
}
|
|
44
|
+
function fuzzyFilterLower(items, queryLower) {
|
|
45
|
+
const trimmed = queryLower.trim();
|
|
46
|
+
if (!trimmed) return items;
|
|
47
|
+
const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0);
|
|
48
|
+
if (tokens.length === 0) return items;
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const text = item.searchTextLower ?? "";
|
|
52
|
+
let totalScore = 0;
|
|
53
|
+
let allMatch = true;
|
|
54
|
+
for (const token of tokens) {
|
|
55
|
+
const score = fuzzyMatchLower(token, text);
|
|
56
|
+
if (score !== null) totalScore += score;
|
|
57
|
+
else {
|
|
58
|
+
allMatch = false;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (allMatch) results.push({
|
|
63
|
+
item,
|
|
64
|
+
score: totalScore
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
results.sort((a, b) => a.score - b.score);
|
|
68
|
+
return results.map((r) => r.item);
|
|
69
|
+
}
|
|
70
|
+
function prepareSearchItems(items) {
|
|
71
|
+
return items.map((item) => {
|
|
72
|
+
const parts = [];
|
|
73
|
+
if (item.label) parts.push(item.label);
|
|
74
|
+
if (item.description) parts.push(item.description);
|
|
75
|
+
if (item.searchText) parts.push(item.searchText);
|
|
76
|
+
return {
|
|
77
|
+
...item,
|
|
78
|
+
searchTextLower: normalizeLowercaseStringOrEmpty(parts.join(" "))
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { findWordBoundaryIndex, fuzzyFilterLower, fuzzyMatchLower, isWordBoundary, normalizeLowercaseStringOrEmpty, prepareSearchItems };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=fuzzy-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fuzzy-filter.js","names":[],"sources":["../../../../src/tui/components/fuzzy-filter.ts"],"sourcesContent":["/**\n * Fuzzy filtering for searchable select lists (openclaw-style).\n */\n\nexport function normalizeLowercaseStringOrEmpty(value: string): string {\n return value.trim().toLowerCase().replace(/\\s+/g, ' ');\n}\n\nconst WORD_BOUNDARY_CHARS = /[\\s\\-_./:#@]/;\n\nexport function isWordBoundary(text: string, index: number): boolean {\n return index === 0 || WORD_BOUNDARY_CHARS.test(text[index - 1] ?? '');\n}\n\nexport function findWordBoundaryIndex(text: string, query: string): number | null {\n if (!query) {\n return null;\n }\n const textLower = normalizeLowercaseStringOrEmpty(text);\n const queryLower = normalizeLowercaseStringOrEmpty(query);\n const maxIndex = textLower.length - queryLower.length;\n if (maxIndex < 0) {\n return null;\n }\n for (let i = 0; i <= maxIndex; i++) {\n if (textLower.startsWith(queryLower, i) && isWordBoundary(textLower, i)) {\n return i;\n }\n }\n return null;\n}\n\nexport function fuzzyMatchLower(queryLower: string, textLower: string): number | null {\n if (queryLower.length === 0) {\n return 0;\n }\n if (queryLower.length > textLower.length) {\n return null;\n }\n\n let queryIndex = 0;\n let score = 0;\n let lastMatchIndex = -1;\n let consecutiveMatches = 0;\n\n for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {\n if (textLower[i] === queryLower[queryIndex]) {\n const atWordBoundary = isWordBoundary(textLower, i);\n if (lastMatchIndex === i - 1) {\n consecutiveMatches++;\n score -= consecutiveMatches * 5;\n } else {\n consecutiveMatches = 0;\n if (lastMatchIndex >= 0) {\n score += (i - lastMatchIndex - 1) * 2;\n }\n }\n if (atWordBoundary) {\n score -= 10;\n }\n score += i * 0.1;\n lastMatchIndex = i;\n queryIndex++;\n }\n }\n return queryIndex < queryLower.length ? null : score;\n}\n\nexport function fuzzyFilterLower<T extends { searchTextLower?: string }>(\n items: T[],\n queryLower: string,\n): T[] {\n const trimmed = queryLower.trim();\n if (!trimmed) {\n return items;\n }\n\n const tokens = trimmed.split(/\\s+/).filter((t) => t.length > 0);\n if (tokens.length === 0) {\n return items;\n }\n\n const results: { item: T; score: number }[] = [];\n for (const item of items) {\n const text = item.searchTextLower ?? '';\n let totalScore = 0;\n let allMatch = true;\n for (const token of tokens) {\n const score = fuzzyMatchLower(token, text);\n if (score !== null) {\n totalScore += score;\n } else {\n allMatch = false;\n break;\n }\n }\n if (allMatch) {\n results.push({ item, score: totalScore });\n }\n }\n results.sort((a, b) => a.score - b.score);\n return results.map((r) => r.item);\n}\n\nexport function prepareSearchItems<\n T extends { label?: string; description?: string; searchText?: string },\n>(items: T[]): (T & { searchTextLower: string })[] {\n return items.map((item) => {\n const parts: string[] = [];\n if (item.label) {\n parts.push(item.label);\n }\n if (item.description) {\n parts.push(item.description);\n }\n if (item.searchText) {\n parts.push(item.searchText);\n }\n return { ...item, searchTextLower: normalizeLowercaseStringOrEmpty(parts.join(' ')) };\n });\n}\n"],"mappings":";;;;AAIA,SAAgB,gCAAgC,OAAuB;AACrE,QAAO,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,QAAQ,IAAI;;AAGxD,MAAM,sBAAsB;AAE5B,SAAgB,eAAe,MAAc,OAAwB;AACnE,QAAO,UAAU,KAAK,oBAAoB,KAAK,KAAK,QAAQ,MAAM,GAAG;;AAGvE,SAAgB,sBAAsB,MAAc,OAA8B;AAChF,KAAI,CAAC,MACH,QAAO;CAET,MAAM,YAAY,gCAAgC,KAAK;CACvD,MAAM,aAAa,gCAAgC,MAAM;CACzD,MAAM,WAAW,UAAU,SAAS,WAAW;AAC/C,KAAI,WAAW,EACb,QAAO;AAET,MAAK,IAAI,IAAI,GAAG,KAAK,UAAU,IAC7B,KAAI,UAAU,WAAW,YAAY,EAAE,IAAI,eAAe,WAAW,EAAE,CACrE,QAAO;AAGX,QAAO;;AAGT,SAAgB,gBAAgB,YAAoB,WAAkC;AACpF,KAAI,WAAW,WAAW,EACxB,QAAO;AAET,KAAI,WAAW,SAAS,UAAU,OAChC,QAAO;CAGT,IAAI,aAAa;CACjB,IAAI,QAAQ;CACZ,IAAI,iBAAiB;CACrB,IAAI,qBAAqB;AAEzB,MAAK,IAAI,IAAI,GAAG,IAAI,UAAU,UAAU,aAAa,WAAW,QAAQ,IACtE,KAAI,UAAU,OAAO,WAAW,aAAa;EAC3C,MAAM,iBAAiB,eAAe,WAAW,EAAE;AACnD,MAAI,mBAAmB,IAAI,GAAG;AAC5B;AACA,YAAS,qBAAqB;SACzB;AACL,wBAAqB;AACrB,OAAI,kBAAkB,EACpB,WAAU,IAAI,iBAAiB,KAAK;;AAGxC,MAAI,eACF,UAAS;AAEX,WAAS,IAAI;AACb,mBAAiB;AACjB;;AAGJ,QAAO,aAAa,WAAW,SAAS,OAAO;;AAGjD,SAAgB,iBACd,OACA,YACK;CACL,MAAM,UAAU,WAAW,MAAM;AACjC,KAAI,CAAC,QACH,QAAO;CAGT,MAAM,SAAS,QAAQ,MAAM,MAAM,CAAC,QAAQ,MAAM,EAAE,SAAS,EAAE;AAC/D,KAAI,OAAO,WAAW,EACpB,QAAO;CAGT,MAAM,UAAwC,EAAE;AAChD,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,KAAK,mBAAmB;EACrC,IAAI,aAAa;EACjB,IAAI,WAAW;AACf,OAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,QAAQ,gBAAgB,OAAO,KAAK;AAC1C,OAAI,UAAU,KACZ,eAAc;QACT;AACL,eAAW;AACX;;;AAGJ,MAAI,SACF,SAAQ,KAAK;GAAE;GAAM,OAAO;GAAY,CAAC;;AAG7C,SAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;AACzC,QAAO,QAAQ,KAAK,MAAM,EAAE,KAAK;;AAGnC,SAAgB,mBAEd,OAAiD;AACjD,QAAO,MAAM,KAAK,SAAS;EACzB,MAAM,QAAkB,EAAE;AAC1B,MAAI,KAAK,MACP,OAAM,KAAK,KAAK,MAAM;AAExB,MAAI,KAAK,YACP,OAAM,KAAK,KAAK,YAAY;AAE9B,MAAI,KAAK,WACP,OAAM,KAAK,KAAK,WAAW;AAE7B,SAAO;GAAE,GAAG;GAAM,iBAAiB,gCAAgC,MAAM,KAAK,IAAI,CAAC;GAAE;GACrF"}
|