@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.
Files changed (160) hide show
  1. package/dist/extensions/telegram/xopc.extension.json +1 -1
  2. package/dist/gateway/static/root/assets/agents-CkgFSiCY.js +216 -0
  3. package/dist/gateway/static/root/assets/agents-CkgFSiCY.js.map +1 -0
  4. package/dist/gateway/static/root/assets/{apps-page-Co95hLOJ.js → apps-page-Bmq19MS-.js} +2 -2
  5. package/dist/gateway/static/root/assets/{apps-page-Co95hLOJ.js.map → apps-page-Bmq19MS-.js.map} +1 -1
  6. package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js +9 -0
  7. package/dist/gateway/static/root/assets/channels-settings-CE7jrdkO.js.map +1 -0
  8. package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js +2 -0
  9. package/dist/gateway/static/root/assets/cron-page-BpPPcykJ.js.map +1 -0
  10. package/dist/gateway/static/root/assets/{cron-utils-BmzF4m1y.js → cron-utils-N1PqD2DB.js} +2 -2
  11. package/dist/gateway/static/root/assets/{cron-utils-BmzF4m1y.js.map → cron-utils-N1PqD2DB.js.map} +1 -1
  12. package/dist/gateway/static/root/assets/{dist-Dn-ufXyc.js → dist--p2HQ2QF.js} +2 -2
  13. package/dist/gateway/static/root/assets/{dist-Dn-ufXyc.js.map → dist--p2HQ2QF.js.map} +1 -1
  14. package/dist/gateway/static/root/assets/{extension-debug-page-BZ8xQ74_.js → extension-debug-page-DwHCB_6T.js} +2 -2
  15. package/dist/gateway/static/root/assets/{extension-debug-page-BZ8xQ74_.js.map → extension-debug-page-DwHCB_6T.js.map} +1 -1
  16. package/dist/gateway/static/root/assets/{extension-page-BlNgKxwW.js → extension-page-BsYwQIex.js} +2 -2
  17. package/dist/gateway/static/root/assets/{extension-page-BlNgKxwW.js.map → extension-page-BsYwQIex.js.map} +1 -1
  18. package/dist/gateway/static/root/assets/{extension-settings-page-CWTdW_oY.js → extension-settings-page-nsisEgjB.js} +2 -2
  19. package/dist/gateway/static/root/assets/{extension-settings-page-CWTdW_oY.js.map → extension-settings-page-nsisEgjB.js.map} +1 -1
  20. package/dist/gateway/static/root/assets/index-CR8zUHGR.js +4734 -0
  21. package/dist/gateway/static/root/assets/{index-lV8FGWlt.js.map → index-CR8zUHGR.js.map} +1 -1
  22. package/dist/gateway/static/root/assets/index-Dnfha4O2.css +1 -0
  23. package/dist/gateway/static/root/assets/logs-page-CQwdV_Xw.js +2 -0
  24. package/dist/gateway/static/root/assets/{logs-page-DG31RpvG.js.map → logs-page-CQwdV_Xw.js.map} +1 -1
  25. package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js +2 -0
  26. package/dist/gateway/static/root/assets/sessions-page-Be5kIGl_.js.map +1 -0
  27. package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js +2 -0
  28. package/dist/gateway/static/root/assets/settings-page-PodSlNwr.js.map +1 -0
  29. package/dist/gateway/static/root/assets/skills-page-Clg8deH0.js +3 -0
  30. package/dist/gateway/static/root/assets/{skills-page-lb7vYtlP.js.map → skills-page-Clg8deH0.js.map} +1 -1
  31. package/dist/gateway/static/root/index.html +2 -2
  32. package/dist/package.js +1 -1
  33. package/dist/src/agent/lifecycle/hook-handler.d.ts +2 -0
  34. package/dist/src/agent/lifecycle/hook-handler.js +24 -0
  35. package/dist/src/agent/lifecycle/hook-handler.js.map +1 -1
  36. package/dist/src/agent/messaging/command-handler.js +10 -2
  37. package/dist/src/agent/messaging/command-handler.js.map +1 -1
  38. package/dist/src/agent/service/process-direct-streaming.js +77 -20
  39. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  40. package/dist/src/agent/service.d.ts +15 -0
  41. package/dist/src/agent/service.js +21 -1
  42. package/dist/src/agent/service.js.map +1 -1
  43. package/dist/src/channels/index.js +2 -2
  44. package/dist/src/channels/manager.js +2 -2
  45. package/dist/src/cli/agent-chat-log-level-preset.d.ts +3 -2
  46. package/dist/src/cli/agent-chat-log-level-preset.js +6 -3
  47. package/dist/src/cli/agent-chat-log-level-preset.js.map +1 -1
  48. package/dist/src/cli/index.js +4 -3
  49. package/dist/src/cli/index.js.map +1 -1
  50. package/dist/src/config/schema.js +5 -2
  51. package/dist/src/config/schema.js.map +1 -1
  52. package/dist/src/extensions/hooks.js +5 -1
  53. package/dist/src/extensions/hooks.js.map +1 -1
  54. package/dist/src/extensions/loader.d.ts +1 -0
  55. package/dist/src/extensions/loader.js +3 -1
  56. package/dist/src/extensions/loader.js.map +1 -1
  57. package/dist/src/extensions/sdk/index.d.ts +1 -1
  58. package/dist/src/extensions/sdk/index.js.map +1 -1
  59. package/dist/src/extensions/types/core.d.ts +8 -0
  60. package/dist/src/extensions/types/hooks.d.ts +16 -1
  61. package/dist/src/extensions/types/hooks.js +1 -0
  62. package/dist/src/extensions/types/hooks.js.map +1 -1
  63. package/dist/src/gateway/agents-admin.d.ts +19 -1
  64. package/dist/src/gateway/agents-admin.js +164 -3
  65. package/dist/src/gateway/agents-admin.js.map +1 -1
  66. package/dist/src/gateway/hono/app.js +1 -0
  67. package/dist/src/gateway/hono/app.js.map +1 -1
  68. package/dist/src/gateway/hono/routes/agents.js +59 -5
  69. package/dist/src/gateway/hono/routes/agents.js.map +1 -1
  70. package/dist/src/gateway/hono/routes/config.js +2 -2
  71. package/dist/src/gateway/hono/routes/config.js.map +1 -1
  72. package/dist/src/gateway/hono/routes/public-gateway.js +1 -0
  73. package/dist/src/gateway/hono/routes/public-gateway.js.map +1 -1
  74. package/dist/src/gateway/hono/routes/sessions.js +17 -0
  75. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  76. package/dist/src/gateway/service.d.ts +2 -0
  77. package/dist/src/gateway/service.js +31 -4
  78. package/dist/src/gateway/service.js.map +1 -1
  79. package/dist/src/session/client-history.d.ts +21 -0
  80. package/dist/src/session/client-history.js +89 -0
  81. package/dist/src/session/client-history.js.map +1 -0
  82. package/dist/src/session/index.d.ts +1 -0
  83. package/dist/src/session/index.js +2 -1
  84. package/dist/src/session/manager.d.ts +2 -0
  85. package/dist/src/session/manager.js +5 -0
  86. package/dist/src/session/manager.js.map +1 -1
  87. package/dist/src/session/thinking-resolve.js +1 -1
  88. package/dist/src/session/thinking-resolve.js.map +1 -1
  89. package/dist/src/tui/backends/embedded-backend.d.ts +1 -1
  90. package/dist/src/tui/backends/embedded-backend.js +15 -2
  91. package/dist/src/tui/backends/embedded-backend.js.map +1 -1
  92. package/dist/src/tui/backends/gateway-sse-backend.d.ts +4 -0
  93. package/dist/src/tui/backends/gateway-sse-backend.js +34 -4
  94. package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -1
  95. package/dist/src/tui/chat-history.d.ts +4 -0
  96. package/dist/src/tui/chat-history.js +29 -0
  97. package/dist/src/tui/chat-history.js.map +1 -0
  98. package/dist/src/tui/components/chat-log.d.ts +3 -1
  99. package/dist/src/tui/components/chat-log.js +17 -3
  100. package/dist/src/tui/components/chat-log.js.map +1 -1
  101. package/dist/src/tui/components/custom-editor.d.ts +1 -0
  102. package/dist/src/tui/components/custom-editor.js +8 -2
  103. package/dist/src/tui/components/custom-editor.js.map +1 -1
  104. package/dist/src/tui/components/fuzzy-filter.d.ts +17 -0
  105. package/dist/src/tui/components/fuzzy-filter.js +85 -0
  106. package/dist/src/tui/components/fuzzy-filter.js.map +1 -0
  107. package/dist/src/tui/components/searchable-select-list.d.ts +39 -0
  108. package/dist/src/tui/components/searchable-select-list.js +257 -0
  109. package/dist/src/tui/components/searchable-select-list.js.map +1 -0
  110. package/dist/src/tui/theme.d.ts +2 -0
  111. package/dist/src/tui/theme.js +7 -1
  112. package/dist/src/tui/theme.js.map +1 -1
  113. package/dist/src/tui/tui-agent-events.d.ts +7 -0
  114. package/dist/src/tui/tui-agent-events.js +103 -0
  115. package/dist/src/tui/tui-agent-events.js.map +1 -0
  116. package/dist/src/tui/tui-backend.d.ts +8 -12
  117. package/dist/src/tui/tui-commands.d.ts +23 -0
  118. package/dist/src/tui/tui-commands.js +165 -0
  119. package/dist/src/tui/tui-commands.js.map +1 -0
  120. package/dist/src/tui/tui-lifecycle.d.ts +26 -0
  121. package/dist/src/tui/tui-lifecycle.js +57 -0
  122. package/dist/src/tui/tui-lifecycle.js.map +1 -0
  123. package/dist/src/tui/tui-local-shell.d.ts +28 -0
  124. package/dist/src/tui/tui-local-shell.js +147 -0
  125. package/dist/src/tui/tui-local-shell.js.map +1 -0
  126. package/dist/src/tui/tui-overlays.d.ts +8 -0
  127. package/dist/src/tui/tui-overlays.js +22 -0
  128. package/dist/src/tui/tui-overlays.js.map +1 -0
  129. package/dist/src/tui/tui-picker-overlay.d.ts +26 -0
  130. package/dist/src/tui/tui-picker-overlay.js +69 -0
  131. package/dist/src/tui/tui-picker-overlay.js.map +1 -0
  132. package/dist/src/tui/tui-stdio-filter.d.ts +17 -0
  133. package/dist/src/tui/tui-stdio-filter.js +96 -0
  134. package/dist/src/tui/tui-stdio-filter.js.map +1 -0
  135. package/dist/src/tui/tui-submit.d.ts +25 -0
  136. package/dist/src/tui/tui-submit.js +102 -0
  137. package/dist/src/tui/tui-submit.js.map +1 -0
  138. package/dist/src/tui/tui-suspend.d.ts +10 -0
  139. package/dist/src/tui/tui-suspend.js +18 -0
  140. package/dist/src/tui/tui-suspend.js.map +1 -0
  141. package/dist/src/tui/tui-types.d.ts +1 -0
  142. package/dist/src/tui/tui-types.js.map +1 -1
  143. package/dist/src/tui/tui.d.ts +2 -0
  144. package/dist/src/tui/tui.js +175 -312
  145. package/dist/src/tui/tui.js.map +1 -1
  146. package/package.json +2 -6
  147. package/dist/gateway/static/root/assets/agents-DplaQYS2.js +0 -216
  148. package/dist/gateway/static/root/assets/agents-DplaQYS2.js.map +0 -1
  149. package/dist/gateway/static/root/assets/channels-settings-CkfSST0k.js +0 -9
  150. package/dist/gateway/static/root/assets/channels-settings-CkfSST0k.js.map +0 -1
  151. package/dist/gateway/static/root/assets/cron-page-D9q6KqL8.js +0 -2
  152. package/dist/gateway/static/root/assets/cron-page-D9q6KqL8.js.map +0 -1
  153. package/dist/gateway/static/root/assets/index-OT4cGzon.css +0 -1
  154. package/dist/gateway/static/root/assets/index-lV8FGWlt.js +0 -4734
  155. package/dist/gateway/static/root/assets/logs-page-DG31RpvG.js +0 -2
  156. package/dist/gateway/static/root/assets/sessions-page-CdmjxDEM.js +0 -2
  157. package/dist/gateway/static/root/assets/sessions-page-CdmjxDEM.js.map +0 -1
  158. package/dist/gateway/static/root/assets/settings-page-DU2XLf5s.js +0 -2
  159. package/dist/gateway/static/root/assets/settings-page-DU2XLf5s.js.map +0 -1
  160. 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 = "off";
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 = 'off';\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"}
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"}
@@ -27,7 +27,7 @@ export declare class EmbeddedBackend implements TuiBackend {
27
27
  }): Promise<{
28
28
  ok: boolean;
29
29
  }>;
30
- loadHistory(_opts: {
30
+ loadHistory(opts: {
31
31
  sessionKey: string;
32
32
  limit?: number;
33
33
  }): Promise<{
@@ -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(_opts) {
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(_opts: {\n sessionKey: string;\n limit?: number;\n }): Promise<{ messages: HistoryMessage[] }> {\n // Session history loading deferred to future iteration\n return { messages: [] };\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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"}
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":";;;;;;;;;;;;;gBAKgF;aAC3B;AAWrD,MAAM,MAAM,aAAa,eAAe;;;;;;;AAQxC,IAAa,kBAAb,MAAmD;CACjD;CACA,QAAqC;CACrC,UAAkB;CAClB,YAA4C;CAE5C;CACA;CACA;CAEA,cAAc;AACZ,OAAK,MAAM,IAAI,YAAY;;CAG7B,IAAI,kBAA0B;AAC5B,SAAO;;CAGT,QAAc;AACZ,MAAI,KAAK,QAAS;AAClB,OAAK,UAAU;EAEf,MAAM,SAAS,YAAY;EAC3B,MAAM,YAAY,iBAAiB,OAAO;EAC1C,MAAM,cAAc,OAAO,QAAQ,UAAU;EAC7C,MAAM,UAAU,OAAO,gBAAgB,WAAW,cAAc,aAAa;AAE7E,OAAK,QAAQ,IAAI,aAAa,KAAK,KAAK;GACtC,WAAW,aAAa,QAAQ,KAAK;GACrC,OAAO;GACP;GACD,CAAC;AAEF,OAAK,MAAM,OAAO,CAAC,OAAO,QAAQ;GAChC,MAAM,eAAe,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AACrE,OAAI,MAAM;IAAE;IAAK;IAAc,EAAE,0BAA0B,eAAe;AAC1E,QAAK,iBAAiB,aAAa;IACnC;AAGF,OAAK,iBAAiB;AAGtB,uBAAqB,KAAK,eAAe,CAAC;;CAG5C,OAAa;AACX,OAAK,UAAU;AACf,OAAK,IAAI,UAAU;AACd,OAAK,OAAO,MAAM;AACvB,OAAK,QAAQ;;CAGf,MAAM,SAAS,MAAmD;AAChE,MAAI,CAAC,KAAK,MAAO,OAAM,IAAI,MAAM,oBAAoB;EAErD,MAAM,QAAQ,OAAO,YAAY;AACjC,OAAK,WAAW,OAAO;AACvB,OAAK,YAAY,IAAI,iBAAiB;EACtC,MAAM,SAAS,KAAK,UAAU;AAE9B,OAAK,UAAU;GAAE,OAAO;GAAU,MAAM;IAAE,QAAQ;IAAW;IAAO;GAAE,CAAC;AAGvE,GAAM,YAAY;AAChB,OAAI;IAIF,MAAM,kBAAkB,KAAK,QAAQ,WAAW,CAAC,WAAW,IAAI,GAC5D,KAAK,UACL,yBAAyB,KAAK,QAAQ;IAE1C,MAAM,SAAS,KAAK,MAAO,uBACzB,iBACA,KAAK,YACL,KAAA,GACA,KAAK,UACL,EAAE,QAAQ,CACX;AAED,eAAW,MAAM,SAAS,QAAQ;AAChC,SAAI,OAAO,QAAS;AACpB,UAAK,UAAU;MAAE,OAAO,MAAM;MAAM,MAAM;MAAO,CAAC;;AAGpD,QAAI,CAAC,OAAO,QACV,MAAK,UAAU;KACb,OAAO;KACP,MAAM,EAAE,IAAI,MAAM;KACnB,CAAC;YAEG,OAAO;AACd,QAAI,OAAO,QAAS;IACpB,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,SAAK,UAAU;KAAE,OAAO;KAAS,MAAM,EAAE,SAAS,cAAc;KAAE,CAAC;;MAEnE;AAEJ,SAAO,EAAE,OAAO;;CAGlB,MAAM,UAAU,OAAwE;AACtF,MAAI,KAAK,WAAW;AAClB,QAAK,UAAU,OAAO;AACtB,QAAK,YAAY;AACjB,UAAO,EAAE,IAAI,MAAM;;AAErB,SAAO,EAAE,IAAI,OAAO;;CAGtB,MAAM,YAAY,MAG0B;AAC1C,MAAI,CAAC,KAAK,MACR,QAAO,EAAE,UAAU,EAAE,EAAE;AAEzB,MAAI;GACF,MAAM,SAAS,MAAM,KAAK,MAAM,kBAAkB,KAAK,WAAW;AAClE,OAAI,CAAC,OACH,QAAO,EAAE,UAAU,EAAE,EAAE;AAEzB,UAAO,EACL,UAAU,wBAAwB,OAAO,UAAU,EAAE,OAAO,KAAK,OAAO,CAAC,EAC1E;WACM,OAAO;GACd,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,OAAI,KAAK;IAAE,KAAK;IAAO;IAAc,EAAE,gCAAgC,eAAe;AACtF,UAAO,EAAE,UAAU,EAAE,EAAE;;;CAI3B,MAAM,eAA0C;AAC9C,SAAO,EAAE;;CAGX,MAAM,eAAe,aAA2C;EAE9D,MAAM,cADS,YACW,CAAC,QAAQ,UAAU;AAE7C,SAAO,EAAE,QADK,OAAO,gBAAgB,WAAW,cAAc,aAAa,YAClD,KAAA,GAAW;;CAGtC,MAAM,aAAwC;EAC5C,MAAM,UAA4B,EAAE;AACpC,OAAK,MAAM,YAAY,iBAAiB,CACtC,MAAK,MAAM,SAAS,oBAAoB,SAAS,CAC/C,SAAQ,KAAK;GACX,IAAI,MAAM;GACV,MAAM,MAAM,QAAQ,MAAM;GAC1B;GACD,CAAC;AAGN,SAAO;;CAGT,MAAM,aAAa,aAAoC;AAErD,OAAK,MAAM;AACX,OAAK,MAAM,IAAI,YAAY;AAC3B,OAAK,OAAO;;CAGd,MAAM,aACJ,aACA,QACe;CAIjB,kBAAgC;AAC9B,GAAM,YAAY;AAChB,UAAO,KAAK,QACV,KAAI;IACF,MAAM,MAAM,MAAM,KAAK,IAAI,iBAAiB;AAC5C,QAAI,MAAM;KAAE,SAAS,IAAI;KAAS,QAAQ,IAAI;KAAS,EAAE,mBAAmB;YACrE,OAAO;AACd,QAAI,iBAAiB,wBAAyB;IAC9C,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,QAAI,KAAK;KAAE,KAAK;KAAO;KAAc,EAAE,8BAA8B,eAAe;AACpF,UAAM,IAAI,SAAS,YAAY,WAAW,SAAS,IAAK,CAAC;;MAG3D"}
@@ -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({ key: opts.sessionKey });
132
+ const params = new URLSearchParams();
125
133
  if (opts.limit) params.set("limit", String(opts.limit));
126
- const res = await gatewayFetch(this.baseUrl, `/api/sessions/${encodeURIComponent(opts.sessionKey)}/messages?${params}`, this.token);
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()).payload?.sessions ?? [];
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
- return (await res.json()).payload ?? {};
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
- this.append(new AssistantMessageComponent(text));
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.append(component);
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(new AssistantMessageComponent(text));\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): 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 this.append(component);\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;CACpE,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;;CAI3D,OAAe,WAA4B;AACzC,OAAK,SAAS,UAAU;AACxB,OAAK,eAAe;;CAGtB,WAAiB;AACf,OAAK,OAAO;AACZ,OAAK,SAAS,OAAO;AACrB,OAAK,cAAc,OAAO;;CAG5B,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,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;;AAEF,OAAK,OAAO,IAAI,0BAA0B,KAAK,CAAC;;CAGlD,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,MAAqB;EACnE,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;AACxC,OAAK,OAAO,UAAU;;CAGxB,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
+ {"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":"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"}
@@ -7,6 +7,7 @@ export declare class CustomEditor extends Editor {
7
7
  onCtrlC?: () => void;
8
8
  onCtrlD?: () => void;
9
9
  onCtrlL?: () => void;
10
+ onCtrlP?: () => void;
10
11
  onCtrlO?: () => void;
11
12
  onCtrlT?: () => void;
12
13
  handleInput(data: string): void;
@@ -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")) && this.onCtrlC) {
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')) && this.onCtrlC) {\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;CAEA,YAAY,MAAoB;AAC9B,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;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,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"}
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"}