@agi-fans/dsh-tui 0.1.1 → 0.3.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 (43) hide show
  1. package/README.md +2 -2
  2. package/lib/autocomplete.js +0 -2
  3. package/lib/autocomplete.js.map +1 -1
  4. package/lib/{command-queue.d.ts → command-export.d.ts} +2 -2
  5. package/lib/command-export.js +39 -0
  6. package/lib/command-export.js.map +1 -0
  7. package/lib/{command-transcript.d.ts → command-permission.d.ts} +2 -2
  8. package/lib/{command-mode.js → command-permission.js} +28 -21
  9. package/lib/command-permission.js.map +1 -0
  10. package/lib/command-session.js +1 -1
  11. package/lib/command-session.js.map +1 -1
  12. package/lib/{command-mode.d.ts → command-steer.d.ts} +2 -2
  13. package/lib/command-steer.js +18 -0
  14. package/lib/command-steer.js.map +1 -0
  15. package/lib/definition.d.ts +14 -0
  16. package/lib/event-views.d.ts +32 -8
  17. package/lib/event-views.js +249 -45
  18. package/lib/event-views.js.map +1 -1
  19. package/lib/hotkeys.d.ts +2 -5
  20. package/lib/hotkeys.js +4 -14
  21. package/lib/hotkeys.js.map +1 -1
  22. package/lib/provider-local.d.ts +4 -1
  23. package/lib/provider-local.js +171 -43
  24. package/lib/provider-local.js.map +1 -1
  25. package/lib/renderer.d.ts +2 -0
  26. package/lib/renderer.js +48 -25
  27. package/lib/renderer.js.map +1 -1
  28. package/lib/runner.js +28 -0
  29. package/lib/runner.js.map +1 -1
  30. package/lib/session-controller.d.ts +27 -3
  31. package/lib/session-controller.js +206 -8
  32. package/lib/session-controller.js.map +1 -1
  33. package/lib/welcome-tips.js +2 -2
  34. package/lib/welcome-tips.js.map +1 -1
  35. package/package.json +13 -12
  36. package/lib/command-mode.js.map +0 -1
  37. package/lib/command-queue.js +0 -62
  38. package/lib/command-queue.js.map +0 -1
  39. package/lib/command-transcript.js +0 -85
  40. package/lib/command-transcript.js.map +0 -1
  41. package/lib/pwd.d.ts +0 -13
  42. package/lib/pwd.js +0 -14
  43. package/lib/pwd.js.map +0 -1
@@ -4,7 +4,7 @@
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
6
  * and mouse-wheel transcript scroll, click-to-caret, Ctrl-O tool
7
- * expand, bracketed paste, double Ctrl-C exit, Ctrl-D quit),
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
10
10
  * printing of settled blocks.
@@ -22,12 +22,11 @@ import { buildCopyTargets, extractCopyTarget, parseCopyKind } from "./copy-targe
22
22
  import { applyHistorySearchEvent, createHistorySearch, hitTestHistorySearch, } from "./history-search.js";
23
23
  import { InputEditor, lineEnd, lineStart } from "./editor.js";
24
24
  import { applySettingsEvent, createSettings, hitTestSettings, selectSetting, tuiSettingItems, } from "./settings-list.js";
25
- import { applyEvent, blockLines, initialTranscript, renderView, TRANSCRIPT_FAST_SCROLL, TRANSCRIPT_WHEEL_SCROLL, } from "./event-views.js";
25
+ import { applyEvent, blockLines, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, TRANSCRIPT_WHEEL_SCROLL, } from "./event-views.js";
26
26
  import { flushPending, MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseKeys } from "./keys.js";
27
27
  import { LineRenderer } from "./renderer.js";
28
28
  import { hitTestEditor } from "./box.js";
29
29
  import { createTheme, detectTrueColor, parseThemeName } from "./theme.js";
30
- import { formatWorkspaceText } from "./pwd.js";
31
30
  import { TUI_SETTINGS_NAMESPACE, TuiSettingsSchema } from "./tui-settings.js";
32
31
  import { defaultStatusBarConfig, resolveStatusBarConfig } from "./status-config.js";
33
32
  import { HistoryStore } from "./history-store.js";
@@ -37,10 +36,12 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
37
36
  import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, } from "./prompt-selector.js";
38
37
  import { resolveProjectContext } from "./project-context.js";
39
38
  import { pickWelcomeTips } from "./welcome-tips.js";
39
+ import { formatHotkeysText, hotkeyCount } from "./hotkeys.js";
40
40
  import { imageMarker, imagePathCandidates, probeImageDimensions, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "./image-paste.js";
41
41
  const APP_NAME = 'omdsh';
42
42
  const APP_VERSION = '0.1.0';
43
43
  const DOUBLE_CTRL_C_MS = 500;
44
+ const DOUBLE_ESCAPE_MS = 500;
44
45
  function shortenPath(cwd) {
45
46
  const home = homedir();
46
47
  if (cwd === home)
@@ -50,8 +51,6 @@ function shortenPath(cwd) {
50
51
  return cwd;
51
52
  }
52
53
  export const name = 'omdsh-tui';
53
- /** Harness commands replaced by terminal-native interaction surfaces. */
54
- const HIDDEN_RUNTIME_COMMANDS = new Set(['permission']);
55
54
  /**
56
55
  * Local terminal presentation service.
57
56
  */
@@ -73,11 +72,17 @@ export class LocalTui {
73
72
  #settings = null;
74
73
  #copySelector = null;
75
74
  #pending = null;
76
- #queue = [];
75
+ #queuedSubmissions = [];
76
+ /** Newer queue entries temporarily detached while Up browses backward. */
77
+ #queueEditNewer = null;
78
+ #queueEditPending = false;
77
79
  #quitRequested = false;
78
80
  #resumeHintRequested = false;
79
81
  #lastSigintTime = 0;
82
+ #lastEscapeTime = 0;
80
83
  #interrupts = new Set();
84
+ #queueEdits = new Set();
85
+ #rewinds = new Set();
81
86
  #disposed = false;
82
87
  #pendingKeys = '';
83
88
  #escapeTimer = null;
@@ -202,6 +207,8 @@ export class LocalTui {
202
207
  }
203
208
  }
204
209
  setStatus(status) {
210
+ if (this.#state.status === 'compacting')
211
+ return;
205
212
  this.#state = { ...this.#state, status };
206
213
  this.#syncTick();
207
214
  if (this.#tty)
@@ -282,10 +289,8 @@ export class LocalTui {
282
289
  });
283
290
  }
284
291
  replaceSession(events, presentations) {
285
- let state = initialTranscript();
286
- for (const event of events)
287
- state = applyEvent(state, event, presentations?.get(event.seq));
288
- this.#state = { ...state, status: 'idle' };
292
+ const state = replayEvents(events, presentations);
293
+ this.#state = { ...state, status: 'idle', compactCommandId: undefined };
289
294
  this.#plainPrinted = 0;
290
295
  this.#followTail();
291
296
  if (this.#tty)
@@ -337,9 +342,11 @@ export class LocalTui {
337
342
  return this.#readlinePlain();
338
343
  // Lines submitted while a turn was still running were queued instead of
339
344
  // dropped; serve the oldest before waiting for fresh input.
340
- const queued = this.#queue.shift();
341
- if (queued !== undefined)
345
+ const queued = this.#queuedSubmissions.shift();
346
+ if (queued !== undefined) {
347
+ this.#render();
342
348
  return Promise.resolve(queued);
349
+ }
343
350
  return new Promise((resolve) => {
344
351
  this.#pending = { resolve };
345
352
  });
@@ -363,10 +370,49 @@ export class LocalTui {
363
370
  if (this.#tty)
364
371
  this.#render();
365
372
  }
373
+ resolveQueueEdit(submission) {
374
+ if (!this.#queueEditPending)
375
+ return;
376
+ this.#queueEditPending = false;
377
+ if (submission === null) {
378
+ if (this.#editor.text === '' && this.#images.length === 0 && this.#queueEditNewer?.length === 0) {
379
+ this.#queueEditNewer = null;
380
+ }
381
+ return;
382
+ }
383
+ if (this.#queueEditNewer === null)
384
+ this.#queueEditNewer = [];
385
+ if (this.#editor.text !== '' || this.#images.length > 0) {
386
+ this.#queueEditNewer.unshift(this.#currentSubmission());
387
+ }
388
+ this.#replaceInput(submission);
389
+ }
390
+ #currentSubmission() {
391
+ return {
392
+ text: this.#editor.text,
393
+ images: this.#images.map(image => ({ ...image })),
394
+ };
395
+ }
396
+ #replaceInput(submission) {
397
+ this.#images = submission.images.map(image => ({ ...image }));
398
+ this.#editor.setText(submission.text);
399
+ this.#historyIndex = 0;
400
+ this.#refreshAutocomplete();
401
+ if (this.#tty)
402
+ this.#render();
403
+ }
366
404
  onInterrupt(listener) {
367
405
  this.#interrupts.add(listener);
368
406
  return () => { this.#interrupts.delete(listener); };
369
407
  }
408
+ onQueueEdit(listener) {
409
+ this.#queueEdits.add(listener);
410
+ return () => { this.#queueEdits.delete(listener); };
411
+ }
412
+ onRewind(listener) {
413
+ this.#rewinds.add(listener);
414
+ return () => { this.#rewinds.delete(listener); };
415
+ }
370
416
  dispose() {
371
417
  if (this.#disposed)
372
418
  return;
@@ -471,7 +517,7 @@ export class LocalTui {
471
517
  this.#term.output.write(out);
472
518
  }
473
519
  #busy() {
474
- if (this.#state.status === 'running')
520
+ if (this.#state.status !== 'idle')
475
521
  return true;
476
522
  return this.#state.blocks.some((block) => block.kind === 'tool' && block.status === 'running');
477
523
  }
@@ -506,6 +552,9 @@ export class LocalTui {
506
552
  input: this.#editor.text,
507
553
  inputCursor: this.#editor.cursor,
508
554
  inputImages: this.#images.length,
555
+ queuedSubmissions: this.#queueEditNewer === null
556
+ ? this.#queuedSubmissions
557
+ : [...this.#queuedSubmissions, ...this.#queueEditNewer],
509
558
  colors: this.#colors,
510
559
  pwd: this.#pwd,
511
560
  ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
@@ -761,6 +810,22 @@ export class LocalTui {
761
810
  this.#editor.setText(text, Math.min(this.#editor.cursor, text.length));
762
811
  }
763
812
  #dispatch(event) {
813
+ if (this.#state.status === 'compacting') {
814
+ if (event.type === 'mouse' && event.wheel !== null) {
815
+ this.#scrollBy(event.wheel < 0 ? -TRANSCRIPT_WHEEL_SCROLL : TRANSCRIPT_WHEEL_SCROLL, true);
816
+ return;
817
+ }
818
+ if (event.type === 'key' && event.id === 'pageUp') {
819
+ this.#scrollBy(-this.#pageSize());
820
+ return;
821
+ }
822
+ if (event.type === 'key' && event.id === 'pageDown') {
823
+ this.#scrollBy(this.#pageSize());
824
+ return;
825
+ }
826
+ if (event.type !== 'key' || event.id !== 'ctrl+c')
827
+ return;
828
+ }
764
829
  // Clipboard image inspection is asynchronous. Preserve the exact key
765
830
  // order so a fast Ctrl+V, Enter submits the finished image draft rather
766
831
  // than an empty prompt.
@@ -789,9 +854,12 @@ export class LocalTui {
789
854
  return;
790
855
  }
791
856
  if (event.type === 'mouse') {
857
+ this.#lastEscapeTime = 0;
792
858
  this.#handleMouse(event);
793
859
  return;
794
860
  }
861
+ if (event.type !== 'key' || event.id !== 'escape')
862
+ this.#lastEscapeTime = 0;
795
863
  if (this.#handlePrompt(event))
796
864
  return;
797
865
  if (event.type === 'key') {
@@ -830,7 +898,7 @@ export class LocalTui {
830
898
  return;
831
899
  }
832
900
  this.#lastSigintTime = now;
833
- if (this.#state.status === 'running') {
901
+ if (this.#state.status !== 'idle') {
834
902
  for (const listener of this.#interrupts)
835
903
  listener();
836
904
  }
@@ -863,8 +931,29 @@ export class LocalTui {
863
931
  this.#applySearch(event);
864
932
  return;
865
933
  }
866
- if (this.#handleAutocomplete(event))
934
+ if (this.#handleAutocomplete(event)) {
935
+ if (event.type === 'key' && event.id === 'escape')
936
+ this.#lastEscapeTime = 0;
867
937
  return;
938
+ }
939
+ if (event.type === 'key' && event.id === 'escape') {
940
+ if (this.#state.status !== 'idle' || this.#pending === null
941
+ || this.#editor.text !== '' || this.#images.length > 0) {
942
+ this.#lastEscapeTime = 0;
943
+ }
944
+ else {
945
+ const now = Date.now();
946
+ if (now - this.#lastEscapeTime < DOUBLE_ESCAPE_MS) {
947
+ this.#lastEscapeTime = 0;
948
+ for (const listener of this.#rewinds)
949
+ listener();
950
+ }
951
+ else {
952
+ this.#lastEscapeTime = now;
953
+ }
954
+ return;
955
+ }
956
+ }
868
957
  if (event.type === 'key' && (event.id === 'backspace' || event.id === 'delete')
869
958
  && this.#removeImageAtCursor(event.id))
870
959
  return;
@@ -1428,6 +1517,8 @@ export class LocalTui {
1428
1517
  return;
1429
1518
  }
1430
1519
  if (command.kind === 'historyPrev') {
1520
+ if (this.#restoreLatestQueuedSubmission())
1521
+ return;
1431
1522
  this.#historyPrev();
1432
1523
  return;
1433
1524
  }
@@ -1445,6 +1536,8 @@ export class LocalTui {
1445
1536
  if (command.kind === 'clear') {
1446
1537
  this.#editor.clear();
1447
1538
  this.#images = [];
1539
+ this.#queueEditNewer = null;
1540
+ this.#queueEditPending = false;
1448
1541
  this.#historyIndex = 0;
1449
1542
  this.#ac = null;
1450
1543
  this.#search = null;
@@ -1482,6 +1575,41 @@ export class LocalTui {
1482
1575
  this.#refreshAutocomplete();
1483
1576
  this.#render();
1484
1577
  }
1578
+ #restoreLatestQueuedSubmission() {
1579
+ if (this.#queueEditPending)
1580
+ return true;
1581
+ if (this.#queueEditNewer !== null) {
1582
+ const previous = this.#queuedSubmissions.pop();
1583
+ if (previous !== undefined) {
1584
+ this.#queueEditNewer.unshift(this.#currentSubmission());
1585
+ this.#replaceInput(previous);
1586
+ return true;
1587
+ }
1588
+ const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
1589
+ if (!hasDurableFollowup || this.#queueEdits.size === 0)
1590
+ return true;
1591
+ this.#queueEditPending = true;
1592
+ for (const listener of this.#queueEdits)
1593
+ listener();
1594
+ return true;
1595
+ }
1596
+ if (this.#editor.text !== '' || this.#images.length > 0 || this.#historyIndex !== 0)
1597
+ return false;
1598
+ const submission = this.#queuedSubmissions.pop();
1599
+ if (submission !== undefined) {
1600
+ this.#queueEditNewer = [];
1601
+ this.#replaceInput(submission);
1602
+ return true;
1603
+ }
1604
+ const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
1605
+ if (!hasDurableFollowup || this.#queueEdits.size === 0)
1606
+ return false;
1607
+ this.#queueEditNewer = [];
1608
+ this.#queueEditPending = true;
1609
+ for (const listener of this.#queueEdits)
1610
+ listener();
1611
+ return true;
1612
+ }
1485
1613
  #historyNext() {
1486
1614
  if (this.#images.length > 0)
1487
1615
  return;
@@ -1512,6 +1640,7 @@ export class LocalTui {
1512
1640
  }
1513
1641
  const images = this.#images.map(image => ({ ...image }));
1514
1642
  const submittedText = images.length > 0 ? text.trim() : text;
1643
+ const queueEditNewer = this.#queueEditNewer;
1515
1644
  const historyText = images.reduce((value, image, index) => value.replaceAll(imageMarker(index, image), ''), submittedText)
1516
1645
  .replace(/[ \t]{2,}/gu, ' ')
1517
1646
  .trim();
@@ -1521,6 +1650,8 @@ export class LocalTui {
1521
1650
  }
1522
1651
  this.#historyIndex = 0;
1523
1652
  this.#draft = '';
1653
+ this.#queueEditNewer = null;
1654
+ this.#queueEditPending = false;
1524
1655
  this.#editor.setText('');
1525
1656
  this.#images = [];
1526
1657
  this.#ac = null;
@@ -1530,18 +1661,26 @@ export class LocalTui {
1530
1661
  this.#followTail();
1531
1662
  const slash = images.length === 0 ? parseSlashInput(submittedText) : null;
1532
1663
  if (slash !== null) {
1664
+ if (queueEditNewer !== null)
1665
+ this.#queuedSubmissions.push(...queueEditNewer);
1533
1666
  this.#runSlash(slash.name, slash.args);
1534
1667
  return;
1535
1668
  }
1536
- this.#render();
1537
1669
  const pending = this.#pending;
1538
1670
  if (pending !== null) {
1539
1671
  this.#pending = null;
1540
1672
  pending.resolve({ text: submittedText, images });
1673
+ if (queueEditNewer !== null)
1674
+ this.#queuedSubmissions.push(...queueEditNewer);
1541
1675
  }
1542
- else if (submittedText !== '' || images.length > 0) {
1543
- this.#queue.push({ text: submittedText, images });
1676
+ else if (submittedText !== '' || images.length > 0 || queueEditNewer !== null) {
1677
+ if (submittedText !== '' || images.length > 0) {
1678
+ this.#queuedSubmissions.push({ text: submittedText, images });
1679
+ }
1680
+ if (queueEditNewer !== null)
1681
+ this.#queuedSubmissions.push(...queueEditNewer);
1544
1682
  }
1683
+ this.#render();
1545
1684
  }
1546
1685
  #runSlash(name, args = '') {
1547
1686
  if (name === '') {
@@ -1569,11 +1708,6 @@ export class LocalTui {
1569
1708
  this.#runSettings(args);
1570
1709
  return;
1571
1710
  }
1572
- if (command.name === 'hotkeys') {
1573
- this.#hotkeyCatalog();
1574
- this.#render();
1575
- return;
1576
- }
1577
1711
  if (command.name === 'copy') {
1578
1712
  void this.#runCopy(args);
1579
1713
  return;
@@ -1583,17 +1717,7 @@ export class LocalTui {
1583
1717
  this.#render();
1584
1718
  return;
1585
1719
  }
1586
- if (command.name === 'pwd') {
1587
- this.#notice(formatWorkspaceText({
1588
- cwd: process.cwd(),
1589
- model: this.#model,
1590
- ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
1591
- }));
1592
- this.#render();
1593
- return;
1594
- }
1595
1720
  if (!BUILTIN_SLASH_COMMANDS.some((entry) => entry.name === command.name)) {
1596
- this.#render();
1597
1721
  const raw = '/' + name + (args === '' ? '' : ' ' + args);
1598
1722
  const pending = this.#pending;
1599
1723
  if (pending !== null) {
@@ -1601,13 +1725,24 @@ export class LocalTui {
1601
1725
  pending.resolve({ text: raw, images: [] });
1602
1726
  }
1603
1727
  else {
1604
- this.#queue.push({ text: raw, images: [] });
1728
+ this.#queuedSubmissions.push({ text: raw, images: [] });
1605
1729
  }
1730
+ this.#render();
1606
1731
  return;
1607
1732
  }
1608
1733
  this.#state = {
1609
1734
  ...this.#state,
1610
- blocks: [...this.#state.blocks, { kind: 'commandOutput', command: 'help', text: formatHelpText(this.#commands()) }],
1735
+ blocks: [...this.#state.blocks, {
1736
+ kind: 'commandOutput',
1737
+ command: 'help',
1738
+ text: [
1739
+ formatHelpText(this.#commands()),
1740
+ '',
1741
+ `**Keyboard Shortcuts · ${hotkeyCount(this.#keybindings)} bindings**`,
1742
+ '',
1743
+ formatHotkeysText(this.#keybindings),
1744
+ ].join('\n'),
1745
+ }],
1611
1746
  };
1612
1747
  this.#focusLatestBlock();
1613
1748
  this.#render();
@@ -1615,7 +1750,7 @@ export class LocalTui {
1615
1750
  #commands() {
1616
1751
  const localNames = new Set(BUILTIN_SLASH_COMMANDS.flatMap((command) => [command.name, ...(command.aliases ?? [])]));
1617
1752
  const runtime = this.#runtimeCommands
1618
- .filter((command) => !localNames.has(command.name) && !HIDDEN_RUNTIME_COMMANDS.has(command.name))
1753
+ .filter((command) => !localNames.has(command.name))
1619
1754
  .map((command) => ({
1620
1755
  name: command.name,
1621
1756
  description: command.description,
@@ -1684,13 +1819,6 @@ export class LocalTui {
1684
1819
  };
1685
1820
  this.#focusLatestBlock();
1686
1821
  }
1687
- #hotkeyCatalog() {
1688
- this.#state = {
1689
- ...this.#state,
1690
- blocks: [...this.#state.blocks, { kind: 'hotkeyCatalog', bindings: this.#keybindings }],
1691
- };
1692
- this.#focusLatestBlock();
1693
- }
1694
1822
  #focusLatestBlock() {
1695
1823
  this.#follow = false;
1696
1824
  this.#focusBlock = Math.max(0, this.#state.blocks.length - 1);