@pellux/goodvibes-tui 1.0.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. 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,13 +22,15 @@ import {
22
22
  DEFAULT_PANEL_PALETTE,
23
23
  type PanelWorkspaceSection,
24
24
  } from './polish.ts';
25
- import { calcSessionCost } 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
 
29
29
  // Pricing lookups are provided by ../export/cost-utils.ts (single source of truth).
30
30
 
31
- function formatCost(usd: number): string {
31
+ /** unpriced=true renders an honest "unpriced" marker instead of a $0.00 that could be mistaken for a real zero cost. */
32
+ function formatCost(usd: number, unpriced = false): string {
33
+ if (unpriced) return 'unpriced';
32
34
  if (usd === 0) return '$0.00';
33
35
  if (usd < 0.0001) return '<$0.0001';
34
36
  if (usd < 0.01) return `$${usd.toFixed(4)}`;
@@ -106,9 +108,20 @@ export class CostTrackerPanel extends BasePanel {
106
108
 
107
109
  // Budget alert threshold in USD (0 = disabled). Mutable at runtime via the
108
110
  // in-panel 'b' numeric entry or the /cost budget <usd> command — both call
109
- // 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.
110
117
  private budgetThreshold: number;
111
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
+
112
125
  // Draft buffer for the in-panel budget-entry mode ('b' key). Non-null while
113
126
  // entry is active; unlike LocalAuthPanel's masked entry, the value is not
114
127
  // secret so it is echoed directly.
@@ -136,15 +149,27 @@ export class CostTrackerPanel extends BasePanel {
136
149
  turnEvents: UiEventFeed<TurnEvent>,
137
150
  agentEvents: UiEventFeed<AgentEvent>,
138
151
  getOrchestratorUsage: () => UsageSnapshot & { model?: string },
139
- opts: { budgetThreshold?: number; getAgentStatus?: (agentId: string) => AgentRecord | null } = {},
152
+ opts: {
153
+ budgetThreshold?: number;
154
+ getAgentStatus?: (agentId: string) => AgentRecord | null;
155
+ configAccess?: BudgetAlertConfigAccess;
156
+ } = {},
140
157
  ) {
141
158
  super('cost', 'Cost', '$', 'providers');
142
159
  this.getOrchestratorUsage = getOrchestratorUsage;
143
160
  this.getAgentStatus = opts.getAgentStatus;
144
- this.budgetThreshold = opts.budgetThreshold ?? 0;
161
+ this.configAccess = opts.configAccess;
162
+ this.budgetThreshold = this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : (opts.budgetThreshold ?? 0);
145
163
  this.attachEvents(turnEvents, agentEvents);
146
164
  }
147
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
+
148
173
  // -------------------------------------------------------------------------
149
174
  // Bus wiring
150
175
  // -------------------------------------------------------------------------
@@ -307,6 +332,7 @@ export class CostTrackerPanel extends BasePanel {
307
332
  public setBudgetThreshold(usd: number): void {
308
333
  if (!Number.isFinite(usd) || usd < 0) return;
309
334
  this.budgetThreshold = usd;
335
+ this.configAccess?.set('behavior.budgetAlertUsd', usd);
310
336
  this.markDirty();
311
337
  }
312
338
 
@@ -314,11 +340,22 @@ export class CostTrackerPanel extends BasePanel {
314
340
  // Input
315
341
  // -------------------------------------------------------------------------
316
342
 
343
+ /**
344
+ * The budget-threshold entry field wants every character of a burst
345
+ * (paste, or fast typing landing in one input.feed() call) delivered one
346
+ * at a time, same as it always has — see the interface doc on
347
+ * `Panel.isCapturingTextBurst`.
348
+ */
349
+ isCapturingTextBurst(): boolean {
350
+ return this.budgetEntry !== null;
351
+ }
352
+
317
353
  handleInput(key: string): boolean {
318
354
  if (this.budgetEntry !== null) return this.handleBudgetEntryInput(key);
319
355
 
320
356
  if (key === 'b') {
321
- this.budgetEntry = this.budgetThreshold > 0 ? String(this.budgetThreshold) : '';
357
+ const threshold = this.currentBudgetThreshold();
358
+ this.budgetEntry = threshold > 0 ? String(threshold) : '';
322
359
  this.markDirty();
323
360
  return true;
324
361
  }
@@ -393,12 +430,13 @@ export class CostTrackerPanel extends BasePanel {
393
430
 
394
431
  const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
395
432
  const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
396
- const overBudget = this.budgetThreshold > 0 && sessionCost > this.budgetThreshold;
433
+ const budgetThreshold = this.currentBudgetThreshold();
434
+ const overBudget = computeBudgetBreach(sessionCost, budgetThreshold);
397
435
  const sparkline = buildSparkline(this.costHistory);
398
- const costStr = formatCost(sessionCost);
436
+ const costStr = formatCost(sessionCost, !isModelPriced(this.sessionModel));
399
437
  const costFg = overBudget ? C.bad : C.cost;
400
- const budgetStr = this.budgetThreshold > 0
401
- ? ` / ${formatCost(this.budgetThreshold)}`
438
+ const budgetStr = budgetThreshold > 0
439
+ ? ` / ${formatCost(budgetThreshold)}`
402
440
  : '';
403
441
  const alertStr = overBudget ? ' ! OVER BUDGET' : '';
404
442
  const sessionLines: Line[] = [
@@ -407,8 +445,8 @@ export class CostTrackerPanel extends BasePanel {
407
445
  // Budget meter — the single most important glance for this panel: how much
408
446
  // of the configured budget the session has consumed. Only shown when a
409
447
  // budget is set (otherwise the bar would be meaningless).
410
- if (this.budgetThreshold > 0) {
411
- const ratio = sessionCost / this.budgetThreshold;
448
+ if (budgetThreshold > 0) {
449
+ const ratio = sessionCost / budgetThreshold;
412
450
  const BAR_W = 24;
413
451
  const filled = Math.max(0, Math.min(BAR_W, Math.round(ratio * BAR_W)));
414
452
  const meterFg = overBudget ? C.bad : ratio >= 0.8 ? C.warn : C.good;
@@ -451,10 +489,14 @@ export class CostTrackerPanel extends BasePanel {
451
489
  const planCost = agentList.reduce((sum, a) => sum + a.cost, 0);
452
490
  const running = agentList.filter((a) => a.status === 'running').length;
453
491
  const failed = agentList.filter((a) => a.status === 'failed').length;
492
+ // Plan total mixes the session model with every agent's model — flag it
493
+ // as unpriced (rather than showing a sum that silently omits unpriced
494
+ // agents' contribution) if any one of them has no real pricing.
495
+ const planHasUnpriced = !isModelPriced(this.sessionModel) || agentList.some((a) => !isModelPriced(a.model));
454
496
  const agentRows: Line[] = [
455
497
  buildStyledPanelLine(width, [
456
498
  { text: ' Plan total ', fg: C.label },
457
- { text: formatCost(planCost + sessionCost), fg: C.cost, bold: true },
499
+ { text: formatCost(planCost + sessionCost, planHasUnpriced), fg: C.cost, bold: true },
458
500
  { text: ` ${agentList.length} agent${agentList.length === 1 ? '' : 's'}`, fg: C.dim },
459
501
  ...(running > 0 ? [{ text: ` ${running} running`, fg: C.running }] : []),
460
502
  ...(failed > 0 ? [{ text: ` ${failed} failed`, fg: C.bad }] : []),
@@ -483,7 +525,14 @@ export class CostTrackerPanel extends BasePanel {
483
525
  { text: agent.model, fg: C.model },
484
526
  { text: agent.inputTokens > 0 ? formatTokens(agent.inputTokens) : '-', fg: C.dim },
485
527
  { text: agent.outputTokens > 0 ? formatTokens(agent.outputTokens) : '-', fg: C.dim },
486
- { text: agent.cost > 0 ? formatCost(agent.cost) : '-', fg: agent.cost > 0 ? C.cost : C.dim },
528
+ {
529
+ // '-' means no usage reported yet; 'unpriced' means usage
530
+ // exists but the model has no real price; otherwise the cost.
531
+ text: agent.inputTokens > 0 && !isModelPriced(agent.model)
532
+ ? 'unpriced'
533
+ : agent.cost > 0 ? formatCost(agent.cost) : '-',
534
+ fg: agent.cost > 0 ? C.cost : C.dim,
535
+ },
487
536
  ],
488
537
  };
489
538
  }),