@pellux/goodvibes-tui 1.1.0 → 1.7.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. 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: () => {
@@ -70,8 +70,14 @@ export interface BuiltinPanelDeps {
70
70
  dismissPlanning?: () => void;
71
71
  /** ForensicsRegistry for the Forensics panel. */
72
72
  forensicsRegistry?: import('@/runtime/index.ts').ForensicsRegistry;
73
- /** EvalRegistry for the Eval panel. */
74
- evalRegistry?: import('../eval-panel.ts').EvalRegistry;
73
+ /**
74
+ * EvalRegistry for the `/eval` command surface. W6.1: 'eval' the panel was
75
+ * deleted (DELETE-disposition), but this field is left in place — no
76
+ * builtin panel factory reads it anymore, and it was never wired at
77
+ * bootstrap in production either way (the eval CLI command reads its own
78
+ * copy via CommandContext.extensions.evalRegistry).
79
+ */
80
+ evalRegistry?: import('../eval-registry.ts').EvalRegistry;
75
81
  /** MemoryRegistry for the Memory panel. */
76
82
  memoryRegistry?: MemoryRegistry;
77
83
  /** Shared policy runtime state for governance/policy diagnostics. */
@@ -109,8 +115,6 @@ export interface BuiltinPanelDeps {
109
115
  projectPlanningProjectId?: string;
110
116
  /** TUI-owned persistent work plan store. */
111
117
  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
118
  /** Explicit UI-facing runtime services for agent/process/WRFC/remote panels and modals. */
115
119
  uiServices?: UiRuntimeServices;
116
120
  /** 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 +141,9 @@ export interface BuiltinPanelDeps {
137
141
  sessionChangeTracker?: Pick<SessionChangeTracker, 'getChangedFiles'>;
138
142
  /**
139
143
  * 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).
144
+ * cross-panel navigation instead of printing a "/panel open …" signpost.
142
145
  */
143
146
  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
147
  }
152
148
 
153
149
  export type ResolvedBuiltinPanelDeps = Omit<
@@ -164,7 +160,6 @@ export type ResolvedBuiltinPanelDeps = Omit<
164
160
  | 'projectPlanningProjectId'
165
161
  | 'workPlanStore'
166
162
  | 'policyRuntimeState'
167
- | 'systemMessagesPanel'
168
163
  > & {
169
164
  readonly configManager: ConfigManager;
170
165
  readonly localUserAuthManager: UserAuthManager;
@@ -178,15 +173,8 @@ export type ResolvedBuiltinPanelDeps = Omit<
178
173
  readonly projectPlanningProjectId: string;
179
174
  readonly workPlanStore: import('../../work-plans/work-plan-store.ts').WorkPlanStore;
180
175
  readonly policyRuntimeState: PolicyRuntimeState;
181
- readonly systemMessagesPanel: import('../system-messages-panel.ts').SystemMessagesPanel;
182
176
  };
183
177
 
184
- export interface ControlPlanePanelFactoryDeps {
185
- readonly approvalBroker: ApprovalBroker;
186
- readonly sessionBroker: SharedSessionBroker;
187
- readonly getControlPlaneRecentEvents: (limit: number) => readonly ControlPlaneRecentEvent[];
188
- }
189
-
190
178
  function requireBuiltinPanelDep<TValue>(value: TValue | undefined, message: string): TValue {
191
179
  if (value === undefined) {
192
180
  throw new Error(message);
@@ -246,31 +234,9 @@ export function resolveBuiltinPanelDeps(deps: BuiltinPanelDeps): ResolvedBuiltin
246
234
  uiServices.platform.policyRuntimeState,
247
235
  'Policy runtime state must be wired at bootstrap for builtin panels.',
248
236
  ),
249
- systemMessagesPanel: requireBuiltinPanelDep(
250
- deps.systemMessagesPanel,
251
- 'System messages panel must be wired at bootstrap for builtin panels.',
252
- ),
253
237
  };
254
238
  }
255
239
 
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
240
  export function requireUiServices(deps: BuiltinPanelDeps): UiRuntimeServices {
275
241
  if (!deps.uiServices) {
276
242
  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;