@agi-fans/dsh-tui 0.12.0 → 0.13.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.
@@ -10,7 +10,7 @@
10
10
  * printing of settled blocks.
11
11
  * @module @agi-fans/dsh-tui
12
12
  */
13
- import { TerminalNotificationController, terminalNotificationSequence } from "./terminal-notifications.js";
13
+ import { TerminalNotificationController, terminalNotificationSequence, terminalProgressSequence, } from "./terminal-notifications.js";
14
14
  import { homedir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
  import { createInterface } from 'node:readline';
@@ -28,6 +28,7 @@ import { applySettingsEvent, createSettings, } from "../views/settings-list.js";
28
28
  import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, } from "../views/trajectory.js";
29
29
  import { applyAgentHubEvent, createAgentHub, updateAgentHub, } from "../views/agent-hub.js";
30
30
  import { applyEvent, blockLines, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
31
+ import { nextRevealStep, revealStreamingAssistant, streamingAssistantKey, streamingAssistantUnits, } from "../views/streaming-reveal.js";
31
32
  import { flushPending, parseKeys } from "../input/keys.js";
32
33
  import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
33
34
  import { createTheme, detectTrueColor, parseThemeName } from "../chrome/theme.js";
@@ -45,6 +46,8 @@ import { imageMarker, imagePathCandidates, probeImageDimensions, stripComposerIm
45
46
  import { APP_NAME, APP_VERSION } from "../session/package-metadata.js";
46
47
  const DOUBLE_CTRL_C_MS = 500;
47
48
  const DOUBLE_ESCAPE_MS = 500;
49
+ const STREAMING_REVEAL_MS = 1000 / 30;
50
+ const TERMINAL_PROGRESS_KEEPALIVE_MS = 1_000;
48
51
  function shortenPath(cwd) {
49
52
  const home = homedir();
50
53
  if (cwd === home)
@@ -97,6 +100,7 @@ export class LocalTui {
97
100
  #resumeHintRequested = false;
98
101
  #lastSigintTime = 0;
99
102
  #lastEscapeTime = 0;
103
+ #inspectEscapeFenceUntil = 0;
100
104
  #interrupts = new Set();
101
105
  #queueEdits = new Set();
102
106
  #rewinds = new Set();
@@ -124,6 +128,8 @@ export class LocalTui {
124
128
  #branch;
125
129
  #spinner = 0;
126
130
  #tick = null;
131
+ #revealTick = null;
132
+ #reveal;
127
133
  #loopTick = null;
128
134
  #scrollStart = 0;
129
135
  #maxStart = 0;
@@ -131,6 +137,10 @@ export class LocalTui {
131
137
  #follow = true;
132
138
  #focusBlock;
133
139
  #expandTools = false;
140
+ #motion = 'full';
141
+ #terminalProgress = false;
142
+ #terminalProgressActive = false;
143
+ #terminalProgressTick = null;
134
144
  #checkUpdates = true;
135
145
  #startupChangelog = 'summary';
136
146
  #notifications = new TerminalNotificationController();
@@ -170,6 +180,10 @@ export class LocalTui {
170
180
  #validateImageDraft;
171
181
  #autocompleteDebounceMs;
172
182
  #persistPrefs = null;
183
+ #agentBehavior;
184
+ #updateAgentBehavior;
185
+ #offAgentBehaviorWatch;
186
+ #agentBehaviorPending = false;
173
187
  /**
174
188
  * @param term - terminal surface (injectable for tests).
175
189
  * @param model - model label for the status line.
@@ -246,12 +260,20 @@ export class LocalTui {
246
260
  }
247
261
  event(event, presentation) {
248
262
  this.#state = applyEvent(this.#state, event, presentation);
249
- this.#emitNotification(this.#notifications.event({ type: event.type, ...('reason' in event && typeof event.reason === 'string' ? { reason: event.reason } : {}) }));
263
+ this.#emitNotification(this.#notifications.event({
264
+ type: event.type,
265
+ time: event.time,
266
+ ...(event.type === 'turn/end' ? { reason: event.data.reason.kind } : {}),
267
+ }));
250
268
  if (this.#trajectory !== null)
251
269
  this.#trajectory = appendTrajectoryEvent(this.#trajectory, event);
270
+ const smoothStream = this.#syncStreamingReveal(event);
252
271
  this.#syncTick();
253
272
  if (this.#tty) {
254
- if (event.type === 'assistant/chunk' && this.#streamRenderMs > 0) {
273
+ if (smoothStream) {
274
+ // The 30 fps reveal clock owns presentation for textual deltas.
275
+ }
276
+ else if (event.type === 'assistant/chunk' && this.#streamRenderMs > 0) {
255
277
  this.#scheduleStreamRender();
256
278
  }
257
279
  else {
@@ -313,8 +335,10 @@ export class LocalTui {
313
335
  }
314
336
  setInspectedSubagent(inspected) {
315
337
  this.#inspected = inspected === undefined ? undefined : { ...inspected };
316
- if (inspected !== undefined)
338
+ if (inspected !== undefined) {
339
+ this.#inspectEscapeFenceUntil = 0;
317
340
  this.#subagentLauncherFocused = false;
341
+ }
318
342
  this.#syncTick();
319
343
  if (this.#tty)
320
344
  this.#render();
@@ -441,6 +465,8 @@ export class LocalTui {
441
465
  };
442
466
  if (!this.#tty)
443
467
  return;
468
+ if (this.#reveal !== undefined && this.#motion !== 'off')
469
+ return;
444
470
  if (this.#streamRenderMs > 0 && this.#busy())
445
471
  this.#scheduleStreamRender();
446
472
  else
@@ -448,8 +474,11 @@ export class LocalTui {
448
474
  }
449
475
  /** Apply prefs loaded from the settings document (does not persist). */
450
476
  applyStoredPrefs(prefs) {
477
+ const previousMotion = this.#motion;
451
478
  this.#themeName = prefs.theme;
452
479
  this.#colors = prefs.colors;
480
+ this.#motion = prefs.motion ?? 'full';
481
+ this.#terminalProgress = prefs.terminalProgress ?? false;
453
482
  this.#expandTools = prefs.expandTools;
454
483
  this.#checkUpdates = prefs.checkUpdates ?? true;
455
484
  this.#startupChangelog = prefs.startupChangelog ?? 'summary';
@@ -458,6 +487,11 @@ export class LocalTui {
458
487
  this.#configureNotifications();
459
488
  this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
460
489
  this.#toolsExpanded = prefs.expandTools;
490
+ if (previousMotion !== this.#motion) {
491
+ this.#stopRevealTick();
492
+ this.#reveal = undefined;
493
+ }
494
+ this.#syncTick();
461
495
  if (this.#settings !== null)
462
496
  this.#settings = { ...this.#settings, prefs };
463
497
  if (this.#tty)
@@ -467,6 +501,29 @@ export class LocalTui {
467
501
  setPrefsPersist(persist) {
468
502
  this.#persistPrefs = persist;
469
503
  }
504
+ bindAgentBehaviorSettings(binding) {
505
+ if (this.#updateAgentBehavior !== undefined) {
506
+ throw new Error('omdsh-tui: Agent behavior settings are already bound');
507
+ }
508
+ let active = true;
509
+ this.#updateAgentBehavior = next => binding.update(next);
510
+ this.#applyStoredAgentBehavior(binding.get());
511
+ this.#offAgentBehaviorWatch = binding.watch(next => { this.#applyStoredAgentBehavior(next); });
512
+ return () => {
513
+ if (!active)
514
+ return;
515
+ active = false;
516
+ this.#offAgentBehaviorWatch?.();
517
+ this.#offAgentBehaviorWatch = undefined;
518
+ this.#updateAgentBehavior = undefined;
519
+ this.#agentBehavior = undefined;
520
+ this.#agentBehaviorPending = false;
521
+ if (this.#settings !== null)
522
+ this.#settings = createSettings(this.#prefs());
523
+ if (this.#tty && !this.#disposed)
524
+ this.#render();
525
+ };
526
+ }
470
527
  readInput() {
471
528
  if (this.#pending !== null)
472
529
  return Promise.reject(new Error('omdsh-tui: input read already in flight'));
@@ -581,6 +638,8 @@ export class LocalTui {
581
638
  clearInterval(this.#tick);
582
639
  this.#tick = null;
583
640
  }
641
+ this.#stopRevealTick();
642
+ this.#setTerminalProgress(false);
584
643
  if (this.#loopTick !== null) {
585
644
  clearInterval(this.#loopTick);
586
645
  this.#loopTick = null;
@@ -609,6 +668,8 @@ export class LocalTui {
609
668
  this.#autocompleteTimer = null;
610
669
  this.#autocompleteAbort = null;
611
670
  this.#lineReader?.close();
671
+ this.#offAgentBehaviorWatch?.();
672
+ this.#offAgentBehaviorWatch = undefined;
612
673
  this.#pending?.resolve(null);
613
674
  this.#pending = null;
614
675
  this.#finishPrompt(null);
@@ -686,7 +747,9 @@ export class LocalTui {
686
747
  #syncTick() {
687
748
  if (!this.#tty)
688
749
  return;
689
- if (this.#busy()) {
750
+ const busy = this.#busy();
751
+ this.#setTerminalProgress(this.#terminalProgress && busy);
752
+ if (busy && this.#motion !== 'off') {
690
753
  if (this.#tick === null) {
691
754
  this.#tick = setInterval(() => {
692
755
  this.#spinner += 1;
@@ -699,6 +762,74 @@ export class LocalTui {
699
762
  this.#tick = null;
700
763
  }
701
764
  }
765
+ #syncStreamingReveal(event) {
766
+ const textualDelta = event.type === 'assistant/chunk'
767
+ && (event.data.chunk.type === 'text-delta' || event.data.chunk.type === 'reasoning-delta');
768
+ const key = streamingAssistantKey(this.#state);
769
+ if (!textualDelta || this.#motion === 'off' || key === undefined) {
770
+ this.#stopRevealTick();
771
+ this.#reveal = undefined;
772
+ return false;
773
+ }
774
+ if (this.#reveal?.key !== key)
775
+ this.#reveal = { key, revealed: 0 };
776
+ this.#startRevealTick();
777
+ return true;
778
+ }
779
+ #startRevealTick() {
780
+ if (!this.#tty || this.#revealTick !== null)
781
+ return;
782
+ this.#revealTick = setInterval(() => {
783
+ const reveal = this.#reveal;
784
+ if (reveal === undefined || streamingAssistantKey(this.#state) !== reveal.key) {
785
+ this.#stopRevealTick();
786
+ this.#reveal = undefined;
787
+ return;
788
+ }
789
+ const total = streamingAssistantUnits(this.#state);
790
+ const backlog = Math.max(0, total - reveal.revealed);
791
+ if (backlog === 0) {
792
+ this.#stopRevealTick();
793
+ return;
794
+ }
795
+ reveal.revealed = Math.min(total, reveal.revealed + nextRevealStep(backlog));
796
+ this.#render();
797
+ if (reveal.revealed >= total)
798
+ this.#stopRevealTick();
799
+ }, STREAMING_REVEAL_MS);
800
+ this.#revealTick.unref?.();
801
+ }
802
+ #stopRevealTick() {
803
+ if (this.#revealTick === null)
804
+ return;
805
+ clearInterval(this.#revealTick);
806
+ this.#revealTick = null;
807
+ }
808
+ #setTerminalProgress(active) {
809
+ if (!this.#tty)
810
+ return;
811
+ if (active) {
812
+ if (this.#terminalProgressActive)
813
+ return;
814
+ this.#terminalProgressActive = true;
815
+ this.#term.output.write(terminalProgressSequence(true));
816
+ this.#terminalProgressTick = setInterval(() => {
817
+ if (!this.#disposed && this.#terminalProgressActive) {
818
+ this.#term.output.write(terminalProgressSequence(true));
819
+ }
820
+ }, TERMINAL_PROGRESS_KEEPALIVE_MS);
821
+ this.#terminalProgressTick.unref?.();
822
+ return;
823
+ }
824
+ if (this.#terminalProgressTick !== null) {
825
+ clearInterval(this.#terminalProgressTick);
826
+ this.#terminalProgressTick = null;
827
+ }
828
+ if (!this.#terminalProgressActive)
829
+ return;
830
+ this.#terminalProgressActive = false;
831
+ this.#term.output.write(terminalProgressSequence(false));
832
+ }
702
833
  #syncLoopTick() {
703
834
  if (!this.#tty)
704
835
  return;
@@ -720,8 +851,11 @@ export class LocalTui {
720
851
  if (this.#deferInitialRender)
721
852
  return;
722
853
  const width = this.#term.width();
854
+ const renderState = this.#reveal === undefined
855
+ ? this.#state
856
+ : revealStreamingAssistant(this.#state, this.#reveal.revealed);
723
857
  const frame = this.#tty
724
- ? renderView(this.#state, {
858
+ ? renderView(renderState, {
725
859
  width,
726
860
  height: this.#term.height(),
727
861
  model: this.#model,
@@ -738,6 +872,7 @@ export class LocalTui {
738
872
  version: APP_VERSION,
739
873
  appName: APP_NAME,
740
874
  spinnerFrame: this.#spinner,
875
+ motion: this.#motion,
741
876
  trueColor: this.#trueColor,
742
877
  themeName: this.#themeName,
743
878
  scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
@@ -1063,8 +1198,17 @@ export class LocalTui {
1063
1198
  this.#pasteBuf = '';
1064
1199
  return;
1065
1200
  }
1066
- if (event.type !== 'key' || event.id !== 'escape')
1201
+ const isEscape = event.type === 'key' && event.id === 'escape';
1202
+ if (!isEscape) {
1067
1203
  this.#lastEscapeTime = 0;
1204
+ this.#inspectEscapeFenceUntil = 0;
1205
+ }
1206
+ else if (this.#inspected === undefined && Date.now() < this.#inspectEscapeFenceUntil) {
1207
+ return;
1208
+ }
1209
+ else if (this.#inspected === undefined) {
1210
+ this.#inspectEscapeFenceUntil = 0;
1211
+ }
1068
1212
  if (this.#handlePrompt(event))
1069
1213
  return;
1070
1214
  if (this.#trajectory !== null) {
@@ -1460,6 +1604,9 @@ export class LocalTui {
1460
1604
  #closeInspect() {
1461
1605
  if (this.#inspected === undefined)
1462
1606
  return;
1607
+ // Keep one exit gesture inside the inspector even when repeated Escape
1608
+ // bytes are decoded after the parent view has already been restored.
1609
+ this.#inspectEscapeFenceUntil = Date.now() + DOUBLE_ESCAPE_MS;
1463
1610
  for (const listener of this.#inspectCloses)
1464
1611
  listener();
1465
1612
  }
@@ -1483,6 +1630,8 @@ export class LocalTui {
1483
1630
  return {
1484
1631
  theme: this.#themeName,
1485
1632
  colors: this.#colors,
1633
+ motion: this.#motion,
1634
+ terminalProgress: this.#terminalProgress,
1486
1635
  expandTools: this.#expandTools,
1487
1636
  checkUpdates: this.#checkUpdates,
1488
1637
  startupChangelog: this.#startupChangelog,
@@ -1503,6 +1652,9 @@ export class LocalTui {
1503
1652
  const expandChanged = prefs.expandTools !== this.#expandTools;
1504
1653
  this.#themeName = prefs.theme;
1505
1654
  this.#colors = prefs.colors;
1655
+ const previousMotion = this.#motion;
1656
+ this.#motion = prefs.motion ?? 'full';
1657
+ this.#terminalProgress = prefs.terminalProgress ?? false;
1506
1658
  this.#expandTools = prefs.expandTools;
1507
1659
  this.#checkUpdates = prefs.checkUpdates ?? true;
1508
1660
  this.#startupChangelog = prefs.startupChangelog ?? 'summary';
@@ -1512,8 +1664,41 @@ export class LocalTui {
1512
1664
  this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
1513
1665
  if (expandChanged)
1514
1666
  this.#toolsExpanded = prefs.expandTools;
1667
+ if (previousMotion !== this.#motion) {
1668
+ this.#stopRevealTick();
1669
+ this.#reveal = undefined;
1670
+ }
1671
+ this.#syncTick();
1515
1672
  this.#persistPrefs?.(prefs);
1516
1673
  }
1674
+ #applyStoredAgentBehavior(next) {
1675
+ this.#agentBehavior = { language: next.language };
1676
+ if (this.#settings !== null) {
1677
+ this.#settings = { ...this.#settings, agent: { ...this.#agentBehavior } };
1678
+ }
1679
+ if (this.#tty && !this.#disposed)
1680
+ this.#render();
1681
+ }
1682
+ async #commitAgentBehavior(next) {
1683
+ const update = this.#updateAgentBehavior;
1684
+ if (update === undefined)
1685
+ return;
1686
+ try {
1687
+ await update(next);
1688
+ this.#applyStoredAgentBehavior(next);
1689
+ }
1690
+ catch (error) {
1691
+ if (this.#settings !== null && this.#agentBehavior !== undefined) {
1692
+ this.#settings = { ...this.#settings, agent: { ...this.#agentBehavior } };
1693
+ }
1694
+ this.notice(`Could not save Agent language: ${error instanceof Error ? error.message : String(error)}`, { level: 'error' });
1695
+ }
1696
+ finally {
1697
+ this.#agentBehaviorPending = false;
1698
+ if (this.#tty && !this.#disposed)
1699
+ this.#render();
1700
+ }
1701
+ }
1517
1702
  #applySettings(event) {
1518
1703
  if (this.#settings === null)
1519
1704
  return;
@@ -1524,6 +1709,15 @@ export class LocalTui {
1524
1709
  return;
1525
1710
  }
1526
1711
  if (command.kind === 'apply') {
1712
+ if (command.domain === 'agent') {
1713
+ if (this.#agentBehaviorPending || command.state.agent === undefined)
1714
+ return;
1715
+ this.#agentBehaviorPending = true;
1716
+ this.#settings = command.state;
1717
+ this.#render();
1718
+ void this.#commitAgentBehavior(command.state.agent);
1719
+ return;
1720
+ }
1527
1721
  this.#settings = command.state;
1528
1722
  this.#applyPrefs(command.state.prefs);
1529
1723
  this.#render();
@@ -2091,7 +2285,7 @@ export class LocalTui {
2091
2285
  }
2092
2286
  this.#search = null;
2093
2287
  this.#ac = null;
2094
- this.#settings = createSettings(this.#prefs());
2288
+ this.#settings = createSettings(this.#prefs(), undefined, this.#agentBehavior);
2095
2289
  this.#render();
2096
2290
  }
2097
2291
  #notice(text) {