@pellux/goodvibes-tui 1.0.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,64 +1,16 @@
1
1
  import type { CommandContext } from './command-registry.ts';
2
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
3
2
  import type { Panel } from '../panels/types.ts';
4
3
  import type { PanelManager } from '../panels/panel-manager.ts';
5
- import { FileExplorerPanel } from '../panels/file-explorer-panel.ts';
6
- import { FilePreviewPanel } from '../panels/file-preview-panel.ts';
7
- import { SymbolOutlinePanel } from '../panels/symbol-outline-panel.ts';
8
- import { ApprovalPanel } from '../panels/approval-panel.ts';
9
- import { TasksPanel } from '../panels/tasks-panel.ts';
10
- import { OrchestrationPanel } from '../panels/orchestration-panel.ts';
11
- import { AgentInspectorPanel } from '../panels/agent-inspector-panel.ts';
12
- import { DiffPanel } from '../panels/diff-panel.ts';
13
-
14
- function ensurePreviewPanel(panelManager: PanelManager): FilePreviewPanel | null {
15
- const existing = panelManager.getPanel('preview');
16
- if (existing instanceof FilePreviewPanel) {
17
- const pane = panelManager.getPaneOf('preview');
18
- panelManager.activateById('preview');
19
- if (pane) panelManager.focusPane(pane);
20
- return existing;
21
- }
22
- const targetPane: 'top' | 'bottom' = panelManager.isBottomPaneVisible()
23
- ? (panelManager.getFocusedPane() === 'top' ? 'bottom' : 'top')
24
- : 'bottom';
25
- const opened = panelManager.open('preview', targetPane);
26
- panelManager.show();
27
- panelManager.focusPane(targetPane);
28
- return opened instanceof FilePreviewPanel ? opened : null;
29
- }
30
-
31
- // WO-133: shared by explorer's and preview's 'd' (diff) key — same
32
- // open/focus bridge as ensurePreviewPanel above.
33
- function ensureDiffPanel(panelManager: PanelManager): DiffPanel | null {
34
- const existing = panelManager.getPanel('diff');
35
- if (existing instanceof DiffPanel) {
36
- const pane = panelManager.getPaneOf('diff');
37
- panelManager.activateById('diff');
38
- if (pane) panelManager.focusPane(pane);
39
- return existing;
40
- }
41
- const targetPane: 'top' | 'bottom' = panelManager.isBottomPaneVisible()
42
- ? (panelManager.getFocusedPane() === 'top' ? 'bottom' : 'top')
43
- : 'bottom';
44
- const opened = panelManager.open('diff', targetPane);
45
- panelManager.show();
46
- panelManager.focusPane(targetPane);
47
- return opened instanceof DiffPanel ? opened : null;
48
- }
49
-
50
- // Exported so a future preview-reload action (e.g. an explicit "r" reload
51
- // key) can re-sync the outline against the same file without duplicating
52
- // this lookup — the panel-integration wiring for that key lands separately.
53
- export function syncSymbolOutlineFromPreview(panelManager: PanelManager, previewPanel: FilePreviewPanel): void {
54
- const symbols = panelManager.getPanel('symbols');
55
- const filePath = previewPanel.getCurrentFilePath();
56
- const source = previewPanel.getSource();
57
- if (symbols instanceof SymbolOutlinePanel && filePath && source !== null) {
58
- symbols.loadFile(filePath, source);
59
- }
60
- }
61
4
 
5
+ // W6.1 (the purge): every instanceof-routed branch this file used to carry
6
+ // (FileExplorerPanel/FilePreviewPanel/SymbolOutlinePanel — DELETE;
7
+ // ApprovalPanel/TasksPanel/OrchestrationPanel/AgentInspectorPanel —
8
+ // RETIRE-INTO-FLEET) targeted a panel class that no longer exists. Panels
9
+ // migrated onto the formal `Panel.handlePanelIntegrationAction` hook (the
10
+ // preferred seam — see the comment below) are unaffected; this function now
11
+ // only provides that passthrough. See
12
+ // .goodvibes/audit/2026-07-04-wave6-briefs.json (W6.1) for the disposition
13
+ // map.
62
14
  export function handlePanelIntegrationAction(
63
15
  panelManager: PanelManager,
64
16
  activePanel: Panel | null,
@@ -77,118 +29,5 @@ export function handlePanelIntegrationAction(
77
29
  if (consumed) return true;
78
30
  }
79
31
 
80
- if ((key === 'enter' || key === 'return' || key === 'right') && activePanel instanceof FileExplorerPanel) {
81
- const filePath = activePanel.getFocusedFilePath();
82
- if (!filePath) return false;
83
- const previewPanel = ensurePreviewPanel(panelManager);
84
- if (!previewPanel) return false;
85
- previewPanel.openFile(filePath);
86
- syncSymbolOutlineFromPreview(panelManager, previewPanel);
87
- return true;
88
- }
89
-
90
- // WO-133: 'd' on the explorer diffs the currently focused file, and 'd' on
91
- // the preview diffs whatever file is currently open — both against HEAD,
92
- // both via the same DiffPanel.showFileDiffs entry point.
93
- if (key === 'd' && activePanel instanceof FileExplorerPanel) {
94
- const filePath = activePanel.getFocusedFilePath();
95
- if (!filePath) return false;
96
- const diffPanel = ensureDiffPanel(panelManager);
97
- if (!diffPanel) return false;
98
- void diffPanel.showFileDiffs([filePath], 'HEAD').catch((err) => {
99
- logger.debug('explorer diff dispatch failed', { err });
100
- });
101
- return true;
102
- }
103
-
104
- if (key === 'd' && activePanel instanceof FilePreviewPanel) {
105
- const filePath = activePanel.getCurrentFilePath();
106
- if (!filePath) return false;
107
- const diffPanel = ensureDiffPanel(panelManager);
108
- if (!diffPanel) return false;
109
- void diffPanel.showFileDiffs([filePath], 'HEAD').catch((err) => {
110
- logger.debug('preview diff dispatch failed', { err });
111
- });
112
- return true;
113
- }
114
-
115
- // WO-133: 'r' on the preview reloads the open file from disk, then
116
- // re-syncs the symbol outline against the refreshed content (WO-126's
117
- // async tree-sitter loadFile via syncSymbolOutlineFromPreview above) once
118
- // the reload actually settles.
119
- if (key === 'r' && activePanel instanceof FilePreviewPanel) {
120
- const reloaded = activePanel.consumePendingReload();
121
- if (!reloaded) return false;
122
- void reloaded.then(() => syncSymbolOutlineFromPreview(panelManager, activePanel)).catch((err) => {
123
- logger.debug('preview reload dispatch failed', { err });
124
- });
125
- return true;
126
- }
127
-
128
- if ((key === 'enter' || key === 'return') && activePanel instanceof SymbolOutlinePanel) {
129
- const location = activePanel.getSelectedLocation();
130
- if (!location) return false;
131
- const previewPanel = ensurePreviewPanel(panelManager);
132
- if (!previewPanel) return false;
133
- if (previewPanel.getCurrentFilePath() !== location.path) {
134
- previewPanel.openFile(location.path);
135
- syncSymbolOutlineFromPreview(panelManager, previewPanel);
136
- }
137
- previewPanel.goToLine(location.line);
138
- return true;
139
- }
140
-
141
- if ((key === 'enter' || key === 'return') && activePanel instanceof ApprovalPanel) {
142
- const command = activePanel.getSelectedCommand();
143
- if (!command || !commandContext?.executeCommand) return false;
144
- const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
145
- const [name, ...args] = parts;
146
- if (!name) return false;
147
- void commandContext.executeCommand(name, args).catch((err) => { logger.debug('approval panel command dispatch failed', { err }); });
148
- return true;
149
- }
150
-
151
- // WO-131: Enter on a Tasks row — agent-kind tasks jump straight to the Agent
152
- // Inspector (which owns the deep per-agent timeline); everything else's
153
- // advertised worktree follow-up is dispatched for real via ctx.executeCommand
154
- // instead of being printed as a static "/worktree task <task-id>" signpost.
155
- // w dispatches the task-family posture review the header line advertises.
156
- if ((key === 'enter' || key === 'return' || key === 'w') && activePanel instanceof TasksPanel) {
157
- const followUp = activePanel.consumePendingFollowUp();
158
- if (!followUp) return false;
159
- if (followUp.kind === 'agent-jump') {
160
- const inspector = panelManager.open('inspector');
161
- if (!(inspector instanceof AgentInspectorPanel)) return false;
162
- inspector.inspectAgent(followUp.agentId);
163
- return true;
164
- }
165
- if (!commandContext?.executeCommand) return false;
166
- if (followUp.kind === 'teamwork-review') {
167
- void commandContext.executeCommand('teamwork', ['review']).catch((err) => {
168
- logger.debug('tasks panel teamwork review dispatch failed', { err });
169
- });
170
- return true;
171
- }
172
- void commandContext.executeCommand('worktree', ['task', followUp.taskId]).catch((err) => {
173
- logger.debug('tasks panel worktree review dispatch failed', { err });
174
- });
175
- return true;
176
- }
177
-
178
- // WO-131: Enter on a node-focused Orchestration row jumps to the Agent
179
- // Inspector (agent-backed nodes) or the Tasks panel (task-backed nodes).
180
- if ((key === 'enter' || key === 'return') && activePanel instanceof OrchestrationPanel) {
181
- const jump = activePanel.consumePendingNodeJump();
182
- if (!jump) return false;
183
- if (jump.kind === 'agent-jump') {
184
- const inspector = panelManager.open('inspector');
185
- if (!(inspector instanceof AgentInspectorPanel)) return false;
186
- inspector.inspectAgent(jump.id);
187
- return true;
188
- }
189
- panelManager.open('tasks');
190
- return true;
191
- }
192
-
193
32
  return false;
194
33
  }
@@ -14,6 +14,7 @@ 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';
17
18
  import {
18
19
  SETTINGS_CATEGORIES,
19
20
  type FlagEntry,
@@ -130,6 +131,25 @@ export function buildSettingGroups(
130
131
  behaviorEntries.push(buildNotifyAfterSecondsSyntheticEntry(configManager));
131
132
  }
132
133
 
134
+ // Inject the W2.3 alert-class toggles + master focus gate. TUI-local
135
+ // synthetic settings, same rationale as notifyAfterSeconds above.
136
+ if (behaviorEntries) {
137
+ for (const entry of buildNotifyAlertSyntheticEntries(configManager)) {
138
+ if (!behaviorEntries.some((e) => e.setting.key === entry.setting.key)) {
139
+ behaviorEntries.push(entry);
140
+ }
141
+ }
142
+ }
143
+
144
+ // Wave 5 (wo804): inject the storage.codeIndexEnabled toggle into the
145
+ // storage category. TUI-local synthetic setting (not in the SDK ConfigKey
146
+ // union — see code-index-services.ts), same rationale as
147
+ // notifyAfterSeconds above: opt-in, default off, states its own bounds.
148
+ const storageEntries = groups.get('storage');
149
+ if (storageEntries && !storageEntries.some((e) => e.setting.key === (CODE_INDEX_ENABLED_CONFIG_KEY as ConfigKey))) {
150
+ storageEntries.push(buildCodeIndexEnabledSyntheticEntry(configManager));
151
+ }
152
+
133
153
  return groups;
134
154
  }
135
155
 
@@ -224,6 +244,81 @@ export function buildNotifyAfterSecondsSyntheticEntry(configManager: Pick<Config
224
244
  };
225
245
  }
226
246
 
247
+ // ---------------------------------------------------------------------------
248
+ // W2.3 alert-class synthetic settings — behavior.notifyOn* + notifyOnlyWhenUnfocused
249
+ // ---------------------------------------------------------------------------
250
+
251
+ /**
252
+ * The five W2.3 alert-gating booleans, all TUI-local (not yet in the SDK
253
+ * ConfigKey union), all defaulting to on. Read/written generically by
254
+ * core/alert-gating.ts (readBooleanConfig) and the per-alert-class modules
255
+ * (budget-breach-notifier.ts, approval-alert.ts, turn-event-wiring.ts,
256
+ * long-task-notifier.ts) — this is only the settings-modal-visible surface.
257
+ */
258
+ const NOTIFY_ALERT_SYNTHETIC_SETTINGS: ReadonlyArray<{ readonly key: string; readonly description: string }> = [
259
+ {
260
+ key: 'behavior.notifyOnBudgetBreach',
261
+ description: 'Alert when session cost crosses the configured budget (set via the Cost panel\'s "b" key).',
262
+ },
263
+ {
264
+ key: 'behavior.notifyOnAgentFailure',
265
+ description: 'Alert when a delegated or background agent fails.',
266
+ },
267
+ {
268
+ key: 'behavior.notifyOnChainFailure',
269
+ description: 'Alert when a WRFC review chain fails.',
270
+ },
271
+ {
272
+ key: 'behavior.notifyOnApprovalPending',
273
+ description: 'Alert when a tool call is waiting on your approval.',
274
+ },
275
+ {
276
+ key: 'behavior.notifyOnlyWhenUnfocused',
277
+ 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.',
278
+ },
279
+ ];
280
+
281
+ function buildBooleanSyntheticEntry(
282
+ configManager: Pick<ConfigManager, 'get'>,
283
+ key: string,
284
+ description: string,
285
+ defaultValue: boolean,
286
+ ): SettingEntry {
287
+ const raw = configManager.get(key as ConfigKey);
288
+ const currentValue = typeof raw === 'boolean' ? raw : defaultValue;
289
+ return {
290
+ setting: { key: key as ConfigKey, type: 'boolean', default: defaultValue, description },
291
+ currentValue,
292
+ isDefault: currentValue === defaultValue,
293
+ };
294
+ }
295
+
296
+ /** Build the five synthetic SettingEntry rows for the behavior category. */
297
+ export function buildNotifyAlertSyntheticEntries(configManager: Pick<ConfigManager, 'get'>): SettingEntry[] {
298
+ return NOTIFY_ALERT_SYNTHETIC_SETTINGS.map((spec) => buildBooleanSyntheticEntry(configManager, spec.key, spec.description, true));
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Wave 5 (wo804) — storage.codeIndexEnabled synthetic setting
303
+ // ---------------------------------------------------------------------------
304
+
305
+ /**
306
+ * The repo source-tree code index's auto-build-on-startup toggle
307
+ * (code-index-services.ts). TUI-local, default OFF: /codebase build is the
308
+ * explicit trigger unless a user opts in here. Honest bounds stated inline
309
+ * so enabling this isn't a surprise — see code-index-services.ts's
310
+ * CODE_INDEX_MAX_FILES/CODE_INDEX_MAX_FILE_BYTES for the numbers this
311
+ * description would otherwise duplicate as magic numbers.
312
+ */
313
+ export function buildCodeIndexEnabledSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
314
+ return buildBooleanSyntheticEntry(
315
+ configManager,
316
+ CODE_INDEX_ENABLED_CONFIG_KEY,
317
+ '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.',
318
+ false,
319
+ );
320
+ }
321
+
227
322
  // ---------------------------------------------------------------------------
228
323
  // buildFlagEntries — snapshot of current feature flag states
229
324
  // ---------------------------------------------------------------------------
@@ -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
  }
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,6 +32,7 @@ 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
37
  import {
37
38
  checkRecoveryFile,
@@ -62,12 +63,16 @@ import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetrics
62
63
  import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
63
64
  import { buildContextStatusHint } from './renderer/context-status-hint.ts';
64
65
  import { evaluateSessionMaintenance } from '@/runtime/index.ts';
66
+ import { createCancelGeneration } from './core/turn-cancellation.ts';
67
+ import { wrapRequestPermissionWithAlert } from './core/approval-alert.ts';
68
+ import { setPanelFrameRequester } from './panels/base-panel.ts';
65
69
 
66
70
  const ALT_SCREEN_ENTER = '\x1b[?1049h'; const ALT_SCREEN_EXIT = '\x1b[?1049l';
67
71
  const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h'; const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
68
72
  const CURSOR_HIDE = '\x1b[?25l'; const CURSOR_SHOW = '\x1b[?25h'; const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
69
73
  const KEYBOARD_EXT_ENABLE = '\x1b[>4;2m' + '\x1b[?1u'; const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
70
74
  const PASTE_ENABLE = '\x1b[?2004h'; const PASTE_DISABLE = '\x1b[?2004l';
75
+ const FOCUS_ENABLE = '\x1b[?1004h'; const FOCUS_DISABLE = '\x1b[?1004l';
71
76
 
72
77
  async function main() {
73
78
  const stdout = process.stdout;
@@ -103,22 +108,17 @@ async function main() {
103
108
  permissionPromptRef,
104
109
  _writeLastSessionPointer: writeLastSessionPointer,
105
110
  systemMessageRouter,
106
- setOpenAgentDetail,
107
111
  } = ctx;
108
112
  const workingDir = ctx.services.workingDirectory;
109
113
  const homeDirectory = ctx.services.homeDirectory;
110
114
  const { approvalBroker, agentManager, modeManager, processManager, providerRegistry, secretsManager, subscriptionManager } = ctx.services;
111
115
  conversation.setSessionMemoryStore(ctx.services.sessionMemoryStore);
112
116
  conversation.setSessionLineageTracker(ctx.services.sessionLineageTracker);
117
+ // Shared payload (single source of truth, includes wo805's memoryRegistry —
118
+ // see orchestrator-core-services.ts) plus this site's favoritesStore.
113
119
  orchestrator.setCoreServices({
114
- configManager,
115
- providerRegistry,
120
+ ...buildSharedOrchestratorCoreServices({ services: ctx.services, configManager, providerRegistry }),
116
121
  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
122
  });
123
123
  ctx.services.wrfcController.setPlanManager(ctx.services.planManager);
124
124
  let activeConversationWidth = stdout.columns || 80;
@@ -130,13 +130,9 @@ async function main() {
130
130
  }
131
131
  }
132
132
 
133
- // TUI default: show token speed ON. The SDK schema default is false;
134
- // applyRuntimeConfigDefault reads both the global settings file and the project
135
- // settings file from disk before deciding whether to apply the default. If the
136
- // user has explicitly set this key to false in EITHER their global or project
137
- // persisted config, their value is respected and the default is NOT applied.
138
- // Only when the key is absent from both files (e.g. a new install) does the
139
- // TUI default of true take effect in-memory — no disk write occurs either way.
133
+ // TUI default: show token speed ON (SDK schema default is false). applyRuntimeConfigDefault
134
+ // reads the global + project settings files and only applies the default in-memory when the
135
+ // key is absent from both (e.g. a new install); an explicit user value is respected. No disk write.
140
136
  applyRuntimeConfigDefault(configManager, 'display.showTokenSpeed', true);
141
137
 
142
138
  const panelManager = ctx.services.panelManager;
@@ -180,6 +176,8 @@ async function main() {
180
176
  ttftRecorded: false,
181
177
  activeToolStartedAtMs: undefined,
182
178
  activeToolName: undefined,
179
+ lastDeltaAtMs: undefined, stallEpisode: 0,
180
+ reconnectAttempt: undefined, reconnectMaxAttempts: undefined,
183
181
  };
184
182
 
185
183
  const getPromptContentWidth = () => computePromptContentWidth(stdout.columns);
@@ -190,10 +188,8 @@ async function main() {
190
188
  const currentModel = providerRegistry.getCurrentModel();
191
189
  const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
192
190
  const rows = stdout.rows || 24;
193
- // Compact threshold must match buildShellFooter's `compact: height < 30`
194
- // posture below — otherwise the cached-height fast path in
195
- // estimateShellFooterHeight can answer a compact-vs-non-compact query
196
- // with the wrong cached mode and throw the viewport math off by several rows.
191
+ // Compact threshold must match buildShellFooter's `compact: height < 30` posture below,
192
+ // else estimateShellFooterHeight's cached-height fast path answers with the wrong mode.
197
193
  return rows - 2 - estimateShellFooterHeight(promptLines, contextWindow, rows < 30);
198
194
  };
199
195
 
@@ -223,7 +219,7 @@ async function main() {
223
219
  stdout,
224
220
  ctx,
225
221
  noAltScreen: cli.flags.noAltScreen,
226
- ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW },
222
+ ansi: { CLEAR_SCREEN, ALT_SCREEN_EXIT, PASTE_DISABLE, KEYBOARD_EXT_DISABLE, MOUSE_DISABLE, CURSOR_SHOW, FOCUS_DISABLE },
227
223
  getInput: () => input,
228
224
  render: () => renderScheduler.flushNow(), // resize: synchronous immediate path
229
225
  getPromptContentWidth,
@@ -307,12 +303,7 @@ async function main() {
307
303
  }
308
304
  };
309
305
 
310
- const cancelGeneration = () => {
311
- spokenTurns.stop('Spoken output stopped.');
312
- if (orchestrator.isThinking) {
313
- orchestrator.abort();
314
- }
315
- };
306
+ const cancelGeneration = createCancelGeneration(orchestrator, spokenTurns);
316
307
 
317
308
  const jumpToBookmark = (key: string) => {
318
309
  conversation.getDisplayBlocks();
@@ -341,6 +332,7 @@ async function main() {
341
332
  commandContext.pasteFromClipboard = () => input.handlePaste();
342
333
  commandContext.executeCommand = (name, args) => commandRegistry.execute(name, args, commandContext);
343
334
  commandContext.cancelGeneration = cancelGeneration;
335
+ commandContext.isGenerating = () => orchestrator.isThinking;
344
336
  commandContext.jumpToBookmark = jumpToBookmark;
345
337
  commandContext.scrollToLine = scrollToLine;
346
338
  commandContext.clearScreen = () => {
@@ -348,14 +340,15 @@ async function main() {
348
340
  allowTerminalWrite(() => stdout.write(CLEAR_SCREEN));
349
341
  render();
350
342
  };
351
- permissionPromptRef.requestPermission = (request) =>
343
+ commandContext.requestFullRepaint = () => { compositor.resetDiff(); render(); };
344
+ permissionPromptRef.requestPermission = wrapRequestPermissionWithAlert((request) =>
352
345
  new Promise((resolve) => {
353
346
  pendingPermission = {
354
347
  ...request,
355
- resolve: (approved: boolean, remember = false) => resolve({ approved, remember }),
348
+ ...buildPendingPermissionExtras(request, resolve),
356
349
  };
357
350
  render();
358
- });
351
+ }), { focusTracker: ctx.services.focusTracker, configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]), webhookNotifier: ctx.services.webhookNotifier });
359
352
 
360
353
  const input: InputHandler = new InputHandler(
361
354
  () => render(),
@@ -388,7 +381,7 @@ async function main() {
388
381
  replayEngine: ctx.services.replayEngine,
389
382
  webhookNotifier: ctx.services.webhookNotifier,
390
383
  policyRuntimeState: ctx.services.policyRuntimeState,
391
- externalServices: uiServices.platform.externalServices,
384
+ externalServices: uiServices.platform.externalServices, focusTracker: ctx.services.focusTracker,
392
385
  },
393
386
  shell: {
394
387
  bookmarkManager: ctx.services.bookmarkManager,
@@ -418,9 +411,8 @@ async function main() {
418
411
  input.setConversationManager(conversation);
419
412
  input.setContentWidth(getPromptContentWidth());
420
413
  input.filePicker.setOnUpdate(() => render());
421
- input.agentDetailModal.setOnRefresh(() => render());
422
- input.processModal.setOnRefresh(() => render());
423
- setOpenAgentDetail((id) => input.agentDetailModal.open(id));
414
+ // W6.1 retirement: agentDetailModal/processModal setOnRefresh wiring removed —
415
+ // those modals were deleted (Fleet subsumes the live process tree via F2).
424
416
 
425
417
  // Model picker callback is handled in bootstrap.ts — do not duplicate here.
426
418
  input.setHistory(inputHistory);
@@ -514,6 +506,8 @@ async function main() {
514
506
  hitlMode: modeManager.getHITLMode(),
515
507
  runningAgentCount,
516
508
  runningProcessCount,
509
+ // Composer must not read as focused while the panel/process indicator owns keyboard focus.
510
+ promptFocused: !input.panelFocused && !input.indicatorFocused,
517
511
  indicatorFocused: input.indicatorFocused,
518
512
  runningAgentProgress: runningAgentSummary.progress,
519
513
  composerMode: composerState.modeLabel,
@@ -558,7 +552,7 @@ async function main() {
558
552
  // Calculate how many rows are consumed by overlays (thinking, permissions, queue, file picker)
559
553
  let overlayRows = 0;
560
554
  if (orchestrator.isThinking) overlayRows += 2; // spinner + blank
561
- if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission);
555
+ if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission, pendingPermission.hunkState);
562
556
  overlayRows += orchestrator.messageQueue.length * 3; // queued messages
563
557
  // File picker and model picker overlay rows computed from actual rendered line count below
564
558
  // Selection modal overlay rows are computed from actual rendered line count below
@@ -584,6 +578,8 @@ async function main() {
584
578
  const partialToolPreview = showPreview ? sessionSnapshot.streamToolPreview : undefined;
585
579
  // Elapsed from turn start (stream or tool execution), used for the thinking indicator timer.
586
580
  const turnElapsedMs = streamMetrics.startTime > 0 ? Date.now() - streamMetrics.startTime : undefined;
581
+ // Suppressed while a tool executes — its ticking timer is the honest indicator then.
582
+ const stallInfo = UIFactory.computeRenderStallInfo(streamMetrics, Date.now());
587
583
  const thinking = UIFactory.createThinkingFragment(
588
584
  conversationWidth,
589
585
  orchestrator.getSpinner(),
@@ -594,6 +590,7 @@ async function main() {
594
590
  orchestrator.streamingOutputTokens > 0 ? orchestrator.streamingOutputTokens : undefined,
595
591
  turnElapsedMs,
596
592
  streamMetrics.ttftMs,
593
+ stallInfo,
597
594
  );
598
595
  viewport.push(...thinking);
599
596
  // Live tool timer: render the currently executing tool row with ticking elapsed.
@@ -604,7 +601,7 @@ async function main() {
604
601
  }
605
602
 
606
603
  if (pendingPermission) {
607
- viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission));
604
+ viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState));
608
605
  }
609
606
 
610
607
  orchestrator.messageQueue.forEach(msg => {
@@ -655,6 +652,7 @@ async function main() {
655
652
  const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, notify: (message) => { systemMessageRouter.low(message); render(); } });
656
653
 
657
654
  setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
655
+ setPanelFrameRequester(render); // live panels repaint when idle (Wave-6 replay: fleet sat stale until keypress)
658
656
  orchestratorRefs.requestRender = render;
659
657
  commandContext.renderRequest = render;
660
658
  wireShellUiOpeners({
@@ -670,6 +668,7 @@ async function main() {
670
668
  subscriptionManager,
671
669
  secretsManager,
672
670
  serviceRegistry: ctx.services.serviceRegistry,
671
+ memoryEmbeddingRegistry: ctx.services.memoryEmbeddingRegistry,
673
672
  workingDirectory: workingDir,
674
673
  homeDirectory,
675
674
  getConfiguredProviderIds: ctx._getConfiguredProviderIds,
@@ -692,15 +691,14 @@ async function main() {
692
691
  gitStatusProvider,
693
692
  lastGitInfoRef,
694
693
  buildSessionContinuityHints,
695
- render, webhookNotifier: ctx.services.webhookNotifier,
694
+ render, webhookNotifier: ctx.services.webhookNotifier, focusTracker: ctx.services.focusTracker,
696
695
  });
697
696
  unsubs.push(...turnUnsubs);
698
697
 
699
698
  // Stable turn context for failover retry — set in submitInput, read by retryTurn.
700
699
  let retryCtx: { count: number; text: string; content?: ContentPart[]; opts?: Parameters<typeof orchestrator.handleUserInput>[2] } | null = null;
701
- // One-key retry affordance: active immediately after a user-visible TURN_ERROR.
702
- // While active, 'r' re-submits on the current provider, 'm' opens the model
703
- // picker. Any other character clears the affordance and routes normally.
700
+ // One-key retry affordance, active right after a user-visible TURN_ERROR: 'r' re-submits on the
701
+ // current provider, 'm' opens the model picker, any other character clears it and routes normally.
704
702
  let errorAffordanceActive = false;
705
703
  const retryTurn = (): void => {
706
704
  if (!retryCtx) return;
@@ -724,9 +722,8 @@ async function main() {
724
722
  }
725
723
  });
726
724
 
727
- // Register terminal-restoring crash/termination handlers BEFORE entering raw mode so a
728
- // throw during terminal setup or the initial render still restores the terminal; the
729
- // 'exit' listener is the final safety net for any process.exit path.
725
+ // Register terminal-restoring crash/termination handlers BEFORE entering raw mode so a throw
726
+ // during setup or the initial render still restores the terminal; 'exit' is the final safety net.
730
727
  process.on('uncaughtException', uncaughtExceptionHandler);
731
728
  process.on('SIGTERM', terminationSignalHandler);
732
729
  process.on('SIGHUP', terminationSignalHandler);
@@ -736,7 +733,7 @@ async function main() {
736
733
  stdin.setRawMode(true);
737
734
  stdin.resume();
738
735
  stdin.setEncoding('utf8');
739
- allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE));
736
+ allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE + FOCUS_ENABLE));
740
737
 
741
738
  applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, render });
742
739