@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
@@ -14,6 +14,14 @@ import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
14
14
  import { buildSubscriptionEntries } from './settings-modal-subscriptions.ts';
15
15
  import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
16
16
  import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
17
+ import { CODE_INDEX_ENABLED_CONFIG_KEY } from '../runtime/code-index-services.ts';
18
+ import {
19
+ THEME_MODE_CONFIG_KEY,
20
+ THEME_MODE_VALUES,
21
+ THEME_MODE_DEFAULT,
22
+ THEME_MODE_DESCRIPTION,
23
+ coerceThemeModeSetting,
24
+ } from '../renderer/theme-mode-config.ts';
17
25
  import {
18
26
  SETTINGS_CATEGORIES,
19
27
  type FlagEntry,
@@ -93,6 +101,14 @@ export function buildSettingGroups(
93
101
  }
94
102
  }
95
103
 
104
+ // DEBT-2: inject the synthetic display.themeMode enum (auto|dark|light). TUI-local
105
+ // key stored under the existing `display` section (see theme-mode-config.ts for why
106
+ // not `appearance`), same rationale as the other synthetic settings below.
107
+ const displayEntries = groups.get('display');
108
+ if (displayEntries && !displayEntries.some((e) => e.setting.key === (THEME_MODE_CONFIG_KEY as ConfigKey))) {
109
+ displayEntries.push(buildThemeModeSyntheticEntry(configManager));
110
+ }
111
+
96
112
  const uiEntries = groups.get('ui');
97
113
  if (uiEntries) {
98
114
  const uiPriority: Record<string, number> = {
@@ -130,6 +146,25 @@ export function buildSettingGroups(
130
146
  behaviorEntries.push(buildNotifyAfterSecondsSyntheticEntry(configManager));
131
147
  }
132
148
 
149
+ // Inject the W2.3 alert-class toggles + master focus gate. TUI-local
150
+ // synthetic settings, same rationale as notifyAfterSeconds above.
151
+ if (behaviorEntries) {
152
+ for (const entry of buildNotifyAlertSyntheticEntries(configManager)) {
153
+ if (!behaviorEntries.some((e) => e.setting.key === entry.setting.key)) {
154
+ behaviorEntries.push(entry);
155
+ }
156
+ }
157
+ }
158
+
159
+ // Wave 5 (wo804): inject the storage.codeIndexEnabled toggle into the
160
+ // storage category. TUI-local synthetic setting (not in the SDK ConfigKey
161
+ // union — see code-index-services.ts), same rationale as
162
+ // notifyAfterSeconds above: opt-in, default off, states its own bounds.
163
+ const storageEntries = groups.get('storage');
164
+ if (storageEntries && !storageEntries.some((e) => e.setting.key === (CODE_INDEX_ENABLED_CONFIG_KEY as ConfigKey))) {
165
+ storageEntries.push(buildCodeIndexEnabledSyntheticEntry(configManager));
166
+ }
167
+
133
168
  return groups;
134
169
  }
135
170
 
@@ -179,6 +214,34 @@ export function buildTtsSpeedSyntheticEntry(configManager: Pick<ConfigManager, '
179
214
  };
180
215
  }
181
216
 
217
+ // ---------------------------------------------------------------------------
218
+ // display.themeMode synthetic setting (DEBT-2 light theme)
219
+ // ---------------------------------------------------------------------------
220
+
221
+ /**
222
+ * The synthetic ConfigSetting descriptor for display.themeMode. TUI-local (not
223
+ * in the SDK ConfigKey union); the key is cast to ConfigKey as the other
224
+ * synthetic settings do. Stored under the existing `display` section so
225
+ * ConfigManager.setDynamic/get round-trip it with no SDK change.
226
+ */
227
+ export const THEME_MODE_SYNTHETIC_SETTING: ConfigSetting = {
228
+ key: THEME_MODE_CONFIG_KEY as ConfigKey,
229
+ type: 'enum',
230
+ default: THEME_MODE_DEFAULT,
231
+ enumValues: [...THEME_MODE_VALUES],
232
+ description: THEME_MODE_DESCRIPTION,
233
+ };
234
+
235
+ /** Build the synthetic SettingEntry for display.themeMode. */
236
+ export function buildThemeModeSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
237
+ const currentValue = coerceThemeModeSetting(configManager.get(THEME_MODE_CONFIG_KEY as ConfigKey));
238
+ return {
239
+ setting: THEME_MODE_SYNTHETIC_SETTING,
240
+ currentValue,
241
+ isDefault: currentValue === THEME_MODE_DEFAULT,
242
+ };
243
+ }
244
+
182
245
  // ---------------------------------------------------------------------------
183
246
  // behavior.notifyAfterSeconds synthetic setting
184
247
  // ---------------------------------------------------------------------------
@@ -224,6 +287,81 @@ export function buildNotifyAfterSecondsSyntheticEntry(configManager: Pick<Config
224
287
  };
225
288
  }
226
289
 
290
+ // ---------------------------------------------------------------------------
291
+ // W2.3 alert-class synthetic settings — behavior.notifyOn* + notifyOnlyWhenUnfocused
292
+ // ---------------------------------------------------------------------------
293
+
294
+ /**
295
+ * The five W2.3 alert-gating booleans, all TUI-local (not yet in the SDK
296
+ * ConfigKey union), all defaulting to on. Read/written generically by
297
+ * core/alert-gating.ts (readBooleanConfig) and the per-alert-class modules
298
+ * (budget-breach-notifier.ts, approval-alert.ts, turn-event-wiring.ts,
299
+ * long-task-notifier.ts) — this is only the settings-modal-visible surface.
300
+ */
301
+ const NOTIFY_ALERT_SYNTHETIC_SETTINGS: ReadonlyArray<{ readonly key: string; readonly description: string }> = [
302
+ {
303
+ key: 'behavior.notifyOnBudgetBreach',
304
+ description: 'Alert when session cost crosses the configured budget (set via the Cost panel\'s "b" key).',
305
+ },
306
+ {
307
+ key: 'behavior.notifyOnAgentFailure',
308
+ description: 'Alert when a delegated or background agent fails.',
309
+ },
310
+ {
311
+ key: 'behavior.notifyOnChainFailure',
312
+ description: 'Alert when a WRFC review chain fails.',
313
+ },
314
+ {
315
+ key: 'behavior.notifyOnApprovalPending',
316
+ description: 'Alert when a tool call is waiting on your approval.',
317
+ },
318
+ {
319
+ key: 'behavior.notifyOnlyWhenUnfocused',
320
+ description: 'Master gate for the four alerts above: fire only when the terminal window is unfocused, or when focus state was never observed (terminal does not report focus). Turn off to always fire regardless of focus.',
321
+ },
322
+ ];
323
+
324
+ function buildBooleanSyntheticEntry(
325
+ configManager: Pick<ConfigManager, 'get'>,
326
+ key: string,
327
+ description: string,
328
+ defaultValue: boolean,
329
+ ): SettingEntry {
330
+ const raw = configManager.get(key as ConfigKey);
331
+ const currentValue = typeof raw === 'boolean' ? raw : defaultValue;
332
+ return {
333
+ setting: { key: key as ConfigKey, type: 'boolean', default: defaultValue, description },
334
+ currentValue,
335
+ isDefault: currentValue === defaultValue,
336
+ };
337
+ }
338
+
339
+ /** Build the five synthetic SettingEntry rows for the behavior category. */
340
+ export function buildNotifyAlertSyntheticEntries(configManager: Pick<ConfigManager, 'get'>): SettingEntry[] {
341
+ return NOTIFY_ALERT_SYNTHETIC_SETTINGS.map((spec) => buildBooleanSyntheticEntry(configManager, spec.key, spec.description, true));
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Wave 5 (wo804) — storage.codeIndexEnabled synthetic setting
346
+ // ---------------------------------------------------------------------------
347
+
348
+ /**
349
+ * The repo source-tree code index's auto-build-on-startup toggle
350
+ * (code-index-services.ts). TUI-local, default OFF: /codebase build is the
351
+ * explicit trigger unless a user opts in here. Honest bounds stated inline
352
+ * so enabling this isn't a surprise — see code-index-services.ts's
353
+ * CODE_INDEX_MAX_FILES/CODE_INDEX_MAX_FILE_BYTES for the numbers this
354
+ * description would otherwise duplicate as magic numbers.
355
+ */
356
+ export function buildCodeIndexEnabledSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
357
+ return buildBooleanSyntheticEntry(
358
+ configManager,
359
+ CODE_INDEX_ENABLED_CONFIG_KEY,
360
+ 'Auto-build the repo source-tree code index on startup (bounded file/size scan; see /codebase status for bounds). Off by default — /codebase build indexes on demand.',
361
+ false,
362
+ );
363
+ }
364
+
227
365
  // ---------------------------------------------------------------------------
228
366
  // buildFlagEntries — snapshot of current feature flag states
229
367
  // ---------------------------------------------------------------------------
@@ -306,6 +444,8 @@ export function refreshEntryValues(
306
444
  // construction time so isDefault stays accurate.
307
445
  if (entry.setting.key === ('tts.speed' as ConfigKey)) {
308
446
  entry.currentValue = normalizeTtsSpeedValue(raw);
447
+ } else if (entry.setting.key === (THEME_MODE_CONFIG_KEY as ConfigKey)) {
448
+ entry.currentValue = coerceThemeModeSetting(raw);
309
449
  } else {
310
450
  entry.currentValue = raw;
311
451
  }
@@ -327,8 +467,12 @@ export function updateEntryForKey(
327
467
  const entry = entries.find((candidate) => candidate.setting.key === key);
328
468
  if (entry) {
329
469
  const raw = configManager.get(key);
330
- // Synthetic tts.speed entry: normalize using the same fallback logic.
331
- entry.currentValue = key === ('tts.speed' as ConfigKey) ? normalizeTtsSpeedValue(raw) : raw;
470
+ // Synthetic entries: normalize using the same fallback logic as construction.
471
+ entry.currentValue = key === ('tts.speed' as ConfigKey)
472
+ ? normalizeTtsSpeedValue(raw)
473
+ : key === (THEME_MODE_CONFIG_KEY as ConfigKey)
474
+ ? coerceThemeModeSetting(raw)
475
+ : raw;
332
476
  entry.isDefault = deepEqual(entry.currentValue, entry.setting.default);
333
477
  }
334
478
  }
@@ -113,13 +113,15 @@ export function persistFlagState(
113
113
  if (newState === 'killed') return; // never persist killed state
114
114
 
115
115
  try {
116
- const current = (configManager.getCategory('featureFlags') as Record<string, PersistedFlagState>) ?? {};
117
116
  if (newState === defaultState) {
118
- delete current[flagId];
117
+ // Back at the default: the override must be REMOVED, not merged.
118
+ // getCategory clones and mergeCategory only sets keys, so the old
119
+ // delete-then-merge approach left the stale override on disk and the
120
+ // flag silently reloaded in the overridden state on the next start.
121
+ configManager.removeCategoryKey('featureFlags', flagId);
119
122
  } else {
120
- current[flagId] = newState;
123
+ configManager.mergeCategory('featureFlags', { [flagId]: newState } as Record<string, PersistedFlagState>);
121
124
  }
122
- configManager.mergeCategory('featureFlags', current);
123
125
  } catch (e) {
124
126
  logger.error('SettingsModal: failed to persist flag state', { flagId, error: summarizeError(e) });
125
127
  }
@@ -11,11 +11,13 @@ export type SettingsCategory =
11
11
  | 'storage'
12
12
  | 'permissions'
13
13
  | 'orchestration'
14
+ | 'planner'
14
15
  | 'wrfc'
15
16
  | 'tools'
16
17
  | 'helper'
17
18
  | 'tts'
18
19
  | 'service'
20
+ | 'daemon'
19
21
  | 'controlPlane'
20
22
  | 'httpListener'
21
23
  | 'web'
@@ -42,9 +44,9 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
42
44
  }> = [
43
45
  { label: 'Interface', categories: ['display', 'ui', 'behavior', 'permissions'] },
44
46
  { label: 'AI Routing', categories: ['provider', 'subscriptions', 'helper', 'tools', 'tts'] },
45
- { label: 'Service & Network', categories: ['service', 'network', 'controlPlane', 'httpListener', 'web'] },
47
+ { label: 'Service & Network', categories: ['service', 'daemon', 'network', 'controlPlane', 'httpListener', 'web'] },
46
48
  { label: 'Surfaces & Cloud', categories: ['surfaces', 'mcp', 'cloudflare'] },
47
- { label: 'Automation', categories: ['batch', 'automation', 'watchers', 'orchestration', 'wrfc'] },
49
+ { label: 'Automation', categories: ['batch', 'automation', 'watchers', 'orchestration', 'planner', 'wrfc'] },
48
50
  { label: 'Runtime & Data', categories: ['storage', 'sandbox', 'runtime', 'cache', 'telemetry'] },
49
51
  { label: 'Advanced', categories: ['flags', 'release', 'danger'] },
50
52
  ];
package/src/main.ts CHANGED
@@ -12,7 +12,7 @@ import { registerAllTools } from '@pellux/goodvibes-sdk/platform/tools';
12
12
  import { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
13
13
  import { PermissionManager } from '@pellux/goodvibes-sdk/platform/permissions';
14
14
  import { AcpManager } from '@pellux/goodvibes-sdk/platform/acp';
15
- import { PermissionPromptUI } from './permissions/prompt.ts';
15
+ import { PermissionPromptUI, buildPendingPermissionExtras } from './permissions/prompt.ts';
16
16
  import { CommandRegistry } from './input/command-registry.ts';
17
17
  import type { CommandContext } from './input/command-registry.ts';
18
18
  import { renderProcessIndicator } from './renderer/process-indicator.ts';
@@ -32,16 +32,11 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
32
32
  import { registerBuiltinPanels } from './panels/builtin-panels.ts';
33
33
  import { bootstrapRuntime } from './runtime/bootstrap.ts';
34
34
  import type { BootstrapContext } from './runtime/bootstrap.ts';
35
+ import { buildSharedOrchestratorCoreServices } from './runtime/orchestrator-core-services.ts';
35
36
  import type { HITLMode } from '@pellux/goodvibes-sdk/platform/state';
36
- import {
37
- checkRecoveryFile,
38
- deleteRecoveryFile,
39
- loadRecoveryConversation,
40
- readLastSessionPointer,
41
- writeRecoveryFile,
42
- } from '@/runtime/index.ts';
37
+ import { readLastSessionPointer, writeRecoveryFile } from '@/runtime/index.ts';
43
38
  import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
44
- import { createPersistRecoverySnapshot, createReopenRecoveryPanels, handleErrorAffordanceKey } from './shell/recovery-input-helpers.ts';
39
+ import { createPersistRecoverySnapshot, createRecoveryFileOps, createReopenRecoveryPanels, handleErrorAffordanceKey, resolveStartupRecoveryInfo } from './shell/recovery-input-helpers.ts';
45
40
  import { wireShellUiOpeners } from './shell/ui-openers.ts';
46
41
  import { deriveComposerState } from './core/composer-state.ts';
47
42
  import { buildPersistedSessionContext, formatReturnContextForDisplay, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
@@ -57,17 +52,18 @@ import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
57
52
  import { createRenderScheduler } from './runtime/render-scheduler.ts';
58
53
  import { buildCommandArgsHint } from './input/command-args-hint.ts';
59
54
  import { summarizeRunningAgents } from './renderer/process-summary.ts';
55
+ import { footerFleetCost } from './panels/fleet-read-model.ts';
60
56
  import { formatUserFacingErrorLine } from './core/format-user-error.ts';
61
57
  import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
62
58
  import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
63
59
  import { buildContextStatusHint } from './renderer/context-status-hint.ts';
64
60
  import { evaluateSessionMaintenance } from '@/runtime/index.ts';
61
+ import { createCancelGeneration } from './core/turn-cancellation.ts';
62
+ import { wrapRequestPermissionWithAlert } from './core/approval-alert.ts';
63
+ import { setPanelFrameRequester } from './panels/base-panel.ts';
65
64
 
66
- const ALT_SCREEN_ENTER = '\x1b[?1049h'; const ALT_SCREEN_EXIT = '\x1b[?1049l';
67
- const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h'; const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
68
- const CURSOR_HIDE = '\x1b[?25l'; const CURSOR_SHOW = '\x1b[?25h'; const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
69
- const KEYBOARD_EXT_ENABLE = '\x1b[>4;2m' + '\x1b[?1u'; const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
70
- const PASTE_ENABLE = '\x1b[?2004h'; const PASTE_DISABLE = '\x1b[?2004l';
65
+ import { ALT_SCREEN_ENTER, ALT_SCREEN_EXIT, MOUSE_ENABLE, MOUSE_DISABLE, CURSOR_HIDE, CURSOR_SHOW, CLEAR_SCREEN, KEYBOARD_EXT_ENABLE, KEYBOARD_EXT_DISABLE, PASTE_ENABLE, PASTE_DISABLE, FOCUS_ENABLE, FOCUS_DISABLE } from './renderer/terminal-escapes.ts';
66
+ import { installBackgroundThemeProbe } from './renderer/terminal-bg-probe.ts';
71
67
 
72
68
  async function main() {
73
69
  const stdout = process.stdout;
@@ -103,22 +99,17 @@ async function main() {
103
99
  permissionPromptRef,
104
100
  _writeLastSessionPointer: writeLastSessionPointer,
105
101
  systemMessageRouter,
106
- setOpenAgentDetail,
107
102
  } = ctx;
108
103
  const workingDir = ctx.services.workingDirectory;
109
104
  const homeDirectory = ctx.services.homeDirectory;
110
105
  const { approvalBroker, agentManager, modeManager, processManager, providerRegistry, secretsManager, subscriptionManager } = ctx.services;
111
106
  conversation.setSessionMemoryStore(ctx.services.sessionMemoryStore);
112
107
  conversation.setSessionLineageTracker(ctx.services.sessionLineageTracker);
108
+ // Shared payload (single source of truth, includes wo805's memoryRegistry —
109
+ // see orchestrator-core-services.ts) plus this site's favoritesStore.
113
110
  orchestrator.setCoreServices({
114
- configManager,
115
- providerRegistry,
111
+ ...buildSharedOrchestratorCoreServices({ services: ctx.services, configManager, providerRegistry }),
116
112
  favoritesStore: ctx.services.favoritesStore,
117
- planManager: ctx.services.planManager,
118
- adaptivePlanner: ctx.services.adaptivePlanner,
119
- sessionMemoryStore: ctx.services.sessionMemoryStore,
120
- sessionLineageTracker: ctx.services.sessionLineageTracker,
121
- idempotencyStore: ctx.services.idempotencyStore,
122
113
  });
123
114
  ctx.services.wrfcController.setPlanManager(ctx.services.planManager);
124
115
  let activeConversationWidth = stdout.columns || 80;
@@ -213,13 +204,15 @@ async function main() {
213
204
  let recoveryInterval: ReturnType<typeof setInterval> | null = null;
214
205
  let stopSpokenOutputForExit: (() => void) | null = null;
215
206
  let recoveryPending = false;
207
+ // Which file the current recovery prompt should load/delete from (see recovery-input-helpers.ts).
208
+ let recoverySource: 'live' | 'preserved' = 'live';
216
209
 
217
210
  const lifecycle = installProcessLifecycle({
218
211
  stdin,
219
212
  stdout,
220
213
  ctx,
221
214
  noAltScreen: cli.flags.noAltScreen,
222
- ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW },
215
+ ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW, FOCUS_DISABLE },
223
216
  getInput: () => input,
224
217
  render: () => renderScheduler.flushNow(), // resize: synchronous immediate path
225
218
  getPromptContentWidth,
@@ -303,12 +296,7 @@ async function main() {
303
296
  }
304
297
  };
305
298
 
306
- const cancelGeneration = () => {
307
- spokenTurns.stop('Spoken output stopped.');
308
- if (orchestrator.isThinking) {
309
- orchestrator.abort();
310
- }
311
- };
299
+ const cancelGeneration = createCancelGeneration(orchestrator, spokenTurns);
312
300
 
313
301
  const jumpToBookmark = (key: string) => {
314
302
  conversation.getDisplayBlocks();
@@ -337,6 +325,7 @@ async function main() {
337
325
  commandContext.pasteFromClipboard = () => input.handlePaste();
338
326
  commandContext.executeCommand = (name, args) => commandRegistry.execute(name, args, commandContext);
339
327
  commandContext.cancelGeneration = cancelGeneration;
328
+ commandContext.isGenerating = () => orchestrator.isThinking;
340
329
  commandContext.jumpToBookmark = jumpToBookmark;
341
330
  commandContext.scrollToLine = scrollToLine;
342
331
  commandContext.clearScreen = () => {
@@ -345,14 +334,14 @@ async function main() {
345
334
  render();
346
335
  };
347
336
  commandContext.requestFullRepaint = () => { compositor.resetDiff(); render(); };
348
- permissionPromptRef.requestPermission = (request) =>
337
+ permissionPromptRef.requestPermission = wrapRequestPermissionWithAlert((request) =>
349
338
  new Promise((resolve) => {
350
339
  pendingPermission = {
351
340
  ...request,
352
- resolve: (approved: boolean, remember = false) => resolve({ approved, remember }),
341
+ ...buildPendingPermissionExtras(request, resolve),
353
342
  };
354
343
  render();
355
- });
344
+ }), { focusTracker: ctx.services.focusTracker, configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]), webhookNotifier: ctx.services.webhookNotifier });
356
345
 
357
346
  const input: InputHandler = new InputHandler(
358
347
  () => render(),
@@ -385,7 +374,7 @@ async function main() {
385
374
  replayEngine: ctx.services.replayEngine,
386
375
  webhookNotifier: ctx.services.webhookNotifier,
387
376
  policyRuntimeState: ctx.services.policyRuntimeState,
388
- externalServices: uiServices.platform.externalServices,
377
+ externalServices: uiServices.platform.externalServices, focusTracker: ctx.services.focusTracker,
389
378
  },
390
379
  shell: {
391
380
  bookmarkManager: ctx.services.bookmarkManager,
@@ -396,7 +385,7 @@ async function main() {
396
385
  },
397
386
  sessions: {
398
387
  sessionManager: ctx.services.sessionManager,
399
- sessionBroker: ctx.services.sessionBroker,
388
+ sessionBroker: uiServices.sessions.sessionBroker,
400
389
  sessionOrchestration: ctx.services.sessionOrchestration,
401
390
  sessionMemoryStore: ctx.services.sessionMemoryStore,
402
391
  },
@@ -415,9 +404,8 @@ async function main() {
415
404
  input.setConversationManager(conversation);
416
405
  input.setContentWidth(getPromptContentWidth());
417
406
  input.filePicker.setOnUpdate(() => render());
418
- input.agentDetailModal.setOnRefresh(() => render());
419
- input.processModal.setOnRefresh(() => render());
420
- setOpenAgentDetail((id) => input.agentDetailModal.open(id));
407
+ // W6.1 retirement: agentDetailModal/processModal setOnRefresh wiring removed —
408
+ // those modals were deleted (Fleet subsumes the live process tree via F2).
421
409
 
422
410
  // Model picker callback is handled in bootstrap.ts — do not duplicate here.
423
411
  input.setHistory(inputHistory);
@@ -482,7 +470,7 @@ async function main() {
482
470
  .slice(0, promptInfo.visibleCursorLine)
483
471
  .reduce((sum: number, line: string) => sum + line.length + 1, 0) + promptInfo.visibleCursorCol
484
472
  : undefined,
485
- usage: { up: orchestrator.usage.input, down: orchestrator.usage.output },
473
+ usage: { up: orchestrator.usage.input, down: orchestrator.usage.output, fleetCostUsd: footerFleetCost(() => ctx.services.processRegistry.query().nodes, runningAgentCount > 0) },
486
474
  showExitNotice: input.showExitNotice,
487
475
  lastCopyTime: input.lastCopyTime,
488
476
  model: runtime.model,
@@ -509,8 +497,8 @@ async function main() {
509
497
  lastInputTokens: orchestrator.lastInputTokens,
510
498
  commandArgsHint,
511
499
  hitlMode: modeManager.getHITLMode(),
512
- runningAgentCount,
513
- runningProcessCount,
500
+ // S3d: cross-surface spine posture segment (adopted-daemon mode only).
501
+ sessionSpineStatus: (() => { const s = uiServices.platform.externalServices?.inspect(); return s?.sessionSpineActive && s.sessionSpineStatus && s.sessionSpineStatus !== 'unknown' ? s.sessionSpineStatus : undefined; })(), runningAgentCount, runningProcessCount,
514
502
  // Composer must not read as focused while the panel/process indicator owns keyboard focus.
515
503
  promptFocused: !input.panelFocused && !input.indicatorFocused,
516
504
  indicatorFocused: input.indicatorFocused,
@@ -557,7 +545,7 @@ async function main() {
557
545
  // Calculate how many rows are consumed by overlays (thinking, permissions, queue, file picker)
558
546
  let overlayRows = 0;
559
547
  if (orchestrator.isThinking) overlayRows += 2; // spinner + blank
560
- if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission);
548
+ if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded);
561
549
  overlayRows += orchestrator.messageQueue.length * 3; // queued messages
562
550
  // File picker and model picker overlay rows computed from actual rendered line count below
563
551
  // Selection modal overlay rows are computed from actual rendered line count below
@@ -596,6 +584,7 @@ async function main() {
596
584
  turnElapsedMs,
597
585
  streamMetrics.ttftMs,
598
586
  stallInfo,
587
+ pendingPermission !== null,
599
588
  );
600
589
  viewport.push(...thinking);
601
590
  // Live tool timer: render the currently executing tool row with ticking elapsed.
@@ -606,7 +595,7 @@ async function main() {
606
595
  }
607
596
 
608
597
  if (pendingPermission) {
609
- viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission));
598
+ viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded));
610
599
  }
611
600
 
612
601
  orchestrator.messageQueue.forEach(msg => {
@@ -653,10 +642,11 @@ async function main() {
653
642
  });
654
643
  };
655
644
  const renderScheduler = createRenderScheduler(renderNow); // WO-208 same-tick coalescer
656
- const render = (): void => renderScheduler.schedule();
657
- const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, notify: (message) => { systemMessageRouter.low(message); render(); } });
645
+ const render = (): void => renderScheduler.schedule(); // captured direct writes → activity log + quiet /debug counter, not repeated transcript lines (UX-B 1a)
646
+ const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, onCapture: (total) => { commandContext.session.runtime.terminalWritesIntercepted = total; render(); } });
658
647
 
659
648
  setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
649
+ setPanelFrameRequester(render); // live panels repaint when idle (Wave-6 replay: fleet sat stale until keypress)
660
650
  orchestratorRefs.requestRender = render;
661
651
  commandContext.renderRequest = render;
662
652
  wireShellUiOpeners({
@@ -672,6 +662,7 @@ async function main() {
672
662
  subscriptionManager,
673
663
  secretsManager,
674
664
  serviceRegistry: ctx.services.serviceRegistry,
665
+ memoryEmbeddingRegistry: ctx.services.memoryEmbeddingRegistry,
675
666
  workingDirectory: workingDir,
676
667
  homeDirectory,
677
668
  getConfiguredProviderIds: ctx._getConfiguredProviderIds,
@@ -694,7 +685,7 @@ async function main() {
694
685
  gitStatusProvider,
695
686
  lastGitInfoRef,
696
687
  buildSessionContinuityHints,
697
- render, webhookNotifier: ctx.services.webhookNotifier,
688
+ render, webhookNotifier: ctx.services.webhookNotifier, focusTracker: ctx.services.focusTracker,
698
689
  });
699
690
  unsubs.push(...turnUnsubs);
700
691
 
@@ -714,6 +705,7 @@ async function main() {
714
705
  events: uiServices.events, orchestrator, providerRegistry,
715
706
  systemMessageRouter, render, metrics: streamMetrics,
716
707
  providerOptimizer: ctx.services.providerOptimizer, costLookup: providerRegistry, retryTurn,
708
+ isApprovalPending: () => pendingPermission !== null,
717
709
  });
718
710
  unsubs.push(...streamResult.unsubs);
719
711
  // Activate one-key retry affordance when a user-visible error surfaces.
@@ -736,16 +728,19 @@ async function main() {
736
728
  stdin.setRawMode(true);
737
729
  stdin.resume();
738
730
  stdin.setEncoding('utf8');
739
- allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE));
731
+ allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE + FOCUS_ENABLE));
732
+ // DEBT-2: forced dark/light applies before first paint; auto (TTY only) fires the
733
+ // OSC 11 probe and repaints once if light wins. filterInput strips the reply from stdin.
734
+ const themeProbe = installBackgroundThemeProbe({ configManager, isTTY: Boolean(stdout.isTTY), env: process.env, writeQuery: (b) => allowTerminalWrite(() => stdout.write(b)), requestRepaint: () => { compositor.resetDiff(); render(); } });
740
735
 
741
736
  applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, render });
742
737
 
743
- stdin.on('data', (data: string) => {
738
+ stdin.on('data', (raw: string) => {
739
+ const data = themeProbe.filterInput(raw); if (data.length === 0) return;
744
740
  const blocking = handleBlockingShellInput({
745
741
  data, pendingPermission, recoveryPending, conversation, systemMessageRouter, render,
746
742
  abortTurn: () => orchestrator.abort(),
747
- loadRecoveryConversation: () => loadRecoveryConversation({ homeDirectory }),
748
- deleteRecoveryFile: () => deleteRecoveryFile({ homeDirectory }),
743
+ ...createRecoveryFileOps(() => recoverySource, { homeDirectory }),
749
744
  homeDirectory, sessionId: runtime.sessionId,
750
745
  persistSnapshot: createPersistRecoverySnapshot({ sessionManager: ctx.services.sessionManager, runtime, conversation }),
751
746
  reopenPanels: createReopenRecoveryPanels({ panelManager, render }),
@@ -772,9 +767,10 @@ async function main() {
772
767
  conversation.rebuildHistory();
773
768
  render();
774
769
 
775
- // --- Crash recovery check ---
776
- const recoveryInfo = checkRecoveryFile({ workingDirectory: workingDir, homeDirectory });
770
+ // --- Crash recovery check (also checks the preserve-on-dismiss sibling; see recovery-input-helpers.ts) ---
771
+ const recoveryInfo = resolveStartupRecoveryInfo({ workingDirectory: workingDir, homeDirectory });
777
772
  if (recoveryInfo) {
773
+ recoverySource = recoveryInfo.source;
778
774
  systemMessageRouter.high(`[Recovery] Found unsaved session from ${new Date(recoveryInfo.timestamp).toLocaleString()}. Title: "${recoveryInfo.title}". Press Ctrl+R to restore, Esc to discard, or start typing to ignore it.`);
779
775
  for (const line of formatReturnContextForDisplay(recoveryInfo.returnContext)) {
780
776
  systemMessageRouter.low(`[Recovery] ${line}`);
@@ -785,6 +781,8 @@ async function main() {
785
781
 
786
782
  // --- Auto-save to recovery file every 60s ---
787
783
  recoveryInterval = setInterval(() => {
784
+ // Skip while an earlier recovery prompt is unresolved (avoids racing preserve-on-dismiss).
785
+ if (recoveryPending) return;
788
786
  const snapshot = conversation.toJSON() as { messages: Array<import('./core/conversation.ts').ConversationMessageSnapshot> };
789
787
  const persisted = buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints());
790
788
  writeRecoveryFile(
@@ -7,7 +7,8 @@ export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool
7
7
 
8
8
  // ---------------------------------------------------------------------------
9
9
  // Shared agent status / stall constants
10
- // Used by AgentInspectorPanel, AgentDetailModal, and cockpit read-model consumers.
10
+ // Used by the Fleet panel's agent views and read-model consumers (formerly also
11
+ // AgentDetailModal, which was retired in W6.1).
11
12
  // ---------------------------------------------------------------------------
12
13
 
13
14
  /** Terminal statuses — cancel not offered; stall check skipped. */
@@ -9,6 +9,20 @@ import { fitDisplay } from '../utils/terminal-width.ts';
9
9
  /** Canonical error-surface foreground (bad/red), kept out of the render body. */
10
10
  const ERROR_FG = '#ef4444';
11
11
 
12
+ /**
13
+ * Frame requester wired by main.ts to the render scheduler. Without it,
14
+ * markDirty() only sets a flag the compositor reads when a frame is ALREADY
15
+ * being composed for another reason (input, streaming) — so a live panel
16
+ * (fleet ticks, registry subscriptions) sat visibly stale while the app was
17
+ * idle until the next keypress (Wave-6 replay finding). The scheduler
18
+ * coalesces same-tick requests, so this stays cheap.
19
+ */
20
+ let panelFrameRequester: (() => void) | null = null;
21
+
22
+ export function setPanelFrameRequester(requester: (() => void) | null): void {
23
+ panelFrameRequester = requester;
24
+ }
25
+
12
26
  export abstract class BasePanel implements Panel {
13
27
  public needsRender = true;
14
28
  public isTransient = false;
@@ -219,7 +233,14 @@ export abstract class BasePanel implements Panel {
219
233
  /** R2: Called by the compositor after a successful render to clear the dirty flag. */
220
234
  public markRendered(): void { this.needsRender = false; }
221
235
 
222
- protected markDirty(): void { this.needsRender = true; }
236
+ protected markDirty(): void {
237
+ // Request only on the false→true transition: a pending frame already
238
+ // covers repeat marks, and a panel that dirties itself mid-render must
239
+ // not schedule frames forever.
240
+ const wasDirty = this.needsRender;
241
+ this.needsRender = true;
242
+ if (!wasDirty) panelFrameRequester?.();
243
+ }
223
244
 
224
245
  /**
225
246
  * Check whether the panel is currently permitted to render.