@agi-fans/dsh-tui 0.1.0 → 0.2.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 (44) 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-auth.d.ts +20 -0
  5. package/lib/command-auth.js +225 -0
  6. package/lib/command-auth.js.map +1 -0
  7. package/lib/{command-queue.d.ts → command-export.d.ts} +2 -2
  8. package/lib/command-export.js +39 -0
  9. package/lib/command-export.js.map +1 -0
  10. package/lib/command-model.js +23 -12
  11. package/lib/command-model.js.map +1 -1
  12. package/lib/{command-transcript.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 +25 -1
  16. package/lib/event-views.d.ts +23 -8
  17. package/lib/event-views.js +207 -47
  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/prompt-selector.d.ts +2 -0
  23. package/lib/prompt-selector.js +35 -13
  24. package/lib/prompt-selector.js.map +1 -1
  25. package/lib/provider-local.d.ts +6 -3
  26. package/lib/provider-local.js +180 -44
  27. package/lib/provider-local.js.map +1 -1
  28. package/lib/renderer.js +11 -1
  29. package/lib/renderer.js.map +1 -1
  30. package/lib/runner.js +32 -4
  31. package/lib/runner.js.map +1 -1
  32. package/lib/session-controller.d.ts +27 -3
  33. package/lib/session-controller.js +200 -7
  34. package/lib/session-controller.js.map +1 -1
  35. package/lib/welcome-tips.js +2 -1
  36. package/lib/welcome-tips.js.map +1 -1
  37. package/package.json +12 -7
  38. package/lib/command-queue.js +0 -62
  39. package/lib/command-queue.js.map +0 -1
  40. package/lib/command-transcript.js +0 -85
  41. package/lib/command-transcript.js.map +0 -1
  42. package/lib/pwd.d.ts +0 -13
  43. package/lib/pwd.js +0 -14
  44. 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)
@@ -73,11 +74,17 @@ export class LocalTui {
73
74
  #settings = null;
74
75
  #copySelector = null;
75
76
  #pending = null;
76
- #queue = [];
77
+ #queuedSubmissions = [];
78
+ /** Newer queue entries temporarily detached while Up browses backward. */
79
+ #queueEditNewer = null;
80
+ #queueEditPending = false;
77
81
  #quitRequested = false;
78
82
  #resumeHintRequested = false;
79
83
  #lastSigintTime = 0;
84
+ #lastEscapeTime = 0;
80
85
  #interrupts = new Set();
86
+ #queueEdits = new Set();
87
+ #rewinds = new Set();
81
88
  #disposed = false;
82
89
  #pendingKeys = '';
83
90
  #escapeTimer = null;
@@ -202,6 +209,8 @@ export class LocalTui {
202
209
  }
203
210
  }
204
211
  setStatus(status) {
212
+ if (this.#state.status === 'compacting')
213
+ return;
205
214
  this.#state = { ...this.#state, status };
206
215
  this.#syncTick();
207
216
  if (this.#tty)
@@ -226,8 +235,14 @@ export class LocalTui {
226
235
  if (this.#tty)
227
236
  this.#render();
228
237
  }
229
- notice(text, level = 'info') {
230
- this.#state = { ...this.#state, blocks: [...this.#state.blocks, { kind: 'notice', level, text }] };
238
+ notice(text, options = {}) {
239
+ const block = {
240
+ kind: 'notice',
241
+ level: options.level ?? 'info',
242
+ text,
243
+ ...(options.framed === true ? { framed: true } : {}),
244
+ };
245
+ this.#state = { ...this.#state, blocks: [...this.#state.blocks, block] };
231
246
  if (this.#tty)
232
247
  this.#render();
233
248
  else
@@ -276,10 +291,8 @@ export class LocalTui {
276
291
  });
277
292
  }
278
293
  replaceSession(events, presentations) {
279
- let state = initialTranscript();
280
- for (const event of events)
281
- state = applyEvent(state, event, presentations?.get(event.seq));
282
- this.#state = { ...state, status: 'idle' };
294
+ const state = replayEvents(events, presentations);
295
+ this.#state = { ...state, status: 'idle', compactCommandId: undefined };
283
296
  this.#plainPrinted = 0;
284
297
  this.#followTail();
285
298
  if (this.#tty)
@@ -331,9 +344,11 @@ export class LocalTui {
331
344
  return this.#readlinePlain();
332
345
  // Lines submitted while a turn was still running were queued instead of
333
346
  // dropped; serve the oldest before waiting for fresh input.
334
- const queued = this.#queue.shift();
335
- if (queued !== undefined)
347
+ const queued = this.#queuedSubmissions.shift();
348
+ if (queued !== undefined) {
349
+ this.#render();
336
350
  return Promise.resolve(queued);
351
+ }
337
352
  return new Promise((resolve) => {
338
353
  this.#pending = { resolve };
339
354
  });
@@ -357,10 +372,49 @@ export class LocalTui {
357
372
  if (this.#tty)
358
373
  this.#render();
359
374
  }
375
+ resolveQueueEdit(submission) {
376
+ if (!this.#queueEditPending)
377
+ return;
378
+ this.#queueEditPending = false;
379
+ if (submission === null) {
380
+ if (this.#editor.text === '' && this.#images.length === 0 && this.#queueEditNewer?.length === 0) {
381
+ this.#queueEditNewer = null;
382
+ }
383
+ return;
384
+ }
385
+ if (this.#queueEditNewer === null)
386
+ this.#queueEditNewer = [];
387
+ if (this.#editor.text !== '' || this.#images.length > 0) {
388
+ this.#queueEditNewer.unshift(this.#currentSubmission());
389
+ }
390
+ this.#replaceInput(submission);
391
+ }
392
+ #currentSubmission() {
393
+ return {
394
+ text: this.#editor.text,
395
+ images: this.#images.map(image => ({ ...image })),
396
+ };
397
+ }
398
+ #replaceInput(submission) {
399
+ this.#images = submission.images.map(image => ({ ...image }));
400
+ this.#editor.setText(submission.text);
401
+ this.#historyIndex = 0;
402
+ this.#refreshAutocomplete();
403
+ if (this.#tty)
404
+ this.#render();
405
+ }
360
406
  onInterrupt(listener) {
361
407
  this.#interrupts.add(listener);
362
408
  return () => { this.#interrupts.delete(listener); };
363
409
  }
410
+ onQueueEdit(listener) {
411
+ this.#queueEdits.add(listener);
412
+ return () => { this.#queueEdits.delete(listener); };
413
+ }
414
+ onRewind(listener) {
415
+ this.#rewinds.add(listener);
416
+ return () => { this.#rewinds.delete(listener); };
417
+ }
364
418
  dispose() {
365
419
  if (this.#disposed)
366
420
  return;
@@ -465,7 +519,7 @@ export class LocalTui {
465
519
  this.#term.output.write(out);
466
520
  }
467
521
  #busy() {
468
- if (this.#state.status === 'running')
522
+ if (this.#state.status !== 'idle')
469
523
  return true;
470
524
  return this.#state.blocks.some((block) => block.kind === 'tool' && block.status === 'running');
471
525
  }
@@ -500,6 +554,9 @@ export class LocalTui {
500
554
  input: this.#editor.text,
501
555
  inputCursor: this.#editor.cursor,
502
556
  inputImages: this.#images.length,
557
+ queuedSubmissions: this.#queueEditNewer === null
558
+ ? this.#queuedSubmissions
559
+ : [...this.#queuedSubmissions, ...this.#queueEditNewer],
503
560
  colors: this.#colors,
504
561
  pwd: this.#pwd,
505
562
  ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
@@ -600,7 +657,7 @@ export class LocalTui {
600
657
  #startAsyncPaste(operation) {
601
658
  this.#pasteInFlight += 1;
602
659
  void operation.catch((error) => {
603
- this.notice('Paste failed: ' + (error instanceof Error ? error.message : String(error)), 'error');
660
+ this.notice('Paste failed: ' + (error instanceof Error ? error.message : String(error)), { level: 'error' });
604
661
  }).finally(() => {
605
662
  this.#pasteInFlight = Math.max(0, this.#pasteInFlight - 1);
606
663
  if (this.#pasteInFlight !== 0 || this.#deferredPasteEvents.length === 0)
@@ -755,6 +812,22 @@ export class LocalTui {
755
812
  this.#editor.setText(text, Math.min(this.#editor.cursor, text.length));
756
813
  }
757
814
  #dispatch(event) {
815
+ if (this.#state.status === 'compacting') {
816
+ if (event.type === 'mouse' && event.wheel !== null) {
817
+ this.#scrollBy(event.wheel < 0 ? -TRANSCRIPT_WHEEL_SCROLL : TRANSCRIPT_WHEEL_SCROLL, true);
818
+ return;
819
+ }
820
+ if (event.type === 'key' && event.id === 'pageUp') {
821
+ this.#scrollBy(-this.#pageSize());
822
+ return;
823
+ }
824
+ if (event.type === 'key' && event.id === 'pageDown') {
825
+ this.#scrollBy(this.#pageSize());
826
+ return;
827
+ }
828
+ if (event.type !== 'key' || event.id !== 'ctrl+c')
829
+ return;
830
+ }
758
831
  // Clipboard image inspection is asynchronous. Preserve the exact key
759
832
  // order so a fast Ctrl+V, Enter submits the finished image draft rather
760
833
  // than an empty prompt.
@@ -783,9 +856,12 @@ export class LocalTui {
783
856
  return;
784
857
  }
785
858
  if (event.type === 'mouse') {
859
+ this.#lastEscapeTime = 0;
786
860
  this.#handleMouse(event);
787
861
  return;
788
862
  }
863
+ if (event.type !== 'key' || event.id !== 'escape')
864
+ this.#lastEscapeTime = 0;
789
865
  if (this.#handlePrompt(event))
790
866
  return;
791
867
  if (event.type === 'key') {
@@ -824,7 +900,7 @@ export class LocalTui {
824
900
  return;
825
901
  }
826
902
  this.#lastSigintTime = now;
827
- if (this.#state.status === 'running') {
903
+ if (this.#state.status !== 'idle') {
828
904
  for (const listener of this.#interrupts)
829
905
  listener();
830
906
  }
@@ -857,8 +933,29 @@ export class LocalTui {
857
933
  this.#applySearch(event);
858
934
  return;
859
935
  }
860
- if (this.#handleAutocomplete(event))
936
+ if (this.#handleAutocomplete(event)) {
937
+ if (event.type === 'key' && event.id === 'escape')
938
+ this.#lastEscapeTime = 0;
861
939
  return;
940
+ }
941
+ if (event.type === 'key' && event.id === 'escape') {
942
+ if (this.#state.status !== 'idle' || this.#pending === null
943
+ || this.#editor.text !== '' || this.#images.length > 0) {
944
+ this.#lastEscapeTime = 0;
945
+ }
946
+ else {
947
+ const now = Date.now();
948
+ if (now - this.#lastEscapeTime < DOUBLE_ESCAPE_MS) {
949
+ this.#lastEscapeTime = 0;
950
+ for (const listener of this.#rewinds)
951
+ listener();
952
+ }
953
+ else {
954
+ this.#lastEscapeTime = now;
955
+ }
956
+ return;
957
+ }
958
+ }
862
959
  if (event.type === 'key' && (event.id === 'backspace' || event.id === 'delete')
863
960
  && this.#removeImageAtCursor(event.id))
864
961
  return;
@@ -1422,6 +1519,8 @@ export class LocalTui {
1422
1519
  return;
1423
1520
  }
1424
1521
  if (command.kind === 'historyPrev') {
1522
+ if (this.#restoreLatestQueuedSubmission())
1523
+ return;
1425
1524
  this.#historyPrev();
1426
1525
  return;
1427
1526
  }
@@ -1439,6 +1538,8 @@ export class LocalTui {
1439
1538
  if (command.kind === 'clear') {
1440
1539
  this.#editor.clear();
1441
1540
  this.#images = [];
1541
+ this.#queueEditNewer = null;
1542
+ this.#queueEditPending = false;
1442
1543
  this.#historyIndex = 0;
1443
1544
  this.#ac = null;
1444
1545
  this.#search = null;
@@ -1476,6 +1577,41 @@ export class LocalTui {
1476
1577
  this.#refreshAutocomplete();
1477
1578
  this.#render();
1478
1579
  }
1580
+ #restoreLatestQueuedSubmission() {
1581
+ if (this.#queueEditPending)
1582
+ return true;
1583
+ if (this.#queueEditNewer !== null) {
1584
+ const previous = this.#queuedSubmissions.pop();
1585
+ if (previous !== undefined) {
1586
+ this.#queueEditNewer.unshift(this.#currentSubmission());
1587
+ this.#replaceInput(previous);
1588
+ return true;
1589
+ }
1590
+ const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
1591
+ if (!hasDurableFollowup || this.#queueEdits.size === 0)
1592
+ return true;
1593
+ this.#queueEditPending = true;
1594
+ for (const listener of this.#queueEdits)
1595
+ listener();
1596
+ return true;
1597
+ }
1598
+ if (this.#editor.text !== '' || this.#images.length > 0 || this.#historyIndex !== 0)
1599
+ return false;
1600
+ const submission = this.#queuedSubmissions.pop();
1601
+ if (submission !== undefined) {
1602
+ this.#queueEditNewer = [];
1603
+ this.#replaceInput(submission);
1604
+ return true;
1605
+ }
1606
+ const hasDurableFollowup = this.#state.nextTurnInbox.some(message => message.source.kind === 'user');
1607
+ if (!hasDurableFollowup || this.#queueEdits.size === 0)
1608
+ return false;
1609
+ this.#queueEditNewer = [];
1610
+ this.#queueEditPending = true;
1611
+ for (const listener of this.#queueEdits)
1612
+ listener();
1613
+ return true;
1614
+ }
1479
1615
  #historyNext() {
1480
1616
  if (this.#images.length > 0)
1481
1617
  return;
@@ -1506,6 +1642,7 @@ export class LocalTui {
1506
1642
  }
1507
1643
  const images = this.#images.map(image => ({ ...image }));
1508
1644
  const submittedText = images.length > 0 ? text.trim() : text;
1645
+ const queueEditNewer = this.#queueEditNewer;
1509
1646
  const historyText = images.reduce((value, image, index) => value.replaceAll(imageMarker(index, image), ''), submittedText)
1510
1647
  .replace(/[ \t]{2,}/gu, ' ')
1511
1648
  .trim();
@@ -1515,6 +1652,8 @@ export class LocalTui {
1515
1652
  }
1516
1653
  this.#historyIndex = 0;
1517
1654
  this.#draft = '';
1655
+ this.#queueEditNewer = null;
1656
+ this.#queueEditPending = false;
1518
1657
  this.#editor.setText('');
1519
1658
  this.#images = [];
1520
1659
  this.#ac = null;
@@ -1524,18 +1663,26 @@ export class LocalTui {
1524
1663
  this.#followTail();
1525
1664
  const slash = images.length === 0 ? parseSlashInput(submittedText) : null;
1526
1665
  if (slash !== null) {
1666
+ if (queueEditNewer !== null)
1667
+ this.#queuedSubmissions.push(...queueEditNewer);
1527
1668
  this.#runSlash(slash.name, slash.args);
1528
1669
  return;
1529
1670
  }
1530
- this.#render();
1531
1671
  const pending = this.#pending;
1532
1672
  if (pending !== null) {
1533
1673
  this.#pending = null;
1534
1674
  pending.resolve({ text: submittedText, images });
1675
+ if (queueEditNewer !== null)
1676
+ this.#queuedSubmissions.push(...queueEditNewer);
1535
1677
  }
1536
- else if (submittedText !== '' || images.length > 0) {
1537
- this.#queue.push({ text: submittedText, images });
1678
+ else if (submittedText !== '' || images.length > 0 || queueEditNewer !== null) {
1679
+ if (submittedText !== '' || images.length > 0) {
1680
+ this.#queuedSubmissions.push({ text: submittedText, images });
1681
+ }
1682
+ if (queueEditNewer !== null)
1683
+ this.#queuedSubmissions.push(...queueEditNewer);
1538
1684
  }
1685
+ this.#render();
1539
1686
  }
1540
1687
  #runSlash(name, args = '') {
1541
1688
  if (name === '') {
@@ -1563,11 +1710,6 @@ export class LocalTui {
1563
1710
  this.#runSettings(args);
1564
1711
  return;
1565
1712
  }
1566
- if (command.name === 'hotkeys') {
1567
- this.#hotkeyCatalog();
1568
- this.#render();
1569
- return;
1570
- }
1571
1713
  if (command.name === 'copy') {
1572
1714
  void this.#runCopy(args);
1573
1715
  return;
@@ -1577,17 +1719,7 @@ export class LocalTui {
1577
1719
  this.#render();
1578
1720
  return;
1579
1721
  }
1580
- if (command.name === 'pwd') {
1581
- this.#notice(formatWorkspaceText({
1582
- cwd: process.cwd(),
1583
- model: this.#model,
1584
- ...(this.#branch !== undefined ? { branch: this.#branch } : {}),
1585
- }));
1586
- this.#render();
1587
- return;
1588
- }
1589
1722
  if (!BUILTIN_SLASH_COMMANDS.some((entry) => entry.name === command.name)) {
1590
- this.#render();
1591
1723
  const raw = '/' + name + (args === '' ? '' : ' ' + args);
1592
1724
  const pending = this.#pending;
1593
1725
  if (pending !== null) {
@@ -1595,13 +1727,24 @@ export class LocalTui {
1595
1727
  pending.resolve({ text: raw, images: [] });
1596
1728
  }
1597
1729
  else {
1598
- this.#queue.push({ text: raw, images: [] });
1730
+ this.#queuedSubmissions.push({ text: raw, images: [] });
1599
1731
  }
1732
+ this.#render();
1600
1733
  return;
1601
1734
  }
1602
1735
  this.#state = {
1603
1736
  ...this.#state,
1604
- blocks: [...this.#state.blocks, { kind: 'commandOutput', command: 'help', text: formatHelpText(this.#commands()) }],
1737
+ blocks: [...this.#state.blocks, {
1738
+ kind: 'commandOutput',
1739
+ command: 'help',
1740
+ text: [
1741
+ formatHelpText(this.#commands()),
1742
+ '',
1743
+ `**Keyboard Shortcuts · ${hotkeyCount(this.#keybindings)} bindings**`,
1744
+ '',
1745
+ formatHotkeysText(this.#keybindings),
1746
+ ].join('\n'),
1747
+ }],
1605
1748
  };
1606
1749
  this.#focusLatestBlock();
1607
1750
  this.#render();
@@ -1678,13 +1821,6 @@ export class LocalTui {
1678
1821
  };
1679
1822
  this.#focusLatestBlock();
1680
1823
  }
1681
- #hotkeyCatalog() {
1682
- this.#state = {
1683
- ...this.#state,
1684
- blocks: [...this.#state.blocks, { kind: 'hotkeyCatalog', bindings: this.#keybindings }],
1685
- };
1686
- this.#focusLatestBlock();
1687
- }
1688
1824
  #focusLatestBlock() {
1689
1825
  this.#follow = false;
1690
1826
  this.#focusBlock = Math.max(0, this.#state.blocks.length - 1);
@@ -1716,7 +1852,7 @@ export class LocalTui {
1716
1852
  this.#reconcileImageDrafts();
1717
1853
  }
1718
1854
  catch (error) {
1719
- this.notice(error instanceof Error ? error.message : String(error), 'error');
1855
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1720
1856
  }
1721
1857
  finally {
1722
1858
  this.#term.input.setRawMode?.(true);