@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
@@ -5,19 +5,26 @@
5
5
  * Delivery is resolved from the message KIND ('system' | 'wrfc' |
6
6
  * 'operational'), which maps to a configurable routing target
7
7
  * (ui.systemMessages / ui.wrfcMessages / ui.operationalMessages, each
8
- * 'panel' | 'conversation' | 'both'). resolveSystemMessageDelivery() turns that
9
- * target — plus whether a panel is attached — into a { toPanel, toConversation }
10
- * decision. The priority ('high' | 'low') only sets the panel emphasis; it does
11
- * not by itself decide conversation delivery.
8
+ * 'panel' | 'conversation' | 'both'). resolveSystemMessageDelivery() turns
9
+ * that target — plus whether a panel is attached — into a { toPanel,
10
+ * toConversation } decision.
12
11
  *
13
- * Critical override: errors, provider failovers, and compaction/context notices
14
- * (see FORCE_CONVERSATION_PREFIXES) ALWAYS also reach the main conversation,
15
- * regardless of target, so the user never has to open the SystemMessagesPanel
16
- * to discover them.
12
+ * W6.1 (the purge): the SystemMessagesPanel this router used to optionally
13
+ * push into was DELETE-disposition (no surviving human surface a picker
14
+ * over the old panel registry, not something worth a dedicated console) and
15
+ * has been removed entirely, so this router now always resolves with
16
+ * `hasPanel = false`. Per resolveSystemMessageDelivery's own contract that
17
+ * means EVERY kind/target combination (including 'panel'-only) falls back
18
+ * to `toConversation: true` — nothing this router routes can vanish; it all
19
+ * reaches conversation.addTypedSystemMessage(), which the transcript
20
+ * renders as a navigable system line. This is deliberate, not a regression:
21
+ * operational chatter that used to be tucked away in a rarely-opened panel
22
+ * now surfaces inline, same as the messages that were already forced there
23
+ * (see FORCE_CONVERSATION_PREFIXES below).
17
24
  *
18
25
  * Usage:
19
26
  * ```ts
20
- * const router = createSystemMessageRouter(conversation, panel);
27
+ * const router = createSystemMessageRouter(conversation);
21
28
  * router.routeSystemMessage('[Provider] anthropic registered', 'low');
22
29
  * router.routeSystemMessage('[Session] Saved session abc123', 'high');
23
30
  * ```
@@ -33,7 +40,6 @@
33
40
  */
34
41
 
35
42
  import type { ConversationManager } from './conversation';
36
- import type { SystemMessagesPanel, SystemMessagePriority } from '../panels/system-messages-panel.ts';
37
43
  import {
38
44
  classifySystemMessageKind,
39
45
  classifySystemMessagePriority,
@@ -48,13 +54,15 @@ export type {
48
54
  SystemMessageTarget,
49
55
  } from '@/runtime/index.ts';
50
56
 
57
+ /** Panel emphasis level. Panel delivery was removed in W6.1 (see file doc); kept as the priority vocabulary for callers and for the SDK's delivery-resolution signature. */
58
+ export type SystemMessagePriority = 'high' | 'low';
59
+
51
60
  /**
52
61
  * Message categories that are operationally critical enough that the user must
53
62
  * see them inline in the main conversation, regardless of the configured
54
- * routing target (ui.systemMessages defaults to panel-only). Errors, provider
55
- * failovers, and compaction/context notices fall here: a user should never have
56
- * to open the SystemMessagesPanel to discover that a turn errored, a provider
57
- * failed over, or the context was compacted.
63
+ * routing target. Errors, provider failovers, and compaction/context notices
64
+ * fall here: a user should never have to go hunting to discover that a turn
65
+ * errored, a provider failed over, or the context was compacted.
58
66
  *
59
67
  * Detection is by the stable message prefix tag, mirroring how the SDK
60
68
  * classifiers key off message content. This deliberately does NOT force every
@@ -72,13 +80,12 @@ function mustReachConversation(message: string): boolean {
72
80
  // ---------------------------------------------------------------------------
73
81
 
74
82
  /**
75
- * Routes system messages to the conversation and/or the SystemMessagesPanel
76
- * based on priority level.
83
+ * Routes system messages to the conversation based on priority level and
84
+ * configured target. See file doc for the W6.1 panel removal.
77
85
  */
78
86
  export class SystemMessageRouter {
79
87
  constructor(
80
88
  private readonly conversation: ConversationManager,
81
- private panel: SystemMessagesPanel | null,
82
89
  private readonly getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultSystemMessageTarget,
83
90
  ) {}
84
91
 
@@ -87,28 +94,26 @@ export class SystemMessageRouter {
87
94
  /**
88
95
  * Route a system message.
89
96
  *
90
- * Delivery is resolved from the kind and its configured target; priority only
91
- * sets panel emphasis. Critical notices (errors/failover/compaction, see
92
- * FORCE_CONVERSATION_PREFIXES) are additionally forced into the conversation.
97
+ * Delivery is resolved from the kind and its configured target. Critical
98
+ * notices (errors/failover/compaction, see FORCE_CONVERSATION_PREFIXES)
99
+ * are additionally forced into the conversation.
93
100
  *
94
101
  * @param message - Message text.
95
- * @param priority - 'high' | 'low'.
102
+ * @param priority - 'high' | 'low' (kept for callers; no longer changes
103
+ * delivery now that there is no panel to emphasize on).
96
104
  * @param kind - Classification kind ('system' | 'wrfc' | 'operational');
97
105
  * used to resolve routing target and conversation navigability.
98
106
  */
99
107
  routeTypedSystemMessage(
100
108
  message: string,
101
- priority: SystemMessagePriority,
109
+ _priority: SystemMessagePriority,
102
110
  kind: SystemMessageKind,
103
111
  ): void {
104
112
  const target = this.getTargetForKind(kind);
105
- const delivery = resolveSystemMessageDelivery(target, this.panel !== null);
106
- if (delivery.toPanel) {
107
- this.panel?.push(message, priority);
108
- }
109
- // Critical notices (errors, failover, compaction) must surface inline even
110
- // when the configured target is panel-only — otherwise a user without the
111
- // SystemMessagesPanel open would never see them.
113
+ // hasPanel is always false post-W6.1 resolveSystemMessageDelivery's own
114
+ // contract means every target ('panel' | 'conversation' | 'both')
115
+ // resolves toConversation: true in that case (see file doc).
116
+ const delivery = resolveSystemMessageDelivery(target, false);
112
117
  const toConversation = delivery.toConversation || mustReachConversation(message);
113
118
  if (toConversation) {
114
119
  // addTypedSystemMessage threads the kind into the conversation so the
@@ -153,21 +158,6 @@ export class SystemMessageRouter {
153
158
  wrfc(message: string, priority: SystemMessagePriority = 'high'): void {
154
159
  this.routeTypedSystemMessage(message, priority, 'wrfc');
155
160
  }
156
-
157
- /**
158
- * Returns the current panel reference.
159
- */
160
- getPanel(): SystemMessagesPanel | null {
161
- return this.panel;
162
- }
163
-
164
- /**
165
- * Replace the panel reference after construction (late binding).
166
- * Pass null to detach the panel.
167
- */
168
- setPanel(panel: SystemMessagesPanel | null): void {
169
- this.panel = panel;
170
- }
171
161
  }
172
162
 
173
163
  // ---------------------------------------------------------------------------
@@ -175,17 +165,13 @@ export class SystemMessageRouter {
175
165
  // ---------------------------------------------------------------------------
176
166
 
177
167
  /**
178
- * Create a SystemMessageRouter wired to the given conversation and panel.
168
+ * Create a SystemMessageRouter wired to the given conversation.
179
169
  *
180
- * @param conversation - The ConversationManager for high-priority messages.
181
- * @param panel - The SystemMessagesPanel for all messages. Can be null
182
- * (router still works; messages to panel are silently
183
- * dropped until a panel is available).
170
+ * @param conversation - The ConversationManager all routed messages reach.
184
171
  */
185
172
  export function createSystemMessageRouter(
186
173
  conversation: ConversationManager,
187
- panel: SystemMessagesPanel | null = null,
188
174
  getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultSystemMessageTarget,
189
175
  ): SystemMessageRouter {
190
- return new SystemMessageRouter(conversation, panel, getTargetForKind);
176
+ return new SystemMessageRouter(conversation, getTargetForKind);
191
177
  }
@@ -0,0 +1,20 @@
1
+ import type { Orchestrator } from './orchestrator';
2
+ import type { SpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
3
+
4
+ /**
5
+ * Builds the shared "cancel the active turn" action: stops any in-flight
6
+ * spoken output, then aborts the orchestrator's turn if one is actively
7
+ * streaming. Extracted from main.ts so the file can stay under its 800-line
8
+ * cap while still wiring `commandContext.isGenerating` (see W1.6 FIX 1).
9
+ */
10
+ export function createCancelGeneration(
11
+ orchestrator: Orchestrator,
12
+ spokenTurns: Pick<SpokenTurnRuntime, 'stop'>,
13
+ ): () => void {
14
+ return () => {
15
+ spokenTurns.stop('Spoken output stopped.');
16
+ if (orchestrator.isThinking) {
17
+ orchestrator.abort();
18
+ }
19
+ };
20
+ }
@@ -6,7 +6,12 @@ import type { HookDispatcher, HookPhase, HookCategory, HookEventPath } from '@pe
6
6
  import type { ConversationManager } from './conversation.ts';
7
7
  import { journalPathFor, openTranscriptJournal, type TranscriptJournal } from './transcript-journal.ts';
8
8
  import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
9
+ import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
9
10
  import { maybeNotifyLongTask, readNotifyAfterSeconds, type LongTaskStatus } from './long-task-notifier.ts';
11
+ import type { FocusTracker } from './focus-tracker.ts';
12
+ import { shouldFireAlert, FORCE_NOTIFY_DURATION_MS } from './alert-gating.ts';
13
+ import { createBudgetBreachNotifier, type BudgetBreachNotifier } from './budget-breach-notifier.ts';
14
+ import { readBudgetAlertUsd } from '../export/cost-utils.ts';
10
15
 
11
16
  /** Infer the options param of persistConversation to pick up SessionManager correctly. */
12
17
  type PersistOptions = NonNullable<Parameters<typeof persistConversation>[5]>;
@@ -14,11 +19,13 @@ type PersistOptions = NonNullable<Parameters<typeof persistConversation>[5]>;
14
19
  /** Minimal orchestrator surface required by turn-event wiring. */
15
20
  interface TurnOrchestrator {
16
21
  readonly lastInputTokens: number;
22
+ /** Cumulative session usage — same object CostTrackerPanel reads, used here for budget-breach checks. */
23
+ readonly usage: { readonly input: number; readonly output: number; readonly cacheRead: number; readonly cacheWrite: number };
17
24
  }
18
25
 
19
26
  /** Minimal provider registry surface required by turn-event wiring. */
20
27
  interface TurnProviderRegistry {
21
- getCurrentModel(): { readonly contextWindow: number };
28
+ getCurrentModel(): { readonly contextWindow: number; readonly id?: string };
22
29
  getContextWindowForModel(model: { readonly contextWindow: number }): number;
23
30
  }
24
31
 
@@ -57,6 +64,16 @@ export interface WireTurnEventHandlersOptions {
57
64
  * when absent.
58
65
  */
59
66
  readonly webhookNotifier?: WebhookNotifier | null;
67
+ /**
68
+ * Terminal focus tracker (W2.3). Gates the long-task, budget-breach, and
69
+ * agent/chain-failure desktop alerts wired in this module — see
70
+ * alert-gating.ts. Optional; when absent, none of the new W2.3 alert
71
+ * behavior is gated by focus (long-task keeps its pre-W2.3 always-fire
72
+ * behavior, and budget-breach/failure alerts are skipped entirely, since
73
+ * they are new-in-W2.3 and have no pre-existing unconditional-fire path
74
+ * to fall back to).
75
+ */
76
+ readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
60
77
  /**
61
78
  * Minimal test seam: injectable clock for controlling Date.now() in tests.
62
79
  * Defaults to the real Date.now when absent.
@@ -94,13 +111,14 @@ export function wireTurnEventHandlers(
94
111
  options: WireTurnEventHandlersOptions,
95
112
  ): WireTurnEventHandlersResult {
96
113
  const {
97
- events, conversation, runtime, configManager, hookDispatcher,
114
+ events, conversation, runtime, configManager, hookDispatcher, orchestrator, providerRegistry,
98
115
  workingDir, homeDirectory, sessionManager, gitStatusProvider,
99
- lastGitInfoRef, buildSessionContinuityHints, render, webhookNotifier,
116
+ lastGitInfoRef, buildSessionContinuityHints, render, webhookNotifier, focusTracker,
100
117
  _clock = Date.now,
101
118
  } = options;
102
119
 
103
120
  const unsubs: Array<() => void> = [];
121
+ const configGet = (k: string): unknown => configManager.get(k as Parameters<typeof configManager.get>[0]);
104
122
 
105
123
  // Create the per-session transcript journal. Path mirrors recovery-file
106
124
  // convention (homeDirectory-scoped). Created lazily on first append.
@@ -112,6 +130,13 @@ export function wireTurnEventHandlers(
112
130
  // Track turn start time for long-task notification threshold.
113
131
  let turnStartTime: number | null = null;
114
132
 
133
+ // Budget-breach edge-trigger checker (W2.3) — one instance per session,
134
+ // piggybacking on the same TURN_COMPLETED handler as the long-task
135
+ // notification below rather than adding a second TURN_COMPLETED subscription.
136
+ const budgetBreachNotifier: BudgetBreachNotifier | null = focusTracker
137
+ ? createBudgetBreachNotifier({ focusTracker, configGet, webhookNotifier: webhookNotifier ?? null, sessionId: runtime.sessionId })
138
+ : null;
139
+
115
140
  const refreshGit = (): void => {
116
141
  gitStatusProvider.refresh().then((info) => { lastGitInfoRef.value = info; render(); }).catch(() => { /* non-fatal */ });
117
142
  };
@@ -140,7 +165,15 @@ export function wireTurnEventHandlers(
140
165
  sessionId: runtime.sessionId,
141
166
  thresholdSeconds: notifyThreshold,
142
167
  webhookNotifier: webhookNotifier ?? null,
168
+ focusTracker,
169
+ configGet,
143
170
  });
171
+ // Budget-breach alert (W2.3): edge-triggered, piggybacks on this same
172
+ // TURN_COMPLETED handler rather than a second subscription.
173
+ if (budgetBreachNotifier) {
174
+ const sessionModel = providerRegistry.getCurrentModel().id ?? 'unknown';
175
+ budgetBreachNotifier.check(orchestrator.usage, sessionModel, readBudgetAlertUsd(configGet));
176
+ }
144
177
  // Auto-save after every LLM turn so kills don't lose the session
145
178
  try {
146
179
  const snapshot = conversation.toJSON() as { messages: Array<import('./conversation.ts').ConversationMessageSnapshot>; timestamp?: number };
@@ -180,5 +213,33 @@ export function wireTurnEventHandlers(
180
213
  refreshGit();
181
214
  }));
182
215
 
216
+ // Agent/chain-failure desktop alerts (W2.3). The SDK's WebhookNotifier and
217
+ // Notifier already fire webhook/Slack/Discord notifications for these two
218
+ // events unconditionally (attachToRuntimeBus in the SDK's
219
+ // platform/integrations — pre-existing, not focus-gated: an out-of-band
220
+ // push to another device is useful regardless of terminal focus). What was
221
+ // missing was a desktop notification, gated by focus like the other three
222
+ // alert classes — that's the only thing added here, to avoid double-firing
223
+ // a webhook that's already covered.
224
+ if (focusTracker) {
225
+ unsubs.push(events.agents.on('AGENT_FAILED', (payload) => {
226
+ if (!shouldFireAlert(focusTracker, configGet, 'behavior.notifyOnAgentFailure')) return;
227
+ try {
228
+ notifyCompletion('GoodVibes — agent failed', `agent ${payload.agentId.slice(0, 8)} failed: ${payload.error}`, FORCE_NOTIFY_DURATION_MS);
229
+ } catch (err) {
230
+ logger.debug('turn-event-wiring: agent-failure notify error', { error: String(err) });
231
+ }
232
+ }));
233
+ unsubs.push(events.workflows.on('WORKFLOW_CHAIN_FAILED', (payload) => {
234
+ if (!shouldFireAlert(focusTracker, configGet, 'behavior.notifyOnChainFailure')) return;
235
+ const kindLabel = payload.failureKind === 'transport' ? 'transient transport error' : payload.reason;
236
+ try {
237
+ notifyCompletion('GoodVibes — WRFC chain failed', `chain ${payload.chainId.slice(0, 12)} failed: ${kindLabel}`, FORCE_NOTIFY_DURATION_MS);
238
+ } catch (err) {
239
+ logger.debug('turn-event-wiring: chain-failure notify error', { error: String(err) });
240
+ }
241
+ }));
242
+ }
243
+
183
244
  return { refreshGit, unsubs, transcriptJournal };
184
245
  }
@@ -1,13 +1,38 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // cost-utils — canonical pricing source for token cost calculations (consumed by CostTrackerPanel and share-runtime)
3
+ //
4
+ // Pricing resolution order (resolvePricing):
5
+ // 1. `:free` suffix (OpenRouter free-tier convention) — always priced at $0.
6
+ // 2. The live SDK model catalog, when a source has been wired via
7
+ // setPricingSource() (bootstrap-core.ts wires ProviderRegistry.getRawCatalogModels()
8
+ // once catalog init completes) — exact id match, then prefix/substring.
9
+ // 3. STATIC_FALLBACK_PRICING — a small hand-maintained safety net for common
10
+ // frontier models, used when the catalog has no source wired (e.g. tests)
11
+ // or hasn't matched (catalog not yet loaded, or a model this net covers
12
+ // that the catalog happens to miss).
13
+ // 4. Unpriced: the model is genuinely unknown to every source above. This is
14
+ // reported explicitly via `priced: false` so consumers can render an
15
+ // honest "unpriced" state instead of a silent $0.
3
16
  // ---------------------------------------------------------------------------
4
17
 
18
+ import type { CatalogModel } from '@pellux/goodvibes-sdk/platform/providers';
19
+
5
20
  export interface ModelPricing {
6
21
  input: number;
7
22
  output: number;
8
23
  }
9
24
 
10
- const MODEL_PRICING: Record<string, ModelPricing> = {
25
+ /** Result of a pricing lookup: the resolved (possibly zero) price, and whether it's real. */
26
+ export interface PricingResult {
27
+ pricing: ModelPricing;
28
+ /** False when no source (catalog or static fallback) recognized the model — the zero pricing is a placeholder, not a real price. */
29
+ priced: boolean;
30
+ }
31
+
32
+ // Hand-maintained safety net, used only when the live catalog has no source
33
+ // wired or does not recognize the model. NOT the primary pricing source —
34
+ // see resolvePricing() below.
35
+ const STATIC_FALLBACK_PRICING: Record<string, ModelPricing> = {
11
36
  // Free tier
12
37
  'openrouter/free': { input: 0, output: 0 },
13
38
 
@@ -34,17 +59,71 @@ const MODEL_PRICING: Record<string, ModelPricing> = {
34
59
  'gemini-2.5-pro': { input: 1.25, output: 5 },
35
60
  };
36
61
 
62
+ // Module-level injection point. Kept as a bare singleton (not threaded through
63
+ // call sites) because the 9 existing consumers are pure functions with no
64
+ // ProviderRegistry in scope; see WO-315 brief for the tradeoff. Tests that
65
+ // never call setPricingSource() fall through to the static-fallback path,
66
+ // which keeps them isolated from catalog state by default.
67
+ let pricingSource: (() => readonly CatalogModel[]) | null = null;
68
+
69
+ /** Wire (or clear, with null) the live catalog as the primary pricing source. */
70
+ export function setPricingSource(source: (() => readonly CatalogModel[]) | null): void {
71
+ pricingSource = source;
72
+ }
73
+
74
+ function findInCatalog(modelId: string, models: readonly CatalogModel[]): ModelPricing | null {
75
+ const exact = models.find((m) => m.id === modelId);
76
+ if (exact) return exact.tier === 'free' ? { input: 0, output: 0 } : exact.pricing;
77
+ for (const m of models) {
78
+ if (modelId.startsWith(m.id) || modelId.includes(m.id)) {
79
+ return m.tier === 'free' ? { input: 0, output: 0 } : m.pricing;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function findInStaticFallback(modelId: string): ModelPricing | null {
86
+ const exact = STATIC_FALLBACK_PRICING[modelId];
87
+ if (exact) return exact;
88
+ for (const [key, pricing] of Object.entries(STATIC_FALLBACK_PRICING)) {
89
+ if (modelId.startsWith(key) || modelId.includes(key)) return pricing;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ /**
95
+ * resolvePricing — resolve USD-per-1M-token pricing for a model ID, and
96
+ * whether that pricing is real (vs. an unpriced placeholder). See the module
97
+ * header for the full resolution order.
98
+ */
99
+ export function resolvePricing(modelId: string): PricingResult {
100
+ if (modelId.endsWith(':free')) return { pricing: { input: 0, output: 0 }, priced: true };
101
+
102
+ const models = pricingSource?.() ?? [];
103
+ const catalogHit = findInCatalog(modelId, models);
104
+ if (catalogHit) return { pricing: catalogHit, priced: true };
105
+
106
+ const fallbackHit = findInStaticFallback(modelId);
107
+ if (fallbackHit) return { pricing: fallbackHit, priced: true };
108
+
109
+ // Genuinely unknown — no source recognizes this model. Report honestly
110
+ // rather than collapsing into the same zero a free model would return.
111
+ return { pricing: { input: 0, output: 0 }, priced: false };
112
+ }
113
+
37
114
  /**
38
115
  * getPricing — resolve USD-per-1M-token pricing for a model ID.
39
- * Exact match first; then prefix/substring; falls back to zero.
116
+ * Back-compat wrapper over resolvePricing(); returns zero for both genuinely
117
+ * free and genuinely unknown models (use resolvePricing/isModelPriced to
118
+ * distinguish the two).
40
119
  */
41
120
  export function getPricing(modelId: string): ModelPricing {
42
- if (MODEL_PRICING[modelId]) return MODEL_PRICING[modelId]!;
43
- if (modelId.endsWith(':free')) return { input: 0, output: 0 };
44
- for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
45
- if (modelId.startsWith(key) || modelId.includes(key)) return pricing;
46
- }
47
- return { input: 0, output: 0 };
121
+ return resolvePricing(modelId).pricing;
122
+ }
123
+
124
+ /** True when `modelId` resolves to a real price (free or paid), false when it's an unpriced placeholder. */
125
+ export function isModelPriced(modelId: string): boolean {
126
+ return resolvePricing(modelId).priced;
48
127
  }
49
128
 
50
129
  /**
@@ -56,6 +135,9 @@ export function getPricing(modelId: string): ModelPricing {
56
135
  * cacheRead — cumulative cache-read tokens
57
136
  * cacheWrite — cumulative cache-write tokens
58
137
  * modelId — registry model identifier
138
+ *
139
+ * Unpriced models contribute 0 to the total (same as before this WO) — only
140
+ * the display layer distinguishes "genuinely free/priced" from "unpriced".
59
141
  */
60
142
  export function calcSessionCost(
61
143
  inputTokens: number,
@@ -69,3 +151,39 @@ export function calcSessionCost(
69
151
  const billableInput = inputTokens + cacheRead + cacheWrite;
70
152
  return (billableInput * pricing.input + outputTokens * pricing.output) / 1_000_000;
71
153
  }
154
+
155
+ /**
156
+ * computeBudgetBreach — pure predicate shared by CostTrackerPanel's render-time
157
+ * "OVER BUDGET" flag and the background budget-breach notifier
158
+ * (core/budget-breach-notifier.ts), so both agree on exactly one definition
159
+ * of "over budget". `budgetThreshold <= 0` means no budget is configured
160
+ * (disabled), which can never be breached.
161
+ */
162
+ export function computeBudgetBreach(sessionCost: number, budgetThreshold: number): boolean {
163
+ return budgetThreshold > 0 && sessionCost > budgetThreshold;
164
+ }
165
+
166
+ /** Read/write access to the `behavior.budgetAlertUsd` TUI-local config key. Plain
167
+ * callbacks (rather than a ConfigManager reference) so callers can pass a
168
+ * generic-key-cast wrapper without this module depending on the SDK config types. */
169
+ export interface BudgetAlertConfigAccess {
170
+ readonly get: (key: string) => unknown;
171
+ readonly set: (key: string, value: unknown) => void;
172
+ }
173
+
174
+ /** Default for the `behavior.budgetAlertUsd` synthetic setting: 0 = no budget configured. */
175
+ export const BUDGET_ALERT_USD_DEFAULT = 0;
176
+
177
+ /**
178
+ * readBudgetAlertUsd — read the session cost-budget alert threshold (USD) from
179
+ * config. This is the single source of truth CostTrackerPanel and the
180
+ * background budget-breach notifier (core/budget-breach-notifier.ts) both
181
+ * read, so a threshold set via the panel's 'b' key is immediately visible to
182
+ * the notifier and vice versa. Falls back to BUDGET_ALERT_USD_DEFAULT (off)
183
+ * when the key is absent or invalid.
184
+ */
185
+ export function readBudgetAlertUsd(configGet: (key: string) => unknown): number {
186
+ const raw = configGet('behavior.budgetAlertUsd');
187
+ const parsed = typeof raw === 'number' ? raw : Number(raw);
188
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : BUDGET_ALERT_USD_DEFAULT;
189
+ }
@@ -7,6 +7,7 @@ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
7
7
  import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
8
8
  import type { SelectionItem, SelectionResult, SelectionAction } from './selection-modal.ts';
9
9
  import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
10
+ import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
10
11
  import type { PanelManager } from '../panels/panel-manager.ts';
11
12
  import type { KeybindingsManager } from './keybindings.ts';
12
13
  import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
@@ -68,6 +69,9 @@ export interface CommandUiActions {
68
69
  };
69
70
  executeCommand?: (name: string, args: string[]) => Promise<boolean>;
70
71
  cancelGeneration?: () => void;
72
+ /** True while an LLM turn is actively streaming. Used to give Escape
73
+ * cancel-turn precedence over a focused panel's own escape handling. */
74
+ isGenerating?: () => boolean;
71
75
  completeModelSelection?: (selection: {
72
76
  model: { id: string; provider: string; displayName: string; registryKey: string };
73
77
  effort: string;
@@ -75,9 +79,25 @@ export interface CommandUiActions {
75
79
  /** Which config target to write the selected model to. Defaults to 'main'. */
76
80
  target?: import('./model-picker.ts').ModelPickerTarget;
77
81
  }) => void;
82
+ /**
83
+ * Commit an embedding-provider selection from the model picker's
84
+ * 'embeddingProvider' mode (the 'embeddings' target). Deliberately separate
85
+ * from completeModelSelection — embedding providers are not
86
+ * ModelDefinition-shaped, so routing them through the same commit path
87
+ * would require fabricating a fake model object.
88
+ */
89
+ completeEmbeddingProviderSelection?: (providerId: string) => void;
78
90
  clearScreen?: () => void;
79
91
  activatePlan?: (planId: string, task: string) => void;
80
92
  requestPermission?: PermissionRequestHandler;
93
+ /**
94
+ * Force a full-screen repaint on the next frame (reuses Compositor.resetDiff(),
95
+ * the same call resize/bootstrap already use). Defense-in-depth for command
96
+ * handlers whose spawned subprocess may have written to the real tty (e.g. a
97
+ * stderr-capture regression) — nulls the diff buffers so the next composite()
98
+ * repaints over any stray output instead of leaving it until an unrelated resize.
99
+ */
100
+ requestFullRepaint?: () => void;
81
101
  }
82
102
 
83
103
  export interface CommandShellUiOpeners {
@@ -99,6 +119,13 @@ export interface CommandShellUiOpeners {
99
119
  callback: (result: SelectionResult | null) => void,
100
120
  ) => void;
101
121
  openSettingsModal?: (target?: string) => void;
122
+ /**
123
+ * Open a MIGRATE-TO-MODAL surface by name (W6.1 purge skeleton — WO-A/B
124
+ * command runtimes call this instead of openCommandPanel once their panel
125
+ * is converted to a ModalFactory config). Threaded from ui-openers.ts the
126
+ * same way openSettingsModal is.
127
+ */
128
+ openModal?: (name: string) => void;
102
129
  openSessionPicker?: () => void;
103
130
  openProfilePicker?: () => void;
104
131
  openShortcutsOverlay?: () => void;
@@ -141,6 +168,26 @@ export interface CommandSessionServices {
141
168
  readonly sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
142
169
  readonly wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
143
170
  readonly changeTracker?: import('@pellux/goodvibes-sdk/platform/sessions').SessionChangeTracker;
171
+ /**
172
+ * Recompute the Orchestrator's session-wide usage totals from the
173
+ * conversation's current message history. Call after a session resume
174
+ * replays historical messages, before the next render, so the footer's
175
+ * token counter reflects the resumed session's real usage instead of a
176
+ * fresh Orchestrator's zeroed default (W0.9).
177
+ */
178
+ readonly hydrateSessionUsage?: () => void;
179
+ /** Wave 4 (wo703): the orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
180
+ readonly workstreamEngine?: import('../runtime/workstream-services.ts').WorkstreamCommandService;
181
+ /** Wave 5 (wo804): the repo source-tree code index — see runtime/code-index-services.ts. */
182
+ readonly codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
183
+ /**
184
+ * Wave 5 (wo805): the MAIN interactive session's per-turn passive-injection
185
+ * honesty ring — `Orchestrator.getTurnInjections()`, the main-session
186
+ * counterpart to `AgentRecord.turnInjections` (wo801). `/recall injections`
187
+ * with no agent id reads this. Optional so command contexts built without an
188
+ * orchestrator (headless, tests) fall back to the honest empty state.
189
+ */
190
+ readonly getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
144
191
  }
145
192
 
146
193
  export interface CommandProviderServices {
@@ -157,6 +204,7 @@ export interface CommandProviderServices {
157
204
  export interface CommandWorkspaceUiServices {
158
205
  keybindingsManager?: KeybindingsManager;
159
206
  fileUndoManager?: FileUndoManager;
207
+ workspaceCheckpointManager?: WorkspaceCheckpointManager;
160
208
  panelManager?: PanelManager;
161
209
  profileManager?: import('@pellux/goodvibes-sdk/platform/profiles').ProfileManager;
162
210
  bookmarkManager?: import('@pellux/goodvibes-sdk/platform/bookmarks').BookmarkManager;
@@ -174,6 +222,11 @@ export interface CommandPlatformConfigServices {
174
222
  readonly configManager: ConfigManager;
175
223
  readonly voiceProviderRegistry?: VoiceProviderRegistry;
176
224
  readonly voiceService?: VoiceService;
225
+ /** Direct-command consumer (`/search`) alongside the existing agent-tool consumer. */
226
+ readonly webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
227
+ /** Direct-command consumer (`/image`) — first production caller of `.generate()`. */
228
+ readonly mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
229
+ readonly artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
177
230
  }
178
231
 
179
232
  export interface CommandPlatformServices
@@ -186,7 +239,7 @@ export interface CommandOpsServices
186
239
  export interface CommandExtensionRegistryServices {
187
240
  readonly toolRegistry: ToolRegistry;
188
241
  readonly mcpRegistry: McpRegistry;
189
- readonly evalRegistry?: import('../panels/eval-panel.ts').EvalRegistry;
242
+ readonly evalRegistry?: import('../panels/eval-registry.ts').EvalRegistry;
190
243
  }
191
244
 
192
245
  export interface CommandExtensionServices