@agi-fans/dsh-tui 0.23.1 → 0.24.1
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 +37 -2
- 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/commands/diff.js +58 -33
- package/lib/commands/diff.js.map +1 -1
- package/lib/definition.d.ts +7 -0
- 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 +20 -3
- package/lib/input/image-paste.js +67 -14
- package/lib/input/image-paste.js.map +1 -1
- package/lib/locale/zh.d.ts +27 -6
- package/lib/locale/zh.js +27 -6
- package/lib/locale/zh.js.map +1 -1
- package/lib/runtime/account-settings.d.ts +1 -0
- package/lib/runtime/account-settings.js +2 -2
- package/lib/runtime/account-settings.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/model-settings.d.ts +2 -0
- package/lib/runtime/model-settings.js +2 -2
- package/lib/runtime/model-settings.js.map +1 -1
- package/lib/runtime/plugin-settings.d.ts +1 -0
- package/lib/runtime/plugin-settings.js +6 -6
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +3 -0
- package/lib/runtime/provider-local.js +171 -77
- 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/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/session/composer-drafts.js +25 -1
- package/lib/session/composer-drafts.js.map +1 -1
- package/lib/session/feature-toggles.js +2 -2
- package/lib/session/feature-toggles.js.map +1 -1
- package/lib/session/session-controller.js +2 -3
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/tui-settings.d.ts +2 -0
- package/lib/session/tui-settings.js +1 -0
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/views/settings-list.d.ts +5 -0
- package/lib/views/settings-list.js +46 -10
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/transcript-render.d.ts +2 -0
- package/lib/views/transcript-render.js +41 -27
- package/lib/views/transcript-render.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -414,6 +415,9 @@ export class LocalTui {
|
|
|
414
415
|
#mermaidMode = 'auto';
|
|
415
416
|
/** Transcript collapses every thought to one line; Ctrl+T flips it. */
|
|
416
417
|
#hideThinking = true;
|
|
418
|
+
#showFileChanges = true;
|
|
419
|
+
#workspaceReview;
|
|
420
|
+
#workspacePress;
|
|
417
421
|
#mouseInteraction = 'auto';
|
|
418
422
|
#tmuxMouseEnabled;
|
|
419
423
|
#disclosurePress;
|
|
@@ -598,6 +602,7 @@ export class LocalTui {
|
|
|
598
602
|
// live window so it stays anchored at the bottom.
|
|
599
603
|
this.#selection.clear();
|
|
600
604
|
this.#mouseActionPress = undefined;
|
|
605
|
+
this.#workspacePress = undefined;
|
|
601
606
|
this.#jumpPress = undefined;
|
|
602
607
|
this.#disclosurePress = undefined;
|
|
603
608
|
const repaint = () => {
|
|
@@ -859,6 +864,7 @@ export class LocalTui {
|
|
|
859
864
|
&& this.#inspected?.writable === inspected?.writable)
|
|
860
865
|
return;
|
|
861
866
|
if (this.#inspected?.id !== inspected?.id) {
|
|
867
|
+
this.#workspacePress = undefined;
|
|
862
868
|
this.#fileReviewAbort?.abort();
|
|
863
869
|
this.#closeFileBrowser();
|
|
864
870
|
}
|
|
@@ -884,8 +890,10 @@ export class LocalTui {
|
|
|
884
890
|
this.#render();
|
|
885
891
|
}
|
|
886
892
|
async reviewFiles(request) {
|
|
893
|
+
this.#cancelPaste();
|
|
887
894
|
if (!this.#tty || this.#disposed || request.signal.aborted)
|
|
888
895
|
return;
|
|
896
|
+
this.#workspacePress = undefined;
|
|
889
897
|
this.#flushPasteBurst(true);
|
|
890
898
|
this.#pasteBurst.clear();
|
|
891
899
|
this.#finishPrompt(null);
|
|
@@ -1220,7 +1228,8 @@ export class LocalTui {
|
|
|
1220
1228
|
const browserState = promptBrowserState(request);
|
|
1221
1229
|
request = localizePrompt(request, resolveUiLanguage(this.#uiLanguage));
|
|
1222
1230
|
// Background approvals take the terminal from a user-opened file reader.
|
|
1223
|
-
if (this.#fileReviewAbort !== undefined && request.signal !== this.#fileReviewAbort.signal
|
|
1231
|
+
if (this.#fileReviewAbort !== undefined && request.signal !== this.#fileReviewAbort.signal
|
|
1232
|
+
&& request.signal !== this.#fileBrowser?.request.signal)
|
|
1224
1233
|
this.#fileReviewAbort.abort();
|
|
1225
1234
|
if (this.#disposed || request.signal?.aborted === true)
|
|
1226
1235
|
return Promise.resolve(null);
|
|
@@ -1237,6 +1246,7 @@ export class LocalTui {
|
|
|
1237
1246
|
this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
1238
1247
|
}
|
|
1239
1248
|
}
|
|
1249
|
+
this.#cancelPaste();
|
|
1240
1250
|
this.#flushPasteBurst(true);
|
|
1241
1251
|
this.#pasteBurst.clear();
|
|
1242
1252
|
if (request.notify !== false) {
|
|
@@ -1251,6 +1261,7 @@ export class LocalTui {
|
|
|
1251
1261
|
this.#promptChoicePress = undefined;
|
|
1252
1262
|
this.#disclosurePress = undefined;
|
|
1253
1263
|
this.#mouseActionPress = undefined;
|
|
1264
|
+
this.#workspacePress = undefined;
|
|
1254
1265
|
this.#jumpPress = undefined;
|
|
1255
1266
|
this.#wheelGesture = IDLE_WHEEL;
|
|
1256
1267
|
const savedInput = this.#editor.snapshot();
|
|
@@ -1341,6 +1352,7 @@ export class LocalTui {
|
|
|
1341
1352
|
});
|
|
1342
1353
|
}
|
|
1343
1354
|
replaceSession(events, presentations, status = 'idle', reason = 'open') {
|
|
1355
|
+
this.#workspacePress = undefined;
|
|
1344
1356
|
if (reason !== 'refresh') {
|
|
1345
1357
|
this.#fileReviewAbort?.abort();
|
|
1346
1358
|
this.#closeFileBrowser();
|
|
@@ -1372,6 +1384,7 @@ export class LocalTui {
|
|
|
1372
1384
|
}
|
|
1373
1385
|
setSession(info) {
|
|
1374
1386
|
if (info.id !== this.#sessionId) {
|
|
1387
|
+
this.#workspacePress = undefined;
|
|
1375
1388
|
this.#fileReviewAbort?.abort();
|
|
1376
1389
|
this.#closeFileBrowser();
|
|
1377
1390
|
}
|
|
@@ -1456,6 +1469,7 @@ export class LocalTui {
|
|
|
1456
1469
|
mathMode: this.#mathMode,
|
|
1457
1470
|
mermaidMode: this.#mermaidMode,
|
|
1458
1471
|
hideThinking: this.#hideThinking,
|
|
1472
|
+
showFileChanges: this.#showFileChanges,
|
|
1459
1473
|
editor: this.#externalEditor,
|
|
1460
1474
|
terminalTitle: this.#terminalTitle,
|
|
1461
1475
|
terminalState: this.#terminalState,
|
|
@@ -1596,8 +1610,8 @@ export class LocalTui {
|
|
|
1596
1610
|
this.#reportDraftError('restore', error);
|
|
1597
1611
|
}
|
|
1598
1612
|
if (existing !== undefined) {
|
|
1599
|
-
images.replace({ text: existing.editor.text, images: existing.images });
|
|
1600
1613
|
editor.restore(existing.editor);
|
|
1614
|
+
images.restoreSnapshot(existing.images);
|
|
1601
1615
|
files.restoreSnapshot(existing.files);
|
|
1602
1616
|
}
|
|
1603
1617
|
images.restore(submission, files.adopt(submission.files ?? []));
|
|
@@ -1743,6 +1757,15 @@ export class LocalTui {
|
|
|
1743
1757
|
return pending.queued.length === 0 && pending.sending.size === 0
|
|
1744
1758
|
&& this.#state.nextTurnInbox.length === 0 && !this.#state.nextStepInbox.some(message => message.source.kind === 'user');
|
|
1745
1759
|
}
|
|
1760
|
+
setWorkspaceReviewHandler(handler) {
|
|
1761
|
+
this.#workspaceReview = handler;
|
|
1762
|
+
return () => {
|
|
1763
|
+
if (this.#workspaceReview === handler) {
|
|
1764
|
+
this.#workspaceReview = undefined;
|
|
1765
|
+
this.#workspacePress = undefined;
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1746
1769
|
setGoalHandler(handler) {
|
|
1747
1770
|
this.#goalHandler = handler;
|
|
1748
1771
|
this.#syncSettingsActions();
|
|
@@ -1822,6 +1845,7 @@ export class LocalTui {
|
|
|
1822
1845
|
this.#flushDraft();
|
|
1823
1846
|
this.#pendingInputs.flush(this.#pendingScope());
|
|
1824
1847
|
this.#disposed = true;
|
|
1848
|
+
this.#cancelPaste();
|
|
1825
1849
|
this.#imagePreviewAbort?.abort();
|
|
1826
1850
|
this.#fileReviewAbort?.abort();
|
|
1827
1851
|
this.#closeFileBrowser();
|
|
@@ -2029,11 +2053,11 @@ export class LocalTui {
|
|
|
2029
2053
|
if (this.#historyIndex > 0 && this.#historyDraft !== undefined)
|
|
2030
2054
|
return this.#historyDraft;
|
|
2031
2055
|
const editor = this.#prompt?.savedInput ?? this.#editor.snapshot();
|
|
2032
|
-
return { editor, images: this.#images.copies(), files: this.#files.snapshot(editor.text), literal: this.#literalInput };
|
|
2056
|
+
return { editor, images: this.#images.copies(editor), files: this.#files.snapshot(editor.text), literal: this.#literalInput };
|
|
2033
2057
|
}
|
|
2034
2058
|
#restoreDraft(draft) {
|
|
2035
|
-
this.#images.replace({ text: draft.editor.text, images: draft.images });
|
|
2036
2059
|
this.#editor.restore(draft.editor);
|
|
2060
|
+
this.#images.restoreSnapshot(draft.images);
|
|
2037
2061
|
this.#files.restoreSnapshot(draft.files);
|
|
2038
2062
|
this.#literalInput = draft.literal;
|
|
2039
2063
|
}
|
|
@@ -2097,6 +2121,7 @@ export class LocalTui {
|
|
|
2097
2121
|
#switchDraft(id) {
|
|
2098
2122
|
if (id === this.#draftScope)
|
|
2099
2123
|
return;
|
|
2124
|
+
this.#cancelPaste();
|
|
2100
2125
|
this.#goalAbort?.abort();
|
|
2101
2126
|
this.#goalAbort = undefined;
|
|
2102
2127
|
this.#queueHotkeyAbort?.abort();
|
|
@@ -2161,6 +2186,7 @@ export class LocalTui {
|
|
|
2161
2186
|
mathMode: this.#mathMode,
|
|
2162
2187
|
mermaidMode: this.#mermaidMode,
|
|
2163
2188
|
hideThinking: true,
|
|
2189
|
+
showFileChanges: this.#showFileChanges,
|
|
2164
2190
|
height: this.#term.height(),
|
|
2165
2191
|
model: this.#model,
|
|
2166
2192
|
...(this.#reasoningEffort === undefined ? {} : { reasoningEffort: this.#reasoningEffort }),
|
|
@@ -2286,7 +2312,8 @@ export class LocalTui {
|
|
|
2286
2312
|
return Math.max(1, this.#scrollBudget > 2 ? this.#scrollBudget - 2 : 1);
|
|
2287
2313
|
}
|
|
2288
2314
|
#scrollBy(delta) {
|
|
2289
|
-
|
|
2315
|
+
this.#workspacePress = undefined;
|
|
2316
|
+
if (this.#follow && (delta > 0 || this.#maxStart === 0))
|
|
2290
2317
|
return;
|
|
2291
2318
|
if (delta === 0 && this.#maxStart === 0)
|
|
2292
2319
|
return;
|
|
@@ -2349,6 +2376,7 @@ export class LocalTui {
|
|
|
2349
2376
|
enabled = enabled && this.#mouseInteraction !== 'native'
|
|
2350
2377
|
&& (this.#mouseInteraction === 'tui' || this.#tmuxMouseEnabled !== false);
|
|
2351
2378
|
if (!enabled) {
|
|
2379
|
+
this.#workspacePress = undefined;
|
|
2352
2380
|
this.#mouseActionPress = undefined;
|
|
2353
2381
|
this.#selection.clear();
|
|
2354
2382
|
this.#jumpPress = undefined;
|
|
@@ -2486,14 +2514,26 @@ export class LocalTui {
|
|
|
2486
2514
|
for (const event of this.#pasteBurst.flush(Date.now(), force))
|
|
2487
2515
|
this.#deliverPasteEmission(event);
|
|
2488
2516
|
}
|
|
2517
|
+
#cancelPaste() {
|
|
2518
|
+
this.#pasteAbort?.abort();
|
|
2519
|
+
this.#pasteAbort = undefined;
|
|
2520
|
+
this.#pasteInFlight = 0;
|
|
2521
|
+
this.#deferredPasteEvents = [];
|
|
2522
|
+
this.#paste = false;
|
|
2523
|
+
this.#pasteBuf = '';
|
|
2524
|
+
}
|
|
2489
2525
|
#startAsyncPaste(operation) {
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2526
|
+
const abort = new AbortController();
|
|
2527
|
+
this.#pasteAbort = abort;
|
|
2528
|
+
this.#pasteInFlight = 1;
|
|
2529
|
+
void operation(abort.signal).catch((error) => {
|
|
2530
|
+
if (!abort.signal.aborted)
|
|
2531
|
+
this.notice('Paste failed: ' + (error instanceof Error ? error.message : String(error)), { level: 'error' });
|
|
2493
2532
|
}).finally(() => {
|
|
2494
|
-
|
|
2495
|
-
if (this.#pasteInFlight !== 0 || this.#deferredPasteEvents.length === 0)
|
|
2533
|
+
if (this.#pasteAbort !== abort)
|
|
2496
2534
|
return;
|
|
2535
|
+
this.#pasteAbort = undefined;
|
|
2536
|
+
this.#pasteInFlight = 0;
|
|
2497
2537
|
const deferred = this.#deferredPasteEvents.splice(0);
|
|
2498
2538
|
for (let index = 0; index < deferred.length; index += 1) {
|
|
2499
2539
|
this.#dispatch(deferred[index]);
|
|
@@ -2504,7 +2544,10 @@ export class LocalTui {
|
|
|
2504
2544
|
}
|
|
2505
2545
|
});
|
|
2506
2546
|
}
|
|
2507
|
-
async #acceptPastedText(text) {
|
|
2547
|
+
async #acceptPastedText(text, signal) {
|
|
2548
|
+
if (signal.aborted)
|
|
2549
|
+
return;
|
|
2550
|
+
text = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
2508
2551
|
if (this.#fileBrowser !== null) {
|
|
2509
2552
|
this.#applyFileBrowserInput({ type: 'text', value: text });
|
|
2510
2553
|
return;
|
|
@@ -2532,42 +2575,50 @@ export class LocalTui {
|
|
|
2532
2575
|
}
|
|
2533
2576
|
const paths = imagePathCandidates(text);
|
|
2534
2577
|
if (paths.length > 0) {
|
|
2535
|
-
const images = await Promise.all(paths.map(path =>
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
for (const image of images) {
|
|
2539
|
-
if (await this.#images.admit(image)) {
|
|
2540
|
-
this.#images.insert(image);
|
|
2541
|
-
inserted = true;
|
|
2542
|
-
}
|
|
2578
|
+
const images = await Promise.all(paths.map(async (path) => {
|
|
2579
|
+
try {
|
|
2580
|
+
return await this.#readImagePath(path);
|
|
2543
2581
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
this.#render();
|
|
2582
|
+
catch {
|
|
2583
|
+
return null;
|
|
2547
2584
|
}
|
|
2585
|
+
}));
|
|
2586
|
+
if (signal.aborted)
|
|
2548
2587
|
return;
|
|
2588
|
+
let inserted = false;
|
|
2589
|
+
for (const [index, image] of images.entries()) {
|
|
2590
|
+
if (image === null) {
|
|
2591
|
+
this.notice(uiText(resolveUiLanguage(this.#uiLanguage), 'Could not read image {path}', { path: paths[index] }), { level: 'error' });
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2594
|
+
const admitted = await this.#images.admit(image, signal);
|
|
2595
|
+
if (signal.aborted)
|
|
2596
|
+
return;
|
|
2597
|
+
if (admitted) {
|
|
2598
|
+
this.#images.insert(image);
|
|
2599
|
+
inserted = true;
|
|
2600
|
+
}
|
|
2549
2601
|
}
|
|
2550
|
-
|
|
2551
|
-
// it before Node reads it. OMP falls back to the still-live clipboard
|
|
2552
|
-
// image instead of leaking that stale path into the prompt.
|
|
2553
|
-
const clipboardImage = await this.#readClipboardImage();
|
|
2554
|
-
if (clipboardImage !== null) {
|
|
2555
|
-
this.#images.insert(clipboardImage);
|
|
2602
|
+
if (inserted)
|
|
2556
2603
|
this.#refreshAutocomplete();
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
}
|
|
2604
|
+
this.#render();
|
|
2605
|
+
return;
|
|
2560
2606
|
}
|
|
2561
2607
|
this.#literalInput ||= this.#editor.cursor === 0 && text.startsWith('!');
|
|
2562
2608
|
this.#editor.paste(text);
|
|
2563
2609
|
this.#refreshAutocomplete();
|
|
2564
2610
|
this.#render();
|
|
2565
2611
|
}
|
|
2566
|
-
async #pasteClipboard() {
|
|
2567
|
-
if (this.#search === null && this.#settings === null) {
|
|
2612
|
+
async #pasteClipboard(signal) {
|
|
2613
|
+
if (this.#prompt === null && this.#search === null && this.#settings === null) {
|
|
2568
2614
|
const image = await this.#readClipboardImage();
|
|
2615
|
+
if (signal.aborted)
|
|
2616
|
+
return;
|
|
2569
2617
|
if (image !== null) {
|
|
2570
|
-
|
|
2618
|
+
const admitted = await this.#images.admit(image, signal);
|
|
2619
|
+
if (signal.aborted)
|
|
2620
|
+
return;
|
|
2621
|
+
if (admitted) {
|
|
2571
2622
|
this.#images.insert(image);
|
|
2572
2623
|
this.#refreshAutocomplete();
|
|
2573
2624
|
this.#render();
|
|
@@ -2575,29 +2626,17 @@ export class LocalTui {
|
|
|
2575
2626
|
return;
|
|
2576
2627
|
}
|
|
2577
2628
|
const files = await this.#readClipboardFiles();
|
|
2629
|
+
if (signal.aborted)
|
|
2630
|
+
return;
|
|
2578
2631
|
const imagePaths = files.filter(path => imagePathCandidates(path).length === 1);
|
|
2579
2632
|
if (imagePaths.length > 0) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
if (images.length > 0) {
|
|
2583
|
-
let inserted = false;
|
|
2584
|
-
for (const candidate of images) {
|
|
2585
|
-
if (await this.#images.admit(candidate)) {
|
|
2586
|
-
this.#images.insert(candidate);
|
|
2587
|
-
inserted = true;
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
if (inserted) {
|
|
2591
|
-
this.#refreshAutocomplete();
|
|
2592
|
-
this.#render();
|
|
2593
|
-
}
|
|
2594
|
-
return;
|
|
2595
|
-
}
|
|
2633
|
+
await this.#acceptPastedText(imagePaths.join('\n'), signal);
|
|
2634
|
+
return;
|
|
2596
2635
|
}
|
|
2597
2636
|
}
|
|
2598
2637
|
const text = await this.#readClipboard();
|
|
2599
|
-
if (text !== '')
|
|
2600
|
-
await this.#acceptPastedText(text);
|
|
2638
|
+
if (text !== '' && !signal.aborted)
|
|
2639
|
+
await this.#acceptPastedText(text, signal);
|
|
2601
2640
|
}
|
|
2602
2641
|
/** Resolve one bounded tmux probe without overwriting an explicit preference. */
|
|
2603
2642
|
setTmuxMouseEnabled(enabled) {
|
|
@@ -2689,6 +2728,18 @@ export class LocalTui {
|
|
|
2689
2728
|
return { row, column: tool.actionColumn, width: tool.key.startsWith('output:') ? 6 : 5, key: tool.key, action: 'tool', block: tool.block };
|
|
2690
2729
|
return undefined;
|
|
2691
2730
|
}
|
|
2731
|
+
#fileRegionAt(row) {
|
|
2732
|
+
const document = this.#renderer.documentRowAt(row);
|
|
2733
|
+
const frame = this.#fileActionFrame;
|
|
2734
|
+
const source = frame?.documentRows;
|
|
2735
|
+
const region = document === undefined ? undefined : frame?.transcript?.fileRegions?.find(region => document >= region.start && document < region.end);
|
|
2736
|
+
const expected = document === undefined || source === undefined || source === null ? undefined
|
|
2737
|
+
: frame?.lines[source.frameStart + document - source.documentStart];
|
|
2738
|
+
const actual = this.#renderer.viewport()[row];
|
|
2739
|
+
// Committed native history can differ from the live document after a reflow.
|
|
2740
|
+
return expected !== undefined && actual !== undefined
|
|
2741
|
+
&& stripAnsi(sanitizeDisplayLine(expected)).trimEnd() === stripAnsi(actual).trimEnd() ? region : undefined;
|
|
2742
|
+
}
|
|
2692
2743
|
#handleMouse(event) {
|
|
2693
2744
|
if (!this.#mouseTracking || this.#paste || this.#pasteInFlight > 0)
|
|
2694
2745
|
return;
|
|
@@ -2765,6 +2816,7 @@ export class LocalTui {
|
|
|
2765
2816
|
const target = this.#mouseActionAt(event.row, event.column);
|
|
2766
2817
|
if (event.action === 'release') {
|
|
2767
2818
|
this.#mouseActionPress = undefined;
|
|
2819
|
+
this.#workspacePress = undefined;
|
|
2768
2820
|
if (event.button === 'left' && !event.shift && target?.key === pressedAction.key && target.row === pressedAction.row && target.column === pressedAction.column) {
|
|
2769
2821
|
if (target.action === 'copy')
|
|
2770
2822
|
void this.#copySelection(true);
|
|
@@ -2795,21 +2847,14 @@ export class LocalTui {
|
|
|
2795
2847
|
}
|
|
2796
2848
|
const inside = (target) => event.row === target.row && event.column >= target.column && event.column < target.column + target.width;
|
|
2797
2849
|
if (event.button === 'right') {
|
|
2850
|
+
this.#workspacePress = undefined;
|
|
2798
2851
|
this.#disclosurePress = undefined;
|
|
2799
2852
|
if (event.action === 'press') {
|
|
2800
2853
|
if (this.#selection.active)
|
|
2801
2854
|
void this.#copySelection(true);
|
|
2802
2855
|
else {
|
|
2803
|
-
const
|
|
2804
|
-
|
|
2805
|
-
const source = frame?.documentRows;
|
|
2806
|
-
const region = document === undefined ? undefined : frame?.transcript?.fileRegions?.find(region => document >= region.start && document < region.end);
|
|
2807
|
-
const expected = document === undefined || source === undefined || source === null ? undefined
|
|
2808
|
-
: frame?.lines[source.frameStart + document - source.documentStart];
|
|
2809
|
-
const actual = this.#renderer.viewport()[event.row];
|
|
2810
|
-
// Native-history snapshots may differ from the current document. Do
|
|
2811
|
-
// not reinterpret their old text as another file after a reflow.
|
|
2812
|
-
if (region !== undefined && expected !== undefined && actual !== undefined && stripAnsi(sanitizeDisplayLine(expected)).trimEnd() === stripAnsi(actual).trimEnd()) {
|
|
2856
|
+
const region = this.#fileRegionAt(event.row);
|
|
2857
|
+
if (region !== undefined) {
|
|
2813
2858
|
const block = this.#state.blocks[region.block];
|
|
2814
2859
|
const action = block === undefined ? undefined : blockFileAction(block);
|
|
2815
2860
|
if (action !== undefined)
|
|
@@ -2820,6 +2865,10 @@ export class LocalTui {
|
|
|
2820
2865
|
return;
|
|
2821
2866
|
}
|
|
2822
2867
|
if (event.action === 'press') {
|
|
2868
|
+
const region = !event.shift && this.#workspaceReview !== undefined ? this.#fileRegionAt(event.row) : undefined;
|
|
2869
|
+
const source = region === undefined ? undefined : this.#state.blocks[region.block];
|
|
2870
|
+
this.#workspacePress = source?.kind === 'workspace' && region !== undefined
|
|
2871
|
+
? { row: event.row, column: event.column, block: region.block, source, ...(region.file === undefined ? {} : { file: region.file }) } : undefined;
|
|
2823
2872
|
const disclosure = event.shift ? undefined : this.#disclosureAt(event.row, event.column);
|
|
2824
2873
|
this.#disclosurePress = disclosure === undefined ? undefined : { target: disclosure, row: event.row, column: event.column };
|
|
2825
2874
|
const target = this.#jumpToLatest;
|
|
@@ -2841,6 +2890,9 @@ export class LocalTui {
|
|
|
2841
2890
|
}
|
|
2842
2891
|
}
|
|
2843
2892
|
else if (event.action === 'drag') {
|
|
2893
|
+
const pressed = this.#workspacePress;
|
|
2894
|
+
if (pressed !== undefined && (event.shift || event.row !== pressed.row || Math.abs(event.column - pressed.column) > 1))
|
|
2895
|
+
this.#workspacePress = undefined;
|
|
2844
2896
|
// Motion confined to the empty gutter is click jitter. Once it leaves,
|
|
2845
2897
|
// selection owns the gesture even if the pointer returns to the arrow.
|
|
2846
2898
|
if (event.shift || this.#pressedDisclosureAt(event.row, event.column) === undefined) {
|
|
@@ -2849,6 +2901,15 @@ export class LocalTui {
|
|
|
2849
2901
|
}
|
|
2850
2902
|
}
|
|
2851
2903
|
else {
|
|
2904
|
+
const pressed = this.#workspacePress;
|
|
2905
|
+
this.#workspacePress = undefined;
|
|
2906
|
+
const region = pressed === undefined ? undefined : this.#fileRegionAt(event.row);
|
|
2907
|
+
if (!event.shift && pressed !== undefined && event.row === pressed.row && Math.abs(event.column - pressed.column) <= 1
|
|
2908
|
+
&& region?.block === pressed.block && region.file === pressed.file && this.#state.blocks[pressed.block] === pressed.source) {
|
|
2909
|
+
this.#selection.clear();
|
|
2910
|
+
this.#openWorkspaceReview(pressed.source, pressed.file);
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2852
2913
|
const target = this.#pressedDisclosureAt(event.row, event.column);
|
|
2853
2914
|
this.#disclosurePress = undefined;
|
|
2854
2915
|
if (!event.shift && target !== undefined) {
|
|
@@ -3041,6 +3102,20 @@ export class LocalTui {
|
|
|
3041
3102
|
this.#fileReviewAbort = undefined;
|
|
3042
3103
|
}
|
|
3043
3104
|
}
|
|
3105
|
+
#openWorkspaceReview(block, file) {
|
|
3106
|
+
const handler = this.#workspaceReview;
|
|
3107
|
+
const sessionId = this.#inspected?.id ?? this.#sessionId;
|
|
3108
|
+
if (handler === undefined || sessionId === undefined || block.kind !== 'workspace' || !this.#tty || this.#disposed
|
|
3109
|
+
|| this.#prompt !== null || this.#fileReviewAbort !== undefined)
|
|
3110
|
+
return;
|
|
3111
|
+
const abort = new AbortController();
|
|
3112
|
+
this.#fileReviewAbort = abort;
|
|
3113
|
+
void handler({ sessionId, turn: block.turn, ...(file === undefined ? {} : { file }), signal: abort.signal })
|
|
3114
|
+
.catch(error => { if (!abort.signal.aborted && !this.#disposed)
|
|
3115
|
+
this.notice(error instanceof Error ? error.message : String(error), { level: 'error' }); })
|
|
3116
|
+
.finally(() => { if (this.#fileReviewAbort === abort)
|
|
3117
|
+
this.#fileReviewAbort = undefined; });
|
|
3118
|
+
}
|
|
3044
3119
|
#openFileAction(action, initial) {
|
|
3045
3120
|
if (!this.#tty || this.#disposed || this.#prompt !== null || this.#fileReviewAbort !== undefined)
|
|
3046
3121
|
return;
|
|
@@ -3092,6 +3167,7 @@ export class LocalTui {
|
|
|
3092
3167
|
this.#jumpPress = undefined;
|
|
3093
3168
|
this.#disclosurePress = undefined;
|
|
3094
3169
|
this.#mouseActionPress = undefined;
|
|
3170
|
+
this.#workspacePress = undefined;
|
|
3095
3171
|
if (this.#keyTest !== undefined && this.#prompt?.request.signal === this.#keyTest.signal
|
|
3096
3172
|
&& (event.type === 'text' || event.type === 'paste-start' || event.type === 'paste-end' || event.type === 'key' && !['escape', 'ctrl+c'].includes(event.id))) {
|
|
3097
3173
|
this.#keyTest.observe(event);
|
|
@@ -3148,6 +3224,8 @@ export class LocalTui {
|
|
|
3148
3224
|
// Clipboard image inspection is asynchronous. Preserve the exact key
|
|
3149
3225
|
// order so a fast Ctrl+V, Enter submits the finished image draft rather
|
|
3150
3226
|
// than an empty prompt.
|
|
3227
|
+
if (this.#pasteInFlight > 0 && event.type === 'key' && (event.id === 'ctrl+c' || event.id === 'escape'))
|
|
3228
|
+
this.#cancelPaste();
|
|
3151
3229
|
if (this.#pasteInFlight > 0) {
|
|
3152
3230
|
this.#deferredPasteEvents.push(event);
|
|
3153
3231
|
return;
|
|
@@ -3158,7 +3236,7 @@ export class LocalTui {
|
|
|
3158
3236
|
const text = this.#pasteBuf.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
3159
3237
|
this.#pasteBuf = '';
|
|
3160
3238
|
if (text !== '')
|
|
3161
|
-
this.#startAsyncPaste(this.#acceptPastedText(text));
|
|
3239
|
+
this.#startAsyncPaste(signal => this.#acceptPastedText(text, signal));
|
|
3162
3240
|
return;
|
|
3163
3241
|
}
|
|
3164
3242
|
if (event.type === 'text')
|
|
@@ -3922,6 +4000,7 @@ export class LocalTui {
|
|
|
3922
4000
|
const previous = options.persist ? this.prefs() : undefined;
|
|
3923
4001
|
const version = this.#prefsVersion;
|
|
3924
4002
|
const previousMotion = this.#motion;
|
|
4003
|
+
this.#workspacePress = undefined;
|
|
3925
4004
|
this.#uiLanguage = prefs.uiLanguage ?? 'auto';
|
|
3926
4005
|
if (this.#prompt !== null)
|
|
3927
4006
|
this.#prompt = { ...this.#prompt, request: localizePrompt(this.#prompt.sourceRequest, resolveUiLanguage(this.#uiLanguage)) };
|
|
@@ -3940,6 +4019,7 @@ export class LocalTui {
|
|
|
3940
4019
|
this.#mathMode = prefs.mathMode ?? 'auto';
|
|
3941
4020
|
this.#mermaidMode = prefs.mermaidMode ?? 'auto';
|
|
3942
4021
|
this.#hideThinking = prefs.hideThinking !== false;
|
|
4022
|
+
this.#showFileChanges = prefs.showFileChanges !== false;
|
|
3943
4023
|
this.#mouseInteraction = prefs.mouseInteraction ?? 'auto';
|
|
3944
4024
|
this.#checkUpdates = prefs.checkUpdates ?? true;
|
|
3945
4025
|
this.#startupChangelog = prefs.startupChangelog ?? 'summary';
|
|
@@ -4544,11 +4624,20 @@ export class LocalTui {
|
|
|
4544
4624
|
#buildSubmission(text) {
|
|
4545
4625
|
const images = this.#images.copies();
|
|
4546
4626
|
const files = this.#files.copies();
|
|
4547
|
-
const
|
|
4627
|
+
const markers = this.#files.snapshot(this.#editor.text).entries.map(item => item.marker);
|
|
4628
|
+
const ranges = this.#images.ranges();
|
|
4629
|
+
for (const [index, marker] of markers.entries()) {
|
|
4630
|
+
const start = text.indexOf(marker);
|
|
4631
|
+
if (start < 0)
|
|
4632
|
+
continue;
|
|
4633
|
+
const end = start + marker.length;
|
|
4634
|
+
ranges.push({ kind: 'file', index, start: start > 0 && text[start - 1] === ' ' ? start - 1 : start,
|
|
4635
|
+
end: text[end] === ' ' ? end + 1 : end });
|
|
4636
|
+
}
|
|
4548
4637
|
const segments = images.length === 0 && files.length === 0 ? []
|
|
4549
|
-
: splitComposerAttachmentMarkers(text
|
|
4550
|
-
|
|
4551
|
-
|
|
4638
|
+
: splitComposerAttachmentMarkers(text, images, markers, images.length > 0 && text === this.#editor.expandedText ? ranges : undefined);
|
|
4639
|
+
const submittedText = images.length === 0 ? this.#files.text(text)
|
|
4640
|
+
: segments.flatMap(segment => segment.kind === 'text' ? [segment.text] : []).join('');
|
|
4552
4641
|
const literal = text === this.#editor.expandedText && (this.#literalInput || this.#editor.startsWithPaste);
|
|
4553
4642
|
const historyText = submittedText.replace(/[ \t]{2,}/gu, ' ').trim();
|
|
4554
4643
|
return {
|
|
@@ -4767,6 +4856,7 @@ export class LocalTui {
|
|
|
4767
4856
|
const pending = this.#prompt;
|
|
4768
4857
|
if (pending === null)
|
|
4769
4858
|
return;
|
|
4859
|
+
this.#cancelPaste();
|
|
4770
4860
|
const draft = structuredPromptAnswer(pending, this.#editor.expandedText);
|
|
4771
4861
|
try {
|
|
4772
4862
|
pending.request.onAnswerDraft?.(draft);
|
|
@@ -4917,23 +5007,24 @@ export class LocalTui {
|
|
|
4917
5007
|
}
|
|
4918
5008
|
this.#search = null;
|
|
4919
5009
|
this.#ac = null;
|
|
5010
|
+
this.#cancelPaste();
|
|
4920
5011
|
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior, this.#features, this.#pluginSettings?.entries(), discoverEditors(this.#externalEditor), this.#settingsActions());
|
|
4921
5012
|
this.#refreshSettingsCatalog();
|
|
4922
5013
|
this.#render();
|
|
4923
5014
|
}
|
|
4924
5015
|
#settingsActions() {
|
|
4925
5016
|
const runtimeActions = [
|
|
4926
|
-
{ id: 'command:model', behavior: 'Session + Profile · Choose scope in Model', group: 'Models', label: 'Model and reasoning', description: '
|
|
4927
|
-
{ id: 'command:permission', behavior: 'Current session · Applies now', group: 'General', label: 'Current session access', description: 'Changes only this session. Default access controls future sessions.', value: this.#sessionControls?.permission ?? 'Choose', values: [], action: 'permission' },
|
|
4928
|
-
{ id: 'command:agent', behavior: 'Current session · Before the first prompt', group: 'Agent presets', label: 'Current session preset', description: 'Choose before the first prompt; does not change the new-session default.', value: this.#sessionControls?.agentPreset === undefined ? 'Choose' : formatAgentPreset(this.#sessionControls.agentPreset), values: [], action: 'agent' },
|
|
4929
|
-
{ id: 'command:login', behavior: '
|
|
4930
|
-
{ id: 'command:logout', behavior: '
|
|
4931
|
-
{ id: 'command:plugins', group: 'Plugins', label: 'Manage plugins', description: 'Inspect installed entries, enable bundles, discover plugins or start Creator.', value: 'Open', values: [], action: 'plugins' },
|
|
5017
|
+
{ id: 'command:model', behavior: 'Session + Profile · Choose scope in Model', group: 'Models', subgroup: 'Model selection', label: 'Model and reasoning', description: 'Choose the current model and reasoning effort. Save as the default or use for this session.', value: [this.#model, this.#reasoningEffort].filter(Boolean).join(' · '), values: [], action: 'model' },
|
|
5018
|
+
{ id: 'command:permission', behavior: 'Current session · Applies now', group: 'General', subgroup: 'Session controls', label: 'Current session access', description: 'Changes only this session. Default access controls future sessions.', value: this.#sessionControls?.permission ?? 'Choose', values: [], action: 'permission' },
|
|
5019
|
+
{ id: 'command:agent', behavior: 'Current session · Before the first prompt', group: 'Agent presets', subgroup: 'Current session', label: 'Current session preset', description: 'Choose before the first prompt; does not change the new-session default.', value: this.#sessionControls?.agentPreset === undefined ? 'Choose' : formatAgentPreset(this.#sessionControls.agentPreset), values: [], action: 'agent' },
|
|
5020
|
+
{ id: 'command:login', behavior: 'Accounts and API keys', group: 'Models', subgroup: 'Accounts and API keys', label: 'Sign in / add API key', description: 'Sign in to a provider account or save an API key.', keywords: ['Provider credentials', 'login', 'API key', 'OAuth'], value: 'Choose provider', values: [], action: 'login' },
|
|
5021
|
+
{ id: 'command:logout', behavior: 'Accounts and API keys', group: 'Models', subgroup: 'Accounts and API keys', label: 'Sign out / remove key', description: 'Choose a provider or account to sign out or remove locally saved credentials.', keywords: ['logout', 'credentials'], value: 'Choose provider', values: [], action: 'logout' },
|
|
5022
|
+
{ id: 'command:plugins', group: 'Plugins', subgroup: 'Plugin management', label: 'Manage plugins', description: 'Inspect installed entries, enable bundles, discover plugins or start Creator.', value: 'Open', values: [], action: 'plugins' },
|
|
4932
5023
|
].filter(item => this.#runtimeCommands.some(command => command.name === item.action));
|
|
4933
5024
|
const customBindings = Object.entries(this.#keybindings).filter(([key, action]) => DEFAULT_KEYBINDINGS[key] !== action).length;
|
|
4934
5025
|
return [
|
|
4935
5026
|
...runtimeActions,
|
|
4936
|
-
...(this.#goalHandler === undefined ? [] : [{ id: 'action:goal', behavior: 'Current session · Applies now', group: 'General', label: 'Current goal', description: 'Edit, pause, resume or clear this session’s goal.', value: 'Open', values: [], action: 'manage-goal' }]),
|
|
5027
|
+
...(this.#goalHandler === undefined ? [] : [{ id: 'action:goal', behavior: 'Current session · Applies now', group: 'General', subgroup: 'Session controls', label: 'Current goal', description: 'Edit, pause, resume or clear this session’s goal.', value: 'Open', values: [], action: 'manage-goal' }]),
|
|
4937
5028
|
...(this.#keybindingsPath === undefined ? [] : [{
|
|
4938
5029
|
id: 'action:keybindings',
|
|
4939
5030
|
label: 'Keybindings',
|
|
@@ -5329,7 +5420,7 @@ export class LocalTui {
|
|
|
5329
5420
|
return true;
|
|
5330
5421
|
}
|
|
5331
5422
|
if (action === 'paste-clipboard') {
|
|
5332
|
-
this.#startAsyncPaste(this.#pasteClipboard());
|
|
5423
|
+
this.#startAsyncPaste(signal => this.#pasteClipboard(signal));
|
|
5333
5424
|
return true;
|
|
5334
5425
|
}
|
|
5335
5426
|
// Explicit queue delivery is independent of the busy-Enter preference.
|
|
@@ -5345,7 +5436,8 @@ export class LocalTui {
|
|
|
5345
5436
|
this.#handTerminal(() => {
|
|
5346
5437
|
this.#literalInput ||= this.#editor.startsWithPaste;
|
|
5347
5438
|
const text = editExternally(this.#editor.expandedText, this.#externalEditor);
|
|
5348
|
-
this.#editor.
|
|
5439
|
+
while (this.#editor.expandPaste()) { /* Keep marker positions aligned with expanded source. */ }
|
|
5440
|
+
this.#editor.replaceText(text);
|
|
5349
5441
|
this.#images.reconcile();
|
|
5350
5442
|
});
|
|
5351
5443
|
}
|
|
@@ -5852,6 +5944,7 @@ export class LocalTui {
|
|
|
5852
5944
|
toolsExpanded: this.#toolsExpanded,
|
|
5853
5945
|
expandedOutputs: this.#expandedOutputs,
|
|
5854
5946
|
hideThinking: true,
|
|
5947
|
+
showFileChanges: this.#showFileChanges,
|
|
5855
5948
|
expandedTools: this.#detailedToolIds(),
|
|
5856
5949
|
expandedReasoning: this.#visibleReasoning(),
|
|
5857
5950
|
...(this.#openedGroups === undefined ? {} : { openedGroups: this.#openedGroups }),
|
|
@@ -5962,6 +6055,7 @@ export function apply(ctx, config) {
|
|
|
5962
6055
|
motion: config.motion.get(),
|
|
5963
6056
|
mathMode: config.mathMode.get(),
|
|
5964
6057
|
mermaidMode: config.mermaidMode.get(),
|
|
6058
|
+
showFileChanges: config.showFileChanges.get(),
|
|
5965
6059
|
editor: config.editor.get(),
|
|
5966
6060
|
terminalTitle: config.terminalTitle.get(),
|
|
5967
6061
|
terminalState: config.terminalState.get(),
|