@agi-fans/dsh-tui 0.6.1 → 0.8.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.
- package/lib/CHANGELOG.md +40 -1
- package/lib/chrome/box.d.ts +0 -7
- package/lib/chrome/box.js +1 -21
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/main-screen-renderer.d.ts +55 -0
- package/lib/chrome/main-screen-renderer.js +343 -0
- package/lib/chrome/main-screen-renderer.js.map +1 -0
- package/lib/chrome/path-mentions.js +12 -3
- package/lib/chrome/path-mentions.js.map +1 -1
- package/lib/chrome/renderer.d.ts +8 -31
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/welcome-tips.js +1 -1
- package/lib/chrome/welcome-tips.js.map +1 -1
- package/lib/definition.d.ts +14 -1
- package/lib/input/image-paste.d.ts +12 -0
- package/lib/input/image-paste.js +41 -0
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/keys.d.ts +1 -28
- package/lib/input/keys.js +15 -31
- package/lib/input/keys.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +14 -6
- package/lib/runtime/provider-local.js +243 -238
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +9 -3
- package/lib/runtime/runner.js.map +1 -1
- package/lib/session/session-controller.d.ts +5 -2
- package/lib/session/session-controller.js +50 -6
- package/lib/session/session-controller.js.map +1 -1
- package/lib/views/at-complete.d.ts +31 -0
- package/lib/views/at-complete.js +104 -0
- package/lib/views/at-complete.js.map +1 -0
- package/lib/views/autocomplete.d.ts +2 -4
- package/lib/views/autocomplete.js +8 -11
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/copy-selector.d.ts +0 -4
- package/lib/views/copy-selector.js +0 -17
- package/lib/views/copy-selector.js.map +1 -1
- package/lib/views/event-views.d.ts +4 -16
- package/lib/views/event-views.js +85 -76
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/history-search.d.ts +0 -5
- package/lib/views/history-search.js +0 -16
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkeys.js +0 -1
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/path-complete.d.ts +6 -0
- package/lib/views/path-complete.js +19 -2
- package/lib/views/path-complete.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +0 -5
- package/lib/views/prompt-selector.js +0 -13
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +0 -5
- package/lib/views/settings-list.js +0 -23
- package/lib/views/settings-list.js.map +1 -1
- package/package.json +27 -25
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
* Owns the tty: raw-mode key input (editing, history, slash/tab
|
|
5
5
|
* autocomplete, /settings overlay, /copy picker, Ctrl-R history search, PgUp/PgDn
|
|
6
|
-
* and
|
|
6
|
+
* and Shift+Up/Down transcript scroll, Ctrl-O tool
|
|
7
7
|
* expand, bracketed paste, double-Escape conversation rewind, double Ctrl-C exit, Ctrl-D quit),
|
|
8
8
|
* SIGWINCH reflow, and the differential renderer. In non-tty mode
|
|
9
9
|
* (pipes, CI) it degrades to line-based input with plain append-only
|
|
@@ -14,18 +14,19 @@ import { homedir } from 'node:os';
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { createInterface } from 'node:readline';
|
|
16
16
|
import { TUI_SERVICE, } from "../definition.js";
|
|
17
|
-
import { applySlashCompletion, formatHelpText,
|
|
18
|
-
import { applyPathCompletion, defaultPathSource, findPathToken, parsePathPrefix, pathSuggestions, searchPathSuggestions, } from "../views/path-complete.js";
|
|
17
|
+
import { applySlashCompletion, formatHelpText, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "../views/autocomplete.js";
|
|
18
|
+
import { applyPathCompletion, defaultPathSource, editorLineAt, findPathToken, parsePathPrefix, pathSuggestions, searchPathSuggestions, } from "../views/path-complete.js";
|
|
19
|
+
import { activeAtToken } from '@deepseek-ai/dsh-file-reference/grammar';
|
|
20
|
+
import { nextSelectableAutocompleteIndex, searchAtSuggestions, } from "../views/at-complete.js";
|
|
19
21
|
import { copyToClipboard, readFromClipboard } from "../input/clipboard.js";
|
|
20
|
-
import { applyCopySelectorEvent, createCopySelector,
|
|
22
|
+
import { applyCopySelectorEvent, createCopySelector, } from "../views/copy-selector.js";
|
|
21
23
|
import { buildCopyTargets, extractCopyTarget, parseCopyKind } from "../views/copy-targets.js";
|
|
22
|
-
import { applyHistorySearchEvent, createHistorySearch,
|
|
24
|
+
import { applyHistorySearchEvent, createHistorySearch, } from "../views/history-search.js";
|
|
23
25
|
import { InputEditor, lineEnd, lineStart } from "../input/editor.js";
|
|
24
|
-
import { applySettingsEvent, createSettings,
|
|
25
|
-
import { applyEvent, blockLines,
|
|
26
|
-
import { flushPending,
|
|
27
|
-
import {
|
|
28
|
-
import { hitTestEditor } from "../chrome/box.js";
|
|
26
|
+
import { applySettingsEvent, createSettings, } from "../views/settings-list.js";
|
|
27
|
+
import { applyEvent, blockLines, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
|
|
28
|
+
import { flushPending, parseKeys } from "../input/keys.js";
|
|
29
|
+
import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
|
|
29
30
|
import { createTheme, detectTrueColor, parseThemeName } from "../chrome/theme.js";
|
|
30
31
|
import { TUI_SETTINGS_NAMESPACE, TuiSettingsSchema } from "../session/tui-settings.js";
|
|
31
32
|
import { defaultStatusBarConfig, resolveStatusBarConfig } from "../chrome/status-config.js";
|
|
@@ -37,7 +38,7 @@ import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selec
|
|
|
37
38
|
import { resolveProjectContext } from "../session/project-context.js";
|
|
38
39
|
import { pickWelcomeTips } from "../chrome/welcome-tips.js";
|
|
39
40
|
import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount } from "../views/hotkeys.js";
|
|
40
|
-
import { imageMarker, imagePathCandidates, probeImageDimensions, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
|
|
41
|
+
import { imageMarker, imagePathCandidates, probeImageDimensions, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
|
|
41
42
|
import { APP_NAME, APP_VERSION } from "../session/package-metadata.js";
|
|
42
43
|
const DOUBLE_CTRL_C_MS = 500;
|
|
43
44
|
const DOUBLE_ESCAPE_MS = 500;
|
|
@@ -53,7 +54,19 @@ export const name = 'omdsh-tui';
|
|
|
53
54
|
/**
|
|
54
55
|
* Local terminal presentation service.
|
|
55
56
|
*/
|
|
57
|
+
function detectTerminalProfile() {
|
|
58
|
+
if (process.env.TMUX !== undefined || process.env.STY !== undefined || process.env.ZELLIJ !== undefined) {
|
|
59
|
+
return 'multiplexer';
|
|
60
|
+
}
|
|
61
|
+
return process.platform === 'win32' ? 'conpty' : 'direct';
|
|
62
|
+
}
|
|
56
63
|
export class LocalTui {
|
|
64
|
+
#deferInitialRender = false;
|
|
65
|
+
#terminalProfile;
|
|
66
|
+
#resizeDebounceMs;
|
|
67
|
+
#resizeTimer = null;
|
|
68
|
+
#streamRenderMs;
|
|
69
|
+
#streamRenderTimer = null;
|
|
57
70
|
#term;
|
|
58
71
|
#model;
|
|
59
72
|
#reasoningEffort;
|
|
@@ -91,7 +104,6 @@ export class LocalTui {
|
|
|
91
104
|
#autocompleteTimer = null;
|
|
92
105
|
#autocompleteAbort = null;
|
|
93
106
|
#autocompleteRequestId = 0;
|
|
94
|
-
#scrollRender = null;
|
|
95
107
|
#paste = false;
|
|
96
108
|
#pasteBuf = '';
|
|
97
109
|
#pasteInFlight = 0;
|
|
@@ -129,10 +141,9 @@ export class LocalTui {
|
|
|
129
141
|
#sessionControls;
|
|
130
142
|
#loopStatus;
|
|
131
143
|
#subagents;
|
|
144
|
+
#subagentLauncherFocused = false;
|
|
132
145
|
#inspected;
|
|
133
|
-
#
|
|
134
|
-
#chromeHit = null;
|
|
135
|
-
#overlayHit = null;
|
|
146
|
+
#promptDocument;
|
|
136
147
|
#trueColor;
|
|
137
148
|
#copy;
|
|
138
149
|
#readClipboard;
|
|
@@ -146,6 +157,9 @@ export class LocalTui {
|
|
|
146
157
|
#home;
|
|
147
158
|
#listDir;
|
|
148
159
|
#searchFiles;
|
|
160
|
+
#searchFileMentions;
|
|
161
|
+
#searchSessions;
|
|
162
|
+
#validateImageDraft;
|
|
149
163
|
#autocompleteDebounceMs;
|
|
150
164
|
#persistPrefs = null;
|
|
151
165
|
/**
|
|
@@ -176,32 +190,49 @@ export class LocalTui {
|
|
|
176
190
|
this.#home = paths.home ?? fallback.home;
|
|
177
191
|
this.#listDir = paths.listDir ?? fallback.listDir;
|
|
178
192
|
this.#searchFiles = paths.searchFiles ?? fallback.searchFiles;
|
|
193
|
+
this.#searchFileMentions = paths.searchFileMentions;
|
|
194
|
+
this.#searchSessions = paths.searchSessions;
|
|
179
195
|
this.#autocompleteDebounceMs = Math.max(0, paths.autocompleteDebounceMs ?? 100);
|
|
180
196
|
this.#welcomeTips = pickWelcomeTips();
|
|
197
|
+
this.#deferInitialRender = paths.deferInitialRender === true;
|
|
198
|
+
this.#terminalProfile = paths.terminalProfile ?? detectTerminalProfile();
|
|
199
|
+
this.#resizeDebounceMs = Math.max(0, paths.resizeDebounceMs ?? 120);
|
|
200
|
+
this.#streamRenderMs = Math.max(0, paths.streamRenderMs ?? 8);
|
|
181
201
|
this.#trueColor = colors && detectTrueColor();
|
|
182
202
|
this.#tty = term.input.isTTY === true;
|
|
183
203
|
this.#pwd = shortenPath(project.root);
|
|
184
204
|
this.#branch = project.gitLabel;
|
|
185
|
-
this.#renderer = new
|
|
205
|
+
this.#renderer = new MainScreenRenderer({ write: (chunk) => { this.#term.output.write(chunk); } }, {
|
|
206
|
+
width: this.#term.width(),
|
|
207
|
+
height: this.#term.height(),
|
|
208
|
+
synchronized: this.#tty,
|
|
209
|
+
clearScrollback: this.#terminalProfile === 'direct',
|
|
210
|
+
alternateScreenOverlays: paths.alternateScreenOverlays === true,
|
|
211
|
+
});
|
|
186
212
|
if (this.#tty) {
|
|
213
|
+
this.#renderer.startEpoch();
|
|
187
214
|
term.input.setRawMode?.(true);
|
|
188
215
|
const listener = (chunk) => { this.#onData(chunk); };
|
|
189
216
|
term.input.on('data', listener);
|
|
190
217
|
this.#offData = () => { term.input.off('data', listener); };
|
|
191
218
|
this.#offResize = term.onResize?.(() => {
|
|
192
|
-
// A terminal resize
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
219
|
+
// A terminal resize changes the committed/live seam; re-anchor the
|
|
220
|
+
// live window so it stays anchored at the bottom.
|
|
221
|
+
const repaint = () => {
|
|
222
|
+
this.#resizeTimer = null;
|
|
223
|
+
this.#renderer.resize(this.#term.width(), this.#term.height());
|
|
224
|
+
this.#render();
|
|
225
|
+
};
|
|
226
|
+
if (this.#terminalProfile === 'multiplexer') {
|
|
227
|
+
if (this.#resizeTimer !== null)
|
|
228
|
+
clearTimeout(this.#resizeTimer);
|
|
229
|
+
this.#resizeTimer = setTimeout(repaint, this.#resizeDebounceMs);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
repaint();
|
|
233
|
+
}
|
|
197
234
|
}) ?? null;
|
|
198
|
-
|
|
199
|
-
// already scrolled the cursor off row 0; the renderer's screen-relative
|
|
200
|
-
// frames require a clean origin. Clear the screen and home the cursor
|
|
201
|
-
// before the first frame. Enable bracketed paste and SGR mouse
|
|
202
|
-
// tracking (wheel drives the virtual transcript; native scrollback
|
|
203
|
-
// is already unusable under full-screen diffs).
|
|
204
|
-
term.output.write('\x1b[2J\x1b[H\x1b[?2004h' + MOUSE_TRACKING_ON);
|
|
235
|
+
term.output.write('\x1b[?2004h');
|
|
205
236
|
}
|
|
206
237
|
this.#render();
|
|
207
238
|
}
|
|
@@ -209,7 +240,12 @@ export class LocalTui {
|
|
|
209
240
|
this.#state = applyEvent(this.#state, event, presentation);
|
|
210
241
|
this.#syncTick();
|
|
211
242
|
if (this.#tty) {
|
|
212
|
-
this.#
|
|
243
|
+
if (event.type === 'assistant/chunk' && this.#streamRenderMs > 0) {
|
|
244
|
+
this.#scheduleStreamRender();
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.#render();
|
|
248
|
+
}
|
|
213
249
|
}
|
|
214
250
|
else if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result' || event.type === 'turn/end') {
|
|
215
251
|
this.#printPlain();
|
|
@@ -239,6 +275,8 @@ export class LocalTui {
|
|
|
239
275
|
this.#subagents = roster === undefined
|
|
240
276
|
? undefined
|
|
241
277
|
: { agents: roster.agents.map(agent => ({ ...agent, activity: [...agent.activity] })) };
|
|
278
|
+
if ((this.#subagents?.agents.length ?? 0) === 0)
|
|
279
|
+
this.#subagentLauncherFocused = false;
|
|
242
280
|
const inspected = this.#inspected;
|
|
243
281
|
if (inspected !== undefined) {
|
|
244
282
|
const current = this.#subagents?.agents.find(agent => agent.id === inspected.id);
|
|
@@ -258,6 +296,8 @@ export class LocalTui {
|
|
|
258
296
|
}
|
|
259
297
|
setInspectedSubagent(inspected) {
|
|
260
298
|
this.#inspected = inspected === undefined ? undefined : { ...inspected };
|
|
299
|
+
if (inspected !== undefined)
|
|
300
|
+
this.#subagentLauncherFocused = false;
|
|
261
301
|
this.#syncTick();
|
|
262
302
|
if (this.#tty)
|
|
263
303
|
this.#render();
|
|
@@ -275,6 +315,21 @@ export class LocalTui {
|
|
|
275
315
|
if (this.#tty)
|
|
276
316
|
this.#render();
|
|
277
317
|
}
|
|
318
|
+
setSessionSearch(search) {
|
|
319
|
+
this.#searchSessions = search;
|
|
320
|
+
this.#refreshAutocomplete();
|
|
321
|
+
if (this.#tty)
|
|
322
|
+
this.#render();
|
|
323
|
+
}
|
|
324
|
+
setFileSearch(search) {
|
|
325
|
+
this.#searchFileMentions = search;
|
|
326
|
+
this.#refreshAutocomplete();
|
|
327
|
+
if (this.#tty)
|
|
328
|
+
this.#render();
|
|
329
|
+
}
|
|
330
|
+
setImageValidator(validate) {
|
|
331
|
+
this.#validateImageDraft = validate;
|
|
332
|
+
}
|
|
278
333
|
notice(text, options = {}) {
|
|
279
334
|
const block = {
|
|
280
335
|
kind: 'notice',
|
|
@@ -330,11 +385,13 @@ export class LocalTui {
|
|
|
330
385
|
}
|
|
331
386
|
});
|
|
332
387
|
}
|
|
333
|
-
replaceSession(events, presentations) {
|
|
388
|
+
replaceSession(events, presentations, status = 'idle') {
|
|
334
389
|
const state = replayEvents(events, presentations);
|
|
335
|
-
this.#state = { ...state, status
|
|
390
|
+
this.#state = { ...state, status, compactCommandId: undefined };
|
|
336
391
|
this.#plainPrinted = 0;
|
|
337
392
|
this.#followTail();
|
|
393
|
+
this.#deferInitialRender = false;
|
|
394
|
+
this.#renderer.startEpoch({ replay: status === 'idle' ? 'full' : 'pinned' });
|
|
338
395
|
if (this.#tty)
|
|
339
396
|
this.#render();
|
|
340
397
|
else
|
|
@@ -473,6 +530,14 @@ export class LocalTui {
|
|
|
473
530
|
if (this.#disposed)
|
|
474
531
|
return;
|
|
475
532
|
this.#disposed = true;
|
|
533
|
+
if (this.#resizeTimer !== null) {
|
|
534
|
+
clearTimeout(this.#resizeTimer);
|
|
535
|
+
this.#resizeTimer = null;
|
|
536
|
+
}
|
|
537
|
+
if (this.#streamRenderTimer !== null) {
|
|
538
|
+
clearTimeout(this.#streamRenderTimer);
|
|
539
|
+
this.#streamRenderTimer = null;
|
|
540
|
+
}
|
|
476
541
|
if (this.#tick !== null) {
|
|
477
542
|
clearInterval(this.#tick);
|
|
478
543
|
this.#tick = null;
|
|
@@ -481,19 +546,15 @@ export class LocalTui {
|
|
|
481
546
|
clearInterval(this.#loopTick);
|
|
482
547
|
this.#loopTick = null;
|
|
483
548
|
}
|
|
484
|
-
if (this.#scrollRender !== null) {
|
|
485
|
-
clearImmediate(this.#scrollRender);
|
|
486
|
-
this.#scrollRender = null;
|
|
487
|
-
}
|
|
488
549
|
if (this.#tty) {
|
|
489
550
|
this.#offData?.();
|
|
490
551
|
this.#offResize?.();
|
|
491
552
|
this.#term.input.setRawMode?.(false);
|
|
492
553
|
// Leave the cursor on a fresh line below the last frame so the shell
|
|
493
|
-
// prompt does not overwrite the transcript. Disable
|
|
494
|
-
// and
|
|
554
|
+
// prompt does not overwrite the transcript. Disable bracketed paste
|
|
555
|
+
// and restore the cursor.
|
|
495
556
|
this.#renderer.finish();
|
|
496
|
-
this.#term.output.write(
|
|
557
|
+
this.#term.output.write('\x1b[?2004l\x1b[?25h\r\n');
|
|
497
558
|
if (this.#resumeHintRequested && this.#sessionId !== undefined) {
|
|
498
559
|
this.#term.output.write(`\r\nResume this session with ${APP_NAME} --resume ${this.#sessionId}\r\n`);
|
|
499
560
|
}
|
|
@@ -613,10 +674,12 @@ export class LocalTui {
|
|
|
613
674
|
}
|
|
614
675
|
}
|
|
615
676
|
#render() {
|
|
616
|
-
if (this.#
|
|
617
|
-
|
|
618
|
-
this.#
|
|
677
|
+
if (this.#streamRenderTimer !== null) {
|
|
678
|
+
clearTimeout(this.#streamRenderTimer);
|
|
679
|
+
this.#streamRenderTimer = null;
|
|
619
680
|
}
|
|
681
|
+
if (this.#deferInitialRender)
|
|
682
|
+
return;
|
|
620
683
|
const width = this.#term.width();
|
|
621
684
|
const frame = this.#tty
|
|
622
685
|
? renderView(this.#state, {
|
|
@@ -649,6 +712,7 @@ export class LocalTui {
|
|
|
649
712
|
...(this.#sessionControls === undefined ? {} : { sessionControls: this.#sessionControls }),
|
|
650
713
|
...(this.#loopStatus === undefined ? {} : { loopStatus: this.#loopStatus }),
|
|
651
714
|
...(this.#subagents === undefined ? {} : { subagents: this.#subagents }),
|
|
715
|
+
subagentLauncherFocused: this.#subagentLauncherFocused,
|
|
652
716
|
...(this.#inspected === undefined ? {} : { inspected: this.#inspected }),
|
|
653
717
|
statusBar: this.#statusBar,
|
|
654
718
|
...(this.#prompt === null ? {} : { promptSelector: this.#prompt }),
|
|
@@ -662,12 +726,19 @@ export class LocalTui {
|
|
|
662
726
|
})
|
|
663
727
|
: { lines: [] };
|
|
664
728
|
this.#focusBlock = undefined;
|
|
665
|
-
this.#
|
|
666
|
-
this.#overlayHit = frame.overlay ?? null;
|
|
667
|
-
this.#chromeHit = frame.chrome ?? null;
|
|
729
|
+
this.#promptDocument = frame.promptDocument;
|
|
668
730
|
this.#syncScroll(frame.transcript);
|
|
669
731
|
this.#renderer.render(frame);
|
|
670
732
|
}
|
|
733
|
+
#scheduleStreamRender() {
|
|
734
|
+
if (this.#streamRenderTimer !== null)
|
|
735
|
+
return;
|
|
736
|
+
this.#streamRenderTimer = setTimeout(() => {
|
|
737
|
+
this.#streamRenderTimer = null;
|
|
738
|
+
if (!this.#disposed)
|
|
739
|
+
this.#render();
|
|
740
|
+
}, this.#streamRenderMs);
|
|
741
|
+
}
|
|
671
742
|
#syncScroll(scroll) {
|
|
672
743
|
if (scroll === undefined) {
|
|
673
744
|
this.#scrollStart = 0;
|
|
@@ -687,24 +758,14 @@ export class LocalTui {
|
|
|
687
758
|
#pageSize() {
|
|
688
759
|
return Math.max(1, this.#scrollBudget > 2 ? this.#scrollBudget - 2 : 1);
|
|
689
760
|
}
|
|
690
|
-
#scrollBy(delta
|
|
761
|
+
#scrollBy(delta) {
|
|
691
762
|
if (delta === 0 && this.#maxStart === 0)
|
|
692
763
|
return;
|
|
693
764
|
this.#follow = false;
|
|
694
765
|
this.#scrollStart += delta;
|
|
695
766
|
if (this.#scrollStart <= 0)
|
|
696
767
|
this.#scrollStart = 0;
|
|
697
|
-
|
|
698
|
-
this.#render();
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
if (this.#scrollRender !== null)
|
|
702
|
-
return;
|
|
703
|
-
this.#scrollRender = setImmediate(() => {
|
|
704
|
-
this.#scrollRender = null;
|
|
705
|
-
if (!this.#disposed)
|
|
706
|
-
this.#render();
|
|
707
|
-
});
|
|
768
|
+
this.#render();
|
|
708
769
|
}
|
|
709
770
|
#followTail() {
|
|
710
771
|
this.#follow = true;
|
|
@@ -765,10 +826,17 @@ export class LocalTui {
|
|
|
765
826
|
if (paths.length > 0) {
|
|
766
827
|
const images = await Promise.all(paths.map(path => this.#readImagePath(path)));
|
|
767
828
|
if (images.every((image) => image !== null)) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
829
|
+
let inserted = false;
|
|
830
|
+
for (const image of images) {
|
|
831
|
+
if (await this.#admitImage(image)) {
|
|
832
|
+
this.#insertImageDraft(image);
|
|
833
|
+
inserted = true;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (inserted) {
|
|
837
|
+
this.#refreshAutocomplete();
|
|
838
|
+
this.#render();
|
|
839
|
+
}
|
|
772
840
|
return;
|
|
773
841
|
}
|
|
774
842
|
// Screenshot tools sometimes paste a transient cache path and remove
|
|
@@ -790,9 +858,11 @@ export class LocalTui {
|
|
|
790
858
|
if (this.#search === null && this.#settings === null && this.#copySelector === null) {
|
|
791
859
|
const image = await this.#readClipboardImage();
|
|
792
860
|
if (image !== null) {
|
|
793
|
-
this.#
|
|
794
|
-
|
|
795
|
-
|
|
861
|
+
if (await this.#admitImage(image)) {
|
|
862
|
+
this.#insertImageDraft(image);
|
|
863
|
+
this.#refreshAutocomplete();
|
|
864
|
+
this.#render();
|
|
865
|
+
}
|
|
796
866
|
return;
|
|
797
867
|
}
|
|
798
868
|
const files = await this.#readClipboardFiles();
|
|
@@ -801,10 +871,17 @@ export class LocalTui {
|
|
|
801
871
|
const images = (await Promise.all(imagePaths.map(path => this.#readImagePath(path))))
|
|
802
872
|
.filter((candidate) => candidate !== null);
|
|
803
873
|
if (images.length > 0) {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
874
|
+
let inserted = false;
|
|
875
|
+
for (const candidate of images) {
|
|
876
|
+
if (await this.#admitImage(candidate)) {
|
|
877
|
+
this.#insertImageDraft(candidate);
|
|
878
|
+
inserted = true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (inserted) {
|
|
882
|
+
this.#refreshAutocomplete();
|
|
883
|
+
this.#render();
|
|
884
|
+
}
|
|
808
885
|
return;
|
|
809
886
|
}
|
|
810
887
|
}
|
|
@@ -813,6 +890,23 @@ export class LocalTui {
|
|
|
813
890
|
if (text !== '')
|
|
814
891
|
await this.#acceptPastedText(text);
|
|
815
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Run the Harness image-admission check for one paste candidate. A refusal
|
|
895
|
+
* becomes an error notice and skips the draft, instead of failing the whole
|
|
896
|
+
* submission after the user has typed a prompt around it.
|
|
897
|
+
*/
|
|
898
|
+
async #admitImage(image) {
|
|
899
|
+
if (this.#validateImageDraft === undefined)
|
|
900
|
+
return true;
|
|
901
|
+
try {
|
|
902
|
+
await this.#validateImageDraft(image);
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
816
910
|
#insertImageDraft(input) {
|
|
817
911
|
const size = input.width === undefined || input.height === undefined
|
|
818
912
|
? probeImageDimensions(input.data, input.mediaType)
|
|
@@ -890,10 +984,6 @@ export class LocalTui {
|
|
|
890
984
|
}
|
|
891
985
|
#dispatch(event) {
|
|
892
986
|
if (this.#state.status === 'compacting') {
|
|
893
|
-
if (event.type === 'mouse' && event.wheel !== null) {
|
|
894
|
-
this.#scrollBy(event.wheel < 0 ? -TRANSCRIPT_WHEEL_SCROLL : TRANSCRIPT_WHEEL_SCROLL, true);
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
987
|
if (event.type === 'key' && event.id === 'pageUp') {
|
|
898
988
|
this.#scrollBy(-this.#pageSize());
|
|
899
989
|
return;
|
|
@@ -932,11 +1022,6 @@ export class LocalTui {
|
|
|
932
1022
|
this.#pasteBuf = '';
|
|
933
1023
|
return;
|
|
934
1024
|
}
|
|
935
|
-
if (event.type === 'mouse') {
|
|
936
|
-
this.#lastEscapeTime = 0;
|
|
937
|
-
this.#handleMouse(event);
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
1025
|
if (event.type !== 'key' || event.id !== 'escape')
|
|
941
1026
|
this.#lastEscapeTime = 0;
|
|
942
1027
|
if (this.#handlePrompt(event))
|
|
@@ -1019,6 +1104,8 @@ export class LocalTui {
|
|
|
1019
1104
|
this.#lastEscapeTime = 0;
|
|
1020
1105
|
return;
|
|
1021
1106
|
}
|
|
1107
|
+
if (this.#handleSubagentLauncher(event))
|
|
1108
|
+
return;
|
|
1022
1109
|
if (event.type === 'key' && event.id === 'escape') {
|
|
1023
1110
|
if (this.#inspected !== undefined) {
|
|
1024
1111
|
if (this.#inspected.writable === true && (this.#editor.text !== '' || this.#images.length > 0)) {
|
|
@@ -1212,7 +1299,7 @@ export class LocalTui {
|
|
|
1212
1299
|
this.#render();
|
|
1213
1300
|
return true;
|
|
1214
1301
|
}
|
|
1215
|
-
const scroll = this.#
|
|
1302
|
+
const scroll = this.#promptDocument;
|
|
1216
1303
|
let documentScroll;
|
|
1217
1304
|
if (event.id === 'up')
|
|
1218
1305
|
documentScroll = (scroll?.start ?? prompt.documentScroll ?? 0) - 1;
|
|
@@ -1262,73 +1349,42 @@ export class LocalTui {
|
|
|
1262
1349
|
}
|
|
1263
1350
|
return true;
|
|
1264
1351
|
}
|
|
1265
|
-
#
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
return;
|
|
1269
|
-
if (this.#clickOverlay(event.row))
|
|
1270
|
-
return;
|
|
1271
|
-
this.#clickEditor(event.row, event.col);
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
if (event.wheel === null)
|
|
1275
|
-
return;
|
|
1276
|
-
const dir = event.wheel;
|
|
1277
|
-
if (this.#prompt !== null) {
|
|
1278
|
-
this.#handlePrompt({ type: 'key', id: dir < 0 ? 'up' : 'down' });
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
if (this.#settings !== null) {
|
|
1282
|
-
this.#applySettings({ type: 'key', id: dir < 0 ? 'up' : 'down' });
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
if (this.#copySelector !== null) {
|
|
1286
|
-
this.#applyCopySelector({ type: 'key', id: dir < 0 ? 'up' : 'down' });
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
if (this.#search !== null) {
|
|
1290
|
-
this.#applySearch({ type: 'key', id: dir < 0 ? 'up' : 'down' });
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
if (this.#ac !== null) {
|
|
1294
|
-
this.#moveAutocomplete(dir);
|
|
1295
|
-
this.#render();
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
this.#scrollBy(dir < 0 ? -TRANSCRIPT_WHEEL_SCROLL : TRANSCRIPT_WHEEL_SCROLL, true);
|
|
1352
|
+
#openInspect(id) {
|
|
1353
|
+
for (const listener of this.#inspects)
|
|
1354
|
+
listener(id);
|
|
1299
1355
|
}
|
|
1300
|
-
#
|
|
1301
|
-
const
|
|
1302
|
-
if (
|
|
1356
|
+
#handleSubagentLauncher(event) {
|
|
1357
|
+
const hasAgents = (this.#subagents?.agents.length ?? 0) > 0;
|
|
1358
|
+
if (this.#subagentLauncherFocused) {
|
|
1359
|
+
if (!hasAgents) {
|
|
1360
|
+
this.#subagentLauncherFocused = false;
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
if (event.type === 'key' && event.id === 'enter') {
|
|
1364
|
+
this.#subagentLauncherFocused = false;
|
|
1365
|
+
void this.#pickSubagent();
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
if (event.type === 'key' && (event.id === 'escape' || event.id === 'up')) {
|
|
1369
|
+
this.#subagentLauncherFocused = false;
|
|
1370
|
+
this.#render();
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
if (event.type === 'key' && event.id === 'down')
|
|
1374
|
+
return true;
|
|
1375
|
+
this.#subagentLauncherFocused = false;
|
|
1303
1376
|
return false;
|
|
1304
|
-
const inspect = hit.inspect;
|
|
1305
|
-
if (inspect !== undefined && row >= inspect.start && row < inspect.start + inspect.rows) {
|
|
1306
|
-
this.#closeInspect();
|
|
1307
|
-
return true;
|
|
1308
1377
|
}
|
|
1309
|
-
|
|
1310
|
-
if (roster === undefined)
|
|
1378
|
+
if (!hasAgents || event.type !== 'key' || event.id !== 'down')
|
|
1311
1379
|
return false;
|
|
1312
|
-
|
|
1313
|
-
if (localRow < 0 || localRow >= roster.rows)
|
|
1380
|
+
if (this.#editor.text !== '' || this.#images.length > 0 || this.#historyIndex !== 0 || this.#ac !== null)
|
|
1314
1381
|
return false;
|
|
1315
|
-
if (
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
const id = hitTestSubagentRoster(roster.items, localRow);
|
|
1320
|
-
if (id === undefined)
|
|
1321
|
-
return true;
|
|
1322
|
-
if (this.#inspected?.id === id)
|
|
1323
|
-
this.#closeInspect();
|
|
1324
|
-
else
|
|
1325
|
-
this.#openInspect(id);
|
|
1382
|
+
if (this.#queuedSubmissions.length > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
|
|
1383
|
+
return false;
|
|
1384
|
+
this.#subagentLauncherFocused = true;
|
|
1385
|
+
this.#render();
|
|
1326
1386
|
return true;
|
|
1327
1387
|
}
|
|
1328
|
-
#openInspect(id) {
|
|
1329
|
-
for (const listener of this.#inspects)
|
|
1330
|
-
listener(id);
|
|
1331
|
-
}
|
|
1332
1388
|
#closeInspect() {
|
|
1333
1389
|
if (this.#inspected === undefined)
|
|
1334
1390
|
return;
|
|
@@ -1336,6 +1392,7 @@ export class LocalTui {
|
|
|
1336
1392
|
listener();
|
|
1337
1393
|
}
|
|
1338
1394
|
async #pickSubagent() {
|
|
1395
|
+
this.#subagentLauncherFocused = false;
|
|
1339
1396
|
const agents = this.#subagents?.agents ?? [];
|
|
1340
1397
|
if (agents.length === 0) {
|
|
1341
1398
|
this.notice('No subagents are available in this session.');
|
|
@@ -1343,7 +1400,7 @@ export class LocalTui {
|
|
|
1343
1400
|
}
|
|
1344
1401
|
const initialValue = this.#inspected?.id ?? agents[0]?.id;
|
|
1345
1402
|
const answer = await this.prompt({
|
|
1346
|
-
title: '
|
|
1403
|
+
title: 'Agent Hub',
|
|
1347
1404
|
question: 'Open a subagent transcript',
|
|
1348
1405
|
options: agents.map(agent => ({
|
|
1349
1406
|
label: agent.label,
|
|
@@ -1352,93 +1409,14 @@ export class LocalTui {
|
|
|
1352
1409
|
})),
|
|
1353
1410
|
...(initialValue === undefined ? {} : { initialValue }),
|
|
1354
1411
|
allowCustom: false,
|
|
1355
|
-
|
|
1412
|
+
presentation: 'fullscreen-list',
|
|
1413
|
+
filterable: true,
|
|
1356
1414
|
submitLabel: 'open',
|
|
1357
1415
|
});
|
|
1358
1416
|
if (answer === null || answer === '')
|
|
1359
1417
|
return;
|
|
1360
1418
|
this.#openInspect(answer);
|
|
1361
1419
|
}
|
|
1362
|
-
#clickOverlay(row) {
|
|
1363
|
-
const hit = this.#overlayHit;
|
|
1364
|
-
if (hit === null)
|
|
1365
|
-
return false;
|
|
1366
|
-
const localRow = row - hit.start;
|
|
1367
|
-
if (hit.kind === 'settings' && this.#settings !== null) {
|
|
1368
|
-
const index = hitTestSettings(hit.itemRows ?? tuiSettingItems(this.#settings.prefs).length, localRow);
|
|
1369
|
-
if (index === undefined)
|
|
1370
|
-
return true;
|
|
1371
|
-
if (index === this.#settings.selected) {
|
|
1372
|
-
this.#applySettings({ type: 'key', id: 'enter' });
|
|
1373
|
-
}
|
|
1374
|
-
else {
|
|
1375
|
-
this.#settings = selectSetting(this.#settings, index);
|
|
1376
|
-
this.#render();
|
|
1377
|
-
}
|
|
1378
|
-
return true;
|
|
1379
|
-
}
|
|
1380
|
-
if (hit.kind === 'copy' && this.#copySelector !== null) {
|
|
1381
|
-
const index = hitTestCopySelector(this.#copySelector.items.length, this.#copySelector.selected, localRow);
|
|
1382
|
-
if (index === undefined)
|
|
1383
|
-
return true;
|
|
1384
|
-
if (index === this.#copySelector.selected) {
|
|
1385
|
-
this.#applyCopySelector({ type: 'key', id: 'enter' });
|
|
1386
|
-
}
|
|
1387
|
-
else {
|
|
1388
|
-
this.#copySelector = selectCopyTarget(this.#copySelector, index);
|
|
1389
|
-
this.#render();
|
|
1390
|
-
}
|
|
1391
|
-
return true;
|
|
1392
|
-
}
|
|
1393
|
-
if (hit.kind === 'autocomplete' && this.#ac !== null) {
|
|
1394
|
-
const index = hitTestAutocomplete(this.#ac.items.length, this.#ac.selected, localRow);
|
|
1395
|
-
if (index === undefined)
|
|
1396
|
-
return true;
|
|
1397
|
-
this.#ac = { ...this.#ac, selected: index };
|
|
1398
|
-
this.#applySelectedCompletion();
|
|
1399
|
-
this.#render();
|
|
1400
|
-
return true;
|
|
1401
|
-
}
|
|
1402
|
-
if (hit.kind === 'search' && this.#search !== null) {
|
|
1403
|
-
const index = hitTestHistorySearch(this.#search.results.length, this.#search.selected, localRow, hit.resultsRow ?? 0);
|
|
1404
|
-
if (index === undefined)
|
|
1405
|
-
return true;
|
|
1406
|
-
this.#search = { ...this.#search, selected: index };
|
|
1407
|
-
this.#applySearch({ type: 'key', id: 'enter' });
|
|
1408
|
-
return true;
|
|
1409
|
-
}
|
|
1410
|
-
if (hit.kind === 'prompt' && this.#prompt !== null) {
|
|
1411
|
-
const index = hit.itemRows?.[localRow];
|
|
1412
|
-
if (index === undefined)
|
|
1413
|
-
return true;
|
|
1414
|
-
if (index === this.#prompt.selected) {
|
|
1415
|
-
this.#finishPrompt(selectedFilteredPromptAnswer(this.#prompt, this.#editor.text));
|
|
1416
|
-
this.#editor.setText('');
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
this.#prompt = { ...this.#prompt, selected: index };
|
|
1420
|
-
}
|
|
1421
|
-
this.#render();
|
|
1422
|
-
return true;
|
|
1423
|
-
}
|
|
1424
|
-
return false;
|
|
1425
|
-
}
|
|
1426
|
-
#clickEditor(row, col) {
|
|
1427
|
-
if (this.#settings !== null || this.#copySelector !== null || this.#search !== null)
|
|
1428
|
-
return;
|
|
1429
|
-
const hit = this.#editorHit;
|
|
1430
|
-
if (hit === null)
|
|
1431
|
-
return;
|
|
1432
|
-
const localRow = row - hit.start;
|
|
1433
|
-
if (localRow < 0 || localRow >= hit.rows)
|
|
1434
|
-
return;
|
|
1435
|
-
const index = hitTestEditor(this.#editor.text, this.#term.width(), localRow, col);
|
|
1436
|
-
if (index === undefined)
|
|
1437
|
-
return;
|
|
1438
|
-
this.#editor.setCursor(index);
|
|
1439
|
-
this.#refreshAutocomplete();
|
|
1440
|
-
this.#render();
|
|
1441
|
-
}
|
|
1442
1420
|
#prefs() {
|
|
1443
1421
|
return {
|
|
1444
1422
|
theme: this.#themeName,
|
|
@@ -1603,10 +1581,20 @@ export class LocalTui {
|
|
|
1603
1581
|
force: forcePath,
|
|
1604
1582
|
};
|
|
1605
1583
|
const token = findPathToken(this.#editor.text, this.#editor.cursor, forcePath);
|
|
1606
|
-
const
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1584
|
+
const { line, col } = editorLineAt(this.#editor.text, this.#editor.cursor);
|
|
1585
|
+
const at = forcePath ? undefined : activeAtToken(line, col);
|
|
1586
|
+
const atPrefix = at?.query.replaceAll('\\', '/')
|
|
1587
|
+
?? (token?.kind === 'at' ? parsePathPrefix(token.prefix).raw.replaceAll('\\', '/') : '');
|
|
1588
|
+
const fuzzyAt = (at !== undefined || token?.kind === 'at') && atPrefix !== '' && !atPrefix.endsWith('/');
|
|
1589
|
+
const harnessAt = at !== undefined
|
|
1590
|
+
&& (this.#searchFileMentions !== undefined || this.#searchSessions !== undefined);
|
|
1591
|
+
if (fuzzyAt || harnessAt) {
|
|
1592
|
+
if (harnessAt && !fuzzyAt) {
|
|
1593
|
+
this.#setAutocomplete(pathSuggestions(this.#editor.text, this.#editor.cursor, pathOptions, commands));
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
this.#ac = null;
|
|
1597
|
+
}
|
|
1610
1598
|
const text = this.#editor.text;
|
|
1611
1599
|
const cursor = this.#editor.cursor;
|
|
1612
1600
|
this.#autocompleteTimer = setTimeout(() => {
|
|
@@ -1615,11 +1603,20 @@ export class LocalTui {
|
|
|
1615
1603
|
return;
|
|
1616
1604
|
const controller = new AbortController();
|
|
1617
1605
|
this.#autocompleteAbort = controller;
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1606
|
+
const search = this.#searchFileMentions === undefined && this.#searchSessions === undefined
|
|
1607
|
+
? searchPathSuggestions(text, cursor, {
|
|
1608
|
+
...pathOptions,
|
|
1609
|
+
searchFiles: this.#searchFiles,
|
|
1610
|
+
signal: controller.signal,
|
|
1611
|
+
}, commands)
|
|
1612
|
+
: searchAtSuggestions(text, cursor, {
|
|
1613
|
+
...pathOptions,
|
|
1614
|
+
searchFiles: this.#searchFiles,
|
|
1615
|
+
...(this.#searchFileMentions === undefined ? {} : { searchFileMentions: this.#searchFileMentions }),
|
|
1616
|
+
...(this.#searchSessions === undefined ? {} : { searchSessions: this.#searchSessions }),
|
|
1617
|
+
signal: controller.signal,
|
|
1618
|
+
}, commands);
|
|
1619
|
+
void search.then((result) => {
|
|
1623
1620
|
if (this.#disposed || controller.signal.aborted || requestId !== this.#autocompleteRequestId)
|
|
1624
1621
|
return;
|
|
1625
1622
|
this.#autocompleteAbort = null;
|
|
@@ -1645,9 +1642,9 @@ export class LocalTui {
|
|
|
1645
1642
|
return;
|
|
1646
1643
|
}
|
|
1647
1644
|
const prev = this.#ac?.items[this.#ac.selected]?.value;
|
|
1648
|
-
let selected = 0;
|
|
1645
|
+
let selected = nextSelectableAutocompleteIndex(result.items, 0, 1);
|
|
1649
1646
|
if (prev !== undefined) {
|
|
1650
|
-
const idx = result.items.findIndex((item) => item.value === prev);
|
|
1647
|
+
const idx = result.items.findIndex((item) => item.value === prev && item.kind !== 'heading');
|
|
1651
1648
|
if (idx >= 0)
|
|
1652
1649
|
selected = idx;
|
|
1653
1650
|
}
|
|
@@ -1656,14 +1653,16 @@ export class LocalTui {
|
|
|
1656
1653
|
#moveAutocomplete(dir) {
|
|
1657
1654
|
if (this.#ac === null || this.#ac.items.length === 0)
|
|
1658
1655
|
return;
|
|
1659
|
-
|
|
1660
|
-
|
|
1656
|
+
this.#ac = {
|
|
1657
|
+
...this.#ac,
|
|
1658
|
+
selected: nextSelectableAutocompleteIndex(this.#ac.items, this.#ac.selected + dir, dir),
|
|
1659
|
+
};
|
|
1661
1660
|
}
|
|
1662
1661
|
#applySelectedCompletion() {
|
|
1663
1662
|
const item = this.#ac?.items[this.#ac.selected];
|
|
1664
|
-
if (item === undefined)
|
|
1663
|
+
if (item === undefined || item.kind === 'heading')
|
|
1665
1664
|
return;
|
|
1666
|
-
const next = item.kind === 'path'
|
|
1665
|
+
const next = item.kind === 'path' || item.kind === 'session'
|
|
1667
1666
|
? applyPathCompletion(this.#editor.text, this.#editor.cursor, item)
|
|
1668
1667
|
: applySlashCompletion(this.#editor.text, this.#editor.cursor, item);
|
|
1669
1668
|
this.#editor.setText(next.text, next.cursor);
|
|
@@ -1730,7 +1729,6 @@ export class LocalTui {
|
|
|
1730
1729
|
}
|
|
1731
1730
|
if (command.kind === 'resetDisplay') {
|
|
1732
1731
|
this.#renderer.reset();
|
|
1733
|
-
this.#term.output.write('\x1b[2J\x1b[H');
|
|
1734
1732
|
this.#render();
|
|
1735
1733
|
}
|
|
1736
1734
|
}
|
|
@@ -1812,7 +1810,7 @@ export class LocalTui {
|
|
|
1812
1810
|
const images = this.#images.map(image => ({ ...image }));
|
|
1813
1811
|
const submittedText = images.length > 0 ? text.trim() : text;
|
|
1814
1812
|
const queueEditNewer = this.#queueEditNewer;
|
|
1815
|
-
const historyText =
|
|
1813
|
+
const historyText = stripComposerImageMarkers(submittedText, images)
|
|
1816
1814
|
.replace(/[ \t]{2,}/gu, ' ')
|
|
1817
1815
|
.trim();
|
|
1818
1816
|
if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
|
|
@@ -1830,6 +1828,9 @@ export class LocalTui {
|
|
|
1830
1828
|
this.#settings = null;
|
|
1831
1829
|
this.#copySelector = null;
|
|
1832
1830
|
this.#followTail();
|
|
1831
|
+
// Image placeholders are TUI-owned. Mixed image+slash drafts stay a
|
|
1832
|
+
// submission so restore can keep the original markers; the runner strips
|
|
1833
|
+
// them only to detect and execute Harness commands.
|
|
1833
1834
|
const slash = images.length === 0 ? parseSlashInput(submittedText) : null;
|
|
1834
1835
|
if (slash !== null) {
|
|
1835
1836
|
if (queueEditNewer !== null)
|
|
@@ -1856,7 +1857,7 @@ export class LocalTui {
|
|
|
1856
1857
|
#submitInspect(text) {
|
|
1857
1858
|
const images = this.#images.map(image => ({ ...image }));
|
|
1858
1859
|
const submittedText = images.length > 0 ? text.trim() : text;
|
|
1859
|
-
const historyText =
|
|
1860
|
+
const historyText = stripComposerImageMarkers(submittedText, images)
|
|
1860
1861
|
.replace(/[ \t]{2,}/gu, ' ')
|
|
1861
1862
|
.trim();
|
|
1862
1863
|
if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
|
|
@@ -1902,6 +1903,7 @@ export class LocalTui {
|
|
|
1902
1903
|
if (command.name === 'clear') {
|
|
1903
1904
|
this.#state = initialTranscript();
|
|
1904
1905
|
this.#followTail();
|
|
1906
|
+
this.#renderer.startEpoch();
|
|
1905
1907
|
this.#render();
|
|
1906
1908
|
return;
|
|
1907
1909
|
}
|
|
@@ -2076,7 +2078,6 @@ export class LocalTui {
|
|
|
2076
2078
|
finally {
|
|
2077
2079
|
this.#term.input.setRawMode?.(true);
|
|
2078
2080
|
this.#renderer.reset();
|
|
2079
|
-
this.#term.output.write('\x1b[2J\x1b[H');
|
|
2080
2081
|
this.#refreshAutocomplete();
|
|
2081
2082
|
this.#render();
|
|
2082
2083
|
}
|
|
@@ -2089,6 +2090,7 @@ export class LocalTui {
|
|
|
2089
2090
|
*/
|
|
2090
2091
|
export function apply(ctx, config) {
|
|
2091
2092
|
const dshHome = process.env.OMDSH_HOME ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
2093
|
+
const terminalProfile = detectTerminalProfile();
|
|
2092
2094
|
const term = {
|
|
2093
2095
|
output: process.stdout,
|
|
2094
2096
|
input: process.stdin,
|
|
@@ -2100,6 +2102,9 @@ export function apply(ctx, config) {
|
|
|
2100
2102
|
},
|
|
2101
2103
|
};
|
|
2102
2104
|
const tui = new LocalTui(term, config.model, config.colors ?? term.output.isTTY === true, parseThemeName(config.theme), copyToClipboard, {
|
|
2105
|
+
deferInitialRender: true,
|
|
2106
|
+
terminalProfile,
|
|
2107
|
+
alternateScreenOverlays: terminalProfile === 'direct',
|
|
2103
2108
|
historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
|
|
2104
2109
|
keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),
|
|
2105
2110
|
});
|