@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
@@ -1,233 +1,32 @@
1
+ import { appendHistoryToChatLog } from "./chat-history.js";
1
2
  import { StreamAssembler } from "./stream-assembler.js";
2
3
  import { editorTheme, theme } from "./theme.js";
4
+ import { clearPendingToolCallIds, dispatchAgentSSE } from "./tui-agent-events.js";
5
+ import { createTuiCommandHandler, getSlashCommands } from "./tui-commands.js";
6
+ import { createBackspaceDeduper, drainAndStopTuiSafely, isIgnorableTuiStopError, resolveCtrlCAction, stopTuiSafely } from "./tui-lifecycle.js";
7
+ import { createLocalShellRunner } from "./tui-local-shell.js";
8
+ import { createOverlayHandlers } from "./tui-overlays.js";
9
+ import { openModelPickerOverlay, openSessionPickerOverlay } from "./tui-picker-overlay.js";
10
+ import { installTuiStdioFilter } from "./tui-stdio-filter.js";
11
+ import { createEditorSubmitHandler, createSubmitBurstCoalescer, shouldEnableWindowsGitBashPasteFallback } from "./tui-submit.js";
12
+ import { withTuiSuspended } from "./tui-suspend.js";
3
13
  import { createInitialState } from "./tui-types.js";
4
14
  import { EmbeddedBackend } from "./backends/embedded-backend.js";
5
15
  import { GatewaySseBackend } from "./backends/gateway-sse-backend.js";
6
16
  import { ChatLog } from "./components/chat-log.js";
7
17
  import { CustomEditor } from "./components/custom-editor.js";
8
- import { CombinedAutocompleteProvider, Container, Loader, ProcessTerminal, TUI, Text } from "@mariozechner/pi-tui";
18
+ import { CombinedAutocompleteProvider, Container, Key, Loader, ProcessTerminal, TUI, Text, getKeybindings, isKeyRelease, matchesKey, parseKey } from "@mariozechner/pi-tui";
9
19
  //#region src/tui/tui.ts
10
- function getSlashCommands(_isLocal) {
11
- return [
12
- {
13
- name: "help",
14
- description: "Show available commands"
15
- },
16
- {
17
- name: "abort",
18
- description: "Abort active run (or press Escape)"
19
- },
20
- {
21
- name: "tools",
22
- description: "Toggle tool output expanded/collapsed (or Ctrl+O)"
23
- },
24
- {
25
- name: "thinking",
26
- description: "Toggle thinking display (or Ctrl+T)"
27
- },
28
- {
29
- name: "exit",
30
- description: "Exit the TUI"
31
- },
32
- {
33
- name: "models",
34
- description: "List available models"
35
- },
36
- {
37
- name: "switch",
38
- description: "Switch model (e.g. /switch openai/gpt-4o)"
39
- },
40
- {
41
- name: "usage",
42
- description: "Show token usage statistics"
43
- },
44
- {
45
- name: "new",
46
- description: "Start a new session"
47
- },
48
- {
49
- name: "clear",
50
- description: "Clear current session"
51
- },
52
- {
53
- name: "list",
54
- description: "List sessions"
55
- },
56
- {
57
- name: "compact",
58
- description: "Compact session history"
59
- },
60
- {
61
- name: "think",
62
- description: "Set thinking level (e.g. /think high)"
63
- },
64
- {
65
- name: "reasoning",
66
- description: "Set reasoning visibility (e.g. /reasoning stream)"
67
- },
68
- {
69
- name: "verbose",
70
- description: "Toggle verbose mode"
71
- },
72
- {
73
- name: "status",
74
- description: "Show agent status"
75
- },
76
- {
77
- name: "config",
78
- description: "Show or update configuration"
79
- },
80
- {
81
- name: "context",
82
- description: "Show context budget"
83
- },
84
- {
85
- name: "btw",
86
- description: "Side question without saving to session"
87
- },
88
- {
89
- name: "export",
90
- description: "Export session (markdown/html/json)"
91
- },
92
- {
93
- name: "settings",
94
- description: "Show current settings"
95
- },
96
- {
97
- name: "start",
98
- description: "Show welcome message"
99
- }
100
- ];
101
- }
102
- function helpText(isLocal) {
103
- const commands = getSlashCommands(isLocal);
104
- const lines = ["Available commands:"];
105
- for (const c of commands) lines.push(` /${c.name} — ${c.description}`);
106
- lines.push("", "Keyboard shortcuts:");
107
- lines.push(" Escape — Abort active run");
108
- lines.push(" Ctrl+O — Toggle tool output");
109
- lines.push(" Ctrl+T — Toggle thinking display");
110
- lines.push(" Ctrl+C — Clear input / exit");
111
- lines.push(" Ctrl+D — Exit");
112
- return lines.join("\n");
113
- }
114
- function resolveCtrlCAction(hasInput, now, lastCtrlCAt) {
115
- if (hasInput) return {
116
- action: "clear",
117
- nextLastCtrlCAt: now
118
- };
119
- if (now - lastCtrlCAt <= 1e3) return {
120
- action: "exit",
121
- nextLastCtrlCAt: lastCtrlCAt
122
- };
123
- return {
124
- action: "warn",
125
- nextLastCtrlCAt: now
126
- };
127
- }
128
- const pendingToolCallIds = /* @__PURE__ */ new Map();
129
- function dispatchAgentSSE(event, data, state, chatLog, assembler, tui, setActivityStatus) {
130
- const runId = state.activeRunId ?? "default";
131
- switch (event) {
132
- case "status":
133
- state.activeRunId = typeof data.runId === "string" ? data.runId : runId;
134
- setActivityStatus("waiting");
135
- break;
136
- case "token": {
137
- const content = typeof data.content === "string" ? data.content : typeof data.delta === "string" ? data.delta : typeof data.text === "string" ? data.text : "";
138
- if (!content) break;
139
- setActivityStatus("streaming");
140
- const display = assembler.ingestToken(runId, content, state.showThinking);
141
- if (display !== null) {
142
- chatLog.updateAssistant(display, runId);
143
- tui.requestRender();
144
- }
145
- break;
146
- }
147
- case "thinking": {
148
- const thinkContent = String(data.content ?? "");
149
- const isDelta = Boolean(data.delta);
150
- if (data.status === "started") break;
151
- setActivityStatus("streaming");
152
- const display = assembler.ingestThinking(runId, thinkContent, isDelta, state.showThinking);
153
- if (display !== null) {
154
- chatLog.updateAssistant(display, runId);
155
- tui.requestRender();
156
- }
157
- break;
158
- }
159
- case "thinking_end":
160
- case "message_end": break;
161
- case "tool_start": {
162
- const toolName = String(data.toolName ?? "unknown");
163
- const toolCallId = String(data.toolCallId || crypto.randomUUID());
164
- const stack = pendingToolCallIds.get(toolName) ?? [];
165
- stack.push(toolCallId);
166
- pendingToolCallIds.set(toolName, stack);
167
- setActivityStatus("running");
168
- chatLog.startTool(toolCallId, toolName, data.args);
169
- tui.requestRender();
170
- break;
171
- }
172
- case "tool_end": {
173
- const toolName = String(data.toolName ?? "");
174
- let toolCallId = typeof data.toolCallId === "string" && data.toolCallId ? data.toolCallId : "";
175
- if (!toolCallId && toolName) {
176
- const stack = pendingToolCallIds.get(toolName);
177
- if (stack && stack.length > 0) {
178
- toolCallId = stack.shift();
179
- if (stack.length === 0) pendingToolCallIds.delete(toolName);
180
- }
181
- }
182
- const resultText = String(data.result ?? "");
183
- const isError = Boolean(data.isError);
184
- if (toolCallId) chatLog.updateToolResult(toolCallId, resultText, isError);
185
- setActivityStatus("streaming");
186
- tui.requestRender();
187
- break;
188
- }
189
- case "error": {
190
- const errorContent = String(data.content ?? "Unknown error");
191
- const finalText = assembler.finalize(runId, state.showThinking);
192
- if (finalText) chatLog.finalizeAssistant(finalText, runId);
193
- chatLog.addSystem(`❌ ${errorContent}`);
194
- state.activeRunId = null;
195
- setActivityStatus("idle");
196
- tui.requestRender();
197
- break;
198
- }
199
- case "result": {
200
- const finalText = assembler.finalize(runId, state.showThinking);
201
- if (finalText) chatLog.finalizeAssistant(finalText, runId);
202
- state.activeRunId = null;
203
- setActivityStatus("idle");
204
- tui.requestRender();
205
- break;
206
- }
207
- case "progress":
208
- setActivityStatus("running");
209
- break;
210
- default: break;
211
- }
20
+ function matchesCtrlCSequence(data) {
21
+ if (isKeyRelease(data)) return false;
22
+ if (data === "") return true;
23
+ if (parseKey(data) === "ctrl+c") return true;
24
+ const kb = getKeybindings();
25
+ return matchesKey(data, Key.ctrl("c")) || kb.matches(data, "tui.input.copy");
212
26
  }
213
27
  async function runTui(opts) {
214
- const originalStdoutWrite = process.stdout.write.bind(process.stdout);
215
- const originalStderrWrite = process.stderr.write.bind(process.stderr);
216
- let suppressLogs = true;
217
- const logFilter = (original) => {
218
- return function filteredWrite(chunk, ...rest) {
219
- if (!suppressLogs) return original(chunk, ...rest);
220
- if ((typeof chunk === "string" ? chunk : chunk instanceof Buffer ? chunk.toString() : "").startsWith("{\"level\":")) return true;
221
- return original(chunk, ...rest);
222
- };
223
- };
224
- process.stdout.write = logFilter(originalStdoutWrite);
225
- process.stderr.write = logFilter(originalStderrWrite);
226
- const restoreStdio = () => {
227
- suppressLogs = false;
228
- process.stdout.write = originalStdoutWrite;
229
- process.stderr.write = originalStderrWrite;
230
- };
28
+ const stdioFilter = installTuiStdioFilter();
29
+ const restoreStdio = () => stdioFilter.restore();
231
30
  const isLocalMode = opts.local === true;
232
31
  const state = createInitialState(opts.session ?? "cli:tui");
233
32
  const assembler = new StreamAssembler();
@@ -236,6 +35,12 @@ async function runTui(opts) {
236
35
  token: opts.token
237
36
  });
238
37
  const tui = new TUI(new ProcessTerminal());
38
+ const dedupeBackspace = createBackspaceDeduper();
39
+ tui.addInputListener((data) => {
40
+ const next = dedupeBackspace(data);
41
+ if (next.length === 0) return { consume: true };
42
+ return { data: next };
43
+ });
239
44
  const header = new Text("", 1, 0);
240
45
  const statusContainer = new Container();
241
46
  const footer = new Text("", 1, 0);
@@ -249,6 +54,7 @@ async function runTui(opts) {
249
54
  root.addChild(editor);
250
55
  tui.addChild(root);
251
56
  tui.setFocus(editor);
57
+ const { openOverlay, closeOverlay } = createOverlayHandlers(tui, editor);
252
58
  const slashCommands = getSlashCommands(isLocalMode);
253
59
  editor.setAutocompleteProvider(new CombinedAutocompleteProvider(slashCommands.map((c) => ({
254
60
  name: c.name,
@@ -265,6 +71,11 @@ async function runTui(opts) {
265
71
  "streaming",
266
72
  "running"
267
73
  ]);
74
+ let lastStreamActivityAt = Date.now();
75
+ let streamWatchdogId = null;
76
+ const touchStreamingActivity = () => {
77
+ lastStreamActivityAt = Date.now();
78
+ };
268
79
  const formatElapsed = (startMs) => {
269
80
  const totalSeconds = Math.max(0, Math.floor((Date.now() - startMs) / 1e3));
270
81
  if (totalSeconds < 60) return `${totalSeconds}s`;
@@ -335,6 +146,38 @@ async function runTui(opts) {
335
146
  tui.requestRender();
336
147
  } catch {}
337
148
  };
149
+ let finishTui = null;
150
+ let exitResult = { exitReason: "exit" };
151
+ const requestExit = () => {
152
+ if (state.exitRequested) return;
153
+ state.exitRequested = true;
154
+ if (elapsedTimerId) {
155
+ clearInterval(elapsedTimerId);
156
+ elapsedTimerId = null;
157
+ }
158
+ if (streamWatchdogId) {
159
+ clearInterval(streamWatchdogId);
160
+ streamWatchdogId = null;
161
+ }
162
+ client.stop();
163
+ drainAndStopTuiSafely(tui).then(() => {
164
+ restoreStdio();
165
+ finishTui?.();
166
+ });
167
+ };
168
+ const abortActive = async () => {
169
+ if (!state.activeRunId) return;
170
+ const runId = state.activeRunId;
171
+ state.activeRunId = null;
172
+ assembler.drop(runId);
173
+ chatLog.dropAssistant(runId);
174
+ setActivityStatus("idle");
175
+ tui.requestRender();
176
+ await client.abortChat({
177
+ sessionKey: state.currentSessionKey,
178
+ runId
179
+ }).catch(() => {});
180
+ };
338
181
  const sendMessage = (text) => {
339
182
  if (state.activeRunId) {
340
183
  chatLog.addSystem("A response is still in progress. Use /abort or press Escape to cancel.");
@@ -343,6 +186,7 @@ async function runTui(opts) {
343
186
  }
344
187
  chatLog.addUser(text);
345
188
  setActivityStatus("sending");
189
+ touchStreamingActivity();
346
190
  tui.requestRender();
347
191
  client.sendChat({
348
192
  sessionKey: state.currentSessionKey,
@@ -355,79 +199,78 @@ async function runTui(opts) {
355
199
  tui.requestRender();
356
200
  });
357
201
  };
358
- const abortActive = async () => {
359
- if (!state.activeRunId) return;
360
- const runId = state.activeRunId;
361
- state.activeRunId = null;
362
- assembler.drop(runId);
363
- chatLog.dropAssistant(runId);
364
- setActivityStatus("idle");
365
- tui.requestRender();
366
- await client.abortChat({
367
- sessionKey: state.currentSessionKey,
368
- runId
369
- }).catch(() => {});
370
- };
371
- const handleCommand = (input) => {
372
- const [commandName] = input.replace(/^\//, "").trim().split(/\s+/);
373
- const normalizedCommand = (commandName ?? "").toLowerCase();
374
- switch (normalizedCommand) {
375
- case "help":
376
- chatLog.addSystem(helpText(isLocalMode));
377
- tui.requestRender();
378
- return;
379
- case "exit":
380
- case "quit":
381
- requestExit();
382
- return;
383
- case "abort":
384
- case "stop":
385
- case "cancel":
386
- abortActive().then(() => {
387
- chatLog.addSystem("Aborted.");
388
- tui.requestRender();
389
- });
390
- return;
391
- case "tools":
392
- state.toolsExpanded = !state.toolsExpanded;
393
- chatLog.setToolsExpanded(state.toolsExpanded);
394
- chatLog.addSystem(`Tools: ${state.toolsExpanded ? "expanded" : "collapsed"}`);
395
- tui.requestRender();
396
- return;
397
- case "thinking":
398
- state.showThinking = !state.showThinking;
399
- chatLog.addSystem(`Thinking display: ${state.showThinking ? "on" : "off"}`);
400
- updateFooter();
401
- tui.requestRender();
402
- return;
403
- default: break;
202
+ const handleCommand = createTuiCommandHandler({
203
+ state,
204
+ chatLog,
205
+ tui,
206
+ assembler,
207
+ isLocalMode,
208
+ abortActive,
209
+ sendMessage,
210
+ requestExit,
211
+ updateFooter
212
+ });
213
+ const { runLocalShellLine } = createLocalShellRunner({
214
+ chatLog,
215
+ tui,
216
+ editor,
217
+ openOverlay,
218
+ closeOverlay,
219
+ pauseStdioFilter: () => stdioFilter.pause(),
220
+ resumeStdioFilter: () => stdioFilter.resume(),
221
+ runWithInheritedStdio: async (work) => {
222
+ await withTuiSuspended(tui, work);
404
223
  }
405
- switch (normalizedCommand) {
406
- case "new":
407
- case "reset":
408
- case "restart":
409
- case "clear":
410
- abortActive().then(() => {
411
- assembler.clear();
412
- chatLog.clearAll();
413
- tui.requestRender();
414
- sendMessage(input);
415
- });
416
- return;
417
- default: break;
224
+ });
225
+ editor.onSubmit = createSubmitBurstCoalescer({
226
+ submit: createEditorSubmitHandler({
227
+ editor,
228
+ handleCommand,
229
+ sendMessage,
230
+ handleBangLine: runLocalShellLine
231
+ }),
232
+ enabled: shouldEnableWindowsGitBashPasteFallback()
233
+ });
234
+ const setSessionKey = (key) => {
235
+ state.currentSessionKey = key;
236
+ };
237
+ const clearChatForSessionSwitch = () => {
238
+ assembler.clear();
239
+ chatLog.clearAll();
240
+ clearPendingToolCallIds();
241
+ state.historyLoaded = false;
242
+ };
243
+ const loadSessionHistory = async () => {
244
+ try {
245
+ const { messages } = await client.loadHistory({
246
+ sessionKey: state.currentSessionKey,
247
+ limit: 200
248
+ });
249
+ appendHistoryToChatLog(chatLog, messages, state.toolsExpanded);
250
+ } catch {} finally {
251
+ state.historyLoaded = true;
252
+ tui.requestRender();
418
253
  }
419
- sendMessage(input);
420
254
  };
421
- editor.onSubmit = (text) => {
422
- const value = text.trim();
423
- editor.setText("");
424
- if (!value) return;
425
- editor.addToHistory(value);
426
- if (value.startsWith("/")) handleCommand(value);
427
- else sendMessage(value);
255
+ const pickerSvc = {
256
+ tui,
257
+ editor,
258
+ openOverlay,
259
+ closeOverlay,
260
+ chatLog,
261
+ client,
262
+ sendMessage,
263
+ refreshSessionInfo,
264
+ updateHeader,
265
+ state,
266
+ setSessionKey,
267
+ clearChatForSessionSwitch,
268
+ loadSessionHistory
428
269
  };
429
270
  editor.onEscape = () => void abortActive();
430
271
  editor.onCtrlD = () => requestExit();
272
+ editor.onCtrlL = () => void openModelPickerOverlay(pickerSvc);
273
+ editor.onCtrlP = () => void openSessionPickerOverlay(pickerSvc);
431
274
  editor.onCtrlO = () => {
432
275
  state.toolsExpanded = !state.toolsExpanded;
433
276
  chatLog.setToolsExpanded(state.toolsExpanded);
@@ -441,44 +284,54 @@ async function runTui(opts) {
441
284
  };
442
285
  const handleCtrlC = () => {
443
286
  const now = Date.now();
444
- const decision = resolveCtrlCAction(editor.getText().trim().length > 0, now, state.lastCtrlCAt);
287
+ const decision = resolveCtrlCAction({
288
+ hasInput: editor.getText().trim().length > 0,
289
+ now,
290
+ lastCtrlCAt: state.lastCtrlCAt
291
+ });
445
292
  state.lastCtrlCAt = decision.nextLastCtrlCAt;
446
293
  if (decision.action === "clear") {
447
294
  editor.setText("");
448
295
  setActivityStatus("cleared input; press ctrl+c again to exit");
449
296
  tui.requestRender();
450
- } else if (decision.action === "exit") requestExit();
451
- else {
452
- setActivityStatus("press ctrl+c again to exit");
453
- tui.requestRender();
297
+ return;
454
298
  }
455
- };
456
- editor.onCtrlC = handleCtrlC;
457
- let finishTui = null;
458
- let exitResult = { exitReason: "exit" };
459
- const requestExit = () => {
460
- if (state.exitRequested) return;
461
- state.exitRequested = true;
462
- if (elapsedTimerId) {
463
- clearInterval(elapsedTimerId);
464
- elapsedTimerId = null;
299
+ if (decision.action === "exit") {
300
+ requestExit();
301
+ return;
465
302
  }
466
- client.stop();
467
- try {
468
- tui.stop();
469
- } catch {}
470
- restoreStdio();
471
- finishTui?.();
303
+ setActivityStatus("press ctrl+c again to exit");
304
+ tui.requestRender();
472
305
  };
306
+ editor.onCtrlC = handleCtrlC;
307
+ tui.addInputListener((data) => {
308
+ if (!matchesCtrlCSequence(data)) return void 0;
309
+ handleCtrlC();
310
+ return { consume: true };
311
+ });
312
+ streamWatchdogId = setInterval(() => {
313
+ if (!state.activeRunId) return;
314
+ if (!busyStates.has(state.activityStatus)) return;
315
+ if (Date.now() - lastStreamActivityAt < 3e4) return;
316
+ const rid = state.activeRunId;
317
+ const finalText = assembler.finalize(rid, state.showThinking);
318
+ if (finalText) chatLog.finalizeAssistant(finalText, rid);
319
+ chatLog.addSystem("⚠️ No stream activity for 30s; UI reset (connection may have stalled). Retry or check gateway.");
320
+ state.activeRunId = null;
321
+ setActivityStatus("idle");
322
+ tui.requestRender();
323
+ }, 5e3);
473
324
  client.onEvent = (evt) => {
474
325
  const data = evt.data ?? {};
475
- dispatchAgentSSE(evt.event, data, state, chatLog, assembler, tui, setActivityStatus);
326
+ dispatchAgentSSE(evt.event, data, state, chatLog, assembler, tui, setActivityStatus, touchStreamingActivity);
476
327
  };
477
328
  client.onConnected = () => {
478
329
  state.isConnected = true;
479
330
  setConnectionStatus(isLocalMode ? "local ready" : "gateway connected");
331
+ touchStreamingActivity();
480
332
  (async () => {
481
333
  await refreshSessionInfo();
334
+ await loadSessionHistory();
482
335
  updateHeader();
483
336
  updateFooter();
484
337
  tui.requestRender();
@@ -491,16 +344,22 @@ async function runTui(opts) {
491
344
  client.onDisconnected = (reason) => {
492
345
  const wasConnected = state.isConnected;
493
346
  state.isConnected = false;
347
+ touchStreamingActivity();
494
348
  if (isLocalMode) setConnectionStatus(`local stopped: ${reason}`);
495
349
  else {
496
- setConnectionStatus(`disconnected: ${reason}`);
350
+ setConnectionStatus(`disconnected${wasConnected || state.historyLoaded ? ` (${reason}). Reconnecting broadcast stream…` : `. Ensure gateway is running (xopc gateway) or use --local.`}`);
497
351
  if (!wasConnected && !state.historyLoaded) {
498
352
  const gatewayUrl = opts.url ?? "http://localhost:3120";
499
- chatLog.addSystem(`Cannot reach gateway at ${gatewayUrl}.\nMake sure the gateway is running (xopc gateway), or use --local for embedded mode.`);
353
+ chatLog.addSystem(`Cannot reach gateway at ${gatewayUrl}.\nStart the gateway (\`xopc gateway\`) or run \`xopc tui --local\` for embedded mode.`);
500
354
  }
501
355
  }
502
356
  tui.requestRender();
503
357
  };
358
+ client.onGap = (info) => {
359
+ chatLog.addSystem(`⚠️ Event gap: expected ${info.expected}, received ${info.received}. Some updates may be missing.`);
360
+ setConnectionStatus(`event gap: expected ${info.expected}, got ${info.received}`);
361
+ tui.requestRender();
362
+ };
504
363
  const sigintHandler = () => handleCtrlC();
505
364
  const sigtermHandler = () => requestExit();
506
365
  process.on("SIGINT", sigintHandler);
@@ -514,6 +373,10 @@ async function runTui(opts) {
514
373
  finishTui = () => {
515
374
  process.removeListener("SIGINT", sigintHandler);
516
375
  process.removeListener("SIGTERM", sigtermHandler);
376
+ if (streamWatchdogId) {
377
+ clearInterval(streamWatchdogId);
378
+ streamWatchdogId = null;
379
+ }
517
380
  finishTui = null;
518
381
  resolve();
519
382
  };
@@ -521,6 +384,6 @@ async function runTui(opts) {
521
384
  return exitResult;
522
385
  }
523
386
  //#endregion
524
- export { runTui };
387
+ export { createBackspaceDeduper, drainAndStopTuiSafely, isIgnorableTuiStopError, resolveCtrlCAction, runTui, stopTuiSafely, withTuiSuspended };
525
388
 
526
389
  //# sourceMappingURL=tui.js.map