@agi-fans/dsh-tui 0.25.0 → 0.26.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.
Files changed (62) hide show
  1. package/lib/CHANGELOG.md +31 -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 +16 -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/clipboard.js +2 -1
  22. package/lib/input/clipboard.js.map +1 -1
  23. package/lib/input/editor.js +4 -0
  24. package/lib/input/editor.js.map +1 -1
  25. package/lib/input/key-platform.d.ts +2 -0
  26. package/lib/input/key-platform.js +8 -0
  27. package/lib/input/key-platform.js.map +1 -0
  28. package/lib/input/keybindings-config.d.ts +2 -1
  29. package/lib/input/keybindings-config.js +14 -9
  30. package/lib/input/keybindings-config.js.map +1 -1
  31. package/lib/input/keys.d.ts +2 -0
  32. package/lib/input/keys.js +10 -0
  33. package/lib/input/keys.js.map +1 -1
  34. package/lib/locale/zh.d.ts +3 -2
  35. package/lib/locale/zh.js +3 -2
  36. package/lib/locale/zh.js.map +1 -1
  37. package/lib/runtime/help-reader.d.ts +1 -1
  38. package/lib/runtime/help-reader.js +2 -2
  39. package/lib/runtime/help-reader.js.map +1 -1
  40. package/lib/runtime/herdr-agent.d.ts +2 -0
  41. package/lib/runtime/herdr-agent.js +7 -1
  42. package/lib/runtime/herdr-agent.js.map +1 -1
  43. package/lib/runtime/provider-local.d.ts +9 -1
  44. package/lib/runtime/provider-local.js +247 -51
  45. package/lib/runtime/provider-local.js.map +1 -1
  46. package/lib/runtime/terminal-state.d.ts +4 -1
  47. package/lib/runtime/terminal-state.js +28 -7
  48. package/lib/runtime/terminal-state.js.map +1 -1
  49. package/lib/session/session-controller.js +56 -6
  50. package/lib/session/session-controller.js.map +1 -1
  51. package/lib/views/file-browser.d.ts +3 -0
  52. package/lib/views/file-browser.js +1 -0
  53. package/lib/views/file-browser.js.map +1 -1
  54. package/lib/views/hotkeys.d.ts +4 -4
  55. package/lib/views/hotkeys.js +24 -14
  56. package/lib/views/hotkeys.js.map +1 -1
  57. package/lib/views/settings-list.js +2 -2
  58. package/lib/views/settings-list.js.map +1 -1
  59. package/lib/views/transcript-render.d.ts +1 -0
  60. package/lib/views/transcript-render.js +25 -12
  61. package/lib/views/transcript-render.js.map +1 -1
  62. 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();
@@ -712,9 +735,19 @@ export class LocalTui {
712
735
  this.#compactionStartedAt = Date.now();
713
736
  else if (!compacting)
714
737
  this.#compactionStartedAt = undefined;
715
- if (event.type === 'turn/end' && this.#inspected === undefined) {
716
- this.#herdr.settled(event.data.reason.kind);
717
- this.#syncWindowTitle();
738
+ if (this.#inspected === undefined) {
739
+ if (event.type === 'compaction/start') {
740
+ this.#herdr.compactionStart(event.data.compactionId);
741
+ this.#syncWindowTitle();
742
+ }
743
+ else if (event.type === 'compaction/end') {
744
+ this.#herdr.compactionEnd(event.data.compactionId, event.data.error);
745
+ this.#syncWindowTitle();
746
+ }
747
+ else if (event.type === 'turn/end') {
748
+ this.#herdr.settled(event.data.reason.kind);
749
+ this.#syncWindowTitle();
750
+ }
718
751
  }
719
752
  this.#emitNotification(this.#notifications.event({
720
753
  type: event.type,
@@ -777,6 +810,12 @@ export class LocalTui {
777
810
  // Parent lifecycle remains authoritative while the child owns the visible view.
778
811
  if (options.root === true || this.#inspected === undefined) {
779
812
  this.#herdr.setRunning(status === 'running');
813
+ if (options.compaction?.phase === 'start') {
814
+ this.#herdr.compactionStart(options.compaction.id);
815
+ }
816
+ else if (options.compaction?.phase === 'end') {
817
+ this.#herdr.compactionEnd(options.compaction.id, options.compaction.error);
818
+ }
780
819
  if (options.outcome !== undefined)
781
820
  this.#herdr.settled(options.outcome);
782
821
  this.#syncWindowTitle();
@@ -842,6 +881,7 @@ export class LocalTui {
842
881
  const current = this.#subagents?.agents.find(agent => agent.id === inspected.id);
843
882
  if (current !== undefined) {
844
883
  this.#inspected = {
884
+ ...inspected,
845
885
  id: current.id,
846
886
  label: current.label,
847
887
  phase: current.phase,
@@ -860,15 +900,25 @@ export class LocalTui {
860
900
  setInspectedSubagent(inspected) {
861
901
  if (this.#inspected?.id === inspected?.id && this.#inspected?.label === inspected?.label
862
902
  && this.#inspected?.phase === inspected?.phase && this.#inspected?.needsApproval === inspected?.needsApproval && this.#inspected?.mode === inspected?.mode
863
- && this.#inspected?.writable === inspected?.writable)
903
+ && this.#inspected?.writable === inspected?.writable && this.#inspected?.model === inspected?.model
904
+ && this.#inspected?.reasoningEffort === inspected?.reasoningEffort && this.#inspected?.stats === inspected?.stats
905
+ && this.#inspected?.controls === inspected?.controls)
864
906
  return;
865
907
  if (this.#inspected?.id !== inspected?.id) {
908
+ this.#transcriptScopeChanged = true;
866
909
  this.#workspacePress = undefined;
867
910
  this.#fileReviewAbort?.abort();
868
911
  this.#closeFileBrowser();
912
+ this.#transcriptSearch = null;
913
+ this.#followTail();
869
914
  }
870
- this.#switchDraft(inspected?.id ?? this.#sessionId);
915
+ const leaving = this.#inspected !== undefined && inspected === undefined;
871
916
  this.#inspected = inspected === undefined ? undefined : { ...inspected };
917
+ if (leaving) {
918
+ this.#setViewedDirectory(this.#rootDirectory.cwd);
919
+ this.#syncWindowTitle();
920
+ }
921
+ this.#switchDraft(inspected?.id ?? this.#sessionId);
872
922
  this.#drainPendingInput();
873
923
  if (inspected !== undefined) {
874
924
  this.#inspectEscapeFenceUntil = 0;
@@ -902,7 +952,7 @@ export class LocalTui {
902
952
  this.#settings = null;
903
953
  this.#search = null;
904
954
  this.#ac = null;
905
- const browser = new FileBrowser(request, () => { if (!this.#disposed)
955
+ const browser = new FileBrowser({ ...request, canQuote: request.canQuote && (this.#inspected === undefined || this.#inspected.writable) }, () => { if (!this.#disposed)
906
956
  this.#render(); }, () => {
907
957
  if (this.#fileBrowser === browser)
908
958
  this.#fileBrowser = null;
@@ -1249,9 +1299,9 @@ export class LocalTui {
1249
1299
  this.#flushPasteBurst(true);
1250
1300
  this.#pasteBurst.clear();
1251
1301
  if (request.notify !== false) {
1252
- this.#emitNotification(this.#notifications.humanPrompt());
1253
1302
  this.#herdr.prompt(request.title, request.attentionKind);
1254
1303
  this.#syncWindowTitle();
1304
+ this.#emitNotification(this.#notifications.humanPrompt());
1255
1305
  }
1256
1306
  if (this.#tty && request.documentImage !== undefined && this.#imageProtocol !== undefined)
1257
1307
  this.#term.output.write('\x1b[16t');
@@ -1370,10 +1420,23 @@ export class LocalTui {
1370
1420
  // A notice does not turn a blank session into a document that /new needs
1371
1421
  // to separate. Refreshes are the same document.
1372
1422
  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;
1423
+ const refreshing = reason === 'refresh' && !this.#transcriptScopeChanged;
1424
+ this.#transcriptScopeChanged = false;
1425
+ const seam = refreshing && this.#state.blocks[0]?.kind === 'notice' && this.#state.blocks[0].boundary === true
1426
+ ? this.#state.blocks[0] : undefined;
1427
+ const blocks = boundary ? [transcriptSeam('session-opened'), ...state.blocks] : seam === undefined ? state.blocks : [seam, ...state.blocks];
1374
1428
  this.#state = { ...state, status, compactCommandId: undefined, blocks };
1375
1429
  this.#plain.resetPrinted();
1376
- this.#followTail();
1430
+ this.#promptAnchor = undefined;
1431
+ if (!refreshing) {
1432
+ this.#transcriptSearch = null;
1433
+ this.#followTail();
1434
+ }
1435
+ else if (this.#transcriptSearch !== null) {
1436
+ const match = this.#searchOccurrences()[this.#transcriptSearch.focus];
1437
+ if (match !== undefined)
1438
+ this.#focusSearchMatch(match);
1439
+ }
1377
1440
  this.#deferInitialRender = false;
1378
1441
  this.#renderer.startEpoch({ replay: reason === 'refresh' ? 'viewport' : status === 'idle' ? 'full' : 'pinned' });
1379
1442
  if (this.#tty)
@@ -1383,6 +1446,7 @@ export class LocalTui {
1383
1446
  }
1384
1447
  setSession(info) {
1385
1448
  if (info.id !== this.#sessionId) {
1449
+ this.#transcriptScopeChanged = true;
1386
1450
  this.#workspacePress = undefined;
1387
1451
  this.#fileReviewAbort?.abort();
1388
1452
  this.#closeFileBrowser();
@@ -1414,14 +1478,15 @@ export class LocalTui {
1414
1478
  this.#render();
1415
1479
  }
1416
1480
  /** Only the foreground terminal owner emits title and program-state sequences. */
1417
- #syncWindowTitle(notify = false) {
1481
+ #syncWindowTitle() {
1418
1482
  if (!this.#tty || this.#disposed || this.#session.handedOff)
1419
1483
  return;
1420
1484
  const status = this.#herdr.programStatus;
1421
1485
  this.#writeWorkingDirectory(this.#cwd);
1422
1486
  this.#syncProgress(status.state);
1423
- const title = terminalTitle(this.#terminalTitle, status, this.#sessionTitle, this.#cwd, this.#branch);
1424
- if (title !== this.#writtenWindowTitle && !(this.#terminalTitle === 'off' && this.#writtenWindowTitle === undefined)) {
1487
+ const nativeStatus = this.#reportsProgramState;
1488
+ const title = terminalTitle(this.#terminalTitle, status, this.#sessionTitle, this.#rootDirectory.cwd, this.#rootDirectory.branch, nativeStatus);
1489
+ if (title !== this.#writtenWindowTitle && !(title === '' && this.#writtenWindowTitle === undefined)) {
1425
1490
  this.#writtenWindowTitle = title;
1426
1491
  this.#term.output.write(`\x1b]2;${title}\x07`);
1427
1492
  }
@@ -1429,7 +1494,7 @@ export class LocalTui {
1429
1494
  this.#programQuerySent = true;
1430
1495
  this.#term.output.write(PROGRAM_STATUS_QUERY);
1431
1496
  }
1432
- if (!this.#reportsProgramState) {
1497
+ if (!nativeStatus) {
1433
1498
  if (this.#writtenProgramState !== undefined)
1434
1499
  this.#term.output.write(programStatusSequence('clear'));
1435
1500
  this.#writtenProgramState = undefined;
@@ -1438,17 +1503,8 @@ export class LocalTui {
1438
1503
  // Only the session name is exported as metadata; prompt bodies and errors stay in the TUI.
1439
1504
  const sequence = programStatusSequence({ state: status.state,
1440
1505
  ...(status.kind === undefined ? {} : { kind: status.kind }),
1441
- ...(this.#sessionTitle === undefined ? {} : { message: this.#sessionTitle }),
1506
+ ...(this.#sessionTitle === undefined ? {} : { title: this.#sessionTitle }),
1442
1507
  });
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
- }
1452
1508
  if (sequence === this.#writtenProgramState)
1453
1509
  return;
1454
1510
  this.#writtenProgramState = sequence;
@@ -2005,6 +2061,19 @@ export class LocalTui {
2005
2061
  refresh() {
2006
2062
  this.#render();
2007
2063
  }
2064
+ searchTranscript(query) {
2065
+ if (!this.#tty || this.#disposed || query.trim() === '')
2066
+ return;
2067
+ // Commit the resumed document before moving its live viewport into history.
2068
+ this.#render();
2069
+ this.#transcriptSearch = { query, editing: false, focus: 0 };
2070
+ this.#promptAnchor = undefined;
2071
+ this.#ac = null;
2072
+ const match = this.#searchOccurrences()[0];
2073
+ if (match !== undefined)
2074
+ this.#focusSearchMatch(match);
2075
+ this.#render();
2076
+ }
2008
2077
  #busy() {
2009
2078
  if (this.#state.status !== 'idle')
2010
2079
  return true;
@@ -2230,6 +2299,13 @@ export class LocalTui {
2230
2299
  if (this.#probeGate)
2231
2300
  return;
2232
2301
  const width = this.#term.width();
2302
+ const alternateSendKey = keysForAction(Object.fromEntries(Object.entries(this.#keybindings).filter(([key]) => {
2303
+ if (key === 'super+enter')
2304
+ return this.#receivedModifiedEnter.has(key);
2305
+ if (key === 'ctrl+enter')
2306
+ return this.#session.keyboardProtocol === 'kitty' || this.#receivedModifiedEnter.has(key);
2307
+ return true;
2308
+ })), 'alternate-send');
2233
2309
  const renderState = this.#reveal === undefined
2234
2310
  ? this.#state
2235
2311
  : revealStreamingAssistant(this.#state, this.#reveal.revealed);
@@ -2288,6 +2364,7 @@ export class LocalTui {
2288
2364
  toolDetailsKey: keysForAction(this.#keybindings, 'toggle-tool-details'),
2289
2365
  turnDetailsKey: keysForAction(this.#keybindings, 'toggle-tools'),
2290
2366
  busyEnter: this.#queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#busyEnter,
2367
+ ...(this.#queueEditNewer !== null || this.#queueEditPending || alternateSendKey === 'Disabled' ? {} : { alternateSendKey }),
2291
2368
  steerKey: keysForAction(this.#keybindings, 'steer-turn'),
2292
2369
  queueKey: keysForAction(this.#keybindings, 'follow-up'),
2293
2370
  openedGroups: this.#openedGroups,
@@ -2323,7 +2400,8 @@ export class LocalTui {
2323
2400
  this.#syncScroll(frame.transcript);
2324
2401
  this.#jumpToLatest = frame.jumpToLatest;
2325
2402
  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'
2403
+ this.#inputFrame = frame.inputRows === undefined ? undefined : frame;
2404
+ 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
2405
  && (this.#inspecting || this.#maxStart > 0 || (frame.transcript.fileRegions?.length ?? 0) > 0 || (frame.transcript.disclosures?.length ?? 0) > 0)));
2328
2406
  // A run opening or folding reshapes rows above the screen in place; the
2329
2407
  // renderer's frozen boundary is a row index and has to be told, or a fold
@@ -2369,6 +2447,7 @@ export class LocalTui {
2369
2447
  }
2370
2448
  #scrollBy(delta) {
2371
2449
  this.#workspacePress = undefined;
2450
+ this.#promptAnchor = undefined;
2372
2451
  if (this.#follow && (delta > 0 || this.#maxStart === 0))
2373
2452
  return;
2374
2453
  if (delta === 0 && this.#maxStart === 0)
@@ -2392,6 +2471,7 @@ export class LocalTui {
2392
2471
  this.#render();
2393
2472
  }
2394
2473
  #followTail() {
2474
+ this.#promptAnchor = undefined;
2395
2475
  this.#follow = true;
2396
2476
  this.#scrollStart = this.#maxStart;
2397
2477
  this.#endInspection();
@@ -2479,6 +2559,7 @@ export class LocalTui {
2479
2559
  * or go back to the live tail once nothing is open any more.
2480
2560
  */
2481
2561
  #inspect(target) {
2562
+ this.#promptAnchor = undefined;
2482
2563
  const open = this.#toolsExpanded || this.#openedGroups.size > 0 || this.#expandedOutputs.size > 0;
2483
2564
  if (!open) {
2484
2565
  this.#inspecting = false;
@@ -2814,6 +2895,40 @@ export class LocalTui {
2814
2895
  this.#handleTrajectoryMouse(event);
2815
2896
  return;
2816
2897
  }
2898
+ const frameRow = this.#renderer.frameRowAt(event.row);
2899
+ const inputRow = this.#inputFrame?.inputRows?.find(row => row.row === frameRow);
2900
+ const completionRow = this.#inputFrame?.autocompleteRows?.find(row => row.row === frameRow);
2901
+ if (event.button === 'left') {
2902
+ const pressed = this.#inputPress;
2903
+ if (event.action === 'press' && !event.shift && (inputRow !== undefined || completionRow !== undefined)) {
2904
+ const completion = completionRow === undefined ? undefined : this.#ac?.items[completionRow.index];
2905
+ this.#inputPress = { row: event.row, column: event.column, text: this.#editor.text,
2906
+ ...(inputRow === undefined ? {} : { cursor: inputRow.start + indexAtCell(inputRow.text, Math.max(0, event.column - inputRow.column)) }),
2907
+ ...(completion === undefined ? {} : { completion }) };
2908
+ }
2909
+ else if (event.action === 'drag')
2910
+ this.#inputPress = undefined;
2911
+ else if (event.action === 'release') {
2912
+ this.#inputPress = undefined;
2913
+ if (pressed !== undefined && !event.shift && pressed.row === event.row && pressed.column === event.column && pressed.text === this.#editor.text) {
2914
+ this.#selection.clear();
2915
+ this.#processSelection = undefined;
2916
+ this.#subagentSelection = undefined;
2917
+ this.#transcriptSearch = null;
2918
+ if (pressed.completion !== undefined && this.#ac !== null && completionRow !== undefined && this.#ac.items[completionRow.index] === pressed.completion) {
2919
+ this.#ac.selected = completionRow.index;
2920
+ this.#applySelectedCompletion();
2921
+ }
2922
+ else if (pressed.cursor !== undefined && inputRow !== undefined
2923
+ && pressed.cursor === inputRow.start + indexAtCell(inputRow.text, Math.max(0, event.column - inputRow.column))) {
2924
+ this.#editor.setCursor(pressed.cursor);
2925
+ this.#refreshAutocomplete();
2926
+ }
2927
+ this.#render();
2928
+ return;
2929
+ }
2930
+ }
2931
+ }
2817
2932
  const promptAction = this.#promptActions.find(action => event.row === action.row && event.column >= action.column && event.column < action.column + action.width);
2818
2933
  if (this.#promptActionPress !== undefined) {
2819
2934
  const pressed = this.#promptActionPress;
@@ -3085,15 +3200,20 @@ export class LocalTui {
3085
3200
  }
3086
3201
  }
3087
3202
  #canQuoteSelection() {
3088
- return this.#prompt === null && this.#settings === null && this.#trajectory === null && this.#fileBrowser === null && this.#search === null
3203
+ return this.#prompt === null && this.#settings === null && this.#trajectory === null && (this.#fileBrowser === null || this.#fileBrowser.state.canQuote && !this.#fileBrowserOwnsInput()) && this.#search === null
3089
3204
  && this.#transcriptSearch === null && (this.#inspected === undefined || this.#inspected.writable === true);
3090
3205
  }
3206
+ #fileBrowserOwnsInput() {
3207
+ const state = this.#fileBrowser?.state;
3208
+ return state !== undefined && (state.editing !== undefined || state.help || state.menu !== undefined);
3209
+ }
3091
3210
  #quoteSelection() {
3092
3211
  if (!this.#canQuoteSelection())
3093
3212
  return;
3094
3213
  const text = this.#selection.valid(this.#renderer.viewport()) ? this.#selection.text() : undefined;
3095
3214
  this.#selection.clear();
3096
3215
  if (text !== undefined) {
3216
+ this.#closeFileBrowser();
3097
3217
  this.#appendQuote(text.split('\n').map(line => '> ' + line).join('\n'));
3098
3218
  }
3099
3219
  this.#render();
@@ -3216,6 +3336,9 @@ export class LocalTui {
3216
3336
  }
3217
3337
  if (this.#session.handle(event))
3218
3338
  return;
3339
+ if (!this.#paste && event.type === 'key' && (event.id === 'ctrl+enter' || event.id === 'super+enter')) {
3340
+ this.#receivedModifiedEnter.add(event.id);
3341
+ }
3219
3342
  if (event.type === 'focus') {
3220
3343
  this.#terminalFocused = event.focused;
3221
3344
  if (event.focused && this.#notificationPolicy === 'unfocused')
@@ -3232,17 +3355,22 @@ export class LocalTui {
3232
3355
  this.#disclosurePress = undefined;
3233
3356
  this.#mouseActionPress = undefined;
3234
3357
  this.#workspacePress = undefined;
3358
+ this.#inputPress = undefined;
3235
3359
  if (this.#keyTest !== undefined && this.#prompt?.request.signal === this.#keyTest.signal
3236
3360
  && (event.type === 'text' || event.type === 'paste-start' || event.type === 'paste-end' || event.type === 'key' && !['escape', 'ctrl+c'].includes(event.id))) {
3237
3361
  this.#keyTest.observe(event);
3238
3362
  return;
3239
3363
  }
3240
3364
  if (this.#selection.active) {
3241
- if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection') {
3365
+ if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection' && this.#canQuoteSelection()) {
3366
+ this.#quoteSelection();
3367
+ return;
3368
+ }
3369
+ if (this.#fileBrowser !== null && event.type === 'text' && event.value.toLowerCase() === 'q' && this.#canQuoteSelection()) {
3242
3370
  this.#quoteSelection();
3243
3371
  return;
3244
3372
  }
3245
- if (event.type === 'key' && (['super+c', 'enter'].includes(event.id) || this.#boundAction(event.id) === 'copy-prompt')) {
3373
+ if (event.type === 'key' && (event.id === 'super+c' || event.id === 'enter' && !this.#fileBrowserOwnsInput() || this.#boundAction(event.id) === 'copy-prompt')) {
3246
3374
  void this.#copySelection();
3247
3375
  return;
3248
3376
  }
@@ -3262,6 +3390,11 @@ export class LocalTui {
3262
3390
  this.#applyTrajectoryInput(event);
3263
3391
  return;
3264
3392
  }
3393
+ if (this.#ac !== null && this.#inputFrame?.autocompleteRows?.some(row => row.row === this.#renderer.frameRowAt(event.row ?? -1))) {
3394
+ this.#ac.selected = nextSelectableAutocompleteIndex(this.#ac.items, this.#ac.selected + (event.direction === 'up' ? -1 : 1), event.direction === 'up' ? -1 : 1);
3395
+ this.#render();
3396
+ return;
3397
+ }
3265
3398
  this.#jumpPress = undefined;
3266
3399
  if (this.#mouseTracking && !this.#paste && this.#pasteInFlight === 0) {
3267
3400
  const step = wheelStep(this.#wheelGesture, Date.now(), event.direction === 'up' ? -1 : 1, !this.#wheelAccelerates);
@@ -3408,7 +3541,7 @@ export class LocalTui {
3408
3541
  this.#render();
3409
3542
  return;
3410
3543
  }
3411
- if (event.type === 'key' && (event.id === 'ctrl+-' || event.id === 'ctrl+_')
3544
+ if (event.type === 'key' && (event.id === 'ctrl+-' || event.id === 'ctrl+_' || event.id === 'super+z')
3412
3545
  && this.#clearedDraft !== undefined && this.#clearedDraft.revision === this.#editor.revision
3413
3546
  && !this.#hasComposerContent()) {
3414
3547
  const cleared = this.#clearedDraft.draft;
@@ -4737,6 +4870,11 @@ export class LocalTui {
4737
4870
  this.#queuedSubmissions.push(submission);
4738
4871
  this.#pendingInputs.flush(this.#pendingScope());
4739
4872
  }
4873
+ #resolveDelivery(delivery) {
4874
+ if (delivery === 'alternate')
4875
+ return this.#busyEnter === 'queue' ? 'steer' : 'queue';
4876
+ return delivery ?? this.#busyEnter;
4877
+ }
4740
4878
  #submit(text, delivery) {
4741
4879
  if (this.#prompt !== null) {
4742
4880
  if (this.#prompt.request.presentation === 'progress')
@@ -4758,7 +4896,7 @@ export class LocalTui {
4758
4896
  submission.literal = true;
4759
4897
  const command = !submission.literal && (submittedText.trimStart().startsWith('/') || (images.length === 0 && files.length === 0 && submittedText.startsWith('!')));
4760
4898
  if (!command && this.#state.status === 'running') {
4761
- submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : delivery ?? this.#busyEnter;
4899
+ submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#resolveDelivery(delivery);
4762
4900
  }
4763
4901
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
4764
4902
  this.#history.push(historyText);
@@ -4806,7 +4944,7 @@ export class LocalTui {
4806
4944
  const newer = this.#queueEditNewer;
4807
4945
  if (delivery === 'steer')
4808
4946
  submission.literal = true;
4809
- submission.delivery = newer !== null || this.#queueEditPending ? 'queue' : this.#state.status === 'running' ? delivery ?? this.#busyEnter : 'queue';
4947
+ submission.delivery = newer !== null || this.#queueEditPending || this.#state.status !== 'running' ? 'queue' : this.#resolveDelivery(delivery);
4810
4948
  const { images, files = [], literal = false, text: submittedText } = submission;
4811
4949
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
4812
4950
  this.#history.push(historyText);
@@ -5151,6 +5289,8 @@ export class LocalTui {
5151
5289
  #emitNotification(notification) {
5152
5290
  if (notification === undefined || !this.#tty || this.#disposed)
5153
5291
  return;
5292
+ if (this.#reportsProgramState && notification.programState !== undefined && programStatusNeedsAttention(notification.programState))
5293
+ return;
5154
5294
  if (this.#notificationPolicy === 'unfocused' && this.#terminalFocused === true)
5155
5295
  return;
5156
5296
  this.#notificationQueue.offer(notification);
@@ -5169,20 +5309,19 @@ export class LocalTui {
5169
5309
  return;
5170
5310
  if (pending.programState !== undefined && pending.programState !== this.#herdr.programStatus.state)
5171
5311
  return;
5172
- if (this.#reportsProgramState && programStatusNeedsAttention(this.#herdr.programStatus.state)) {
5173
- this.#syncWindowTitle(true);
5312
+ if (this.#reportsProgramState && pending.programState !== undefined && programStatusNeedsAttention(pending.programState))
5174
5313
  return;
5175
- }
5176
5314
  const sequence = terminalNotificationSequence(pending);
5177
5315
  if (sequence !== '')
5178
5316
  this.#term.output.write(sequence);
5179
5317
  }, 25);
5180
5318
  }
5181
5319
  #runHelp(full) {
5320
+ const enhancedKeys = this.#session.keyboardProtocol === 'kitty';
5182
5321
  if (!this.#tty) {
5183
- this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings),
5322
+ this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings, enhancedKeys),
5184
5323
  '', formatHelpText(this.#commands(), resolveUiLanguage(this.#uiLanguage)),
5185
- ...(full ? ['', formatHotkeysText(this.#keybindings)] : ['', 'Use `/help full` to view every keyboard shortcut.'])].join('\n'));
5324
+ ...(full ? ['', formatHotkeysText(this.#keybindings, enhancedKeys)] : ['', 'Use `/help full` to view every keyboard shortcut.'])].join('\n'));
5186
5325
  return;
5187
5326
  }
5188
5327
  if (this.#fileReviewAbort !== undefined || this.#prompt !== null)
@@ -5194,7 +5333,7 @@ export class LocalTui {
5194
5333
  copyText: (text, label) => this.copyText(text, label),
5195
5334
  insertText: (text) => this.insertText(text),
5196
5335
  };
5197
- void browseHelp(surface, this.#commands(), this.#keybindings, resolveUiLanguage(this.#uiLanguage), full, abort.signal)
5336
+ void browseHelp(surface, this.#commands(), this.#keybindings, resolveUiLanguage(this.#uiLanguage), full, abort.signal, enhancedKeys)
5198
5337
  .catch(error => { if (!abort.signal.aborted && !this.#disposed)
5199
5338
  this.notice(String(error), { level: 'error' }); })
5200
5339
  .finally(() => { if (this.#fileReviewAbort === abort)
@@ -5250,6 +5389,19 @@ export class LocalTui {
5250
5389
  // closes. Claiming it here would skip that layering.
5251
5390
  if (action === 'interrupt')
5252
5391
  return false;
5392
+ if (action === 'alternate-send') {
5393
+ if (this.#settings !== null || this.#search !== null)
5394
+ return false;
5395
+ if (this.#inspected !== undefined && !this.#inspected.writable)
5396
+ return true;
5397
+ if (this.#handleAutocomplete({ type: 'key', id: 'enter' }))
5398
+ return true;
5399
+ if (this.#hasComposerContent())
5400
+ this.#submit(this.#editor.expandedText, 'alternate');
5401
+ else
5402
+ this.#render();
5403
+ return true;
5404
+ }
5253
5405
  if (action === 'clear-composer')
5254
5406
  return this.#runClearComposer();
5255
5407
  if (action === 'quit') {
@@ -5416,6 +5568,33 @@ export class LocalTui {
5416
5568
  this.#scrollBy(-this.#pageSize());
5417
5569
  return true;
5418
5570
  }
5571
+ if (action === 'previous-turn' || action === 'next-turn') {
5572
+ if (this.#search !== null)
5573
+ return false;
5574
+ const prompts = this.#state.blocks.flatMap((block, index) => block.kind === 'user' ? [index] : []);
5575
+ const anchor = this.#promptAnchor === undefined ? -1 : this.#state.blocks.indexOf(this.#promptAnchor);
5576
+ const top = this.#follow
5577
+ ? this.#renderer.viewport().map((_, row) => this.#renderer.documentRowAt(row)).find(row => row !== undefined) ?? this.#scrollStart
5578
+ : this.#scrollStart;
5579
+ const target = action === 'previous-turn'
5580
+ ? prompts.findLast(index => anchor >= 0 ? index < anchor : (this.#blockStarts[index] ?? 0) < top)
5581
+ : prompts.find(index => anchor >= 0 ? index > anchor : (this.#blockStarts[index] ?? 0) > top);
5582
+ this.#processSelection = undefined;
5583
+ this.#ac = null;
5584
+ if (target === undefined) {
5585
+ if (action === 'next-turn')
5586
+ this.#followTail();
5587
+ }
5588
+ else {
5589
+ this.#promptAnchor = this.#state.blocks[target];
5590
+ this.#inspecting = true;
5591
+ this.#follow = false;
5592
+ this.#focusBlock = target;
5593
+ this.#focusBlockEdge = 'start';
5594
+ }
5595
+ this.#render();
5596
+ return true;
5597
+ }
5419
5598
  if (action === 'scroll-page-down') {
5420
5599
  if (this.#search !== null)
5421
5600
  return false;
@@ -5711,7 +5890,7 @@ export class LocalTui {
5711
5890
  });
5712
5891
  if (entered === null || this.#disposed)
5713
5892
  continue;
5714
- const normalized = entered.trim().toLowerCase();
5893
+ const normalized = normalizeKeyId(entered);
5715
5894
  if (normalized === '')
5716
5895
  continue;
5717
5896
  const next = { ...this.#keybindings };
@@ -5846,6 +6025,7 @@ export class LocalTui {
5846
6025
  return true;
5847
6026
  }
5848
6027
  if (event.id === 'home') {
6028
+ this.#promptAnchor = undefined;
5849
6029
  const current = this.#fileActionFrame?.transcript?.disclosures?.findLast(target => target.detailEnd !== undefined && this.#scrollStart > target.row && this.#scrollStart < target.detailEnd);
5850
6030
  this.#processSelection = current?.block ?? selected;
5851
6031
  this.#focusBlock = this.#processSelection;
@@ -5854,6 +6034,7 @@ export class LocalTui {
5854
6034
  return true;
5855
6035
  }
5856
6036
  if (event.id === 'up' || event.id === 'down') {
6037
+ this.#promptAnchor = undefined;
5857
6038
  const rows = this.#processRows();
5858
6039
  const at = Math.max(0, rows.indexOf(selected));
5859
6040
  const next = rows[Math.max(0, Math.min(rows.length - 1, at + (event.id === 'up' ? -1 : 1)))];
@@ -6040,12 +6221,18 @@ export class LocalTui {
6040
6221
  return;
6041
6222
  }
6042
6223
  this.#transcriptSearch = command.state;
6043
- if (command.kind === 'focus') {
6044
- this.#focusBlock = command.match.block;
6045
- this.#focusBlockEdge = 'start';
6046
- }
6224
+ if (command.kind === 'focus')
6225
+ this.#focusSearchMatch(command.match);
6047
6226
  this.#render();
6048
6227
  }
6228
+ #focusSearchMatch(match) {
6229
+ this.#promptAnchor = undefined;
6230
+ this.#processSelection = undefined;
6231
+ this.#inspecting = true;
6232
+ this.#follow = false;
6233
+ this.#focusBlock = match.block;
6234
+ this.#focusBlockEdge = 'start';
6235
+ }
6049
6236
  }
6050
6237
  /**
6051
6238
  * Mount the local terminal provider as the tui service.
@@ -6084,7 +6271,16 @@ export function apply(ctx, config) {
6084
6271
  },
6085
6272
  terminalProfile,
6086
6273
  alternateScreenOverlays: terminalProfile === 'direct',
6087
- herdrReporter: new HerdrAgentReporter({ interactive: term.input.isTTY === true && term.output.isTTY === true }),
6274
+ herdrReporter: (() => {
6275
+ const interactive = term.input.isTTY === true && term.output.isTTY === true;
6276
+ const reporter = new HerdrAgentReporter({ interactive });
6277
+ if (interactive && process.env.OMDSH === undefined) {
6278
+ process.env.OMDSH = '1';
6279
+ ctx.effect(() => () => { if (process.env.OMDSH === '1')
6280
+ delete process.env.OMDSH; });
6281
+ }
6282
+ return reporter;
6283
+ })(),
6088
6284
  historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
6089
6285
  draftsPath: join(config.dshHome ?? dshHome, 'omdsh', 'drafts'),
6090
6286
  keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),