@agi-fans/dsh-tui 0.16.0 → 0.18.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 (95) hide show
  1. package/lib/CHANGELOG.md +53 -1
  2. package/lib/chrome/status-config.d.ts +4 -1
  3. package/lib/chrome/status-config.js.map +1 -1
  4. package/lib/chrome/tool-args.d.ts +9 -0
  5. package/lib/chrome/tool-args.js +20 -0
  6. package/lib/chrome/tool-args.js.map +1 -0
  7. package/lib/chrome/tool-renderers.js +4 -15
  8. package/lib/chrome/tool-renderers.js.map +1 -1
  9. package/lib/commands/auth.js +4 -2
  10. package/lib/commands/auth.js.map +1 -1
  11. package/lib/commands/jobs.d.ts +2 -2
  12. package/lib/commands/jobs.js +2 -2
  13. package/lib/commands/jobs.js.map +1 -1
  14. package/lib/commands/session.js +2 -4
  15. package/lib/commands/session.js.map +1 -1
  16. package/lib/definition.d.ts +3 -0
  17. package/lib/definition.js.map +1 -1
  18. package/lib/index.d.ts +1 -1
  19. package/lib/index.js +1 -1
  20. package/lib/index.js.map +1 -1
  21. package/lib/runtime/composer-images.d.ts +44 -0
  22. package/lib/runtime/composer-images.js +149 -0
  23. package/lib/runtime/composer-images.js.map +1 -0
  24. package/lib/runtime/herdr-agent.d.ts +95 -0
  25. package/lib/runtime/herdr-agent.js +258 -0
  26. package/lib/runtime/herdr-agent.js.map +1 -0
  27. package/lib/runtime/plain-tui.d.ts +34 -0
  28. package/lib/runtime/plain-tui.js +119 -0
  29. package/lib/runtime/plain-tui.js.map +1 -0
  30. package/lib/runtime/provider-local.d.ts +46 -7
  31. package/lib/runtime/provider-local.js +181 -299
  32. package/lib/runtime/provider-local.js.map +1 -1
  33. package/lib/runtime/session-runtime.d.ts +9 -1
  34. package/lib/runtime/session-runtime.js +2 -2
  35. package/lib/runtime/session-runtime.js.map +1 -1
  36. package/lib/runtime/startup-notices.js +6 -9
  37. package/lib/runtime/startup-notices.js.map +1 -1
  38. package/lib/runtime/tool-presentation.js +18 -3
  39. package/lib/runtime/tool-presentation.js.map +1 -1
  40. package/lib/session/content-text.d.ts +12 -0
  41. package/lib/session/content-text.js +13 -0
  42. package/lib/session/content-text.js.map +1 -0
  43. package/lib/session/history-store.js.map +1 -0
  44. package/lib/session/job-notice.d.ts +2 -2
  45. package/lib/session/job-notice.js.map +1 -1
  46. package/lib/session/json-file.d.ts +5 -0
  47. package/lib/session/json-file.js +21 -0
  48. package/lib/session/json-file.js.map +1 -0
  49. package/lib/session/model-favorites.js +15 -25
  50. package/lib/session/model-favorites.js.map +1 -1
  51. package/lib/session/release-notes.js +1 -18
  52. package/lib/session/release-notes.js.map +1 -1
  53. package/lib/session/session-configuration.d.ts +0 -3
  54. package/lib/session/session-configuration.js +0 -4
  55. package/lib/session/session-configuration.js.map +1 -1
  56. package/lib/session/session-controller.d.ts +6 -4
  57. package/lib/session/session-controller.js +160 -182
  58. package/lib/session/session-controller.js.map +1 -1
  59. package/lib/session/session-library.d.ts +20 -0
  60. package/lib/session/session-library.js +55 -13
  61. package/lib/session/session-library.js.map +1 -1
  62. package/lib/session/subagent-roster.js +16 -46
  63. package/lib/session/subagent-roster.js.map +1 -1
  64. package/lib/session/subagent-tracker.d.ts +51 -0
  65. package/lib/session/subagent-tracker.js +128 -0
  66. package/lib/session/subagent-tracker.js.map +1 -0
  67. package/lib/session/tui-settings.d.ts +41 -18
  68. package/lib/session/tui-settings.js +57 -39
  69. package/lib/session/tui-settings.js.map +1 -1
  70. package/lib/session/update-check.js +2 -15
  71. package/lib/session/update-check.js.map +1 -1
  72. package/lib/session/version-compare.d.ts +3 -0
  73. package/lib/session/version-compare.js +21 -0
  74. package/lib/session/version-compare.js.map +1 -0
  75. package/lib/views/at-complete.d.ts +2 -0
  76. package/lib/views/at-complete.js +2 -2
  77. package/lib/views/at-complete.js.map +1 -1
  78. package/lib/views/event-views.d.ts +11 -270
  79. package/lib/views/event-views.js +21 -976
  80. package/lib/views/event-views.js.map +1 -1
  81. package/lib/views/settings-list.d.ts +2 -2
  82. package/lib/views/settings-list.js +12 -11
  83. package/lib/views/settings-list.js.map +1 -1
  84. package/lib/views/transcript-export.js +0 -2
  85. package/lib/views/transcript-export.js.map +1 -1
  86. package/lib/views/transcript-render.d.ts +179 -0
  87. package/lib/views/transcript-render.js +916 -0
  88. package/lib/views/transcript-render.js.map +1 -0
  89. package/lib/views/transcript-types.d.ts +104 -0
  90. package/lib/views/transcript-types.js +53 -0
  91. package/lib/views/transcript-types.js.map +1 -0
  92. package/package.json +39 -37
  93. package/lib/views/history-store.js.map +0 -1
  94. /package/lib/{views → session}/history-store.d.ts +0 -0
  95. /package/lib/{views → session}/history-store.js +0 -0
@@ -11,9 +11,11 @@
11
11
  * @module @agi-fans/dsh-tui
12
12
  */
13
13
  import { TerminalNotificationController, terminalNotificationSequence, terminalProgressSequence, } from "./terminal-notifications.js";
14
+ import { HerdrAgentReporter } from "./herdr-agent.js";
15
+ import { ComposerImages } from "./composer-images.js";
16
+ import { PlainTui } from "./plain-tui.js";
14
17
  import { homedir } from 'node:os';
15
18
  import { join, sep } from 'node:path';
16
- import { createInterface } from 'node:readline';
17
19
  import { StringDecoder } from 'node:string_decoder';
18
20
  import { TUI_SERVICE, } from "../definition.js";
19
21
  import { applySlashCompletion, formatHelpText, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "../views/autocomplete.js";
@@ -28,28 +30,43 @@ import { InputEditor, lineEnd, lineStart } from "../input/editor.js";
28
30
  import { applySettingsEvent, createSettings, } from "../views/settings-list.js";
29
31
  import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, trajectoryDetailMetrics, trajectoryListMetrics, } from "../views/trajectory.js";
30
32
  import { applyAgentHubEvent, createAgentHub, updateAgentHub, } from "../views/agent-hub.js";
31
- import { applyEvent, applyStreamChunk, blockLines, blockSearchText, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
33
+ import { applyEvent, applyStreamChunk, blockSearchText, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
32
34
  import { applyTranscriptSearchEvent, createTranscriptSearch, searchBlockIndexes, } from "../views/transcript-search.js";
33
35
  import { nextRevealStep, revealStreamingAssistant, streamingAssistantKey, streamingAssistantUnits, } from "../views/streaming-reveal.js";
34
36
  import { flushPending, parseKeys } from "../input/keys.js";
35
37
  import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
36
- import { colorDisabledByEnv, createTheme, detectTrueColor, parseThemeName } from "../chrome/theme.js";
38
+ import { colorDisabledByEnv, detectTrueColor } from "../chrome/theme.js";
37
39
  import { renderTool } from "../chrome/tool-renderers.js";
38
- import { TUI_SETTINGS_NAMESPACE, TuiSettingsSchema } from "../session/tui-settings.js";
40
+ import { TUI_SETTINGS_ENTRY, TUI_SETTINGS_FIELDS } from "../session/tui-settings.js";
39
41
  import { defaultStatusBarConfig, resolveStatusBarConfig } from "../chrome/status-config.js";
40
- import { HistoryStore } from "../views/history-store.js";
42
+ import { HistoryStore } from "../session/history-store.js";
41
43
  import { loadKeybindings } from "../input/keybindings-config.js";
42
44
  import { editExternally } from "../input/external-editor.js";
45
+ import z from '@deepseek-ai/schemastery';
43
46
  import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, } from "../views/prompt-selector.js";
44
47
  import { refreshProjectContext, resolveProjectContext } from "../session/project-context.js";
45
48
  import { pickWelcomeTips } from "../chrome/welcome-tips.js";
46
49
  import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount } from "../views/hotkeys.js";
47
- import { imageMarker, imagePathCandidates, probeImageDimensions, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
50
+ import { imagePathCandidates, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
48
51
  import { APP_NAME, APP_VERSION } from "../session/package-metadata.js";
49
52
  const DOUBLE_CTRL_C_MS = 500;
50
53
  const DOUBLE_ESCAPE_MS = 500;
51
54
  const STREAMING_REVEAL_MS = 1000 / 30;
52
55
  const TERMINAL_PROGRESS_KEEPALIVE_MS = 1_000;
56
+ /**
57
+ * Durable settlement events that arrive in bursts while a turn runs. Renders
58
+ * for these are coalesced through the stream-render timer; rarer control
59
+ * events (command lifecycle, inbox splices, titles, notices) render at once.
60
+ */
61
+ const BURST_EVENT_TYPES = new Set([
62
+ 'step/start',
63
+ 'step/end',
64
+ 'tool/call',
65
+ 'tool/result',
66
+ 'assistant/message',
67
+ 'assistant/attempt',
68
+ 'llm/retry',
69
+ ]);
53
70
  function shortenPath(cwd) {
54
71
  const home = homedir();
55
72
  if (cwd === home)
@@ -73,6 +90,17 @@ function windowTitleText(title) {
73
90
  return [...cleaned].slice(0, WINDOW_TITLE_MAX).join('');
74
91
  }
75
92
  export const name = 'omdsh-tui';
93
+ /**
94
+ * Row schema. The durable preferences ride as volatile fields, so a settings
95
+ * form edit commits into their references and reaches this fiber through
96
+ * `loader/volatile-update` without remounting the terminal provider.
97
+ */
98
+ export const Config = z.object({
99
+ model: z.string(),
100
+ historyPath: z.string(),
101
+ keybindingsPath: z.string(),
102
+ ...TUI_SETTINGS_FIELDS,
103
+ });
76
104
  /**
77
105
  * Local terminal presentation service.
78
106
  */
@@ -146,11 +174,11 @@ export class LocalTui {
146
174
  #pasteBuf = '';
147
175
  #pasteInFlight = 0;
148
176
  #deferredPasteEvents = [];
149
- #images = [];
150
- #lineReader = null;
151
- #plainPending = null;
152
- #plainClosed = false;
153
- #plainPrinted = 0;
177
+ #images = new ComposerImages({
178
+ editor: this.#editor,
179
+ notice: text => { this.notice(text, { level: 'error' }); },
180
+ });
181
+ #plain;
154
182
  #offData = null;
155
183
  #offResize = null;
156
184
  #pwd;
@@ -173,6 +201,12 @@ export class LocalTui {
173
201
  #checkUpdates = true;
174
202
  #startupChangelog = 'summary';
175
203
  #notifications = new TerminalNotificationController();
204
+ /**
205
+ * Herdr lifecycle authority for this pane. The plugin entry injects the
206
+ * environment-aware reporter; a directly constructed instance is inert so
207
+ * tests running inside a Herdr pane cannot report against the live server.
208
+ */
209
+ #herdr;
176
210
  #notificationPolicy = 'off';
177
211
  #notificationThreshold = '30s';
178
212
  #statusBar = defaultStatusBarConfig();
@@ -220,7 +254,6 @@ export class LocalTui {
220
254
  #searchFiles;
221
255
  #searchFileMentions;
222
256
  #searchSessions;
223
- #validateImageDraft;
224
257
  #autocompleteDebounceMs;
225
258
  #persistPrefs = null;
226
259
  #agentBehavior;
@@ -264,8 +297,15 @@ export class LocalTui {
264
297
  this.#terminalProfile = paths.terminalProfile ?? detectTerminalProfile();
265
298
  this.#resizeDebounceMs = Math.max(0, paths.resizeDebounceMs ?? 120);
266
299
  this.#streamRenderMs = Math.max(0, paths.streamRenderMs ?? 8);
300
+ this.#herdr = paths.herdrReporter ?? new HerdrAgentReporter({ env: {} });
267
301
  this.#syncTrueColor();
268
302
  this.#tty = term.input.isTTY === true;
303
+ this.#plain = new PlainTui({
304
+ term,
305
+ blocks: () => this.#state.blocks,
306
+ promptRequest: () => this.#prompt?.request,
307
+ finishPrompt: answer => { this.#finishPrompt(answer); },
308
+ });
269
309
  this.#pwd = shortenPath(project.root);
270
310
  this.#branch = project.gitLabel;
271
311
  this.#renderer = new MainScreenRenderer({ write: (chunk) => { this.#term.output.write(chunk); } }, {
@@ -301,6 +341,7 @@ export class LocalTui {
301
341
  term.output.write('\x1b[?2004h');
302
342
  }
303
343
  this.#render();
344
+ this.#herdr.start();
304
345
  }
305
346
  /** Refresh Git workspace metadata after a turn; the footer shows realtime branch/dirty state. */
306
347
  #refreshProjectContext() {
@@ -325,10 +366,18 @@ export class LocalTui {
325
366
  this.#syncStreamingReveal(undefined);
326
367
  this.#syncTick();
327
368
  if (this.#tty) {
328
- this.#render();
369
+ // Settlement events arrive in bursts (parallel tool calls, in-process
370
+ // subagent settlements); coalesce them like setSession. Control events
371
+ // (user echo, command lifecycle, inbox splices) stay immediate.
372
+ if (this.#streamRenderMs > 0 && this.#busy() && BURST_EVENT_TYPES.has(event.type)) {
373
+ this.#scheduleStreamRender();
374
+ }
375
+ else {
376
+ this.#render();
377
+ }
329
378
  }
330
379
  else if (event.type === 'user/message' || event.type === 'assistant/message' || event.type === 'tool/result' || event.type === 'turn/end') {
331
- this.#printPlain();
380
+ this.#plain.print();
332
381
  }
333
382
  }
334
383
  /**
@@ -353,6 +402,10 @@ export class LocalTui {
353
402
  }
354
403
  }
355
404
  setStatus(status) {
405
+ // Only the displayed top-level agent drives the Herdr pane state; an
406
+ // inspected child must not, and closing the inspector re-syncs the root.
407
+ if (this.#inspected === undefined)
408
+ this.#herdr.setRunning(status === 'running');
356
409
  if (this.#state.status === 'compacting')
357
410
  return;
358
411
  this.#state = { ...this.#state, status };
@@ -463,7 +516,7 @@ export class LocalTui {
463
516
  this.#render();
464
517
  }
465
518
  setImageValidator(validate) {
466
- this.#validateImageDraft = validate;
519
+ this.#images.setValidator(validate);
467
520
  }
468
521
  toolCallContext(callId) {
469
522
  const block = this.#state.blocks.findLast(candidate => candidate.kind === 'tool' && String(candidate.callId) === callId);
@@ -501,14 +554,14 @@ export class LocalTui {
501
554
  if (this.#tty)
502
555
  this.#render();
503
556
  else
504
- this.#printPlain();
557
+ this.#plain.print();
505
558
  }
506
559
  commandOutput(command, text) {
507
560
  this.#state = { ...this.#state, blocks: [...this.#state.blocks, { kind: 'commandOutput', command, text }] };
508
561
  if (this.#tty)
509
562
  this.#render();
510
563
  else
511
- this.#printPlain();
564
+ this.#plain.print();
512
565
  }
513
566
  prompt(request) {
514
567
  if (this.#prompt !== null)
@@ -516,6 +569,7 @@ export class LocalTui {
516
569
  if (this.#disposed || request.signal?.aborted === true)
517
570
  return Promise.resolve(null);
518
571
  this.#emitNotification(this.#notifications.humanPrompt());
572
+ this.#herdr.prompt(request.title);
519
573
  this.#editor.setText('');
520
574
  this.#ac = null;
521
575
  const displaced = this.#displaceSurface();
@@ -552,17 +606,18 @@ export class LocalTui {
552
606
  this.#trajectory = null;
553
607
  const state = replayEvents(events, presentations);
554
608
  this.#state = { ...state, status, compactCommandId: undefined };
555
- this.#plainPrinted = 0;
609
+ this.#plain.resetPrinted();
556
610
  this.#followTail();
557
611
  this.#deferInitialRender = false;
558
612
  this.#renderer.startEpoch({ replay: status === 'idle' ? 'full' : 'pinned' });
559
613
  if (this.#tty)
560
614
  this.#render();
561
615
  else
562
- this.#printPlain();
616
+ this.#plain.print();
563
617
  }
564
618
  setSession(info) {
565
619
  this.#sessionId = info.id;
620
+ this.#herdr.setSession(info.id);
566
621
  const title = info.title?.trim();
567
622
  this.#sessionTitle = title === undefined || title === '' ? undefined : title;
568
623
  this.#recentSessions = info.recent.map((session) => ({ ...session }));
@@ -602,32 +657,37 @@ export class LocalTui {
602
657
  #syncTrueColor() {
603
658
  this.#trueColor = this.#colors && detectTrueColor();
604
659
  }
605
- /** Apply prefs loaded from the settings document (does not persist). */
660
+ /** Apply prefs loaded from this entry's config (does not persist). */
606
661
  applyStoredPrefs(prefs) {
607
- const previousMotion = this.#motion;
608
- this.#themeName = prefs.theme;
609
- this.#colors = prefs.colors;
610
- this.#syncTrueColor();
611
- this.#motion = prefs.motion ?? 'full';
612
- this.#terminalProgress = prefs.terminalProgress ?? false;
613
- this.#expandTools = prefs.expandTools;
614
- this.#checkUpdates = prefs.checkUpdates ?? true;
615
- this.#startupChangelog = prefs.startupChangelog ?? 'summary';
616
- this.#notificationPolicy = prefs.notifications ?? 'off';
617
- this.#notificationThreshold = prefs.notificationThreshold ?? '30s';
618
- this.#configureNotifications();
619
- this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
620
- this.#toolsExpanded = prefs.expandTools;
621
- if (previousMotion !== this.#motion) {
622
- this.#stopRevealTick();
623
- this.#reveal = undefined;
624
- }
625
- this.#syncTick();
662
+ this.#applyPrefs(prefs, { persist: false, forceToolsSync: true });
626
663
  if (this.#settings !== null)
627
664
  this.#settings = { ...this.#settings, prefs };
628
665
  if (this.#tty)
629
666
  this.#render();
630
667
  }
668
+ /** Currently applied preferences; sibling rows read these instead of the config references. */
669
+ prefs() {
670
+ return {
671
+ theme: this.#themeName,
672
+ colors: this.#colors,
673
+ motion: this.#motion,
674
+ terminalProgress: this.#terminalProgress,
675
+ expandTools: this.#expandTools,
676
+ checkUpdates: this.#checkUpdates,
677
+ startupChangelog: this.#startupChangelog,
678
+ notifications: this.#notificationPolicy,
679
+ notificationThreshold: this.#notificationThreshold,
680
+ statusBar: {
681
+ ...this.#statusBar,
682
+ groups: [...this.#statusBar.groups],
683
+ ...(this.#statusBar.order === undefined ? {} : { order: [...this.#statusBar.order] }),
684
+ ...(this.#statusBar.meta === undefined ? {} : { meta: [...this.#statusBar.meta] }),
685
+ ...(this.#statusBar.metaOrder === undefined ? {} : { metaOrder: [...this.#statusBar.metaOrder] }),
686
+ ...(this.#statusBar.colors === undefined ? {} : { colors: { ...this.#statusBar.colors } }),
687
+ ...(this.#statusBar.sides === undefined ? {} : { sides: { ...this.#statusBar.sides } }),
688
+ },
689
+ };
690
+ }
631
691
  /** Called after a live `/settings` change. */
632
692
  setPrefsPersist(persist) {
633
693
  this.#persistPrefs = persist;
@@ -650,7 +710,7 @@ export class LocalTui {
650
710
  this.#agentBehavior = undefined;
651
711
  this.#agentBehaviorPending = false;
652
712
  if (this.#settings !== null)
653
- this.#settings = createSettings(this.#prefs());
713
+ this.#settings = createSettings(this.prefs());
654
714
  if (this.#tty && !this.#disposed)
655
715
  this.#render();
656
716
  };
@@ -667,7 +727,7 @@ export class LocalTui {
667
727
  return Promise.resolve(null);
668
728
  }
669
729
  if (!this.#tty)
670
- return this.#readlinePlain(signal);
730
+ return this.#plain.readline(signal);
671
731
  // Lines submitted while a turn was still running were queued instead of
672
732
  // dropped; serve the oldest before waiting for fresh input.
673
733
  const queued = this.#queuedSubmissions.shift();
@@ -695,16 +755,7 @@ export class LocalTui {
695
755
  return (await this.readInput())?.text ?? null;
696
756
  }
697
757
  restoreInput(submission) {
698
- const currentText = this.#editor.text;
699
- const currentImages = this.#images;
700
- let rebasedCurrent = currentText;
701
- for (let index = currentImages.length - 1; index >= 0; index -= 1) {
702
- const image = currentImages[index];
703
- rebasedCurrent = rebasedCurrent.replaceAll(imageMarker(index, image), imageMarker(index + submission.images.length, image));
704
- }
705
- const separator = submission.text !== '' && rebasedCurrent !== '' ? '\n' : '';
706
- this.#images = [...submission.images.map(image => ({ ...image })), ...currentImages];
707
- this.#editor.setText(submission.text + separator + rebasedCurrent);
758
+ this.#images.restore(submission);
708
759
  this.#refreshAutocomplete();
709
760
  if (this.#tty)
710
761
  this.#render();
@@ -714,14 +765,14 @@ export class LocalTui {
714
765
  return;
715
766
  this.#queueEditPending = false;
716
767
  if (submission === null) {
717
- if (this.#editor.text === '' && this.#images.length === 0 && this.#queueEditNewer?.length === 0) {
768
+ if (this.#editor.text === '' && this.#images.count === 0 && this.#queueEditNewer?.length === 0) {
718
769
  this.#queueEditNewer = null;
719
770
  }
720
771
  return;
721
772
  }
722
773
  if (this.#queueEditNewer === null)
723
774
  this.#queueEditNewer = [];
724
- if (this.#editor.text !== '' || this.#images.length > 0) {
775
+ if (this.#editor.text !== '' || this.#images.count > 0) {
725
776
  this.#queueEditNewer.unshift(this.#currentSubmission());
726
777
  }
727
778
  this.#replaceInput(submission);
@@ -729,12 +780,11 @@ export class LocalTui {
729
780
  #currentSubmission() {
730
781
  return {
731
782
  text: this.#editor.text,
732
- images: this.#images.map(image => ({ ...image })),
783
+ images: this.#images.copies(),
733
784
  };
734
785
  }
735
786
  #replaceInput(submission) {
736
- this.#images = submission.images.map(image => ({ ...image }));
737
- this.#editor.setText(submission.text);
787
+ this.#images.replace(submission);
738
788
  this.#historyIndex = 0;
739
789
  this.#refreshAutocomplete();
740
790
  if (this.#tty)
@@ -814,11 +864,12 @@ export class LocalTui {
814
864
  this.#autocompleteAbort?.abort();
815
865
  this.#autocompleteTimer = null;
816
866
  this.#autocompleteAbort = null;
817
- this.#lineReader?.close();
867
+ this.#plain.dispose();
818
868
  this.#offAgentBehaviorWatch?.();
819
869
  this.#offAgentBehaviorWatch = undefined;
820
870
  this.#settlePending(null);
821
871
  this.#finishPrompt(null);
872
+ this.#herdr.release();
822
873
  }
823
874
  /** Settle one pending read, detaching its abort listener. */
824
875
  #settlePending(submission) {
@@ -833,96 +884,6 @@ export class LocalTui {
833
884
  refresh() {
834
885
  this.#render();
835
886
  }
836
- #readlinePlain(signal) {
837
- return new Promise((resolve) => {
838
- if (this.#lineReader === null) {
839
- this.#lineReader = createInterface({ input: this.#term.input });
840
- // Permanent listeners: once() handlers would auto-pause the input
841
- // stream after one line and miss the EOF close.
842
- this.#lineReader.on('line', (line) => { this.#plainResolve(line); });
843
- this.#lineReader.on('close', () => {
844
- this.#plainClosed = true;
845
- this.#plainResolve(null);
846
- });
847
- }
848
- const pending = { resolve, signal: signal ?? null };
849
- this.#plainPending = pending;
850
- if (signal !== undefined) {
851
- const onAbort = () => {
852
- if (this.#plainPending !== pending)
853
- return;
854
- this.#plainPending = null;
855
- resolve(null);
856
- };
857
- signal.addEventListener('abort', onAbort, { once: true });
858
- pending.offAbort = () => { signal.removeEventListener('abort', onAbort); };
859
- }
860
- this.#pumpPlain();
861
- });
862
- }
863
- /**
864
- * Queue one readline delivery, then drain. A single stream chunk can carry
865
- * several lines; every line is buffered until the runner asks for the next
866
- * read, and EOF waits for the queue before closing the reader.
867
- */
868
- #plainResolve(line) {
869
- if (this.#prompt !== null && line !== null) {
870
- const value = line.trim();
871
- if (value === '') {
872
- this.#finishPrompt(null);
873
- }
874
- else if (this.#prompt.request.allowCustom === false) {
875
- const options = this.#prompt.request.options ?? [];
876
- const numeric = /^\d+$/u.test(value) ? Number(value) - 1 : -1;
877
- const option = numeric >= 0
878
- ? options[numeric]
879
- : options.find(item => item.label.toLowerCase() === value.toLowerCase());
880
- this.#finishPrompt(option?.value ?? option?.label ?? null);
881
- }
882
- else {
883
- this.#finishPrompt(value);
884
- }
885
- return;
886
- }
887
- if (this.#prompt !== null)
888
- this.#finishPrompt(null);
889
- if (line !== null)
890
- this.#plainQueue.push(line);
891
- this.#pumpPlain();
892
- }
893
- /** Resolve the pending plain read from the queue, or close it after EOF drained. */
894
- #pumpPlain() {
895
- const pending = this.#plainPending;
896
- if (pending === null)
897
- return;
898
- const line = this.#plainQueue.shift();
899
- if (line !== undefined) {
900
- pending.offAbort?.();
901
- this.#plainPending = null;
902
- pending.resolve({ text: line, images: [] });
903
- return;
904
- }
905
- if (this.#plainClosed) {
906
- pending.offAbort?.();
907
- this.#plainPending = null;
908
- pending.resolve(null);
909
- }
910
- }
911
- /** Print plain-mode blocks that settled since the last flush. */
912
- #printPlain() {
913
- const theme = createTheme(false, false);
914
- const width = this.#term.width();
915
- const fresh = this.#state.blocks.slice(this.#plainPrinted);
916
- let out = '';
917
- for (const block of fresh) {
918
- // Pipe / CI output is not a viewport: print the full tool body.
919
- for (const line of blockLines(block, theme, width, 0, true))
920
- out += line + '\n';
921
- }
922
- this.#plainPrinted = this.#state.blocks.length;
923
- if (out !== '')
924
- this.#term.output.write(out);
925
- }
926
887
  #busy() {
927
888
  if (this.#state.status !== 'idle')
928
889
  return true;
@@ -1048,7 +1009,7 @@ export class LocalTui {
1048
1009
  ...(this.#reasoningEffort === undefined ? {} : { reasoningEffort: this.#reasoningEffort }),
1049
1010
  input: this.#editor.text,
1050
1011
  inputCursor: this.#editor.cursor,
1051
- inputImages: this.#images.length,
1012
+ inputImages: this.#images.count,
1052
1013
  queuedSubmissions: this.#queueEditNewer === null
1053
1014
  ? this.#queuedSubmissions
1054
1015
  : [...this.#queuedSubmissions, ...this.#queueEditNewer],
@@ -1143,7 +1104,6 @@ export class LocalTui {
1143
1104
  this.#scrollStart = this.#maxStart;
1144
1105
  }
1145
1106
  #utf8 = new StringDecoder('utf8');
1146
- #plainQueue = [];
1147
1107
  #onData(chunk) {
1148
1108
  // Decode bytes across the whole stream so a multi-byte UTF-8 character
1149
1109
  // split between data events is never corrupted into replacement chars.
@@ -1204,8 +1164,8 @@ export class LocalTui {
1204
1164
  if (images.every((image) => image !== null)) {
1205
1165
  let inserted = false;
1206
1166
  for (const image of images) {
1207
- if (await this.#admitImage(image)) {
1208
- this.#insertImageDraft(image);
1167
+ if (await this.#images.admit(image)) {
1168
+ this.#images.insert(image);
1209
1169
  inserted = true;
1210
1170
  }
1211
1171
  }
@@ -1220,7 +1180,7 @@ export class LocalTui {
1220
1180
  // image instead of leaking that stale path into the prompt.
1221
1181
  const clipboardImage = await this.#readClipboardImage();
1222
1182
  if (clipboardImage !== null) {
1223
- this.#insertImageDraft(clipboardImage);
1183
+ this.#images.insert(clipboardImage);
1224
1184
  this.#refreshAutocomplete();
1225
1185
  this.#render();
1226
1186
  return;
@@ -1234,8 +1194,8 @@ export class LocalTui {
1234
1194
  if (this.#search === null && this.#settings === null && this.#copySelector === null) {
1235
1195
  const image = await this.#readClipboardImage();
1236
1196
  if (image !== null) {
1237
- if (await this.#admitImage(image)) {
1238
- this.#insertImageDraft(image);
1197
+ if (await this.#images.admit(image)) {
1198
+ this.#images.insert(image);
1239
1199
  this.#refreshAutocomplete();
1240
1200
  this.#render();
1241
1201
  }
@@ -1249,8 +1209,8 @@ export class LocalTui {
1249
1209
  if (images.length > 0) {
1250
1210
  let inserted = false;
1251
1211
  for (const candidate of images) {
1252
- if (await this.#admitImage(candidate)) {
1253
- this.#insertImageDraft(candidate);
1212
+ if (await this.#images.admit(candidate)) {
1213
+ this.#images.insert(candidate);
1254
1214
  inserted = true;
1255
1215
  }
1256
1216
  }
@@ -1266,98 +1226,6 @@ export class LocalTui {
1266
1226
  if (text !== '')
1267
1227
  await this.#acceptPastedText(text);
1268
1228
  }
1269
- /**
1270
- * Run the Harness image-admission check for one paste candidate. A refusal
1271
- * becomes an error notice and skips the draft, instead of failing the whole
1272
- * submission after the user has typed a prompt around it.
1273
- */
1274
- async #admitImage(image) {
1275
- if (this.#validateImageDraft === undefined)
1276
- return true;
1277
- try {
1278
- await this.#validateImageDraft(image);
1279
- return true;
1280
- }
1281
- catch (error) {
1282
- this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1283
- return false;
1284
- }
1285
- }
1286
- #insertImageDraft(input) {
1287
- const size = input.width === undefined || input.height === undefined
1288
- ? probeImageDimensions(input.data, input.mediaType)
1289
- : undefined;
1290
- const image = {
1291
- ...input,
1292
- ...(input.width === undefined && size !== undefined ? { width: size.width } : {}),
1293
- ...(input.height === undefined && size !== undefined ? { height: size.height } : {}),
1294
- };
1295
- const marker = imageMarker(this.#images.length, image);
1296
- const before = this.#editor.cursor > 0 && !/\s/u.test(this.#editor.text[this.#editor.cursor - 1] ?? '') ? ' ' : '';
1297
- const after = this.#editor.cursor >= this.#editor.text.length || !/\s/u.test(this.#editor.text[this.#editor.cursor] ?? '')
1298
- ? ' '
1299
- : '';
1300
- this.#images.push(image);
1301
- this.#editor.handle({ type: 'text', value: before + marker + after });
1302
- }
1303
- #removeImageAtCursor(key) {
1304
- const cursor = this.#editor.cursor;
1305
- for (let index = 0; index < this.#images.length; index += 1) {
1306
- const image = this.#images[index];
1307
- const marker = imageMarker(index, image);
1308
- const start = this.#editor.text.indexOf(marker);
1309
- if (start < 0)
1310
- continue;
1311
- let from = start;
1312
- let to = start + marker.length;
1313
- const touches = key === 'backspace'
1314
- ? cursor > start && cursor <= to
1315
- : cursor >= start && cursor < to;
1316
- if (!touches)
1317
- continue;
1318
- if (this.#editor.text[to] === ' ')
1319
- to += 1;
1320
- else if (from > 0 && this.#editor.text[from - 1] === ' ')
1321
- from -= 1;
1322
- const oldImages = this.#images;
1323
- let text = this.#editor.text.slice(0, from) + this.#editor.text.slice(to);
1324
- const nextImages = oldImages.filter((_, oldIndex) => oldIndex !== index);
1325
- let nextIndex = 0;
1326
- for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
1327
- if (oldIndex === index)
1328
- continue;
1329
- const remaining = oldImages[oldIndex];
1330
- text = text.replaceAll(imageMarker(oldIndex, remaining), imageMarker(nextIndex, remaining));
1331
- nextIndex += 1;
1332
- }
1333
- this.#images = nextImages;
1334
- this.#editor.setText(text, Math.min(from, text.length));
1335
- this.#refreshAutocomplete();
1336
- this.#render();
1337
- return true;
1338
- }
1339
- return false;
1340
- }
1341
- #reconcileImageDrafts() {
1342
- if (this.#images.length === 0)
1343
- return;
1344
- const oldImages = this.#images;
1345
- const retained = oldImages.filter((image, index) => this.#editor.text.includes(imageMarker(index, image)));
1346
- if (retained.length === oldImages.length)
1347
- return;
1348
- let text = this.#editor.text;
1349
- let nextIndex = 0;
1350
- for (let oldIndex = 0; oldIndex < oldImages.length; oldIndex += 1) {
1351
- const image = oldImages[oldIndex];
1352
- const oldMarker = imageMarker(oldIndex, image);
1353
- if (!text.includes(oldMarker))
1354
- continue;
1355
- text = text.replaceAll(oldMarker, imageMarker(nextIndex, image));
1356
- nextIndex += 1;
1357
- }
1358
- this.#images = retained;
1359
- this.#editor.setText(text, Math.min(this.#editor.cursor, text.length));
1360
- }
1361
1229
  #dispatch(event) {
1362
1230
  if (this.#state.status === 'compacting') {
1363
1231
  if (event.type === 'key' && event.id === 'pageUp') {
@@ -1483,7 +1351,7 @@ export class LocalTui {
1483
1351
  }
1484
1352
  else {
1485
1353
  this.#editor.clear();
1486
- this.#images = [];
1354
+ this.#images.clear();
1487
1355
  this.#historyIndex = 0;
1488
1356
  this.#ac = null;
1489
1357
  this.#render();
@@ -1511,9 +1379,9 @@ export class LocalTui {
1511
1379
  return;
1512
1380
  if (event.type === 'key' && event.id === 'escape') {
1513
1381
  if (this.#inspected !== undefined) {
1514
- if (this.#inspected.writable === true && (this.#editor.text !== '' || this.#images.length > 0)) {
1382
+ if (this.#inspected.writable === true && (this.#editor.text !== '' || this.#images.count > 0)) {
1515
1383
  this.#editor.clear();
1516
- this.#images = [];
1384
+ this.#images.clear();
1517
1385
  this.#ac = null;
1518
1386
  this.#render();
1519
1387
  return;
@@ -1522,7 +1390,7 @@ export class LocalTui {
1522
1390
  return;
1523
1391
  }
1524
1392
  if (this.#state.status !== 'idle' || this.#pending === null
1525
- || this.#editor.text !== '' || this.#images.length > 0) {
1393
+ || this.#editor.text !== '' || this.#images.count > 0) {
1526
1394
  this.#lastEscapeTime = 0;
1527
1395
  }
1528
1396
  else {
@@ -1539,8 +1407,11 @@ export class LocalTui {
1539
1407
  }
1540
1408
  }
1541
1409
  if (event.type === 'key' && (event.id === 'backspace' || event.id === 'delete')
1542
- && this.#removeImageAtCursor(event.id))
1410
+ && this.#images.removeAtCursor(event.id)) {
1411
+ this.#refreshAutocomplete();
1412
+ this.#render();
1543
1413
  return;
1414
+ }
1544
1415
  this.#applyCommand(this.#editor.handle(event));
1545
1416
  }
1546
1417
  #handlePrompt(event) {
@@ -1751,7 +1622,7 @@ export class LocalTui {
1751
1622
  }
1752
1623
  if (!hasAgents || event.type !== 'key' || event.id !== 'down')
1753
1624
  return false;
1754
- if (this.#editor.text !== '' || this.#images.length > 0 || this.#historyIndex !== 0 || this.#ac !== null)
1625
+ if (this.#editor.text !== '' || this.#images.count > 0 || this.#historyIndex !== 0 || this.#ac !== null)
1755
1626
  return false;
1756
1627
  if (this.#queuedSubmissions.length > 0 || this.#queueEditNewer !== null || this.#queueEditPending)
1757
1628
  return false;
@@ -1784,34 +1655,12 @@ export class LocalTui {
1784
1655
  this.#agentHub = createAgentHub({ agents });
1785
1656
  this.#render();
1786
1657
  }
1787
- #prefs() {
1788
- return {
1789
- theme: this.#themeName,
1790
- colors: this.#colors,
1791
- motion: this.#motion,
1792
- terminalProgress: this.#terminalProgress,
1793
- expandTools: this.#expandTools,
1794
- checkUpdates: this.#checkUpdates,
1795
- startupChangelog: this.#startupChangelog,
1796
- notifications: this.#notificationPolicy,
1797
- notificationThreshold: this.#notificationThreshold,
1798
- statusBar: {
1799
- ...this.#statusBar,
1800
- groups: [...this.#statusBar.groups],
1801
- ...(this.#statusBar.order === undefined ? {} : { order: [...this.#statusBar.order] }),
1802
- ...(this.#statusBar.meta === undefined ? {} : { meta: [...this.#statusBar.meta] }),
1803
- ...(this.#statusBar.metaOrder === undefined ? {} : { metaOrder: [...this.#statusBar.metaOrder] }),
1804
- ...(this.#statusBar.colors === undefined ? {} : { colors: { ...this.#statusBar.colors } }),
1805
- ...(this.#statusBar.sides === undefined ? {} : { sides: { ...this.#statusBar.sides } }),
1806
- },
1807
- };
1808
- }
1809
- #applyPrefs(prefs) {
1658
+ #applyPrefs(prefs, options) {
1659
+ const previousMotion = this.#motion;
1810
1660
  const expandChanged = prefs.expandTools !== this.#expandTools;
1811
1661
  this.#themeName = prefs.theme;
1812
1662
  this.#colors = prefs.colors;
1813
1663
  this.#syncTrueColor();
1814
- const previousMotion = this.#motion;
1815
1664
  this.#motion = prefs.motion ?? 'full';
1816
1665
  this.#terminalProgress = prefs.terminalProgress ?? false;
1817
1666
  this.#expandTools = prefs.expandTools;
@@ -1821,14 +1670,15 @@ export class LocalTui {
1821
1670
  this.#notificationThreshold = prefs.notificationThreshold ?? '30s';
1822
1671
  this.#configureNotifications();
1823
1672
  this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
1824
- if (expandChanged)
1673
+ if (options.forceToolsSync || expandChanged)
1825
1674
  this.#toolsExpanded = prefs.expandTools;
1826
1675
  if (previousMotion !== this.#motion) {
1827
1676
  this.#stopRevealTick();
1828
1677
  this.#reveal = undefined;
1829
1678
  }
1830
1679
  this.#syncTick();
1831
- this.#persistPrefs?.(prefs);
1680
+ if (options.persist)
1681
+ this.#persistPrefs?.(prefs);
1832
1682
  }
1833
1683
  #applyStoredAgentBehavior(next) {
1834
1684
  this.#agentBehavior = { language: next.language };
@@ -1878,7 +1728,7 @@ export class LocalTui {
1878
1728
  return;
1879
1729
  }
1880
1730
  this.#settings = command.state;
1881
- this.#applyPrefs(command.state.prefs);
1731
+ this.#applyPrefs(command.state.prefs, { persist: true, forceToolsSync: false });
1882
1732
  this.#render();
1883
1733
  return;
1884
1734
  }
@@ -2090,7 +1940,7 @@ export class LocalTui {
2090
1940
  }
2091
1941
  #applyCommand(command) {
2092
1942
  if (command.kind === 'changed') {
2093
- this.#reconcileImageDrafts();
1943
+ this.#images.reconcile();
2094
1944
  if (command.edited === true)
2095
1945
  this.#historyIndex = 0;
2096
1946
  this.#refreshAutocomplete();
@@ -2125,7 +1975,7 @@ export class LocalTui {
2125
1975
  }
2126
1976
  if (command.kind === 'clear') {
2127
1977
  this.#editor.clear();
2128
- this.#images = [];
1978
+ this.#images.clear();
2129
1979
  this.#queueEditNewer = null;
2130
1980
  this.#queueEditPending = false;
2131
1981
  this.#historyIndex = 0;
@@ -2153,7 +2003,7 @@ export class LocalTui {
2153
2003
  }
2154
2004
  }
2155
2005
  #historyPrev() {
2156
- if (this.#images.length > 0)
2006
+ if (this.#images.count > 0)
2157
2007
  return;
2158
2008
  if (this.#history.length === 0 || this.#historyIndex >= this.#history.length)
2159
2009
  return;
@@ -2182,7 +2032,7 @@ export class LocalTui {
2182
2032
  listener();
2183
2033
  return true;
2184
2034
  }
2185
- if (this.#editor.text !== '' || this.#images.length > 0 || this.#historyIndex !== 0)
2035
+ if (this.#editor.text !== '' || this.#images.count > 0 || this.#historyIndex !== 0)
2186
2036
  return false;
2187
2037
  const submission = this.#queuedSubmissions.pop();
2188
2038
  if (submission !== undefined) {
@@ -2200,7 +2050,7 @@ export class LocalTui {
2200
2050
  return true;
2201
2051
  }
2202
2052
  #historyNext() {
2203
- if (this.#images.length > 0)
2053
+ if (this.#images.count > 0)
2204
2054
  return;
2205
2055
  if (this.#historyIndex === 0)
2206
2056
  return;
@@ -2228,7 +2078,7 @@ export class LocalTui {
2228
2078
  this.#render();
2229
2079
  return;
2230
2080
  }
2231
- const images = this.#images.map(image => ({ ...image }));
2081
+ const images = this.#images.copies();
2232
2082
  const submittedText = images.length > 0 ? text.trim() : text;
2233
2083
  const queueEditNewer = this.#queueEditNewer;
2234
2084
  const historyText = stripComposerImageMarkers(submittedText, images)
@@ -2243,7 +2093,7 @@ export class LocalTui {
2243
2093
  this.#queueEditNewer = null;
2244
2094
  this.#queueEditPending = false;
2245
2095
  this.#editor.setText('');
2246
- this.#images = [];
2096
+ this.#images.clear();
2247
2097
  this.#ac = null;
2248
2098
  this.#search = null;
2249
2099
  this.#settings = null;
@@ -2279,7 +2129,7 @@ export class LocalTui {
2279
2129
  this.#render();
2280
2130
  }
2281
2131
  #submitInspect(text) {
2282
- const images = this.#images.map(image => ({ ...image }));
2132
+ const images = this.#images.copies();
2283
2133
  const submittedText = images.length > 0 ? text.trim() : text;
2284
2134
  const historyText = stripComposerImageMarkers(submittedText, images)
2285
2135
  .replace(/[ \t]{2,}/gu, ' ')
@@ -2291,7 +2141,7 @@ export class LocalTui {
2291
2141
  this.#historyIndex = 0;
2292
2142
  this.#draft = '';
2293
2143
  this.#editor.setText('');
2294
- this.#images = [];
2144
+ this.#images.clear();
2295
2145
  this.#ac = null;
2296
2146
  this.#search = null;
2297
2147
  this.#followTail();
@@ -2410,6 +2260,7 @@ export class LocalTui {
2410
2260
  if (pending === null)
2411
2261
  return;
2412
2262
  this.#prompt = null;
2263
+ this.#herdr.promptResolved();
2413
2264
  pending.offAbort?.();
2414
2265
  pending.resolve(answer);
2415
2266
  this.#restoreDisplacedSurface();
@@ -2486,7 +2337,7 @@ export class LocalTui {
2486
2337
  }
2487
2338
  this.#search = null;
2488
2339
  this.#ac = null;
2489
- this.#settings = createSettings(this.#prefs(), undefined, this.#agentBehavior);
2340
+ this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior);
2490
2341
  this.#render();
2491
2342
  }
2492
2343
  #notice(text) {
@@ -2559,7 +2410,7 @@ export class LocalTui {
2559
2410
  return true;
2560
2411
  }
2561
2412
  if (action === 'search-history') {
2562
- if (this.#images.length > 0)
2413
+ if (this.#images.count > 0)
2563
2414
  return false;
2564
2415
  this.#search = createHistorySearch(this.#history);
2565
2416
  this.#ac = null;
@@ -2571,7 +2422,7 @@ export class LocalTui {
2571
2422
  // transcript there; a non-empty draft keeps the editor's own binding.
2572
2423
  if (this.#search !== null)
2573
2424
  return false;
2574
- if (this.#editor.text !== '' || this.#images.length > 0)
2425
+ if (this.#editor.text !== '' || this.#images.count > 0)
2575
2426
  return false;
2576
2427
  this.#transcriptSearch = createTranscriptSearch();
2577
2428
  this.#ac = null;
@@ -2615,7 +2466,7 @@ export class LocalTui {
2615
2466
  this.#term.input.setRawMode?.(false);
2616
2467
  const text = editExternally(this.#editor.text);
2617
2468
  this.#editor.setText(text);
2618
- this.#reconcileImageDrafts();
2469
+ this.#images.reconcile();
2619
2470
  }
2620
2471
  catch (error) {
2621
2472
  this.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
@@ -2683,20 +2534,51 @@ export function apply(ctx, config) {
2683
2534
  return () => { process.stdout.removeListener('resize', listener); };
2684
2535
  },
2685
2536
  };
2686
- const tui = new LocalTui(term, config.model, config.colors, parseThemeName(config.theme), copyToClipboard, {
2537
+ const tui = new LocalTui(term, config.model, config.colors.get(), config.theme.get(), copyToClipboard, {
2687
2538
  deferInitialRender: true,
2688
2539
  terminalProfile,
2689
2540
  alternateScreenOverlays: terminalProfile === 'direct',
2541
+ herdrReporter: new HerdrAgentReporter(),
2690
2542
  historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
2691
2543
  keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),
2692
2544
  });
2693
2545
  ctx.provide(TUI_SERVICE, tui);
2694
2546
  ctx.effect(() => () => { tui.dispose(); });
2547
+ // Preferences are this entry's own volatile config: the initial read applies
2548
+ // them without persisting, a committed form edit dispatches a volatile update
2549
+ // to this fiber only, and an in-app `/settings` change writes back through the
2550
+ // settings service, which persists the edit into the active profile patch.
2551
+ const storedPrefs = () => {
2552
+ const prefs = {
2553
+ theme: config.theme.get(),
2554
+ colors: resolveColors(config.colors.get(), term.output.isTTY === true),
2555
+ motion: config.motion.get(),
2556
+ terminalProgress: config.terminalProgress.get(),
2557
+ expandTools: config.expandTools.get(),
2558
+ checkUpdates: config.checkUpdates.get(),
2559
+ startupChangelog: config.startupChangelog.get(),
2560
+ notifications: config.notifications.get(),
2561
+ notificationThreshold: config.notificationThreshold.get(),
2562
+ };
2563
+ const statusBar = config.statusBar.get();
2564
+ // The reference holds a frozen snapshot; the provider keeps a mutable copy.
2565
+ if (statusBar !== undefined)
2566
+ prefs.statusBar = structuredClone(statusBar);
2567
+ const statusPreset = config.statusPreset.get();
2568
+ if (statusPreset !== undefined)
2569
+ prefs.statusPreset = statusPreset;
2570
+ return prefs;
2571
+ };
2572
+ tui.applyStoredPrefs(storedPrefs());
2573
+ ctx.on('loader/volatile-update', () => { tui.applyStoredPrefs(storedPrefs()); });
2574
+ // Persistence needs the settings service; without one the provider still runs
2575
+ // and an in-app change stays session-local, as it did before.
2695
2576
  ctx.inject(['settings'], (settingsCtx) => {
2696
- const scope = settingsCtx.settings.register(TUI_SETTINGS_NAMESPACE, TuiSettingsSchema, { base: { theme: parseThemeName(config.theme), colors: resolveColors(config.colors, term.output.isTTY === true), expandTools: false } });
2697
- tui.applyStoredPrefs(scope.get());
2698
- tui.setPrefsPersist((prefs) => { void scope.update(prefs); });
2699
- scope.watch((next) => { tui.applyStoredPrefs(next); });
2577
+ tui.setPrefsPersist((prefs) => {
2578
+ const ops = Object.entries(prefs).map(([key, value]) => ({ op: 'set', path: [key], value }));
2579
+ void settingsCtx.settings.mutate(TUI_SETTINGS_ENTRY, ops)
2580
+ .catch((error) => { settingsCtx.logger.warn(error); });
2581
+ });
2700
2582
  });
2701
2583
  }
2702
2584
  //# sourceMappingURL=provider-local.js.map