@agi-fans/dsh-tui 0.24.1 → 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 (88) hide show
  1. package/lib/CHANGELOG.md +42 -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 +4 -2
  6. package/lib/chrome/main-screen-renderer.js +11 -5
  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 +1 -1
  10. package/lib/chrome/renderer.js.map +1 -1
  11. package/lib/chrome/status-line.js +3 -2
  12. package/lib/chrome/status-line.js.map +1 -1
  13. package/lib/chrome/width.d.ts +2 -0
  14. package/lib/chrome/width.js +10 -0
  15. package/lib/chrome/width.js.map +1 -1
  16. package/lib/commands/session.js +23 -12
  17. package/lib/commands/session.js.map +1 -1
  18. package/lib/definition.d.ts +13 -2
  19. package/lib/input/actions.d.ts +1 -1
  20. package/lib/input/actions.js +16 -5
  21. package/lib/input/actions.js.map +1 -1
  22. package/lib/input/clipboard.d.ts +1 -1
  23. package/lib/input/clipboard.js +10 -4
  24. package/lib/input/clipboard.js.map +1 -1
  25. package/lib/input/editor.js +4 -0
  26. package/lib/input/editor.js.map +1 -1
  27. package/lib/input/image-paste.d.ts +1 -2
  28. package/lib/input/image-paste.js +2 -7
  29. package/lib/input/image-paste.js.map +1 -1
  30. package/lib/input/key-platform.d.ts +2 -0
  31. package/lib/input/key-platform.js +8 -0
  32. package/lib/input/key-platform.js.map +1 -0
  33. package/lib/input/keybindings-config.d.ts +2 -1
  34. package/lib/input/keybindings-config.js +14 -9
  35. package/lib/input/keybindings-config.js.map +1 -1
  36. package/lib/input/keys.d.ts +2 -0
  37. package/lib/input/keys.js +10 -0
  38. package/lib/input/keys.js.map +1 -1
  39. package/lib/locale/zh.d.ts +4 -3
  40. package/lib/locale/zh.js +4 -3
  41. package/lib/locale/zh.js.map +1 -1
  42. package/lib/runtime/help-reader.d.ts +1 -1
  43. package/lib/runtime/help-reader.js +2 -2
  44. package/lib/runtime/help-reader.js.map +1 -1
  45. package/lib/runtime/herdr-agent.d.ts +2 -2
  46. package/lib/runtime/herdr-agent.js +2 -2
  47. package/lib/runtime/herdr-agent.js.map +1 -1
  48. package/lib/runtime/provider-local.d.ts +4 -1
  49. package/lib/runtime/provider-local.js +327 -71
  50. package/lib/runtime/provider-local.js.map +1 -1
  51. package/lib/runtime/terminal-notifications.d.ts +3 -1
  52. package/lib/runtime/terminal-notifications.js +7 -2
  53. package/lib/runtime/terminal-notifications.js.map +1 -1
  54. package/lib/runtime/terminal-session.d.ts +1 -0
  55. package/lib/runtime/terminal-session.js +1 -0
  56. package/lib/runtime/terminal-session.js.map +1 -1
  57. package/lib/runtime/terminal-state.d.ts +10 -1
  58. package/lib/runtime/terminal-state.js +39 -7
  59. package/lib/runtime/terminal-state.js.map +1 -1
  60. package/lib/session/authorization-interaction.js +3 -3
  61. package/lib/session/authorization-interaction.js.map +1 -1
  62. package/lib/session/content-text.d.ts +5 -0
  63. package/lib/session/content-text.js +7 -0
  64. package/lib/session/content-text.js.map +1 -1
  65. package/lib/session/interaction-adapter.js +1 -0
  66. package/lib/session/interaction-adapter.js.map +1 -1
  67. package/lib/session/session-controller.js +40 -6
  68. package/lib/session/session-controller.js.map +1 -1
  69. package/lib/views/file-browser.d.ts +3 -0
  70. package/lib/views/file-browser.js +1 -0
  71. package/lib/views/file-browser.js.map +1 -1
  72. package/lib/views/hotkeys.d.ts +4 -4
  73. package/lib/views/hotkeys.js +24 -14
  74. package/lib/views/hotkeys.js.map +1 -1
  75. package/lib/views/prompt-selector.d.ts +1 -0
  76. package/lib/views/prompt-selector.js +6 -3
  77. package/lib/views/prompt-selector.js.map +1 -1
  78. package/lib/views/settings-list.js +3 -3
  79. package/lib/views/settings-list.js.map +1 -1
  80. package/lib/views/trajectory.js +5 -5
  81. package/lib/views/trajectory.js.map +1 -1
  82. package/lib/views/transcript-render.d.ts +1 -0
  83. package/lib/views/transcript-render.js +48 -29
  84. package/lib/views/transcript-render.js.map +1 -1
  85. package/lib/views/transcript-types.d.ts +1 -4
  86. package/lib/views/transcript-types.js +4 -7
  87. package/lib/views/transcript-types.js.map +1 -1
  88. package/package.json +1 -1
@@ -23,7 +23,7 @@ import { localizePrompt } from "../locale/prompt.js";
23
23
  import { resolveUiLanguage, uiText } from "../locale/index.js";
24
24
  import { formatAgentPreset } from "../session/session-configuration.js";
25
25
  import { TerminalNotificationController, TerminalNotificationQueue, terminalNotificationSequence, } from "./terminal-notifications.js";
26
- import { PROGRAM_STATUS_QUERY, programStatusSequence, terminalTitle } from "./terminal-state.js";
26
+ import { PROGRAM_STATUS_QUERY, programStatusNeedsAttention, programStatusSequence, terminalProgressSequence, terminalTitle, workingDirectorySequence, } from "./terminal-state.js";
27
27
  import { TerminalSession } from "./terminal-session.js";
28
28
  import { BRACKETED_PASTE, FOCUS_REPORTING, MOUSE_TRACKING, } from "./terminal-modes.js";
29
29
  import { IDLE_WHEEL, terminalAcceleratesWheel, wheelStep } from "../input/wheel-scroll.js";
@@ -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();
@@ -378,6 +381,10 @@ export class LocalTui {
378
381
  #compactionStartedAt;
379
382
  #readImageInput;
380
383
  #writtenProgramState;
384
+ #writtenDirectory;
385
+ #launchDirectory = process.cwd();
386
+ #writtenProgress;
387
+ #progressTimer;
381
388
  #checkUpdates = true;
382
389
  #startupChangelog = 'summary';
383
390
  #notifications = new TerminalNotificationController();
@@ -432,6 +439,8 @@ export class LocalTui {
432
439
  #expandedReasoning = new Set();
433
440
  #expandedToolRows = new Set();
434
441
  #processSelection;
442
+ #promptAnchor;
443
+ #transcriptScopeChanged = false;
435
444
  #closedReasoning = new Set();
436
445
  /**
437
446
  * The last action's confirmation, drawn on the composer's bottom border.
@@ -501,6 +510,7 @@ export class LocalTui {
501
510
  #keybindingsPath;
502
511
  #keybindings;
503
512
  #cwd;
513
+ #rootDirectory;
504
514
  #projectRoot;
505
515
  #home;
506
516
  #listDir;
@@ -555,6 +565,7 @@ export class LocalTui {
555
565
  this.#cwd = paths.cwd ?? fallback.cwd;
556
566
  this.#readImagePath = paths.readImagePath ?? (path => readImageFile(path, this.#cwd));
557
567
  const project = resolveProjectContext(this.#cwd);
568
+ this.#rootDirectory = { cwd: this.#cwd, branch: project.gitLabel };
558
569
  this.#projectRoot = paths.projectRoot ?? project.root;
559
570
  this.#home = paths.home ?? fallback.home;
560
571
  this.#listDir = paths.listDir ?? fallback.listDir;
@@ -602,6 +613,7 @@ export class LocalTui {
602
613
  // live window so it stays anchored at the bottom.
603
614
  this.#selection.clear();
604
615
  this.#mouseActionPress = undefined;
616
+ this.#inputPress = undefined;
605
617
  this.#workspacePress = undefined;
606
618
  this.#jumpPress = undefined;
607
619
  this.#disclosurePress = undefined;
@@ -623,14 +635,7 @@ export class LocalTui {
623
635
  const resume = () => {
624
636
  if (this.#disposed)
625
637
  return;
626
- this.#terminalFocused = undefined;
627
- this.#writtenWindowTitle = undefined;
628
- this.#writtenProgramState = undefined;
629
- this.#programQuerySent = false;
630
- this.#programSupported = false;
631
- this.#syncWindowTitle();
632
- this.#session.takeBack();
633
- this.#renderer.reset();
638
+ this.#takeBackTerminal();
634
639
  this.#render();
635
640
  };
636
641
  process.on('SIGCONT', resume);
@@ -652,23 +657,38 @@ export class LocalTui {
652
657
  this.#syncWindowTitle();
653
658
  }
654
659
  /** Follow the viewed Session without replacing its transcript or native history. */
655
- 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
+ }
656
669
  if (cwd === this.#cwd && cwd === this.#projectRoot)
657
670
  return;
658
- this.#cwd = cwd;
671
+ this.#setViewedDirectory(cwd);
672
+ this.#syncWindowTitle();
673
+ if (this.#tty)
674
+ this.#render();
675
+ }
676
+ #setViewedDirectory(cwd) {
659
677
  const project = resolveProjectContext(cwd);
678
+ this.#cwd = cwd;
660
679
  this.#projectRoot = cwd;
661
680
  this.#pwd = shortenPath(cwd);
662
681
  this.#branch = project.gitLabel;
663
682
  this.#refreshAutocomplete();
664
- this.#syncWindowTitle();
665
- if (this.#tty)
666
- this.#render();
667
683
  }
668
684
  /** Refresh Git metadata without changing the Session's effective directory. */
669
685
  #refreshProjectContext() {
670
686
  const branch = refreshProjectContext(this.#cwd).gitLabel;
671
- 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)
672
692
  return;
673
693
  this.#branch = branch;
674
694
  this.#syncWindowTitle();
@@ -705,6 +725,8 @@ export class LocalTui {
705
725
  else if (event.type === 'session/end-seed')
706
726
  this.#directInputIds.clear();
707
727
  this.#state = applyEvent(this.#state, event, presentation, workspace);
728
+ if (event.type === 'turn/start' && this.#herdr.programStatus.state !== 'blocked')
729
+ this.#notificationQueue.clear();
708
730
  if (claimedHumanInput && this.#tty && event.type === 'agent/inbox/spliced') {
709
731
  this.#showTransientStatus(event.data.target === 'next-step' ? 'Steer taken for the next step' : 'Queue taken for the next turn');
710
732
  }
@@ -843,6 +865,7 @@ export class LocalTui {
843
865
  const current = this.#subagents?.agents.find(agent => agent.id === inspected.id);
844
866
  if (current !== undefined) {
845
867
  this.#inspected = {
868
+ ...inspected,
846
869
  id: current.id,
847
870
  label: current.label,
848
871
  phase: current.phase,
@@ -861,15 +884,25 @@ export class LocalTui {
861
884
  setInspectedSubagent(inspected) {
862
885
  if (this.#inspected?.id === inspected?.id && this.#inspected?.label === inspected?.label
863
886
  && this.#inspected?.phase === inspected?.phase && this.#inspected?.needsApproval === inspected?.needsApproval && this.#inspected?.mode === inspected?.mode
864
- && 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)
865
890
  return;
866
891
  if (this.#inspected?.id !== inspected?.id) {
892
+ this.#transcriptScopeChanged = true;
867
893
  this.#workspacePress = undefined;
868
894
  this.#fileReviewAbort?.abort();
869
895
  this.#closeFileBrowser();
896
+ this.#transcriptSearch = null;
897
+ this.#followTail();
870
898
  }
871
- this.#switchDraft(inspected?.id ?? this.#sessionId);
899
+ const leaving = this.#inspected !== undefined && inspected === undefined;
872
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);
873
906
  this.#drainPendingInput();
874
907
  if (inspected !== undefined) {
875
908
  this.#inspectEscapeFenceUntil = 0;
@@ -903,7 +936,7 @@ export class LocalTui {
903
936
  this.#settings = null;
904
937
  this.#search = null;
905
938
  this.#ac = null;
906
- 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)
907
940
  this.#render(); }, () => {
908
941
  if (this.#fileBrowser === browser)
909
942
  this.#fileBrowser = null;
@@ -1251,7 +1284,7 @@ export class LocalTui {
1251
1284
  this.#pasteBurst.clear();
1252
1285
  if (request.notify !== false) {
1253
1286
  this.#emitNotification(this.#notifications.humanPrompt());
1254
- this.#herdr.prompt(request.title);
1287
+ this.#herdr.prompt(request.title, request.attentionKind);
1255
1288
  this.#syncWindowTitle();
1256
1289
  }
1257
1290
  if (this.#tty && request.documentImage !== undefined && this.#imageProtocol !== undefined)
@@ -1371,10 +1404,23 @@ export class LocalTui {
1371
1404
  // A notice does not turn a blank session into a document that /new needs
1372
1405
  // to separate. Refreshes are the same document.
1373
1406
  const boundary = reason === 'open' || (reason === 'new' && this.#state.blocks.some(block => block.kind !== 'notice'));
1374
- 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];
1375
1412
  this.#state = { ...state, status, compactCommandId: undefined, blocks };
1376
1413
  this.#plain.resetPrinted();
1377
- 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
+ }
1378
1424
  this.#deferInitialRender = false;
1379
1425
  this.#renderer.startEpoch({ replay: reason === 'refresh' ? 'viewport' : status === 'idle' ? 'full' : 'pinned' });
1380
1426
  if (this.#tty)
@@ -1384,9 +1430,12 @@ export class LocalTui {
1384
1430
  }
1385
1431
  setSession(info) {
1386
1432
  if (info.id !== this.#sessionId) {
1433
+ this.#transcriptScopeChanged = true;
1387
1434
  this.#workspacePress = undefined;
1388
1435
  this.#fileReviewAbort?.abort();
1389
1436
  this.#closeFileBrowser();
1437
+ this.#notificationQueue.clear();
1438
+ this.#notifications.reset();
1390
1439
  }
1391
1440
  this.#switchDraft(this.#inspected?.id ?? info.id);
1392
1441
  this.#sessionId = info.id;
@@ -1413,11 +1462,13 @@ export class LocalTui {
1413
1462
  this.#render();
1414
1463
  }
1415
1464
  /** Only the foreground terminal owner emits title and program-state sequences. */
1416
- #syncWindowTitle() {
1417
- if (!this.#tty || this.#disposed)
1465
+ #syncWindowTitle(notify = false) {
1466
+ if (!this.#tty || this.#disposed || this.#session.handedOff)
1418
1467
  return;
1419
1468
  const status = this.#herdr.programStatus;
1420
- const title = terminalTitle(this.#terminalTitle, status, this.#sessionTitle, this.#cwd, this.#branch);
1469
+ this.#writeWorkingDirectory(this.#cwd);
1470
+ this.#syncProgress(status.state);
1471
+ const title = terminalTitle(this.#terminalTitle, status, this.#sessionTitle, this.#rootDirectory.cwd, this.#rootDirectory.branch);
1421
1472
  if (title !== this.#writtenWindowTitle && !(this.#terminalTitle === 'off' && this.#writtenWindowTitle === undefined)) {
1422
1473
  this.#writtenWindowTitle = title;
1423
1474
  this.#term.output.write(`\x1b]2;${title}\x07`);
@@ -1426,20 +1477,78 @@ export class LocalTui {
1426
1477
  this.#programQuerySent = true;
1427
1478
  this.#term.output.write(PROGRAM_STATUS_QUERY);
1428
1479
  }
1429
- const enabled = this.#terminalState === 'on' || this.#terminalState === 'auto' && this.#programSupported;
1430
- if (!enabled) {
1480
+ if (!this.#reportsProgramState) {
1431
1481
  if (this.#writtenProgramState !== undefined)
1432
1482
  this.#term.output.write(programStatusSequence('clear'));
1433
1483
  this.#writtenProgramState = undefined;
1434
1484
  return;
1435
1485
  }
1436
1486
  // Only the session name is exported as metadata; prompt bodies and errors stay in the TUI.
1437
- const sequence = programStatusSequence({ state: status.state, ...(this.#sessionTitle === undefined ? {} : { message: this.#sessionTitle }) });
1487
+ const sequence = programStatusSequence({ state: status.state,
1488
+ ...(status.kind === undefined ? {} : { kind: status.kind }),
1489
+ ...(this.#sessionTitle === undefined ? {} : { message: this.#sessionTitle }),
1490
+ });
1491
+ // Supporting terminals can turn these states into desktop notifications.
1492
+ // New attention reports use the same coalescing and focus gate as OSC 9.
1493
+ if (programStatusNeedsAttention(status.state)
1494
+ && (this.#notificationPolicy === 'off' || !notify && sequence !== this.#writtenProgramState)) {
1495
+ if (this.#writtenProgramState !== undefined)
1496
+ this.#term.output.write(programStatusSequence('clear'));
1497
+ this.#writtenProgramState = undefined;
1498
+ return;
1499
+ }
1438
1500
  if (sequence === this.#writtenProgramState)
1439
1501
  return;
1440
1502
  this.#writtenProgramState = sequence;
1441
1503
  this.#term.output.write(sequence);
1442
1504
  }
1505
+ get #reportsProgramState() {
1506
+ return this.#terminalState === 'on' || this.#terminalState === 'auto' && this.#programSupported;
1507
+ }
1508
+ #writeWorkingDirectory(cwd) {
1509
+ const sequence = workingDirectorySequence(cwd);
1510
+ if (sequence === this.#writtenDirectory)
1511
+ return;
1512
+ this.#term.output.write(sequence);
1513
+ this.#writtenDirectory = sequence;
1514
+ }
1515
+ #syncProgress(state) {
1516
+ const sequence = this.#terminalState === 'off' ? terminalProgressSequence('clear') : terminalProgressSequence(state);
1517
+ const active = sequence !== terminalProgressSequence('clear');
1518
+ if (sequence !== this.#writtenProgress && (active || this.#writtenProgress !== undefined))
1519
+ this.#term.output.write(sequence);
1520
+ this.#writtenProgress = active ? sequence : undefined;
1521
+ if (this.#progressTimer !== undefined && (state !== 'working' || this.#terminalState === 'off')) {
1522
+ clearInterval(this.#progressTimer);
1523
+ this.#progressTimer = undefined;
1524
+ }
1525
+ // Some terminals expire indeterminate progress without a heartbeat.
1526
+ if (state === 'working' && this.#terminalState !== 'off' && this.#progressTimer === undefined) {
1527
+ this.#progressTimer = setInterval(() => {
1528
+ if (!this.#disposed && !this.#session.handedOff && this.#writtenProgress !== undefined)
1529
+ this.#term.output.write(this.#writtenProgress);
1530
+ }, 1000);
1531
+ this.#progressTimer.unref?.();
1532
+ }
1533
+ }
1534
+ /** Give shell/editor ownership back without retaining omdsh metadata. */
1535
+ #releaseTerminalMetadata() {
1536
+ if (this.#progressTimer !== undefined)
1537
+ clearInterval(this.#progressTimer);
1538
+ this.#progressTimer = undefined;
1539
+ if (this.#writtenProgress !== undefined)
1540
+ this.#term.output.write(terminalProgressSequence('clear'));
1541
+ this.#writtenProgress = undefined;
1542
+ if (this.#writtenProgramState !== undefined)
1543
+ this.#term.output.write(programStatusSequence('clear'));
1544
+ this.#writtenProgramState = undefined;
1545
+ if (this.#writtenWindowTitle !== undefined && this.#writtenWindowTitle !== '')
1546
+ this.#term.output.write('\x1b]2;\x07');
1547
+ this.#writtenWindowTitle = undefined;
1548
+ if (this.#writtenDirectory !== undefined)
1549
+ this.#writeWorkingDirectory(this.#launchDirectory);
1550
+ this.#writtenDirectory = undefined;
1551
+ }
1443
1552
  /**
1444
1553
  * Re-derive the color depth from the active color preference. Every path
1445
1554
  * that changes `#colors` calls this so `/settings` takes effect immediately.
@@ -1835,7 +1944,9 @@ export class LocalTui {
1835
1944
  this.#renderer.finish();
1836
1945
  }
1837
1946
  catch { /* best effort while exiting */ }
1947
+ this.#releaseTerminalMetadata();
1838
1948
  this.#session.release();
1949
+ this.#term.input.setRawMode?.(false);
1839
1950
  this.#term.output.write('\x1b[?25h');
1840
1951
  }
1841
1952
  dispose() {
@@ -1898,14 +2009,7 @@ export class LocalTui {
1898
2009
  // prompt does not overwrite the transcript. Disable bracketed paste
1899
2010
  // and restore the cursor.
1900
2011
  this.#renderer.finish();
1901
- // Clear the session title we owned so the shell's own title returns.
1902
- if (this.#writtenWindowTitle !== undefined && this.#writtenWindowTitle !== '') {
1903
- this.#term.output.write('\x1b]2;\x07');
1904
- this.#writtenWindowTitle = undefined;
1905
- }
1906
- if (this.#writtenProgramState !== undefined)
1907
- this.#term.output.write(programStatusSequence('clear'));
1908
- this.#writtenProgramState = undefined;
2012
+ this.#releaseTerminalMetadata();
1909
2013
  this.#offExit?.();
1910
2014
  this.#offExit = undefined;
1911
2015
  this.#session.dispose();
@@ -1949,6 +2053,19 @@ export class LocalTui {
1949
2053
  refresh() {
1950
2054
  this.#render();
1951
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
+ }
1952
2069
  #busy() {
1953
2070
  if (this.#state.status !== 'idle')
1954
2071
  return true;
@@ -2174,6 +2291,13 @@ export class LocalTui {
2174
2291
  if (this.#probeGate)
2175
2292
  return;
2176
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');
2177
2301
  const renderState = this.#reveal === undefined
2178
2302
  ? this.#state
2179
2303
  : revealStreamingAssistant(this.#state, this.#reveal.revealed);
@@ -2211,9 +2335,9 @@ export class LocalTui {
2211
2335
  themeName: this.#themeName,
2212
2336
  terminal: this.#terminalColors,
2213
2337
  ...(workingLabel === undefined ? {} : { workingLabel }),
2338
+ ...(this.#selection.active ? { selectionActions: this.#canQuoteSelection() ? 'copy-quote' : 'copy' } : {}),
2214
2339
  ...(this.#transientStatus !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), this.#transientStatus) }
2215
- : this.#selection.active ? { selectionActions: this.#canQuoteSelection() ? 'copy-quote' : 'copy' }
2216
- : this.#processSelection !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), 'Process · ↑↓ select · Enter toggle · {key} actions · Home title · Esc return', { key: keysForAction(this.#keybindings, 'tool-actions') }) } : {}),
2340
+ : !this.#selection.active && this.#processSelection !== undefined ? { transientStatus: uiText(resolveUiLanguage(this.#uiLanguage), 'Process · ↑↓ select · Enter toggle · {key} actions · Home title · Esc return', { key: keysForAction(this.#keybindings, 'tool-actions') }) } : {}),
2217
2341
  scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
2218
2342
  ...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start', ...(this.#focusBlockOffset === undefined ? {} : { focusBlockOffset: this.#focusBlockOffset }) }),
2219
2343
  ...(this.#transcriptSearch === null ? {} : {
@@ -2232,6 +2356,7 @@ export class LocalTui {
2232
2356
  toolDetailsKey: keysForAction(this.#keybindings, 'toggle-tool-details'),
2233
2357
  turnDetailsKey: keysForAction(this.#keybindings, 'toggle-tools'),
2234
2358
  busyEnter: this.#queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#busyEnter,
2359
+ ...(this.#queueEditNewer !== null || this.#queueEditPending || alternateSendKey === 'Disabled' ? {} : { alternateSendKey }),
2235
2360
  steerKey: keysForAction(this.#keybindings, 'steer-turn'),
2236
2361
  queueKey: keysForAction(this.#keybindings, 'follow-up'),
2237
2362
  openedGroups: this.#openedGroups,
@@ -2267,7 +2392,8 @@ export class LocalTui {
2267
2392
  this.#syncScroll(frame.transcript);
2268
2393
  this.#jumpToLatest = frame.jumpToLatest;
2269
2394
  this.#fileActionFrame = frame.transcript === undefined || frame.transientSurface === 'overlay' ? undefined : frame;
2270
- 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'
2271
2397
  && (this.#inspecting || this.#maxStart > 0 || (frame.transcript.fileRegions?.length ?? 0) > 0 || (frame.transcript.disclosures?.length ?? 0) > 0)));
2272
2398
  // A run opening or folding reshapes rows above the screen in place; the
2273
2399
  // renderer's frozen boundary is a row index and has to be told, or a fold
@@ -2313,6 +2439,7 @@ export class LocalTui {
2313
2439
  }
2314
2440
  #scrollBy(delta) {
2315
2441
  this.#workspacePress = undefined;
2442
+ this.#promptAnchor = undefined;
2316
2443
  if (this.#follow && (delta > 0 || this.#maxStart === 0))
2317
2444
  return;
2318
2445
  if (delta === 0 && this.#maxStart === 0)
@@ -2336,6 +2463,7 @@ export class LocalTui {
2336
2463
  this.#render();
2337
2464
  }
2338
2465
  #followTail() {
2466
+ this.#promptAnchor = undefined;
2339
2467
  this.#follow = true;
2340
2468
  this.#scrollStart = this.#maxStart;
2341
2469
  this.#endInspection();
@@ -2390,20 +2518,31 @@ export class LocalTui {
2390
2518
  /** Give every terminal mode back before the terminal is lent out (Ctrl+Z, external editor). */
2391
2519
  #handOffTerminal() {
2392
2520
  this.#setMouseTracking(false);
2521
+ this.#releaseTerminalMetadata();
2393
2522
  this.#session.handOff();
2523
+ this.#term.input.setRawMode?.(false);
2524
+ }
2525
+ #takeBackTerminal() {
2526
+ this.#session.takeBack();
2527
+ this.#term.input.setRawMode?.(true);
2528
+ this.#terminalFocused = undefined;
2529
+ this.#writtenWindowTitle = undefined;
2530
+ this.#writtenProgramState = undefined;
2531
+ this.#writtenDirectory = undefined;
2532
+ this.#programQuerySent = false;
2533
+ this.#programSupported = false;
2534
+ this.#syncWindowTitle();
2535
+ this.#renderer.reset();
2394
2536
  }
2395
2537
  /** Run `run` with the terminal handed off; take it back and repaint afterwards. */
2396
2538
  #handTerminal(run) {
2397
2539
  this.#handOffTerminal();
2398
- this.#term.input.setRawMode?.(false);
2399
2540
  try {
2400
2541
  return run();
2401
2542
  }
2402
2543
  finally {
2403
2544
  if (!this.#disposed) {
2404
- this.#term.input.setRawMode?.(true);
2405
- this.#session.takeBack();
2406
- this.#renderer.reset();
2545
+ this.#takeBackTerminal();
2407
2546
  }
2408
2547
  }
2409
2548
  }
@@ -2412,6 +2551,7 @@ export class LocalTui {
2412
2551
  * or go back to the live tail once nothing is open any more.
2413
2552
  */
2414
2553
  #inspect(target) {
2554
+ this.#promptAnchor = undefined;
2415
2555
  const open = this.#toolsExpanded || this.#openedGroups.size > 0 || this.#expandedOutputs.size > 0;
2416
2556
  if (!open) {
2417
2557
  this.#inspecting = false;
@@ -2747,6 +2887,40 @@ export class LocalTui {
2747
2887
  this.#handleTrajectoryMouse(event);
2748
2888
  return;
2749
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
+ }
2750
2924
  const promptAction = this.#promptActions.find(action => event.row === action.row && event.column >= action.column && event.column < action.column + action.width);
2751
2925
  if (this.#promptActionPress !== undefined) {
2752
2926
  const pressed = this.#promptActionPress;
@@ -2819,7 +2993,7 @@ export class LocalTui {
2819
2993
  this.#workspacePress = undefined;
2820
2994
  if (event.button === 'left' && !event.shift && target?.key === pressedAction.key && target.row === pressedAction.row && target.column === pressedAction.column) {
2821
2995
  if (target.action === 'copy')
2822
- void this.#copySelection(true);
2996
+ void this.#copySelection();
2823
2997
  else if (target.action === 'quote')
2824
2998
  this.#quoteSelection();
2825
2999
  else {
@@ -2851,7 +3025,7 @@ export class LocalTui {
2851
3025
  this.#disclosurePress = undefined;
2852
3026
  if (event.action === 'press') {
2853
3027
  if (this.#selection.active)
2854
- void this.#copySelection(true);
3028
+ void this.#copySelection();
2855
3029
  else {
2856
3030
  const region = this.#fileRegionAt(event.row);
2857
3031
  if (region !== undefined) {
@@ -2926,7 +3100,7 @@ export class LocalTui {
2926
3100
  #handleTrajectoryMouse(event) {
2927
3101
  if (event.button === 'right') {
2928
3102
  if (event.action === 'press' && this.#selection.active)
2929
- void this.#copySelection(true);
3103
+ void this.#copySelection();
2930
3104
  return;
2931
3105
  }
2932
3106
  if (event.action === 'press') {
@@ -2998,7 +3172,7 @@ export class LocalTui {
2998
3172
  this.#copyTask = task.catch(() => { });
2999
3173
  return task;
3000
3174
  }
3001
- async #copySelection(explicit = false) {
3175
+ async #copySelection() {
3002
3176
  if (!this.#selection.valid(this.#renderer.viewport())) {
3003
3177
  this.#selection.clear();
3004
3178
  return;
@@ -3008,28 +3182,30 @@ export class LocalTui {
3008
3182
  return;
3009
3183
  try {
3010
3184
  const delivery = await this.#deliverCopy(text);
3011
- if (explicit && !this.#disposed) {
3185
+ if (!this.#disposed && this.#selection.text() === text) {
3012
3186
  this.#showTransientStatus(delivery === 'requested' ? 'Selection sent to terminal clipboard' : 'Copied selection');
3013
- this.#render();
3014
3187
  }
3015
3188
  }
3016
3189
  catch {
3017
- if (!this.#disposed) {
3190
+ if (!this.#disposed && this.#selection.text() === text)
3018
3191
  this.#showTransientStatus('Copy failed');
3019
- this.#render();
3020
- }
3021
3192
  }
3022
3193
  }
3023
3194
  #canQuoteSelection() {
3024
- 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
3025
3196
  && this.#transcriptSearch === null && (this.#inspected === undefined || this.#inspected.writable === true);
3026
3197
  }
3198
+ #fileBrowserOwnsInput() {
3199
+ const state = this.#fileBrowser?.state;
3200
+ return state !== undefined && (state.editing !== undefined || state.help || state.menu !== undefined);
3201
+ }
3027
3202
  #quoteSelection() {
3028
3203
  if (!this.#canQuoteSelection())
3029
3204
  return;
3030
3205
  const text = this.#selection.valid(this.#renderer.viewport()) ? this.#selection.text() : undefined;
3031
3206
  this.#selection.clear();
3032
3207
  if (text !== undefined) {
3208
+ this.#closeFileBrowser();
3033
3209
  this.#appendQuote(text.split('\n').map(line => '> ' + line).join('\n'));
3034
3210
  }
3035
3211
  this.#render();
@@ -3152,6 +3328,9 @@ export class LocalTui {
3152
3328
  }
3153
3329
  if (this.#session.handle(event))
3154
3330
  return;
3331
+ if (!this.#paste && event.type === 'key' && (event.id === 'ctrl+enter' || event.id === 'super+enter')) {
3332
+ this.#receivedModifiedEnter.add(event.id);
3333
+ }
3155
3334
  if (event.type === 'focus') {
3156
3335
  this.#terminalFocused = event.focused;
3157
3336
  if (event.focused && this.#notificationPolicy === 'unfocused')
@@ -3168,18 +3347,23 @@ export class LocalTui {
3168
3347
  this.#disclosurePress = undefined;
3169
3348
  this.#mouseActionPress = undefined;
3170
3349
  this.#workspacePress = undefined;
3350
+ this.#inputPress = undefined;
3171
3351
  if (this.#keyTest !== undefined && this.#prompt?.request.signal === this.#keyTest.signal
3172
3352
  && (event.type === 'text' || event.type === 'paste-start' || event.type === 'paste-end' || event.type === 'key' && !['escape', 'ctrl+c'].includes(event.id))) {
3173
3353
  this.#keyTest.observe(event);
3174
3354
  return;
3175
3355
  }
3176
3356
  if (this.#selection.active) {
3177
- if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection') {
3357
+ if (event.type === 'key' && this.#boundAction(event.id) === 'quote-selection' && this.#canQuoteSelection()) {
3178
3358
  this.#quoteSelection();
3179
3359
  return;
3180
3360
  }
3181
- if (event.type === 'key' && (['super+c', 'enter'].includes(event.id) || this.#boundAction(event.id) === 'copy-prompt')) {
3182
- void this.#copySelection(true);
3361
+ if (this.#fileBrowser !== null && event.type === 'text' && event.value.toLowerCase() === 'q' && this.#canQuoteSelection()) {
3362
+ this.#quoteSelection();
3363
+ return;
3364
+ }
3365
+ if (event.type === 'key' && (event.id === 'super+c' || event.id === 'enter' && !this.#fileBrowserOwnsInput() || this.#boundAction(event.id) === 'copy-prompt')) {
3366
+ void this.#copySelection();
3183
3367
  return;
3184
3368
  }
3185
3369
  this.#selection.clear();
@@ -3198,6 +3382,11 @@ export class LocalTui {
3198
3382
  this.#applyTrajectoryInput(event);
3199
3383
  return;
3200
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
+ }
3201
3390
  this.#jumpPress = undefined;
3202
3391
  if (this.#mouseTracking && !this.#paste && this.#pasteInFlight === 0) {
3203
3392
  const step = wheelStep(this.#wheelGesture, Date.now(), event.direction === 'up' ? -1 : 1, !this.#wheelAccelerates);
@@ -3344,7 +3533,7 @@ export class LocalTui {
3344
3533
  this.#render();
3345
3534
  return;
3346
3535
  }
3347
- 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')
3348
3537
  && this.#clearedDraft !== undefined && this.#clearedDraft.revision === this.#editor.revision
3349
3538
  && !this.#hasComposerContent()) {
3350
3539
  const cleared = this.#clearedDraft.draft;
@@ -4014,7 +4203,6 @@ export class LocalTui {
4014
4203
  this.#externalEditor = prefs.editor ?? 'auto';
4015
4204
  this.#terminalTitle = prefs.terminalTitle ?? 'status';
4016
4205
  this.#terminalState = prefs.terminalState ?? 'auto';
4017
- this.#syncWindowTitle();
4018
4206
  this.#copyOnSelect = prefs.copyOnSelect ?? true;
4019
4207
  this.#mathMode = prefs.mathMode ?? 'auto';
4020
4208
  this.#mermaidMode = prefs.mermaidMode ?? 'auto';
@@ -4027,6 +4215,7 @@ export class LocalTui {
4027
4215
  if (this.#notificationPolicy === 'off')
4028
4216
  this.#notificationQueue.clear();
4029
4217
  this.#configureNotifications();
4218
+ this.#syncWindowTitle();
4030
4219
  this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
4031
4220
  if (options.forceToolsSync)
4032
4221
  this.#toolsExpanded = false;
@@ -4522,7 +4711,10 @@ export class LocalTui {
4522
4711
  try {
4523
4712
  process.kill(process.pid, 'SIGTSTP');
4524
4713
  }
4525
- catch { /* no controlling tty */ }
4714
+ catch {
4715
+ this.#takeBackTerminal();
4716
+ this.#render();
4717
+ }
4526
4718
  }
4527
4719
  #historyPrev() {
4528
4720
  if (this.#images.count > 0)
@@ -4670,6 +4862,11 @@ export class LocalTui {
4670
4862
  this.#queuedSubmissions.push(submission);
4671
4863
  this.#pendingInputs.flush(this.#pendingScope());
4672
4864
  }
4865
+ #resolveDelivery(delivery) {
4866
+ if (delivery === 'alternate')
4867
+ return this.#busyEnter === 'queue' ? 'steer' : 'queue';
4868
+ return delivery ?? this.#busyEnter;
4869
+ }
4673
4870
  #submit(text, delivery) {
4674
4871
  if (this.#prompt !== null) {
4675
4872
  if (this.#prompt.request.presentation === 'progress')
@@ -4691,7 +4888,7 @@ export class LocalTui {
4691
4888
  submission.literal = true;
4692
4889
  const command = !submission.literal && (submittedText.trimStart().startsWith('/') || (images.length === 0 && files.length === 0 && submittedText.startsWith('!')));
4693
4890
  if (!command && this.#state.status === 'running') {
4694
- submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : delivery ?? this.#busyEnter;
4891
+ submission.delivery = queueEditNewer !== null || this.#queueEditPending ? 'queue' : this.#resolveDelivery(delivery);
4695
4892
  }
4696
4893
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
4697
4894
  this.#history.push(historyText);
@@ -4739,7 +4936,7 @@ export class LocalTui {
4739
4936
  const newer = this.#queueEditNewer;
4740
4937
  if (delivery === 'steer')
4741
4938
  submission.literal = true;
4742
- 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);
4743
4940
  const { images, files = [], literal = false, text: submittedText } = submission;
4744
4941
  if (historyText !== '' && this.#history[this.#history.length - 1] !== historyText) {
4745
4942
  this.#history.push(historyText);
@@ -4880,6 +5077,8 @@ export class LocalTui {
4880
5077
  this.#wheelGesture = IDLE_WHEEL;
4881
5078
  if (pending.request.notify !== false) {
4882
5079
  this.#herdr.promptResolved();
5080
+ if (this.#herdr.programStatus.state !== 'blocked')
5081
+ this.#notificationQueue.clear('blocked');
4883
5082
  this.#syncWindowTitle();
4884
5083
  }
4885
5084
  pending.offAbort?.();
@@ -5092,20 +5291,29 @@ export class LocalTui {
5092
5291
  const pending = this.#notificationQueue.take();
5093
5292
  if (pending === undefined || this.#disposed || this.#notificationPolicy === 'off')
5094
5293
  return;
5294
+ if (this.#session.handedOff)
5295
+ return;
5095
5296
  // Re-read focus at delivery time: the reader may have come back inside the
5096
5297
  // coalescing window, and a turn finishing while they watch is not news.
5097
5298
  if (this.#notificationPolicy === 'unfocused' && this.#terminalFocused === true)
5098
5299
  return;
5300
+ if (pending.programState !== undefined && pending.programState !== this.#herdr.programStatus.state)
5301
+ return;
5302
+ if (this.#reportsProgramState && programStatusNeedsAttention(this.#herdr.programStatus.state)) {
5303
+ this.#syncWindowTitle(true);
5304
+ return;
5305
+ }
5099
5306
  const sequence = terminalNotificationSequence(pending);
5100
5307
  if (sequence !== '')
5101
5308
  this.#term.output.write(sequence);
5102
5309
  }, 25);
5103
5310
  }
5104
5311
  #runHelp(full) {
5312
+ const enhancedKeys = this.#session.keyboardProtocol === 'kitty';
5105
5313
  if (!this.#tty) {
5106
- this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings),
5314
+ this.commandOutput('help', ['Help', '', '## Essential Shortcuts', '', formatEssentialHotkeysText(this.#keybindings, enhancedKeys),
5107
5315
  '', formatHelpText(this.#commands(), resolveUiLanguage(this.#uiLanguage)),
5108
- ...(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'));
5109
5317
  return;
5110
5318
  }
5111
5319
  if (this.#fileReviewAbort !== undefined || this.#prompt !== null)
@@ -5117,7 +5325,7 @@ export class LocalTui {
5117
5325
  copyText: (text, label) => this.copyText(text, label),
5118
5326
  insertText: (text) => this.insertText(text),
5119
5327
  };
5120
- 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)
5121
5329
  .catch(error => { if (!abort.signal.aborted && !this.#disposed)
5122
5330
  this.notice(String(error), { level: 'error' }); })
5123
5331
  .finally(() => { if (this.#fileReviewAbort === abort)
@@ -5173,6 +5381,19 @@ export class LocalTui {
5173
5381
  // closes. Claiming it here would skip that layering.
5174
5382
  if (action === 'interrupt')
5175
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
+ }
5176
5397
  if (action === 'clear-composer')
5177
5398
  return this.#runClearComposer();
5178
5399
  if (action === 'quit') {
@@ -5339,6 +5560,33 @@ export class LocalTui {
5339
5560
  this.#scrollBy(-this.#pageSize());
5340
5561
  return true;
5341
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
+ }
5342
5590
  if (action === 'scroll-page-down') {
5343
5591
  if (this.#search !== null)
5344
5592
  return false;
@@ -5634,7 +5882,7 @@ export class LocalTui {
5634
5882
  });
5635
5883
  if (entered === null || this.#disposed)
5636
5884
  continue;
5637
- const normalized = entered.trim().toLowerCase();
5885
+ const normalized = normalizeKeyId(entered);
5638
5886
  if (normalized === '')
5639
5887
  continue;
5640
5888
  const next = { ...this.#keybindings };
@@ -5769,6 +6017,7 @@ export class LocalTui {
5769
6017
  return true;
5770
6018
  }
5771
6019
  if (event.id === 'home') {
6020
+ this.#promptAnchor = undefined;
5772
6021
  const current = this.#fileActionFrame?.transcript?.disclosures?.findLast(target => target.detailEnd !== undefined && this.#scrollStart > target.row && this.#scrollStart < target.detailEnd);
5773
6022
  this.#processSelection = current?.block ?? selected;
5774
6023
  this.#focusBlock = this.#processSelection;
@@ -5777,6 +6026,7 @@ export class LocalTui {
5777
6026
  return true;
5778
6027
  }
5779
6028
  if (event.id === 'up' || event.id === 'down') {
6029
+ this.#promptAnchor = undefined;
5780
6030
  const rows = this.#processRows();
5781
6031
  const at = Math.max(0, rows.indexOf(selected));
5782
6032
  const next = rows[Math.max(0, Math.min(rows.length - 1, at + (event.id === 'up' ? -1 : 1)))];
@@ -5963,12 +6213,18 @@ export class LocalTui {
5963
6213
  return;
5964
6214
  }
5965
6215
  this.#transcriptSearch = command.state;
5966
- if (command.kind === 'focus') {
5967
- this.#focusBlock = command.match.block;
5968
- this.#focusBlockEdge = 'start';
5969
- }
6216
+ if (command.kind === 'focus')
6217
+ this.#focusSearchMatch(command.match);
5970
6218
  this.#render();
5971
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
+ }
5972
6228
  }
5973
6229
  /**
5974
6230
  * Mount the local terminal provider as the tui service.