@pellux/goodvibes-tui 0.27.0 → 0.28.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 (101) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/eval.ts +4 -3
  9. package/src/input/handler-feed-routes.ts +61 -4
  10. package/src/input/handler-feed.ts +13 -0
  11. package/src/input/handler-picker-routes.ts +18 -13
  12. package/src/input/handler-ui-state.ts +4 -0
  13. package/src/input/keybindings.ts +5 -0
  14. package/src/input/model-picker.ts +5 -0
  15. package/src/input/panel-integration-actions.ts +10 -0
  16. package/src/main.ts +0 -1
  17. package/src/panels/agent-inspector-panel.ts +18 -13
  18. package/src/panels/agent-inspector-shared.ts +3 -3
  19. package/src/panels/agent-logs-panel.ts +22 -53
  20. package/src/panels/approval-panel.ts +146 -95
  21. package/src/panels/automation-control-panel.ts +24 -1
  22. package/src/panels/base-panel.ts +26 -2
  23. package/src/panels/builtin/development.ts +1 -1
  24. package/src/panels/cockpit-panel.ts +24 -5
  25. package/src/panels/cockpit-read-model.ts +2 -1
  26. package/src/panels/communication-panel.ts +108 -43
  27. package/src/panels/context-visualizer-panel.ts +25 -1
  28. package/src/panels/control-plane-panel.ts +27 -1
  29. package/src/panels/cost-tracker-panel.ts +82 -54
  30. package/src/panels/debug-panel.ts +51 -16
  31. package/src/panels/diff-panel.ts +59 -30
  32. package/src/panels/docs-panel.ts +19 -3
  33. package/src/panels/eval-panel.ts +130 -81
  34. package/src/panels/expandable-list-panel.ts +189 -0
  35. package/src/panels/file-explorer-panel.ts +40 -41
  36. package/src/panels/file-preview-panel.ts +9 -2
  37. package/src/panels/forensics-panel.ts +27 -13
  38. package/src/panels/git-panel.ts +58 -70
  39. package/src/panels/hooks-panel.ts +36 -2
  40. package/src/panels/incident-review-panel.ts +31 -10
  41. package/src/panels/intelligence-panel.ts +152 -71
  42. package/src/panels/knowledge-graph-panel.ts +89 -27
  43. package/src/panels/local-auth-panel.ts +17 -11
  44. package/src/panels/marketplace-panel.ts +31 -12
  45. package/src/panels/memory-panel.ts +5 -4
  46. package/src/panels/ops-control-panel.ts +69 -7
  47. package/src/panels/ops-strategy-panel.ts +61 -43
  48. package/src/panels/orchestration-panel.ts +34 -4
  49. package/src/panels/panel-list-panel.ts +33 -8
  50. package/src/panels/panel-manager.ts +23 -4
  51. package/src/panels/plan-dashboard-panel.ts +183 -66
  52. package/src/panels/plugins-panel.ts +48 -10
  53. package/src/panels/policy-panel.ts +60 -23
  54. package/src/panels/polish-core.ts +157 -0
  55. package/src/panels/polish-tables.ts +258 -0
  56. package/src/panels/polish.ts +44 -145
  57. package/src/panels/project-planning-panel.ts +22 -0
  58. package/src/panels/provider-accounts-panel.ts +55 -18
  59. package/src/panels/provider-health-panel.ts +65 -23
  60. package/src/panels/provider-stats-panel.ts +47 -22
  61. package/src/panels/qr-panel.ts +23 -11
  62. package/src/panels/remote-panel.ts +12 -5
  63. package/src/panels/routes-panel.ts +27 -5
  64. package/src/panels/sandbox-panel.ts +62 -27
  65. package/src/panels/schedule-panel.ts +40 -17
  66. package/src/panels/scrollable-list-panel.ts +124 -10
  67. package/src/panels/security-panel.ts +72 -20
  68. package/src/panels/services-panel.ts +68 -22
  69. package/src/panels/session-browser-panel.ts +29 -5
  70. package/src/panels/session-maintenance.ts +2 -2
  71. package/src/panels/settings-sync-panel.ts +30 -15
  72. package/src/panels/subscription-panel.ts +21 -3
  73. package/src/panels/symbol-outline-panel.ts +40 -47
  74. package/src/panels/system-messages-panel.ts +60 -27
  75. package/src/panels/tasks-panel.ts +34 -7
  76. package/src/panels/thinking-panel.ts +26 -25
  77. package/src/panels/token-budget-panel.ts +49 -38
  78. package/src/panels/tool-inspector-panel.ts +27 -21
  79. package/src/panels/types.ts +25 -0
  80. package/src/panels/watchers-panel.ts +25 -5
  81. package/src/panels/work-plan-panel.ts +125 -34
  82. package/src/panels/worktree-panel.ts +65 -20
  83. package/src/panels/wrfc-panel-format.ts +133 -0
  84. package/src/panels/wrfc-panel.ts +31 -127
  85. package/src/renderer/compaction-preview.ts +2 -1
  86. package/src/renderer/compositor.ts +46 -43
  87. package/src/renderer/model-picker-overlay.ts +9 -4
  88. package/src/renderer/model-workspace.ts +2 -3
  89. package/src/renderer/panel-composite.ts +7 -20
  90. package/src/renderer/panel-workspace-bar.ts +14 -8
  91. package/src/renderer/progress.ts +3 -2
  92. package/src/renderer/tab-strip.ts +148 -34
  93. package/src/renderer/ui-factory.ts +4 -6
  94. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  95. package/src/runtime/ui/model-picker/types.ts +4 -0
  96. package/src/utils/format-duration.ts +55 -0
  97. package/src/utils/format-number.ts +71 -0
  98. package/src/version.ts +1 -1
  99. package/src/panels/panel-picker.ts +0 -106
  100. package/src/renderer/panel-picker-overlay.ts +0 -202
  101. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,13 +1,14 @@
1
1
  import type { Line } from '../types/grid.ts';
2
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
2
3
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
4
  import {
4
5
  buildEmptyState,
5
6
  buildGuidanceLine,
6
7
  buildKeyValueLine,
8
+ buildKeyboardHints,
7
9
  buildPanelLine,
8
10
  buildPanelWorkspace,
9
11
  DEFAULT_PANEL_PALETTE,
10
- type PanelWorkspaceSection,
11
12
  } from './polish.ts';
12
13
  import {
13
14
  type EcosystemCatalogPathOptions,
@@ -45,9 +46,17 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
45
46
  private readonly ecosystemPaths?: EcosystemCatalogPathOptions,
46
47
  ) {
47
48
  super('marketplace', 'Marketplace', 'M', 'monitoring');
49
+ this.filterEnabled = true;
50
+ this.filterLabel = 'Filter marketplace';
48
51
  this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
49
52
  }
50
53
 
54
+ protected override filterMatches(row: MarketplaceRow, q: string): boolean {
55
+ return row.kind.toLowerCase().includes(q)
56
+ || row.entry.name.toLowerCase().includes(q)
57
+ || (row.entry.provenance ?? 'local').toLowerCase().includes(q);
58
+ }
59
+
51
60
  public override onDestroy(): void {
52
61
  this.unsub?.();
53
62
  }
@@ -71,8 +80,8 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
71
80
  return buildPanelLine(width, [
72
81
  [' ', C.label, bg],
73
82
  [row.kind.padEnd(11), C.info, bg],
74
- [row.entry.name.slice(0, 20).padEnd(20), C.value, bg],
75
- [` ${provenance.slice(0, 16).padEnd(16)}`, provenance === 'local' ? C.dim : C.info, bg],
83
+ [fitDisplay(row.entry.name, 20), C.value, bg],
84
+ [` ${fitDisplay(provenance, 16)}`, provenance === 'local' ? C.dim : C.info, bg],
76
85
  [` ${(row.installed ? 'INSTALLED' : 'CURATED').padEnd(9)} `, statusColor(row.installed), bg],
77
86
  [` ${row.entry.version ?? 'n/a'}`, C.dim, bg],
78
87
  ]);
@@ -167,13 +176,13 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
167
176
  const recommendationLines = recommendations.length > 0
168
177
  ? recommendations.slice(0, 4).map((recommendation) => buildPanelLine(width, [
169
178
  [' ', C.label],
170
- [`${recommendation.kind} ${recommendation.entry.id}`.slice(0, 28).padEnd(28), C.info],
171
- [` ${recommendation.title}`.slice(0, Math.max(0, width - 31)), C.dim],
179
+ [fitDisplay(`${recommendation.kind} ${recommendation.entry.id}`, 28), C.info],
180
+ [truncateDisplay(` ${recommendation.title}`, Math.max(0, width - 31)), C.dim],
172
181
  ]))
173
182
  : [buildPanelLine(width, [[' No contextual marketplace recommendations right now.', C.dim]])];
174
183
 
175
184
  const startupIssueLines = startupIssues.length > 0
176
- ? startupIssues.slice(0, 4).map((issue) => buildPanelLine(width, [[' ', C.label], [issue.slice(0, Math.max(0, width - 2)), C.warn]]))
185
+ ? startupIssues.slice(0, 4).map((issue) => buildPanelLine(width, [[' ', C.label], [truncateDisplay(issue, Math.max(0, width - 2)), C.warn]]))
177
186
  : [buildPanelLine(width, [[' No startup or lifecycle issues are currently pushing marketplace repair recommendations.', C.dim]])];
178
187
 
179
188
  const selectedRow = this.rows[this.selectedIndex];
@@ -182,11 +191,11 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
182
191
  const review = reviewEcosystemCatalogEntry(selectedRow.entry, this.ecosystemPaths!);
183
192
  selectedLines.push(buildPanelLine(width, [
184
193
  [' Provenance: ', C.label],
185
- [(selectedRow.entry.provenance ?? '(none)').slice(0, Math.max(0, width - 15)), selectedRow.entry.provenance ? C.info : C.dim],
194
+ [truncateDisplay(selectedRow.entry.provenance ?? '(none)', Math.max(0, width - 15)), selectedRow.entry.provenance ? C.info : C.dim],
186
195
  ]));
187
196
  selectedLines.push(buildPanelLine(width, [
188
197
  [' Source: ', C.label],
189
- [selectedRow.entry.source.slice(0, Math.max(0, width - 11)), C.value],
198
+ [truncateDisplay(selectedRow.entry.source, Math.max(0, width - 11)), C.value],
190
199
  ]));
191
200
  selectedLines.push(buildKeyValueLine(width, [
192
201
  { label: 'Compatibility', value: review.compatibility.status, valueColor: review.compatibility.status === 'supported' ? C.good : C.warn },
@@ -196,9 +205,19 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
196
205
  selectedLines.push(buildGuidanceLine(width, '/marketplace review <id>', 'inspect full compatibility and receipt detail for the selected entry', C));
197
206
  }
198
207
 
199
- const postureSection: PanelWorkspaceSection = { title: 'Marketplace posture', lines: postureLines };
200
- const startupIssuesSection: PanelWorkspaceSection = { title: 'Startup Issues', lines: startupIssueLines };
201
- const recommendationsSection: PanelWorkspaceSection = { title: 'Recommendations', lines: recommendationLines };
208
+ // Context-aware hints: filter mode vs. browse mode (install only makes sense
209
+ // when a curated, not-yet-installed entry is selected).
210
+ const hints = this.filterActive
211
+ ? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
212
+ : [
213
+ { keys: 'Up/Down', label: 'move' },
214
+ ...(selectedRow && !selectedRow.installed ? [{ keys: '/marketplace install', label: 'add' }] : []),
215
+ { keys: '/', label: 'filter' },
216
+ ];
217
+
218
+ const footer: Line[] = selectedLines.length > 0 && height >= 20
219
+ ? [...selectedLines, buildKeyboardHints(width, hints, C)]
220
+ : [buildKeyboardHints(width, hints, C)];
202
221
 
203
222
  return this.renderList(width, height, {
204
223
  title: 'Marketplace Control Room',
@@ -207,7 +226,7 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
207
226
  ...startupIssueLines,
208
227
  ...recommendationLines,
209
228
  ],
210
- footer: selectedLines.length > 0 && height >= 20 ? selectedLines : [],
229
+ footer,
211
230
  });
212
231
  }
213
232
  }
@@ -14,6 +14,7 @@ import type { Line } from '../types/grid.ts';
14
14
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
15
15
  import type { MemoryClass, MemoryRecord, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
16
16
  import { ScrollableListPanel, SearchableListPanel } from './scrollable-list-panel.ts';
17
+ import { truncateDisplay } from '../utils/terminal-width.ts';
17
18
  import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
18
19
  import {
19
20
  buildBodyText,
@@ -176,7 +177,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
176
177
  [' ', C.label, bg],
177
178
  [record.reviewState.padEnd(13), reviewStateColor(record.reviewState), bg],
178
179
  [` ${formatConfidence(record.confidence)} `, C.value, bg],
179
- [record.summary.slice(0, Math.max(0, width - 26)), C.value, bg],
180
+ [truncateDisplay(record.summary, Math.max(0, width - 26)), C.value, bg],
180
181
  ]);
181
182
  }
182
183
  // All-mode row: scope/class + id + time + summary (matches former MemoryPanel row)
@@ -188,7 +189,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
188
189
  [' ', C.label, bg],
189
190
  [fmtTime(record.createdAt), C.dim, bg],
190
191
  [' ', C.label, bg],
191
- [record.summary.slice(0, Math.max(0, width - 33)), C.value, bg],
192
+ [truncateDisplay(record.summary, Math.max(0, width - 33)), C.value, bg],
192
193
  ]);
193
194
  }
194
195
 
@@ -306,13 +307,13 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
306
307
  }
307
308
  if (key === 's') {
308
309
  if (!selected) return false;
309
- this.confirm = { subject: { id: selected.id, action: 'stale' }, label: selected.summary.slice(0, 40) };
310
+ this.confirm = { subject: { id: selected.id, action: 'stale' }, label: truncateDisplay(selected.summary, 40) };
310
311
  this.markDirty();
311
312
  return true;
312
313
  }
313
314
  if (key === 'c') {
314
315
  if (!selected) return false;
315
- this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: selected.summary.slice(0, 40) };
316
+ this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: truncateDisplay(selected.summary, 40) };
316
317
  this.markDirty();
317
318
  return true;
318
319
  }
@@ -8,12 +8,15 @@
8
8
  * Open via Ctrl+O keybind or `/ops view` command.
9
9
  */
10
10
  import type { Line } from '../types/grid.ts';
11
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
11
12
  import type { OpsEvent } from '@/runtime/index.ts';
12
13
  import type { UiEventFeed } from '../runtime/ui-events.ts';
13
14
  import type { OpsAuditEntry } from '../runtime/diagnostics/panels/ops.ts';
14
15
  import { OpsPanel } from '../runtime/diagnostics/panels/ops.ts';
15
16
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
16
17
  import {
18
+ buildKeyValueLine,
19
+ buildKeyboardHints,
17
20
  buildPanelLine,
18
21
  DEFAULT_PANEL_PALETTE,
19
22
  type PanelPalette,
@@ -77,6 +80,8 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
77
80
  public constructor(eventFeed: UiEventFeed<OpsEvent>) {
78
81
  super('ops-control', 'Ops Control', 'Q', 'agent');
79
82
  this.showSelectionGutter = true; // I5: non-color selection affordance
83
+ this.filterEnabled = true;
84
+ this.filterLabel = 'Filter audit';
80
85
  this._opsPanel = new OpsPanel(eventFeed);
81
86
  this._unsub = this._opsPanel.subscribe(() => this.markDirty());
82
87
  }
@@ -103,16 +108,23 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
103
108
  return [...this._opsPanel.getSnapshot()].reverse();
104
109
  }
105
110
 
111
+ protected override filterMatches(entry: OpsAuditEntry, q: string): boolean {
112
+ return entry.action.toLowerCase().includes(q)
113
+ || entry.targetKind.toLowerCase().includes(q)
114
+ || entry.targetId.toLowerCase().includes(q)
115
+ || entry.outcome.toLowerCase().includes(q);
116
+ }
117
+
106
118
  protected renderItem(entry: OpsAuditEntry, _index: number, _selected: boolean, width: number): Line {
107
119
  const seqStr = String(entry.seq).padStart(4, ' ');
108
120
  const timeStr = fmtTime(entry.ts);
109
- const action = entry.action.slice(0, 15).padEnd(15, ' ');
121
+ const action = fitDisplay(entry.action, 15);
110
122
  const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
111
- // Truncation is intentional: TUI column width limits target ID display to 14 chars
123
+ // Last 10 chars of the id keep the unique suffix; fitDisplay caps display width.
112
124
  const shortId = entry.targetId.slice(-10);
113
- const target = (kindTag + shortId).slice(0, 14).padEnd(14, ' ');
125
+ const target = fitDisplay(kindTag + shortId, 14);
114
126
  const outLabel = outcomeLabel(entry.outcome);
115
- const noteRaw = (entry.note ?? entry.errorMessage ?? '').slice(0, Math.max(0, width - 63));
127
+ const noteRaw = truncateDisplay(entry.note ?? entry.errorMessage ?? '', Math.max(0, width - 63));
116
128
 
117
129
  const segs: Array<[string, string, string?]> = [
118
130
  [` ${seqStr} `, C.seq],
@@ -134,12 +146,62 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
134
146
  }
135
147
 
136
148
  public render(width: number, height: number): Line[] {
149
+ const entries = this.getVisibleItems();
150
+ this.clampSelection();
151
+
152
+ // Outcome tallies surface posture at a glance (most important runtime info first).
153
+ let ok = 0;
154
+ let rejected = 0;
155
+ let errored = 0;
156
+ for (const e of entries) {
157
+ if (e.outcome === 'success') ok++;
158
+ else if (e.outcome === 'rejected') rejected++;
159
+ else errored++;
160
+ }
161
+
137
162
  const headerLines: Line[] = [
163
+ buildKeyValueLine(width, [
164
+ { label: 'logged', value: String(entries.length), valueColor: entries.length > 0 ? C.value : C.dim },
165
+ { label: 'ok', value: String(ok), valueColor: ok > 0 ? C.success : C.dim },
166
+ { label: 'rejected', value: String(rejected), valueColor: rejected > 0 ? C.rejected : C.dim },
167
+ { label: 'errors', value: String(errored), valueColor: errored > 0 ? C.error : C.dim },
168
+ ], C),
138
169
  buildPanelLine(width, [[' SEQ TIME ACTION TARGET OUT NOTE', C.label]]),
139
170
  ];
140
- const footerLines: Line[] = [
141
- buildPanelLine(width, [[' Up/Down scroll the intervention log', C.dim]]),
142
- ];
171
+
172
+ const selected = entries[this.selectedIndex];
173
+ const footerLines: Line[] = [];
174
+ if (selected) {
175
+ const detail = selected.note ?? selected.errorMessage ?? '(no note)';
176
+ footerLines.push(
177
+ buildPanelLine(width, [
178
+ [' #', C.label],
179
+ [String(selected.seq), C.value],
180
+ [' ', C.dim],
181
+ [outcomeLabel(selected.outcome).trim(), outcomeColor(selected.outcome)],
182
+ [' ', C.dim],
183
+ [selected.action, C.value],
184
+ [' ', C.dim],
185
+ [`${selected.targetKind}:${selected.targetId}`, targetColor(selected.targetKind)],
186
+ ]),
187
+ buildPanelLine(width, [
188
+ [' ', C.label],
189
+ [truncateDisplay(detail, Math.max(0, width - 4)), C.note],
190
+ ]),
191
+ );
192
+ }
193
+ footerLines.push(
194
+ this.filterActive
195
+ ? buildKeyboardHints(width, [
196
+ { keys: 'type', label: 'filter audit' },
197
+ { keys: 'Enter', label: 'apply' },
198
+ { keys: 'Esc', label: 'clear' },
199
+ ], C)
200
+ : buildKeyboardHints(width, [
201
+ { keys: 'Up/Down', label: 'browse log' },
202
+ { keys: '/', label: 'filter' },
203
+ ], C),
204
+ );
143
205
 
144
206
  return this.renderList(width, height, {
145
207
  title: 'Operator Control Plane',
@@ -9,25 +9,37 @@
9
9
 
10
10
  import { BasePanel } from './base-panel.ts';
11
11
  import type { Line } from '../types/grid.ts';
12
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
12
13
  import type { PlannerDecision, ExecutionStrategy } from '@pellux/goodvibes-sdk/platform/core';
13
14
  import type { PlannerEvent } from '@/runtime/index.ts';
14
15
  import type { UiEventFeed } from '../runtime/ui-events.ts';
15
16
  import type { OpsStrategyQuery } from '../runtime/ui-service-queries.ts';
16
17
  import {
18
+ buildAlignedRow,
17
19
  buildEmptyState,
20
+ buildKeyboardHints,
18
21
  buildPanelLine,
19
22
  buildStyledPanelLine,
20
23
  buildPanelWorkspace,
24
+ extendPalette,
21
25
  resolveScrollablePanelSection,
22
26
  DEFAULT_PANEL_PALETTE,
23
27
  } from './polish.ts';
24
28
 
25
- const STRATEGY_FG: Record<ExecutionStrategy, string> = {
29
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
26
30
  auto: '#00cccc',
27
31
  single: '#00cc66',
28
32
  cohort: '#cccc00',
29
33
  background: '#cc66cc',
30
34
  remote: '#cccccc',
35
+ });
36
+
37
+ const STRATEGY_FG: Record<ExecutionStrategy, string> = {
38
+ auto: C.auto,
39
+ single: C.single,
40
+ cohort: C.cohort,
41
+ background: C.background,
42
+ remote: C.remote,
31
43
  };
32
44
 
33
45
  const STRATEGY_ICON: Record<ExecutionStrategy, string> = {
@@ -104,58 +116,63 @@ export class OpsStrategyPanel extends BasePanel {
104
116
  const latest = this.adaptivePlanner.getLatest();
105
117
  const mode = this.adaptivePlanner.getMode();
106
118
  const override = this.adaptivePlanner.getOverride();
119
+ const intro = 'Review adaptive execution planner decisions, overrides, and recent strategy history.';
120
+ const footerLines = [
121
+ buildKeyboardHints(width, [
122
+ { keys: 'Up/Down', label: 'scroll history' },
123
+ { keys: 'g/G', label: 'top/bottom' },
124
+ ], C),
125
+ ];
107
126
  const statusLines: Line[] = [
108
127
  buildPanelLine(width, [
109
- [' Mode ', DEFAULT_PANEL_PALETTE.label],
110
- [mode.toUpperCase(), DEFAULT_PANEL_PALETTE.value],
111
- [' Override ', DEFAULT_PANEL_PALETTE.label],
112
- [override ? `${override.toUpperCase()} [ACTIVE]` : 'none', override ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
128
+ [' Mode ', C.label],
129
+ [mode.toUpperCase(), C.value],
130
+ [' Override ', C.label],
131
+ [override ? `${override.toUpperCase()} [ACTIVE]` : 'none', override ? C.warn : C.dim],
132
+ [' Decisions ', C.label],
133
+ [String(this.history.length), this.history.length > 0 ? C.info : C.dim],
113
134
  ]),
114
135
  ];
115
136
 
116
137
  if (latest) {
117
138
  statusLines.push(buildPanelLine(width, [
118
- [' Last ', DEFAULT_PANEL_PALETTE.label],
139
+ [' Last ', C.label],
119
140
  [`${STRATEGY_ICON[latest.selected]} ${latest.selected.toUpperCase()}`, STRATEGY_FG[latest.selected]],
120
- [' Reason ', DEFAULT_PANEL_PALETTE.label],
121
- [latest.reasonCode, DEFAULT_PANEL_PALETTE.dim],
141
+ [' Reason ', C.label],
142
+ [latest.reasonCode, C.dim],
122
143
  ]));
123
144
  }
124
145
 
125
146
  if (this.history.length === 0) {
126
147
  return buildPanelWorkspace(width, height, {
127
148
  title: ' Ops Strategy',
128
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
149
+ intro,
129
150
  sections: [
130
151
  { title: 'Status', lines: statusLines },
131
152
  {
132
153
  lines: buildEmptyState(
133
154
  width,
134
155
  ' No decisions recorded yet',
135
- 'Adaptive planner decisions appear here once the planner begins selecting strategies.',
136
- [],
137
- DEFAULT_PANEL_PALETTE,
156
+ 'Adaptive planner decisions appear here once the planner begins selecting strategies. Run agents or use /ops to drive runtime activity.',
157
+ [{ command: '/ops', summary: 'open the operator control room to drive planner activity' }],
158
+ C,
138
159
  ),
139
160
  },
140
161
  ],
141
- footerLines: [
142
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll history', DEFAULT_PANEL_PALETTE.dim], [' g/G', DEFAULT_PANEL_PALETTE.info], [' top/bottom', DEFAULT_PANEL_PALETTE.dim]]),
143
- ],
144
- palette: DEFAULT_PANEL_PALETTE,
162
+ footerLines,
163
+ palette: C,
145
164
  });
146
165
  }
147
166
 
148
167
  const historyLines = this._renderHistory(width);
149
168
  const statusSection = { title: 'Status', lines: statusLines } as const;
150
169
  const historySection = resolveScrollablePanelSection(width, height, {
151
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
152
- footerLines: [
153
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll history', DEFAULT_PANEL_PALETTE.dim], [' g/G', DEFAULT_PANEL_PALETTE.info], [' top/bottom', DEFAULT_PANEL_PALETTE.dim]]),
154
- ],
155
- palette: DEFAULT_PANEL_PALETTE,
170
+ intro,
171
+ footerLines,
172
+ palette: C,
156
173
  beforeSections: [statusSection],
157
174
  section: {
158
- title: 'Decision History',
175
+ title: `Decision History (${this.history.length})`,
159
176
  scrollableLines: historyLines,
160
177
  scrollOffset: Math.min(this.scrollOffset, Math.max(0, historyLines.length - 1)),
161
178
  minRows: 8,
@@ -165,15 +182,13 @@ export class OpsStrategyPanel extends BasePanel {
165
182
 
166
183
  return buildPanelWorkspace(width, height, {
167
184
  title: ' Ops Strategy',
168
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
185
+ intro,
169
186
  sections: [
170
187
  statusSection,
171
188
  historySection.section,
172
189
  ],
173
- footerLines: [
174
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll history', DEFAULT_PANEL_PALETTE.dim], [' g/G', DEFAULT_PANEL_PALETTE.info], [' top/bottom', DEFAULT_PANEL_PALETTE.dim]]),
175
- ],
176
- palette: DEFAULT_PANEL_PALETTE,
190
+ footerLines,
191
+ palette: C,
177
192
  });
178
193
  }
179
194
 
@@ -187,11 +202,10 @@ export class OpsStrategyPanel extends BasePanel {
187
202
 
188
203
  private _renderHistory(width: number): Line[] {
189
204
  if (this.history.length === 0) {
190
- return [buildStyledPanelLine(width, [{ text: ' No history yet.', fg: DEFAULT_PANEL_PALETTE.dim, dim: true }])];
205
+ return [buildStyledPanelLine(width, [{ text: ' No history yet.', fg: C.dim, dim: true }])];
191
206
  }
192
207
 
193
208
  const lines: Line[] = [];
194
- lines.push(buildStyledPanelLine(width, [{ text: ' Decision History', fg: DEFAULT_PANEL_PALETTE.value, bold: true }]));
195
209
 
196
210
  const reversed = [...this.history].reverse();
197
211
  for (let i = 0; i < reversed.length; i++) {
@@ -202,29 +216,33 @@ export class OpsStrategyPanel extends BasePanel {
202
216
  const num = String(i + 1).padStart(3);
203
217
  const overrideMark = d.overrideActive ? ' [O]' : '';
204
218
 
205
- // Row 1: index + icon + strategy + timestamp (right-aligned)
206
- const leftBase = ` ${num}. ${icon} ${d.selected.toUpperCase()}${overrideMark}`;
207
- const rightText = ` ${ts}`;
208
- const pad = Math.max(1, width - leftBase.length - rightText.length);
209
- lines.push(buildStyledPanelLine(width, [
210
- { text: ` ${num}. `, fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
211
- { text: `${icon} ${d.selected.toUpperCase()}`, fg, bold: true },
212
- { text: overrideMark, fg: DEFAULT_PANEL_PALETTE.warn },
213
- { text: ' '.repeat(pad), fg: DEFAULT_PANEL_PALETTE.dim },
214
- { text: rightText, fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
215
- ]));
219
+ // Row 1: index + icon + strategy on the left, timestamp right-aligned.
220
+ // buildAlignedRow keeps the timestamp flush-right even with wide glyphs.
221
+ const tsCol = getDisplayWidth(`${ts} `);
222
+ lines.push(buildAlignedRow(
223
+ width,
224
+ [
225
+ { text: ` ${num}. ${icon} ${d.selected.toUpperCase()}${overrideMark}`, fg, bold: true },
226
+ { text: `${ts} `, fg: C.dim, dim: true },
227
+ ],
228
+ [
229
+ { width: Math.max(0, width - tsCol - 1) },
230
+ { width: tsCol, align: 'right' },
231
+ ],
232
+ { gap: 1 },
233
+ ));
216
234
 
217
235
  // Row 2: reason code
218
- lines.push(buildStyledPanelLine(width, [{ text: ` ${d.reasonCode}`, fg: DEFAULT_PANEL_PALETTE.dim, dim: true }]));
236
+ lines.push(buildStyledPanelLine(width, [{ text: ` ${d.reasonCode}`, fg: C.dim, dim: true }]));
219
237
 
220
238
  // Row 3+: top-2 scored candidates (auto mode only)
221
239
  if (!d.overrideActive && d.candidates.length > 1) {
222
240
  const top2 = d.candidates.slice(0, 2);
223
241
  for (const c of top2) {
224
242
  lines.push(buildStyledPanelLine(width, [
225
- { text: ' ', fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
243
+ { text: ' ', fg: C.dim, dim: true },
226
244
  { text: c.strategy.padEnd(12), fg: STRATEGY_FG[c.strategy] },
227
- { text: ` score ${c.score}`, fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
245
+ { text: ` score ${c.score}`, fg: C.dim, dim: true },
228
246
  ]));
229
247
  }
230
248
  }
@@ -8,12 +8,15 @@
8
8
 
9
9
  import type { Line } from '../types/grid.ts';
10
10
  import { createEmptyLine } from '../types/grid.ts';
11
+ import { truncateDisplay } from '../utils/terminal-width.ts';
11
12
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
12
13
  import type { UiOrchestrationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
13
14
  import type { OrchestrationGraphRecord } from '@/runtime/index.ts';
15
+ import { formatElapsed } from '../utils/format-elapsed.ts';
14
16
  import {
15
17
  buildEmptyState,
16
18
  buildGuidanceLine,
19
+ buildKeyboardHints,
17
20
  buildKeyValueLine,
18
21
  buildPanelLine,
19
22
  buildPanelWorkspace,
@@ -76,12 +79,17 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
76
79
  width: number,
77
80
  ): Line {
78
81
  const bg = selected ? C.selectBg : undefined;
82
+ const marker = selected ? '▸ ' : ' ';
83
+ const nodeCount = `${graph.nodeOrder.length}n`;
84
+ // Reserve room for marker(2) + status(10) + mode(17) + id(8) + nodes(~5) + gaps.
85
+ const titleBudget = Math.max(0, width - 47);
79
86
  return buildPanelLine(width, [
80
- [' ', C.label, bg],
87
+ [marker, selected ? C.value : C.dim, bg],
81
88
  [graph.status.padEnd(10), statusColor(graph.status), bg],
82
89
  [` ${graph.mode.padEnd(17)}`, C.value, bg],
83
90
  [` ${graph.id.slice(0, 8)} `, C.dim, bg],
84
- [graph.title.slice(0, Math.max(0, width - 39)), C.value, bg],
91
+ [`${nodeCount.padStart(4)} `, C.dim, bg],
92
+ [truncateDisplay(graph.title, titleBudget), C.value, bg],
85
93
  ]);
86
94
  }
87
95
 
@@ -165,14 +173,22 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
165
173
 
166
174
  this.clampSelection();
167
175
  const selected = graphs[this.selectedIndex]!;
176
+ const isActive = snapshot.activeGraphIds.includes(selected.id);
177
+ const elapsed = selected.startedAt
178
+ ? formatElapsed(Math.max(0, (selected.endedAt ?? Date.now()) - selected.startedAt))
179
+ : 'n/a';
168
180
  const detailLines: Line[] = [
169
181
  buildPanelLine(width, [
170
182
  [' Title: ', C.label],
171
- [selected.title, C.value],
183
+ [truncateDisplay(selected.title, Math.max(0, width - 11)), C.value],
184
+ ]),
185
+ buildPanelLine(width, [
172
186
  [' Status: ', C.label],
173
187
  [selected.status, statusColor(selected.status)],
174
188
  [' Mode: ', C.label],
175
189
  [selected.mode, C.value],
190
+ [' Live: ', C.label],
191
+ [isActive ? 'yes' : 'no', isActive ? C.running : C.dim],
176
192
  ]),
177
193
  buildPanelLine(width, [
178
194
  [' Nodes: ', C.label],
@@ -181,6 +197,8 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
181
197
  [selected.startedAt ? new Date(selected.startedAt).toLocaleTimeString() : 'n/a', C.dim],
182
198
  [' Ended: ', C.label],
183
199
  [selected.endedAt ? new Date(selected.endedAt).toLocaleTimeString() : 'n/a', C.dim],
200
+ [' Duration: ', C.label],
201
+ [elapsed, C.dim],
184
202
  ]),
185
203
  ];
186
204
  if (selected.lastRecursionGuard) {
@@ -227,7 +245,7 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
227
245
  [node.status.padEnd(10), statusColor(node.status)],
228
246
  [` ${node.role.padEnd(10)}`, C.value],
229
247
  [` ${node.id.slice(0, 8)} `, C.dim],
230
- [`${node.title}${depends}`.slice(0, Math.max(0, width - 34)), C.value],
248
+ [truncateDisplay(`${node.title}${depends}`, Math.max(0, width - 34)), C.value],
231
249
  ]);
232
250
  });
233
251
 
@@ -235,11 +253,22 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
235
253
  this.renderItem(graph, index, index === this.selectedIndex, width),
236
254
  );
237
255
 
256
+ // Context-aware footer: surface position + only the keys that work here.
257
+ const footerLines: Line[] = [
258
+ buildKeyboardHints(width, [
259
+ { keys: `${this.selectedIndex + 1}/${graphs.length}`, label: 'graph' },
260
+ { keys: '↑/↓', label: 'select' },
261
+ { keys: 'g/G', label: 'top/bottom' },
262
+ { keys: 'PgUp/PgDn', label: 'page' },
263
+ ], C),
264
+ ];
265
+
238
266
  const postureSection: PanelWorkspaceSection = { title: 'Orchestration posture', lines: postureLines };
239
267
  const selectedGraphSection: PanelWorkspaceSection = { title: 'Selected Graph', lines: detailLines };
240
268
  const nodesSection: PanelWorkspaceSection = { title: 'Nodes', lines: nodeLines };
241
269
  const graphsSection = resolveScrollablePanelSection(width, height, {
242
270
  intro,
271
+ footerLines,
243
272
  palette: C,
244
273
  beforeSections: [postureSection],
245
274
  section: {
@@ -265,6 +294,7 @@ export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRe
265
294
  title: 'Orchestration Control Room',
266
295
  intro,
267
296
  sections,
297
+ footerLines,
268
298
  palette: C,
269
299
  });
270
300
  while (lines.length < height) lines.push(createEmptyLine(width));