@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
@@ -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
  }
@@ -0,0 +1,97 @@
1
+ // ---------------------------------------------------------------------------
2
+ // panel-mouse-geometry.ts
3
+ //
4
+ // Pure mouse-geometry helpers for the panel workspace, extracted from
5
+ // handler-feed-routes.ts so that file stays under the 800-line architecture
6
+ // cap once the UX-C focus verbs and the DEBT-5 paste-flood guard both live in
7
+ // it. These map a terminal (row, col) onto the panel/pane/workspace-tab under
8
+ // the cursor. No dependency on the feed-route state types, so no import cycle.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ import type { PanelManager } from '../panels/panel-manager.ts';
12
+ import { renderPanelWorkspaceBar } from '../renderer/panel-workspace-bar.ts';
13
+ import type { TabHitRegion } from '../renderer/tab-strip.ts';
14
+
15
+ /** Screen rectangle the panel workspace occupies, plus its vertical split. */
16
+ export type PanelMouseLayout = {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ height: number;
21
+ hasBottomPane: boolean;
22
+ verticalSplitRatio: number;
23
+ };
24
+
25
+ function clampRatio(value: number): number {
26
+ return Math.max(0.2, Math.min(0.8, value));
27
+ }
28
+
29
+ export function getActivePanelInPane(panelManager: PanelManager, pane: 'top' | 'bottom') {
30
+ const target = pane === 'top' ? panelManager.getTopPane() : panelManager.getBottomPane();
31
+ return target.panels[target.activeIndex] ?? null;
32
+ }
33
+
34
+ export function getPanelUnderMouse(
35
+ panelManager: PanelManager,
36
+ layout: PanelMouseLayout | null,
37
+ row: number,
38
+ col: number,
39
+ ) {
40
+ if (
41
+ layout === null
42
+ || !panelManager.isVisible()
43
+ || panelManager.getAllOpen().length === 0
44
+ || col < layout.x
45
+ || col >= layout.x + layout.width
46
+ || row < layout.y
47
+ || row >= layout.y + layout.height
48
+ ) {
49
+ return null;
50
+ }
51
+
52
+ const panelRow = row - layout.y;
53
+ if (!layout.hasBottomPane) {
54
+ return getActivePanelInPane(panelManager, 'top');
55
+ }
56
+
57
+ // Single consolidated workspace bar (row 0) + h-separator; the rest splits
58
+ // between the two panes' content.
59
+ const panelAreaRows = Math.max(0, layout.height - 1);
60
+ const contentRows = Math.max(0, panelAreaRows - 1);
61
+ const topContentRows = contentRows <= 1
62
+ ? contentRows
63
+ : Math.max(1, Math.floor(contentRows * clampRatio(layout.verticalSplitRatio)));
64
+ // panelRow 0 = workspace bar; rows 1..topContentRows = top pane; rest = bottom.
65
+ return panelRow <= topContentRows
66
+ ? getActivePanelInPane(panelManager, 'top')
67
+ : getActivePanelInPane(panelManager, 'bottom');
68
+ }
69
+
70
+ /**
71
+ * If the mouse is over the consolidated workspace tab bar (the first panel
72
+ * row), return the index of the tab under the cursor, else null. Recomputes the
73
+ * tab hit regions by rendering the bar with a layout callback — cheap and keeps
74
+ * the click geometry in lockstep with what was drawn.
75
+ */
76
+ export function workspaceTabAtMouse(
77
+ panelManager: PanelManager,
78
+ layout: PanelMouseLayout | null,
79
+ row: number,
80
+ col: number,
81
+ ): number | null {
82
+ if (
83
+ layout === null
84
+ || !panelManager.isVisible()
85
+ || panelManager.getAllOpen().length === 0
86
+ || row !== layout.y // workspace bar is the first panel row
87
+ || col < layout.x
88
+ || col >= layout.x + layout.width
89
+ ) {
90
+ return null;
91
+ }
92
+ let regions: readonly TabHitRegion[] = [];
93
+ renderPanelWorkspaceBar(panelManager.getWorkspaceTabs(), layout.width, true, (r) => { regions = r; });
94
+ const relCol = col - layout.x;
95
+ const hit = regions.find((rg) => relCol >= rg.startCol && relCol < rg.endCol);
96
+ return hit ? hit.index : null;
97
+ }
@@ -0,0 +1,86 @@
1
+ // ---------------------------------------------------------------------------
2
+ // panel-paste-flood-guard.ts — DEBT-5 item 5.
3
+ //
4
+ // A terminal WITHOUT bracketed paste delivers a pasted block as a burst of
5
+ // discrete 1-char 'text' tokens (isPasteToken stays false for every one of
6
+ // them, since that flag only fires for a single token whose value.length > 1
7
+ // — see handler-feed-routes.ts's own Invariant B doc). Before this guard,
8
+ // each such character became a real panel hotkey on a focused non-capturing
9
+ // panel (K arms kill, etc.) once it reached handlePanelFocusToken's per-char
10
+ // dispatch loop.
11
+ //
12
+ // This is a RATE guard: more than PANEL_PASTE_FLOOD_THRESHOLD qualifying
13
+ // tokens within the trailing PANEL_PASTE_FLOOD_WINDOW_MS, evaluated with a
14
+ // real sliding window (old timestamps age out of `timestamps` every call).
15
+ // It is deliberately NOT the retired per-feed char-SUM burst heuristic this
16
+ // same input layer already tore out once (see panel-focus-burst.test.ts's
17
+ // header doc): that old guard summed one feed()'s character count with no
18
+ // timing signal at all, so two ordinary nav keystrokes landing in a single
19
+ // feed() — a real, common case, not an edge case — were misread as a burst
20
+ // and focus was silently flipped to the composer. This guard:
21
+ // - never touches focus (panelFocused is untouched by trackPanelPasteFloodGuard
22
+ // and by its caller);
23
+ // - is keyed on WALL-CLOCK TIMING, not a per-feed token count, so it
24
+ // doesn't care how many tokens land in one feed() call, only how fast
25
+ // they arrive relative to each other;
26
+ // - is sticky once tripped (only a quiet gap — no qualifying token for a
27
+ // full window — clears it) so it doesn't flap dispatch on/off as the
28
+ // count oscillates near the threshold mid-flood.
29
+ //
30
+ // ~8 keys/120ms is far beyond sustained human typing (a fast typist peaks
31
+ // well under that inter-key rate over any real span) but is exactly the
32
+ // shape an unbracketed paste replay takes. It also keeps the pre-existing
33
+ // Invariant B regression tests (2 keys in one feed(), no timing control at
34
+ // all) far under threshold while catching a real flood within its first ~9
35
+ // characters.
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export const PANEL_PASTE_FLOOD_WINDOW_MS = 120;
39
+ export const PANEL_PASTE_FLOOD_THRESHOLD = 8;
40
+
41
+ /**
42
+ * Guard state — a single persistent instance lives on the caller's
43
+ * long-lived context (handler-feed.ts's InputFeedContext, mirroring how that
44
+ * object already owns `nextPasteId`/`mouseDownRow`/etc.) and is MUTATED IN
45
+ * PLACE by trackPanelPasteFloodGuard below, never replaced — so callers never
46
+ * need to thread a return value back into their own state (unlike
47
+ * `panelFocused`, which handlePanelFocusToken returns because it is NOT a
48
+ * mutable outparam).
49
+ */
50
+ export interface PanelBurstGuardState {
51
+ timestamps: readonly number[];
52
+ suspended: boolean;
53
+ hintShown: boolean;
54
+ }
55
+
56
+ export interface PanelBurstGuardResult {
57
+ /** False while suspended — the caller must drop this token, not dispatch it. */
58
+ readonly dispatch: boolean;
59
+ /** True exactly once per burst: the call that just tripped suspension. */
60
+ readonly showHintNow: boolean;
61
+ }
62
+
63
+ /** Advance `guard` (mutated in place) by one qualifying token at time `now` (ms). */
64
+ export function trackPanelPasteFloodGuard(guard: PanelBurstGuardState, now: number): PanelBurstGuardResult {
65
+ const lastAt = guard.timestamps.length > 0 ? guard.timestamps[guard.timestamps.length - 1]! : -Infinity;
66
+ const isQuietGap = now - lastAt > PANEL_PASTE_FLOOD_WINDOW_MS;
67
+ if (isQuietGap && guard.suspended) {
68
+ // A silence at least as long as the window means whatever burst was
69
+ // happening has ended — un-suspend so a LATER burst gets its own fresh
70
+ // count and its own one-shot hint.
71
+ guard.suspended = false;
72
+ guard.hintShown = false;
73
+ }
74
+ guard.timestamps = isQuietGap
75
+ ? [now]
76
+ : [...guard.timestamps.filter((t) => t > now - PANEL_PASTE_FLOOD_WINDOW_MS), now];
77
+ if (guard.timestamps.length > PANEL_PASTE_FLOOD_THRESHOLD) {
78
+ guard.suspended = true;
79
+ }
80
+ let showHintNow = false;
81
+ if (guard.suspended && !guard.hintShown) {
82
+ guard.hintShown = true;
83
+ showHintNow = true;
84
+ }
85
+ return { dispatch: !guard.suspended, showHintNow };
86
+ }
@@ -36,6 +36,14 @@ export class SelectionModal {
36
36
  public selectedIndex = 0;
37
37
  public allowSearch = true;
38
38
  public customActions: Map<string, SelectionAction> = new Map();
39
+ /**
40
+ * UX-C vocab unification: overrides the default per-primaryAction Enter
41
+ * verb ("Select"/"Toggle"/"Edit"/"Delete") shown in the footer hint. Used by
42
+ * callers whose items are really commands (e.g. /help) so the hint reads
43
+ * "[Enter] Run", matching the slash-command palette's own verb, instead of
44
+ * the generic "Select".
45
+ */
46
+ public primaryVerbLabel: string | undefined = undefined;
39
47
 
40
48
  /** Open the modal with items and title */
41
49
  open(
@@ -45,6 +53,7 @@ export class SelectionModal {
45
53
  preSelectId?: string;
46
54
  allowSearch?: boolean;
47
55
  customActions?: Map<string, SelectionAction>;
56
+ primaryVerbLabel?: string;
48
57
  }
49
58
  ): void {
50
59
  this.title = title;
@@ -53,6 +62,7 @@ export class SelectionModal {
53
62
  this.allowSearch = opts?.allowSearch ?? true;
54
63
  this.searchFocused = false;
55
64
  this.customActions = opts?.customActions ?? new Map();
65
+ this.primaryVerbLabel = opts?.primaryVerbLabel;
56
66
  this.active = true;
57
67
  this.filterItems();
58
68
 
@@ -74,6 +84,7 @@ export class SelectionModal {
74
84
  this.filteredItems = [];
75
85
  this.selectedIndex = 0;
76
86
  this.customActions = new Map();
87
+ this.primaryVerbLabel = undefined;
77
88
  }
78
89
 
79
90
  moveUp(): void {
@@ -1,12 +1,28 @@
1
1
  /**
2
2
  * SessionPickerModal — state management for the /sessions picker modal.
3
3
  *
4
- * Lists sessions from SessionManager.list(), tracks selected index,
5
- * and handles load/delete actions.
4
+ * Lists LOCAL saved transcript files from SessionManager.list() (load/delete
5
+ * operate on these — a saved JSONL session is a different thing from a live
6
+ * control-plane session, see below), tracks selected index, and handles
7
+ * load/delete actions.
8
+ *
9
+ * W3-T2 (union-sessions surface): additionally surfaces the cross-surface
10
+ * session union from `sessionBroker` (a SessionReadFacade — normally
11
+ * `uiServices.sessions.sessionBroker`, the SessionUnionCache) so a user can
12
+ * SEE what sessions are live/closed across every surface sharing this
13
+ * daemon (TUI, webui, companion, automation), not just this process's own
14
+ * saved files. This is deliberately READ-ONLY: a SharedSessionRecord has no
15
+ * on-disk transcript this process can load()/delete() the way a local
16
+ * SessionInfo does, so cross-surface rows are visible + badged, not
17
+ * selectable for load/delete. `sessionBroker` is optional so every existing
18
+ * caller/test that only cares about local saved-session management keeps
19
+ * working unchanged.
6
20
  */
7
21
 
8
22
  import { unlinkSync } from 'node:fs';
9
23
  import type { SessionInfo, SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
24
+ import type { SharedSessionRecord } from '@pellux/goodvibes-sdk/platform/control-plane';
25
+ import type { CrossSurfaceView, SessionReadFacade } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
10
26
  import type { ConversationManager } from '../core/conversation';
11
27
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
12
28
 
@@ -14,9 +30,22 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
14
30
  // SessionPickerModal
15
31
  // ---------------------------------------------------------------------------
16
32
 
33
+ /** Honest default posture when no `sessionBroker` was wired: no cross-surface claim. */
34
+ const DORMANT_CROSS_SURFACE_VIEW: CrossSurfaceView = {
35
+ mode: 'local',
36
+ online: false,
37
+ stale: false,
38
+ lastSyncAt: null,
39
+ offlineNote: null,
40
+ };
41
+
17
42
  export class SessionPickerModal {
18
43
  public active = false;
19
44
  public sessions: SessionInfo[] = [];
45
+ /** Cross-surface session union (view-only) — see class doc. Empty until open(). */
46
+ public crossSurfaceSessions: readonly SharedSessionRecord[] = [];
47
+ /** Honest posture for crossSurfaceSessions: mode/online/stale/offlineNote. */
48
+ public crossSurfaceView: CrossSurfaceView = DORMANT_CROSS_SURFACE_VIEW;
20
49
  public selectedIndex = 0;
21
50
  public scrollOffset = 0;
22
51
  public visibleRows = 8;
@@ -25,13 +54,24 @@ export class SessionPickerModal {
25
54
  /** Last status message to show in the modal (e.g. error or success). */
26
55
  public statusMessage = '';
27
56
 
28
- public constructor(private readonly sessionManager: SessionManager) {}
57
+ public constructor(
58
+ private readonly sessionManager: SessionManager,
59
+ private readonly sessionBroker?: SessionReadFacade,
60
+ ) {}
29
61
 
30
62
  /**
31
- * Open the modal, loading sessions from SessionManager.
63
+ * Open the modal, loading local saved sessions from SessionManager and (when
64
+ * a sessionBroker was wired) the cross-surface session union, honestly.
32
65
  */
33
66
  open(): void {
34
67
  this.sessions = this.sessionManager.list();
68
+ if (this.sessionBroker) {
69
+ this.crossSurfaceSessions = this.sessionBroker.listSessions();
70
+ this.crossSurfaceView = this.sessionBroker.crossSurfaceView;
71
+ } else {
72
+ this.crossSurfaceSessions = [];
73
+ this.crossSurfaceView = DORMANT_CROSS_SURFACE_VIEW;
74
+ }
35
75
  this.selectedIndex = 0;
36
76
  this.scrollOffset = 0;
37
77
  this.statusMessage = '';