@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
@@ -2,7 +2,12 @@
2
2
  // PanelManager — central manager for panel lifecycle, navigation, and split
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
- import type { Panel, PanelRegistration, PanelCategory } from './types.ts';
5
+ import type { Panel, PanelRegistration, PanelCategory, PanelDeepLinkTarget } from './types.ts';
6
+ export type { PanelDeepLinkTarget } from './types.ts';
7
+ // Type-only, erased at runtime, routed through the `@/` alias (not a relative
8
+ // path) so it stays out of the relative-import graph the architecture
9
+ // cycle-checker walks — same discipline as the PanelManager import in types.ts.
10
+ import type { ConfigModalSurface } from '@/input/config-modal-types.ts';
6
11
 
7
12
  // ---------------------------------------------------------------------------
8
13
  // Pane
@@ -31,6 +36,29 @@ export class PanelManager {
31
36
  private retainedPanels = new Map<string, Panel>();
32
37
  /** Old/absorbed panel id -> merged target id (WO-1xx console merges). */
33
38
  private aliases = new Map<string, string>();
39
+ /**
40
+ * Retired panel id -> modal name (W6.1 purge, MIGRATE-TO-MODAL surfaces).
41
+ * Unlike `aliases` (panel -> panel), a hit here means no panel is ever
42
+ * constructed for this id — `open()` invokes `openModalCallback` instead
43
+ * and returns a sentinel. Registrations are added by WO-A/B; this map and
44
+ * the open()-time check are WO-C's mechanism only.
45
+ */
46
+ private modalRedirects = new Map<string, string>();
47
+ /**
48
+ * Modal name -> the surface the config-modal host renders (W6.1). Built once
49
+ * in builtin-modals.ts (closing over read-models) and looked up by
50
+ * ui-openers' openModal callback. Distinct from `modalRedirects` (panel id ->
51
+ * modal name): a redirect resolves a legacy panel id to a name; this map
52
+ * resolves that name to the actual surface data/actions.
53
+ */
54
+ private modalSurfaces = new Map<string, ConfigModalSurface>();
55
+ /**
56
+ * Late-bound: the modal stack is constructed after PanelManager (same
57
+ * ordering constraint as the openAgentDetail callback in
58
+ * builtin/shared.ts), so this is injected via a setter rather than the
59
+ * constructor.
60
+ */
61
+ private openModalCallback?: (modalName: string) => void;
34
62
  private _visible: boolean = false;
35
63
  private _splitRatio: number = 0.6;
36
64
 
@@ -99,10 +127,76 @@ export class PanelManager {
99
127
  this.aliases.set(aliasId, targetId);
100
128
  }
101
129
 
130
+ /**
131
+ * Register a redirect so `open(panelId)` opens the named modal instead of
132
+ * constructing a panel (MIGRATE-TO-MODAL surfaces). Checked in open()
133
+ * before alias/registry resolution.
134
+ */
135
+ registerModalRedirect(panelId: string, modalName: string): void {
136
+ this.modalRedirects.set(panelId, modalName);
137
+ }
138
+
139
+ /**
140
+ * Inject the callback `open()` invokes when it hits a modal redirect.
141
+ * Late-bound via setter because the modal stack is constructed after
142
+ * PanelManager (mirrors the openAgentDetail callback pattern).
143
+ */
144
+ setOpenModalCallback(callback: (modalName: string) => void): void {
145
+ this.openModalCallback = callback;
146
+ }
147
+
148
+ /**
149
+ * The modal name `panelIdOrAlias` redirects to, if any — lets callers
150
+ * (e.g. the `/panel` command) print "moved to the <name> modal" before or
151
+ * instead of calling open().
152
+ */
153
+ getModalRedirect(panelIdOrAlias: string): string | undefined {
154
+ return this.modalRedirects.get(panelIdOrAlias);
155
+ }
156
+
157
+ /**
158
+ * Register the surface (data + actions) the config-modal host renders for a
159
+ * modal name. Called from builtin-modals.ts alongside registerModalRedirect.
160
+ */
161
+ registerModalSurface(surface: ConfigModalSurface): void {
162
+ this.modalSurfaces.set(surface.name, surface);
163
+ }
164
+
165
+ /** Resolve a modal name to its registered surface, if any. */
166
+ getModalSurface(name: string): ConfigModalSurface | undefined {
167
+ return this.modalSurfaces.get(name);
168
+ }
169
+
102
170
  private _resolveId(panelId: string): string {
103
171
  return this.aliases.get(panelId) ?? panelId;
104
172
  }
105
173
 
174
+ /**
175
+ * Placeholder returned by open() for a modal-redirected id. Never pushed
176
+ * into a pane, never retained, never rendered — it exists only so open()
177
+ * can keep its non-null `Panel` return type without constructing the real
178
+ * (deleted) panel view. `name` carries the modal name so a caller that
179
+ * inspects the returned panel's `name` (rather than calling
180
+ * getModalRedirect() beforehand) still gets an honest answer.
181
+ */
182
+ private _modalRedirectSentinel(panelId: string, modalName: string): Panel {
183
+ return {
184
+ id: panelId,
185
+ name: modalName,
186
+ icon: '·',
187
+ category: 'session',
188
+ onActivate: () => {},
189
+ onDeactivate: () => {},
190
+ onDestroy: () => {},
191
+ render: () => [],
192
+ isTransient: true,
193
+ isPinned: false,
194
+ needsRender: false,
195
+ invalidate: () => {},
196
+ markRendered: () => {},
197
+ };
198
+ }
199
+
106
200
  getRegisteredTypes(): PanelRegistration[] {
107
201
  return [...this.registry];
108
202
  }
@@ -135,7 +229,12 @@ export class PanelManager {
135
229
  this._cachedWorkspaceTabs = null;
136
230
  }
137
231
 
138
- open(panelIdOrAlias: string, pane?: 'top' | 'bottom'): Panel {
232
+ open(panelIdOrAlias: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget): Panel {
233
+ const modalName = this.modalRedirects.get(panelIdOrAlias);
234
+ if (modalName !== undefined) {
235
+ this.openModalCallback?.(modalName);
236
+ return this._modalRedirectSentinel(panelIdOrAlias, modalName);
237
+ }
139
238
  const panelId = this._resolveId(panelIdOrAlias);
140
239
  this._recordRecent(panelId);
141
240
  const existingPane = this._findPaneOf(panelId);
@@ -146,12 +245,12 @@ export class PanelManager {
146
245
  // pane (fixes `/panel open <id> top` and the panel-list T/B move keys).
147
246
  if (pane && pane !== existingPane) {
148
247
  this._moveBetweenPanes(existingPane, pane, panelId);
149
- return this.getPanel(panelId)!;
248
+ return this._deliverDeepLink(this.getPanel(panelId)!, target);
150
249
  }
151
250
  this._activateByIdInPane(panelId, existingPane);
152
251
  this._focusedPane = existingPane;
153
252
  if (existingPane === 'bottom') this._bottomPaneVisible = true;
154
- return this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!;
253
+ return this._deliverDeepLink(this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!, target);
155
254
  }
156
255
 
157
256
  const targetPane = pane ?? this._focusedPane;
@@ -173,6 +272,16 @@ export class PanelManager {
173
272
  }
174
273
  panel.onActivate();
175
274
  this._invalidateWorkspaceTabs();
275
+ return this._deliverDeepLink(panel, target);
276
+ }
277
+
278
+ /**
279
+ * Hand an optional deep-link target (DEBT-5 item 4) to a panel that just
280
+ * resolved from open(). Panels without a "node" concept simply don't
281
+ * implement `receiveDeepLink` — the call is a no-op via optional chaining.
282
+ */
283
+ private _deliverDeepLink(panel: Panel, target: PanelDeepLinkTarget | undefined): Panel {
284
+ if (target) panel.receiveDeepLink?.(target);
176
285
  return panel;
177
286
  }
178
287
 
@@ -387,9 +496,12 @@ export class PanelManager {
387
496
  this.bottomPane.panels.push(panel);
388
497
  this.bottomPane.activeIndex = 0;
389
498
  } else {
390
- // Open a predictable default panel in the bottom pane (the panel list),
391
- // rather than an arbitrary registration-order-dependent panel.
392
- const defaultPanel = this._getRegistration('panel-list') ?? this.registry[0];
499
+ // Open a predictable default panel in the bottom pane. W6.1 purge:
500
+ // 'panel-list' was deleted (dead weight over a 5-panel registry —
501
+ // see the DELETE disposition), so the default is explicitly
502
+ // 'fleet' rather than falling back to registry[0] (whatever
503
+ // registers first, currently 'git' — see risk 5 in the brief).
504
+ const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
393
505
  if (defaultPanel) {
394
506
  this.open(defaultPanel.id, 'bottom');
395
507
  }
@@ -493,9 +605,11 @@ export class PanelManager {
493
605
 
494
606
  toggle(): void {
495
607
  this._visible = !this._visible;
496
- // Auto-open a default panel if toggling visible with nothing open
608
+ // Auto-open a default panel if toggling visible with nothing open.
609
+ // W6.1 purge: explicitly 'fleet' rather than registry[0] — see the
610
+ // matching comment in toggleBottomPane() above.
497
611
  if (this._visible && this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
498
- const defaultPanel = this._getRegistration('panel-list') ?? this.registry[0];
612
+ const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
499
613
  if (defaultPanel) this.open(defaultPanel.id);
500
614
  }
501
615
  }
@@ -1,12 +1,7 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine, createStyledCell } from '../types/grid.ts';
3
3
  import { getDisplayWidth } from '../utils/terminal-width.ts';
4
- import { resolveUiTones } from '../renderer/theme.ts';
5
-
6
- // DEFAULT_PANEL_PALETTE is built from the mode-resolved chrome tones
7
- // (resolveUiTones) rather than the static UI_TONES constant — WO-001 single
8
- // read path. Mode is fixed to 'dark' until the terminal-bg-probe lands.
9
- const UI_TONES = resolveUiTones('dark');
4
+ import { activeUiTones, registerThemeRefresh } from '../renderer/theme.ts';
10
5
 
11
6
  // ---------------------------------------------------------------------------
12
7
  // Panel palette + core line primitives.
@@ -36,24 +31,36 @@ export interface PanelPalette {
36
31
  readonly selectBg?: string;
37
32
  }
38
33
 
39
- export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = {
40
- header: UI_TONES.fg.primary,
41
- headerBg: UI_TONES.bg.title,
42
- label: UI_TONES.fg.muted,
43
- value: UI_TONES.fg.primary,
44
- dim: UI_TONES.fg.dim,
45
- info: UI_TONES.state.info,
46
- good: UI_TONES.state.good,
47
- warn: UI_TONES.state.warn,
48
- bad: UI_TONES.state.bad,
49
- empty: UI_TONES.fg.empty,
50
- surfaceBg: UI_TONES.bg.surface,
51
- sectionBg: UI_TONES.bg.section,
52
- summaryBg: UI_TONES.bg.summary,
53
- inputBg: UI_TONES.bg.input,
54
- accent: UI_TONES.fg.secondary,
55
- selectBg: UI_TONES.bg.selected,
56
- } as const;
34
+ // Built from the mode-resolved chrome tones (activeUiTones). Because 100+ call
35
+ // sites read this object by reference, mode changes rebuild it IN PLACE via the
36
+ // registered refresher below rather than re-resolving per call — see theme.ts's
37
+ // active-mode runtime note. This refresher is registered before any panel's
38
+ // extendPalette() runs (panels import polish → polish-core first), so on a mode
39
+ // flip the base is rebuilt before the extended palettes re-merge from it.
40
+ function buildPanelPalette(): Required<PanelPalette> {
41
+ const t = activeUiTones();
42
+ return {
43
+ header: t.fg.primary,
44
+ headerBg: t.bg.title,
45
+ label: t.fg.muted,
46
+ value: t.fg.primary,
47
+ dim: t.fg.dim,
48
+ info: t.state.info,
49
+ good: t.state.good,
50
+ warn: t.state.warn,
51
+ bad: t.state.bad,
52
+ empty: t.fg.empty,
53
+ surfaceBg: t.bg.surface,
54
+ sectionBg: t.bg.section,
55
+ summaryBg: t.bg.summary,
56
+ inputBg: t.bg.input,
57
+ accent: t.fg.secondary,
58
+ selectBg: t.bg.selected,
59
+ };
60
+ }
61
+
62
+ export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = buildPanelPalette();
63
+ registerThemeRefresh(() => Object.assign(DEFAULT_PANEL_PALETTE as Required<PanelPalette>, buildPanelPalette()));
57
64
 
58
65
  /**
59
66
  * Extend the base panel palette with domain-specific colors.
@@ -73,7 +80,13 @@ export function extendPalette<T extends Record<string, string>>(
73
80
  base: typeof DEFAULT_PANEL_PALETTE,
74
81
  extras: T,
75
82
  ): typeof DEFAULT_PANEL_PALETTE & T {
76
- return { ...base, ...extras };
83
+ const merged = { ...base, ...extras } as typeof DEFAULT_PANEL_PALETTE & T;
84
+ // Self-register an in-place rebuild so every extendPalette-derived panel
85
+ // palette (cost/token/git/skills/diff/wrfc) tracks the active mode with zero
86
+ // per-panel churn. Runs AFTER the base refresher (registered at module eval,
87
+ // before any panel calls this), so `base` already carries the new-mode values.
88
+ registerThemeRefresh(() => Object.assign(merged as Record<string, string>, base, extras));
89
+ return merged;
77
90
  }
78
91
 
79
92
  export function buildPanelLine(
@@ -13,7 +13,7 @@ export interface PlanningAnswerAction {
13
13
  readonly label: string;
14
14
  readonly detail: string;
15
15
  readonly answer: string;
16
- readonly kind?: 'answer' | 'approve' | 'dismiss';
16
+ readonly kind?: 'answer' | 'approve';
17
17
  readonly disabled?: boolean;
18
18
  }
19
19
 
@@ -35,8 +35,12 @@ function compactAnswerDetail(text: string): string {
35
35
  * canned suggestions first (de-duplicated by normalized answer text — a
36
36
  * question that matches more than one keyword category, e.g. scope +
37
37
  * recommendedAnswer echoing one of the scope answers verbatim, must not show
38
- * the same suggested answer twice), then the fixed ask-narrower/custom/
39
- * dismiss actions.
38
+ * the same suggested answer twice), then the fixed ask-narrower/custom actions.
39
+ *
40
+ * DEBT-3: dismissing planning is no longer a pseudo answer-row — it is a
41
+ * first-class confirmed modal action (the `d` key), so no dismiss row is
42
+ * produced here. A canned answer records against the current open question via
43
+ * /plan answer; the custom row's free-form text is submitted to chat.
40
44
  */
41
45
  export function buildAnswerActions(question: ProjectPlanningQuestion, draftAnswer: string): PlanningAnswerAction[] {
42
46
  const canned: PlanningAnswerAction[] = [];
@@ -119,16 +123,9 @@ export function buildAnswerActions(question: ProjectPlanningQuestion, draftAnswe
119
123
  actions.push({
120
124
  id: 'custom',
121
125
  label: 'Submit typed answer',
122
- detail: draftAnswer ? compactAnswerDetail(draftAnswer) : 'Type an answer first; this row becomes the custom answer.',
126
+ detail: draftAnswer ? compactAnswerDetail(draftAnswer) : 'Type an answer first; this row submits it to chat.',
123
127
  answer: draftAnswer.trim(),
124
128
  disabled: !draftAnswer.trim(),
125
129
  });
126
- actions.push({
127
- id: 'dismiss-planning',
128
- label: 'Close planning and continue without it',
129
- detail: 'Pause project planning for this workspace. Normal chat continues; /plan can reopen it later.',
130
- answer: 'Pause project planning for this workspace and continue without the planning panel.',
131
- kind: 'dismiss',
132
- });
133
130
  return actions;
134
131
  }
@@ -5,8 +5,6 @@ import { createEmptyLine } from '../types/grid.ts';
5
5
  import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
6
6
  import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
7
7
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
8
- import { FilePreviewPanel } from './file-preview-panel.ts';
9
- import type { PanelIntegrationContext } from './types.ts';
10
8
  import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
11
9
  import { listInstalledEcosystemEntries, type EcosystemCatalogPathOptions, type ShellPathService } from '@/runtime/index.ts';
12
10
  import {
@@ -257,11 +255,6 @@ export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
257
255
  // I1: confirm state for destructive delete
258
256
  private confirm: ConfirmState | null = null;
259
257
  private readyPromise: Promise<void> | null = null;
260
- // Staged pending action consumed by handlePanelIntegrationAction (same
261
- // pattern as diff-panel.ts's pendingOpenPreview): Enter marks the intent
262
- // here, the actual PanelManager/preview wiring happens once the
263
- // integration context is available.
264
- private pendingOpenPreview = false;
265
258
 
266
259
  public constructor(options: SkillsPanelOptions) {
267
260
  super('skills', 'Skills', '▩', 'automation-control', options.componentHealthMonitor);
@@ -374,50 +367,13 @@ export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
374
367
 
375
368
  public override onDestroy(): void {}
376
369
 
377
- protected override onSelect(_skill: SkillRecord): void {
378
- // Enter opens the skill's markdown source in the preview panel — see
379
- // handlePanelIntegrationAction for the actual PanelManager wiring
380
- // (needs the integration context, not available here). Selection itself
381
- // is read back from getVisibleItems() in that hook. onSelect is only
382
- // invoked by ScrollableListPanel's navigation handler when the filter is
383
- // not active, so no manual guard is needed here.
384
- this.pendingOpenPreview = true;
385
- }
386
-
387
- /**
388
- * Cross-panel integration hook — Enter opens the selected skill's markdown
389
- * source in the preview panel via the same open/focus bridge DiffPanel
390
- * uses (src/input/panel-integration-actions.ts), without this panel
391
- * needing to know about PanelManager pane/focus mechanics beyond what ctx
392
- * exposes.
393
- */
394
- public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
395
- if (!this.pendingOpenPreview) return false;
396
- this.pendingOpenPreview = false;
397
- const skill = this.getSelectedItem();
398
- if (!skill) return false;
399
-
400
- const pm = ctx.panelManager;
401
- let previewPanel = pm.getPanel('preview');
402
- if (previewPanel instanceof FilePreviewPanel) {
403
- const pane = pm.getPaneOf('preview');
404
- pm.activateById('preview');
405
- if (pane) pm.focusPane(pane);
406
- } else {
407
- const targetPane: 'top' | 'bottom' = pm.isBottomPaneVisible()
408
- ? (pm.getFocusedPane() === 'top' ? 'bottom' : 'top')
409
- : 'bottom';
410
- const opened = pm.open('preview', targetPane);
411
- pm.show();
412
- pm.focusPane(targetPane);
413
- previewPanel = opened instanceof FilePreviewPanel ? opened : null;
414
- }
415
- if (previewPanel instanceof FilePreviewPanel) {
416
- previewPanel.openFile(skill.path);
417
- return true;
418
- }
419
- return false;
420
- }
370
+ // W6.1 (the purge): Enter used to open the skill's markdown source in the
371
+ // preview panel via a staged pendingOpenPreview flag + handlePanelIntegrationAction
372
+ // (same pattern diff-panel.ts used). 'preview' is DELETE-disposition with no
373
+ // successor surface (no file-picker-overlay preview to repoint to either
374
+ // verified), so that cross-panel jump was removed rather than repointed;
375
+ // Enter is currently a no-op on this list (browse-only until WO-B migrates
376
+ // Skills to a modal).
421
377
 
422
378
  public handleInput(key: string): boolean {
423
379
  // I1: y/n confirmation dialog for delete
@@ -16,6 +16,18 @@ export interface PanelIntegrationContext {
16
16
  readonly executeCommand?: (name: string, args: string[]) => Promise<unknown>;
17
17
  }
18
18
 
19
+ /**
20
+ * A cross-panel deep-link target (DEBT-5 item 4): a specific node a panel
21
+ * should select + reveal when opened via `PanelManager.open(id, pane, target)`,
22
+ * e.g. a Fleet tree node keyed by its process id and (optionally) its
23
+ * ProcessKind for disambiguation. Panel-agnostic on purpose — only panels that
24
+ * have a "node" concept implement `Panel.receiveDeepLink`.
25
+ */
26
+ export interface PanelDeepLinkTarget {
27
+ readonly id: string;
28
+ readonly kind?: string;
29
+ }
30
+
19
31
  /**
20
32
  * Named logical key identifiers emitted by the input tokenizer.
21
33
  * These are the ONLY key names that will appear in `handleInput` calls;
@@ -126,6 +138,31 @@ export interface Panel {
126
138
  * back to its built-in `instanceof` routing.
127
139
  */
128
140
  handlePanelIntegrationAction?(key: string, ctx: PanelIntegrationContext): boolean;
141
+
142
+ /**
143
+ * Optional: called by the global Ctrl+X (`panel-close`) shortcut BEFORE it
144
+ * closes this panel (handler-shortcuts.ts). Return `true` to consume
145
+ * Ctrl+X for an in-panel action instead — e.g. FleetPanel (Wave-3 session
146
+ * tabs) detaches its active tab and leaves the panel open. Return `false`
147
+ * (or omit this hook) to fall through to the ordinary close behavior.
148
+ * Ctrl+X never reaches a panel's own `handleInput` (ctrl/meta combos are
149
+ * intercepted earlier — see handlePanelFocusToken in
150
+ * src/input/handler-feed-routes.ts), so this is the one seam a panel has
151
+ * to intervene before the global shortcut acts.
152
+ */
153
+ interceptPanelClose?(): boolean;
154
+
155
+ /**
156
+ * Optional: consume a deep-link target handed in by `PanelManager.open(id,
157
+ * pane, target)` (DEBT-5 item 4) — select + reveal the matching node on the
158
+ * panel's next snapshot. A panel without a "node" concept simply doesn't
159
+ * implement this (the call is a no-op via optional chaining in
160
+ * PanelManager). A panel that DOES implement it owns its own honest
161
+ * "not found" surface (e.g. FleetPanel.setError) when the target no longer
162
+ * resolves — PanelManager has no transcript/print seam to report on its
163
+ * behalf.
164
+ */
165
+ receiveDeepLink?(target: PanelDeepLinkTarget): void;
129
166
  }
130
167
 
131
168
  export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' | 'category'> {
@@ -0,0 +1,179 @@
1
+ import type { PermissionPromptRequest } from '@pellux/goodvibes-sdk/platform/permissions';
2
+
3
+ /**
4
+ * Per-hunk accept/reject at the approval gate (W1.3 v1).
5
+ *
6
+ * Scoped to the `edit` tool only: EditInput.edits is already an array of
7
+ * independent, atomic find/replace units (each a natural "hunk"), so no
8
+ * diff/hunk-apply primitive is needed here. The `write` (whole-file
9
+ * overwrite) tool stays whole-payload accept/reject — true whole-file hunk
10
+ * splitting would require a genuinely new SDK diff/patch-apply primitive,
11
+ * flagged as an explicit v2 follow-up, not built here.
12
+ *
13
+ * NOTE ON SDK TYPES: the installed @pellux/goodvibes-sdk (0.36.0) does not
14
+ * yet export `EditItem`/`EditInput` from its `platform/tools` subpath (that
15
+ * export is added in the in-flight SDK change for this work order, landing
16
+ * in 0.37). Until then this module reads `request.args.edits` defensively
17
+ * off a narrow local structural type rather than importing a not-yet-published
18
+ * symbol, per the cross-repo compile contract for this wave.
19
+ */
20
+
21
+ /** Structural shape of one EditItem, read defensively — see module doc. */
22
+ export interface EditItemLike {
23
+ readonly path: string;
24
+ readonly find: string;
25
+ readonly replace: string;
26
+ readonly id?: string | undefined;
27
+ }
28
+
29
+ export interface HunkSelectionState {
30
+ readonly hunks: readonly EditItemLike[];
31
+ readonly cursor: number;
32
+ readonly selected: ReadonlySet<number>;
33
+ }
34
+
35
+ export type HunkCommit = 'apply' | 'cancel' | null;
36
+
37
+ /**
38
+ * Structural widening of the installed SDK's `PermissionPromptDecision`
39
+ * (0.36.0, no `modifiedArgs` field yet) so main.ts can resolve a decision
40
+ * carrying `modifiedArgs` without a nominal import of a not-yet-published
41
+ * SDK symbol. Assign to a variable of this type before calling the SDK's
42
+ * typed `resolve(decision: PermissionPromptDecision)` — passing a variable
43
+ * (not an object literal) sidesteps TypeScript's excess-property check, and
44
+ * the extra field is invisible to 0.36.0 callers, structurally compatible
45
+ * once 0.37 adds it for real.
46
+ */
47
+ export interface PermissionPromptDecisionWithModifiedArgs {
48
+ approved: boolean;
49
+ remember?: boolean;
50
+ modifiedArgs?: Record<string, unknown>;
51
+ }
52
+
53
+ export interface HunkKeyResult {
54
+ readonly state: HunkSelectionState;
55
+ readonly commit: HunkCommit;
56
+ }
57
+
58
+ /** Runtime guard: is `value` shaped like an EditItem? */
59
+ function isEditItemLike(value: unknown): value is EditItemLike {
60
+ if (typeof value !== 'object' || value === null) return false;
61
+ const candidate = value as Record<string, unknown>;
62
+ return (
63
+ typeof candidate['path'] === 'string' &&
64
+ typeof candidate['find'] === 'string' &&
65
+ typeof candidate['replace'] === 'string' &&
66
+ (candidate['id'] === undefined || typeof candidate['id'] === 'string')
67
+ );
68
+ }
69
+
70
+ /** Extracts a validated EditItemLike[] from a permission request's args, or null if not edit-shaped. */
71
+ function readEditItems(args: Record<string, unknown>): EditItemLike[] | null {
72
+ const edits = args['edits'];
73
+ if (!Array.isArray(edits) || edits.length === 0) return null;
74
+ const items: EditItemLike[] = [];
75
+ for (const entry of edits) {
76
+ if (!isEditItemLike(entry)) return null;
77
+ items.push(entry);
78
+ }
79
+ return items;
80
+ }
81
+
82
+ /**
83
+ * isHunkSelectable — true iff this is a multi-edit `edit` tool call. Single-edit
84
+ * `edit` calls, `write` calls, and everything else keep the exact current
85
+ * y/a/n flow untouched (zero behavior change for the common case).
86
+ */
87
+ export function isHunkSelectable(request: PermissionPromptRequest): boolean {
88
+ if (request.tool !== 'edit') return false;
89
+ const items = readEditItems(request.args);
90
+ return items !== null && items.length > 1;
91
+ }
92
+
93
+ /** Builds initial hunk-selection state — all hunks selected by default (matches today's "y = approve everything" expectation as the zero-navigation path). */
94
+ export function buildHunkSelectionState(request: PermissionPromptRequest): HunkSelectionState {
95
+ const hunks = readEditItems(request.args) ?? [];
96
+ return {
97
+ hunks,
98
+ cursor: 0,
99
+ selected: new Set(hunks.map((_, i) => i)),
100
+ };
101
+ }
102
+
103
+ /**
104
+ * applyHunkKey — pure reducer over raw stdin `data` for an active hunk-selection
105
+ * prompt. Mirrors the outer y/a/n prompt's raw-data key routing (not the
106
+ * normalized panel key vocabulary) since Space must be distinguishable from
107
+ * the trimmed/lowercased `key` the outer switch uses.
108
+ */
109
+ export function applyHunkKey(state: HunkSelectionState, data: string): HunkKeyResult {
110
+ const lower = data.toLowerCase().trim();
111
+
112
+ if (data === 'j' || data === '\x1b[B') {
113
+ return { state: { ...state, cursor: Math.min(state.hunks.length - 1, state.cursor + 1) }, commit: null };
114
+ }
115
+ if (data === 'k' || data === '\x1b[A') {
116
+ return { state: { ...state, cursor: Math.max(0, state.cursor - 1) }, commit: null };
117
+ }
118
+ if (data === ' ') {
119
+ const next = new Set(state.selected);
120
+ if (next.has(state.cursor)) {
121
+ next.delete(state.cursor);
122
+ } else {
123
+ next.add(state.cursor);
124
+ }
125
+ return { state: { ...state, selected: next }, commit: null };
126
+ }
127
+ if (lower === 'a') {
128
+ return { state: { ...state, selected: new Set(state.hunks.map((_, i) => i)) }, commit: null };
129
+ }
130
+ if (data === '\r' || data === '\n') {
131
+ if (state.selected.size === 0) {
132
+ // No-op: nothing selected, prompt stays open rather than applying an empty edit.
133
+ return { state, commit: null };
134
+ }
135
+ return { state, commit: 'apply' };
136
+ }
137
+ if (lower === 'y') {
138
+ // Bypass-navigation shortcut: preserves muscle memory for non-hunk prompts.
139
+ return { state: { ...state, selected: new Set(state.hunks.map((_, i) => i)) }, commit: 'apply' };
140
+ }
141
+ if (lower === 'n' || data === '\x1b' || data === '\x03') {
142
+ return { state, commit: 'cancel' };
143
+ }
144
+
145
+ return { state, commit: null };
146
+ }
147
+
148
+ /**
149
+ * buildModifiedEditArgs — returns request.args with `edits` filtered to exactly
150
+ * the selected hunks (in original order), preserving every other EditInput
151
+ * field (match/transaction/output/dry_run/validate) untouched.
152
+ */
153
+ export function buildModifiedEditArgs(
154
+ request: PermissionPromptRequest,
155
+ state: HunkSelectionState,
156
+ ): Record<string, unknown> {
157
+ const filtered = state.hunks.filter((_, i) => state.selected.has(i));
158
+ return { ...request.args, edits: filtered };
159
+ }
160
+
161
+ /**
162
+ * buildPendingPermissionExtras — the `hunkState` + wrapped `resolve` fields
163
+ * main.ts spreads onto its PendingPermissionState alongside `...request`.
164
+ * Bundled into one helper (rather than inlined in main.ts) to keep main.ts's
165
+ * net line count at zero for this change — main.ts sits at the 800-line
166
+ * architecture cap and must never grow it.
167
+ */
168
+ export function buildPendingPermissionExtras(
169
+ request: PermissionPromptRequest,
170
+ resolvePromise: (decision: PermissionPromptDecisionWithModifiedArgs) => void,
171
+ ): {
172
+ hunkState: HunkSelectionState | undefined;
173
+ resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>) => void;
174
+ } {
175
+ return {
176
+ hunkState: isHunkSelectable(request) ? buildHunkSelectionState(request) : undefined,
177
+ resolve: (approved, remember = false, modifiedArgs) => resolvePromise({ approved, remember, modifiedArgs }),
178
+ };
179
+ }