@pellux/goodvibes-tui 1.1.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -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,7 @@
33
40
  */
34
41
 
35
42
  import type { ConversationManager } from './conversation';
36
- import type { SystemMessagesPanel, SystemMessagePriority } from '../panels/system-messages-panel.ts';
43
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
37
44
  import {
38
45
  classifySystemMessageKind,
39
46
  classifySystemMessagePriority,
@@ -42,19 +49,27 @@ import {
42
49
  type SystemMessageKind,
43
50
  type SystemMessageTarget,
44
51
  } from '@/runtime/index.ts';
52
+ import {
53
+ classifyNoise,
54
+ foldProviderReplayLines,
55
+ providerNameFromReplay,
56
+ type NoiseGateDeps,
57
+ } from './system-message-noise.ts';
45
58
 
46
59
  export type {
47
60
  SystemMessageKind,
48
61
  SystemMessageTarget,
49
62
  } from '@/runtime/index.ts';
50
63
 
64
+ /** 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. */
65
+ export type SystemMessagePriority = 'high' | 'low';
66
+
51
67
  /**
52
68
  * Message categories that are operationally critical enough that the user must
53
69
  * 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.
70
+ * routing target. Errors, provider failovers, and compaction/context notices
71
+ * fall here: a user should never have to go hunting to discover that a turn
72
+ * errored, a provider failed over, or the context was compacted.
58
73
  *
59
74
  * Detection is by the stable message prefix tag, mirroring how the SDK
60
75
  * classifiers key off message content. This deliberately does NOT force every
@@ -72,14 +87,19 @@ function mustReachConversation(message: string): boolean {
72
87
  // ---------------------------------------------------------------------------
73
88
 
74
89
  /**
75
- * Routes system messages to the conversation and/or the SystemMessagesPanel
76
- * based on priority level.
90
+ * Routes system messages to the conversation based on priority level and
91
+ * configured target. See file doc for the W6.1 panel removal.
77
92
  */
78
93
  export class SystemMessageRouter {
94
+ /** Buffered provider "from last session" replay lines, folded on a microtask. */
95
+ private providerReplayBuffer: string[] = [];
96
+ private providerReplayScheduled = false;
97
+
79
98
  constructor(
80
99
  private readonly conversation: ConversationManager,
81
- private panel: SystemMessagesPanel | null,
82
100
  private readonly getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultSystemMessageTarget,
101
+ /** Noise-gate dependencies (WRFC terminal-chain lookup). See system-message-noise.ts. */
102
+ private readonly noiseDeps: NoiseGateDeps = {},
83
103
  ) {}
84
104
 
85
105
  // ── Public API ────────────────────────────────────────────────────────────
@@ -87,28 +107,40 @@ export class SystemMessageRouter {
87
107
  /**
88
108
  * Route a system message.
89
109
  *
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.
110
+ * Delivery is resolved from the kind and its configured target. Critical
111
+ * notices (errors/failover/compaction, see FORCE_CONVERSATION_PREFIXES)
112
+ * are additionally forced into the conversation.
93
113
  *
94
114
  * @param message - Message text.
95
- * @param priority - 'high' | 'low'.
115
+ * @param priority - 'high' | 'low' (kept for callers; no longer changes
116
+ * delivery now that there is no panel to emphasize on).
96
117
  * @param kind - Classification kind ('system' | 'wrfc' | 'operational');
97
118
  * used to resolve routing target and conversation navigability.
98
119
  */
99
120
  routeTypedSystemMessage(
100
121
  message: string,
101
- priority: SystemMessagePriority,
122
+ _priority: SystemMessagePriority,
102
123
  kind: SystemMessageKind,
103
124
  ): void {
104
- const target = this.getTargetForKind(kind);
105
- const delivery = resolveSystemMessageDelivery(target, this.panel !== null);
106
- if (delivery.toPanel) {
107
- this.panel?.push(message, priority);
125
+ // Noise gate — keep first-run plumbing out of the transcript while the
126
+ // information stays reachable via other live surfaces. (UX-B item 1.)
127
+ const verdict = classifyNoise(message, this.noiseDeps);
128
+ if (verdict.action === 'drop') return;
129
+ if (verdict.action === 'foldProviderReplay') {
130
+ this.bufferProviderReplay(message);
131
+ return;
108
132
  }
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.
133
+
134
+ this.deliver(message, kind);
135
+ }
136
+
137
+ /** Post-noise-gate delivery: resolve target and append to the conversation. */
138
+ private deliver(message: string, kind: SystemMessageKind): void {
139
+ const target = this.getTargetForKind(kind);
140
+ // hasPanel is always false post-W6.1 — resolveSystemMessageDelivery's own
141
+ // contract means every target ('panel' | 'conversation' | 'both')
142
+ // resolves toConversation: true in that case (see file doc).
143
+ const delivery = resolveSystemMessageDelivery(target, false);
112
144
  const toConversation = delivery.toConversation || mustReachConversation(message);
113
145
  if (toConversation) {
114
146
  // addTypedSystemMessage threads the kind into the conversation so the
@@ -117,6 +149,45 @@ export class SystemMessageRouter {
117
149
  }
118
150
  }
119
151
 
152
+ /**
153
+ * Buffer a provider "from last session" replay line and schedule a microtask
154
+ * flush. The SDK emits the whole persisted-provider burst synchronously, so a
155
+ * single microtask captures the full burst and folds it to one quiet line.
156
+ * (UX-B item 1b.)
157
+ */
158
+ private bufferProviderReplay(message: string): void {
159
+ this.providerReplayBuffer.push(message);
160
+ if (this.providerReplayScheduled) return;
161
+ this.providerReplayScheduled = true;
162
+ queueMicrotask(() => this.flushProviderReplay());
163
+ }
164
+
165
+ /**
166
+ * Record the folded provider-replay summary and reset the buffer.
167
+ *
168
+ * Papercut sweep item 2: this used to `deliver()` the folded line into the
169
+ * transcript — one line instead of a burst, but still boot plumbing the
170
+ * user never asked to see there ("the transcript at boot shows product
171
+ * signal only"). The persisted-provider set this summarizes is reachable
172
+ * on demand via `/health provider` (providers-modal lists every registered
173
+ * provider, discovered/local ones included) and `/model` (lists every
174
+ * selectable model, discovered ones included) — so nothing is lost by
175
+ * keeping it out of the transcript. It still goes to the activity log
176
+ * (.goodvibes/logs/activity.md) for diagnosis. Only this boot-only "— from
177
+ * last session" burst moves; unrelated provider-discovery lines emitted
178
+ * mid-session (e.g. "[Scan] Found …", "[Scan] … no longer reachable") are
179
+ * untouched — they never match PROVIDER_REPLAY_RE, so they never enter this
180
+ * buffer/fold path and keep reaching the transcript as live product signal.
181
+ */
182
+ flushProviderReplay(): void {
183
+ this.providerReplayScheduled = false;
184
+ if (this.providerReplayBuffer.length === 0) return;
185
+ const summary = foldProviderReplayLines(this.providerReplayBuffer);
186
+ const providerNames = this.providerReplayBuffer.map(providerNameFromReplay);
187
+ this.providerReplayBuffer = [];
188
+ logger.info(summary, { count: providerNames.length, providers: providerNames });
189
+ }
190
+
120
191
  routeSystemMessage(message: string, priority: SystemMessagePriority): void {
121
192
  this.routeTypedSystemMessage(message, priority, classifySystemMessageKind(message));
122
193
  }
@@ -153,21 +224,6 @@ export class SystemMessageRouter {
153
224
  wrfc(message: string, priority: SystemMessagePriority = 'high'): void {
154
225
  this.routeTypedSystemMessage(message, priority, 'wrfc');
155
226
  }
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
227
  }
172
228
 
173
229
  // ---------------------------------------------------------------------------
@@ -175,17 +231,14 @@ export class SystemMessageRouter {
175
231
  // ---------------------------------------------------------------------------
176
232
 
177
233
  /**
178
- * Create a SystemMessageRouter wired to the given conversation and panel.
234
+ * Create a SystemMessageRouter wired to the given conversation.
179
235
  *
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).
236
+ * @param conversation - The ConversationManager all routed messages reach.
184
237
  */
185
238
  export function createSystemMessageRouter(
186
239
  conversation: ConversationManager,
187
- panel: SystemMessagesPanel | null = null,
188
240
  getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultSystemMessageTarget,
241
+ noiseDeps: NoiseGateDeps = {},
189
242
  ): SystemMessageRouter {
190
- return new SystemMessageRouter(conversation, panel, getTargetForKind);
243
+ return new SystemMessageRouter(conversation, getTargetForKind, noiseDeps);
191
244
  }
@@ -0,0 +1,25 @@
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
+ * Returns whether SPEECH was actively stopped — a Ctrl+C that silenced live
11
+ * TTS is consumed by that job (batch replay D5), same as a press that
12
+ * cleared a non-empty prompt; the quit chord starts from a quiet state.
13
+ */
14
+ export function createCancelGeneration(
15
+ orchestrator: Orchestrator,
16
+ spokenTurns: Pick<SpokenTurnRuntime, 'stop'>,
17
+ ): () => boolean {
18
+ return () => {
19
+ const stoppedSpeech = spokenTurns.stop('Spoken output stopped.');
20
+ if (orchestrator.isThinking) {
21
+ orchestrator.abort();
22
+ }
23
+ return stoppedSpeech;
24
+ };
25
+ }
@@ -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,41 @@ 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
+ try {
236
+ // An operator cancellation is an intended stop, not a failure — narrate it
237
+ // as cancelled (the reason already carries the landed-work count from the
238
+ // chain's edit ledger), so the notification never contradicts the cancelled
239
+ // chain/owner/cohort surfaces.
240
+ if (payload.failureKind === 'cancelled') {
241
+ notifyCompletion('GoodVibes — WRFC chain cancelled', `chain ${payload.chainId.slice(0, 12)} cancelled: ${payload.reason}`, FORCE_NOTIFY_DURATION_MS);
242
+ } else {
243
+ const kindLabel = payload.failureKind === 'transport' ? 'transient transport error' : payload.reason;
244
+ notifyCompletion('GoodVibes — WRFC chain failed', `chain ${payload.chainId.slice(0, 12)} failed: ${kindLabel}`, FORCE_NOTIFY_DURATION_MS);
245
+ }
246
+ } catch (err) {
247
+ logger.debug('turn-event-wiring: chain-failure notify error', { error: String(err) });
248
+ }
249
+ }));
250
+ }
251
+
183
252
  return { refreshGit, unsubs, transcriptJournal };
184
253
  }
package/src/daemon/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { homedir, networkInterfaces } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
  import { readFileSync } from 'node:fs';
4
- import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
4
+ import { ConfigManager, resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
5
5
  import { formatProviderModel, getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
6
6
  import { RuntimeEventBus, GlobalNetworkTransportInstaller } from '@/runtime/index.ts';
7
7
  import { createFeatureFlagManager } from '@/runtime/index.ts';
@@ -27,6 +27,7 @@ import {
27
27
  persistProviders,
28
28
  } from '@pellux/goodvibes-sdk/platform/discovery';
29
29
  import { createSafeHostServeFactory } from './safe-serve.ts';
30
+ import { isDaemonServiceSubcommand, resolveInstalledDaemonBinary, runDaemonServiceCli } from './service-commands.ts';
30
31
 
31
32
  import {
32
33
  parseGoodVibesCli,
@@ -161,6 +162,32 @@ async function main(): Promise<void> {
161
162
  process.env['GOODVIBES_DAEMON_HOST'] = cliFlags.hostname;
162
163
  logger.info('daemon: --hostname flag applied', { hostname: cliFlags.hostname });
163
164
  }
165
+
166
+ // D7a Layer 1: `install-service` / `uninstall-service` / `service-status` manage
167
+ // the systemd USER unit for the shared daemon. They run BEFORE the daemon boots —
168
+ // no runtime/services are constructed — and exit with the honest result code.
169
+ const serviceSubcommand = cli.positionals[0];
170
+ if (isDaemonServiceSubcommand(serviceSubcommand)) {
171
+ const host = String(process.env.GOODVIBES_DAEMON_HOST ?? config.get('controlPlane.host') ?? '127.0.0.1');
172
+ const port = Number(config.get('controlPlane.port'));
173
+ const binaryPath = resolveInstalledDaemonBinary({ moduleUrl: import.meta.url });
174
+ const result = await runDaemonServiceCli({
175
+ subcommand: serviceSubcommand,
176
+ binaryPath,
177
+ homeDir: homeDirectory,
178
+ host,
179
+ port,
180
+ // migrate-service only: never auto-migrate — requires the same explicit
181
+ // consent as any other non-interactive destructive confirmation.
182
+ confirmMigration: cliFlags.yes,
183
+ });
184
+ for (const line of result.lines) {
185
+ // eslint-disable-next-line no-console
186
+ console.log(line);
187
+ }
188
+ process.exit(result.exitCode);
189
+ }
190
+
164
191
  const runtimeBus = new RuntimeEventBus();
165
192
  const runtimeStore = createRuntimeStore();
166
193
  const featureFlags = createFeatureFlagManager();
@@ -269,7 +296,7 @@ async function main(): Promise<void> {
269
296
  process.on('SIGTERM', handleSignal);
270
297
 
271
298
  logger.info('goodvibes daemon host started', {
272
- daemon: config.get('danger.daemon'),
299
+ daemon: resolveDaemonEnabled(config),
273
300
  httpListener: config.get('danger.httpListener'),
274
301
  });
275
302
 
@@ -111,7 +111,14 @@ export function registerCatalogHandler<TBody, TResult>(
111
111
  ): Unregister {
112
112
  const descriptor = catalog.get(methodId);
113
113
  if (!descriptor) {
114
- throw new HandlerError(`Unknown gateway method: ${methodId}`, 'UNKNOWN_METHOD', 404);
114
+ // 'METHOD_NOT_FOUND' (not the old locally-coined 'UNKNOWN_METHOD') so this lines
115
+ // up byte-for-byte with SDKErrorCodes.METHOD_NOT_FOUND — the code the SDK's own
116
+ // uncataloged-method 404 now carries (method-catalog.ts's GatewayMethodCatalog
117
+ // .invoke(), daemon/control-plane.ts's invokeGatewayMethodCall, and daemon-sdk's
118
+ // control-routes.ts getGatewayMethod/invokeGatewayMethod). A literal string, not
119
+ // an import of SDKErrorCodes: the pinned SDK (0.38.0) predates that constant, and
120
+ // matching the value this way needs no SDK version bump to stay aligned.
121
+ throw new HandlerError(`Unknown gateway method: ${methodId}`, 'METHOD_NOT_FOUND', 404);
115
122
  }
116
123
 
117
124
  const wrapped = async (inv: GatewayMethodInvocation): Promise<unknown> => {