@agi-fans/dsh-tui 0.1.0

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/lib/autocomplete.d.ts +80 -0
  4. package/lib/autocomplete.js +320 -0
  5. package/lib/autocomplete.js.map +1 -0
  6. package/lib/box.d.ts +79 -0
  7. package/lib/box.js +276 -0
  8. package/lib/box.js.map +1 -0
  9. package/lib/clipboard.d.ts +19 -0
  10. package/lib/clipboard.js +77 -0
  11. package/lib/clipboard.js.map +1 -0
  12. package/lib/command-integrations.d.ts +10 -0
  13. package/lib/command-integrations.js +49 -0
  14. package/lib/command-integrations.js.map +1 -0
  15. package/lib/command-mode.d.ts +5 -0
  16. package/lib/command-mode.js +73 -0
  17. package/lib/command-mode.js.map +1 -0
  18. package/lib/command-model.d.ts +5 -0
  19. package/lib/command-model.js +87 -0
  20. package/lib/command-model.js.map +1 -0
  21. package/lib/command-output.d.ts +6 -0
  22. package/lib/command-output.js +33 -0
  23. package/lib/command-output.js.map +1 -0
  24. package/lib/command-queue.d.ts +5 -0
  25. package/lib/command-queue.js +62 -0
  26. package/lib/command-queue.js.map +1 -0
  27. package/lib/command-registration.d.ts +5 -0
  28. package/lib/command-registration.js +20 -0
  29. package/lib/command-registration.js.map +1 -0
  30. package/lib/command-session.d.ts +5 -0
  31. package/lib/command-session.js +153 -0
  32. package/lib/command-session.js.map +1 -0
  33. package/lib/command-transcript.d.ts +5 -0
  34. package/lib/command-transcript.js +85 -0
  35. package/lib/command-transcript.js.map +1 -0
  36. package/lib/copy-selector.d.ts +52 -0
  37. package/lib/copy-selector.js +136 -0
  38. package/lib/copy-selector.js.map +1 -0
  39. package/lib/copy-targets.d.ts +34 -0
  40. package/lib/copy-targets.js +170 -0
  41. package/lib/copy-targets.js.map +1 -0
  42. package/lib/definition.d.ts +171 -0
  43. package/lib/definition.js +14 -0
  44. package/lib/definition.js.map +1 -0
  45. package/lib/editor.d.ts +66 -0
  46. package/lib/editor.js +365 -0
  47. package/lib/editor.js.map +1 -0
  48. package/lib/event-views.d.ts +188 -0
  49. package/lib/event-views.js +678 -0
  50. package/lib/event-views.js.map +1 -0
  51. package/lib/external-editor.d.ts +2 -0
  52. package/lib/external-editor.js +31 -0
  53. package/lib/external-editor.js.map +1 -0
  54. package/lib/history-search.d.ts +61 -0
  55. package/lib/history-search.js +239 -0
  56. package/lib/history-search.js.map +1 -0
  57. package/lib/history-store.d.ts +8 -0
  58. package/lib/history-store.js +36 -0
  59. package/lib/history-store.js.map +1 -0
  60. package/lib/hotkeys.d.ts +14 -0
  61. package/lib/hotkeys.js +115 -0
  62. package/lib/hotkeys.js.map +1 -0
  63. package/lib/human-interaction.d.ts +5 -0
  64. package/lib/human-interaction.js +8 -0
  65. package/lib/human-interaction.js.map +1 -0
  66. package/lib/image-paste.d.ts +21 -0
  67. package/lib/image-paste.js +277 -0
  68. package/lib/image-paste.js.map +1 -0
  69. package/lib/index.d.ts +10 -0
  70. package/lib/index.js +11 -0
  71. package/lib/index.js.map +1 -0
  72. package/lib/interaction-adapter.d.ts +18 -0
  73. package/lib/interaction-adapter.js +93 -0
  74. package/lib/interaction-adapter.js.map +1 -0
  75. package/lib/keybindings-config.d.ts +5 -0
  76. package/lib/keybindings-config.js +28 -0
  77. package/lib/keybindings-config.js.map +1 -0
  78. package/lib/keys.d.ts +59 -0
  79. package/lib/keys.js +263 -0
  80. package/lib/keys.js.map +1 -0
  81. package/lib/markdown.d.ts +13 -0
  82. package/lib/markdown.js +453 -0
  83. package/lib/markdown.js.map +1 -0
  84. package/lib/path-complete.d.ts +85 -0
  85. package/lib/path-complete.js +231 -0
  86. package/lib/path-complete.js.map +1 -0
  87. package/lib/path-mentions.d.ts +4 -0
  88. package/lib/path-mentions.js +50 -0
  89. package/lib/path-mentions.js.map +1 -0
  90. package/lib/project-context.d.ts +13 -0
  91. package/lib/project-context.js +52 -0
  92. package/lib/project-context.js.map +1 -0
  93. package/lib/project-file-search.d.ts +25 -0
  94. package/lib/project-file-search.js +207 -0
  95. package/lib/project-file-search.js.map +1 -0
  96. package/lib/prompt-selector.d.ts +56 -0
  97. package/lib/prompt-selector.js +297 -0
  98. package/lib/prompt-selector.js.map +1 -0
  99. package/lib/provider-local.d.ts +115 -0
  100. package/lib/provider-local.js +1760 -0
  101. package/lib/provider-local.js.map +1 -0
  102. package/lib/pwd.d.ts +13 -0
  103. package/lib/pwd.js +14 -0
  104. package/lib/pwd.js.map +1 -0
  105. package/lib/relative-time.d.ts +2 -0
  106. package/lib/relative-time.js +11 -0
  107. package/lib/relative-time.js.map +1 -0
  108. package/lib/renderer.d.ts +112 -0
  109. package/lib/renderer.js +181 -0
  110. package/lib/renderer.js.map +1 -0
  111. package/lib/runner.d.ts +9 -0
  112. package/lib/runner.js +94 -0
  113. package/lib/runner.js.map +1 -0
  114. package/lib/session-controller.d.ts +92 -0
  115. package/lib/session-controller.js +571 -0
  116. package/lib/session-controller.js.map +1 -0
  117. package/lib/session-runtime.d.ts +16 -0
  118. package/lib/session-runtime.js +17 -0
  119. package/lib/session-runtime.js.map +1 -0
  120. package/lib/settings-list.d.ts +67 -0
  121. package/lib/settings-list.js +380 -0
  122. package/lib/settings-list.js.map +1 -0
  123. package/lib/status-config.d.ts +32 -0
  124. package/lib/status-config.js +52 -0
  125. package/lib/status-config.js.map +1 -0
  126. package/lib/status-line.d.ts +41 -0
  127. package/lib/status-line.js +351 -0
  128. package/lib/status-line.js.map +1 -0
  129. package/lib/style.d.ts +22 -0
  130. package/lib/style.js +23 -0
  131. package/lib/style.js.map +1 -0
  132. package/lib/theme.d.ts +83 -0
  133. package/lib/theme.js +317 -0
  134. package/lib/theme.js.map +1 -0
  135. package/lib/tool-presentation.d.ts +20 -0
  136. package/lib/tool-presentation.js +79 -0
  137. package/lib/tool-presentation.js.map +1 -0
  138. package/lib/tool-renderers.d.ts +21 -0
  139. package/lib/tool-renderers.js +133 -0
  140. package/lib/tool-renderers.js.map +1 -0
  141. package/lib/tools-list.d.ts +14 -0
  142. package/lib/tools-list.js +61 -0
  143. package/lib/tools-list.js.map +1 -0
  144. package/lib/transcript-export.d.ts +4 -0
  145. package/lib/transcript-export.js +66 -0
  146. package/lib/transcript-export.js.map +1 -0
  147. package/lib/tui-settings.d.ts +20 -0
  148. package/lib/tui-settings.js +23 -0
  149. package/lib/tui-settings.js.map +1 -0
  150. package/lib/welcome-tips.d.ts +10 -0
  151. package/lib/welcome-tips.js +36 -0
  152. package/lib/welcome-tips.js.map +1 -0
  153. package/lib/width.d.ts +54 -0
  154. package/lib/width.js +323 -0
  155. package/lib/width.js.map +1 -0
  156. package/package.json +119 -0
@@ -0,0 +1,1760 @@
1
+ /**
2
+ * TUI capability seam — local terminal provider.
3
+
4
+ * Owns the tty: raw-mode key input (editing, history, slash/tab
5
+ * autocomplete, /settings overlay, /copy picker, Ctrl-R history search, PgUp/PgDn
6
+ * and mouse-wheel transcript scroll, click-to-caret, Ctrl-O tool
7
+ * expand, bracketed paste, double Ctrl-C exit, Ctrl-D quit),
8
+ * SIGWINCH reflow, and the differential renderer. In non-tty mode
9
+ * (pipes, CI) it degrades to line-based input with plain append-only
10
+ * printing of settled blocks.
11
+ * @module @agi-fans/dsh-tui
12
+ */
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { createInterface } from 'node:readline';
16
+ import { TUI_SERVICE, } from "./definition.js";
17
+ import { applySlashCompletion, formatHelpText, hitTestAutocomplete, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "./autocomplete.js";
18
+ import { applyPathCompletion, defaultPathSource, findPathToken, parsePathPrefix, pathSuggestions, searchPathSuggestions, } from "./path-complete.js";
19
+ import { copyToClipboard, readFromClipboard } from "./clipboard.js";
20
+ import { applyCopySelectorEvent, createCopySelector, hitTestCopySelector, selectCopyTarget, } from "./copy-selector.js";
21
+ import { buildCopyTargets, extractCopyTarget, parseCopyKind } from "./copy-targets.js";
22
+ import { applyHistorySearchEvent, createHistorySearch, hitTestHistorySearch, } from "./history-search.js";
23
+ import { InputEditor, lineEnd, lineStart } from "./editor.js";
24
+ import { applySettingsEvent, createSettings, hitTestSettings, selectSetting, tuiSettingItems, } from "./settings-list.js";
25
+ import { applyEvent, blockLines, initialTranscript, renderView, TRANSCRIPT_FAST_SCROLL, TRANSCRIPT_WHEEL_SCROLL, } from "./event-views.js";
26
+ import { flushPending, MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseKeys } from "./keys.js";
27
+ import { LineRenderer } from "./renderer.js";
28
+ import { hitTestEditor } from "./box.js";
29
+ import { createTheme, detectTrueColor, parseThemeName } from "./theme.js";
30
+ import { formatWorkspaceText } from "./pwd.js";
31
+ import { TUI_SETTINGS_NAMESPACE, TuiSettingsSchema } from "./tui-settings.js";
32
+ import { defaultStatusBarConfig, resolveStatusBarConfig } from "./status-config.js";
33
+ import { HistoryStore } from "./history-store.js";
34
+ import { loadKeybindings } from "./keybindings-config.js";
35
+ import { editExternally } from "./external-editor.js";
36
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
37
+ import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, } from "./prompt-selector.js";
38
+ import { resolveProjectContext } from "./project-context.js";
39
+ import { pickWelcomeTips } from "./welcome-tips.js";
40
+ import { imageMarker, imagePathCandidates, probeImageDimensions, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "./image-paste.js";
41
+ const APP_NAME = 'omdsh';
42
+ const APP_VERSION = '0.1.0';
43
+ const DOUBLE_CTRL_C_MS = 500;
44
+ function shortenPath(cwd) {
45
+ const home = homedir();
46
+ if (cwd === home)
47
+ return '~';
48
+ if (cwd.startsWith(home + '/'))
49
+ return '~' + cwd.slice(home.length);
50
+ return cwd;
51
+ }
52
+ export const name = 'omdsh-tui';
53
+ /** Harness commands replaced by terminal-native interaction surfaces. */
54
+ const HIDDEN_RUNTIME_COMMANDS = new Set(['permission']);
55
+ /**
56
+ * Local terminal presentation service.
57
+ */
58
+ export class LocalTui {
59
+ #term;
60
+ #model;
61
+ #reasoningEffort;
62
+ #colors;
63
+ #themeName;
64
+ #tty;
65
+ #renderer;
66
+ #state = initialTranscript();
67
+ #editor = new InputEditor();
68
+ #history = [];
69
+ #historyIndex = 0;
70
+ #draft = '';
71
+ #ac = null;
72
+ #search = null;
73
+ #settings = null;
74
+ #copySelector = null;
75
+ #pending = null;
76
+ #queue = [];
77
+ #quitRequested = false;
78
+ #resumeHintRequested = false;
79
+ #lastSigintTime = 0;
80
+ #interrupts = new Set();
81
+ #disposed = false;
82
+ #pendingKeys = '';
83
+ #escapeTimer = null;
84
+ #autocompleteTimer = null;
85
+ #autocompleteAbort = null;
86
+ #autocompleteRequestId = 0;
87
+ #scrollRender = null;
88
+ #paste = false;
89
+ #pasteBuf = '';
90
+ #pasteInFlight = 0;
91
+ #deferredPasteEvents = [];
92
+ #images = [];
93
+ #lineReader = null;
94
+ #plainPending = null;
95
+ #plainClosed = false;
96
+ #plainPrinted = 0;
97
+ #offData = null;
98
+ #offResize = null;
99
+ #pwd;
100
+ #branch;
101
+ #spinner = 0;
102
+ #tick = null;
103
+ #scrollStart = 0;
104
+ #maxStart = 0;
105
+ #scrollBudget = 0;
106
+ #follow = true;
107
+ #focusBlock;
108
+ #expandTools = false;
109
+ #statusBar = defaultStatusBarConfig();
110
+ #toolsExpanded = false;
111
+ #expandedToolCalls = new Set();
112
+ #tools = [];
113
+ #runtimeCommands = [];
114
+ #prompt = null;
115
+ #recentSessions = [];
116
+ #welcomeTips;
117
+ #sessionId;
118
+ #sessionStats;
119
+ #sessionControls;
120
+ #editorHit = null;
121
+ #overlayHit = null;
122
+ #trueColor;
123
+ #copy;
124
+ #readClipboard;
125
+ #readClipboardImage;
126
+ #readClipboardFiles;
127
+ #readImagePath;
128
+ #historyStore;
129
+ #keybindings;
130
+ #cwd;
131
+ #projectRoot;
132
+ #home;
133
+ #listDir;
134
+ #searchFiles;
135
+ #autocompleteDebounceMs;
136
+ #persistPrefs = null;
137
+ /**
138
+ * @param term - terminal surface (injectable for tests).
139
+ * @param model - model label for the status line.
140
+ * @param colors - SGR styling switch.
141
+ * @param themeName - shipped palette.
142
+ * @param copy - clipboard writer (defaults to the platform tool).
143
+ * @param paths - cwd/home/listing used by `@` and path autocomplete.
144
+ */
145
+ constructor(term, model, colors, themeName = 'dark', copy = copyToClipboard, paths = {}) {
146
+ this.#term = term;
147
+ this.#model = model;
148
+ this.#colors = colors;
149
+ this.#themeName = themeName;
150
+ this.#copy = copy;
151
+ this.#readClipboard = paths.readClipboard ?? readFromClipboard;
152
+ this.#readClipboardImage = paths.readClipboardImage ?? readImageFromClipboard;
153
+ this.#readClipboardFiles = paths.readClipboardFiles ?? readMacClipboardFiles;
154
+ this.#historyStore = paths.historyPath === undefined ? undefined : new HistoryStore(paths.historyPath);
155
+ this.#history = this.#historyStore?.load() ?? [];
156
+ this.#keybindings = loadKeybindings(paths.keybindingsPath);
157
+ const fallback = defaultPathSource();
158
+ this.#cwd = paths.cwd ?? fallback.cwd;
159
+ this.#readImagePath = paths.readImagePath ?? (path => readImageFile(path, this.#cwd));
160
+ const project = resolveProjectContext(this.#cwd);
161
+ this.#projectRoot = paths.projectRoot ?? project.root;
162
+ this.#home = paths.home ?? fallback.home;
163
+ this.#listDir = paths.listDir ?? fallback.listDir;
164
+ this.#searchFiles = paths.searchFiles ?? fallback.searchFiles;
165
+ this.#autocompleteDebounceMs = Math.max(0, paths.autocompleteDebounceMs ?? 100);
166
+ this.#welcomeTips = pickWelcomeTips();
167
+ this.#trueColor = colors && detectTrueColor();
168
+ this.#tty = term.input.isTTY === true;
169
+ this.#pwd = shortenPath(project.root);
170
+ this.#branch = project.gitLabel;
171
+ this.#renderer = new LineRenderer({ write: (chunk) => { this.#term.output.write(chunk); } }, { synchronized: this.#tty });
172
+ if (this.#tty) {
173
+ term.input.setRawMode?.(true);
174
+ const listener = (chunk) => { this.#onData(chunk); };
175
+ term.input.on('data', listener);
176
+ this.#offData = () => { term.input.off('data', listener); };
177
+ this.#offResize = term.onResize?.(() => {
178
+ // A terminal resize invalidates both the physical cursor position and
179
+ // the screen-relative frame retained by the differential renderer.
180
+ this.#renderer.reset();
181
+ this.#term.output.write('\x1b[2J\x1b[H');
182
+ this.#render();
183
+ }) ?? null;
184
+ // Boot output above us (package-manager warnings, loader logs) has
185
+ // already scrolled the cursor off row 0; the renderer's screen-relative
186
+ // frames require a clean origin. Clear the screen and home the cursor
187
+ // before the first frame. Enable bracketed paste and SGR mouse
188
+ // tracking (wheel drives the virtual transcript; native scrollback
189
+ // is already unusable under full-screen diffs).
190
+ term.output.write('\x1b[2J\x1b[H\x1b[?2004h' + MOUSE_TRACKING_ON);
191
+ }
192
+ this.#render();
193
+ }
194
+ event(event, presentation) {
195
+ this.#state = applyEvent(this.#state, event, presentation);
196
+ this.#syncTick();
197
+ if (this.#tty) {
198
+ this.#render();
199
+ }
200
+ else if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result' || event.type === 'turn/end') {
201
+ this.#printPlain();
202
+ }
203
+ }
204
+ setStatus(status) {
205
+ this.#state = { ...this.#state, status };
206
+ this.#syncTick();
207
+ if (this.#tty)
208
+ this.#render();
209
+ }
210
+ setModel(model, reasoningEffort) {
211
+ this.#model = model;
212
+ this.#reasoningEffort = reasoningEffort;
213
+ if (this.#tty)
214
+ this.#render();
215
+ }
216
+ setTools(tools) {
217
+ this.#tools = tools.map((tool) => ({ name: tool.name, description: tool.description }));
218
+ }
219
+ setCommands(commands) {
220
+ this.#runtimeCommands = commands.map((command) => ({
221
+ name: command.name,
222
+ description: command.description,
223
+ ...(command.inputHint === undefined ? {} : { inputHint: command.inputHint }),
224
+ }));
225
+ this.#refreshAutocomplete();
226
+ if (this.#tty)
227
+ this.#render();
228
+ }
229
+ notice(text, level = 'info') {
230
+ this.#state = { ...this.#state, blocks: [...this.#state.blocks, { kind: 'notice', level, text }] };
231
+ if (this.#tty)
232
+ this.#render();
233
+ else
234
+ this.#printPlain();
235
+ }
236
+ commandOutput(command, text) {
237
+ this.#state = { ...this.#state, blocks: [...this.#state.blocks, { kind: 'commandOutput', command, text }] };
238
+ if (this.#tty)
239
+ this.#render();
240
+ else
241
+ this.#printPlain();
242
+ }
243
+ prompt(request) {
244
+ if (this.#prompt !== null)
245
+ return Promise.reject(new Error('omdsh-tui: prompt already in flight'));
246
+ if (this.#disposed || request.signal?.aborted === true)
247
+ return Promise.resolve(null);
248
+ this.#editor.setText('');
249
+ this.#ac = null;
250
+ return new Promise((resolve) => {
251
+ const selected = Math.max(0, request.options?.findIndex(option => (option.value ?? option.label) === request.initialValue) ?? 0);
252
+ const pending = { request, selected, checked: new Set(), resolve };
253
+ if (request.signal !== undefined) {
254
+ const onAbort = () => { this.#finishPrompt(null); };
255
+ request.signal.addEventListener('abort', onAbort, { once: true });
256
+ pending.offAbort = () => { request.signal?.removeEventListener('abort', onAbort); };
257
+ }
258
+ this.#prompt = pending;
259
+ if (this.#tty) {
260
+ this.#render();
261
+ }
262
+ else {
263
+ const lines = [request.question];
264
+ if (request.detail !== undefined && request.detail !== '')
265
+ lines.push('', request.detail);
266
+ if (request.options !== undefined && request.options.length > 0) {
267
+ lines.push('', ...request.options.map((option, index) => `${index + 1}. ${option.label}${option.description === undefined ? '' : ' — ' + option.description}`));
268
+ lines.push('', request.allowCustom === false
269
+ ? 'Choose a label or number.'
270
+ : request.multiSelect === true
271
+ ? 'Choose labels/numbers separated by commas, or type a custom answer.'
272
+ : 'Choose a label/number, or type a custom answer.');
273
+ }
274
+ this.notice(`${request.title}\n${lines.join('\n')}`);
275
+ }
276
+ });
277
+ }
278
+ replaceSession(events, presentations) {
279
+ let state = initialTranscript();
280
+ for (const event of events)
281
+ state = applyEvent(state, event, presentations?.get(event.seq));
282
+ this.#state = { ...state, status: 'idle' };
283
+ this.#plainPrinted = 0;
284
+ this.#followTail();
285
+ if (this.#tty)
286
+ this.#render();
287
+ else
288
+ this.#printPlain();
289
+ }
290
+ setSession(info) {
291
+ this.#sessionId = info.id;
292
+ this.#recentSessions = info.recent.map((session) => ({ ...session }));
293
+ this.#sessionStats = info.stats === undefined ? undefined : { ...info.stats };
294
+ this.#sessionControls = info.controls === undefined
295
+ ? undefined
296
+ : {
297
+ ...(info.controls.plan === undefined ? {} : { plan: { ...info.controls.plan } }),
298
+ ...(info.controls.permission === undefined ? {} : { permission: info.controls.permission }),
299
+ };
300
+ if (this.#tty)
301
+ this.#render();
302
+ }
303
+ /** Apply prefs loaded from the settings document (does not persist). */
304
+ applyStoredPrefs(prefs) {
305
+ this.#themeName = prefs.theme;
306
+ this.#colors = prefs.colors;
307
+ this.#expandTools = prefs.expandTools;
308
+ this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
309
+ this.#toolsExpanded = prefs.expandTools;
310
+ if (this.#settings !== null)
311
+ this.#settings = { ...this.#settings, prefs };
312
+ if (this.#tty)
313
+ this.#render();
314
+ }
315
+ /** Called after a live `/settings` change. */
316
+ setPrefsPersist(persist) {
317
+ this.#persistPrefs = persist;
318
+ }
319
+ readInput() {
320
+ if (this.#pending !== null)
321
+ return Promise.reject(new Error('omdsh-tui: input read already in flight'));
322
+ if (this.#disposed)
323
+ return Promise.resolve(null);
324
+ // A Ctrl-D pressed while the previous turn was still settling lands here
325
+ // (no pending readline existed to resolve); honor it now.
326
+ if (this.#quitRequested) {
327
+ this.#quitRequested = false;
328
+ return Promise.resolve(null);
329
+ }
330
+ if (!this.#tty)
331
+ return this.#readlinePlain();
332
+ // Lines submitted while a turn was still running were queued instead of
333
+ // dropped; serve the oldest before waiting for fresh input.
334
+ const queued = this.#queue.shift();
335
+ if (queued !== undefined)
336
+ return Promise.resolve(queued);
337
+ return new Promise((resolve) => {
338
+ this.#pending = { resolve };
339
+ });
340
+ }
341
+ /** Compatibility helper for provider-level text editing tests and embedders. */
342
+ async readline() {
343
+ return (await this.readInput())?.text ?? null;
344
+ }
345
+ restoreInput(submission) {
346
+ const currentText = this.#editor.text;
347
+ const currentImages = this.#images;
348
+ let rebasedCurrent = currentText;
349
+ for (let index = currentImages.length - 1; index >= 0; index -= 1) {
350
+ const image = currentImages[index];
351
+ rebasedCurrent = rebasedCurrent.replaceAll(imageMarker(index, image), imageMarker(index + submission.images.length, image));
352
+ }
353
+ const separator = submission.text !== '' && rebasedCurrent !== '' ? '\n' : '';
354
+ this.#images = [...submission.images.map(image => ({ ...image })), ...currentImages];
355
+ this.#editor.setText(submission.text + separator + rebasedCurrent);
356
+ this.#refreshAutocomplete();
357
+ if (this.#tty)
358
+ this.#render();
359
+ }
360
+ onInterrupt(listener) {
361
+ this.#interrupts.add(listener);
362
+ return () => { this.#interrupts.delete(listener); };
363
+ }
364
+ dispose() {
365
+ if (this.#disposed)
366
+ return;
367
+ this.#disposed = true;
368
+ if (this.#tick !== null) {
369
+ clearInterval(this.#tick);
370
+ this.#tick = null;
371
+ }
372
+ if (this.#scrollRender !== null) {
373
+ clearImmediate(this.#scrollRender);
374
+ this.#scrollRender = null;
375
+ }
376
+ if (this.#tty) {
377
+ this.#offData?.();
378
+ this.#offResize?.();
379
+ this.#term.input.setRawMode?.(false);
380
+ // Leave the cursor on a fresh line below the last frame so the shell
381
+ // prompt does not overwrite the transcript. Disable mouse tracking
382
+ // and bracketed paste.
383
+ this.#renderer.finish();
384
+ this.#term.output.write(MOUSE_TRACKING_OFF + '\x1b[?2004l\x1b[?25h\r\n');
385
+ if (this.#resumeHintRequested && this.#sessionId !== undefined) {
386
+ this.#term.output.write(`\r\nResume this session with ${APP_NAME} --resume ${this.#sessionId}\r\n`);
387
+ }
388
+ // A tty stdin keeps the event loop alive after the tree disposes;
389
+ // release the descriptor so natural completion can exit the process.
390
+ this.#term.input.destroy?.();
391
+ }
392
+ if (this.#escapeTimer !== null)
393
+ clearTimeout(this.#escapeTimer);
394
+ if (this.#autocompleteTimer !== null)
395
+ clearTimeout(this.#autocompleteTimer);
396
+ this.#autocompleteAbort?.abort();
397
+ this.#autocompleteTimer = null;
398
+ this.#autocompleteAbort = null;
399
+ this.#lineReader?.close();
400
+ this.#pending?.resolve(null);
401
+ this.#pending = null;
402
+ this.#finishPrompt(null);
403
+ }
404
+ /** Re-render the current frame (resize reflow). */
405
+ refresh() {
406
+ this.#render();
407
+ }
408
+ #readlinePlain() {
409
+ return new Promise((resolve) => {
410
+ if (this.#lineReader === null) {
411
+ this.#lineReader = createInterface({ input: this.#term.input });
412
+ // Permanent listeners: once() handlers would auto-pause the input
413
+ // stream after one line and miss the EOF close.
414
+ this.#lineReader.on('line', (line) => { this.#plainResolve(line); });
415
+ this.#lineReader.on('close', () => {
416
+ this.#plainClosed = true;
417
+ this.#plainResolve(null);
418
+ });
419
+ }
420
+ if (this.#plainClosed) {
421
+ resolve(null);
422
+ return;
423
+ }
424
+ this.#plainPending = { resolve };
425
+ });
426
+ }
427
+ #plainResolve(line) {
428
+ if (this.#prompt !== null && line !== null) {
429
+ const value = line.trim();
430
+ if (value === '') {
431
+ this.#finishPrompt(null);
432
+ }
433
+ else if (this.#prompt.request.allowCustom === false) {
434
+ const options = this.#prompt.request.options ?? [];
435
+ const numeric = /^\d+$/u.test(value) ? Number(value) - 1 : -1;
436
+ const option = numeric >= 0
437
+ ? options[numeric]
438
+ : options.find(item => item.label.toLowerCase() === value.toLowerCase());
439
+ this.#finishPrompt(option?.value ?? option?.label ?? null);
440
+ }
441
+ else {
442
+ this.#finishPrompt(value);
443
+ }
444
+ return;
445
+ }
446
+ if (this.#prompt !== null)
447
+ this.#finishPrompt(null);
448
+ const pending = this.#plainPending;
449
+ this.#plainPending = null;
450
+ pending?.resolve(line === null ? null : { text: line, images: [] });
451
+ }
452
+ /** Print plain-mode blocks that settled since the last flush. */
453
+ #printPlain() {
454
+ const theme = createTheme(false, false);
455
+ const width = this.#term.width();
456
+ const fresh = this.#state.blocks.slice(this.#plainPrinted);
457
+ let out = '';
458
+ for (const block of fresh) {
459
+ // Pipe / CI output is not a viewport: print the full tool body.
460
+ for (const line of blockLines(block, theme, width, 0, true))
461
+ out += line + '\n';
462
+ }
463
+ this.#plainPrinted = this.#state.blocks.length;
464
+ if (out !== '')
465
+ this.#term.output.write(out);
466
+ }
467
+ #busy() {
468
+ if (this.#state.status === 'running')
469
+ return true;
470
+ return this.#state.blocks.some((block) => block.kind === 'tool' && block.status === 'running');
471
+ }
472
+ #syncTick() {
473
+ if (!this.#tty)
474
+ return;
475
+ if (this.#busy()) {
476
+ if (this.#tick === null) {
477
+ this.#tick = setInterval(() => {
478
+ this.#spinner += 1;
479
+ this.#render();
480
+ }, 80);
481
+ }
482
+ }
483
+ else if (this.#tick !== null) {
484
+ clearInterval(this.#tick);
485
+ this.#tick = null;
486
+ }
487
+ }
488
+ #render() {
489
+ if (this.#scrollRender !== null) {
490
+ clearImmediate(this.#scrollRender);
491
+ this.#scrollRender = null;
492
+ }
493
+ const width = this.#term.width();
494
+ const frame = this.#tty
495
+ ? renderView(this.#state, {
496
+ width,
497
+ height: this.#term.height(),
498
+ model: this.#model,
499
+ ...(this.#reasoningEffort === undefined ? {} : { reasoningEffort: this.#reasoningEffort }),
500
+ input: this.#editor.text,
501
+ inputCursor: this.#editor.cursor,
502
+ inputImages: this.#images.length,
503
+ colors: this.#colors,
504
+ pwd: this.#pwd,
505
+ ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
506
+ version: APP_VERSION,
507
+ appName: APP_NAME,
508
+ spinnerFrame: this.#spinner,
509
+ trueColor: this.#trueColor,
510
+ themeName: this.#themeName,
511
+ scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
512
+ ...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock }),
513
+ toolsExpanded: this.#toolsExpanded,
514
+ expandedTools: this.#expandedToolCalls,
515
+ commands: this.#commands(),
516
+ recentSessions: this.#recentSessions,
517
+ welcomeTips: this.#welcomeTips,
518
+ ...(this.#sessionStats === undefined ? {} : { sessionStats: this.#sessionStats }),
519
+ ...(this.#sessionControls === undefined ? {} : { sessionControls: this.#sessionControls }),
520
+ statusBar: this.#statusBar,
521
+ ...(this.#prompt === null ? {} : { promptSelector: this.#prompt }),
522
+ ...(this.#settings !== null
523
+ ? { settings: this.#settings }
524
+ : this.#copySelector !== null
525
+ ? { copySelector: this.#copySelector }
526
+ : this.#search !== null
527
+ ? { historySearch: this.#search }
528
+ : this.#ac !== null ? { autocomplete: this.#ac } : {}),
529
+ })
530
+ : { lines: [] };
531
+ this.#focusBlock = undefined;
532
+ this.#editorHit = frame.editor ?? null;
533
+ this.#overlayHit = frame.overlay ?? null;
534
+ this.#syncScroll(frame.transcript);
535
+ this.#renderer.render(frame);
536
+ }
537
+ #syncScroll(scroll) {
538
+ if (scroll === undefined) {
539
+ this.#scrollStart = 0;
540
+ this.#maxStart = 0;
541
+ this.#scrollBudget = 0;
542
+ this.#follow = true;
543
+ return;
544
+ }
545
+ this.#scrollStart = scroll.start;
546
+ this.#maxStart = scroll.maxStart;
547
+ this.#scrollBudget = scroll.budget;
548
+ if (this.#follow || this.#scrollStart >= this.#maxStart) {
549
+ this.#follow = true;
550
+ this.#scrollStart = this.#maxStart;
551
+ }
552
+ }
553
+ #pageSize() {
554
+ return Math.max(1, this.#scrollBudget > 2 ? this.#scrollBudget - 2 : 1);
555
+ }
556
+ #scrollBy(delta, coalesce = false) {
557
+ if (delta === 0 && this.#maxStart === 0)
558
+ return;
559
+ this.#follow = false;
560
+ this.#scrollStart += delta;
561
+ if (this.#scrollStart <= 0)
562
+ this.#scrollStart = 0;
563
+ if (!coalesce) {
564
+ this.#render();
565
+ return;
566
+ }
567
+ if (this.#scrollRender !== null)
568
+ return;
569
+ this.#scrollRender = setImmediate(() => {
570
+ this.#scrollRender = null;
571
+ if (!this.#disposed)
572
+ this.#render();
573
+ });
574
+ }
575
+ #followTail() {
576
+ this.#follow = true;
577
+ this.#scrollStart = this.#maxStart;
578
+ }
579
+ #onData(chunk) {
580
+ const { events, rest } = parseKeys(this.#pendingKeys + chunk.toString('utf8'));
581
+ this.#pendingKeys = rest;
582
+ if (this.#escapeTimer !== null) {
583
+ clearTimeout(this.#escapeTimer);
584
+ this.#escapeTimer = null;
585
+ }
586
+ for (const event of events)
587
+ this.#dispatch(event);
588
+ if (rest === '\x1b') {
589
+ this.#escapeTimer = setTimeout(() => {
590
+ this.#pendingKeys = '';
591
+ this.#escapeTimer = null;
592
+ for (const event of flushPending(rest))
593
+ this.#dispatch(event);
594
+ }, 80);
595
+ }
596
+ else if (rest.length > 32) {
597
+ this.#pendingKeys = '';
598
+ }
599
+ }
600
+ #startAsyncPaste(operation) {
601
+ this.#pasteInFlight += 1;
602
+ void operation.catch((error) => {
603
+ this.notice('Paste failed: ' + (error instanceof Error ? error.message : String(error)), 'error');
604
+ }).finally(() => {
605
+ this.#pasteInFlight = Math.max(0, this.#pasteInFlight - 1);
606
+ if (this.#pasteInFlight !== 0 || this.#deferredPasteEvents.length === 0)
607
+ return;
608
+ const deferred = this.#deferredPasteEvents.splice(0);
609
+ for (let index = 0; index < deferred.length; index += 1) {
610
+ this.#dispatch(deferred[index]);
611
+ if (this.#pasteInFlight === 0)
612
+ continue;
613
+ this.#deferredPasteEvents.unshift(...deferred.slice(index + 1));
614
+ break;
615
+ }
616
+ });
617
+ }
618
+ async #acceptPastedText(text) {
619
+ if (this.#prompt !== null) {
620
+ this.#editor.handle({ type: 'text', value: text });
621
+ this.#render();
622
+ return;
623
+ }
624
+ if (this.#settings !== null || this.#copySelector !== null)
625
+ return;
626
+ if (this.#search !== null) {
627
+ this.#applySearch({ type: 'text', value: text });
628
+ return;
629
+ }
630
+ const paths = imagePathCandidates(text);
631
+ if (paths.length > 0) {
632
+ const images = await Promise.all(paths.map(path => this.#readImagePath(path)));
633
+ if (images.every((image) => image !== null)) {
634
+ for (const image of images)
635
+ this.#insertImageDraft(image);
636
+ this.#refreshAutocomplete();
637
+ this.#render();
638
+ return;
639
+ }
640
+ // Screenshot tools sometimes paste a transient cache path and remove
641
+ // it before Node reads it. OMP falls back to the still-live clipboard
642
+ // image instead of leaking that stale path into the prompt.
643
+ const clipboardImage = await this.#readClipboardImage();
644
+ if (clipboardImage !== null) {
645
+ this.#insertImageDraft(clipboardImage);
646
+ this.#refreshAutocomplete();
647
+ this.#render();
648
+ return;
649
+ }
650
+ }
651
+ this.#editor.handle({ type: 'text', value: text });
652
+ this.#refreshAutocomplete();
653
+ this.#render();
654
+ }
655
+ async #pasteClipboard() {
656
+ if (this.#search === null && this.#settings === null && this.#copySelector === null) {
657
+ const image = await this.#readClipboardImage();
658
+ if (image !== null) {
659
+ this.#insertImageDraft(image);
660
+ this.#refreshAutocomplete();
661
+ this.#render();
662
+ return;
663
+ }
664
+ const files = await this.#readClipboardFiles();
665
+ const imagePaths = files.filter(path => imagePathCandidates(path).length === 1);
666
+ if (imagePaths.length > 0) {
667
+ const images = (await Promise.all(imagePaths.map(path => this.#readImagePath(path))))
668
+ .filter((candidate) => candidate !== null);
669
+ if (images.length > 0) {
670
+ for (const candidate of images)
671
+ this.#insertImageDraft(candidate);
672
+ this.#refreshAutocomplete();
673
+ this.#render();
674
+ return;
675
+ }
676
+ }
677
+ }
678
+ const text = await this.#readClipboard();
679
+ if (text !== '')
680
+ await this.#acceptPastedText(text);
681
+ }
682
+ #insertImageDraft(input) {
683
+ const size = input.width === undefined || input.height === undefined
684
+ ? probeImageDimensions(input.data, input.mediaType)
685
+ : undefined;
686
+ const image = {
687
+ ...input,
688
+ ...(input.width === undefined && size !== undefined ? { width: size.width } : {}),
689
+ ...(input.height === undefined && size !== undefined ? { height: size.height } : {}),
690
+ };
691
+ const marker = imageMarker(this.#images.length, image);
692
+ const before = this.#editor.cursor > 0 && !/\s/u.test(this.#editor.text[this.#editor.cursor - 1] ?? '') ? ' ' : '';
693
+ const after = this.#editor.cursor >= this.#editor.text.length || !/\s/u.test(this.#editor.text[this.#editor.cursor] ?? '')
694
+ ? ' '
695
+ : '';
696
+ this.#images.push(image);
697
+ this.#editor.handle({ type: 'text', value: before + marker + after });
698
+ }
699
+ #removeImageAtCursor(key) {
700
+ const cursor = this.#editor.cursor;
701
+ for (let index = 0; index < this.#images.length; index += 1) {
702
+ const image = this.#images[index];
703
+ const marker = imageMarker(index, image);
704
+ const start = this.#editor.text.indexOf(marker);
705
+ if (start < 0)
706
+ continue;
707
+ let from = start;
708
+ let to = start + marker.length;
709
+ const touches = key === 'backspace'
710
+ ? cursor > start && cursor <= to
711
+ : cursor >= start && cursor < to;
712
+ if (!touches)
713
+ continue;
714
+ if (this.#editor.text[to] === ' ')
715
+ to += 1;
716
+ else if (from > 0 && this.#editor.text[from - 1] === ' ')
717
+ from -= 1;
718
+ const oldImages = this.#images;
719
+ let text = this.#editor.text.slice(0, from) + this.#editor.text.slice(to);
720
+ const nextImages = oldImages.filter((_, oldIndex) => oldIndex !== index);
721
+ let nextIndex = 0;
722
+ for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
723
+ if (oldIndex === index)
724
+ continue;
725
+ const remaining = oldImages[oldIndex];
726
+ text = text.replaceAll(imageMarker(oldIndex, remaining), imageMarker(nextIndex, remaining));
727
+ nextIndex += 1;
728
+ }
729
+ this.#images = nextImages;
730
+ this.#editor.setText(text, Math.min(from, text.length));
731
+ this.#refreshAutocomplete();
732
+ this.#render();
733
+ return true;
734
+ }
735
+ return false;
736
+ }
737
+ #reconcileImageDrafts() {
738
+ if (this.#images.length === 0)
739
+ return;
740
+ const oldImages = this.#images;
741
+ const retained = oldImages.filter((image, index) => this.#editor.text.includes(imageMarker(index, image)));
742
+ if (retained.length === oldImages.length)
743
+ return;
744
+ let text = this.#editor.text;
745
+ let nextIndex = 0;
746
+ for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
747
+ const image = oldImages[oldIndex];
748
+ const oldMarker = imageMarker(oldIndex, image);
749
+ if (!text.includes(oldMarker))
750
+ continue;
751
+ text = text.replaceAll(oldMarker, imageMarker(nextIndex, image));
752
+ nextIndex += 1;
753
+ }
754
+ this.#images = retained;
755
+ this.#editor.setText(text, Math.min(this.#editor.cursor, text.length));
756
+ }
757
+ #dispatch(event) {
758
+ // Clipboard image inspection is asynchronous. Preserve the exact key
759
+ // order so a fast Ctrl+V, Enter submits the finished image draft rather
760
+ // than an empty prompt.
761
+ if (this.#pasteInFlight > 0) {
762
+ this.#deferredPasteEvents.push(event);
763
+ return;
764
+ }
765
+ if (this.#paste) {
766
+ if (event.type === 'paste-end') {
767
+ this.#paste = false;
768
+ const text = this.#pasteBuf.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
769
+ this.#pasteBuf = '';
770
+ if (text !== '')
771
+ this.#startAsyncPaste(this.#acceptPastedText(text));
772
+ return;
773
+ }
774
+ if (event.type === 'text')
775
+ this.#pasteBuf += event.value;
776
+ else if (event.type === 'key' && (event.id === 'enter' || event.id === 'ctrl+j'))
777
+ this.#pasteBuf += '\n';
778
+ return;
779
+ }
780
+ if (event.type === 'paste-start') {
781
+ this.#paste = true;
782
+ this.#pasteBuf = '';
783
+ return;
784
+ }
785
+ if (event.type === 'mouse') {
786
+ this.#handleMouse(event);
787
+ return;
788
+ }
789
+ if (this.#handlePrompt(event))
790
+ return;
791
+ if (event.type === 'key') {
792
+ const action = this.#keybindings[event.id];
793
+ if (action !== undefined) {
794
+ this.#runAction(action);
795
+ return;
796
+ }
797
+ }
798
+ if (event.type === 'key' && event.id === 'ctrl+c') {
799
+ if (this.#prompt !== null) {
800
+ this.#finishPrompt(null);
801
+ this.#editor.setText('');
802
+ this.#render();
803
+ return;
804
+ }
805
+ if (this.#settings !== null) {
806
+ this.#settings = null;
807
+ this.#render();
808
+ return;
809
+ }
810
+ if (this.#copySelector !== null) {
811
+ this.#copySelector = null;
812
+ this.#render();
813
+ return;
814
+ }
815
+ if (this.#search !== null) {
816
+ this.#search = null;
817
+ this.#render();
818
+ return;
819
+ }
820
+ const now = Date.now();
821
+ if (now - this.#lastSigintTime < DOUBLE_CTRL_C_MS) {
822
+ this.#lastSigintTime = 0;
823
+ this.#quit();
824
+ return;
825
+ }
826
+ this.#lastSigintTime = now;
827
+ if (this.#state.status === 'running') {
828
+ for (const listener of this.#interrupts)
829
+ listener();
830
+ }
831
+ else {
832
+ this.#editor.clear();
833
+ this.#images = [];
834
+ this.#historyIndex = 0;
835
+ this.#ac = null;
836
+ this.#render();
837
+ }
838
+ return;
839
+ }
840
+ if (this.#settings !== null) {
841
+ this.#applySettings(event);
842
+ return;
843
+ }
844
+ if (this.#copySelector !== null) {
845
+ this.#applyCopySelector(event);
846
+ return;
847
+ }
848
+ if (event.type === 'key' && event.id === 'ctrl+r') {
849
+ if (this.#images.length > 0)
850
+ return;
851
+ this.#search = createHistorySearch(this.#history);
852
+ this.#ac = null;
853
+ this.#render();
854
+ return;
855
+ }
856
+ if (this.#search !== null) {
857
+ this.#applySearch(event);
858
+ return;
859
+ }
860
+ if (this.#handleAutocomplete(event))
861
+ return;
862
+ if (event.type === 'key' && (event.id === 'backspace' || event.id === 'delete')
863
+ && this.#removeImageAtCursor(event.id))
864
+ return;
865
+ if (event.type === 'key') {
866
+ if (event.id === 'pageUp') {
867
+ this.#scrollBy(-this.#pageSize());
868
+ return;
869
+ }
870
+ if (event.id === 'pageDown') {
871
+ this.#scrollBy(this.#pageSize());
872
+ return;
873
+ }
874
+ if (event.id === 'shift+up') {
875
+ this.#scrollBy(-TRANSCRIPT_FAST_SCROLL);
876
+ return;
877
+ }
878
+ if (event.id === 'shift+down') {
879
+ this.#scrollBy(TRANSCRIPT_FAST_SCROLL);
880
+ return;
881
+ }
882
+ if (event.id === 'ctrl+o') {
883
+ const last = this.#state.blocks.at(-1);
884
+ const tool = last?.kind === 'toolCatalog'
885
+ ? undefined
886
+ : this.#state.blocks.findLast(block => block.kind === 'tool');
887
+ if (last?.kind === 'toolCatalog') {
888
+ this.#toolsExpanded = !this.#toolsExpanded;
889
+ }
890
+ else if (tool?.kind === 'tool') {
891
+ if (this.#expandedToolCalls.has(tool.callId))
892
+ this.#expandedToolCalls.delete(tool.callId);
893
+ else
894
+ this.#expandedToolCalls.add(tool.callId);
895
+ }
896
+ else {
897
+ this.#toolsExpanded = !this.#toolsExpanded;
898
+ }
899
+ this.#render();
900
+ return;
901
+ }
902
+ }
903
+ this.#applyCommand(this.#editor.handle(event));
904
+ }
905
+ #handlePrompt(event) {
906
+ const prompt = this.#prompt;
907
+ if (prompt === null)
908
+ return false;
909
+ if (prompt.request.presentation === 'plan-review')
910
+ return this.#handlePlanReview(event, prompt);
911
+ if (event.type === 'text' && event.value === ' ' && prompt.request.multiSelect === true && this.#editor.text === '') {
912
+ this.#prompt = togglePromptSelection(prompt);
913
+ this.#render();
914
+ return true;
915
+ }
916
+ if (event.type === 'text' && prompt.request.filterable === true) {
917
+ const command = this.#editor.handle(event);
918
+ if (command.kind === 'changed') {
919
+ this.#prompt = { ...prompt, selected: 0 };
920
+ this.#render();
921
+ }
922
+ return true;
923
+ }
924
+ if (event.type === 'text' && prompt.request.allowCustom === false)
925
+ return true;
926
+ if (event.type !== 'key')
927
+ return false;
928
+ const filtered = filteredPromptOptions(prompt.request, this.#editor.text);
929
+ const count = filtered.length;
930
+ if (event.id === 'escape' || event.id === 'ctrl+c') {
931
+ this.#editor.setText('');
932
+ this.#finishPrompt(null);
933
+ this.#render();
934
+ return true;
935
+ }
936
+ if (count === 0) {
937
+ if (prompt.request.filterable !== true)
938
+ return false;
939
+ if (event.id === 'enter')
940
+ return true;
941
+ const command = this.#editor.handle(event);
942
+ if (command.kind === 'changed') {
943
+ this.#prompt = { ...prompt, selected: 0 };
944
+ this.#render();
945
+ }
946
+ return true;
947
+ }
948
+ let next;
949
+ if (event.id === 'up' || event.id === 'shift+tab')
950
+ next = prompt.selected - 1;
951
+ else if (event.id === 'down' || event.id === 'tab')
952
+ next = prompt.selected + 1;
953
+ else if (event.id === 'pageUp')
954
+ next = prompt.selected - 10;
955
+ else if (event.id === 'pageDown')
956
+ next = prompt.selected + 10;
957
+ else if (event.id === 'home')
958
+ next = 0;
959
+ else if (event.id === 'end')
960
+ next = count - 1;
961
+ if (next !== undefined) {
962
+ this.#prompt = movePromptSelection(prompt, next, count);
963
+ this.#render();
964
+ return true;
965
+ }
966
+ if (event.id === 'enter' && this.#editor.text === '') {
967
+ const answer = selectedPromptAnswer(prompt);
968
+ if (prompt.request.multiSelect === true && answer === null)
969
+ return true;
970
+ this.#finishPrompt(answer);
971
+ this.#render();
972
+ return true;
973
+ }
974
+ if (event.id === 'enter' && prompt.request.filterable === true) {
975
+ this.#finishPrompt(selectedFilteredPromptAnswer(prompt, this.#editor.text));
976
+ this.#editor.setText('');
977
+ this.#render();
978
+ return true;
979
+ }
980
+ if (prompt.request.filterable === true) {
981
+ const command = this.#editor.handle(event);
982
+ if (command.kind === 'changed') {
983
+ this.#prompt = { ...prompt, selected: 0 };
984
+ this.#render();
985
+ }
986
+ return true;
987
+ }
988
+ return false;
989
+ }
990
+ #handlePlanReview(event, prompt) {
991
+ if (prompt.feedback === true) {
992
+ if (event.type === 'key' && event.id === 'ctrl+c') {
993
+ this.#editor.setText('');
994
+ this.#finishPrompt(null);
995
+ this.#render();
996
+ return true;
997
+ }
998
+ if (event.type === 'key' && event.id === 'escape') {
999
+ this.#editor.setText('');
1000
+ this.#prompt = { ...prompt, feedback: false };
1001
+ this.#render();
1002
+ return true;
1003
+ }
1004
+ if (event.type === 'key' && (event.id === 'enter' || event.id === 'ctrl+j')) {
1005
+ const feedback = this.#editor.text.trim();
1006
+ this.#editor.setText('');
1007
+ this.#finishPrompt(feedback === '' ? selectedPromptAnswer(prompt) : feedback);
1008
+ this.#render();
1009
+ return true;
1010
+ }
1011
+ const command = this.#editor.handle(event);
1012
+ if (command.kind === 'changed')
1013
+ this.#render();
1014
+ return true;
1015
+ }
1016
+ if (event.type === 'text')
1017
+ return true;
1018
+ if (event.type !== 'key')
1019
+ return false;
1020
+ if (event.id === 'escape' || event.id === 'ctrl+c') {
1021
+ this.#editor.setText('');
1022
+ this.#finishPrompt(null);
1023
+ this.#render();
1024
+ return true;
1025
+ }
1026
+ const scroll = this.#overlayHit?.kind === 'prompt' ? this.#overlayHit.document : undefined;
1027
+ let documentScroll;
1028
+ if (event.id === 'up')
1029
+ documentScroll = (scroll?.start ?? prompt.documentScroll ?? 0) - 1;
1030
+ else if (event.id === 'down')
1031
+ documentScroll = (scroll?.start ?? prompt.documentScroll ?? 0) + 1;
1032
+ else if (event.id === 'pageUp')
1033
+ documentScroll = (scroll?.start ?? 0) - (scroll?.pageSize ?? 8);
1034
+ else if (event.id === 'pageDown')
1035
+ documentScroll = (scroll?.start ?? 0) + (scroll?.pageSize ?? 8);
1036
+ else if (event.id === 'home')
1037
+ documentScroll = 0;
1038
+ else if (event.id === 'end')
1039
+ documentScroll = scroll?.maxStart ?? prompt.documentScroll ?? 0;
1040
+ if (documentScroll !== undefined) {
1041
+ this.#prompt = {
1042
+ ...prompt,
1043
+ documentScroll: Math.max(0, Math.min(documentScroll, scroll?.maxStart ?? Number.POSITIVE_INFINITY)),
1044
+ };
1045
+ this.#render();
1046
+ return true;
1047
+ }
1048
+ if (event.id === 'tab' || event.id === 'right') {
1049
+ this.#prompt = movePromptSelection(prompt, prompt.selected + 1);
1050
+ this.#render();
1051
+ return true;
1052
+ }
1053
+ if (event.id === 'shift+tab' || event.id === 'left') {
1054
+ this.#prompt = movePromptSelection(prompt, prompt.selected - 1);
1055
+ this.#render();
1056
+ return true;
1057
+ }
1058
+ if (event.id === 'enter') {
1059
+ const answer = selectedPromptAnswer(prompt);
1060
+ if (answer === null)
1061
+ return true;
1062
+ const approve = prompt.request.approveValue ?? prompt.request.options?.[0]?.value
1063
+ ?? prompt.request.options?.[0]?.label;
1064
+ if (answer === approve) {
1065
+ this.#finishPrompt(answer);
1066
+ }
1067
+ else {
1068
+ this.#editor.setText('');
1069
+ this.#prompt = { ...prompt, feedback: true };
1070
+ }
1071
+ this.#render();
1072
+ return true;
1073
+ }
1074
+ return true;
1075
+ }
1076
+ #handleMouse(event) {
1077
+ if (event.leftClick) {
1078
+ if (this.#clickOverlay(event.row))
1079
+ return;
1080
+ this.#clickEditor(event.row, event.col);
1081
+ return;
1082
+ }
1083
+ if (event.wheel === null)
1084
+ return;
1085
+ const dir = event.wheel;
1086
+ if (this.#prompt !== null) {
1087
+ this.#handlePrompt({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1088
+ return;
1089
+ }
1090
+ if (this.#settings !== null) {
1091
+ this.#applySettings({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1092
+ return;
1093
+ }
1094
+ if (this.#copySelector !== null) {
1095
+ this.#applyCopySelector({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1096
+ return;
1097
+ }
1098
+ if (this.#search !== null) {
1099
+ this.#applySearch({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1100
+ return;
1101
+ }
1102
+ if (this.#ac !== null) {
1103
+ this.#moveAutocomplete(dir);
1104
+ this.#render();
1105
+ return;
1106
+ }
1107
+ this.#scrollBy(dir < 0 ? -TRANSCRIPT_WHEEL_SCROLL : TRANSCRIPT_WHEEL_SCROLL, true);
1108
+ }
1109
+ #clickOverlay(row) {
1110
+ const hit = this.#overlayHit;
1111
+ if (hit === null)
1112
+ return false;
1113
+ const localRow = row - hit.start;
1114
+ if (hit.kind === 'settings' && this.#settings !== null) {
1115
+ const index = hitTestSettings(hit.itemRows ?? tuiSettingItems(this.#settings.prefs).length, localRow);
1116
+ if (index === undefined)
1117
+ return true;
1118
+ if (index === this.#settings.selected) {
1119
+ this.#applySettings({ type: 'key', id: 'enter' });
1120
+ }
1121
+ else {
1122
+ this.#settings = selectSetting(this.#settings, index);
1123
+ this.#render();
1124
+ }
1125
+ return true;
1126
+ }
1127
+ if (hit.kind === 'copy' && this.#copySelector !== null) {
1128
+ const index = hitTestCopySelector(this.#copySelector.items.length, this.#copySelector.selected, localRow);
1129
+ if (index === undefined)
1130
+ return true;
1131
+ if (index === this.#copySelector.selected) {
1132
+ this.#applyCopySelector({ type: 'key', id: 'enter' });
1133
+ }
1134
+ else {
1135
+ this.#copySelector = selectCopyTarget(this.#copySelector, index);
1136
+ this.#render();
1137
+ }
1138
+ return true;
1139
+ }
1140
+ if (hit.kind === 'autocomplete' && this.#ac !== null) {
1141
+ const index = hitTestAutocomplete(this.#ac.items.length, this.#ac.selected, localRow);
1142
+ if (index === undefined)
1143
+ return true;
1144
+ this.#ac = { ...this.#ac, selected: index };
1145
+ this.#applySelectedCompletion();
1146
+ this.#render();
1147
+ return true;
1148
+ }
1149
+ if (hit.kind === 'search' && this.#search !== null) {
1150
+ const index = hitTestHistorySearch(this.#search.results.length, this.#search.selected, localRow, hit.resultsRow ?? 0);
1151
+ if (index === undefined)
1152
+ return true;
1153
+ this.#search = { ...this.#search, selected: index };
1154
+ this.#applySearch({ type: 'key', id: 'enter' });
1155
+ return true;
1156
+ }
1157
+ if (hit.kind === 'prompt' && this.#prompt !== null) {
1158
+ const index = hit.itemRows?.[localRow];
1159
+ if (index === undefined)
1160
+ return true;
1161
+ if (index === this.#prompt.selected) {
1162
+ this.#finishPrompt(selectedFilteredPromptAnswer(this.#prompt, this.#editor.text));
1163
+ this.#editor.setText('');
1164
+ }
1165
+ else {
1166
+ this.#prompt = { ...this.#prompt, selected: index };
1167
+ }
1168
+ this.#render();
1169
+ return true;
1170
+ }
1171
+ return false;
1172
+ }
1173
+ #clickEditor(row, col) {
1174
+ if (this.#settings !== null || this.#copySelector !== null || this.#search !== null)
1175
+ return;
1176
+ const hit = this.#editorHit;
1177
+ if (hit === null)
1178
+ return;
1179
+ const localRow = row - hit.start;
1180
+ if (localRow < 0 || localRow >= hit.rows)
1181
+ return;
1182
+ const index = hitTestEditor(this.#editor.text, this.#term.width(), localRow, col);
1183
+ if (index === undefined)
1184
+ return;
1185
+ this.#editor.setCursor(index);
1186
+ this.#refreshAutocomplete();
1187
+ this.#render();
1188
+ }
1189
+ #prefs() {
1190
+ return {
1191
+ theme: this.#themeName,
1192
+ colors: this.#colors,
1193
+ expandTools: this.#expandTools,
1194
+ statusBar: {
1195
+ ...this.#statusBar,
1196
+ groups: [...this.#statusBar.groups],
1197
+ ...(this.#statusBar.order === undefined ? {} : { order: [...this.#statusBar.order] }),
1198
+ },
1199
+ };
1200
+ }
1201
+ #applyPrefs(prefs) {
1202
+ const expandChanged = prefs.expandTools !== this.#expandTools;
1203
+ this.#themeName = prefs.theme;
1204
+ this.#colors = prefs.colors;
1205
+ this.#expandTools = prefs.expandTools;
1206
+ this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
1207
+ if (expandChanged)
1208
+ this.#toolsExpanded = prefs.expandTools;
1209
+ this.#persistPrefs?.(prefs);
1210
+ }
1211
+ #applySettings(event) {
1212
+ if (this.#settings === null)
1213
+ return;
1214
+ const command = applySettingsEvent(this.#settings, event);
1215
+ if (command.kind === 'update') {
1216
+ this.#settings = command.state;
1217
+ this.#render();
1218
+ return;
1219
+ }
1220
+ if (command.kind === 'apply') {
1221
+ this.#settings = command.state;
1222
+ this.#applyPrefs(command.state.prefs);
1223
+ this.#render();
1224
+ return;
1225
+ }
1226
+ if (command.kind === 'close') {
1227
+ this.#settings = null;
1228
+ this.#render();
1229
+ }
1230
+ }
1231
+ #applyCopySelector(event) {
1232
+ if (this.#copySelector === null)
1233
+ return;
1234
+ const command = applyCopySelectorEvent(this.#copySelector, event);
1235
+ if (command.kind === 'update') {
1236
+ this.#copySelector = command.state;
1237
+ this.#render();
1238
+ return;
1239
+ }
1240
+ if (command.kind === 'pick') {
1241
+ this.#copySelector = null;
1242
+ void this.#copyPicked(command.item.text, command.item.copyMessage);
1243
+ return;
1244
+ }
1245
+ if (command.kind === 'close') {
1246
+ this.#copySelector = null;
1247
+ this.#render();
1248
+ }
1249
+ }
1250
+ async #copyPicked(text, label) {
1251
+ try {
1252
+ await this.#copy(text);
1253
+ this.#notice('Copied ' + label);
1254
+ }
1255
+ catch {
1256
+ this.#notice('Copy failed');
1257
+ }
1258
+ this.#render();
1259
+ }
1260
+ #applySearch(event) {
1261
+ if (this.#search === null)
1262
+ return;
1263
+ const command = applyHistorySearchEvent(this.#search, event, this.#history);
1264
+ if (command.kind === 'update') {
1265
+ this.#search = command.state;
1266
+ this.#render();
1267
+ return;
1268
+ }
1269
+ if (command.kind === 'select') {
1270
+ this.#search = null;
1271
+ this.#editor.setText(command.text);
1272
+ this.#historyIndex = 0;
1273
+ this.#refreshAutocomplete();
1274
+ this.#render();
1275
+ return;
1276
+ }
1277
+ if (command.kind === 'cancel') {
1278
+ this.#search = null;
1279
+ this.#render();
1280
+ }
1281
+ }
1282
+ #handleAutocomplete(event) {
1283
+ if (event.type !== 'key')
1284
+ return false;
1285
+ if (event.id === 'tab') {
1286
+ if (this.#ac !== null) {
1287
+ this.#applySelectedCompletion();
1288
+ }
1289
+ else {
1290
+ this.#refreshAutocomplete(true);
1291
+ }
1292
+ this.#render();
1293
+ return true;
1294
+ }
1295
+ if (this.#ac === null)
1296
+ return false;
1297
+ if (event.id === 'shift+tab' || event.id === 'up') {
1298
+ this.#moveAutocomplete(-1);
1299
+ this.#render();
1300
+ return true;
1301
+ }
1302
+ if (event.id === 'down') {
1303
+ this.#moveAutocomplete(1);
1304
+ this.#render();
1305
+ return true;
1306
+ }
1307
+ if (event.id === 'escape') {
1308
+ this.#ac = null;
1309
+ this.#render();
1310
+ return true;
1311
+ }
1312
+ if (event.id === 'enter') {
1313
+ this.#applySelectedCompletion();
1314
+ this.#submit(this.#editor.text);
1315
+ return true;
1316
+ }
1317
+ return false;
1318
+ }
1319
+ #refreshAutocomplete(forcePath = false) {
1320
+ const requestId = ++this.#autocompleteRequestId;
1321
+ if (this.#autocompleteTimer !== null) {
1322
+ clearTimeout(this.#autocompleteTimer);
1323
+ this.#autocompleteTimer = null;
1324
+ }
1325
+ this.#autocompleteAbort?.abort();
1326
+ this.#autocompleteAbort = null;
1327
+ if (this.#prompt !== null) {
1328
+ this.#ac = null;
1329
+ return;
1330
+ }
1331
+ const commands = this.#commands();
1332
+ const slashResult = slashSuggestions(this.#editor.text, this.#editor.cursor, commands);
1333
+ if (slashResult !== null) {
1334
+ this.#setAutocomplete(slashResult);
1335
+ return;
1336
+ }
1337
+ const pathOptions = {
1338
+ cwd: this.#cwd,
1339
+ projectRoot: this.#projectRoot,
1340
+ home: this.#home,
1341
+ listDir: this.#listDir,
1342
+ force: forcePath,
1343
+ };
1344
+ const token = findPathToken(this.#editor.text, this.#editor.cursor, forcePath);
1345
+ const atPrefix = token?.kind === 'at' ? parsePathPrefix(token.prefix).raw.replaceAll('\\', '/') : '';
1346
+ const fuzzyAt = token?.kind === 'at' && atPrefix !== '' && !atPrefix.endsWith('/');
1347
+ if (fuzzyAt) {
1348
+ this.#ac = null;
1349
+ const text = this.#editor.text;
1350
+ const cursor = this.#editor.cursor;
1351
+ this.#autocompleteTimer = setTimeout(() => {
1352
+ this.#autocompleteTimer = null;
1353
+ if (this.#disposed || requestId !== this.#autocompleteRequestId)
1354
+ return;
1355
+ const controller = new AbortController();
1356
+ this.#autocompleteAbort = controller;
1357
+ void searchPathSuggestions(text, cursor, {
1358
+ ...pathOptions,
1359
+ searchFiles: this.#searchFiles,
1360
+ signal: controller.signal,
1361
+ }, commands).then((result) => {
1362
+ if (this.#disposed || controller.signal.aborted || requestId !== this.#autocompleteRequestId)
1363
+ return;
1364
+ this.#autocompleteAbort = null;
1365
+ this.#setAutocomplete(result);
1366
+ this.#render();
1367
+ }).catch((error) => {
1368
+ if (controller.signal.aborted || requestId !== this.#autocompleteRequestId)
1369
+ return;
1370
+ this.#autocompleteAbort = null;
1371
+ this.#ac = null;
1372
+ if (error.name !== 'AbortError')
1373
+ this.#render();
1374
+ });
1375
+ }, this.#autocompleteDebounceMs);
1376
+ return;
1377
+ }
1378
+ const result = pathSuggestions(this.#editor.text, this.#editor.cursor, pathOptions, commands);
1379
+ this.#setAutocomplete(result);
1380
+ }
1381
+ #setAutocomplete(result) {
1382
+ if (result === null) {
1383
+ this.#ac = null;
1384
+ return;
1385
+ }
1386
+ const prev = this.#ac?.items[this.#ac.selected]?.value;
1387
+ let selected = 0;
1388
+ if (prev !== undefined) {
1389
+ const idx = result.items.findIndex((item) => item.value === prev);
1390
+ if (idx >= 0)
1391
+ selected = idx;
1392
+ }
1393
+ this.#ac = { items: result.items, selected };
1394
+ }
1395
+ #moveAutocomplete(dir) {
1396
+ if (this.#ac === null || this.#ac.items.length === 0)
1397
+ return;
1398
+ const n = this.#ac.items.length;
1399
+ this.#ac = { ...this.#ac, selected: (this.#ac.selected + dir + n) % n };
1400
+ }
1401
+ #applySelectedCompletion() {
1402
+ const item = this.#ac?.items[this.#ac.selected];
1403
+ if (item === undefined)
1404
+ return;
1405
+ const next = item.kind === 'path'
1406
+ ? applyPathCompletion(this.#editor.text, this.#editor.cursor, item)
1407
+ : applySlashCompletion(this.#editor.text, this.#editor.cursor, item);
1408
+ this.#editor.setText(next.text, next.cursor);
1409
+ this.#refreshAutocomplete();
1410
+ }
1411
+ #applyCommand(command) {
1412
+ if (command.kind === 'changed') {
1413
+ this.#reconcileImageDrafts();
1414
+ if (command.edited === true)
1415
+ this.#historyIndex = 0;
1416
+ this.#refreshAutocomplete();
1417
+ this.#render();
1418
+ return;
1419
+ }
1420
+ if (command.kind === 'submit') {
1421
+ this.#submit(command.text);
1422
+ return;
1423
+ }
1424
+ if (command.kind === 'historyPrev') {
1425
+ this.#historyPrev();
1426
+ return;
1427
+ }
1428
+ if (command.kind === 'historyNext') {
1429
+ this.#historyNext();
1430
+ return;
1431
+ }
1432
+ if (command.kind === 'interrupt') {
1433
+ if (this.#state.status === 'running') {
1434
+ for (const listener of this.#interrupts)
1435
+ listener();
1436
+ }
1437
+ return;
1438
+ }
1439
+ if (command.kind === 'clear') {
1440
+ this.#editor.clear();
1441
+ this.#images = [];
1442
+ this.#historyIndex = 0;
1443
+ this.#ac = null;
1444
+ this.#search = null;
1445
+ this.#render();
1446
+ return;
1447
+ }
1448
+ if (command.kind === 'quit') {
1449
+ this.#quit();
1450
+ return;
1451
+ }
1452
+ if (command.kind === 'suspend') {
1453
+ if (process.platform !== 'win32') {
1454
+ try {
1455
+ process.kill(process.pid, 'SIGTSTP');
1456
+ }
1457
+ catch { /* no controlling tty */ }
1458
+ }
1459
+ return;
1460
+ }
1461
+ if (command.kind === 'resetDisplay') {
1462
+ this.#renderer.reset();
1463
+ this.#term.output.write('\x1b[2J\x1b[H');
1464
+ this.#render();
1465
+ }
1466
+ }
1467
+ #historyPrev() {
1468
+ if (this.#images.length > 0)
1469
+ return;
1470
+ if (this.#history.length === 0 || this.#historyIndex >= this.#history.length)
1471
+ return;
1472
+ if (this.#historyIndex === 0)
1473
+ this.#draft = this.#editor.text;
1474
+ this.#historyIndex += 1;
1475
+ this.#editor.setText(this.#history[this.#history.length - this.#historyIndex] ?? '');
1476
+ this.#refreshAutocomplete();
1477
+ this.#render();
1478
+ }
1479
+ #historyNext() {
1480
+ if (this.#images.length > 0)
1481
+ return;
1482
+ if (this.#historyIndex === 0)
1483
+ return;
1484
+ this.#historyIndex -= 1;
1485
+ this.#editor.setText(this.#historyIndex === 0 ? this.#draft : (this.#history[this.#history.length - this.#historyIndex] ?? ''));
1486
+ this.#refreshAutocomplete();
1487
+ this.#render();
1488
+ }
1489
+ #quit() {
1490
+ this.#resumeHintRequested = true;
1491
+ if (this.#pending !== null) {
1492
+ const pending = this.#pending;
1493
+ this.#pending = null;
1494
+ pending.resolve(null);
1495
+ }
1496
+ else {
1497
+ this.#quitRequested = true;
1498
+ }
1499
+ }
1500
+ #submit(text) {
1501
+ if (this.#prompt !== null) {
1502
+ this.#editor.setText('');
1503
+ this.#finishPrompt(text.trim() === '' ? null : text.trim());
1504
+ this.#render();
1505
+ return;
1506
+ }
1507
+ const images = this.#images.map(image => ({ ...image }));
1508
+ const submittedText = images.length > 0 ? text.trim() : text;
1509
+ const historyText = images.reduce((value, image, index) => value.replaceAll(imageMarker(index, image), ''), submittedText)
1510
+ .replace(/[ \t]{2,}/gu, ' ')
1511
+ .trim();
1512
+ if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
1513
+ this.#history.push(historyText);
1514
+ this.#historyStore?.add(historyText);
1515
+ }
1516
+ this.#historyIndex = 0;
1517
+ this.#draft = '';
1518
+ this.#editor.setText('');
1519
+ this.#images = [];
1520
+ this.#ac = null;
1521
+ this.#search = null;
1522
+ this.#settings = null;
1523
+ this.#copySelector = null;
1524
+ this.#followTail();
1525
+ const slash = images.length === 0 ? parseSlashInput(submittedText) : null;
1526
+ if (slash !== null) {
1527
+ this.#runSlash(slash.name, slash.args);
1528
+ return;
1529
+ }
1530
+ this.#render();
1531
+ const pending = this.#pending;
1532
+ if (pending !== null) {
1533
+ this.#pending = null;
1534
+ pending.resolve({ text: submittedText, images });
1535
+ }
1536
+ else if (submittedText !== '' || images.length > 0) {
1537
+ this.#queue.push({ text: submittedText, images });
1538
+ }
1539
+ }
1540
+ #runSlash(name, args = '') {
1541
+ if (name === '') {
1542
+ this.#render();
1543
+ return;
1544
+ }
1545
+ const command = resolveSlashCommand(name, this.#commands());
1546
+ if (command === undefined) {
1547
+ this.#notice('unknown command: /' + name);
1548
+ this.#render();
1549
+ return;
1550
+ }
1551
+ if (command.name === 'quit') {
1552
+ this.#render();
1553
+ this.#quit();
1554
+ return;
1555
+ }
1556
+ if (command.name === 'clear') {
1557
+ this.#state = initialTranscript();
1558
+ this.#followTail();
1559
+ this.#render();
1560
+ return;
1561
+ }
1562
+ if (command.name === 'settings') {
1563
+ this.#runSettings(args);
1564
+ return;
1565
+ }
1566
+ if (command.name === 'hotkeys') {
1567
+ this.#hotkeyCatalog();
1568
+ this.#render();
1569
+ return;
1570
+ }
1571
+ if (command.name === 'copy') {
1572
+ void this.#runCopy(args);
1573
+ return;
1574
+ }
1575
+ if (command.name === 'tools') {
1576
+ this.#toolCatalog();
1577
+ this.#render();
1578
+ return;
1579
+ }
1580
+ if (command.name === 'pwd') {
1581
+ this.#notice(formatWorkspaceText({
1582
+ cwd: process.cwd(),
1583
+ model: this.#model,
1584
+ ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
1585
+ }));
1586
+ this.#render();
1587
+ return;
1588
+ }
1589
+ if (!BUILTIN_SLASH_COMMANDS.some((entry) => entry.name === command.name)) {
1590
+ this.#render();
1591
+ const raw = '/' + name + (args === '' ? '' : ' ' + args);
1592
+ const pending = this.#pending;
1593
+ if (pending !== null) {
1594
+ this.#pending = null;
1595
+ pending.resolve({ text: raw, images: [] });
1596
+ }
1597
+ else {
1598
+ this.#queue.push({ text: raw, images: [] });
1599
+ }
1600
+ return;
1601
+ }
1602
+ this.#state = {
1603
+ ...this.#state,
1604
+ blocks: [...this.#state.blocks, { kind: 'commandOutput', command: 'help', text: formatHelpText(this.#commands()) }],
1605
+ };
1606
+ this.#focusLatestBlock();
1607
+ this.#render();
1608
+ }
1609
+ #commands() {
1610
+ const localNames = new Set(BUILTIN_SLASH_COMMANDS.flatMap((command) => [command.name, ...(command.aliases ?? [])]));
1611
+ const runtime = this.#runtimeCommands
1612
+ .filter((command) => !localNames.has(command.name) && !HIDDEN_RUNTIME_COMMANDS.has(command.name))
1613
+ .map((command) => ({
1614
+ name: command.name,
1615
+ description: command.description,
1616
+ ...(command.inputHint === undefined ? {} : { inputHint: command.inputHint }),
1617
+ }));
1618
+ return [...BUILTIN_SLASH_COMMANDS, ...runtime];
1619
+ }
1620
+ #finishPrompt(answer) {
1621
+ const pending = this.#prompt;
1622
+ if (pending === null)
1623
+ return;
1624
+ this.#prompt = null;
1625
+ pending.offAbort?.();
1626
+ pending.resolve(answer);
1627
+ }
1628
+ async #runCopy(args) {
1629
+ if (args.trim() === '') {
1630
+ const items = buildCopyTargets(this.#state.blocks);
1631
+ if (items.length === 0) {
1632
+ this.#notice('Nothing to copy.');
1633
+ this.#render();
1634
+ return;
1635
+ }
1636
+ this.#search = null;
1637
+ this.#ac = null;
1638
+ this.#settings = null;
1639
+ this.#copySelector = createCopySelector(items);
1640
+ this.#render();
1641
+ return;
1642
+ }
1643
+ const kind = parseCopyKind(args);
1644
+ if (kind === undefined) {
1645
+ this.#notice('Usage: /copy [code|cmd]');
1646
+ this.#render();
1647
+ return;
1648
+ }
1649
+ const target = extractCopyTarget(this.#state.blocks, kind);
1650
+ if (target === undefined) {
1651
+ this.#notice(kind === 'code' ? 'No code block to copy.' : kind === 'cmd' ? 'No command to copy.' : 'Nothing to copy.');
1652
+ this.#render();
1653
+ return;
1654
+ }
1655
+ await this.#copyPicked(target.text, target.label);
1656
+ }
1657
+ #runSettings(args) {
1658
+ if (args.trim() !== '') {
1659
+ this.#notice('Usage: /settings');
1660
+ this.#render();
1661
+ return;
1662
+ }
1663
+ this.#search = null;
1664
+ this.#ac = null;
1665
+ this.#settings = createSettings(this.#prefs());
1666
+ this.#render();
1667
+ }
1668
+ #notice(text) {
1669
+ this.#state = {
1670
+ ...this.#state,
1671
+ blocks: [...this.#state.blocks, { kind: 'notice', level: 'info', text }],
1672
+ };
1673
+ }
1674
+ #toolCatalog() {
1675
+ this.#state = {
1676
+ ...this.#state,
1677
+ blocks: [...this.#state.blocks, { kind: 'toolCatalog', tools: this.#tools }],
1678
+ };
1679
+ this.#focusLatestBlock();
1680
+ }
1681
+ #hotkeyCatalog() {
1682
+ this.#state = {
1683
+ ...this.#state,
1684
+ blocks: [...this.#state.blocks, { kind: 'hotkeyCatalog', bindings: this.#keybindings }],
1685
+ };
1686
+ this.#focusLatestBlock();
1687
+ }
1688
+ #focusLatestBlock() {
1689
+ this.#follow = false;
1690
+ this.#focusBlock = Math.max(0, this.#state.blocks.length - 1);
1691
+ }
1692
+ #runAction(action) {
1693
+ if (this.#prompt !== null || this.#settings !== null || this.#copySelector !== null)
1694
+ return;
1695
+ if (action === 'retry') {
1696
+ this.#submit('/retry');
1697
+ return;
1698
+ }
1699
+ if (action === 'copy-prompt') {
1700
+ void this.#copyPicked(this.#editor.text, 'current prompt');
1701
+ return;
1702
+ }
1703
+ if (action === 'copy-line') {
1704
+ const text = this.#editor.text.slice(lineStart(this.#editor.text, this.#editor.cursor), lineEnd(this.#editor.text, this.#editor.cursor));
1705
+ void this.#copyPicked(text, 'current line');
1706
+ return;
1707
+ }
1708
+ if (action === 'paste-clipboard') {
1709
+ this.#startAsyncPaste(this.#pasteClipboard());
1710
+ return;
1711
+ }
1712
+ try {
1713
+ this.#term.input.setRawMode?.(false);
1714
+ const text = editExternally(this.#editor.text);
1715
+ this.#editor.setText(text);
1716
+ this.#reconcileImageDrafts();
1717
+ }
1718
+ catch (error) {
1719
+ this.notice(error instanceof Error ? error.message : String(error), 'error');
1720
+ }
1721
+ finally {
1722
+ this.#term.input.setRawMode?.(true);
1723
+ this.#renderer.reset();
1724
+ this.#term.output.write('\x1b[2J\x1b[H');
1725
+ this.#refreshAutocomplete();
1726
+ this.#render();
1727
+ }
1728
+ }
1729
+ }
1730
+ /**
1731
+ * Mount the local terminal provider as the tui service.
1732
+ * @param ctx - plugin context.
1733
+ * @param config - model label and color switch.
1734
+ */
1735
+ export function apply(ctx, config) {
1736
+ const dshHome = process.env.OMDSH_HOME ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
1737
+ const term = {
1738
+ output: process.stdout,
1739
+ input: process.stdin,
1740
+ width: () => process.stdout.columns ?? 80,
1741
+ height: () => process.stdout.rows ?? 24,
1742
+ onResize: (listener) => {
1743
+ process.stdout.on('resize', listener);
1744
+ return () => { process.stdout.removeListener('resize', listener); };
1745
+ },
1746
+ };
1747
+ const tui = new LocalTui(term, config.model, config.colors ?? term.output.isTTY === true, parseThemeName(config.theme), copyToClipboard, {
1748
+ historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
1749
+ keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),
1750
+ });
1751
+ ctx.provide(TUI_SERVICE, tui);
1752
+ ctx.effect(() => () => { tui.dispose(); });
1753
+ ctx.inject(['settings'], (settingsCtx) => {
1754
+ const scope = settingsCtx.settings.register(settingsNamespace(TUI_SETTINGS_NAMESPACE), TuiSettingsSchema, { base: { theme: parseThemeName(config.theme), colors: config.colors ?? term.output.isTTY === true, expandTools: false } });
1755
+ tui.applyStoredPrefs(scope.get());
1756
+ tui.setPrefsPersist((prefs) => { void scope.update(prefs); });
1757
+ scope.watch((next) => { tui.applyStoredPrefs(next); });
1758
+ });
1759
+ }
1760
+ //# sourceMappingURL=provider-local.js.map