@agi-fans/dsh-tui 0.24.0 → 0.25.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 +36 -2
- package/lib/chrome/main-screen-renderer.d.ts +2 -2
- package/lib/chrome/main-screen-renderer.js +5 -5
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/renderer.js +1 -1
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/theme.js +1 -1
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/tool-renderers.d.ts +2 -0
- package/lib/chrome/tool-renderers.js +5 -0
- package/lib/chrome/tool-renderers.js.map +1 -1
- package/lib/definition.d.ts +2 -0
- package/lib/input/clipboard.d.ts +1 -1
- package/lib/input/clipboard.js +10 -4
- package/lib/input/clipboard.js.map +1 -1
- package/lib/input/editor.d.ts +14 -0
- package/lib/input/editor.js +55 -5
- package/lib/input/editor.js.map +1 -1
- package/lib/input/image-paste.d.ts +21 -5
- package/lib/input/image-paste.js +69 -21
- package/lib/input/image-paste.js.map +1 -1
- package/lib/locale/zh.d.ts +2 -1
- package/lib/locale/zh.js +2 -1
- package/lib/locale/zh.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +11 -11
- package/lib/runtime/composer-images.js +105 -96
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/herdr-agent.d.ts +2 -2
- package/lib/runtime/herdr-agent.js +2 -2
- package/lib/runtime/herdr-agent.js.map +1 -1
- package/lib/runtime/provider-local.js +206 -99
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +3 -3
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/terminal-notifications.d.ts +3 -1
- package/lib/runtime/terminal-notifications.js +7 -2
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-session.d.ts +1 -0
- package/lib/runtime/terminal-session.js +1 -0
- package/lib/runtime/terminal-session.js.map +1 -1
- package/lib/runtime/terminal-state.d.ts +10 -1
- package/lib/runtime/terminal-state.js +39 -7
- package/lib/runtime/terminal-state.js.map +1 -1
- package/lib/session/authorization-interaction.js +3 -3
- package/lib/session/authorization-interaction.js.map +1 -1
- package/lib/session/composer-drafts.js +25 -1
- package/lib/session/composer-drafts.js.map +1 -1
- package/lib/session/content-text.d.ts +5 -0
- package/lib/session/content-text.js +7 -0
- package/lib/session/content-text.js.map +1 -1
- package/lib/session/interaction-adapter.js +1 -0
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/session-controller.js +2 -3
- package/lib/session/session-controller.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +1 -0
- package/lib/views/prompt-selector.js +6 -3
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.js +1 -1
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/trajectory.js +5 -5
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.js +35 -27
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +1 -4
- package/lib/views/transcript-types.js +4 -7
- package/lib/views/transcript-types.js.map +1 -1
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ import { localizePrompt } from "../locale/prompt.js";
|
|
|
23
23
|
import { resolveUiLanguage, uiText } from "../locale/index.js";
|
|
24
24
|
import { formatAgentPreset } from "../session/session-configuration.js";
|
|
25
25
|
import { TerminalNotificationController, TerminalNotificationQueue, terminalNotificationSequence, } from "./terminal-notifications.js";
|
|
26
|
-
import { PROGRAM_STATUS_QUERY, programStatusSequence, terminalTitle } from "./terminal-state.js";
|
|
26
|
+
import { PROGRAM_STATUS_QUERY, programStatusNeedsAttention, programStatusSequence, terminalProgressSequence, terminalTitle, workingDirectorySequence, } from "./terminal-state.js";
|
|
27
27
|
import { TerminalSession } from "./terminal-session.js";
|
|
28
28
|
import { BRACKETED_PASTE, FOCUS_REPORTING, MOUSE_TRACKING, } from "./terminal-modes.js";
|
|
29
29
|
import { IDLE_WHEEL, terminalAcceleratesWheel, wheelStep } from "../input/wheel-scroll.js";
|
|
@@ -87,7 +87,7 @@ import { edgeIndex, listNavigation, navigateIndex } from "../views/list-navigati
|
|
|
87
87
|
import { refreshProjectContext, resolveGitStatusFiles, resolveProjectContext } from "../session/project-context.js";
|
|
88
88
|
import { modelRouteReady } from "../session/model-readiness.js";
|
|
89
89
|
import { formatEssentialHotkeysText, formatHotkeysText, keysForAction } from "../views/hotkeys.js";
|
|
90
|
-
import { imagePathCandidates, splitComposerAttachmentMarkers,
|
|
90
|
+
import { imagePathCandidates, splitComposerAttachmentMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
|
|
91
91
|
import { APP_DISPLAY_NAME, APP_NAME, APP_VERSION } from "../session/package-metadata.js";
|
|
92
92
|
const DOUBLE_CTRL_C_MS = 500;
|
|
93
93
|
/**
|
|
@@ -316,6 +316,7 @@ export class LocalTui {
|
|
|
316
316
|
#paste = false;
|
|
317
317
|
#pasteBuf = '';
|
|
318
318
|
#pasteInFlight = 0;
|
|
319
|
+
#pasteAbort;
|
|
319
320
|
#deferredPasteEvents = [];
|
|
320
321
|
#images = new ComposerImages({
|
|
321
322
|
editor: this.#editor,
|
|
@@ -377,6 +378,10 @@ export class LocalTui {
|
|
|
377
378
|
#compactionStartedAt;
|
|
378
379
|
#readImageInput;
|
|
379
380
|
#writtenProgramState;
|
|
381
|
+
#writtenDirectory;
|
|
382
|
+
#launchDirectory = process.cwd();
|
|
383
|
+
#writtenProgress;
|
|
384
|
+
#progressTimer;
|
|
380
385
|
#checkUpdates = true;
|
|
381
386
|
#startupChangelog = 'summary';
|
|
382
387
|
#notifications = new TerminalNotificationController();
|
|
@@ -622,14 +627,7 @@ export class LocalTui {
|
|
|
622
627
|
const resume = () => {
|
|
623
628
|
if (this.#disposed)
|
|
624
629
|
return;
|
|
625
|
-
this.#
|
|
626
|
-
this.#writtenWindowTitle = undefined;
|
|
627
|
-
this.#writtenProgramState = undefined;
|
|
628
|
-
this.#programQuerySent = false;
|
|
629
|
-
this.#programSupported = false;
|
|
630
|
-
this.#syncWindowTitle();
|
|
631
|
-
this.#session.takeBack();
|
|
632
|
-
this.#renderer.reset();
|
|
630
|
+
this.#takeBackTerminal();
|
|
633
631
|
this.#render();
|
|
634
632
|
};
|
|
635
633
|
process.on('SIGCONT', resume);
|
|
@@ -704,6 +702,8 @@ export class LocalTui {
|
|
|
704
702
|
else if (event.type === 'session/end-seed')
|
|
705
703
|
this.#directInputIds.clear();
|
|
706
704
|
this.#state = applyEvent(this.#state, event, presentation, workspace);
|
|
705
|
+
if (event.type === 'turn/start' && this.#herdr.programStatus.state !== 'blocked')
|
|
706
|
+
this.#notificationQueue.clear();
|
|
707
707
|
if (claimedHumanInput && this.#tty && event.type === 'agent/inbox/spliced') {
|
|
708
708
|
this.#showTransientStatus(event.data.target === 'next-step' ? 'Steer taken for the next step' : 'Queue taken for the next turn');
|
|
709
709
|
}
|
|
@@ -889,6 +889,7 @@ export class LocalTui {
|
|
|
889
889
|
this.#render();
|
|
890
890
|
}
|
|
891
891
|
async reviewFiles(request) {
|
|
892
|
+
this.#cancelPaste();
|
|
892
893
|
if (!this.#tty || this.#disposed || request.signal.aborted)
|
|
893
894
|
return;
|
|
894
895
|
this.#workspacePress = undefined;
|
|
@@ -1244,11 +1245,12 @@ export class LocalTui {
|
|
|
1244
1245
|
this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
1245
1246
|
}
|
|
1246
1247
|
}
|
|
1248
|
+
this.#cancelPaste();
|
|
1247
1249
|
this.#flushPasteBurst(true);
|
|
1248
1250
|
this.#pasteBurst.clear();
|
|
1249
1251
|
if (request.notify !== false) {
|
|
1250
1252
|
this.#emitNotification(this.#notifications.humanPrompt());
|
|
1251
|
-
this.#herdr.prompt(request.title);
|
|
1253
|
+
this.#herdr.prompt(request.title, request.attentionKind);
|
|
1252
1254
|
this.#syncWindowTitle();
|
|
1253
1255
|
}
|
|
1254
1256
|
if (this.#tty && request.documentImage !== undefined && this.#imageProtocol !== undefined)
|
|
@@ -1384,6 +1386,8 @@ export class LocalTui {
|
|
|
1384
1386
|
this.#workspacePress = undefined;
|
|
1385
1387
|
this.#fileReviewAbort?.abort();
|
|
1386
1388
|
this.#closeFileBrowser();
|
|
1389
|
+
this.#notificationQueue.clear();
|
|
1390
|
+
this.#notifications.reset();
|
|
1387
1391
|
}
|
|
1388
1392
|
this.#switchDraft(this.#inspected?.id ?? info.id);
|
|
1389
1393
|
this.#sessionId = info.id;
|
|
@@ -1410,10 +1414,12 @@ export class LocalTui {
|
|
|
1410
1414
|
this.#render();
|
|
1411
1415
|
}
|
|
1412
1416
|
/** Only the foreground terminal owner emits title and program-state sequences. */
|
|
1413
|
-
#syncWindowTitle() {
|
|
1414
|
-
if (!this.#tty || this.#disposed)
|
|
1417
|
+
#syncWindowTitle(notify = false) {
|
|
1418
|
+
if (!this.#tty || this.#disposed || this.#session.handedOff)
|
|
1415
1419
|
return;
|
|
1416
1420
|
const status = this.#herdr.programStatus;
|
|
1421
|
+
this.#writeWorkingDirectory(this.#cwd);
|
|
1422
|
+
this.#syncProgress(status.state);
|
|
1417
1423
|
const title = terminalTitle(this.#terminalTitle, status, this.#sessionTitle, this.#cwd, this.#branch);
|
|
1418
1424
|
if (title !== this.#writtenWindowTitle && !(this.#terminalTitle === 'off' && this.#writtenWindowTitle === undefined)) {
|
|
1419
1425
|
this.#writtenWindowTitle = title;
|
|
@@ -1423,20 +1429,78 @@ export class LocalTui {
|
|
|
1423
1429
|
this.#programQuerySent = true;
|
|
1424
1430
|
this.#term.output.write(PROGRAM_STATUS_QUERY);
|
|
1425
1431
|
}
|
|
1426
|
-
|
|
1427
|
-
if (!enabled) {
|
|
1432
|
+
if (!this.#reportsProgramState) {
|
|
1428
1433
|
if (this.#writtenProgramState !== undefined)
|
|
1429
1434
|
this.#term.output.write(programStatusSequence('clear'));
|
|
1430
1435
|
this.#writtenProgramState = undefined;
|
|
1431
1436
|
return;
|
|
1432
1437
|
}
|
|
1433
1438
|
// Only the session name is exported as metadata; prompt bodies and errors stay in the TUI.
|
|
1434
|
-
const sequence = programStatusSequence({ state: status.state,
|
|
1439
|
+
const sequence = programStatusSequence({ state: status.state,
|
|
1440
|
+
...(status.kind === undefined ? {} : { kind: status.kind }),
|
|
1441
|
+
...(this.#sessionTitle === undefined ? {} : { message: this.#sessionTitle }),
|
|
1442
|
+
});
|
|
1443
|
+
// Supporting terminals can turn these states into desktop notifications.
|
|
1444
|
+
// New attention reports use the same coalescing and focus gate as OSC 9.
|
|
1445
|
+
if (programStatusNeedsAttention(status.state)
|
|
1446
|
+
&& (this.#notificationPolicy === 'off' || !notify && sequence !== this.#writtenProgramState)) {
|
|
1447
|
+
if (this.#writtenProgramState !== undefined)
|
|
1448
|
+
this.#term.output.write(programStatusSequence('clear'));
|
|
1449
|
+
this.#writtenProgramState = undefined;
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1435
1452
|
if (sequence === this.#writtenProgramState)
|
|
1436
1453
|
return;
|
|
1437
1454
|
this.#writtenProgramState = sequence;
|
|
1438
1455
|
this.#term.output.write(sequence);
|
|
1439
1456
|
}
|
|
1457
|
+
get #reportsProgramState() {
|
|
1458
|
+
return this.#terminalState === 'on' || this.#terminalState === 'auto' && this.#programSupported;
|
|
1459
|
+
}
|
|
1460
|
+
#writeWorkingDirectory(cwd) {
|
|
1461
|
+
const sequence = workingDirectorySequence(cwd);
|
|
1462
|
+
if (sequence === this.#writtenDirectory)
|
|
1463
|
+
return;
|
|
1464
|
+
this.#term.output.write(sequence);
|
|
1465
|
+
this.#writtenDirectory = sequence;
|
|
1466
|
+
}
|
|
1467
|
+
#syncProgress(state) {
|
|
1468
|
+
const sequence = this.#terminalState === 'off' ? terminalProgressSequence('clear') : terminalProgressSequence(state);
|
|
1469
|
+
const active = sequence !== terminalProgressSequence('clear');
|
|
1470
|
+
if (sequence !== this.#writtenProgress && (active || this.#writtenProgress !== undefined))
|
|
1471
|
+
this.#term.output.write(sequence);
|
|
1472
|
+
this.#writtenProgress = active ? sequence : undefined;
|
|
1473
|
+
if (this.#progressTimer !== undefined && (state !== 'working' || this.#terminalState === 'off')) {
|
|
1474
|
+
clearInterval(this.#progressTimer);
|
|
1475
|
+
this.#progressTimer = undefined;
|
|
1476
|
+
}
|
|
1477
|
+
// Some terminals expire indeterminate progress without a heartbeat.
|
|
1478
|
+
if (state === 'working' && this.#terminalState !== 'off' && this.#progressTimer === undefined) {
|
|
1479
|
+
this.#progressTimer = setInterval(() => {
|
|
1480
|
+
if (!this.#disposed && !this.#session.handedOff && this.#writtenProgress !== undefined)
|
|
1481
|
+
this.#term.output.write(this.#writtenProgress);
|
|
1482
|
+
}, 1000);
|
|
1483
|
+
this.#progressTimer.unref?.();
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
/** Give shell/editor ownership back without retaining omdsh metadata. */
|
|
1487
|
+
#releaseTerminalMetadata() {
|
|
1488
|
+
if (this.#progressTimer !== undefined)
|
|
1489
|
+
clearInterval(this.#progressTimer);
|
|
1490
|
+
this.#progressTimer = undefined;
|
|
1491
|
+
if (this.#writtenProgress !== undefined)
|
|
1492
|
+
this.#term.output.write(terminalProgressSequence('clear'));
|
|
1493
|
+
this.#writtenProgress = undefined;
|
|
1494
|
+
if (this.#writtenProgramState !== undefined)
|
|
1495
|
+
this.#term.output.write(programStatusSequence('clear'));
|
|
1496
|
+
this.#writtenProgramState = undefined;
|
|
1497
|
+
if (this.#writtenWindowTitle !== undefined && this.#writtenWindowTitle !== '')
|
|
1498
|
+
this.#term.output.write('\x1b]2;\x07');
|
|
1499
|
+
this.#writtenWindowTitle = undefined;
|
|
1500
|
+
if (this.#writtenDirectory !== undefined)
|
|
1501
|
+
this.#writeWorkingDirectory(this.#launchDirectory);
|
|
1502
|
+
this.#writtenDirectory = undefined;
|
|
1503
|
+
}
|
|
1440
1504
|
/**
|
|
1441
1505
|
* Re-derive the color depth from the active color preference. Every path
|
|
1442
1506
|
* that changes `#colors` calls this so `/settings` takes effect immediately.
|
|
@@ -1607,8 +1671,8 @@ export class LocalTui {
|
|
|
1607
1671
|
this.#reportDraftError('restore', error);
|
|
1608
1672
|
}
|
|
1609
1673
|
if (existing !== undefined) {
|
|
1610
|
-
images.replace({ text: existing.editor.text, images: existing.images });
|
|
1611
1674
|
editor.restore(existing.editor);
|
|
1675
|
+
images.restoreSnapshot(existing.images);
|
|
1612
1676
|
files.restoreSnapshot(existing.files);
|
|
1613
1677
|
}
|
|
1614
1678
|
images.restore(submission, files.adopt(submission.files ?? []));
|
|
@@ -1832,7 +1896,9 @@ export class LocalTui {
|
|
|
1832
1896
|
this.#renderer.finish();
|
|
1833
1897
|
}
|
|
1834
1898
|
catch { /* best effort while exiting */ }
|
|
1899
|
+
this.#releaseTerminalMetadata();
|
|
1835
1900
|
this.#session.release();
|
|
1901
|
+
this.#term.input.setRawMode?.(false);
|
|
1836
1902
|
this.#term.output.write('\x1b[?25h');
|
|
1837
1903
|
}
|
|
1838
1904
|
dispose() {
|
|
@@ -1842,6 +1908,7 @@ export class LocalTui {
|
|
|
1842
1908
|
this.#flushDraft();
|
|
1843
1909
|
this.#pendingInputs.flush(this.#pendingScope());
|
|
1844
1910
|
this.#disposed = true;
|
|
1911
|
+
this.#cancelPaste();
|
|
1845
1912
|
this.#imagePreviewAbort?.abort();
|
|
1846
1913
|
this.#fileReviewAbort?.abort();
|
|
1847
1914
|
this.#closeFileBrowser();
|
|
@@ -1894,14 +1961,7 @@ export class LocalTui {
|
|
|
1894
1961
|
// prompt does not overwrite the transcript. Disable bracketed paste
|
|
1895
1962
|
// and restore the cursor.
|
|
1896
1963
|
this.#renderer.finish();
|
|
1897
|
-
|
|
1898
|
-
if (this.#writtenWindowTitle !== undefined && this.#writtenWindowTitle !== '') {
|
|
1899
|
-
this.#term.output.write('\x1b]2;\x07');
|
|
1900
|
-
this.#writtenWindowTitle = undefined;
|
|
1901
|
-
}
|
|
1902
|
-
if (this.#writtenProgramState !== undefined)
|
|
1903
|
-
this.#term.output.write(programStatusSequence('clear'));
|
|
1904
|
-
this.#writtenProgramState = undefined;
|
|
1964
|
+
this.#releaseTerminalMetadata();
|
|
1905
1965
|
this.#offExit?.();
|
|
1906
1966
|
this.#offExit = undefined;
|
|
1907
1967
|
this.#session.dispose();
|
|
@@ -2049,11 +2109,11 @@ export class LocalTui {
|
|
|
2049
2109
|
if (this.#historyIndex > 0 && this.#historyDraft !== undefined)
|
|
2050
2110
|
return this.#historyDraft;
|
|
2051
2111
|
const editor = this.#prompt?.savedInput ?? this.#editor.snapshot();
|
|
2052
|
-
return { editor, images: this.#images.copies(), files: this.#files.snapshot(editor.text), literal: this.#literalInput };
|
|
2112
|
+
return { editor, images: this.#images.copies(editor), files: this.#files.snapshot(editor.text), literal: this.#literalInput };
|
|
2053
2113
|
}
|
|
2054
2114
|
#restoreDraft(draft) {
|
|
2055
|
-
this.#images.replace({ text: draft.editor.text, images: draft.images });
|
|
2056
2115
|
this.#editor.restore(draft.editor);
|
|
2116
|
+
this.#images.restoreSnapshot(draft.images);
|
|
2057
2117
|
this.#files.restoreSnapshot(draft.files);
|
|
2058
2118
|
this.#literalInput = draft.literal;
|
|
2059
2119
|
}
|
|
@@ -2117,6 +2177,7 @@ export class LocalTui {
|
|
|
2117
2177
|
#switchDraft(id) {
|
|
2118
2178
|
if (id === this.#draftScope)
|
|
2119
2179
|
return;
|
|
2180
|
+
this.#cancelPaste();
|
|
2120
2181
|
this.#goalAbort?.abort();
|
|
2121
2182
|
this.#goalAbort = undefined;
|
|
2122
2183
|
this.#queueHotkeyAbort?.abort();
|
|
@@ -2206,9 +2267,9 @@ export class LocalTui {
|
|
|
2206
2267
|
themeName: this.#themeName,
|
|
2207
2268
|
terminal: this.#terminalColors,
|
|
2208
2269
|
...(workingLabel === undefined ? {} : { workingLabel }),
|
|
2270
|
+
...(this.#selection.active ? { selectionActions: this.#canQuoteSelection() ? 'copy-quote' : 'copy' } : {}),
|
|
2209
2271
|
...(this.#transientStatus !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), this.#transientStatus) }
|
|
2210
|
-
: this.#selection.active ? {
|
|
2211
|
-
: this.#processSelection !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), 'Process · ↑↓ select · Enter toggle · {key} actions · Home title · Esc return', { key: keysForAction(this.#keybindings, 'tool-actions') }) } : {}),
|
|
2272
|
+
: !this.#selection.active && this.#processSelection !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), 'Process · ↑↓ select · Enter toggle · {key} actions · Home title · Esc return', { key: keysForAction(this.#keybindings, 'tool-actions') }) } : {}),
|
|
2212
2273
|
scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
|
|
2213
2274
|
...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start', ...(this.#focusBlockOffset === undefined ? {} : { focusBlockOffset: this.#focusBlockOffset }) }),
|
|
2214
2275
|
...(this.#transcriptSearch === null ? {} : {
|
|
@@ -2385,20 +2446,31 @@ export class LocalTui {
|
|
|
2385
2446
|
/** Give every terminal mode back before the terminal is lent out (Ctrl+Z, external editor). */
|
|
2386
2447
|
#handOffTerminal() {
|
|
2387
2448
|
this.#setMouseTracking(false);
|
|
2449
|
+
this.#releaseTerminalMetadata();
|
|
2388
2450
|
this.#session.handOff();
|
|
2451
|
+
this.#term.input.setRawMode?.(false);
|
|
2452
|
+
}
|
|
2453
|
+
#takeBackTerminal() {
|
|
2454
|
+
this.#session.takeBack();
|
|
2455
|
+
this.#term.input.setRawMode?.(true);
|
|
2456
|
+
this.#terminalFocused = undefined;
|
|
2457
|
+
this.#writtenWindowTitle = undefined;
|
|
2458
|
+
this.#writtenProgramState = undefined;
|
|
2459
|
+
this.#writtenDirectory = undefined;
|
|
2460
|
+
this.#programQuerySent = false;
|
|
2461
|
+
this.#programSupported = false;
|
|
2462
|
+
this.#syncWindowTitle();
|
|
2463
|
+
this.#renderer.reset();
|
|
2389
2464
|
}
|
|
2390
2465
|
/** Run `run` with the terminal handed off; take it back and repaint afterwards. */
|
|
2391
2466
|
#handTerminal(run) {
|
|
2392
2467
|
this.#handOffTerminal();
|
|
2393
|
-
this.#term.input.setRawMode?.(false);
|
|
2394
2468
|
try {
|
|
2395
2469
|
return run();
|
|
2396
2470
|
}
|
|
2397
2471
|
finally {
|
|
2398
2472
|
if (!this.#disposed) {
|
|
2399
|
-
this.#
|
|
2400
|
-
this.#session.takeBack();
|
|
2401
|
-
this.#renderer.reset();
|
|
2473
|
+
this.#takeBackTerminal();
|
|
2402
2474
|
}
|
|
2403
2475
|
}
|
|
2404
2476
|
}
|
|
@@ -2509,14 +2581,26 @@ export class LocalTui {
|
|
|
2509
2581
|
for (const event of this.#pasteBurst.flush(Date.now(), force))
|
|
2510
2582
|
this.#deliverPasteEmission(event);
|
|
2511
2583
|
}
|
|
2584
|
+
#cancelPaste() {
|
|
2585
|
+
this.#pasteAbort?.abort();
|
|
2586
|
+
this.#pasteAbort = undefined;
|
|
2587
|
+
this.#pasteInFlight = 0;
|
|
2588
|
+
this.#deferredPasteEvents = [];
|
|
2589
|
+
this.#paste = false;
|
|
2590
|
+
this.#pasteBuf = '';
|
|
2591
|
+
}
|
|
2512
2592
|
#startAsyncPaste(operation) {
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2593
|
+
const abort = new AbortController();
|
|
2594
|
+
this.#pasteAbort = abort;
|
|
2595
|
+
this.#pasteInFlight = 1;
|
|
2596
|
+
void operation(abort.signal).catch((error) => {
|
|
2597
|
+
if (!abort.signal.aborted)
|
|
2598
|
+
this.notice('Paste failed: ' + (error instanceof Error ? error.message : String(error)), { level: 'error' });
|
|
2516
2599
|
}).finally(() => {
|
|
2517
|
-
|
|
2518
|
-
if (this.#pasteInFlight !== 0 || this.#deferredPasteEvents.length === 0)
|
|
2600
|
+
if (this.#pasteAbort !== abort)
|
|
2519
2601
|
return;
|
|
2602
|
+
this.#pasteAbort = undefined;
|
|
2603
|
+
this.#pasteInFlight = 0;
|
|
2520
2604
|
const deferred = this.#deferredPasteEvents.splice(0);
|
|
2521
2605
|
for (let index = 0; index < deferred.length; index += 1) {
|
|
2522
2606
|
this.#dispatch(deferred[index]);
|
|
@@ -2527,7 +2611,10 @@ export class LocalTui {
|
|
|
2527
2611
|
}
|
|
2528
2612
|
});
|
|
2529
2613
|
}
|
|
2530
|
-
async #acceptPastedText(text) {
|
|
2614
|
+
async #acceptPastedText(text, signal) {
|
|
2615
|
+
if (signal.aborted)
|
|
2616
|
+
return;
|
|
2617
|
+
text = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
2531
2618
|
if (this.#fileBrowser !== null) {
|
|
2532
2619
|
this.#applyFileBrowserInput({ type: 'text', value: text });
|
|
2533
2620
|
return;
|
|
@@ -2555,42 +2642,50 @@ export class LocalTui {
|
|
|
2555
2642
|
}
|
|
2556
2643
|
const paths = imagePathCandidates(text);
|
|
2557
2644
|
if (paths.length > 0) {
|
|
2558
|
-
const images = await Promise.all(paths.map(path =>
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
for (const image of images) {
|
|
2562
|
-
if (await this.#images.admit(image)) {
|
|
2563
|
-
this.#images.insert(image);
|
|
2564
|
-
inserted = true;
|
|
2565
|
-
}
|
|
2645
|
+
const images = await Promise.all(paths.map(async (path) => {
|
|
2646
|
+
try {
|
|
2647
|
+
return await this.#readImagePath(path);
|
|
2566
2648
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
this.#render();
|
|
2649
|
+
catch {
|
|
2650
|
+
return null;
|
|
2570
2651
|
}
|
|
2652
|
+
}));
|
|
2653
|
+
if (signal.aborted)
|
|
2571
2654
|
return;
|
|
2655
|
+
let inserted = false;
|
|
2656
|
+
for (const [index, image] of images.entries()) {
|
|
2657
|
+
if (image === null) {
|
|
2658
|
+
this.notice(uiText(resolveUiLanguage(this.#uiLanguage), 'Could not read image {path}', { path: paths[index] }), { level: 'error' });
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
const admitted = await this.#images.admit(image, signal);
|
|
2662
|
+
if (signal.aborted)
|
|
2663
|
+
return;
|
|
2664
|
+
if (admitted) {
|
|
2665
|
+
this.#images.insert(image);
|
|
2666
|
+
inserted = true;
|
|
2667
|
+
}
|
|
2572
2668
|
}
|
|
2573
|
-
|
|
2574
|
-
// it before Node reads it. OMP falls back to the still-live clipboard
|
|
2575
|
-
// image instead of leaking that stale path into the prompt.
|
|
2576
|
-
const clipboardImage = await this.#readClipboardImage();
|
|
2577
|
-
if (clipboardImage !== null) {
|
|
2578
|
-
this.#images.insert(clipboardImage);
|
|
2669
|
+
if (inserted)
|
|
2579
2670
|
this.#refreshAutocomplete();
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
}
|
|
2671
|
+
this.#render();
|
|
2672
|
+
return;
|
|
2583
2673
|
}
|
|
2584
2674
|
this.#literalInput ||= this.#editor.cursor === 0 && text.startsWith('!');
|
|
2585
2675
|
this.#editor.paste(text);
|
|
2586
2676
|
this.#refreshAutocomplete();
|
|
2587
2677
|
this.#render();
|
|
2588
2678
|
}
|
|
2589
|
-
async #pasteClipboard() {
|
|
2590
|
-
if (this.#search === null && this.#settings === null) {
|
|
2679
|
+
async #pasteClipboard(signal) {
|
|
2680
|
+
if (this.#prompt === null && this.#search === null && this.#settings === null) {
|
|
2591
2681
|
const image = await this.#readClipboardImage();
|
|
2682
|
+
if (signal.aborted)
|
|
2683
|
+
return;
|
|
2592
2684
|
if (image !== null) {
|
|
2593
|
-
|
|
2685
|
+
const admitted = await this.#images.admit(image, signal);
|
|
2686
|
+
if (signal.aborted)
|
|
2687
|
+
return;
|
|
2688
|
+
if (admitted) {
|
|
2594
2689
|
this.#images.insert(image);
|
|
2595
2690
|
this.#refreshAutocomplete();
|
|
2596
2691
|
this.#render();
|
|
@@ -2598,29 +2693,17 @@ export class LocalTui {
|
|
|
2598
2693
|
return;
|
|
2599
2694
|
}
|
|
2600
2695
|
const files = await this.#readClipboardFiles();
|
|
2696
|
+
if (signal.aborted)
|
|
2697
|
+
return;
|
|
2601
2698
|
const imagePaths = files.filter(path => imagePathCandidates(path).length === 1);
|
|
2602
2699
|
if (imagePaths.length > 0) {
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
if (images.length > 0) {
|
|
2606
|
-
let inserted = false;
|
|
2607
|
-
for (const candidate of images) {
|
|
2608
|
-
if (await this.#images.admit(candidate)) {
|
|
2609
|
-
this.#images.insert(candidate);
|
|
2610
|
-
inserted = true;
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
if (inserted) {
|
|
2614
|
-
this.#refreshAutocomplete();
|
|
2615
|
-
this.#render();
|
|
2616
|
-
}
|
|
2617
|
-
return;
|
|
2618
|
-
}
|
|
2700
|
+
await this.#acceptPastedText(imagePaths.join('\n'), signal);
|
|
2701
|
+
return;
|
|
2619
2702
|
}
|
|
2620
2703
|
}
|
|
2621
2704
|
const text = await this.#readClipboard();
|
|
2622
|
-
if (text !== '')
|
|
2623
|
-
await this.#acceptPastedText(text);
|
|
2705
|
+
if (text !== '' && !signal.aborted)
|
|
2706
|
+
await this.#acceptPastedText(text, signal);
|
|
2624
2707
|
}
|
|
2625
2708
|
/** Resolve one bounded tmux probe without overwriting an explicit preference. */
|
|
2626
2709
|
setTmuxMouseEnabled(enabled) {
|
|
@@ -2803,7 +2886,7 @@ export class LocalTui {
|
|
|
2803
2886
|
this.#workspacePress = undefined;
|
|
2804
2887
|
if (event.button === 'left' && !event.shift && target?.key === pressedAction.key && target.row === pressedAction.row && target.column === pressedAction.column) {
|
|
2805
2888
|
if (target.action === 'copy')
|
|
2806
|
-
void this.#copySelection(
|
|
2889
|
+
void this.#copySelection();
|
|
2807
2890
|
else if (target.action === 'quote')
|
|
2808
2891
|
this.#quoteSelection();
|
|
2809
2892
|
else {
|
|
@@ -2835,7 +2918,7 @@ export class LocalTui {
|
|
|
2835
2918
|
this.#disclosurePress = undefined;
|
|
2836
2919
|
if (event.action === 'press') {
|
|
2837
2920
|
if (this.#selection.active)
|
|
2838
|
-
void this.#copySelection(
|
|
2921
|
+
void this.#copySelection();
|
|
2839
2922
|
else {
|
|
2840
2923
|
const region = this.#fileRegionAt(event.row);
|
|
2841
2924
|
if (region !== undefined) {
|
|
@@ -2910,7 +2993,7 @@ export class LocalTui {
|
|
|
2910
2993
|
#handleTrajectoryMouse(event) {
|
|
2911
2994
|
if (event.button === 'right') {
|
|
2912
2995
|
if (event.action === 'press' && this.#selection.active)
|
|
2913
|
-
void this.#copySelection(
|
|
2996
|
+
void this.#copySelection();
|
|
2914
2997
|
return;
|
|
2915
2998
|
}
|
|
2916
2999
|
if (event.action === 'press') {
|
|
@@ -2982,7 +3065,7 @@ export class LocalTui {
|
|
|
2982
3065
|
this.#copyTask = task.catch(() => { });
|
|
2983
3066
|
return task;
|
|
2984
3067
|
}
|
|
2985
|
-
async #copySelection(
|
|
3068
|
+
async #copySelection() {
|
|
2986
3069
|
if (!this.#selection.valid(this.#renderer.viewport())) {
|
|
2987
3070
|
this.#selection.clear();
|
|
2988
3071
|
return;
|
|
@@ -2992,16 +3075,13 @@ export class LocalTui {
|
|
|
2992
3075
|
return;
|
|
2993
3076
|
try {
|
|
2994
3077
|
const delivery = await this.#deliverCopy(text);
|
|
2995
|
-
if (
|
|
3078
|
+
if (!this.#disposed && this.#selection.text() === text) {
|
|
2996
3079
|
this.#showTransientStatus(delivery === 'requested' ? 'Selection sent to terminal clipboard' : 'Copied selection');
|
|
2997
|
-
this.#render();
|
|
2998
3080
|
}
|
|
2999
3081
|
}
|
|
3000
3082
|
catch {
|
|
3001
|
-
if (!this.#disposed)
|
|
3083
|
+
if (!this.#disposed && this.#selection.text() === text)
|
|
3002
3084
|
this.#showTransientStatus('Copy failed');
|
|
3003
|
-
this.#render();
|
|
3004
|
-
}
|
|
3005
3085
|
}
|
|
3006
3086
|
}
|
|
3007
3087
|
#canQuoteSelection() {
|
|
@@ -3163,7 +3243,7 @@ export class LocalTui {
|
|
|
3163
3243
|
return;
|
|
3164
3244
|
}
|
|
3165
3245
|
if (event.type === 'key' && (['super+c', 'enter'].includes(event.id) || this.#boundAction(event.id) === 'copy-prompt')) {
|
|
3166
|
-
void this.#copySelection(
|
|
3246
|
+
void this.#copySelection();
|
|
3167
3247
|
return;
|
|
3168
3248
|
}
|
|
3169
3249
|
this.#selection.clear();
|
|
@@ -3208,6 +3288,8 @@ export class LocalTui {
|
|
|
3208
3288
|
// Clipboard image inspection is asynchronous. Preserve the exact key
|
|
3209
3289
|
// order so a fast Ctrl+V, Enter submits the finished image draft rather
|
|
3210
3290
|
// than an empty prompt.
|
|
3291
|
+
if (this.#pasteInFlight > 0 && event.type === 'key' && (event.id === 'ctrl+c' || event.id === 'escape'))
|
|
3292
|
+
this.#cancelPaste();
|
|
3211
3293
|
if (this.#pasteInFlight > 0) {
|
|
3212
3294
|
this.#deferredPasteEvents.push(event);
|
|
3213
3295
|
return;
|
|
@@ -3218,7 +3300,7 @@ export class LocalTui {
|
|
|
3218
3300
|
const text = this.#pasteBuf.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
3219
3301
|
this.#pasteBuf = '';
|
|
3220
3302
|
if (text !== '')
|
|
3221
|
-
this.#startAsyncPaste(this.#acceptPastedText(text));
|
|
3303
|
+
this.#startAsyncPaste(signal => this.#acceptPastedText(text, signal));
|
|
3222
3304
|
return;
|
|
3223
3305
|
}
|
|
3224
3306
|
if (event.type === 'text')
|
|
@@ -3996,7 +4078,6 @@ export class LocalTui {
|
|
|
3996
4078
|
this.#externalEditor = prefs.editor ?? 'auto';
|
|
3997
4079
|
this.#terminalTitle = prefs.terminalTitle ?? 'status';
|
|
3998
4080
|
this.#terminalState = prefs.terminalState ?? 'auto';
|
|
3999
|
-
this.#syncWindowTitle();
|
|
4000
4081
|
this.#copyOnSelect = prefs.copyOnSelect ?? true;
|
|
4001
4082
|
this.#mathMode = prefs.mathMode ?? 'auto';
|
|
4002
4083
|
this.#mermaidMode = prefs.mermaidMode ?? 'auto';
|
|
@@ -4009,6 +4090,7 @@ export class LocalTui {
|
|
|
4009
4090
|
if (this.#notificationPolicy === 'off')
|
|
4010
4091
|
this.#notificationQueue.clear();
|
|
4011
4092
|
this.#configureNotifications();
|
|
4093
|
+
this.#syncWindowTitle();
|
|
4012
4094
|
this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
|
|
4013
4095
|
if (options.forceToolsSync)
|
|
4014
4096
|
this.#toolsExpanded = false;
|
|
@@ -4504,7 +4586,10 @@ export class LocalTui {
|
|
|
4504
4586
|
try {
|
|
4505
4587
|
process.kill(process.pid, 'SIGTSTP');
|
|
4506
4588
|
}
|
|
4507
|
-
catch {
|
|
4589
|
+
catch {
|
|
4590
|
+
this.#takeBackTerminal();
|
|
4591
|
+
this.#render();
|
|
4592
|
+
}
|
|
4508
4593
|
}
|
|
4509
4594
|
#historyPrev() {
|
|
4510
4595
|
if (this.#images.count > 0)
|
|
@@ -4606,11 +4691,20 @@ export class LocalTui {
|
|
|
4606
4691
|
#buildSubmission(text) {
|
|
4607
4692
|
const images = this.#images.copies();
|
|
4608
4693
|
const files = this.#files.copies();
|
|
4609
|
-
const
|
|
4694
|
+
const markers = this.#files.snapshot(this.#editor.text).entries.map(item => item.marker);
|
|
4695
|
+
const ranges = this.#images.ranges();
|
|
4696
|
+
for (const [index, marker] of markers.entries()) {
|
|
4697
|
+
const start = text.indexOf(marker);
|
|
4698
|
+
if (start < 0)
|
|
4699
|
+
continue;
|
|
4700
|
+
const end = start + marker.length;
|
|
4701
|
+
ranges.push({ kind: 'file', index, start: start > 0 && text[start - 1] === ' ' ? start - 1 : start,
|
|
4702
|
+
end: text[end] === ' ' ? end + 1 : end });
|
|
4703
|
+
}
|
|
4610
4704
|
const segments = images.length === 0 && files.length === 0 ? []
|
|
4611
|
-
: splitComposerAttachmentMarkers(text
|
|
4612
|
-
|
|
4613
|
-
|
|
4705
|
+
: splitComposerAttachmentMarkers(text, images, markers, images.length > 0 && text === this.#editor.expandedText ? ranges : undefined);
|
|
4706
|
+
const submittedText = images.length === 0 ? this.#files.text(text)
|
|
4707
|
+
: segments.flatMap(segment => segment.kind === 'text' ? [segment.text] : []).join('');
|
|
4614
4708
|
const literal = text === this.#editor.expandedText && (this.#literalInput || this.#editor.startsWithPaste);
|
|
4615
4709
|
const historyText = submittedText.replace(/[ \t]{2,}/gu, ' ').trim();
|
|
4616
4710
|
return {
|
|
@@ -4829,6 +4923,7 @@ export class LocalTui {
|
|
|
4829
4923
|
const pending = this.#prompt;
|
|
4830
4924
|
if (pending === null)
|
|
4831
4925
|
return;
|
|
4926
|
+
this.#cancelPaste();
|
|
4832
4927
|
const draft = structuredPromptAnswer(pending, this.#editor.expandedText);
|
|
4833
4928
|
try {
|
|
4834
4929
|
pending.request.onAnswerDraft?.(draft);
|
|
@@ -4852,6 +4947,8 @@ export class LocalTui {
|
|
|
4852
4947
|
this.#wheelGesture = IDLE_WHEEL;
|
|
4853
4948
|
if (pending.request.notify !== false) {
|
|
4854
4949
|
this.#herdr.promptResolved();
|
|
4950
|
+
if (this.#herdr.programStatus.state !== 'blocked')
|
|
4951
|
+
this.#notificationQueue.clear('blocked');
|
|
4855
4952
|
this.#syncWindowTitle();
|
|
4856
4953
|
}
|
|
4857
4954
|
pending.offAbort?.();
|
|
@@ -4979,6 +5076,7 @@ export class LocalTui {
|
|
|
4979
5076
|
}
|
|
4980
5077
|
this.#search = null;
|
|
4981
5078
|
this.#ac = null;
|
|
5079
|
+
this.#cancelPaste();
|
|
4982
5080
|
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior, this.#features, this.#pluginSettings?.entries(), discoverEditors(this.#externalEditor), this.#settingsActions());
|
|
4983
5081
|
this.#refreshSettingsCatalog();
|
|
4984
5082
|
this.#render();
|
|
@@ -5063,10 +5161,18 @@ export class LocalTui {
|
|
|
5063
5161
|
const pending = this.#notificationQueue.take();
|
|
5064
5162
|
if (pending === undefined || this.#disposed || this.#notificationPolicy === 'off')
|
|
5065
5163
|
return;
|
|
5164
|
+
if (this.#session.handedOff)
|
|
5165
|
+
return;
|
|
5066
5166
|
// Re-read focus at delivery time: the reader may have come back inside the
|
|
5067
5167
|
// coalescing window, and a turn finishing while they watch is not news.
|
|
5068
5168
|
if (this.#notificationPolicy === 'unfocused' && this.#terminalFocused === true)
|
|
5069
5169
|
return;
|
|
5170
|
+
if (pending.programState !== undefined && pending.programState !== this.#herdr.programStatus.state)
|
|
5171
|
+
return;
|
|
5172
|
+
if (this.#reportsProgramState && programStatusNeedsAttention(this.#herdr.programStatus.state)) {
|
|
5173
|
+
this.#syncWindowTitle(true);
|
|
5174
|
+
return;
|
|
5175
|
+
}
|
|
5070
5176
|
const sequence = terminalNotificationSequence(pending);
|
|
5071
5177
|
if (sequence !== '')
|
|
5072
5178
|
this.#term.output.write(sequence);
|
|
@@ -5391,7 +5497,7 @@ export class LocalTui {
|
|
|
5391
5497
|
return true;
|
|
5392
5498
|
}
|
|
5393
5499
|
if (action === 'paste-clipboard') {
|
|
5394
|
-
this.#startAsyncPaste(this.#pasteClipboard());
|
|
5500
|
+
this.#startAsyncPaste(signal => this.#pasteClipboard(signal));
|
|
5395
5501
|
return true;
|
|
5396
5502
|
}
|
|
5397
5503
|
// Explicit queue delivery is independent of the busy-Enter preference.
|
|
@@ -5407,7 +5513,8 @@ export class LocalTui {
|
|
|
5407
5513
|
this.#handTerminal(() => {
|
|
5408
5514
|
this.#literalInput ||= this.#editor.startsWithPaste;
|
|
5409
5515
|
const text = editExternally(this.#editor.expandedText, this.#externalEditor);
|
|
5410
|
-
this.#editor.
|
|
5516
|
+
while (this.#editor.expandPaste()) { /* Keep marker positions aligned with expanded source. */ }
|
|
5517
|
+
this.#editor.replaceText(text);
|
|
5411
5518
|
this.#images.reconcile();
|
|
5412
5519
|
});
|
|
5413
5520
|
}
|