@agi-fans/dsh-tui 0.7.0 → 0.9.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 (65) hide show
  1. package/lib/CHANGELOG.md +44 -1
  2. package/lib/chrome/box.d.ts +0 -7
  3. package/lib/chrome/box.js +3 -22
  4. package/lib/chrome/box.js.map +1 -1
  5. package/lib/chrome/code-highlight.d.ts +45 -0
  6. package/lib/chrome/code-highlight.js +155 -0
  7. package/lib/chrome/code-highlight.js.map +1 -0
  8. package/lib/chrome/diff-render.d.ts +3 -1
  9. package/lib/chrome/diff-render.js +73 -9
  10. package/lib/chrome/diff-render.js.map +1 -1
  11. package/lib/chrome/main-screen-renderer.d.ts +55 -0
  12. package/lib/chrome/main-screen-renderer.js +343 -0
  13. package/lib/chrome/main-screen-renderer.js.map +1 -0
  14. package/lib/chrome/markdown.d.ts +3 -6
  15. package/lib/chrome/markdown.js +5 -59
  16. package/lib/chrome/markdown.js.map +1 -1
  17. package/lib/chrome/md-style.d.ts +31 -0
  18. package/lib/chrome/md-style.js +58 -0
  19. package/lib/chrome/md-style.js.map +1 -0
  20. package/lib/chrome/renderer.d.ts +8 -31
  21. package/lib/chrome/renderer.js.map +1 -1
  22. package/lib/chrome/welcome-tips.js +2 -2
  23. package/lib/chrome/welcome-tips.js.map +1 -1
  24. package/lib/chrome/width.d.ts +15 -0
  25. package/lib/chrome/width.js +83 -0
  26. package/lib/chrome/width.js.map +1 -1
  27. package/lib/commands/auth.d.ts +2 -1
  28. package/lib/commands/auth.js +318 -38
  29. package/lib/commands/auth.js.map +1 -1
  30. package/lib/commands/permission.js +1 -6
  31. package/lib/commands/permission.js.map +1 -1
  32. package/lib/definition.d.ts +1 -1
  33. package/lib/input/keys.d.ts +1 -28
  34. package/lib/input/keys.js +15 -31
  35. package/lib/input/keys.js.map +1 -1
  36. package/lib/runtime/provider-local.d.ts +7 -5
  37. package/lib/runtime/provider-local.js +134 -208
  38. package/lib/runtime/provider-local.js.map +1 -1
  39. package/lib/session/authorization-interaction.d.ts +12 -0
  40. package/lib/session/authorization-interaction.js +70 -0
  41. package/lib/session/authorization-interaction.js.map +1 -0
  42. package/lib/session/session-controller.d.ts +8 -4
  43. package/lib/session/session-controller.js +13 -12
  44. package/lib/session/session-controller.js.map +1 -1
  45. package/lib/views/autocomplete.d.ts +0 -2
  46. package/lib/views/autocomplete.js +0 -8
  47. package/lib/views/autocomplete.js.map +1 -1
  48. package/lib/views/copy-selector.d.ts +0 -4
  49. package/lib/views/copy-selector.js +0 -17
  50. package/lib/views/copy-selector.js.map +1 -1
  51. package/lib/views/event-views.d.ts +3 -16
  52. package/lib/views/event-views.js +127 -93
  53. package/lib/views/event-views.js.map +1 -1
  54. package/lib/views/history-search.d.ts +0 -5
  55. package/lib/views/history-search.js +0 -16
  56. package/lib/views/history-search.js.map +1 -1
  57. package/lib/views/hotkeys.js +0 -1
  58. package/lib/views/hotkeys.js.map +1 -1
  59. package/lib/views/prompt-selector.d.ts +0 -5
  60. package/lib/views/prompt-selector.js +0 -13
  61. package/lib/views/prompt-selector.js.map +1 -1
  62. package/lib/views/settings-list.d.ts +0 -5
  63. package/lib/views/settings-list.js +0 -23
  64. package/lib/views/settings-list.js.map +1 -1
  65. package/package.json +29 -27
@@ -3,7 +3,7 @@
3
3
 
4
4
  * Owns the tty: raw-mode key input (editing, history, slash/tab
5
5
  * autocomplete, /settings overlay, /copy picker, Ctrl-R history search, PgUp/PgDn
6
- * and mouse-wheel transcript scroll, click-to-caret, Ctrl-O tool
6
+ * and Shift+Up/Down transcript scroll, Ctrl-O tool
7
7
  * expand, bracketed paste, double-Escape conversation rewind, double Ctrl-C exit, Ctrl-D quit),
8
8
  * SIGWINCH reflow, and the differential renderer. In non-tty mode
9
9
  * (pipes, CI) it degrades to line-based input with plain append-only
@@ -14,20 +14,19 @@ import { homedir } from 'node:os';
14
14
  import { join } from 'node:path';
15
15
  import { createInterface } from 'node:readline';
16
16
  import { TUI_SERVICE, } from "../definition.js";
17
- import { applySlashCompletion, formatHelpText, hitTestAutocomplete, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "../views/autocomplete.js";
17
+ import { applySlashCompletion, formatHelpText, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "../views/autocomplete.js";
18
18
  import { applyPathCompletion, defaultPathSource, editorLineAt, findPathToken, parsePathPrefix, pathSuggestions, searchPathSuggestions, } from "../views/path-complete.js";
19
19
  import { activeAtToken } from '@deepseek-ai/dsh-file-reference/grammar';
20
20
  import { nextSelectableAutocompleteIndex, searchAtSuggestions, } from "../views/at-complete.js";
21
21
  import { copyToClipboard, readFromClipboard } from "../input/clipboard.js";
22
- import { applyCopySelectorEvent, createCopySelector, hitTestCopySelector, selectCopyTarget, } from "../views/copy-selector.js";
22
+ import { applyCopySelectorEvent, createCopySelector, } from "../views/copy-selector.js";
23
23
  import { buildCopyTargets, extractCopyTarget, parseCopyKind } from "../views/copy-targets.js";
24
- import { applyHistorySearchEvent, createHistorySearch, hitTestHistorySearch, } from "../views/history-search.js";
24
+ import { applyHistorySearchEvent, createHistorySearch, } from "../views/history-search.js";
25
25
  import { InputEditor, lineEnd, lineStart } from "../input/editor.js";
26
- import { applySettingsEvent, createSettings, hitTestSettings, selectSetting, tuiSettingItems, } from "../views/settings-list.js";
27
- import { applyEvent, blockLines, hitTestSubagentRoster, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, TRANSCRIPT_WHEEL_SCROLL, } from "../views/event-views.js";
28
- import { flushPending, MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseKeys } from "../input/keys.js";
29
- import { LineRenderer } from "../chrome/renderer.js";
30
- import { hitTestEditor } from "../chrome/box.js";
26
+ import { applySettingsEvent, createSettings, } from "../views/settings-list.js";
27
+ import { applyEvent, blockLines, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
28
+ import { flushPending, parseKeys } from "../input/keys.js";
29
+ import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
31
30
  import { createTheme, detectTrueColor, parseThemeName } from "../chrome/theme.js";
32
31
  import { TUI_SETTINGS_NAMESPACE, TuiSettingsSchema } from "../session/tui-settings.js";
33
32
  import { defaultStatusBarConfig, resolveStatusBarConfig } from "../chrome/status-config.js";
@@ -55,7 +54,19 @@ export const name = 'omdsh-tui';
55
54
  /**
56
55
  * Local terminal presentation service.
57
56
  */
57
+ function detectTerminalProfile() {
58
+ if (process.env.TMUX !== undefined || process.env.STY !== undefined || process.env.ZELLIJ !== undefined) {
59
+ return 'multiplexer';
60
+ }
61
+ return process.platform === 'win32' ? 'conpty' : 'direct';
62
+ }
58
63
  export class LocalTui {
64
+ #deferInitialRender = false;
65
+ #terminalProfile;
66
+ #resizeDebounceMs;
67
+ #resizeTimer = null;
68
+ #streamRenderMs;
69
+ #streamRenderTimer = null;
59
70
  #term;
60
71
  #model;
61
72
  #reasoningEffort;
@@ -93,7 +104,6 @@ export class LocalTui {
93
104
  #autocompleteTimer = null;
94
105
  #autocompleteAbort = null;
95
106
  #autocompleteRequestId = 0;
96
- #scrollRender = null;
97
107
  #paste = false;
98
108
  #pasteBuf = '';
99
109
  #pasteInFlight = 0;
@@ -131,10 +141,9 @@ export class LocalTui {
131
141
  #sessionControls;
132
142
  #loopStatus;
133
143
  #subagents;
144
+ #subagentLauncherFocused = false;
134
145
  #inspected;
135
- #editorHit = null;
136
- #chromeHit = null;
137
- #overlayHit = null;
146
+ #promptDocument;
138
147
  #trueColor;
139
148
  #copy;
140
149
  #readClipboard;
@@ -185,30 +194,45 @@ export class LocalTui {
185
194
  this.#searchSessions = paths.searchSessions;
186
195
  this.#autocompleteDebounceMs = Math.max(0, paths.autocompleteDebounceMs ?? 100);
187
196
  this.#welcomeTips = pickWelcomeTips();
197
+ this.#deferInitialRender = paths.deferInitialRender === true;
198
+ this.#terminalProfile = paths.terminalProfile ?? detectTerminalProfile();
199
+ this.#resizeDebounceMs = Math.max(0, paths.resizeDebounceMs ?? 120);
200
+ this.#streamRenderMs = Math.max(0, paths.streamRenderMs ?? 8);
188
201
  this.#trueColor = colors && detectTrueColor();
189
202
  this.#tty = term.input.isTTY === true;
190
203
  this.#pwd = shortenPath(project.root);
191
204
  this.#branch = project.gitLabel;
192
- this.#renderer = new LineRenderer({ write: (chunk) => { this.#term.output.write(chunk); } }, { synchronized: this.#tty });
205
+ this.#renderer = new MainScreenRenderer({ write: (chunk) => { this.#term.output.write(chunk); } }, {
206
+ width: this.#term.width(),
207
+ height: this.#term.height(),
208
+ synchronized: this.#tty,
209
+ clearScrollback: this.#terminalProfile === 'direct',
210
+ alternateScreenOverlays: paths.alternateScreenOverlays === true,
211
+ });
193
212
  if (this.#tty) {
213
+ this.#renderer.startEpoch();
194
214
  term.input.setRawMode?.(true);
195
215
  const listener = (chunk) => { this.#onData(chunk); };
196
216
  term.input.on('data', listener);
197
217
  this.#offData = () => { term.input.off('data', listener); };
198
218
  this.#offResize = term.onResize?.(() => {
199
- // A terminal resize invalidates both the physical cursor position and
200
- // the screen-relative frame retained by the differential renderer.
201
- this.#renderer.reset();
202
- this.#term.output.write('\x1b[2J\x1b[H');
203
- this.#render();
219
+ // A terminal resize changes the committed/live seam; re-anchor the
220
+ // live window so it stays anchored at the bottom.
221
+ const repaint = () => {
222
+ this.#resizeTimer = null;
223
+ this.#renderer.resize(this.#term.width(), this.#term.height());
224
+ this.#render();
225
+ };
226
+ if (this.#terminalProfile === 'multiplexer') {
227
+ if (this.#resizeTimer !== null)
228
+ clearTimeout(this.#resizeTimer);
229
+ this.#resizeTimer = setTimeout(repaint, this.#resizeDebounceMs);
230
+ }
231
+ else {
232
+ repaint();
233
+ }
204
234
  }) ?? null;
205
- // Boot output above us (package-manager warnings, loader logs) has
206
- // already scrolled the cursor off row 0; the renderer's screen-relative
207
- // frames require a clean origin. Clear the screen and home the cursor
208
- // before the first frame. Enable bracketed paste and SGR mouse
209
- // tracking (wheel drives the virtual transcript; native scrollback
210
- // is already unusable under full-screen diffs).
211
- term.output.write('\x1b[2J\x1b[H\x1b[?2004h' + MOUSE_TRACKING_ON);
235
+ term.output.write('\x1b[?2004h');
212
236
  }
213
237
  this.#render();
214
238
  }
@@ -216,7 +240,12 @@ export class LocalTui {
216
240
  this.#state = applyEvent(this.#state, event, presentation);
217
241
  this.#syncTick();
218
242
  if (this.#tty) {
219
- this.#render();
243
+ if (event.type === 'assistant/chunk' && this.#streamRenderMs > 0) {
244
+ this.#scheduleStreamRender();
245
+ }
246
+ else {
247
+ this.#render();
248
+ }
220
249
  }
221
250
  else if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result' || event.type === 'turn/end') {
222
251
  this.#printPlain();
@@ -246,6 +275,8 @@ export class LocalTui {
246
275
  this.#subagents = roster === undefined
247
276
  ? undefined
248
277
  : { agents: roster.agents.map(agent => ({ ...agent, activity: [...agent.activity] })) };
278
+ if ((this.#subagents?.agents.length ?? 0) === 0)
279
+ this.#subagentLauncherFocused = false;
249
280
  const inspected = this.#inspected;
250
281
  if (inspected !== undefined) {
251
282
  const current = this.#subagents?.agents.find(agent => agent.id === inspected.id);
@@ -265,6 +296,8 @@ export class LocalTui {
265
296
  }
266
297
  setInspectedSubagent(inspected) {
267
298
  this.#inspected = inspected === undefined ? undefined : { ...inspected };
299
+ if (inspected !== undefined)
300
+ this.#subagentLauncherFocused = false;
268
301
  this.#syncTick();
269
302
  if (this.#tty)
270
303
  this.#render();
@@ -352,11 +385,13 @@ export class LocalTui {
352
385
  }
353
386
  });
354
387
  }
355
- replaceSession(events, presentations) {
388
+ replaceSession(events, presentations, status = 'idle') {
356
389
  const state = replayEvents(events, presentations);
357
- this.#state = { ...state, status: 'idle', compactCommandId: undefined };
390
+ this.#state = { ...state, status, compactCommandId: undefined };
358
391
  this.#plainPrinted = 0;
359
392
  this.#followTail();
393
+ this.#deferInitialRender = false;
394
+ this.#renderer.startEpoch({ replay: status === 'idle' ? 'full' : 'pinned' });
360
395
  if (this.#tty)
361
396
  this.#render();
362
397
  else
@@ -495,6 +530,14 @@ export class LocalTui {
495
530
  if (this.#disposed)
496
531
  return;
497
532
  this.#disposed = true;
533
+ if (this.#resizeTimer !== null) {
534
+ clearTimeout(this.#resizeTimer);
535
+ this.#resizeTimer = null;
536
+ }
537
+ if (this.#streamRenderTimer !== null) {
538
+ clearTimeout(this.#streamRenderTimer);
539
+ this.#streamRenderTimer = null;
540
+ }
498
541
  if (this.#tick !== null) {
499
542
  clearInterval(this.#tick);
500
543
  this.#tick = null;
@@ -503,19 +546,15 @@ export class LocalTui {
503
546
  clearInterval(this.#loopTick);
504
547
  this.#loopTick = null;
505
548
  }
506
- if (this.#scrollRender !== null) {
507
- clearImmediate(this.#scrollRender);
508
- this.#scrollRender = null;
509
- }
510
549
  if (this.#tty) {
511
550
  this.#offData?.();
512
551
  this.#offResize?.();
513
552
  this.#term.input.setRawMode?.(false);
514
553
  // Leave the cursor on a fresh line below the last frame so the shell
515
- // prompt does not overwrite the transcript. Disable mouse tracking
516
- // and bracketed paste.
554
+ // prompt does not overwrite the transcript. Disable bracketed paste
555
+ // and restore the cursor.
517
556
  this.#renderer.finish();
518
- this.#term.output.write(MOUSE_TRACKING_OFF + '\x1b[?2004l\x1b[?25h\r\n');
557
+ this.#term.output.write('\x1b[?2004l\x1b[?25h\r\n');
519
558
  if (this.#resumeHintRequested && this.#sessionId !== undefined) {
520
559
  this.#term.output.write(`\r\nResume this session with ${APP_NAME} --resume ${this.#sessionId}\r\n`);
521
560
  }
@@ -635,10 +674,12 @@ export class LocalTui {
635
674
  }
636
675
  }
637
676
  #render() {
638
- if (this.#scrollRender !== null) {
639
- clearImmediate(this.#scrollRender);
640
- this.#scrollRender = null;
677
+ if (this.#streamRenderTimer !== null) {
678
+ clearTimeout(this.#streamRenderTimer);
679
+ this.#streamRenderTimer = null;
641
680
  }
681
+ if (this.#deferInitialRender)
682
+ return;
642
683
  const width = this.#term.width();
643
684
  const frame = this.#tty
644
685
  ? renderView(this.#state, {
@@ -671,6 +712,7 @@ export class LocalTui {
671
712
  ...(this.#sessionControls === undefined ? {} : { sessionControls: this.#sessionControls }),
672
713
  ...(this.#loopStatus === undefined ? {} : { loopStatus: this.#loopStatus }),
673
714
  ...(this.#subagents === undefined ? {} : { subagents: this.#subagents }),
715
+ subagentLauncherFocused: this.#subagentLauncherFocused,
674
716
  ...(this.#inspected === undefined ? {} : { inspected: this.#inspected }),
675
717
  statusBar: this.#statusBar,
676
718
  ...(this.#prompt === null ? {} : { promptSelector: this.#prompt }),
@@ -684,12 +726,19 @@ export class LocalTui {
684
726
  })
685
727
  : { lines: [] };
686
728
  this.#focusBlock = undefined;
687
- this.#editorHit = frame.editor ?? null;
688
- this.#overlayHit = frame.overlay ?? null;
689
- this.#chromeHit = frame.chrome ?? null;
729
+ this.#promptDocument = frame.promptDocument;
690
730
  this.#syncScroll(frame.transcript);
691
731
  this.#renderer.render(frame);
692
732
  }
733
+ #scheduleStreamRender() {
734
+ if (this.#streamRenderTimer !== null)
735
+ return;
736
+ this.#streamRenderTimer = setTimeout(() => {
737
+ this.#streamRenderTimer = null;
738
+ if (!this.#disposed)
739
+ this.#render();
740
+ }, this.#streamRenderMs);
741
+ }
693
742
  #syncScroll(scroll) {
694
743
  if (scroll === undefined) {
695
744
  this.#scrollStart = 0;
@@ -709,24 +758,14 @@ export class LocalTui {
709
758
  #pageSize() {
710
759
  return Math.max(1, this.#scrollBudget > 2 ? this.#scrollBudget - 2 : 1);
711
760
  }
712
- #scrollBy(delta, coalesce = false) {
761
+ #scrollBy(delta) {
713
762
  if (delta === 0 && this.#maxStart === 0)
714
763
  return;
715
764
  this.#follow = false;
716
765
  this.#scrollStart += delta;
717
766
  if (this.#scrollStart <= 0)
718
767
  this.#scrollStart = 0;
719
- if (!coalesce) {
720
- this.#render();
721
- return;
722
- }
723
- if (this.#scrollRender !== null)
724
- return;
725
- this.#scrollRender = setImmediate(() => {
726
- this.#scrollRender = null;
727
- if (!this.#disposed)
728
- this.#render();
729
- });
768
+ this.#render();
730
769
  }
731
770
  #followTail() {
732
771
  this.#follow = true;
@@ -945,10 +984,6 @@ export class LocalTui {
945
984
  }
946
985
  #dispatch(event) {
947
986
  if (this.#state.status === 'compacting') {
948
- if (event.type === 'mouse' && event.wheel !== null) {
949
- this.#scrollBy(event.wheel < 0 ? -TRANSCRIPT_WHEEL_SCROLL : TRANSCRIPT_WHEEL_SCROLL, true);
950
- return;
951
- }
952
987
  if (event.type === 'key' && event.id === 'pageUp') {
953
988
  this.#scrollBy(-this.#pageSize());
954
989
  return;
@@ -987,11 +1022,6 @@ export class LocalTui {
987
1022
  this.#pasteBuf = '';
988
1023
  return;
989
1024
  }
990
- if (event.type === 'mouse') {
991
- this.#lastEscapeTime = 0;
992
- this.#handleMouse(event);
993
- return;
994
- }
995
1025
  if (event.type !== 'key' || event.id !== 'escape')
996
1026
  this.#lastEscapeTime = 0;
997
1027
  if (this.#handlePrompt(event))
@@ -1074,6 +1104,8 @@ export class LocalTui {
1074
1104
  this.#lastEscapeTime = 0;
1075
1105
  return;
1076
1106
  }
1107
+ if (this.#handleSubagentLauncher(event))
1108
+ return;
1077
1109
  if (event.type === 'key' && event.id === 'escape') {
1078
1110
  if (this.#inspected !== undefined) {
1079
1111
  if (this.#inspected.writable === true && (this.#editor.text !== '' || this.#images.length > 0)) {
@@ -1267,7 +1299,7 @@ export class LocalTui {
1267
1299
  this.#render();
1268
1300
  return true;
1269
1301
  }
1270
- const scroll = this.#overlayHit?.kind === 'prompt' ? this.#overlayHit.document : undefined;
1302
+ const scroll = this.#promptDocument;
1271
1303
  let documentScroll;
1272
1304
  if (event.id === 'up')
1273
1305
  documentScroll = (scroll?.start ?? prompt.documentScroll ?? 0) - 1;
@@ -1317,73 +1349,42 @@ export class LocalTui {
1317
1349
  }
1318
1350
  return true;
1319
1351
  }
1320
- #handleMouse(event) {
1321
- if (event.leftClick) {
1322
- if (this.#clickChrome(event.row))
1323
- return;
1324
- if (this.#clickOverlay(event.row))
1325
- return;
1326
- this.#clickEditor(event.row, event.col);
1327
- return;
1328
- }
1329
- if (event.wheel === null)
1330
- return;
1331
- const dir = event.wheel;
1332
- if (this.#prompt !== null) {
1333
- this.#handlePrompt({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1334
- return;
1335
- }
1336
- if (this.#settings !== null) {
1337
- this.#applySettings({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1338
- return;
1339
- }
1340
- if (this.#copySelector !== null) {
1341
- this.#applyCopySelector({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1342
- return;
1343
- }
1344
- if (this.#search !== null) {
1345
- this.#applySearch({ type: 'key', id: dir < 0 ? 'up' : 'down' });
1346
- return;
1347
- }
1348
- if (this.#ac !== null) {
1349
- this.#moveAutocomplete(dir);
1350
- this.#render();
1351
- return;
1352
- }
1353
- this.#scrollBy(dir < 0 ? -TRANSCRIPT_WHEEL_SCROLL : TRANSCRIPT_WHEEL_SCROLL, true);
1352
+ #openInspect(id) {
1353
+ for (const listener of this.#inspects)
1354
+ listener(id);
1354
1355
  }
1355
- #clickChrome(row) {
1356
- const hit = this.#chromeHit;
1357
- if (hit === undefined || hit === null)
1356
+ #handleSubagentLauncher(event) {
1357
+ const hasAgents = (this.#subagents?.agents.length ?? 0) > 0;
1358
+ if (this.#subagentLauncherFocused) {
1359
+ if (!hasAgents) {
1360
+ this.#subagentLauncherFocused = false;
1361
+ return false;
1362
+ }
1363
+ if (event.type === 'key' && event.id === 'enter') {
1364
+ this.#subagentLauncherFocused = false;
1365
+ void this.#pickSubagent();
1366
+ return true;
1367
+ }
1368
+ if (event.type === 'key' && (event.id === 'escape' || event.id === 'up')) {
1369
+ this.#subagentLauncherFocused = false;
1370
+ this.#render();
1371
+ return true;
1372
+ }
1373
+ if (event.type === 'key' && event.id === 'down')
1374
+ return true;
1375
+ this.#subagentLauncherFocused = false;
1358
1376
  return false;
1359
- const inspect = hit.inspect;
1360
- if (inspect !== undefined && row >= inspect.start && row < inspect.start + inspect.rows) {
1361
- this.#closeInspect();
1362
- return true;
1363
1377
  }
1364
- const roster = hit.subagents;
1365
- if (roster === undefined)
1378
+ if (!hasAgents || event.type !== 'key' || event.id !== 'down')
1366
1379
  return false;
1367
- const localRow = row - roster.start;
1368
- if (localRow < 0 || localRow >= roster.rows)
1380
+ if (this.#editor.text !== '' || this.#images.length > 0 || this.#historyIndex !== 0 || this.#ac !== null)
1369
1381
  return false;
1370
- if (localRow < roster.headerRows) {
1371
- void this.#pickSubagent();
1372
- return true;
1373
- }
1374
- const id = hitTestSubagentRoster(roster.items, localRow);
1375
- if (id === undefined)
1376
- return true;
1377
- if (this.#inspected?.id === id)
1378
- this.#closeInspect();
1379
- else
1380
- this.#openInspect(id);
1382
+ if (this.#queuedSubmissions.length > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
1383
+ return false;
1384
+ this.#subagentLauncherFocused = true;
1385
+ this.#render();
1381
1386
  return true;
1382
1387
  }
1383
- #openInspect(id) {
1384
- for (const listener of this.#inspects)
1385
- listener(id);
1386
- }
1387
1388
  #closeInspect() {
1388
1389
  if (this.#inspected === undefined)
1389
1390
  return;
@@ -1391,6 +1392,7 @@ export class LocalTui {
1391
1392
  listener();
1392
1393
  }
1393
1394
  async #pickSubagent() {
1395
+ this.#subagentLauncherFocused = false;
1394
1396
  const agents = this.#subagents?.agents ?? [];
1395
1397
  if (agents.length === 0) {
1396
1398
  this.notice('No subagents are available in this session.');
@@ -1398,7 +1400,7 @@ export class LocalTui {
1398
1400
  }
1399
1401
  const initialValue = this.#inspected?.id ?? agents[0]?.id;
1400
1402
  const answer = await this.prompt({
1401
- title: 'Agents',
1403
+ title: 'Agent Hub',
1402
1404
  question: 'Open a subagent transcript',
1403
1405
  options: agents.map(agent => ({
1404
1406
  label: agent.label,
@@ -1407,93 +1409,14 @@ export class LocalTui {
1407
1409
  })),
1408
1410
  ...(initialValue === undefined ? {} : { initialValue }),
1409
1411
  allowCustom: false,
1410
- ...(agents.length > 8 ? { presentation: 'fullscreen-list', filterable: true } : {}),
1412
+ presentation: 'fullscreen-list',
1413
+ filterable: true,
1411
1414
  submitLabel: 'open',
1412
1415
  });
1413
1416
  if (answer === null || answer === '')
1414
1417
  return;
1415
1418
  this.#openInspect(answer);
1416
1419
  }
1417
- #clickOverlay(row) {
1418
- const hit = this.#overlayHit;
1419
- if (hit === null)
1420
- return false;
1421
- const localRow = row - hit.start;
1422
- if (hit.kind === 'settings' && this.#settings !== null) {
1423
- const index = hitTestSettings(hit.itemRows ?? tuiSettingItems(this.#settings.prefs).length, localRow);
1424
- if (index === undefined)
1425
- return true;
1426
- if (index === this.#settings.selected) {
1427
- this.#applySettings({ type: 'key', id: 'enter' });
1428
- }
1429
- else {
1430
- this.#settings = selectSetting(this.#settings, index);
1431
- this.#render();
1432
- }
1433
- return true;
1434
- }
1435
- if (hit.kind === 'copy' && this.#copySelector !== null) {
1436
- const index = hitTestCopySelector(this.#copySelector.items.length, this.#copySelector.selected, localRow);
1437
- if (index === undefined)
1438
- return true;
1439
- if (index === this.#copySelector.selected) {
1440
- this.#applyCopySelector({ type: 'key', id: 'enter' });
1441
- }
1442
- else {
1443
- this.#copySelector = selectCopyTarget(this.#copySelector, index);
1444
- this.#render();
1445
- }
1446
- return true;
1447
- }
1448
- if (hit.kind === 'autocomplete' && this.#ac !== null) {
1449
- const index = hitTestAutocomplete(this.#ac.items.length, this.#ac.selected, localRow);
1450
- if (index === undefined || this.#ac.items[index]?.kind === 'heading')
1451
- return true;
1452
- this.#ac = { ...this.#ac, selected: index };
1453
- this.#applySelectedCompletion();
1454
- this.#render();
1455
- return true;
1456
- }
1457
- if (hit.kind === 'search' && this.#search !== null) {
1458
- const index = hitTestHistorySearch(this.#search.results.length, this.#search.selected, localRow, hit.resultsRow ?? 0);
1459
- if (index === undefined)
1460
- return true;
1461
- this.#search = { ...this.#search, selected: index };
1462
- this.#applySearch({ type: 'key', id: 'enter' });
1463
- return true;
1464
- }
1465
- if (hit.kind === 'prompt' && this.#prompt !== null) {
1466
- const index = hit.itemRows?.[localRow];
1467
- if (index === undefined)
1468
- return true;
1469
- if (index === this.#prompt.selected) {
1470
- this.#finishPrompt(selectedFilteredPromptAnswer(this.#prompt, this.#editor.text));
1471
- this.#editor.setText('');
1472
- }
1473
- else {
1474
- this.#prompt = { ...this.#prompt, selected: index };
1475
- }
1476
- this.#render();
1477
- return true;
1478
- }
1479
- return false;
1480
- }
1481
- #clickEditor(row, col) {
1482
- if (this.#settings !== null || this.#copySelector !== null || this.#search !== null)
1483
- return;
1484
- const hit = this.#editorHit;
1485
- if (hit === null)
1486
- return;
1487
- const localRow = row - hit.start;
1488
- if (localRow < 0 || localRow >= hit.rows)
1489
- return;
1490
- const index = hitTestEditor(this.#editor.text, this.#term.width(), localRow, col);
1491
- if (index === undefined)
1492
- return;
1493
- this.#editor.setCursor(index);
1494
- this.#refreshAutocomplete();
1495
- this.#render();
1496
- }
1497
1420
  #prefs() {
1498
1421
  return {
1499
1422
  theme: this.#themeName,
@@ -1806,7 +1729,6 @@ export class LocalTui {
1806
1729
  }
1807
1730
  if (command.kind === 'resetDisplay') {
1808
1731
  this.#renderer.reset();
1809
- this.#term.output.write('\x1b[2J\x1b[H');
1810
1732
  this.#render();
1811
1733
  }
1812
1734
  }
@@ -1981,6 +1903,7 @@ export class LocalTui {
1981
1903
  if (command.name === 'clear') {
1982
1904
  this.#state = initialTranscript();
1983
1905
  this.#followTail();
1906
+ this.#renderer.startEpoch();
1984
1907
  this.#render();
1985
1908
  return;
1986
1909
  }
@@ -2155,7 +2078,6 @@ export class LocalTui {
2155
2078
  finally {
2156
2079
  this.#term.input.setRawMode?.(true);
2157
2080
  this.#renderer.reset();
2158
- this.#term.output.write('\x1b[2J\x1b[H');
2159
2081
  this.#refreshAutocomplete();
2160
2082
  this.#render();
2161
2083
  }
@@ -2168,6 +2090,7 @@ export class LocalTui {
2168
2090
  */
2169
2091
  export function apply(ctx, config) {
2170
2092
  const dshHome = process.env.OMDSH_HOME ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
2093
+ const terminalProfile = detectTerminalProfile();
2171
2094
  const term = {
2172
2095
  output: process.stdout,
2173
2096
  input: process.stdin,
@@ -2179,6 +2102,9 @@ export function apply(ctx, config) {
2179
2102
  },
2180
2103
  };
2181
2104
  const tui = new LocalTui(term, config.model, config.colors ?? term.output.isTTY === true, parseThemeName(config.theme), copyToClipboard, {
2105
+ deferInitialRender: true,
2106
+ terminalProfile,
2107
+ alternateScreenOverlays: terminalProfile === 'direct',
2182
2108
  historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
2183
2109
  keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),
2184
2110
  });