@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
@@ -18,15 +18,21 @@
18
18
  * When neither target is available the function is an honest no-op (debug log
19
19
  * only; no user-facing error spam).
20
20
  *
21
- * Focus tracking: terminal focus state is not tracked anywhere in the TUI.
22
- * Notifications therefore fire regardless of whether the terminal window is
23
- * focused. A future implementation may suppress notifications when the TUI is
24
- * in the foreground by reading a focus-state ref. // seam: wire focus ref here.
21
+ * Focus tracking (W2.3): when `focusTracker` is supplied, notifications are
22
+ * gated the same way as the other unfocused-user alert classes (see
23
+ * alert-gating.ts) they fire when the terminal is unfocused or focus state
24
+ * was never observed, and are suppressed when it's known to be focused,
25
+ * unless `behavior.notifyOnlyWhenUnfocused` is turned off. `focusTracker` is
26
+ * optional and defaults to "always fire" (the pre-W2.3 behavior) when
27
+ * omitted, so existing callers that don't have a FocusTracker in scope are
28
+ * unaffected.
25
29
  */
26
30
 
27
31
  import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
28
32
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
29
33
  import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
34
+ import type { FocusTracker } from './focus-tracker.ts';
35
+ import { readNotifyOnlyWhenUnfocused, type ConfigGet } from './alert-gating.ts';
30
36
 
31
37
  /** Default threshold in seconds. Turns shorter than this do not notify. */
32
38
  export const NOTIFY_AFTER_SECONDS_DEFAULT = 60;
@@ -74,6 +80,18 @@ export interface MaybeNotifyLongTaskOptions {
74
80
  * skipped silently.
75
81
  */
76
82
  readonly webhookNotifier?: WebhookNotifier | null;
83
+
84
+ /**
85
+ * Terminal focus tracker (W2.3). When supplied together with `configGet`,
86
+ * the notification additionally respects behavior.notifyOnlyWhenUnfocused
87
+ * (default on): suppressed when the terminal is known to be focused, fires
88
+ * when unfocused or when focus was never observed. Omit both to preserve
89
+ * pre-W2.3 behavior (always fire once the threshold is met).
90
+ */
91
+ readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
92
+
93
+ /** Config reader used only for the notifyOnlyWhenUnfocused gate above. */
94
+ readonly configGet?: ConfigGet;
77
95
  }
78
96
 
79
97
  /**
@@ -81,13 +99,14 @@ export interface MaybeNotifyLongTaskOptions {
81
99
  * exceeds the configured threshold.
82
100
  *
83
101
  * Returns true when at least one delivery was attempted, false when the
84
- * call was a no-op (threshold not reached, or off-state).
102
+ * call was a no-op (threshold not reached, off-state, or focus-gated off
103
+ * see `focusTracker`/`configGet` above).
85
104
  *
86
105
  * PRIVACY: builds message text from structural metadata only (kind, elapsed,
87
106
  * status, sessionId). Never includes conversation content.
88
107
  */
89
108
  export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
90
- const { elapsedMs, status, kind, sessionId, thresholdSeconds, webhookNotifier } = opts;
109
+ const { elapsedMs, status, kind, sessionId, thresholdSeconds, webhookNotifier, focusTracker, configGet } = opts;
91
110
 
92
111
  // Off-state: 0 disables notifications entirely.
93
112
  if (thresholdSeconds === NOTIFY_AFTER_SECONDS_OFF) {
@@ -102,6 +121,14 @@ export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
102
121
  return false;
103
122
  }
104
123
 
124
+ // Focus gate (W2.3): only applied when both a tracker and a config reader
125
+ // are supplied. Absent either one, behavior is unchanged from pre-W2.3
126
+ // (always fire once the threshold is met).
127
+ if (focusTracker && configGet && readNotifyOnlyWhenUnfocused(configGet) && !focusTracker.shouldAlertWhenUnfocused()) {
128
+ logger.debug('long-task-notifier: suppressed — terminal focused');
129
+ return false;
130
+ }
131
+
105
132
  // Build concise, metadata-only message. No conversation text.
106
133
  const statusLabel = status === 'ok' ? 'completed' : 'failed';
107
134
  const title = `GoodVibes — ${kind} ${statusLabel}`;
@@ -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
  }
@@ -151,3 +151,39 @@ export function calcSessionCost(
151
151
  const billableInput = inputTokens + cacheRead + cacheWrite;
152
152
  return (billableInput * pricing.input + outputTokens * pricing.output) / 1_000_000;
153
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,6 +79,14 @@ 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;
@@ -107,6 +119,13 @@ export interface CommandShellUiOpeners {
107
119
  callback: (result: SelectionResult | null) => void,
108
120
  ) => void;
109
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;
110
129
  openSessionPicker?: () => void;
111
130
  openProfilePicker?: () => void;
112
131
  openShortcutsOverlay?: () => void;
@@ -157,6 +176,18 @@ export interface CommandSessionServices {
157
176
  * fresh Orchestrator's zeroed default (W0.9).
158
177
  */
159
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[];
160
191
  }
161
192
 
162
193
  export interface CommandProviderServices {
@@ -173,6 +204,7 @@ export interface CommandProviderServices {
173
204
  export interface CommandWorkspaceUiServices {
174
205
  keybindingsManager?: KeybindingsManager;
175
206
  fileUndoManager?: FileUndoManager;
207
+ workspaceCheckpointManager?: WorkspaceCheckpointManager;
176
208
  panelManager?: PanelManager;
177
209
  profileManager?: import('@pellux/goodvibes-sdk/platform/profiles').ProfileManager;
178
210
  bookmarkManager?: import('@pellux/goodvibes-sdk/platform/bookmarks').BookmarkManager;
@@ -190,6 +222,11 @@ export interface CommandPlatformConfigServices {
190
222
  readonly configManager: ConfigManager;
191
223
  readonly voiceProviderRegistry?: VoiceProviderRegistry;
192
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;
193
230
  }
194
231
 
195
232
  export interface CommandPlatformServices
@@ -202,7 +239,7 @@ export interface CommandOpsServices
202
239
  export interface CommandExtensionRegistryServices {
203
240
  readonly toolRegistry: ToolRegistry;
204
241
  readonly mcpRegistry: McpRegistry;
205
- readonly evalRegistry?: import('../panels/eval-panel.ts').EvalRegistry;
242
+ readonly evalRegistry?: import('../panels/eval-registry.ts').EvalRegistry;
206
243
  }
207
244
 
208
245
  export interface CommandExtensionServices