@pellux/goodvibes-tui 0.28.0 → 1.0.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 (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,8 +1,11 @@
1
1
  import type { Line } from '../types/grid.ts';
2
- import type { ExecutionPlan, PlanItem, PlanItemStatus } from '@pellux/goodvibes-sdk/platform/core';
2
+ import type { ExecutionPlan, ExecutionPlanManager, PlanItem, PlanItemStatus } from '@pellux/goodvibes-sdk/platform/core';
3
3
  import { truncateDisplay } from '../utils/terminal-width.ts';
4
4
  import { BasePanel } from './base-panel.ts';
5
- import type { PlanDashboardQuery } from '../runtime/ui-service-queries.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';
6
9
  import {
7
10
  buildEmptyState,
8
11
  buildKeyboardHints,
@@ -52,6 +55,21 @@ const STATUS_FG: Record<PlanItemStatus, string> = {
52
55
  skipped: C.skipped,
53
56
  };
54
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
+
55
73
  // ---------------------------------------------------------------------------
56
74
  // PlanDashboardPanel
57
75
  // ---------------------------------------------------------------------------
@@ -65,11 +83,37 @@ export class PlanDashboardPanel extends BasePanel {
65
83
  // Item flat-list parallel to navigable rows (set during render) for detail.
66
84
  private navItems: PlanItem[] = [];
67
85
  private blockedItemIds = new Set<string>();
68
- private readonly planManager: PlanDashboardQuery;
69
86
 
70
- constructor(planManager: PlanDashboardQuery) {
71
- super('plan', 'Plan', 'P', 'agent');
72
- this.planManager = planManager;
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();
73
117
  }
74
118
 
75
119
  override onActivate(): void {
@@ -78,11 +122,52 @@ export class PlanDashboardPanel extends BasePanel {
78
122
  this.markDirty();
79
123
  }
80
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
+
81
153
  // --------------------------------------------------------------------------
82
154
  // Input
83
155
  // --------------------------------------------------------------------------
84
156
 
85
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
+ }
86
171
  if (key === 'up' || key === 'k') {
87
172
  if (this.selectedIndex > 0) {
88
173
  this.selectedIndex--;
@@ -108,17 +193,94 @@ export class PlanDashboardPanel extends BasePanel {
108
193
  this.markDirty();
109
194
  }
110
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;
111
202
  }
112
203
  return false;
113
204
  }
114
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
+
115
274
  // --------------------------------------------------------------------------
116
275
  // Render
117
276
  // --------------------------------------------------------------------------
118
277
 
119
278
  render(width: number, height: number): Line[] {
120
279
  return this.trackedRender(() => {
121
- const plan = this.planManager.getActive();
280
+ if (this.historyMode) {
281
+ return this.renderHistory(width, height);
282
+ }
283
+ const plan = this.resolveViewedPlan();
122
284
  if (!plan) {
123
285
  return buildPanelWorkspace(width, height, {
124
286
  title: ' Plan Dashboard',
@@ -128,14 +290,16 @@ export class PlanDashboardPanel extends BasePanel {
128
290
  lines: buildEmptyState(
129
291
  width,
130
292
  ' No active execution plan',
131
- 'No plan is being executed yet. Create one and its phases, progress, blocked steps, and next action surface here.',
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.',
132
294
  [
133
- { command: '/plan', summary: 'create an execution plan to populate this dashboard' },
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' },
134
297
  ],
135
298
  C,
136
299
  ),
137
300
  },
138
301
  ],
302
+ footerLines: this.historyFooterHintLines(width),
139
303
  palette: C,
140
304
  });
141
305
  }
@@ -143,11 +307,149 @@ export class PlanDashboardPanel extends BasePanel {
143
307
  });
144
308
  }
145
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
+
146
445
  // --------------------------------------------------------------------------
147
446
  // Plan render
148
447
  // --------------------------------------------------------------------------
149
448
 
150
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
+
151
453
  const phaseOrder: string[] = [];
152
454
  const byPhase = new Map<string, PlanItem[]>();
153
455
  for (const item of plan.items) {
@@ -247,6 +549,16 @@ export class PlanDashboardPanel extends BasePanel {
247
549
  ],
248
550
  };
249
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
+
250
562
  const detailSection = this.buildDetailSection(width);
251
563
  const footerLines = this.footerLines(width);
252
564
 
@@ -254,7 +566,7 @@ export class PlanDashboardPanel extends BasePanel {
254
566
  intro: plan.title,
255
567
  footerLines,
256
568
  palette: C,
257
- beforeSections: [summary],
569
+ beforeSections: historyBanner ? [historyBanner, summary] : [summary],
258
570
  afterSections: detailSection ? [detailSection] : [],
259
571
  section: {
260
572
  title: 'Execution Plan',
@@ -271,6 +583,7 @@ export class PlanDashboardPanel extends BasePanel {
271
583
  title: ` Plan Dashboard`,
272
584
  intro: plan.title,
273
585
  sections: [
586
+ ...(historyBanner ? [historyBanner] : []),
274
587
  summary,
275
588
  planSection.section,
276
589
  ...(detailSection ? [detailSection] : []),
@@ -281,7 +594,7 @@ export class PlanDashboardPanel extends BasePanel {
281
594
  }
282
595
 
283
596
  private buildDetailSection(width: number): PanelWorkspaceSection | null {
284
- const item = this.navItems[this.selectedIndex];
597
+ const item = this.selectedNavItem();
285
598
  if (!item) return null;
286
599
  const blocked = this.blockedItemIds.has(item.id);
287
600
  const lines: Line[] = [
@@ -307,13 +620,16 @@ export class PlanDashboardPanel extends BasePanel {
307
620
  }
308
621
 
309
622
  private footerLines(width: number): Line[] {
310
- return [
311
- buildKeyboardHints(width, [
312
- { keys: this.totalRows > 0 ? `${this.selectedIndex + 1}/${this.totalRows}` : '0/0', label: 'step' },
313
- { keys: '↑/↓', label: 'navigate' },
314
- { keys: 'Home/End', label: 'jump' },
315
- ], C),
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' },
316
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)];
317
633
  }
318
634
 
319
635
  // --------------------------------------------------------------------------