@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.
- package/CHANGELOG.md +23 -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/eval.ts +4 -3
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/main.ts +0 -1
- package/src/panels/agent-inspector-panel.ts +18 -13
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +22 -53
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/development.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +25 -1
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +82 -54
- package/src/panels/debug-panel.ts +51 -16
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +19 -3
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +40 -41
- package/src/panels/file-preview-panel.ts +9 -2
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +58 -70
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +31 -12
- package/src/panels/memory-panel.ts +5 -4
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +22 -0
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +65 -23
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +40 -17
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +29 -5
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +34 -7
- package/src/panels/thinking-panel.ts +26 -25
- package/src/panels/token-budget-panel.ts +49 -38
- package/src/panels/tool-inspector-panel.ts +27 -21
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +125 -34
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +31 -127
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/version.ts +1 -1
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- 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
|
|
75
|
-
[` ${provenance
|
|
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}
|
|
171
|
-
[` ${recommendation.title}
|
|
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
|
|
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)'
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
121
|
+
const action = fitDisplay(entry.action, 15);
|
|
110
122
|
const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
|
|
111
|
-
//
|
|
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
|
|
125
|
+
const target = fitDisplay(kindTag + shortId, 14);
|
|
114
126
|
const outLabel = outcomeLabel(entry.outcome);
|
|
115
|
-
const noteRaw = (entry.note ?? entry.errorMessage ?? ''
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
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 ',
|
|
110
|
-
[mode.toUpperCase(),
|
|
111
|
-
[' Override ',
|
|
112
|
-
[override ? `${override.toUpperCase()} [ACTIVE]` : 'none', override ?
|
|
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 ',
|
|
139
|
+
[' Last ', C.label],
|
|
119
140
|
[`${STRATEGY_ICON[latest.selected]} ${latest.selected.toUpperCase()}`, STRATEGY_FG[latest.selected]],
|
|
120
|
-
[' Reason ',
|
|
121
|
-
[latest.reasonCode,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
footerLines
|
|
153
|
-
|
|
154
|
-
],
|
|
155
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
170
|
+
intro,
|
|
171
|
+
footerLines,
|
|
172
|
+
palette: C,
|
|
156
173
|
beforeSections: [statusSection],
|
|
157
174
|
section: {
|
|
158
|
-
title:
|
|
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
|
|
185
|
+
intro,
|
|
169
186
|
sections: [
|
|
170
187
|
statusSection,
|
|
171
188
|
historySection.section,
|
|
172
189
|
],
|
|
173
|
-
footerLines
|
|
174
|
-
|
|
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:
|
|
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
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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}
|
|
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));
|