@pellux/goodvibes-tui 0.27.0 → 0.29.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 (143) hide show
  1. package/CHANGELOG.md +124 -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/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -8,35 +8,31 @@
8
8
  * Open via Ctrl+O keybind or `/ops view` command.
9
9
  */
10
10
  import type { Line } from '../types/grid.ts';
11
- import type { OpsEvent } from '@/runtime/index.ts';
11
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
12
+ import type { OpsApi, 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';
17
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
18
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
16
19
  import {
20
+ buildKeyValueLine,
21
+ buildKeyboardHints,
17
22
  buildPanelLine,
18
23
  DEFAULT_PANEL_PALETTE,
24
+ extendPalette,
19
25
  type PanelPalette,
20
26
  } from './polish.ts';
21
27
 
22
28
  // ── Colour palette ──────────────────────────────────────────────────────────
23
- const C = {
24
- ...DEFAULT_PANEL_PALETTE,
25
- header: '#94a3b8',
26
- headerBg: '#1e293b',
27
- success: '#22c55e',
28
- rejected: '#f97316',
29
- error: '#ef4444',
30
- dim: '#4b5563',
31
- label: '#64748b',
32
- value: '#cbd5e1',
33
- note: '#eab308',
34
- seq: '#475569',
35
- taskColor: '#22d3ee',
36
- agentColor: '#a78bfa',
37
- empty: '#334155',
38
- selectBg: '#0f172a',
39
- } as const;
29
+ // Domain accents only; base chrome (header/headerBg/dim/label/value/good/bad/
30
+ // warn/empty/selectBg) comes from DEFAULT_PANEL_PALETTE.
31
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
32
+ rejected: '#f97316', // rejected-outcome badge — distinct from success/error
33
+ taskColor: '#22d3ee', // task-target tag
34
+ agentColor: '#a78bfa', // agent-target tag
35
+ } as const);
40
36
 
41
37
  // ── Helpers ──────────────────────────────────────────────────────────────────
42
38
 
@@ -50,9 +46,9 @@ function fmtTime(ts: number): string {
50
46
 
51
47
  function outcomeColor(outcome: OpsAuditEntry['outcome']): string {
52
48
  switch (outcome) {
53
- case 'success': return C.success;
49
+ case 'success': return C.good;
54
50
  case 'rejected': return C.rejected;
55
- case 'error': return C.error;
51
+ case 'error': return C.bad;
56
52
  }
57
53
  }
58
54
 
@@ -68,16 +64,41 @@ function targetColor(kind: OpsAuditEntry['targetKind']): string {
68
64
  return kind === 'task' ? C.taskColor : C.agentColor;
69
65
  }
70
66
 
67
+ // ── Operator intervention actions ───────────────────────────────────────────
68
+ // Target-entry mode: c/p/u/y act on the currently *selected* audit row's
69
+ // target (task or agent) rather than requiring a typed id, mirroring the
70
+ // `/ops task <action> <id>` / `/ops agent cancel <id>` slash commands.
71
+
72
+ type OpsAction = 'cancel' | 'pause' | 'resume' | 'retry';
73
+
74
+ interface OpsActionSubject {
75
+ readonly action: OpsAction;
76
+ readonly targetKind: OpsAuditEntry['targetKind'];
77
+ readonly targetId: string;
78
+ }
79
+
80
+ const ACTION_VERB: Record<OpsAction, string> = {
81
+ cancel: 'Cancel',
82
+ pause: 'Pause',
83
+ resume: 'Resume',
84
+ retry: 'Retry',
85
+ };
86
+
71
87
  // ── OpsControlPanel ──────────────────────────────────────────────────────────
72
88
 
73
89
  export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
74
90
  private readonly _opsPanel: OpsPanel;
91
+ private readonly _opsApi: OpsApi | undefined;
75
92
  private _unsub: (() => void) | null = null;
93
+ private _confirm: ConfirmState<OpsActionSubject> | null = null;
76
94
 
77
- public constructor(eventFeed: UiEventFeed<OpsEvent>) {
78
- super('ops-control', 'Ops Control', 'Q', 'agent');
95
+ public constructor(eventFeed: UiEventFeed<OpsEvent>, opsApi?: OpsApi) {
96
+ super('ops-control', 'Ops Control', '', 'runtime-ops');
79
97
  this.showSelectionGutter = true; // I5: non-color selection affordance
98
+ this.filterEnabled = true;
99
+ this.filterLabel = 'Filter audit';
80
100
  this._opsPanel = new OpsPanel(eventFeed);
101
+ this._opsApi = opsApi;
81
102
  this._unsub = this._opsPanel.subscribe(() => this.markDirty());
82
103
  }
83
104
 
@@ -103,25 +124,120 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
103
124
  return [...this._opsPanel.getSnapshot()].reverse();
104
125
  }
105
126
 
127
+ protected override filterMatches(entry: OpsAuditEntry, q: string): boolean {
128
+ return entry.action.toLowerCase().includes(q)
129
+ || entry.targetKind.toLowerCase().includes(q)
130
+ || entry.targetId.toLowerCase().includes(q)
131
+ || entry.outcome.toLowerCase().includes(q);
132
+ }
133
+
134
+ // -------------------------------------------------------------------------
135
+ // Input — target-entry mode dispatch (acts on the selected audit row)
136
+ // -------------------------------------------------------------------------
137
+
138
+ public override handleInput(key: string): boolean {
139
+ if (this.lastError !== null) this.clearError();
140
+
141
+ if (this._confirm) {
142
+ const result = handleConfirmInput(this._confirm, key);
143
+ if (result === 'confirmed') {
144
+ this._executeConfirmed(this._confirm.subject);
145
+ this._confirm = null;
146
+ this.markDirty();
147
+ return true;
148
+ }
149
+ if (result === 'cancelled') {
150
+ this._confirm = null;
151
+ this.markDirty();
152
+ }
153
+ return true;
154
+ }
155
+
156
+ if (!this.filterActive) {
157
+ switch (key) {
158
+ case 'c': this._requestAction('cancel'); return true;
159
+ case 'p': this._requestAction('pause'); return true;
160
+ case 'u': this._requestAction('resume'); return true;
161
+ case 'y': this._requestAction('retry'); return true;
162
+ default: break;
163
+ }
164
+ }
165
+
166
+ return super.handleInput(key);
167
+ }
168
+
169
+ private _requestAction(action: OpsAction): void {
170
+ const selected = this.getSelectedItem();
171
+ if (!selected) return;
172
+ if (action !== 'cancel' && selected.targetKind === 'agent') {
173
+ this.setError(`Only cancel is supported for agent targets ("${action}" is task-only).`);
174
+ return;
175
+ }
176
+ this._confirm = {
177
+ subject: { action, targetKind: selected.targetKind, targetId: selected.targetId },
178
+ label: `${selected.targetKind} ${selected.targetId}`,
179
+ verb: ACTION_VERB[action],
180
+ };
181
+ this.markDirty();
182
+ }
183
+
184
+ private _executeConfirmed(subject: OpsActionSubject): void {
185
+ if (!this._opsApi) {
186
+ this.setError('Ops API is not wired for this runtime.');
187
+ return;
188
+ }
189
+ try {
190
+ if (subject.targetKind === 'agent') {
191
+ this._opsApi.agents.cancel(subject.targetId);
192
+ } else {
193
+ switch (subject.action) {
194
+ case 'cancel': this._opsApi.tasks.cancel(subject.targetId); break;
195
+ case 'pause': this._opsApi.tasks.pause(subject.targetId); break;
196
+ case 'resume': this._opsApi.tasks.resume(subject.targetId); break;
197
+ case 'retry': this._opsApi.tasks.retry(subject.targetId); break;
198
+ }
199
+ }
200
+ } catch (err) {
201
+ // Dispatched actions re-appear as OPS_AUDIT rows regardless of legality
202
+ // (OpsControlPlane audits rejections too); this surfaces the rare case
203
+ // where the target vanished before the audit event was emitted.
204
+ this.setError(summarizeError(err));
205
+ }
206
+ }
207
+
208
+ /** Live posture: tasks currently eligible for intervention, independent of the audit log. */
209
+ private _postureCounts(): { running: number; blocked: number; retryable: number } {
210
+ const tasks = this._opsApi?.tasks.snapshot().tasks ?? [];
211
+ let running = 0;
212
+ let blocked = 0;
213
+ let retryable = 0;
214
+ for (const task of tasks) {
215
+ if (task.status === 'running') running++;
216
+ else if (task.status === 'blocked') blocked++;
217
+ else if (task.status === 'failed' || task.status === 'cancelled') retryable++;
218
+ }
219
+ return { running, blocked, retryable };
220
+ }
221
+
106
222
  protected renderItem(entry: OpsAuditEntry, _index: number, _selected: boolean, width: number): Line {
107
223
  const seqStr = String(entry.seq).padStart(4, ' ');
108
224
  const timeStr = fmtTime(entry.ts);
109
- const action = entry.action.slice(0, 15).padEnd(15, ' ');
225
+ const action = fitDisplay(entry.action, 15);
110
226
  const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
111
- // Truncation is intentional: TUI column width limits target ID display to 14 chars
227
+ // Last 10 chars of the id keep the unique suffix; fitDisplay caps display width.
112
228
  const shortId = entry.targetId.slice(-10);
113
- const target = (kindTag + shortId).slice(0, 14).padEnd(14, ' ');
229
+ const target = fitDisplay(kindTag + shortId, 14);
114
230
  const outLabel = outcomeLabel(entry.outcome);
115
- const noteRaw = (entry.note ?? entry.errorMessage ?? '').slice(0, Math.max(0, width - 63));
231
+ const noteRaw = truncateDisplay(entry.note ?? entry.errorMessage ?? '', Math.max(0, width - 63));
116
232
 
117
233
  const segs: Array<[string, string, string?]> = [
118
- [` ${seqStr} `, C.seq],
234
+ [` ${seqStr} `, C.dim],
119
235
  [`${timeStr} `, C.dim],
120
236
  [`${action} `, C.value],
121
237
  [`${target} `, targetColor(entry.targetKind)],
122
238
  [outLabel, outcomeColor(entry.outcome)],
123
239
  ];
124
- if (noteRaw) segs.push([` ${noteRaw}`, C.note]);
240
+ if (noteRaw) segs.push([` ${noteRaw}`, C.warn]);
125
241
  return buildPanelLine(width, segs);
126
242
  }
127
243
 
@@ -130,16 +246,79 @@ export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
130
246
  }
131
247
 
132
248
  protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
133
- return [{ command: '/cockpit', summary: 'open the cockpit and drive runtime interventions from the control rooms' }];
249
+ // No signpost the live posture row in the header (running/blocked/retryable
250
+ // task counts) already tells the operator whether there is anything to act on.
251
+ return [];
134
252
  }
135
253
 
136
254
  public render(width: number, height: number): Line[] {
255
+ const entries = this.getVisibleItems();
256
+ this.clampSelection();
257
+
258
+ // Outcome tallies surface posture at a glance (most important runtime info first).
259
+ let ok = 0;
260
+ let rejected = 0;
261
+ let errored = 0;
262
+ for (const e of entries) {
263
+ if (e.outcome === 'success') ok++;
264
+ else if (e.outcome === 'rejected') rejected++;
265
+ else errored++;
266
+ }
267
+
268
+ const posture = this._postureCounts();
269
+
137
270
  const headerLines: Line[] = [
271
+ buildKeyValueLine(width, [
272
+ { label: 'logged', value: String(entries.length), valueColor: entries.length > 0 ? C.value : C.dim },
273
+ { label: 'ok', value: String(ok), valueColor: ok > 0 ? C.good : C.dim },
274
+ { label: 'rejected', value: String(rejected), valueColor: rejected > 0 ? C.rejected : C.dim },
275
+ { label: 'errors', value: String(errored), valueColor: errored > 0 ? C.bad : C.dim },
276
+ ], C),
277
+ buildKeyValueLine(width, [
278
+ { label: 'running', value: String(posture.running), valueColor: posture.running > 0 ? C.warn : C.dim },
279
+ { label: 'blocked', value: String(posture.blocked), valueColor: posture.blocked > 0 ? C.warn : C.dim },
280
+ { label: 'retryable', value: String(posture.retryable), valueColor: posture.retryable > 0 ? C.info : C.dim },
281
+ ], C),
138
282
  buildPanelLine(width, [[' SEQ TIME ACTION TARGET OUT NOTE', C.label]]),
139
283
  ];
140
- const footerLines: Line[] = [
141
- buildPanelLine(width, [[' Up/Down scroll the intervention log', C.dim]]),
142
- ];
284
+
285
+ const selected = this.getSelectedItem();
286
+ const footerLines: Line[] = [];
287
+ if (selected) {
288
+ const detail = selected.note ?? selected.errorMessage ?? '(no note)';
289
+ footerLines.push(
290
+ buildPanelLine(width, [
291
+ [' #', C.label],
292
+ [String(selected.seq), C.value],
293
+ [' ', C.dim],
294
+ [outcomeLabel(selected.outcome).trim(), outcomeColor(selected.outcome)],
295
+ [' ', C.dim],
296
+ [selected.action, C.value],
297
+ [' ', C.dim],
298
+ [`${selected.targetKind}:${selected.targetId}`, targetColor(selected.targetKind)],
299
+ ]),
300
+ buildPanelLine(width, [
301
+ [' ', C.label],
302
+ [truncateDisplay(detail, Math.max(0, width - 4)), C.warn],
303
+ ]),
304
+ );
305
+ }
306
+ if (this._confirm) {
307
+ footerLines.push(...renderConfirmLines(width, this._confirm));
308
+ }
309
+ footerLines.push(
310
+ this.filterActive
311
+ ? buildKeyboardHints(width, [
312
+ { keys: 'type', label: 'filter audit' },
313
+ { keys: 'Enter', label: 'apply' },
314
+ { keys: 'Esc', label: 'clear' },
315
+ ], C)
316
+ : buildKeyboardHints(width, [
317
+ { keys: 'Up/Down', label: 'browse log' },
318
+ { keys: 'c/p/u/y', label: 'cancel/pause/resume/retry selected' },
319
+ { keys: '/', label: 'filter' },
320
+ ], C),
321
+ );
143
322
 
144
323
  return this.renderList(width, height, {
145
324
  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
- import type { PlannerEvent } from '@/runtime/index.ts';
14
+ import type { PlannerEvent, PlanRuntimeService } 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> = {
@@ -38,6 +50,15 @@ const STRATEGY_ICON: Record<ExecutionStrategy, string> = {
38
50
  remote: '▸',
39
51
  };
40
52
 
53
+ // Cycle order for the o (override) and m (mode) keys — mirrors /plan's
54
+ // documented strategy list (planner-command-handler.ts VALID_STRATEGIES).
55
+ const STRATEGY_CYCLE: readonly ExecutionStrategy[] = ['auto', 'single', 'cohort', 'background', 'remote'];
56
+
57
+ function nextStrategy(current: ExecutionStrategy): ExecutionStrategy {
58
+ const idx = STRATEGY_CYCLE.indexOf(current);
59
+ return STRATEGY_CYCLE[(idx + 1) % STRATEGY_CYCLE.length]!;
60
+ }
61
+
41
62
  // ---------------------------------------------------------------------------
42
63
  // OpsStrategyPanel
43
64
  // ---------------------------------------------------------------------------
@@ -47,13 +68,16 @@ export class OpsStrategyPanel extends BasePanel {
47
68
  private scrollOffset = 0;
48
69
  private history: PlannerDecision[] = [];
49
70
  private readonly adaptivePlanner: OpsStrategyQuery;
71
+ private readonly planRuntime: PlanRuntimeService | undefined;
50
72
 
51
73
  constructor(
52
74
  private readonly plannerEvents: UiEventFeed<PlannerEvent>,
53
75
  adaptivePlanner: OpsStrategyQuery,
76
+ planRuntime?: PlanRuntimeService,
54
77
  ) {
55
- super('ops', 'Ops', 'O', 'agent');
78
+ super('ops', 'Ops', '', 'runtime-ops');
56
79
  this.adaptivePlanner = adaptivePlanner;
80
+ this.planRuntime = planRuntime;
57
81
  }
58
82
 
59
83
  override onActivate(): void {
@@ -77,6 +101,8 @@ export class OpsStrategyPanel extends BasePanel {
77
101
  }
78
102
 
79
103
  handleInput(key: string): boolean {
104
+ if (this.lastError !== null) this.clearError();
105
+
80
106
  if (key === 'up' || key === 'k') {
81
107
  this.scrollOffset = Math.max(0, this.scrollOffset - 1);
82
108
  this.markDirty();
@@ -97,65 +123,124 @@ export class OpsStrategyPanel extends BasePanel {
97
123
  this.markDirty();
98
124
  return true;
99
125
  }
126
+ if (key === 'o') {
127
+ this._cycleOverride();
128
+ return true;
129
+ }
130
+ if (key === 'c') {
131
+ this._clearOverride();
132
+ return true;
133
+ }
134
+ if (key === 'm') {
135
+ this._cycleMode();
136
+ return true;
137
+ }
100
138
  return false;
101
139
  }
102
140
 
141
+ // -------------------------------------------------------------------------
142
+ // Override verbs — o=cycle override, c=clear, m=cycle mode, all dispatched
143
+ // through the same /plan subcommand bridge the slash command uses
144
+ // (planning-runtime.ts), so panel and slash-command behavior stay identical.
145
+ // -------------------------------------------------------------------------
146
+
147
+ private _cycleOverride(): void {
148
+ if (!this.planRuntime) {
149
+ this.setError('Plan runtime is not wired for this runtime.');
150
+ return;
151
+ }
152
+ const current = this.adaptivePlanner.getOverride() ?? this.adaptivePlanner.getMode();
153
+ const result = this.planRuntime('override', [nextStrategy(current)]);
154
+ if (!result.ok) this.setError(result.output);
155
+ this.markDirty();
156
+ }
157
+
158
+ private _clearOverride(): void {
159
+ if (!this.planRuntime) {
160
+ this.setError('Plan runtime is not wired for this runtime.');
161
+ return;
162
+ }
163
+ const result = this.planRuntime('clear', []);
164
+ if (!result.ok) this.setError(result.output);
165
+ this.markDirty();
166
+ }
167
+
168
+ private _cycleMode(): void {
169
+ if (!this.planRuntime) {
170
+ this.setError('Plan runtime is not wired for this runtime.');
171
+ return;
172
+ }
173
+ const result = this.planRuntime('mode', [nextStrategy(this.adaptivePlanner.getMode())]);
174
+ if (!result.ok) this.setError(result.output);
175
+ this.markDirty();
176
+ }
177
+
103
178
  render(width: number, height: number): Line[] {
104
179
  const latest = this.adaptivePlanner.getLatest();
105
180
  const mode = this.adaptivePlanner.getMode();
106
181
  const override = this.adaptivePlanner.getOverride();
182
+ const intro = 'Review adaptive execution planner decisions, overrides, and recent strategy history.';
183
+ const errorLine = this.renderErrorLine(width);
184
+ const footerLines = [
185
+ ...(errorLine ? [errorLine] : []),
186
+ buildKeyboardHints(width, [
187
+ { keys: 'Up/Down', label: 'scroll history' },
188
+ { keys: 'g/G', label: 'top/bottom' },
189
+ { keys: 'o', label: 'cycle override' },
190
+ { keys: 'c', label: 'clear override' },
191
+ { keys: 'm', label: 'cycle mode' },
192
+ ], C),
193
+ ];
107
194
  const statusLines: Line[] = [
108
195
  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],
196
+ [' Mode ', C.label],
197
+ [mode.toUpperCase(), C.value],
198
+ [' Override ', C.label],
199
+ [override ? `${override.toUpperCase()} [ACTIVE]` : 'none', override ? C.warn : C.dim],
200
+ [' Decisions ', C.label],
201
+ [String(this.history.length), this.history.length > 0 ? C.info : C.dim],
113
202
  ]),
114
203
  ];
115
204
 
116
205
  if (latest) {
117
206
  statusLines.push(buildPanelLine(width, [
118
- [' Last ', DEFAULT_PANEL_PALETTE.label],
207
+ [' Last ', C.label],
119
208
  [`${STRATEGY_ICON[latest.selected]} ${latest.selected.toUpperCase()}`, STRATEGY_FG[latest.selected]],
120
- [' Reason ', DEFAULT_PANEL_PALETTE.label],
121
- [latest.reasonCode, DEFAULT_PANEL_PALETTE.dim],
209
+ [' Reason ', C.label],
210
+ [latest.reasonCode, C.dim],
122
211
  ]));
123
212
  }
124
213
 
125
214
  if (this.history.length === 0) {
126
215
  return buildPanelWorkspace(width, height, {
127
216
  title: ' Ops Strategy',
128
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
217
+ intro,
129
218
  sections: [
130
219
  { title: 'Status', lines: statusLines },
131
220
  {
132
221
  lines: buildEmptyState(
133
222
  width,
134
223
  ' No decisions recorded yet',
135
- 'Adaptive planner decisions appear here once the planner begins selecting strategies.',
136
- [],
137
- DEFAULT_PANEL_PALETTE,
224
+ 'Adaptive planner decisions appear here once the planner begins selecting strategies. Seed or inspect an execution plan to drive runtime activity.',
225
+ [{ command: '/plan', summary: 'seed or inspect an execution plan; strategy decisions start appearing here once it runs' }],
226
+ C,
138
227
  ),
139
228
  },
140
229
  ],
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,
230
+ footerLines,
231
+ palette: C,
145
232
  });
146
233
  }
147
234
 
148
235
  const historyLines = this._renderHistory(width);
149
236
  const statusSection = { title: 'Status', lines: statusLines } as const;
150
237
  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,
238
+ intro,
239
+ footerLines,
240
+ palette: C,
156
241
  beforeSections: [statusSection],
157
242
  section: {
158
- title: 'Decision History',
243
+ title: `Decision History (${this.history.length})`,
159
244
  scrollableLines: historyLines,
160
245
  scrollOffset: Math.min(this.scrollOffset, Math.max(0, historyLines.length - 1)),
161
246
  minRows: 8,
@@ -165,15 +250,13 @@ export class OpsStrategyPanel extends BasePanel {
165
250
 
166
251
  return buildPanelWorkspace(width, height, {
167
252
  title: ' Ops Strategy',
168
- intro: 'Review adaptive execution planner decisions, overrides, and recent strategy history.',
253
+ intro,
169
254
  sections: [
170
255
  statusSection,
171
256
  historySection.section,
172
257
  ],
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,
258
+ footerLines,
259
+ palette: C,
177
260
  });
178
261
  }
179
262
 
@@ -187,11 +270,10 @@ export class OpsStrategyPanel extends BasePanel {
187
270
 
188
271
  private _renderHistory(width: number): Line[] {
189
272
  if (this.history.length === 0) {
190
- return [buildStyledPanelLine(width, [{ text: ' No history yet.', fg: DEFAULT_PANEL_PALETTE.dim, dim: true }])];
273
+ return [buildStyledPanelLine(width, [{ text: ' No history yet.', fg: C.dim, dim: true }])];
191
274
  }
192
275
 
193
276
  const lines: Line[] = [];
194
- lines.push(buildStyledPanelLine(width, [{ text: ' Decision History', fg: DEFAULT_PANEL_PALETTE.value, bold: true }]));
195
277
 
196
278
  const reversed = [...this.history].reverse();
197
279
  for (let i = 0; i < reversed.length; i++) {
@@ -202,29 +284,33 @@ export class OpsStrategyPanel extends BasePanel {
202
284
  const num = String(i + 1).padStart(3);
203
285
  const overrideMark = d.overrideActive ? ' [O]' : '';
204
286
 
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
- ]));
287
+ // Row 1: index + icon + strategy on the left, timestamp right-aligned.
288
+ // buildAlignedRow keeps the timestamp flush-right even with wide glyphs.
289
+ const tsCol = getDisplayWidth(`${ts} `);
290
+ lines.push(buildAlignedRow(
291
+ width,
292
+ [
293
+ { text: ` ${num}. ${icon} ${d.selected.toUpperCase()}${overrideMark}`, fg, bold: true },
294
+ { text: `${ts} `, fg: C.dim, dim: true },
295
+ ],
296
+ [
297
+ { width: Math.max(0, width - tsCol - 1) },
298
+ { width: tsCol, align: 'right' },
299
+ ],
300
+ { gap: 1 },
301
+ ));
216
302
 
217
303
  // Row 2: reason code
218
- lines.push(buildStyledPanelLine(width, [{ text: ` ${d.reasonCode}`, fg: DEFAULT_PANEL_PALETTE.dim, dim: true }]));
304
+ lines.push(buildStyledPanelLine(width, [{ text: ` ${d.reasonCode}`, fg: C.dim, dim: true }]));
219
305
 
220
306
  // Row 3+: top-2 scored candidates (auto mode only)
221
307
  if (!d.overrideActive && d.candidates.length > 1) {
222
308
  const top2 = d.candidates.slice(0, 2);
223
309
  for (const c of top2) {
224
310
  lines.push(buildStyledPanelLine(width, [
225
- { text: ' ', fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
311
+ { text: ' ', fg: C.dim, dim: true },
226
312
  { text: c.strategy.padEnd(12), fg: STRATEGY_FG[c.strategy] },
227
- { text: ` score ${c.score}`, fg: DEFAULT_PANEL_PALETTE.dim, dim: true },
313
+ { text: ` score ${c.score}`, fg: C.dim, dim: true },
228
314
  ]));
229
315
  }
230
316
  }