@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,707 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import type { ExecutionPlan, ExecutionPlanManager, PlanItem, PlanItemStatus } from '@pellux/goodvibes-sdk/platform/core';
3
- import { truncateDisplay } from '../utils/terminal-width.ts';
4
- import { BasePanel } from './base-panel.ts';
5
- import { AgentInspectorPanel } from './agent-inspector-panel.ts';
6
- import type { PanelIntegrationContext } from './types.ts';
7
- import type { WorkflowEvent } from '@/runtime/index.ts';
8
- import type { UiEventFeed } from '../runtime/ui-events.ts';
9
- import {
10
- buildEmptyState,
11
- buildKeyboardHints,
12
- buildKeyValueLine,
13
- buildMeterLine,
14
- buildPanelLine,
15
- buildPanelWorkspace,
16
- buildSelectablePanelLine,
17
- resolveScrollablePanelSection,
18
- DEFAULT_PANEL_PALETTE,
19
- extendPalette,
20
- type PanelWorkspaceSection,
21
- } from './polish.ts';
22
-
23
- // ---------------------------------------------------------------------------
24
- // Palette (no inline hex in render bodies)
25
- // ---------------------------------------------------------------------------
26
-
27
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
28
- complete: '#22c55e',
29
- active: '#00ffff',
30
- pending: '#94a3b8',
31
- failed: '#ef4444',
32
- skipped: '#64748b',
33
- blocked: '#f97316',
34
- selectBg: '#1e293b',
35
- phase: '#cbd5e1',
36
- });
37
-
38
- // ---------------------------------------------------------------------------
39
- // Status display maps
40
- // ---------------------------------------------------------------------------
41
-
42
- const STATUS_ICON: Record<PlanItemStatus, string> = {
43
- complete: '✓',
44
- in_progress: '▸',
45
- pending: '•',
46
- failed: '✗',
47
- skipped: '–',
48
- };
49
-
50
- const STATUS_FG: Record<PlanItemStatus, string> = {
51
- complete: C.complete,
52
- in_progress: C.active,
53
- pending: C.pending,
54
- failed: C.failed,
55
- skipped: C.skipped,
56
- };
57
-
58
- // ---------------------------------------------------------------------------
59
- // Deps
60
- // ---------------------------------------------------------------------------
61
-
62
- /**
63
- * The subset of ExecutionPlanManager the dashboard needs: the live active
64
- * plan (getActive) plus history/switching support (list/getSummary/
65
- * toMarkdown — the same trio /plan list|show consume, planning-runtime.ts:
66
- * 140-165) so the panel can browse and view any plan on disk, not just the
67
- * currently active one.
68
- */
69
- export interface PlanDashboardPanelDeps {
70
- readonly planManager: Pick<ExecutionPlanManager, 'getActive' | 'list' | 'getSummary' | 'toMarkdown'>;
71
- }
72
-
73
- // ---------------------------------------------------------------------------
74
- // PlanDashboardPanel
75
- // ---------------------------------------------------------------------------
76
-
77
- export class PlanDashboardPanel extends BasePanel {
78
- private scrollOffset = 0;
79
- private selectedIndex = 0;
80
-
81
- // Flat list of navigable row indices (set during render)
82
- private totalRows = 0;
83
- // Item flat-list parallel to navigable rows (set during render) for detail.
84
- private navItems: PlanItem[] = [];
85
- private blockedItemIds = new Set<string>();
86
-
87
- /**
88
- * The plan item under the cursor. This panel owns its own selection state
89
- * (`selectedIndex` navigates the `this.navItems` flat list directly), so
90
- * every selected-row read routes through this one accessor — indexing
91
- * `this.navItems` by the cursor directly is banned by the
92
- * no-raw-selectedindex-read architecture rule.
93
- */
94
- private selectedNavItem(): PlanItem | undefined {
95
- return this.navItems.at(this.selectedIndex);
96
- }
97
-
98
- // Plan history / switching (planManager.list()/getSummary()/toMarkdown()).
99
- // `viewingPlanId` is null while following the live active plan; once set,
100
- // the dashboard renders that specific plan from disk instead.
101
- private viewingPlanId: string | null = null;
102
- private historyMode = false;
103
- private historyPlans: ExecutionPlan[] = [];
104
- private historySelectedIndex = 0;
105
- private historyScrollOffset = 0;
106
-
107
- // Live-refresh subscriptions (WrfcPanel pattern: subscribe ui.events.workflows
108
- // so the dashboard updates during a run without requiring a keypress).
109
- private unsubscribers: Array<() => void> = [];
110
-
111
- constructor(
112
- private readonly workflowEvents: UiEventFeed<WorkflowEvent>,
113
- private readonly deps: PlanDashboardPanelDeps,
114
- ) {
115
- super('plan', 'Plan', '▤', 'agent');
116
- this.subscribeToEvents();
117
- }
118
-
119
- override onActivate(): void {
120
- super.onActivate();
121
- // Re-read plan from disk each time the panel is activated
122
- this.markDirty();
123
- }
124
-
125
- override onDestroy(): void {
126
- for (const unsub of this.unsubscribers) unsub();
127
- this.unsubscribers = [];
128
- super.onDestroy();
129
- }
130
-
131
- // --------------------------------------------------------------------------
132
- // Live refresh — mirrors WrfcPanel: any workflow event may have changed the
133
- // active plan's item statuses (WRFC updates plan items via
134
- // wrfcController.setPlanManager, main.ts:120), so re-render without
135
- // requiring a keypress.
136
- // --------------------------------------------------------------------------
137
-
138
- private subscribeToEvents(): void {
139
- const refresh = () => this.markDirty();
140
- this.unsubscribers.push(
141
- this.workflowEvents.on('WORKFLOW_CHAIN_CREATED', refresh),
142
- this.workflowEvents.on('WORKFLOW_STATE_CHANGED', refresh),
143
- this.workflowEvents.on('WORKFLOW_REVIEW_COMPLETED', refresh),
144
- this.workflowEvents.on('WORKFLOW_FIX_ATTEMPTED', refresh),
145
- this.workflowEvents.on('WORKFLOW_GATE_RESULT', refresh),
146
- this.workflowEvents.on('WORKFLOW_CHAIN_PASSED', refresh),
147
- this.workflowEvents.on('WORKFLOW_CHAIN_FAILED', refresh),
148
- this.workflowEvents.on('WORKFLOW_AUTO_COMMITTED', refresh),
149
- this.workflowEvents.on('WORKFLOW_CASCADE_ABORTED', refresh),
150
- );
151
- }
152
-
153
- // --------------------------------------------------------------------------
154
- // Input
155
- // --------------------------------------------------------------------------
156
-
157
- handleInput(key: string): boolean {
158
- if (this.historyMode) {
159
- return this.handleHistoryInput(key);
160
- }
161
- if (key === 'h') {
162
- this.enterHistoryMode();
163
- return true;
164
- }
165
- if (key === 'a' && this.viewingPlanId !== null) {
166
- this.viewingPlanId = null;
167
- this.selectedIndex = 0;
168
- this.markDirty();
169
- return true;
170
- }
171
- if (key === 'up' || key === 'k') {
172
- if (this.selectedIndex > 0) {
173
- this.selectedIndex--;
174
- this.markDirty();
175
- return true;
176
- }
177
- } else if (key === 'down' || key === 'j') {
178
- if (this.selectedIndex < this.totalRows - 1) {
179
- this.selectedIndex++;
180
- this.markDirty();
181
- return true;
182
- }
183
- } else if (key === 'home' || key === 'g') {
184
- if (this.selectedIndex !== 0) {
185
- this.selectedIndex = 0;
186
- this.markDirty();
187
- }
188
- return true;
189
- } else if (key === 'end' || key === 'G') {
190
- const last = Math.max(0, this.totalRows - 1);
191
- if (this.selectedIndex !== last) {
192
- this.selectedIndex = last;
193
- this.markDirty();
194
- }
195
- return true;
196
- } else if (key === 'enter' || key === 'return') {
197
- // Consumed here (so the integration router fires next) only when the
198
- // selected item actually has an agent to jump to; see
199
- // handlePanelIntegrationAction below.
200
- const item = this.selectedNavItem();
201
- if (item?.agentId) return true;
202
- }
203
- return false;
204
- }
205
-
206
- /**
207
- * Cross-panel jump: Enter on a plan item with an assigned agent opens the
208
- * Inspector focused on that agent (inspectAgent, WO-110). The inspector
209
- * already renders a WRFC badge for the inspected agent, so this also
210
- * cross-links the owning WRFC chain without the dashboard needing to know
211
- * about WrfcController directly.
212
- */
213
- handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
214
- if (this.historyMode) return false;
215
- if (key !== 'enter' && key !== 'return') return false;
216
- const item = this.selectedNavItem();
217
- if (!item?.agentId) return false;
218
- const inspector = ctx.panelManager.open('inspector');
219
- if (inspector instanceof AgentInspectorPanel) {
220
- inspector.inspectAgent(item.agentId);
221
- }
222
- return true;
223
- }
224
-
225
- // --------------------------------------------------------------------------
226
- // Plan history / switching
227
- // --------------------------------------------------------------------------
228
-
229
- private enterHistoryMode(): void {
230
- this.historyPlans = [...this.deps.planManager.list()].sort(
231
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
232
- );
233
- this.historySelectedIndex = 0;
234
- this.historyScrollOffset = 0;
235
- this.historyMode = true;
236
- this.markDirty();
237
- }
238
-
239
- private handleHistoryInput(key: string): boolean {
240
- if (key === 'up' || key === 'k') {
241
- if (this.historySelectedIndex > 0) {
242
- this.historySelectedIndex--;
243
- this.markDirty();
244
- }
245
- return true;
246
- }
247
- if (key === 'down' || key === 'j') {
248
- if (this.historySelectedIndex < this.historyPlans.length - 1) {
249
- this.historySelectedIndex++;
250
- this.markDirty();
251
- }
252
- return true;
253
- }
254
- if (key === 'enter' || key === 'return') {
255
- const plan = this.historyPlans[this.historySelectedIndex];
256
- if (plan) {
257
- this.viewingPlanId = plan.id;
258
- this.selectedIndex = 0;
259
- }
260
- this.historyMode = false;
261
- this.markDirty();
262
- return true;
263
- }
264
- if (key === 'escape' || key === 'h') {
265
- this.historyMode = false;
266
- this.markDirty();
267
- return true;
268
- }
269
- // Modal browsing: absorb everything else (mirrors ConfirmState's "others
270
- // absorbed" contract) so stray keys cannot leak into item navigation.
271
- return true;
272
- }
273
-
274
- // --------------------------------------------------------------------------
275
- // Render
276
- // --------------------------------------------------------------------------
277
-
278
- render(width: number, height: number): Line[] {
279
- return this.trackedRender(() => {
280
- if (this.historyMode) {
281
- return this.renderHistory(width, height);
282
- }
283
- const plan = this.resolveViewedPlan();
284
- if (!plan) {
285
- return buildPanelWorkspace(width, height, {
286
- title: ' Plan Dashboard',
287
- intro: 'Track the active execution plan, item status, phase grouping, and overall completion.',
288
- sections: [
289
- {
290
- lines: buildEmptyState(
291
- width,
292
- ' No active execution plan',
293
- 'Execution plans are produced by WRFC-reviewed tasks (gather-plan-apply execution). Start one from Teamwork and its phases, progress, blocked steps, and next action will surface here.',
294
- [
295
- { command: '/teamwork create-mode local-engineer <title>', summary: 'start a WRFC-reviewed engineer task — the only producer of a real execution plan' },
296
- { command: '/teamwork modes', summary: 'see every reviewMode: wrfc mode that can seed a plan' },
297
- ],
298
- C,
299
- ),
300
- },
301
- ],
302
- footerLines: this.historyFooterHintLines(width),
303
- palette: C,
304
- });
305
- }
306
- return this.renderPlan(plan, width, height);
307
- });
308
- }
309
-
310
- /** Resolve which plan to render: the pinned history selection, or the live active plan. */
311
- private resolveViewedPlan(): ExecutionPlan | null {
312
- if (this.viewingPlanId !== null) {
313
- const found = this.deps.planManager.list().find((p) => p.id === this.viewingPlanId);
314
- if (found) return found;
315
- // The pinned plan is gone (e.g. deleted from disk) — fall back to live.
316
- this.viewingPlanId = null;
317
- }
318
- return this.deps.planManager.getActive();
319
- }
320
-
321
- /** Keyboard hint shown even on the empty state so 'h' (history) stays discoverable. */
322
- private historyFooterHintLines(width: number): Line[] {
323
- return [
324
- buildKeyboardHints(width, [
325
- { keys: 'h', label: 'plan history' },
326
- ], C),
327
- ];
328
- }
329
-
330
- // --------------------------------------------------------------------------
331
- // Plan history browser
332
- // --------------------------------------------------------------------------
333
-
334
- private renderHistory(width: number, height: number): Line[] {
335
- if (this.historyPlans.length === 0) {
336
- return buildPanelWorkspace(width, height, {
337
- title: ' Plan History',
338
- intro: 'Every execution plan ever created for this project.',
339
- sections: [
340
- {
341
- lines: buildEmptyState(
342
- width,
343
- ' No plans on disk',
344
- 'Plans appear here once a WRFC-reviewed task (gather-plan-apply execution) creates one.',
345
- [],
346
- C,
347
- ),
348
- },
349
- ],
350
- footerLines: [buildKeyboardHints(width, [{ keys: 'Esc / h', label: 'back' }], C)],
351
- palette: C,
352
- });
353
- }
354
-
355
- const activePlan = this.deps.planManager.getActive();
356
- const rows: Line[] = [];
357
- let selectedLineIndex = 0;
358
- for (let i = 0; i < this.historyPlans.length; i++) {
359
- const plan = this.historyPlans[i]!;
360
- const isSelected = i === this.historySelectedIndex;
361
- if (isSelected) selectedLineIndex = rows.length;
362
- rows.push(this.renderHistoryRow(plan, activePlan?.id === plan.id, isSelected, width));
363
- }
364
-
365
- const highlighted = this.historyPlans[this.historySelectedIndex];
366
- const detailSection: PanelWorkspaceSection | null = highlighted ? {
367
- title: 'Plan Detail',
368
- lines: this.buildHistoryDetailLines(highlighted, width),
369
- } : null;
370
-
371
- const footerLines: Line[] = [
372
- buildKeyboardHints(width, [
373
- { keys: '↑/↓', label: 'browse' },
374
- { keys: 'Enter', label: 'view plan' },
375
- { keys: 'Esc / h', label: 'back' },
376
- ], C),
377
- ];
378
-
379
- const intro = `${this.historyPlans.length} plan${this.historyPlans.length === 1 ? '' : 's'} on disk`;
380
- const historySection = resolveScrollablePanelSection(width, height, {
381
- intro,
382
- footerLines,
383
- palette: C,
384
- afterSections: detailSection ? [detailSection] : [],
385
- section: {
386
- title: 'Plan History',
387
- scrollableLines: rows,
388
- selectedIndex: selectedLineIndex,
389
- scrollOffset: this.historyScrollOffset,
390
- minRows: 6,
391
- },
392
- });
393
- this.historyScrollOffset = historySection.scrollOffset;
394
-
395
- return buildPanelWorkspace(width, height, {
396
- title: ' Plan History',
397
- intro,
398
- sections: [
399
- historySection.section,
400
- ...(detailSection ? [detailSection] : []),
401
- ],
402
- footerLines,
403
- palette: C,
404
- });
405
- }
406
-
407
- private renderHistoryRow(plan: ExecutionPlan, isActive: boolean, isSelected: boolean, width: number): Line {
408
- const marker = isActive ? '▶' : ' ';
409
- const idShort = plan.id.slice(0, 8);
410
- const statusTag = `[${plan.status.padEnd(8)}]`;
411
- const prefix = ` ${marker} ${idShort} ${statusTag} `;
412
- const label = truncateDisplay(plan.title, Math.max(6, width - prefix.length - 2));
413
- return buildSelectablePanelLine(width, [
414
- { text: ` ${marker} `, fg: isActive ? C.good : C.dim, bold: isActive },
415
- { text: `${idShort} `, fg: C.dim },
416
- { text: `${statusTag} `, fg: this.statusColor(plan.status) },
417
- { text: label, fg: C.value },
418
- ], {
419
- selected: isSelected,
420
- selectedBg: C.selectBg,
421
- leadingMarker: '▸',
422
- });
423
- }
424
-
425
- private buildHistoryDetailLines(plan: ExecutionPlan, width: number): Line[] {
426
- const lines: Line[] = [
427
- buildPanelLine(width, [
428
- [' Summary ', C.label],
429
- [truncateDisplay(this.deps.planManager.getSummary(plan), Math.max(8, width - 12)), C.value],
430
- ]),
431
- ];
432
- const markdown = this.deps.planManager.toMarkdown(plan)
433
- .split('\n')
434
- .filter((line) => line.trim().length > 0)
435
- .slice(0, 4);
436
- for (const line of markdown) {
437
- lines.push(buildPanelLine(width, [
438
- [' ', C.label],
439
- [truncateDisplay(line, Math.max(8, width - 3)), C.dim],
440
- ]));
441
- }
442
- return lines;
443
- }
444
-
445
- // --------------------------------------------------------------------------
446
- // Plan render
447
- // --------------------------------------------------------------------------
448
-
449
- private renderPlan(plan: ExecutionPlan, width: number, height: number): Line[] {
450
- const activePlan = this.deps.planManager.getActive();
451
- const isHistorical = activePlan?.id !== plan.id;
452
-
453
- const phaseOrder: string[] = [];
454
- const byPhase = new Map<string, PlanItem[]>();
455
- for (const item of plan.items) {
456
- if (!byPhase.has(item.phase)) {
457
- byPhase.set(item.phase, []);
458
- phaseOrder.push(item.phase);
459
- }
460
- byPhase.get(item.phase)!.push(item);
461
- }
462
-
463
- // Build a set of complete item IDs for dependency blocking detection
464
- const completeIds = new Set(
465
- plan.items
466
- .filter((i) => i.status === 'complete' || i.status === 'skipped')
467
- .map((i) => i.id),
468
- );
469
-
470
- const isBlocked = (item: PlanItem): boolean =>
471
- item.status === 'pending' &&
472
- item.dependencies !== undefined &&
473
- item.dependencies.length > 0 &&
474
- !item.dependencies.every((depId) => completeIds.has(depId));
475
-
476
- let rowCount = 0;
477
- let selectedLineIndex = 0;
478
- const planLines: Line[] = [];
479
- const navItems: PlanItem[] = [];
480
- const blockedItemIds = new Set<string>();
481
-
482
- for (const phase of phaseOrder) {
483
- const items = byPhase.get(phase)!;
484
- planLines.push(this.renderPhaseHeaderLine(phase, items, width));
485
- for (const item of items) {
486
- const isSelected = rowCount === this.selectedIndex;
487
- if (isSelected) selectedLineIndex = planLines.length;
488
- const blocked = isBlocked(item);
489
- if (blocked) blockedItemIds.add(item.id);
490
- navItems.push(item);
491
- planLines.push(this.renderItem(item, isSelected, blocked, width));
492
- rowCount++;
493
- }
494
- }
495
-
496
- this.totalRows = rowCount;
497
- this.navItems = navItems;
498
- this.blockedItemIds = blockedItemIds;
499
- if (this.selectedIndex >= this.totalRows) {
500
- this.selectedIndex = Math.max(0, this.totalRows - 1);
501
- }
502
-
503
- const total = plan.items.length;
504
- const done = plan.items.filter((i) => i.status === 'complete' || i.status === 'skipped').length;
505
- const inProgress = plan.items.filter((i) => i.status === 'in_progress').length;
506
- const failed = plan.items.filter((i) => i.status === 'failed').length;
507
- const blockedCount = blockedItemIds.size;
508
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
509
-
510
- // The single most useful pointer: what should happen next. Prefer the first
511
- // in-progress item, else the first actionable (unblocked) pending item.
512
- const nextItem = plan.items.find((i) => i.status === 'in_progress')
513
- ?? plan.items.find((i) => i.status === 'pending' && !isBlocked(i));
514
-
515
- const meterWidth = Math.max(10, Math.min(28, width - 30));
516
- const summary: PanelWorkspaceSection = {
517
- title: 'Summary',
518
- lines: [
519
- buildPanelLine(width, [
520
- [' Status ', C.label],
521
- [plan.status, this.statusColor(plan.status)],
522
- [' ', C.label],
523
- [`${done}/${total} items`, C.value],
524
- [' ', C.label],
525
- [`${pct}%`, pct === 100 ? C.good : C.info],
526
- ]),
527
- buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
528
- filled: pct === 100 ? C.good : C.info,
529
- empty: C.empty,
530
- label: C.label,
531
- }, { prefix: ' Progress ', suffix: ` ${pct}% ` }),
532
- buildKeyValueLine(width, [
533
- { label: 'in progress', value: String(inProgress), valueColor: inProgress > 0 ? C.active : C.dim },
534
- { label: 'blocked', value: String(blockedCount), valueColor: blockedCount > 0 ? C.blocked : C.dim },
535
- { label: 'failed', value: String(failed), valueColor: failed > 0 ? C.bad : C.dim },
536
- ], C),
537
- nextItem
538
- ? buildPanelLine(width, [
539
- [' Next ', C.label],
540
- [STATUS_ICON[nextItem.status], STATUS_FG[nextItem.status]],
541
- [' ', C.label],
542
- [truncateDisplay(nextItem.description, Math.max(8, width - 9)), C.value],
543
- ])
544
- : buildPanelLine(width, [
545
- [' Next ', C.label],
546
- [failed > 0 ? 'review failed steps' : blockedCount > 0 ? 'unblock dependencies' : 'plan complete',
547
- failed > 0 ? C.bad : blockedCount > 0 ? C.blocked : C.good],
548
- ]),
549
- ],
550
- };
551
-
552
- const historyBanner: PanelWorkspaceSection | null = isHistorical ? {
553
- lines: [
554
- buildPanelLine(width, [
555
- [' Viewing history ', C.warn],
556
- [plan.status.toUpperCase(), this.statusColor(plan.status)],
557
- [' — press a to return to the live plan', C.dim],
558
- ]),
559
- ],
560
- } : null;
561
-
562
- const detailSection = this.buildDetailSection(width);
563
- const footerLines = this.footerLines(width);
564
-
565
- const planSection = resolveScrollablePanelSection(width, height, {
566
- intro: plan.title,
567
- footerLines,
568
- palette: C,
569
- beforeSections: historyBanner ? [historyBanner, summary] : [summary],
570
- afterSections: detailSection ? [detailSection] : [],
571
- section: {
572
- title: 'Execution Plan',
573
- scrollableLines: planLines,
574
- selectedIndex: selectedLineIndex,
575
- scrollOffset: this.scrollOffset,
576
- minRows: 6,
577
- appendWindowSummary: { dimColor: C.dim },
578
- },
579
- });
580
- this.scrollOffset = planSection.scrollOffset;
581
-
582
- return buildPanelWorkspace(width, height, {
583
- title: ` Plan Dashboard`,
584
- intro: plan.title,
585
- sections: [
586
- ...(historyBanner ? [historyBanner] : []),
587
- summary,
588
- planSection.section,
589
- ...(detailSection ? [detailSection] : []),
590
- ],
591
- footerLines,
592
- palette: C,
593
- });
594
- }
595
-
596
- private buildDetailSection(width: number): PanelWorkspaceSection | null {
597
- const item = this.selectedNavItem();
598
- if (!item) return null;
599
- const blocked = this.blockedItemIds.has(item.id);
600
- const lines: Line[] = [
601
- buildPanelLine(width, [
602
- [' ', C.label],
603
- [STATUS_ICON[item.status], STATUS_FG[item.status]],
604
- [' ', C.label],
605
- [truncateDisplay(item.description, Math.max(8, width - 4)), C.value],
606
- ]),
607
- buildKeyValueLine(width, [
608
- { label: 'phase', value: item.phase, valueColor: C.phase },
609
- { label: 'status', value: blocked ? 'blocked' : item.status.replace(/_/g, ' '), valueColor: blocked ? C.blocked : STATUS_FG[item.status] },
610
- ...(item.agentId ? [{ label: 'agent', value: item.agentId, valueColor: C.info }] : []),
611
- ], C),
612
- ];
613
- if (item.dependencies && item.dependencies.length > 0) {
614
- lines.push(buildPanelLine(width, [
615
- [' depends on ', C.label],
616
- [truncateDisplay(item.dependencies.join(', '), Math.max(8, width - 13)), blocked ? C.blocked : C.dim],
617
- ]));
618
- }
619
- return { title: 'Selected Step', lines };
620
- }
621
-
622
- private footerLines(width: number): Line[] {
623
- const hints: Array<{ keys: string; label: string }> = [
624
- { keys: this.totalRows > 0 ? `${this.selectedIndex + 1}/${this.totalRows}` : '0/0', label: 'step' },
625
- { keys: '↑/↓', label: 'navigate' },
626
- { keys: 'Home/End', label: 'jump' },
627
- { keys: 'h', label: 'history' },
628
- ];
629
- const item = this.selectedNavItem();
630
- if (item?.agentId) hints.push({ keys: 'Enter', label: 'inspect agent' });
631
- if (this.viewingPlanId !== null) hints.push({ keys: 'a', label: 'back to active' });
632
- return [buildKeyboardHints(width, hints, C)];
633
- }
634
-
635
- // --------------------------------------------------------------------------
636
- // Phase header — title + progress meter
637
- // --------------------------------------------------------------------------
638
-
639
- private renderPhaseHeaderLine(phase: string, items: PlanItem[], width: number): Line {
640
- const done = items.filter(
641
- (i) => i.status === 'complete' || i.status === 'skipped',
642
- ).length;
643
- const total = items.length;
644
- const active = items.some((i) => i.status === 'in_progress');
645
- const failed = items.some((i) => i.status === 'failed');
646
-
647
- const phaseFg = failed ? C.failed : active ? C.active : done === total ? C.complete : C.phase;
648
-
649
- const barW = 8;
650
- const filledB = total > 0 ? Math.round((done / total) * barW) : 0;
651
- const progressText = ` ${done}/${total}`;
652
- const label = truncateDisplay(phase, Math.max(4, width - barW - progressText.length - 6));
653
-
654
- return buildSelectablePanelLine(width, [
655
- { text: ` ${active ? '▸' : '▪'} `, fg: phaseFg, bold: true },
656
- { text: label, fg: phaseFg, bold: true },
657
- { text: ' ', fg: C.dim },
658
- { text: '▰'.repeat(filledB) + '▱'.repeat(barW - filledB), fg: phaseFg },
659
- { text: progressText, fg: C.dim },
660
- ], { fillBg: C.sectionBg });
661
- }
662
-
663
- // --------------------------------------------------------------------------
664
- // Individual item
665
- // --------------------------------------------------------------------------
666
-
667
- private renderItem(
668
- item: PlanItem,
669
- isSelected: boolean,
670
- isBlocked: boolean,
671
- width: number,
672
- ): Line {
673
- const icon = STATUS_ICON[item.status];
674
- const fg = isBlocked ? C.blocked : STATUS_FG[item.status];
675
- const dim = item.status === 'skipped';
676
-
677
- // Indent blocked items to visually signal they depend on others
678
- const indent = isBlocked ? ' ' : ' ';
679
- const agentTag = item.agentId && (item.status === 'in_progress' || item.status === 'complete')
680
- ? ` [${item.agentId}]`
681
- : '';
682
- const blockedTag = isBlocked ? ' ⊘ blocked' : '';
683
- const reserve = indent.length + 2 + agentTag.length + blockedTag.length;
684
- const desc = truncateDisplay(item.description, Math.max(6, width - reserve - 2));
685
-
686
- return buildSelectablePanelLine(width, [
687
- { text: `${indent}${icon} `, fg, dim, bold: item.status === 'in_progress' },
688
- { text: desc, fg, dim },
689
- { text: agentTag, fg: C.info },
690
- { text: blockedTag, fg: C.blocked },
691
- ], {
692
- selected: isSelected,
693
- selectedBg: C.selectBg,
694
- leadingMarker: '▸',
695
- });
696
- }
697
-
698
- private statusColor(status: ExecutionPlan['status']): string {
699
- return status === 'complete'
700
- ? C.good
701
- : status === 'failed'
702
- ? C.bad
703
- : status === 'active'
704
- ? C.info
705
- : C.dim;
706
- }
707
- }