@pellux/goodvibes-tui 1.1.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,120 +1,27 @@
1
- import { networkInterfaces } from 'node:os';
2
- import { readFileSync } from 'node:fs';
3
1
  import type { PanelManager } from '../panel-manager.ts';
4
- import { SessionBrowserPanel } from '../session-browser-panel.ts';
5
- import { QrPanel } from '../qr-panel.ts';
6
- import { DocsPanel } from '../docs-panel.ts';
7
- import { PanelListPanel } from '../panel-list-panel.ts';
8
2
  import { TokenBudgetPanel } from '../token-budget-panel.ts';
9
3
  import type { ResolvedBuiltinPanelDeps } from './shared.ts';
10
4
  import { requireUiServices } from './shared.ts';
11
- import {
12
- getOrCreateCompanionToken,
13
- regenerateCompanionToken,
14
- buildCompanionConnectionInfo,
15
- } from '@pellux/goodvibes-sdk/platform/pairing';
16
- import { copyToClipboard } from '../../utils/clipboard.ts';
17
-
18
- function getLocalNetworkIp(): string {
19
- const nets = networkInterfaces();
20
- for (const name of Object.keys(nets)) {
21
- for (const net of nets[name] ?? []) {
22
- if (net.family === 'IPv4' && !net.internal) {
23
- return net.address;
24
- }
25
- }
26
- }
27
- return 'localhost';
28
- }
29
-
30
- function readBootstrapPassword(credentialPath: string): string | undefined {
31
- try {
32
- const content = readFileSync(credentialPath, 'utf-8');
33
- for (const line of content.split('\n')) {
34
- if (line.startsWith('password=')) {
35
- return line.slice('password='.length).trim();
36
- }
37
- }
38
- } catch {
39
- // credential file may not exist yet
40
- }
41
- return undefined;
42
- }
43
5
 
6
+ // W6.1 (the purge): panel-list and system-messages were registered here
7
+ // before the purge — both DELETE-disposition. panel-list was a picker over a
8
+ // handful of panels (dead weight now that the registry is much smaller —
9
+ // the picker itself is replaced by a live-registry selection modal on
10
+ // Ctrl+P, see shell/ui-openers.ts). system-messages' buffered notices are
11
+ // rerouted to the transcript's system channel instead of vanishing: with no
12
+ // panel attached, SystemMessageRouter's delivery resolution
13
+ // (resolveSystemMessageDelivery, SDK) already falls back to
14
+ // conversation.addTypedSystemMessage for every kind/target combination — see
15
+ // bootstrap-shell.ts and core/system-message-router.ts.
16
+ //
17
+ // W6.1 (the purge) — group B (WO-P): 'qr-code', 'sessions', and 'docs' also
18
+ // migrated. 'qr-code' → the 'pairing-modal' surface; 'docs' → the
19
+ // 'keybindings-modal' surface (merged with the shortcuts-overlay content);
20
+ // 'sessions' folds into the existing session-picker modal ('sessions' redirects
21
+ // to 'sessionPicker'). Their surfaces AND panel→modal redirects are registered
22
+ // centrally in registerBuiltinModals (builtin-modals.ts). Only 'tokens' (KEEP)
23
+ // is still registered here.
44
24
  export function registerSessionPanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
45
- const ui = requireUiServices(deps);
46
- manager.registerType({
47
- id: 'qr-code',
48
- name: 'QR Code',
49
- icon: 'Q',
50
- category: 'session',
51
- description: 'QR code for companion app pairing — scan to connect a mobile or desktop companion',
52
- factory: () => {
53
- if (!deps.daemonHomeDir) throw new Error('daemonHomeDir must be provided to the session panel factory via BuiltinPanelDeps');
54
- const daemonHomeDir = deps.daemonHomeDir;
55
- const tokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
56
- const daemonPort = deps.configManager.get('controlPlane.port');
57
- const daemonHost = String(process.env['GOODVIBES_DAEMON_HOST'] ?? getLocalNetworkIp());
58
- const daemonUrl = `http://${daemonHost}:${daemonPort}`;
59
- const bootstrapPassword = readBootstrapPassword(deps.localUserAuthManager.getBootstrapCredentialPath());
60
- const connectionInfo = buildCompanionConnectionInfo({
61
- daemonUrl,
62
- token: tokenRecord.token,
63
- password: bootstrapPassword,
64
- surface: 'tui',
65
- });
66
- const regenerate = (): typeof connectionInfo => {
67
- const newRecord = regenerateCompanionToken({ daemonHomeDir });
68
- return buildCompanionConnectionInfo({
69
- daemonUrl,
70
- token: newRecord.token,
71
- password: bootstrapPassword,
72
- surface: 'tui',
73
- });
74
- };
75
- return new QrPanel(connectionInfo, regenerate, copyToClipboard, ui.readModels.controlPlane, deps.localUserAuthManager);
76
- },
77
- });
78
-
79
- manager.registerType({
80
- id: 'sessions',
81
- name: 'Sessions',
82
- icon: 'H',
83
- category: 'session',
84
- description: 'Browse, search, and resume past conversation sessions',
85
- factory: () => new SessionBrowserPanel(deps.sessionManager, deps.resumeSession),
86
- });
87
-
88
- manager.registerType({
89
- id: 'docs',
90
- name: 'Docs',
91
- icon: '?',
92
- category: 'session',
93
- description: 'Tool list, model capabilities, and keyboard shortcut reference',
94
- factory: () => new DocsPanel(deps.toolRegistry, deps.providerRegistry, requireUiServices(deps).shell.keybindingsManager),
95
- });
96
-
97
- manager.registerType({
98
- id: 'panel-list',
99
- name: 'Panel List',
100
- icon: 'L',
101
- category: 'session',
102
- description: 'Browse all registered panels grouped by category, with open/closed status and Enter-to-open',
103
- factory: () => new PanelListPanel(manager, deps.componentHealthMonitor),
104
- });
105
-
106
- manager.registerType({
107
- id: 'system-messages',
108
- name: 'System Messages',
109
- // WO-152: was 'J' (collided with intelligence + tasks).
110
- icon: '▥',
111
- category: 'runtime-ops',
112
- description: 'Operational system messages routed away from the main conversation (scans, discovery, plugin events, tool status)',
113
- preload: true,
114
- retainOnClose: true,
115
- factory: () => deps.systemMessagesPanel,
116
- });
117
-
118
25
  manager.registerType({
119
26
  id: 'tokens',
120
27
  name: 'Tokens',
@@ -127,7 +34,9 @@ export function registerSessionPanels(manager: PanelManager, deps: ResolvedBuilt
127
34
  description: 'Token + context console: gauge, true composition, per-turn history, inline cost, and one-key compact',
128
35
  // Preloaded (absorbed from the retired ContextVisualizerPanel) so turn
129
36
  // history and pressure accumulate in the background even before the user
130
- // opens the tab.
37
+ // opens the tab. The only builtin panel that still preloads post-purge
38
+ // (W6.1) — see registerBuiltinPanels callers for the others' preload
39
+ // removal.
131
40
  preload: true,
132
41
  retainOnClose: true,
133
42
  factory: () => {
@@ -4,7 +4,8 @@ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
4
4
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
5
5
  import type { Orchestrator } from '../../core/orchestrator';
6
6
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
7
- import type { ApprovalBroker, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
7
+ import type { ApprovalBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
8
+ import type { SessionReadFacade } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
8
9
  import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
9
10
  import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/control-plane';
10
11
  import type { UiRuntimeServices } from '../../runtime/ui-services.ts';
@@ -70,16 +71,27 @@ export interface BuiltinPanelDeps {
70
71
  dismissPlanning?: () => void;
71
72
  /** ForensicsRegistry for the Forensics panel. */
72
73
  forensicsRegistry?: import('@/runtime/index.ts').ForensicsRegistry;
73
- /** EvalRegistry for the Eval panel. */
74
- evalRegistry?: import('../eval-panel.ts').EvalRegistry;
74
+ /**
75
+ * EvalRegistry for the `/eval` command surface. W6.1: 'eval' the panel was
76
+ * deleted (DELETE-disposition), but this field is left in place — no
77
+ * builtin panel factory reads it anymore, and it was never wired at
78
+ * bootstrap in production either way (the eval CLI command reads its own
79
+ * copy via CommandContext.extensions.evalRegistry).
80
+ */
81
+ evalRegistry?: import('../eval-registry.ts').EvalRegistry;
75
82
  /** MemoryRegistry for the Memory panel. */
76
83
  memoryRegistry?: MemoryRegistry;
77
84
  /** Shared policy runtime state for governance/policy diagnostics. */
78
85
  policyRuntimeState?: import('@/runtime/index.ts').PolicyRuntimeState;
79
86
  /** Approval broker for control-plane/operator panels. */
80
87
  approvalBroker?: ApprovalBroker;
81
- /** Shared session broker for control-plane/operator panels. */
82
- sessionBroker?: SharedSessionBroker;
88
+ /**
89
+ * S3d: cross-surface session READ facade for control-plane/operator panels.
90
+ * Sync listSessions()/getSession() shape preserved; in adopted-daemon mode it
91
+ * serves the daemon-hosted union (with an honest offline note when the wire is
92
+ * down) instead of only this process's local broker. See session-union-cache.ts.
93
+ */
94
+ sessionBroker?: SessionReadFacade;
83
95
  /** Automation manager for schedule/operator panels. */
84
96
  automationManager?: AutomationManager;
85
97
  /** Recent control-plane events provider for control-plane/operator panels. */
@@ -109,8 +121,6 @@ export interface BuiltinPanelDeps {
109
121
  projectPlanningProjectId?: string;
110
122
  /** TUI-owned persistent work plan store. */
111
123
  workPlanStore?: import('../../work-plans/work-plan-store.ts').WorkPlanStore;
112
- /** Shared system-messages panel instance attached from boot so low-priority chatter stays out of conversation. */
113
- systemMessagesPanel?: import('../system-messages-panel.ts').SystemMessagesPanel;
114
124
  /** Explicit UI-facing runtime services for agent/process/WRFC/remote panels and modals. */
115
125
  uiServices?: UiRuntimeServices;
116
126
  /** Shared plugin manager for plugin and security panels (widened past the read-only observer surface — WO-134 — so PluginsPanel can drive enable/disable/verify/lift-quarantine). */
@@ -137,17 +147,9 @@ export interface BuiltinPanelDeps {
137
147
  sessionChangeTracker?: Pick<SessionChangeTracker, 'getChangedFiles'>;
138
148
  /**
139
149
  * Open (or focus) a panel by id, wrapping `PanelManager.open`. Use for direct
140
- * cross-panel navigation instead of printing a "/panel open …" signpost
141
- * (mirrors the openAgentDetail callback below).
150
+ * cross-panel navigation instead of printing a "/panel open …" signpost.
142
151
  */
143
152
  openPanel?: (panelId: string) => void;
144
- /**
145
- * Open the agent detail modal for the given agent id. Wired from
146
- * InputHandler.agentDetailModal.open() at bootstrap — passed to the
147
- * CockpitPanel factory so the agents workspace inspect key (i) works
148
- * without the panel depending on the modal directly.
149
- */
150
- openAgentDetail?: (agentId: string) => void;
151
153
  }
152
154
 
153
155
  export type ResolvedBuiltinPanelDeps = Omit<
@@ -164,7 +166,6 @@ export type ResolvedBuiltinPanelDeps = Omit<
164
166
  | 'projectPlanningProjectId'
165
167
  | 'workPlanStore'
166
168
  | 'policyRuntimeState'
167
- | 'systemMessagesPanel'
168
169
  > & {
169
170
  readonly configManager: ConfigManager;
170
171
  readonly localUserAuthManager: UserAuthManager;
@@ -178,15 +179,8 @@ export type ResolvedBuiltinPanelDeps = Omit<
178
179
  readonly projectPlanningProjectId: string;
179
180
  readonly workPlanStore: import('../../work-plans/work-plan-store.ts').WorkPlanStore;
180
181
  readonly policyRuntimeState: PolicyRuntimeState;
181
- readonly systemMessagesPanel: import('../system-messages-panel.ts').SystemMessagesPanel;
182
182
  };
183
183
 
184
- export interface ControlPlanePanelFactoryDeps {
185
- readonly approvalBroker: ApprovalBroker;
186
- readonly sessionBroker: SharedSessionBroker;
187
- readonly getControlPlaneRecentEvents: (limit: number) => readonly ControlPlaneRecentEvent[];
188
- }
189
-
190
184
  function requireBuiltinPanelDep<TValue>(value: TValue | undefined, message: string): TValue {
191
185
  if (value === undefined) {
192
186
  throw new Error(message);
@@ -246,31 +240,9 @@ export function resolveBuiltinPanelDeps(deps: BuiltinPanelDeps): ResolvedBuiltin
246
240
  uiServices.platform.policyRuntimeState,
247
241
  'Policy runtime state must be wired at bootstrap for builtin panels.',
248
242
  ),
249
- systemMessagesPanel: requireBuiltinPanelDep(
250
- deps.systemMessagesPanel,
251
- 'System messages panel must be wired at bootstrap for builtin panels.',
252
- ),
253
243
  };
254
244
  }
255
245
 
256
- export function requireControlPlanePanelDeps(deps: BuiltinPanelDeps): ControlPlanePanelFactoryDeps {
257
- if (!deps.approvalBroker || !deps.sessionBroker || !deps.getControlPlaneRecentEvents) {
258
- throw new Error('ControlPlanePanel requires approval/session brokers and recent-event access to be wired at bootstrap.');
259
- }
260
- return {
261
- approvalBroker: deps.approvalBroker,
262
- sessionBroker: deps.sessionBroker,
263
- getControlPlaneRecentEvents: deps.getControlPlaneRecentEvents,
264
- };
265
- }
266
-
267
- export function requireAutomationManager(deps: BuiltinPanelDeps): AutomationManager {
268
- if (!deps.automationManager) {
269
- throw new Error('AutomationControlPanel requires an automation manager to be wired at bootstrap.');
270
- }
271
- return deps.automationManager;
272
- }
273
-
274
246
  export function requireUiServices(deps: BuiltinPanelDeps): UiRuntimeServices {
275
247
  if (!deps.uiServices) {
276
248
  throw new Error('UI runtime services must be wired at bootstrap for agent, process, WRFC, and remote panels.');
@@ -0,0 +1,218 @@
1
+ import { networkInterfaces } from 'node:os';
2
+ import { readFileSync } from 'node:fs';
3
+ import type { PanelManager } from './panel-manager.ts';
4
+ import { requireUiServices, type ResolvedBuiltinPanelDeps } from './builtin/shared.ts';
5
+ import type { ConfigModalSurface, ConfigModalView } from '../input/config-modal-types.ts';
6
+ import { createProviderRuntimeInspectionQuery } from '../runtime/ui-service-queries.ts';
7
+ import { createRuntimeProviderApi } from '@/runtime/index.ts';
8
+ import { copyToClipboard } from '../utils/clipboard.ts';
9
+ import { getOrCreateCompanionToken, buildCompanionConnectionInfo } from '@pellux/goodvibes-sdk/platform/pairing';
10
+ // ── Group A: Providers & Connectivity + Security subset (WO-A) ───────────────
11
+ import { createServicesModalSurface } from './modals/services-modal.ts';
12
+ import { createSubscriptionModalSurface } from './modals/subscription-modal.ts';
13
+ import { createRemoteModalSurface } from './modals/remote-modal.ts';
14
+ import { createSettingsSyncModalSurface } from './modals/settings-sync-modal.ts';
15
+ import { createProviderHealthModalSurface } from './modals/provider-health-modal.ts';
16
+ import { createLocalAuthModalSurface } from './modals/local-auth-modal.ts';
17
+ import { createSandboxModalSurface } from './modals/sandbox-modal.ts';
18
+ // ── Group B: Ecosystem & Governance (WO-P, this work order) ──────────────────
19
+ import { createMarketplaceModalSurface } from './modals/marketplace-modal.ts';
20
+ import { createPluginsModalSurface } from './modals/plugins-modal.ts';
21
+ import { createSkillsModalSurface } from './modals/skills-modal.ts';
22
+ import { createHooksModalSurface } from './modals/hooks-modal.ts';
23
+ import { createSecurityModalSurface } from './modals/security-modal.ts';
24
+ import { createPolicyModalSurface } from './modals/policy-modal.ts';
25
+ import { createKnowledgeModalSurface } from './modals/knowledge-modal.ts';
26
+ import { createMemoryModalSurface, type MemoryModalDeps } from './modals/memory-modal.ts';
27
+ import { createWorkPlanModalSurface } from './modals/work-plan-modal.ts';
28
+ import { createKeybindingsModalSurface } from './modals/keybindings-modal.ts';
29
+ import { createPairingModalSurface, type PairingModalConnectionInfo } from './modals/pairing-modal.ts';
30
+ import { createPlanningModalSurface } from './modals/planning-modal.ts';
31
+
32
+ /**
33
+ * Register the config-modal surfaces + their panel-id redirects (W6.1, the
34
+ * purge). Called once at startup from registerBuiltinPanels, AFTER the panels'
35
+ * deps are resolved (the surfaces close over the same read-models the retired
36
+ * panels used). For each MIGRATE-TO-MODAL surface this does two things:
37
+ * 1. registerModalSurface — the data + actions the config-modal host renders.
38
+ * 2. registerModalRedirect — so `/panel open <old-id>`, saved layouts, and any
39
+ * alias still resolve to the modal.
40
+ *
41
+ * Group A (WO-A) is the Providers & Connectivity + Security subset. Group B
42
+ * (WO-P) is the Ecosystem & Governance set — the 12 ported config-modal
43
+ * surfaces plus the `sessions` fold into the existing session-picker modal.
44
+ */
45
+ export function registerBuiltinModals(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
46
+ const ui = requireUiServices(deps);
47
+
48
+ // ── Providers & Connectivity (WO-A) ─────────────────────────────────────────
49
+ manager.registerModalSurface(createServicesModalSurface(deps.serviceRegistry, deps.subscriptionManager));
50
+ manager.registerModalRedirect('services', 'services-modal');
51
+
52
+ manager.registerModalSurface(createSubscriptionModalSurface(deps.serviceRegistry, deps.subscriptionManager));
53
+ manager.registerModalRedirect('subscription', 'subscription-modal');
54
+
55
+ manager.registerModalSurface(createRemoteModalSurface(ui.readModels.remote));
56
+ manager.registerModalRedirect('remote', 'remote-modal');
57
+
58
+ const providerRuntime = createProviderRuntimeInspectionQuery(createRuntimeProviderApi({
59
+ benchmarkStore: ui.providers.benchmarkStore,
60
+ favoritesStore: ui.providers.favoritesStore,
61
+ providerRegistry: ui.providers.providerRegistry,
62
+ }));
63
+ manager.registerModalSurface(createProviderHealthModalSurface(providerRuntime, ui.readModels.providers));
64
+ manager.registerModalRedirect('provider-health', 'providers-modal');
65
+ manager.registerModalRedirect('providers', 'providers-modal');
66
+ manager.registerModalRedirect('accounts', 'providers-modal');
67
+
68
+ // ── Security & Governance (WO-A subset) ─────────────────────────────────────
69
+ manager.registerModalSurface(createSettingsSyncModalSurface(deps.configManager));
70
+ manager.registerModalRedirect('settings-sync', 'settings-sync-modal');
71
+
72
+ // local-auth-modal is reached via the /local-auth front-door only; no
73
+ // redirect (the 'local-auth' panel id must keep resolving to LocalAuthPanel
74
+ // for the masked password-entry path). See builtin/operations.ts.
75
+ manager.registerModalSurface(createLocalAuthModalSurface(deps.localUserAuthManager));
76
+
77
+ manager.registerModalSurface(createSandboxModalSurface(deps.configManager, deps.sandboxSessionRegistry, deps.requestRender));
78
+ manager.registerModalRedirect('sandbox', 'sandbox-modal');
79
+
80
+ // ── Ecosystem & Governance (WO-P — group B) ─────────────────────────────────
81
+ manager.registerModalSurface(createMarketplaceModalSurface({
82
+ readModel: ui.readModels.marketplace,
83
+ ecosystemPaths: {
84
+ cwd: ui.environment.shellPaths.workingDirectory,
85
+ homeDir: ui.environment.shellPaths.homeDirectory,
86
+ projectCatalogRoot: ui.environment.shellPaths.resolveProjectPath('tui', 'ecosystem'),
87
+ userCatalogRoot: ui.environment.shellPaths.resolveUserPath('tui', 'ecosystem'),
88
+ },
89
+ }));
90
+ manager.registerModalRedirect('marketplace', 'marketplace-modal');
91
+
92
+ // plugins/hooks/knowledge deps are wired at bootstrap in production
93
+ // (bootstrap-shell.ts) but may be absent in a partially-wired context (e.g. a
94
+ // release-gate harness) — register a degraded placeholder rather than throw,
95
+ // so the surface + redirect always resolve (the "always register, degrade
96
+ // honestly" charter pattern).
97
+ manager.registerModalSurface(deps.pluginManager
98
+ ? createPluginsModalSurface({ pluginManager: deps.pluginManager })
99
+ : unwiredSurface('plugins-modal', 'Plugins', 'Plugin manager not wired into this session.'));
100
+ manager.registerModalRedirect('plugins', 'plugins-modal');
101
+
102
+ manager.registerModalSurface(createSkillsModalSurface({
103
+ shellPaths: {
104
+ workingDirectory: ui.environment.shellPaths.workingDirectory,
105
+ homeDirectory: ui.environment.shellPaths.homeDirectory,
106
+ },
107
+ ecosystemPaths: {
108
+ cwd: ui.environment.shellPaths.workingDirectory,
109
+ homeDir: ui.environment.shellPaths.homeDirectory,
110
+ projectCatalogRoot: ui.environment.shellPaths.resolveProjectPath('tui', 'ecosystem'),
111
+ userCatalogRoot: ui.environment.shellPaths.resolveUserPath('tui', 'ecosystem'),
112
+ },
113
+ }));
114
+ manager.registerModalRedirect('skills', 'skills-modal');
115
+
116
+ manager.registerModalSurface(deps.hookDispatcher && deps.hookWorkbench && deps.hookActivityTracker
117
+ ? createHooksModalSurface({ hookDispatcher: deps.hookDispatcher, hookWorkbench: deps.hookWorkbench, hookActivityTracker: deps.hookActivityTracker })
118
+ : unwiredSurface('hooks-modal', 'Hooks', 'Hook dispatcher/workbench not wired into this session.'));
119
+ manager.registerModalRedirect('hooks', 'hooks-modal');
120
+
121
+ manager.registerModalSurface(createSecurityModalSurface({ readModel: ui.readModels.security }));
122
+ manager.registerModalRedirect('security', 'security-modal');
123
+
124
+ manager.registerModalSurface(createPolicyModalSurface({ policyRuntimeState: deps.policyRuntimeState }));
125
+ manager.registerModalRedirect('policy', 'policy-modal');
126
+
127
+ manager.registerModalSurface(deps.knowledgeApi
128
+ ? createKnowledgeModalSurface({ knowledgeApi: deps.knowledgeApi })
129
+ : unwiredSurface('knowledge-modal', 'Knowledge', 'Knowledge API not wired into this session.'));
130
+ manager.registerModalRedirect('knowledge', 'knowledge-modal');
131
+
132
+ manager.registerModalSurface(createMemoryModalSurface({
133
+ memoryRegistry: deps.memoryRegistry as MemoryModalDeps['memoryRegistry'],
134
+ }));
135
+ manager.registerModalRedirect('memory', 'memory-modal');
136
+
137
+ manager.registerModalSurface(createWorkPlanModalSurface({ workPlanStore: deps.workPlanStore }));
138
+ manager.registerModalRedirect('work-plan', 'work-plan-modal');
139
+
140
+ manager.registerModalSurface(createKeybindingsModalSurface({
141
+ toolRegistry: deps.toolRegistry,
142
+ providerRegistry: deps.providerRegistry,
143
+ keybindingsManager: ui.shell.keybindingsManager,
144
+ }));
145
+ manager.registerModalRedirect('docs', 'keybindings-modal');
146
+
147
+ manager.registerModalSurface(createPairingModalSurface({
148
+ getConnectionInfo: () => buildPairingConnectionInfo(deps),
149
+ controlPlaneReadModel: ui.readModels.controlPlane,
150
+ copyToClipboard,
151
+ }));
152
+ manager.registerModalRedirect('qr-code', 'pairing-modal');
153
+
154
+ manager.registerModalSurface(createPlanningModalSurface({
155
+ service: deps.projectPlanningService,
156
+ projectId: deps.projectPlanningProjectId,
157
+ requestRender: deps.requestRender,
158
+ }));
159
+ manager.registerModalRedirect('project-planning', 'planning-modal');
160
+
161
+ // The retired 'sessions' panel folds into the EXISTING session-picker modal
162
+ // (no new config-modal surface); the redirect is a plain hand-off, moved here
163
+ // from the deleted ecosystem bridge.
164
+ manager.registerModalRedirect('sessions', 'sessionPicker');
165
+ }
166
+
167
+ /**
168
+ * Build the companion-pairing connection info lazily (called from the pairing
169
+ * surface's first render, not at registration time) so a missing daemon home
170
+ * degrades to an honest "unavailable" modal instead of throwing at startup.
171
+ * Mirrors the retired QrPanel factory's construction.
172
+ */
173
+ function buildPairingConnectionInfo(deps: ResolvedBuiltinPanelDeps): PairingModalConnectionInfo | null {
174
+ if (!deps.daemonHomeDir) return null;
175
+ try {
176
+ const daemonHomeDir = deps.daemonHomeDir;
177
+ const tokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
178
+ const daemonPort = deps.configManager.get('controlPlane.port');
179
+ const daemonHost = String(process.env['GOODVIBES_DAEMON_HOST'] ?? getLocalNetworkIp());
180
+ const daemonUrl = `http://${daemonHost}:${daemonPort}`;
181
+ const bootstrapPassword = readBootstrapPassword(deps.localUserAuthManager.getBootstrapCredentialPath());
182
+ return buildCompanionConnectionInfo({ daemonUrl, token: tokenRecord.token, password: bootstrapPassword, surface: 'tui' }) as PairingModalConnectionInfo;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ /** A minimal config-modal surface that renders an honest "not wired" degraded
189
+ * state — used when a group-B surface's bootstrap dependency is absent. */
190
+ function unwiredSurface(name: string, title: string, reason: string): ConfigModalSurface {
191
+ return {
192
+ name,
193
+ title,
194
+ buildView: (): ConfigModalView => ({ title, degraded: reason, tabs: [{ id: 'main', label: title, rows: [] }] }),
195
+ };
196
+ }
197
+
198
+ function getLocalNetworkIp(): string {
199
+ const nets = networkInterfaces();
200
+ for (const name of Object.keys(nets)) {
201
+ for (const net of nets[name] ?? []) {
202
+ if (net.family === 'IPv4' && !net.internal) return net.address;
203
+ }
204
+ }
205
+ return 'localhost';
206
+ }
207
+
208
+ function readBootstrapPassword(credentialPath: string): string | undefined {
209
+ try {
210
+ const content = readFileSync(credentialPath, 'utf-8');
211
+ for (const line of content.split('\n')) {
212
+ if (line.startsWith('password=')) return line.slice('password='.length).trim();
213
+ }
214
+ } catch {
215
+ // credential file may not exist yet
216
+ }
217
+ return undefined;
218
+ }
@@ -5,6 +5,7 @@ import { registerOperationsPanels } from './builtin/operations.ts';
5
5
  import { registerAgentPanels } from './builtin/agent.ts';
6
6
  import { registerSessionPanels } from './builtin/session.ts';
7
7
  import { registerKnowledgePanels } from './builtin/knowledge.ts';
8
+ import { registerBuiltinModals } from './builtin-modals.ts';
8
9
 
9
10
  /**
10
11
  * Register all built-in panel types with the given PanelManager.
@@ -18,6 +19,10 @@ export function registerBuiltinPanels(manager: PanelManager, deps: BuiltinPanelD
18
19
  registerAgentPanels(manager, resolved);
19
20
  registerSessionPanels(manager, resolved);
20
21
  registerKnowledgePanels(manager, resolved);
22
+ // W6.1 (the purge): register MIGRATE-TO-MODAL surfaces + their panel-id
23
+ // redirects. Must run after the panel registrations so a redirected id wins
24
+ // over any lingering panel registration in open().
25
+ registerBuiltinModals(manager, resolved);
21
26
  }
22
27
 
23
28
  export type { BuiltinPanelDeps } from './builtin/shared.ts';
@@ -22,7 +22,7 @@ import {
22
22
  DEFAULT_PANEL_PALETTE,
23
23
  type PanelWorkspaceSection,
24
24
  } from './polish.ts';
25
- import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
25
+ import { calcSessionCost, isModelPriced, computeBudgetBreach, readBudgetAlertUsd, type BudgetAlertConfigAccess } from '../export/cost-utils.ts';
26
26
  import { abbreviateCount } from '../utils/format-number.ts';
27
27
  import { isTextBackspace } from '../input/delete-key-policy.ts';
28
28
 
@@ -108,9 +108,20 @@ export class CostTrackerPanel extends BasePanel {
108
108
 
109
109
  // Budget alert threshold in USD (0 = disabled). Mutable at runtime via the
110
110
  // in-panel 'b' numeric entry or the /cost budget <usd> command — both call
111
- // setBudgetThreshold() below.
111
+ // setBudgetThreshold() below. When configAccess is wired (production path),
112
+ // this field is a cache only — currentBudgetThreshold() re-reads
113
+ // behavior.budgetAlertUsd from config on every use so the panel and the
114
+ // background budget-breach notifier (core/budget-breach-notifier.ts) never
115
+ // disagree about the threshold. Tests that omit configAccess fall back to
116
+ // this field as the sole source of truth, unchanged from prior behavior.
112
117
  private budgetThreshold: number;
113
118
 
119
+ // Optional config-backed source of truth for the budget threshold. Absent
120
+ // in most existing unit tests (which construct the panel directly) and in
121
+ // any build that doesn't wire a config manager; present in production via
122
+ // builtin/development.ts.
123
+ private readonly configAccess: BudgetAlertConfigAccess | undefined;
124
+
114
125
  // Draft buffer for the in-panel budget-entry mode ('b' key). Non-null while
115
126
  // entry is active; unlike LocalAuthPanel's masked entry, the value is not
116
127
  // secret so it is echoed directly.
@@ -138,15 +149,27 @@ export class CostTrackerPanel extends BasePanel {
138
149
  turnEvents: UiEventFeed<TurnEvent>,
139
150
  agentEvents: UiEventFeed<AgentEvent>,
140
151
  getOrchestratorUsage: () => UsageSnapshot & { model?: string },
141
- opts: { budgetThreshold?: number; getAgentStatus?: (agentId: string) => AgentRecord | null } = {},
152
+ opts: {
153
+ budgetThreshold?: number;
154
+ getAgentStatus?: (agentId: string) => AgentRecord | null;
155
+ configAccess?: BudgetAlertConfigAccess;
156
+ } = {},
142
157
  ) {
143
158
  super('cost', 'Cost', '$', 'providers');
144
159
  this.getOrchestratorUsage = getOrchestratorUsage;
145
160
  this.getAgentStatus = opts.getAgentStatus;
146
- this.budgetThreshold = opts.budgetThreshold ?? 0;
161
+ this.configAccess = opts.configAccess;
162
+ this.budgetThreshold = this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : (opts.budgetThreshold ?? 0);
147
163
  this.attachEvents(turnEvents, agentEvents);
148
164
  }
149
165
 
166
+ /** The live budget threshold: re-read from config when configAccess is wired
167
+ * (single source of truth shared with the background notifier), otherwise
168
+ * the locally-cached field (test/no-config-manager fallback). */
169
+ private currentBudgetThreshold(): number {
170
+ return this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : this.budgetThreshold;
171
+ }
172
+
150
173
  // -------------------------------------------------------------------------
151
174
  // Bus wiring
152
175
  // -------------------------------------------------------------------------
@@ -309,6 +332,7 @@ export class CostTrackerPanel extends BasePanel {
309
332
  public setBudgetThreshold(usd: number): void {
310
333
  if (!Number.isFinite(usd) || usd < 0) return;
311
334
  this.budgetThreshold = usd;
335
+ this.configAccess?.set('behavior.budgetAlertUsd', usd);
312
336
  this.markDirty();
313
337
  }
314
338
 
@@ -330,7 +354,8 @@ export class CostTrackerPanel extends BasePanel {
330
354
  if (this.budgetEntry !== null) return this.handleBudgetEntryInput(key);
331
355
 
332
356
  if (key === 'b') {
333
- this.budgetEntry = this.budgetThreshold > 0 ? String(this.budgetThreshold) : '';
357
+ const threshold = this.currentBudgetThreshold();
358
+ this.budgetEntry = threshold > 0 ? String(threshold) : '';
334
359
  this.markDirty();
335
360
  return true;
336
361
  }
@@ -405,12 +430,13 @@ export class CostTrackerPanel extends BasePanel {
405
430
 
406
431
  const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
407
432
  const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
408
- const overBudget = this.budgetThreshold > 0 && sessionCost > this.budgetThreshold;
433
+ const budgetThreshold = this.currentBudgetThreshold();
434
+ const overBudget = computeBudgetBreach(sessionCost, budgetThreshold);
409
435
  const sparkline = buildSparkline(this.costHistory);
410
436
  const costStr = formatCost(sessionCost, !isModelPriced(this.sessionModel));
411
437
  const costFg = overBudget ? C.bad : C.cost;
412
- const budgetStr = this.budgetThreshold > 0
413
- ? ` / ${formatCost(this.budgetThreshold)}`
438
+ const budgetStr = budgetThreshold > 0
439
+ ? ` / ${formatCost(budgetThreshold)}`
414
440
  : '';
415
441
  const alertStr = overBudget ? ' ! OVER BUDGET' : '';
416
442
  const sessionLines: Line[] = [
@@ -419,8 +445,8 @@ export class CostTrackerPanel extends BasePanel {
419
445
  // Budget meter — the single most important glance for this panel: how much
420
446
  // of the configured budget the session has consumed. Only shown when a
421
447
  // budget is set (otherwise the bar would be meaningless).
422
- if (this.budgetThreshold > 0) {
423
- const ratio = sessionCost / this.budgetThreshold;
448
+ if (budgetThreshold > 0) {
449
+ const ratio = sessionCost / budgetThreshold;
424
450
  const BAR_W = 24;
425
451
  const filled = Math.max(0, Math.min(BAR_W, Math.round(ratio * BAR_W)));
426
452
  const meterFg = overBudget ? C.bad : ratio >= 0.8 ? C.warn : C.good;