@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.
- package/CHANGELOG.md +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- 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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
49
|
+
case 'success': return C.good;
|
|
54
50
|
case 'rejected': return C.rejected;
|
|
55
|
-
case 'error': return C.
|
|
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', '
|
|
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
|
|
225
|
+
const action = fitDisplay(entry.action, 15);
|
|
110
226
|
const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
|
|
111
|
-
//
|
|
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
|
|
229
|
+
const target = fitDisplay(kindTag + shortId, 14);
|
|
114
230
|
const outLabel = outcomeLabel(entry.outcome);
|
|
115
|
-
const noteRaw = (entry.note ?? entry.errorMessage ?? ''
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
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', '
|
|
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 ',
|
|
110
|
-
[mode.toUpperCase(),
|
|
111
|
-
[' Override ',
|
|
112
|
-
[override ? `${override.toUpperCase()} [ACTIVE]` : 'none', override ?
|
|
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 ',
|
|
207
|
+
[' Last ', C.label],
|
|
119
208
|
[`${STRATEGY_ICON[latest.selected]} ${latest.selected.toUpperCase()}`, STRATEGY_FG[latest.selected]],
|
|
120
|
-
[' Reason ',
|
|
121
|
-
[latest.reasonCode,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
footerLines
|
|
153
|
-
|
|
154
|
-
],
|
|
155
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
238
|
+
intro,
|
|
239
|
+
footerLines,
|
|
240
|
+
palette: C,
|
|
156
241
|
beforeSections: [statusSection],
|
|
157
242
|
section: {
|
|
158
|
-
title:
|
|
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
|
|
253
|
+
intro,
|
|
169
254
|
sections: [
|
|
170
255
|
statusSection,
|
|
171
256
|
historySection.section,
|
|
172
257
|
],
|
|
173
|
-
footerLines
|
|
174
|
-
|
|
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:
|
|
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
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
313
|
+
{ text: ` score ${c.score}`, fg: C.dim, dim: true },
|
|
228
314
|
]));
|
|
229
315
|
}
|
|
230
316
|
}
|