@agi-fans/dsh-tui 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/lib/CHANGELOG.md +24 -2
  2. package/lib/chrome/box.d.ts +7 -0
  3. package/lib/chrome/box.js +1 -0
  4. package/lib/chrome/box.js.map +1 -1
  5. package/lib/chrome/main-screen-renderer.d.ts +2 -0
  6. package/lib/chrome/main-screen-renderer.js +6 -0
  7. package/lib/chrome/main-screen-renderer.js.map +1 -1
  8. package/lib/chrome/renderer.d.ts +12 -0
  9. package/lib/chrome/renderer.js.map +1 -1
  10. package/lib/chrome/status-line.js +3 -2
  11. package/lib/chrome/status-line.js.map +1 -1
  12. package/lib/chrome/width.d.ts +2 -0
  13. package/lib/chrome/width.js +10 -0
  14. package/lib/chrome/width.js.map +1 -1
  15. package/lib/commands/session.js +23 -12
  16. package/lib/commands/session.js.map +1 -1
  17. package/lib/definition.d.ts +11 -2
  18. package/lib/input/actions.d.ts +1 -1
  19. package/lib/input/actions.js +16 -5
  20. package/lib/input/actions.js.map +1 -1
  21. package/lib/input/editor.js +4 -0
  22. package/lib/input/editor.js.map +1 -1
  23. package/lib/input/key-platform.d.ts +2 -0
  24. package/lib/input/key-platform.js +8 -0
  25. package/lib/input/key-platform.js.map +1 -0
  26. package/lib/input/keybindings-config.d.ts +2 -1
  27. package/lib/input/keybindings-config.js +14 -9
  28. package/lib/input/keybindings-config.js.map +1 -1
  29. package/lib/input/keys.d.ts +2 -0
  30. package/lib/input/keys.js +10 -0
  31. package/lib/input/keys.js.map +1 -1
  32. package/lib/locale/zh.d.ts +3 -2
  33. package/lib/locale/zh.js +3 -2
  34. package/lib/locale/zh.js.map +1 -1
  35. package/lib/runtime/help-reader.d.ts +1 -1
  36. package/lib/runtime/help-reader.js +2 -2
  37. package/lib/runtime/help-reader.js.map +1 -1
  38. package/lib/runtime/provider-local.d.ts +4 -1
  39. package/lib/runtime/provider-local.js +209 -30
  40. package/lib/runtime/provider-local.js.map +1 -1
  41. package/lib/session/session-controller.js +40 -6
  42. package/lib/session/session-controller.js.map +1 -1
  43. package/lib/views/file-browser.d.ts +3 -0
  44. package/lib/views/file-browser.js +1 -0
  45. package/lib/views/file-browser.js.map +1 -1
  46. package/lib/views/hotkeys.d.ts +4 -4
  47. package/lib/views/hotkeys.js +24 -14
  48. package/lib/views/hotkeys.js.map +1 -1
  49. package/lib/views/settings-list.js +2 -2
  50. package/lib/views/settings-list.js.map +1 -1
  51. package/lib/views/transcript-render.d.ts +1 -0
  52. package/lib/views/transcript-render.js +25 -12
  53. package/lib/views/transcript-render.js.map +1 -1
  54. package/package.json +1 -1
@@ -64,11 +64,11 @@ import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, trajecto
64
64
  import { applyEvent, applyStreamChunk, renderedBlocks, formatAgentElapsed, rosterPreviewStart, initialTranscript, replayEvents, renderView, turnGroups, transcriptSeam, TRANSCRIPT_FAST_SCROLL, withWorkspaceSummary, } from "../views/event-views.js";
65
65
  import { applyTranscriptSearchEvent, createTranscriptSearch, searchMatches, } from "../views/transcript-search.js";
66
66
  import { nextRevealStep, revealStreamingAssistant, streamingAssistantKey, streamingAssistantUnits, } from "../views/streaming-reveal.js";
67
- import { flushPending, parseKeys } from "../input/keys.js";
67
+ import { flushPending, normalizeKeyId, parseKeys } from "../input/keys.js";
68
68
  import { sanitizeDisplayLine } from "../chrome/renderer.js";
69
69
  import { blockFileAction } from "../views/file-actions.js";
70
70
  import { fileDocument, reviewFiles } from "./file-review.js";
71
- import { stripAnsi } from "../chrome/width.js";
71
+ import { indexAtCell, stripAnsi } from "../chrome/width.js";
72
72
  import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
73
73
  import { supportsHyperlinks } from "./terminal-probe.js";
74
74
  import { colorDisabledByEnv, createTheme, detectColorDepth } from "../chrome/theme.js";
@@ -207,6 +207,8 @@ export class LocalTui {
207
207
  #keyTest;
208
208
  #mouseActionPress;
209
209
  #fileActionFrame;
210
+ #inputFrame;
211
+ #inputPress;
210
212
  #imageTemporary = new Set();
211
213
  #terminalProfile;
212
214
  #resizeDebounceMs;
@@ -227,7 +229,7 @@ export class LocalTui {
227
229
  #files = new ComposerFiles({ text: () => this.#editor.text,
228
230
  append: marker => { this.#editor.setCursor(this.#editor.text.length); this.#editor.handle({ type: 'text', value: (this.#editor.text === '' ? '' : ' ') + marker }); } });
229
231
  insertText(text) {
230
- if (text === '')
232
+ if (text === '' || this.#inspected?.writable === false)
231
233
  return;
232
234
  const before = this.#editor.text.slice(0, this.#editor.cursor);
233
235
  const after = this.#editor.text.slice(this.#editor.cursor);
@@ -289,6 +291,7 @@ export class LocalTui {
289
291
  #resumeHintRequested = false;
290
292
  #lastSigintTime = 0;
291
293
  #lastEscapeTime = 0;
294
+ #receivedModifiedEnter = new Set();
292
295
  #inspectEscapeFenceUntil = 0;
293
296
  #interrupts = new Set();
294
297
  #queueEdits = new Set();
@@ -436,6 +439,8 @@ export class LocalTui {
436
439
  #expandedReasoning = new Set();
437
440
  #expandedToolRows = new Set();
438
441
  #processSelection;
442
+ #promptAnchor;
443
+ #transcriptScopeChanged = false;
439
444
  #closedReasoning = new Set();
440
445
  /**
441
446
  * The last action's confirmation, drawn on the composer's bottom border.
@@ -505,6 +510,7 @@ export class LocalTui {
505
510
  #keybindingsPath;
506
511
  #keybindings;
507
512
  #cwd;
513
+ #rootDirectory;
508
514
  #projectRoot;
509
515
  #home;
510
516
  #listDir;
@@ -559,6 +565,7 @@ export class LocalTui {
559
565
  this.#cwd = paths.cwd ?? fallback.cwd;
560
566
  this.#readImagePath = paths.readImagePath ?? (path => readImageFile(path, this.#cwd));
561
567
  const project = resolveProjectContext(this.#cwd);
568
+ this.#rootDirectory = { cwd: this.#cwd, branch: project.gitLabel };
562
569
  this.#projectRoot = paths.projectRoot ?? project.root;
563
570
  this.#home = paths.home ?? fallback.home;
564
571
  this.#listDir = paths.listDir ?? fallback.listDir;
@@ -606,6 +613,7 @@ export class LocalTui {
606
613
  // live window so it stays anchored at the bottom.
607
614
  this.#selection.clear();
608
615
  this.#mouseActionPress = undefined;
616
+ this.#inputPress = undefined;
609
617
  this.#workspacePress = undefined;
610
618
  this.#jumpPress = undefined;
611
619
  this.#disclosurePress = undefined;
@@ -649,23 +657,38 @@ export class LocalTui {
649
657
  this.#syncWindowTitle();
650
658
  }
651
659
  /** Follow the viewed Session without replacing its transcript or native history. */
652
- setWorkingDirectory(cwd) {
660
+ setWorkingDirectory(cwd, options = {}) {
661
+ const root = options.root ?? this.#inspected === undefined;
662
+ const project = resolveProjectContext(cwd);
663
+ if (root) {
664
+ this.#rootDirectory = { cwd, branch: project.gitLabel };
665
+ this.#syncWindowTitle();
666
+ if (this.#inspected !== undefined)
667
+ return;
668
+ }
653
669
  if (cwd === this.#cwd && cwd === this.#projectRoot)
654
670
  return;
655
- this.#cwd = cwd;
671
+ this.#setViewedDirectory(cwd);
672
+ this.#syncWindowTitle();
673
+ if (this.#tty)
674
+ this.#render();
675
+ }
676
+ #setViewedDirectory(cwd) {
656
677
  const project = resolveProjectContext(cwd);
678
+ this.#cwd = cwd;
657
679
  this.#projectRoot = cwd;
658
680
  this.#pwd = shortenPath(cwd);
659
681
  this.#branch = project.gitLabel;
660
682
  this.#refreshAutocomplete();
661
- this.#syncWindowTitle();
662
- if (this.#tty)
663
- this.#render();
664
683
  }
665
684
  /** Refresh Git metadata without changing the Session's effective directory. */
666
685
  #refreshProjectContext() {
667
686
  const branch = refreshProjectContext(this.#cwd).gitLabel;
668
- if (branch === this.#branch)
687
+ const rootBranch = this.#rootDirectory.cwd === this.#cwd ? branch : refreshProjectContext(this.#rootDirectory.cwd).gitLabel;
688
+ const rootChanged = rootBranch !== this.#rootDirectory.branch;
689
+ if (rootChanged)
690
+ this.#rootDirectory = { ...this.#rootDirectory, branch: rootBranch };
691
+ if (branch === this.#branch && !rootChanged)
669
692
  return;
670
693
  this.#branch = branch;
671
694
  this.#syncWindowTitle();
@@ -842,6 +865,7 @@ export class LocalTui {
842
865
  const current = this.#subagents?.agents.find(agent => agent.id === inspected.id);
843
866
  if (current !== undefined) {
844
867
  this.#inspected = {
868
+ ...inspected,
845
869
  id: current.id,
846
870
  label: current.label,
847
871
  phase: current.phase,
@@ -860,15 +884,25 @@ export class LocalTui {
860
884
  setInspectedSubagent(inspected) {
861
885
  if (this.#inspected?.id === inspected?.id && this.#inspected?.label === inspected?.label
862
886
  && this.#inspected?.phase === inspected?.phase && this.#inspected?.needsApproval === inspected?.needsApproval && this.#inspected?.mode === inspected?.mode
863
- && this.#inspected?.writable === inspected?.writable)
887
+ && this.#inspected?.writable === inspected?.writable && this.#inspected?.model === inspected?.model
888
+ && this.#inspected?.reasoningEffort === inspected?.reasoningEffort && this.#inspected?.stats === inspected?.stats
889
+ && this.#inspected?.controls === inspected?.controls)
864
890
  return;
865
891
  if (this.#inspected?.id !== inspected?.id) {
892
+ this.#transcriptScopeChanged = true;
866
893
  this.#workspacePress = undefined;
867
894
  this.#fileReviewAbort?.abort();
868
895
  this.#closeFileBrowser();
896
+ this.#transcriptSearch = null;
897
+ this.#followTail();
869
898
  }
870
- this.#switchDraft(inspected?.id ?? this.#sessionId);
899
+ const leaving = this.#inspected !== undefined && inspected === undefined;
871
900
  this.#inspected = inspected === undefined ? undefined : { ...inspected };
901
+ if (leaving) {
902
+ this.#setViewedDirectory(this.#rootDirectory.cwd);
903
+ this.#syncWindowTitle();
904
+ }
905
+ this.#switchDraft(inspected?.id ?? this.#sessionId);
872
906
  this.#drainPendingInput();
873
907
  if (inspected !== undefined) {
874
908
  this.#inspectEscapeFenceUntil = 0;
@@ -902,7 +936,7 @@ export class LocalTui {
902
936
  this.#settings = null;
903
937
  this.#search = null;
904
938
  this.#ac = null;
905
- const browser = new FileBrowser(request, () => { if (!this.#disposed)
939
+ const browser = new FileBrowser({ ...request, canQuote: request.canQuote && (this.#inspected === undefined || this.#inspected.writable) }, () => { if (!this.#disposed)
906
940
  this.#render(); }, () => {
907
941
  if (this.#fileBrowser === browser)
908
942
  this.#fileBrowser = null;
@@ -1370,10 +1404,23 @@ export class LocalTui {
1370
1404
  // A notice does not turn a blank session into a document that /new needs
1371
1405
  // to separate. Refreshes are the same document.
1372
1406
  const boundary = reason === 'open' || (reason === 'new' && this.#state.blocks.some(block => block.kind !== 'notice'));
1373
- const blocks = boundary ? [transcriptSeam('session-opened'), ...state.blocks] : state.blocks;
1407
+ const refreshing = reason === 'refresh' && !this.#transcriptScopeChanged;
1408
+ this.#transcriptScopeChanged = false;
1409
+ const seam = refreshing && this.#state.blocks[0]?.kind === 'notice' && this.#state.blocks[0].boundary === true
1410
+ ? this.#state.blocks[0] : undefined;
1411
+ const blocks = boundary ? [transcriptSeam('session-opened'), ...state.blocks] : seam === undefined ? state.blocks : [seam, ...state.blocks];
1374
1412
  this.#state = { ...state, status, compactCommandId: undefined, blocks };
1375
1413
  this.#plain.resetPrinted();
1376
- this.#followTail();
1414
+ this.#promptAnchor = undefined;
1415
+ if (!refreshing) {
1416
+ this.#transcriptSearch = null;
1417
+ this.#followTail();
1418
+ }
1419
+ else if (this.#transcriptSearch !== null) {
1420
+ const match = this.#searchOccurrences()[this.#transcriptSearch.focus];
1421
+ if (match !== undefined)
1422
+ this.#focusSearchMatch(match);
1423
+ }
1377
1424
  this.#deferInitialRender = false;
1378
1425
  this.#renderer.startEpoch({ replay: reason === 'refresh' ? 'viewport' : status === 'idle' ? 'full' : 'pinned' });
1379
1426
  if (this.#tty)
@@ -1383,6 +1430,7 @@ export class LocalTui {
1383
1430
  }
1384
1431
  setSession(info) {
1385
1432
  if (info.id !== this.#sessionId) {
1433
+ this.#transcriptScopeChanged = true;
1386
1434
  this.#workspacePress = undefined;
1387
1435
  this.#fileReviewAbort?.abort();
1388
1436
  this.#closeFileBrowser();
@@ -1420,7 +1468,7 @@ export class LocalTui {
1420
1468
  const status = this.#herdr.programStatus;
1421
1469
  this.#writeWorkingDirectory(this.#cwd);
1422
1470
  this.#syncProgress(status.state);
1423
- const title = terminalTitle(this.#terminalTitle, status, this.#sessionTitle, this.#cwd, this.#branch);
1471
+ const title = terminalTitle(this.#terminalTitle, status, this.#sessionTitle, this.#rootDirectory.cwd, this.#rootDirectory.branch);
1424
1472
  if (title !== this.#writtenWindowTitle && !(this.#terminalTitle === 'off' && this.#writtenWindowTitle === undefined)) {
1425
1473
  this.#writtenWindowTitle = title;
1426
1474
  this.#term.output.write(`\x1b]2;${title}\x07`);
@@ -2005,6 +2053,19 @@ export class LocalTui {
2005
2053
  refresh() {
2006
2054
  this.#render();
2007
2055
  }
2056
+ searchTranscript(query) {
2057
+ if (!this.#tty || this.#disposed || query.trim() === '')
2058
+ return;
2059
+ // Commit the resumed document before moving its live viewport into history.
2060
+ this.#render();
2061
+ this.#transcriptSearch = { query, editing: false, focus: 0 };
2062
+ this.#promptAnchor = undefined;
2063
+ this.#ac = null;
2064
+ const match = this.#searchOccurrences()[0];
2065
+ if (match !== undefined)
2066
+ this.#focusSearchMatch(match);
2067
+ this.#render();
2068
+ }
2008
2069
  #busy() {
2009
2070
  if (this.#state.status !== 'idle')
2010
2071
  return true;
@@ -2230,6 +2291,13 @@ export class LocalTui {
2230
2291
  if (this.#probeGate)
2231
2292
  return;
2232
2293
  const width = this.#term.width();
2294
+ const alternateSendKey = keysForAction(Object.fromEntries(Object.entries(this.#keybindings).filter(([key]) => {
2295
+ if (key === 'super+enter')
2296
+ return this.#receivedModifiedEnter.has(key);
2297
+ if (key === 'ctrl+enter')
2298
+ return this.#session.keyboardProtocol === 'kitty' || this.#receivedModifiedEnter.has(key);
2299
+ return true;
2300
+ })), 'alternate-send');
2233
2301
  const renderState = this.#reveal === undefined
2234
2302
  ? this.#state
2235
2303
  : revealStreamingAssistant(this.#state, this.#reveal.revealed);
@@ -2288,6 +2356,7 @@ export class LocalTui {
2288
2356
  toolDetailsKey: keysForAction(this.#keybindings, 'toggle-tool-details'),
2289
2357
  turnDetailsKey: keysForAction(this.#keybindings, 'toggle-tools'),
2290
2358
  busyEnter: this.#queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#busyEnter,
2359
+ ...(this.#queueEditNewer !== null || this.#queueEditPending || alternateSendKey === 'Disabled' ? {} : { alternateSendKey }),
2291
2360
  steerKey: keysForAction(this.#keybindings, 'steer-turn'),
2292
2361
  queueKey: keysForAction(this.#keybindings, 'follow-up'),
2293
2362
  openedGroups: this.#openedGroups,
@@ -2323,7 +2392,8 @@ export class LocalTui {
2323
2392
  this.#syncScroll(frame.transcript);
2324
2393
  this.#jumpToLatest = frame.jumpToLatest;
2325
2394
  this.#fileActionFrame = frame.transcript === undefined || frame.transientSurface === 'overlay' ? undefined : frame;
2326
- this.#setMouseTracking(this.#fileBrowser !== null || this.#trajectory !== null || this.#documentMouseEnabled() || this.#promptChoices.length > 0 || this.#promptActions.length > 0 || (frame.transcript !== undefined && frame.transientSurface !== 'overlay'
2395
+ this.#inputFrame = frame.inputRows === undefined ? undefined : frame;
2396
+ this.#setMouseTracking(this.#inputFrame !== undefined || this.#fileBrowser !== null || this.#trajectory !== null || this.#documentMouseEnabled() || this.#promptChoices.length > 0 || this.#promptActions.length > 0 || (frame.transcript !== undefined && frame.transientSurface !== 'overlay'
2327
2397
  && (this.#inspecting || this.#maxStart > 0 || (frame.transcript.fileRegions?.length ?? 0) > 0 || (frame.transcript.disclosures?.length ?? 0) > 0)));
2328
2398
  // A run opening or folding reshapes rows above the screen in place; the
2329
2399
  // renderer's frozen boundary is a row index and has to be told, or a fold
@@ -2369,6 +2439,7 @@ export class LocalTui {
2369
2439
  }
2370
2440
  #scrollBy(delta) {
2371
2441
  this.#workspacePress = undefined;
2442
+ this.#promptAnchor = undefined;
2372
2443
  if (this.#follow && (delta > 0 || this.#maxStart === 0))
2373
2444
  return;
2374
2445
  if (delta === 0 && this.#maxStart === 0)
@@ -2392,6 +2463,7 @@ export class LocalTui {
2392
2463
  this.#render();
2393
2464
  }
2394
2465
  #followTail() {
2466
+ this.#promptAnchor = undefined;
2395
2467
  this.#follow = true;
2396
2468
  this.#scrollStart = this.#maxStart;
2397
2469
  this.#endInspection();
@@ -2479,6 +2551,7 @@ export class LocalTui {
2479
2551
  * or go back to the live tail once nothing is open any more.
2480
2552
  */
2481
2553
  #inspect(target) {
2554
+ this.#promptAnchor = undefined;
2482
2555
  const open = this.#toolsExpanded || this.#openedGroups.size > 0 || this.#expandedOutputs.size > 0;
2483
2556
  if (!open) {
2484
2557
  this.#inspecting = false;
@@ -2814,6 +2887,40 @@ export class LocalTui {
2814
2887
  this.#handleTrajectoryMouse(event);
2815
2888
  return;
2816
2889
  }
2890
+ const frameRow = this.#renderer.frameRowAt(event.row);
2891
+ const inputRow = this.#inputFrame?.inputRows?.find(row => row.row === frameRow);
2892
+ const completionRow = this.#inputFrame?.autocompleteRows?.find(row => row.row === frameRow);
2893
+ if (event.button === 'left') {
2894
+ const pressed = this.#inputPress;
2895
+ if (event.action === 'press' && !event.shift && (inputRow !== undefined || completionRow !== undefined)) {
2896
+ const completion = completionRow === undefined ? undefined : this.#ac?.items[completionRow.index];
2897
+ this.#inputPress = { row: event.row, column: event.column, text: this.#editor.text,
2898
+ ...(inputRow === undefined ? {} : { cursor: inputRow.start + indexAtCell(inputRow.text, Math.max(0, event.column - inputRow.column)) }),
2899
+ ...(completion === undefined ? {} : { completion }) };
2900
+ }
2901
+ else if (event.action === 'drag')
2902
+ this.#inputPress = undefined;
2903
+ else if (event.action === 'release') {
2904
+ this.#inputPress = undefined;
2905
+ if (pressed !== undefined && !event.shift && pressed.row === event.row && pressed.column === event.column && pressed.text === this.#editor.text) {
2906
+ this.#selection.clear();
2907
+ this.#processSelection = undefined;
2908
+ this.#subagentSelection = undefined;
2909
+ this.#transcriptSearch = null;
2910
+ if (pressed.completion !== undefined && this.#ac !== null && completionRow !== undefined && this.#ac.items[completionRow.index] === pressed.completion) {
2911
+ this.#ac.selected = completionRow.index;
2912
+ this.#applySelectedCompletion();
2913
+ }
2914
+ else if (pressed.cursor !== undefined && inputRow !== undefined
2915
+ && pressed.cursor === inputRow.start + indexAtCell(inputRow.text, Math.max(0, event.column - inputRow.column))) {
2916
+ this.#editor.setCursor(pressed.cursor);
2917
+ this.#refreshAutocomplete();
2918
+ }
2919
+ this.#render();
2920
+ return;
2921
+ }
2922
+ }
2923
+ }
2817
2924
  const promptAction = this.#promptActions.find(action => event.row === action.row && event.column >= action.column && event.column < action.column + action.width);
2818
2925
  if (this.#promptActionPress !== undefined) {
2819
2926
  const pressed = this.#promptActionPress;
@@ -3085,15 +3192,20 @@ export class LocalTui {
3085
3192
  }
3086
3193
  }
3087
3194
  #canQuoteSelection() {
3088
- return this.#prompt === null && this.#settings === null && this.#trajectory === null && this.#fileBrowser === null && this.#search === null
3195
+ return this.#prompt === null && this.#settings === null && this.#trajectory === null && (this.#fileBrowser === null || this.#fileBrowser.state.canQuote && !this.#fileBrowserOwnsInput()) && this.#search === null
3089
3196
  && this.#transcriptSearch === null && (this.#inspected === undefined || this.#inspected.writable === true);
3090
3197
  }
3198
+ #fileBrowserOwnsInput() {
3199
+ const state = this.#fileBrowser?.state;
3200
+ return state !== undefined && (state.editing !== undefined || state.help || state.menu !== undefined);
3201
+ }
3091
3202
  #quoteSelection() {
3092
3203
  if (!this.#canQuoteSelection())
3093
3204
  return;
3094
3205
  const text = this.#selection.valid(this.#renderer.viewport()) ? this.#selection.text() : undefined;
3095
3206
  this.#selection.clear();
3096
3207
  if (text !== undefined) {
3208
+ this.#closeFileBrowser();
3097
3209
  this.#appendQuote(text.split('\n').map(line => '> ' + line).join('\n'));
3098
3210
  }
3099
3211
  this.#render();
@@ -3216,6 +3328,9 @@ export class LocalTui {
3216
3328
  }
3217
3329
  if (this.#session.handle(event))
3218
3330
  return;
3331
+ if (!this.#paste && event.type === 'key' && (event.id === 'ctrl+enter' || event.id === 'super+enter')) {
3332
+ this.#receivedModifiedEnter.add(event.id);
3333
+ }
3219
3334
  if (event.type === 'focus') {
3220
3335
  this.#terminalFocused = event.focused;
3221
3336
  if (event.focused && this.#notificationPolicy === 'unfocused')
@@ -3232,17 +3347,22 @@ export class LocalTui {
3232
3347
  this.#disclosurePress = undefined;
3233
3348
  this.#mouseActionPress = undefined;
3234
3349
  this.#workspacePress = undefined;
3350
+ this.#inputPress = undefined;
3235
3351
  if (this.#keyTest !== undefined && this.#prompt?.request.signal === this.#keyTest.signal
3236
3352
  && (event.type === 'text' || event.type === 'paste-start' || event.type === 'paste-end' || event.type === 'key' && !['escape', 'ctrl+c'].includes(event.id))) {
3237
3353
  this.#keyTest.observe(event);
3238
3354
  return;
3239
3355
  }
3240
3356
  if (this.#selection.active) {
3241
- if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection') {
3357
+ if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection' && this.#canQuoteSelection()) {
3358
+ this.#quoteSelection();
3359
+ return;
3360
+ }
3361
+ if (this.#fileBrowser !== null && event.type === 'text' && event.value.toLowerCase() === 'q' && this.#canQuoteSelection()) {
3242
3362
  this.#quoteSelection();
3243
3363
  return;
3244
3364
  }
3245
- if (event.type === 'key' && (['super+c', 'enter'].includes(event.id) || this.#boundAction(event.id) === 'copy-prompt')) {
3365
+ if (event.type === 'key' && (event.id === 'super+c' || event.id === 'enter' && !this.#fileBrowserOwnsInput() || this.#boundAction(event.id) === 'copy-prompt')) {
3246
3366
  void this.#copySelection();
3247
3367
  return;
3248
3368
  }
@@ -3262,6 +3382,11 @@ export class LocalTui {
3262
3382
  this.#applyTrajectoryInput(event);
3263
3383
  return;
3264
3384
  }
3385
+ if (this.#ac !== null && this.#inputFrame?.autocompleteRows?.some(row => row.row === this.#renderer.frameRowAt(event.row ?? -1))) {
3386
+ this.#ac.selected = nextSelectableAutocompleteIndex(this.#ac.items, this.#ac.selected + (event.direction === 'up' ? -1 : 1), event.direction === 'up' ? -1 : 1);
3387
+ this.#render();
3388
+ return;
3389
+ }
3265
3390
  this.#jumpPress = undefined;
3266
3391
  if (this.#mouseTracking && !this.#paste && this.#pasteInFlight === 0) {
3267
3392
  const step = wheelStep(this.#wheelGesture, Date.now(), event.direction === 'up' ? -1 : 1, !this.#wheelAccelerates);
@@ -3408,7 +3533,7 @@ export class LocalTui {
3408
3533
  this.#render();
3409
3534
  return;
3410
3535
  }
3411
- if (event.type === 'key' && (event.id === 'ctrl+-' || event.id === 'ctrl+_')
3536
+ if (event.type === 'key' && (event.id === 'ctrl+-' || event.id === 'ctrl+_' || event.id === 'super+z')
3412
3537
  && this.#clearedDraft !== undefined && this.#clearedDraft.revision === this.#editor.revision
3413
3538
  && !this.#hasComposerContent()) {
3414
3539
  const cleared = this.#clearedDraft.draft;
@@ -4737,6 +4862,11 @@ export class LocalTui {
4737
4862
  this.#queuedSubmissions.push(submission);
4738
4863
  this.#pendingInputs.flush(this.#pendingScope());
4739
4864
  }
4865
+ #resolveDelivery(delivery) {
4866
+ if (delivery === 'alternate')
4867
+ return this.#busyEnter === 'queue' ? 'steer' : 'queue';
4868
+ return delivery ?? this.#busyEnter;
4869
+ }
4740
4870
  #submit(text, delivery) {
4741
4871
  if (this.#prompt !== null) {
4742
4872
  if (this.#prompt.request.presentation === 'progress')
@@ -4758,7 +4888,7 @@ export class LocalTui {
4758
4888
  submission.literal = true;
4759
4889
  const command = !submission.literal && (submittedText.trimStart().startsWith('/') || (images.length === 0 && files.length === 0 && submittedText.startsWith('!')));
4760
4890
  if (!command && this.#state.status === 'running') {
4761
- submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : delivery ?? this.#busyEnter;
4891
+ submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#resolveDelivery(delivery);
4762
4892
  }
4763
4893
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
4764
4894
  this.#history.push(historyText);
@@ -4806,7 +4936,7 @@ export class LocalTui {
4806
4936
  const newer = this.#queueEditNewer;
4807
4937
  if (delivery === 'steer')
4808
4938
  submission.literal = true;
4809
- submission.delivery = newer !== null || this.#queueEditPending ? 'queue' : this.#state.status === 'running' ? delivery ?? this.#busyEnter : 'queue';
4939
+ submission.delivery = newer !== null || this.#queueEditPending || this.#state.status !== 'running' ? 'queue' : this.#resolveDelivery(delivery);
4810
4940
  const { images, files = [], literal = false, text: submittedText } = submission;
4811
4941
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
4812
4942
  this.#history.push(historyText);
@@ -5179,10 +5309,11 @@ export class LocalTui {
5179
5309
  }, 25);
5180
5310
  }
5181
5311
  #runHelp(full) {
5312
+ const enhancedKeys = this.#session.keyboardProtocol === 'kitty';
5182
5313
  if (!this.#tty) {
5183
- this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings),
5314
+ this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings, enhancedKeys),
5184
5315
  '', formatHelpText(this.#commands(), resolveUiLanguage(this.#uiLanguage)),
5185
- ...(full ? ['', formatHotkeysText(this.#keybindings)] : ['', 'Use `/help full` to view every keyboard shortcut.'])].join('\n'));
5316
+ ...(full ? ['', formatHotkeysText(this.#keybindings, enhancedKeys)] : ['', 'Use `/help full` to view every keyboard shortcut.'])].join('\n'));
5186
5317
  return;
5187
5318
  }
5188
5319
  if (this.#fileReviewAbort !== undefined || this.#prompt !== null)
@@ -5194,7 +5325,7 @@ export class LocalTui {
5194
5325
  copyText: (text, label) => this.copyText(text, label),
5195
5326
  insertText: (text) => this.insertText(text),
5196
5327
  };
5197
- void browseHelp(surface, this.#commands(), this.#keybindings, resolveUiLanguage(this.#uiLanguage), full, abort.signal)
5328
+ void browseHelp(surface, this.#commands(), this.#keybindings, resolveUiLanguage(this.#uiLanguage), full, abort.signal, enhancedKeys)
5198
5329
  .catch(error => { if (!abort.signal.aborted && !this.#disposed)
5199
5330
  this.notice(String(error), { level: 'error' }); })
5200
5331
  .finally(() => { if (this.#fileReviewAbort === abort)
@@ -5250,6 +5381,19 @@ export class LocalTui {
5250
5381
  // closes. Claiming it here would skip that layering.
5251
5382
  if (action === 'interrupt')
5252
5383
  return false;
5384
+ if (action === 'alternate-send') {
5385
+ if (this.#settings !== null || this.#search !== null)
5386
+ return false;
5387
+ if (this.#inspected !== undefined && !this.#inspected.writable)
5388
+ return true;
5389
+ if (this.#handleAutocomplete({ type: 'key', id: 'enter' }))
5390
+ return true;
5391
+ if (this.#hasComposerContent())
5392
+ this.#submit(this.#editor.expandedText, 'alternate');
5393
+ else
5394
+ this.#render();
5395
+ return true;
5396
+ }
5253
5397
  if (action === 'clear-composer')
5254
5398
  return this.#runClearComposer();
5255
5399
  if (action === 'quit') {
@@ -5416,6 +5560,33 @@ export class LocalTui {
5416
5560
  this.#scrollBy(-this.#pageSize());
5417
5561
  return true;
5418
5562
  }
5563
+ if (action === 'previous-turn' || action === 'next-turn') {
5564
+ if (this.#search !== null)
5565
+ return false;
5566
+ const prompts = this.#state.blocks.flatMap((block, index) => block.kind === 'user' ? [index] : []);
5567
+ const anchor = this.#promptAnchor === undefined ? -1 : this.#state.blocks.indexOf(this.#promptAnchor);
5568
+ const top = this.#follow
5569
+ ? this.#renderer.viewport().map((_, row) => this.#renderer.documentRowAt(row)).find(row => row !== undefined) ?? this.#scrollStart
5570
+ : this.#scrollStart;
5571
+ const target = action === 'previous-turn'
5572
+ ? prompts.findLast(index => anchor >= 0 ? index < anchor : (this.#blockStarts[index] ?? 0) < top)
5573
+ : prompts.find(index => anchor >= 0 ? index > anchor : (this.#blockStarts[index] ?? 0) > top);
5574
+ this.#processSelection = undefined;
5575
+ this.#ac = null;
5576
+ if (target === undefined) {
5577
+ if (action === 'next-turn')
5578
+ this.#followTail();
5579
+ }
5580
+ else {
5581
+ this.#promptAnchor = this.#state.blocks[target];
5582
+ this.#inspecting = true;
5583
+ this.#follow = false;
5584
+ this.#focusBlock = target;
5585
+ this.#focusBlockEdge = 'start';
5586
+ }
5587
+ this.#render();
5588
+ return true;
5589
+ }
5419
5590
  if (action === 'scroll-page-down') {
5420
5591
  if (this.#search !== null)
5421
5592
  return false;
@@ -5711,7 +5882,7 @@ export class LocalTui {
5711
5882
  });
5712
5883
  if (entered === null || this.#disposed)
5713
5884
  continue;
5714
- const normalized = entered.trim().toLowerCase();
5885
+ const normalized = normalizeKeyId(entered);
5715
5886
  if (normalized === '')
5716
5887
  continue;
5717
5888
  const next = { ...this.#keybindings };
@@ -5846,6 +6017,7 @@ export class LocalTui {
5846
6017
  return true;
5847
6018
  }
5848
6019
  if (event.id === 'home') {
6020
+ this.#promptAnchor = undefined;
5849
6021
  const current = this.#fileActionFrame?.transcript?.disclosures?.findLast(target => target.detailEnd !== undefined && this.#scrollStart > target.row && this.#scrollStart < target.detailEnd);
5850
6022
  this.#processSelection = current?.block ?? selected;
5851
6023
  this.#focusBlock = this.#processSelection;
@@ -5854,6 +6026,7 @@ export class LocalTui {
5854
6026
  return true;
5855
6027
  }
5856
6028
  if (event.id === 'up' || event.id === 'down') {
6029
+ this.#promptAnchor = undefined;
5857
6030
  const rows = this.#processRows();
5858
6031
  const at = Math.max(0, rows.indexOf(selected));
5859
6032
  const next = rows[Math.max(0, Math.min(rows.length - 1, at + (event.id === 'up' ? -1 : 1)))];
@@ -6040,12 +6213,18 @@ export class LocalTui {
6040
6213
  return;
6041
6214
  }
6042
6215
  this.#transcriptSearch = command.state;
6043
- if (command.kind === 'focus') {
6044
- this.#focusBlock = command.match.block;
6045
- this.#focusBlockEdge = 'start';
6046
- }
6216
+ if (command.kind === 'focus')
6217
+ this.#focusSearchMatch(command.match);
6047
6218
  this.#render();
6048
6219
  }
6220
+ #focusSearchMatch(match) {
6221
+ this.#promptAnchor = undefined;
6222
+ this.#processSelection = undefined;
6223
+ this.#inspecting = true;
6224
+ this.#follow = false;
6225
+ this.#focusBlock = match.block;
6226
+ this.#focusBlockEdge = 'start';
6227
+ }
6049
6228
  }
6050
6229
  /**
6051
6230
  * Mount the local terminal provider as the tui service.