@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
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
resolvePrimaryScrollableSection,
|
|
31
31
|
type PanelWorkspaceSection,
|
|
32
32
|
} from './polish.ts';
|
|
33
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
33
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
34
34
|
import { wrapWithHangingIndent } from '../renderer/text-layout.ts';
|
|
35
35
|
import {
|
|
36
36
|
getPanelSearchFocusTransition,
|
|
@@ -78,11 +78,14 @@ const CATEGORY_LABELS: Record<PanelCategory, string> = {
|
|
|
78
78
|
const NAME_COL_WIDTH = 22;
|
|
79
79
|
const PREFIX_WIDTH = 4; // arrow + dot + space + space
|
|
80
80
|
|
|
81
|
-
/** A flat entry in the navigable list — either a
|
|
81
|
+
/** A flat entry in the navigable list — either a section header or a panel row. */
|
|
82
82
|
type ListEntry =
|
|
83
|
-
| { kind: 'header';
|
|
83
|
+
| { kind: 'header'; label: string }
|
|
84
84
|
| { kind: 'panel'; reg: PanelRegistration };
|
|
85
85
|
|
|
86
|
+
/** Number of most-recently-opened panels to surface in the Recent group. */
|
|
87
|
+
const RECENT_LIMIT = 5;
|
|
88
|
+
|
|
86
89
|
function panelPlacementMarker(options: {
|
|
87
90
|
isTopOpen: boolean;
|
|
88
91
|
isBottomOpen: boolean;
|
|
@@ -323,7 +326,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
323
326
|
const topIds = new Set(pm.getTopPane().panels.map(p => p.id));
|
|
324
327
|
const bottomIds = new Set(pm.getBottomPane().panels.map(p => p.id));
|
|
325
328
|
const focusedPane = pm.getFocusedPane();
|
|
326
|
-
const footerLines = [buildPanelLine(width, [[` [${this._selectedIndex + 1}/${panelEntries.length}] ↑/↓ nav Enter open T/B place M move S split Tab focus
|
|
329
|
+
const footerLines = [buildPanelLine(width, [[truncateDisplay(` [${this._selectedIndex + 1}/${panelEntries.length}] ↑/↓ nav Enter open T/B place M move S split Tab focus`, width), C.hint]])];
|
|
327
330
|
const postureLines: Line[] = [
|
|
328
331
|
buildKeyValueLine(width, [
|
|
329
332
|
{ label: 'visible panels', value: String(pm.getAllOpen().length), valueColor: pm.getAllOpen().length > 0 ? C.name : C.dim },
|
|
@@ -345,10 +348,10 @@ export class PanelListPanel extends BasePanel {
|
|
|
345
348
|
let flatPanelIndex = 0;
|
|
346
349
|
for (const entry of entries) {
|
|
347
350
|
if (entry.kind === 'header') {
|
|
348
|
-
const label = ` ── ${
|
|
351
|
+
const label = ` ── ${entry.label} ${'─'.repeat(Math.max(0, width - 6 - entry.label.length))}`;
|
|
349
352
|
renderedBlocks.push({
|
|
350
353
|
entry,
|
|
351
|
-
lines: [buildPanelLine(width, [[label
|
|
354
|
+
lines: [buildPanelLine(width, [[truncateDisplay(label, width), C.category, C.categoryBg]])],
|
|
352
355
|
});
|
|
353
356
|
} else {
|
|
354
357
|
const flatIdx = flatPanelIndex++;
|
|
@@ -358,17 +361,23 @@ export class PanelListPanel extends BasePanel {
|
|
|
358
361
|
const dot = isTopOpen || isBottomOpen ? '●' : '○';
|
|
359
362
|
const dotColor = isTopOpen || isBottomOpen ? C.openDot : C.closedDot;
|
|
360
363
|
const nameColor = isSelected ? C.selected : C.name;
|
|
361
|
-
const nameStr = entry.reg.name
|
|
364
|
+
const nameStr = fitDisplay(entry.reg.name, NAME_COL_WIDTH);
|
|
362
365
|
const descStartCol = PREFIX_WIDTH + NAME_COL_WIDTH + 1;
|
|
363
366
|
const descWidth = Math.max(1, width - descStartCol);
|
|
364
367
|
const descLines = wrapPanelDescription(entry.reg.description, descWidth, 2);
|
|
365
368
|
const placement = panelPlacementMarker({ isTopOpen, isBottomOpen, focusedPane });
|
|
369
|
+
// When searching, the flat result list loses its category grouping — so
|
|
370
|
+
// tag each result with a dim [Category] so the origin stays discoverable.
|
|
371
|
+
const categoryTag = this._query
|
|
372
|
+
? `[${CATEGORY_LABELS[entry.reg.category]}] `
|
|
373
|
+
: '';
|
|
366
374
|
const blockLines: Line[] = [
|
|
367
375
|
buildPanelListRow(width, [
|
|
368
376
|
{ text: dot, fg: dotColor },
|
|
369
377
|
{ text: placement.text, fg: placement.color },
|
|
370
378
|
{ text: ' ', fg: C.dim },
|
|
371
379
|
{ text: `${nameStr} `, fg: nameColor },
|
|
380
|
+
{ text: categoryTag, fg: C.category },
|
|
372
381
|
{ text: descLines[0] ?? '', fg: C.desc },
|
|
373
382
|
], C, { selected: isSelected, selectedBg: C.selectedBg, markerColor: C.selIcon }),
|
|
374
383
|
];
|
|
@@ -443,6 +452,22 @@ export class PanelListPanel extends BasePanel {
|
|
|
443
452
|
const q = this._query.toLowerCase();
|
|
444
453
|
const entries: ListEntry[] = [];
|
|
445
454
|
|
|
455
|
+
// Recent group (browse mode only): the most-recently-opened panels, newest
|
|
456
|
+
// first, so frequently used panels are one keystroke away.
|
|
457
|
+
if (!q) {
|
|
458
|
+
const byId = new Map(manager.getRegisteredTypes().map((r) => [r.id, r]));
|
|
459
|
+
const recent: PanelRegistration[] = [];
|
|
460
|
+
for (const id of manager.getRecentlyOpened()) {
|
|
461
|
+
const reg = byId.get(id);
|
|
462
|
+
if (reg) recent.push(reg);
|
|
463
|
+
if (recent.length >= RECENT_LIMIT) break;
|
|
464
|
+
}
|
|
465
|
+
if (recent.length > 0) {
|
|
466
|
+
entries.push({ kind: 'header', label: 'Recent' });
|
|
467
|
+
for (const reg of recent) entries.push({ kind: 'panel', reg });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
446
471
|
for (const cat of CATEGORY_ORDER) {
|
|
447
472
|
const regs = byCategory.get(cat) ?? [];
|
|
448
473
|
const filtered = q
|
|
@@ -456,7 +481,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
456
481
|
|
|
457
482
|
if (filtered.length === 0) continue;
|
|
458
483
|
|
|
459
|
-
entries.push({ kind: 'header',
|
|
484
|
+
entries.push({ kind: 'header', label: CATEGORY_LABELS[cat] });
|
|
460
485
|
for (const reg of filtered) {
|
|
461
486
|
entries.push({ kind: 'panel', reg });
|
|
462
487
|
}
|
|
@@ -42,6 +42,22 @@ export class PanelManager {
|
|
|
42
42
|
// Cache for getWorkspaceTabs() — invalidated on every panel lifecycle event
|
|
43
43
|
private _cachedWorkspaceTabs: readonly WorkspaceTab[] | null = null;
|
|
44
44
|
|
|
45
|
+
// Most-recently-opened panel ids (front = newest), for the picker's "Recent"
|
|
46
|
+
// group. Session-scoped; capped to keep it a short list.
|
|
47
|
+
private _recentlyOpened: string[] = [];
|
|
48
|
+
private static readonly RECENT_CAP = 8;
|
|
49
|
+
|
|
50
|
+
/** Record a panel id as most-recently-opened (front of the ring, deduped). */
|
|
51
|
+
private _recordRecent(panelId: string): void {
|
|
52
|
+
this._recentlyOpened = [panelId, ...this._recentlyOpened.filter((id) => id !== panelId)]
|
|
53
|
+
.slice(0, PanelManager.RECENT_CAP);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Most-recently-opened panel ids, newest first. */
|
|
57
|
+
getRecentlyOpened(): readonly string[] {
|
|
58
|
+
return this._recentlyOpened;
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
// -------------------------------------------------------------------------
|
|
46
62
|
// Registration
|
|
47
63
|
// -------------------------------------------------------------------------
|
|
@@ -88,6 +104,7 @@ export class PanelManager {
|
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
open(panelId: string, pane?: 'top' | 'bottom'): Panel {
|
|
107
|
+
this._recordRecent(panelId);
|
|
91
108
|
const existingPane = this._findPaneOf(panelId);
|
|
92
109
|
if (existingPane) {
|
|
93
110
|
this._activateByIdInPane(panelId, existingPane);
|
|
@@ -285,10 +302,11 @@ export class PanelManager {
|
|
|
285
302
|
this.bottomPane.panels.push(panel);
|
|
286
303
|
this.bottomPane.activeIndex = 0;
|
|
287
304
|
} else {
|
|
288
|
-
// Open a default panel in bottom pane
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
305
|
+
// Open a predictable default panel in the bottom pane (the panel list),
|
|
306
|
+
// rather than an arbitrary registration-order-dependent panel.
|
|
307
|
+
const defaultPanel = this._getRegistration('panel-list') ?? this.registry[0];
|
|
308
|
+
if (defaultPanel) {
|
|
309
|
+
this.open(defaultPanel.id, 'bottom');
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
312
|
}
|
|
@@ -460,6 +478,7 @@ export class PanelManager {
|
|
|
460
478
|
this.bottomPane = { panels: [], activeIndex: 0 };
|
|
461
479
|
this.retainedPanels.clear();
|
|
462
480
|
this.registry = [];
|
|
481
|
+
this._recentlyOpened = [];
|
|
463
482
|
this._focusedPane = 'top';
|
|
464
483
|
this._bottomPaneVisible = false;
|
|
465
484
|
this._visible = false;
|
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import type { ExecutionPlan, PlanItem, PlanItemStatus } from '@pellux/goodvibes-sdk/platform/core';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { BasePanel } from './base-panel.ts';
|
|
4
5
|
import type { PlanDashboardQuery } from '../runtime/ui-service-queries.ts';
|
|
5
6
|
import {
|
|
6
7
|
buildEmptyState,
|
|
8
|
+
buildKeyboardHints,
|
|
9
|
+
buildKeyValueLine,
|
|
10
|
+
buildMeterLine,
|
|
7
11
|
buildPanelLine,
|
|
8
12
|
buildPanelWorkspace,
|
|
13
|
+
buildSelectablePanelLine,
|
|
9
14
|
resolveScrollablePanelSection,
|
|
10
15
|
DEFAULT_PANEL_PALETTE,
|
|
16
|
+
extendPalette,
|
|
11
17
|
type PanelWorkspaceSection,
|
|
12
18
|
} from './polish.ts';
|
|
13
19
|
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Palette (no inline hex in render bodies)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
25
|
+
complete: '#22c55e',
|
|
26
|
+
active: '#00ffff',
|
|
27
|
+
pending: '#94a3b8',
|
|
28
|
+
failed: '#ef4444',
|
|
29
|
+
skipped: '#64748b',
|
|
30
|
+
blocked: '#f97316',
|
|
31
|
+
selectBg: '#1e293b',
|
|
32
|
+
phase: '#cbd5e1',
|
|
33
|
+
});
|
|
34
|
+
|
|
14
35
|
// ---------------------------------------------------------------------------
|
|
15
36
|
// Status display maps
|
|
16
37
|
// ---------------------------------------------------------------------------
|
|
@@ -24,11 +45,11 @@ const STATUS_ICON: Record<PlanItemStatus, string> = {
|
|
|
24
45
|
};
|
|
25
46
|
|
|
26
47
|
const STATUS_FG: Record<PlanItemStatus, string> = {
|
|
27
|
-
complete:
|
|
28
|
-
in_progress:
|
|
29
|
-
pending:
|
|
30
|
-
failed:
|
|
31
|
-
skipped:
|
|
48
|
+
complete: C.complete,
|
|
49
|
+
in_progress: C.active,
|
|
50
|
+
pending: C.pending,
|
|
51
|
+
failed: C.failed,
|
|
52
|
+
skipped: C.skipped,
|
|
32
53
|
};
|
|
33
54
|
|
|
34
55
|
// ---------------------------------------------------------------------------
|
|
@@ -41,6 +62,9 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
41
62
|
|
|
42
63
|
// Flat list of navigable row indices (set during render)
|
|
43
64
|
private totalRows = 0;
|
|
65
|
+
// Item flat-list parallel to navigable rows (set during render) for detail.
|
|
66
|
+
private navItems: PlanItem[] = [];
|
|
67
|
+
private blockedItemIds = new Set<string>();
|
|
44
68
|
private readonly planManager: PlanDashboardQuery;
|
|
45
69
|
|
|
46
70
|
constructor(planManager: PlanDashboardQuery) {
|
|
@@ -71,6 +95,19 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
71
95
|
this.markDirty();
|
|
72
96
|
return true;
|
|
73
97
|
}
|
|
98
|
+
} else if (key === 'home' || key === 'g') {
|
|
99
|
+
if (this.selectedIndex !== 0) {
|
|
100
|
+
this.selectedIndex = 0;
|
|
101
|
+
this.markDirty();
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
} else if (key === 'end' || key === 'G') {
|
|
105
|
+
const last = Math.max(0, this.totalRows - 1);
|
|
106
|
+
if (this.selectedIndex !== last) {
|
|
107
|
+
this.selectedIndex = last;
|
|
108
|
+
this.markDirty();
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
74
111
|
}
|
|
75
112
|
return false;
|
|
76
113
|
}
|
|
@@ -91,13 +128,15 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
91
128
|
lines: buildEmptyState(
|
|
92
129
|
width,
|
|
93
130
|
' No active execution plan',
|
|
94
|
-
'
|
|
95
|
-
[
|
|
96
|
-
|
|
131
|
+
'No plan is being executed yet. Create one and its phases, progress, blocked steps, and next action surface here.',
|
|
132
|
+
[
|
|
133
|
+
{ command: '/plan', summary: 'create an execution plan to populate this dashboard' },
|
|
134
|
+
],
|
|
135
|
+
C,
|
|
97
136
|
),
|
|
98
137
|
},
|
|
99
138
|
],
|
|
100
|
-
palette:
|
|
139
|
+
palette: C,
|
|
101
140
|
});
|
|
102
141
|
}
|
|
103
142
|
return this.renderPlan(plan, width, height);
|
|
@@ -105,7 +144,7 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
// --------------------------------------------------------------------------
|
|
108
|
-
//
|
|
147
|
+
// Plan render
|
|
109
148
|
// --------------------------------------------------------------------------
|
|
110
149
|
|
|
111
150
|
private renderPlan(plan: ExecutionPlan, width: number, height: number): Line[] {
|
|
@@ -126,9 +165,17 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
126
165
|
.map((i) => i.id),
|
|
127
166
|
);
|
|
128
167
|
|
|
168
|
+
const isBlocked = (item: PlanItem): boolean =>
|
|
169
|
+
item.status === 'pending' &&
|
|
170
|
+
item.dependencies !== undefined &&
|
|
171
|
+
item.dependencies.length > 0 &&
|
|
172
|
+
!item.dependencies.every((depId) => completeIds.has(depId));
|
|
173
|
+
|
|
129
174
|
let rowCount = 0;
|
|
130
175
|
let selectedLineIndex = 0;
|
|
131
176
|
const planLines: Line[] = [];
|
|
177
|
+
const navItems: PlanItem[] = [];
|
|
178
|
+
const blockedItemIds = new Set<string>();
|
|
132
179
|
|
|
133
180
|
for (const phase of phaseOrder) {
|
|
134
181
|
const items = byPhase.get(phase)!;
|
|
@@ -136,52 +183,86 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
136
183
|
for (const item of items) {
|
|
137
184
|
const isSelected = rowCount === this.selectedIndex;
|
|
138
185
|
if (isSelected) selectedLineIndex = planLines.length;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
!item.dependencies.every((depId) => completeIds.has(depId));
|
|
144
|
-
|
|
145
|
-
planLines.push(this.renderItem(item, isSelected, isBlocked, width));
|
|
186
|
+
const blocked = isBlocked(item);
|
|
187
|
+
if (blocked) blockedItemIds.add(item.id);
|
|
188
|
+
navItems.push(item);
|
|
189
|
+
planLines.push(this.renderItem(item, isSelected, blocked, width));
|
|
146
190
|
rowCount++;
|
|
147
191
|
}
|
|
148
192
|
}
|
|
149
193
|
|
|
150
194
|
this.totalRows = rowCount;
|
|
195
|
+
this.navItems = navItems;
|
|
196
|
+
this.blockedItemIds = blockedItemIds;
|
|
151
197
|
if (this.selectedIndex >= this.totalRows) {
|
|
152
198
|
this.selectedIndex = Math.max(0, this.totalRows - 1);
|
|
153
199
|
}
|
|
154
200
|
|
|
155
201
|
const total = plan.items.length;
|
|
156
202
|
const done = plan.items.filter((i) => i.status === 'complete' || i.status === 'skipped').length;
|
|
203
|
+
const inProgress = plan.items.filter((i) => i.status === 'in_progress').length;
|
|
204
|
+
const failed = plan.items.filter((i) => i.status === 'failed').length;
|
|
205
|
+
const blockedCount = blockedItemIds.size;
|
|
157
206
|
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
207
|
+
|
|
208
|
+
// The single most useful pointer: what should happen next. Prefer the first
|
|
209
|
+
// in-progress item, else the first actionable (unblocked) pending item.
|
|
210
|
+
const nextItem = plan.items.find((i) => i.status === 'in_progress')
|
|
211
|
+
?? plan.items.find((i) => i.status === 'pending' && !isBlocked(i));
|
|
212
|
+
|
|
213
|
+
const meterWidth = Math.max(10, Math.min(28, width - 30));
|
|
158
214
|
const summary: PanelWorkspaceSection = {
|
|
159
215
|
title: 'Summary',
|
|
160
216
|
lines: [
|
|
161
217
|
buildPanelLine(width, [
|
|
162
|
-
[' Status ',
|
|
218
|
+
[' Status ', C.label],
|
|
163
219
|
[plan.status, this.statusColor(plan.status)],
|
|
164
|
-
['
|
|
165
|
-
[`${
|
|
166
|
-
['
|
|
167
|
-
[`${
|
|
220
|
+
[' ', C.label],
|
|
221
|
+
[`${done}/${total} items`, C.value],
|
|
222
|
+
[' ', C.label],
|
|
223
|
+
[`${pct}%`, pct === 100 ? C.good : C.info],
|
|
168
224
|
]),
|
|
225
|
+
buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
|
|
226
|
+
filled: pct === 100 ? C.good : C.info,
|
|
227
|
+
empty: C.empty,
|
|
228
|
+
label: C.label,
|
|
229
|
+
}, { prefix: ' Progress ', suffix: ` ${pct}% ` }),
|
|
230
|
+
buildKeyValueLine(width, [
|
|
231
|
+
{ label: 'in progress', value: String(inProgress), valueColor: inProgress > 0 ? C.active : C.dim },
|
|
232
|
+
{ label: 'blocked', value: String(blockedCount), valueColor: blockedCount > 0 ? C.blocked : C.dim },
|
|
233
|
+
{ label: 'failed', value: String(failed), valueColor: failed > 0 ? C.bad : C.dim },
|
|
234
|
+
], C),
|
|
235
|
+
nextItem
|
|
236
|
+
? buildPanelLine(width, [
|
|
237
|
+
[' Next ', C.label],
|
|
238
|
+
[STATUS_ICON[nextItem.status], STATUS_FG[nextItem.status]],
|
|
239
|
+
[' ', C.label],
|
|
240
|
+
[truncateDisplay(nextItem.description, Math.max(8, width - 9)), C.value],
|
|
241
|
+
])
|
|
242
|
+
: buildPanelLine(width, [
|
|
243
|
+
[' Next ', C.label],
|
|
244
|
+
[failed > 0 ? 'review failed steps' : blockedCount > 0 ? 'unblock dependencies' : 'plan complete',
|
|
245
|
+
failed > 0 ? C.bad : blockedCount > 0 ? C.blocked : C.good],
|
|
246
|
+
]),
|
|
169
247
|
],
|
|
170
248
|
};
|
|
171
249
|
|
|
250
|
+
const detailSection = this.buildDetailSection(width);
|
|
251
|
+
const footerLines = this.footerLines(width);
|
|
252
|
+
|
|
172
253
|
const planSection = resolveScrollablePanelSection(width, height, {
|
|
173
254
|
intro: plan.title,
|
|
174
|
-
footerLines
|
|
175
|
-
|
|
176
|
-
],
|
|
177
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
255
|
+
footerLines,
|
|
256
|
+
palette: C,
|
|
178
257
|
beforeSections: [summary],
|
|
258
|
+
afterSections: detailSection ? [detailSection] : [],
|
|
179
259
|
section: {
|
|
180
260
|
title: 'Execution Plan',
|
|
181
261
|
scrollableLines: planLines,
|
|
182
262
|
selectedIndex: selectedLineIndex,
|
|
183
263
|
scrollOffset: this.scrollOffset,
|
|
184
|
-
minRows:
|
|
264
|
+
minRows: 6,
|
|
265
|
+
appendWindowSummary: { dimColor: C.dim },
|
|
185
266
|
},
|
|
186
267
|
});
|
|
187
268
|
this.scrollOffset = planSection.scrollOffset;
|
|
@@ -192,16 +273,51 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
192
273
|
sections: [
|
|
193
274
|
summary,
|
|
194
275
|
planSection.section,
|
|
276
|
+
...(detailSection ? [detailSection] : []),
|
|
195
277
|
],
|
|
196
|
-
footerLines
|
|
197
|
-
|
|
198
|
-
],
|
|
199
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
278
|
+
footerLines,
|
|
279
|
+
palette: C,
|
|
200
280
|
});
|
|
201
281
|
}
|
|
202
282
|
|
|
283
|
+
private buildDetailSection(width: number): PanelWorkspaceSection | null {
|
|
284
|
+
const item = this.navItems[this.selectedIndex];
|
|
285
|
+
if (!item) return null;
|
|
286
|
+
const blocked = this.blockedItemIds.has(item.id);
|
|
287
|
+
const lines: Line[] = [
|
|
288
|
+
buildPanelLine(width, [
|
|
289
|
+
[' ', C.label],
|
|
290
|
+
[STATUS_ICON[item.status], STATUS_FG[item.status]],
|
|
291
|
+
[' ', C.label],
|
|
292
|
+
[truncateDisplay(item.description, Math.max(8, width - 4)), C.value],
|
|
293
|
+
]),
|
|
294
|
+
buildKeyValueLine(width, [
|
|
295
|
+
{ label: 'phase', value: item.phase, valueColor: C.phase },
|
|
296
|
+
{ label: 'status', value: blocked ? 'blocked' : item.status.replace(/_/g, ' '), valueColor: blocked ? C.blocked : STATUS_FG[item.status] },
|
|
297
|
+
...(item.agentId ? [{ label: 'agent', value: item.agentId, valueColor: C.info }] : []),
|
|
298
|
+
], C),
|
|
299
|
+
];
|
|
300
|
+
if (item.dependencies && item.dependencies.length > 0) {
|
|
301
|
+
lines.push(buildPanelLine(width, [
|
|
302
|
+
[' depends on ', C.label],
|
|
303
|
+
[truncateDisplay(item.dependencies.join(', '), Math.max(8, width - 13)), blocked ? C.blocked : C.dim],
|
|
304
|
+
]));
|
|
305
|
+
}
|
|
306
|
+
return { title: 'Selected Step', lines };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private footerLines(width: number): Line[] {
|
|
310
|
+
return [
|
|
311
|
+
buildKeyboardHints(width, [
|
|
312
|
+
{ keys: this.totalRows > 0 ? `${this.selectedIndex + 1}/${this.totalRows}` : '0/0', label: 'step' },
|
|
313
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
314
|
+
{ keys: 'Home/End', label: 'jump' },
|
|
315
|
+
], C),
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
|
|
203
319
|
// --------------------------------------------------------------------------
|
|
204
|
-
//
|
|
320
|
+
// Phase header — title + progress meter
|
|
205
321
|
// --------------------------------------------------------------------------
|
|
206
322
|
|
|
207
323
|
private renderPhaseHeaderLine(phase: string, items: PlanItem[], width: number): Line {
|
|
@@ -212,21 +328,20 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
212
328
|
const active = items.some((i) => i.status === 'in_progress');
|
|
213
329
|
const failed = items.some((i) => i.status === 'failed');
|
|
214
330
|
|
|
215
|
-
const phaseFg = failed ?
|
|
331
|
+
const phaseFg = failed ? C.failed : active ? C.active : done === total ? C.complete : C.phase;
|
|
216
332
|
|
|
217
333
|
const barW = 8;
|
|
218
334
|
const filledB = total > 0 ? Math.round((done / total) * barW) : 0;
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
]);
|
|
335
|
+
const progressText = ` ${done}/${total}`;
|
|
336
|
+
const label = truncateDisplay(phase, Math.max(4, width - barW - progressText.length - 6));
|
|
337
|
+
|
|
338
|
+
return buildSelectablePanelLine(width, [
|
|
339
|
+
{ text: ` ${active ? '▸' : '▪'} `, fg: phaseFg, bold: true },
|
|
340
|
+
{ text: label, fg: phaseFg, bold: true },
|
|
341
|
+
{ text: ' ', fg: C.dim },
|
|
342
|
+
{ text: '▰'.repeat(filledB) + '▱'.repeat(barW - filledB), fg: phaseFg },
|
|
343
|
+
{ text: progressText, fg: C.dim },
|
|
344
|
+
], { fillBg: C.sectionBg });
|
|
230
345
|
}
|
|
231
346
|
|
|
232
347
|
// --------------------------------------------------------------------------
|
|
@@ -240,35 +355,37 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
240
355
|
width: number,
|
|
241
356
|
): Line {
|
|
242
357
|
const icon = STATUS_ICON[item.status];
|
|
243
|
-
const fg = isBlocked ?
|
|
244
|
-
const dim = item.status === 'skipped'
|
|
358
|
+
const fg = isBlocked ? C.blocked : STATUS_FG[item.status];
|
|
359
|
+
const dim = item.status === 'skipped';
|
|
245
360
|
|
|
246
361
|
// Indent blocked items to visually signal they depend on others
|
|
247
|
-
const indent = isBlocked ? '
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
text
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
362
|
+
const indent = isBlocked ? ' ' : ' ';
|
|
363
|
+
const agentTag = item.agentId && (item.status === 'in_progress' || item.status === 'complete')
|
|
364
|
+
? ` [${item.agentId}]`
|
|
365
|
+
: '';
|
|
366
|
+
const blockedTag = isBlocked ? ' ⊘ blocked' : '';
|
|
367
|
+
const reserve = indent.length + 2 + agentTag.length + blockedTag.length;
|
|
368
|
+
const desc = truncateDisplay(item.description, Math.max(6, width - reserve - 2));
|
|
369
|
+
|
|
370
|
+
return buildSelectablePanelLine(width, [
|
|
371
|
+
{ text: `${indent}${icon} `, fg, dim, bold: item.status === 'in_progress' },
|
|
372
|
+
{ text: desc, fg, dim },
|
|
373
|
+
{ text: agentTag, fg: C.info },
|
|
374
|
+
{ text: blockedTag, fg: C.blocked },
|
|
375
|
+
], {
|
|
376
|
+
selected: isSelected,
|
|
377
|
+
selectedBg: C.selectBg,
|
|
378
|
+
leadingMarker: '▸',
|
|
379
|
+
});
|
|
263
380
|
}
|
|
264
381
|
|
|
265
382
|
private statusColor(status: ExecutionPlan['status']): string {
|
|
266
383
|
return status === 'complete'
|
|
267
|
-
?
|
|
384
|
+
? C.good
|
|
268
385
|
: status === 'failed'
|
|
269
|
-
?
|
|
386
|
+
? C.bad
|
|
270
387
|
: status === 'active'
|
|
271
|
-
?
|
|
272
|
-
:
|
|
388
|
+
? C.info
|
|
389
|
+
: C.dim;
|
|
273
390
|
}
|
|
274
391
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
5
|
import type { PluginManagerObserver, PluginStatus } from '@pellux/goodvibes-sdk/platform/plugins';
|
|
5
6
|
import {
|
|
7
|
+
buildDetailBlock,
|
|
6
8
|
buildEmptyState,
|
|
9
|
+
buildKeyValueLine,
|
|
10
|
+
buildKeyboardHints,
|
|
7
11
|
buildPanelLine,
|
|
8
12
|
buildPanelWorkspace,
|
|
9
13
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -53,10 +57,18 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
53
57
|
public constructor(manager: PluginManagerObserver) {
|
|
54
58
|
super('plugins', 'Plugins', 'P', 'monitoring');
|
|
55
59
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
60
|
+
this.filterEnabled = true;
|
|
61
|
+
this.filterLabel = 'Filter plugins';
|
|
56
62
|
this.manager = manager;
|
|
57
63
|
this.unsub = manager.subscribe(() => this.markDirty());
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
protected override filterMatches(plugin: PluginStatus, q: string): boolean {
|
|
67
|
+
return plugin.name.toLowerCase().includes(q)
|
|
68
|
+
|| plugin.trustTier.toLowerCase().includes(q)
|
|
69
|
+
|| plugin.version.toLowerCase().includes(q);
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
public override onActivate(): void {
|
|
61
73
|
super.onActivate();
|
|
62
74
|
this.selectedIndex = 0;
|
|
@@ -122,15 +134,30 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
122
134
|
return workspace;
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
// Provenance/error posture header — surface trust + quarantine pressure first.
|
|
138
|
+
const quarantined = plugins.filter((p) => p.quarantined).length;
|
|
139
|
+
const untrusted = plugins.filter((p) => p.trustTier === 'untrusted').length;
|
|
140
|
+
const active = plugins.filter((p) => p.active).length;
|
|
141
|
+
const headerLines: Line[] = [
|
|
142
|
+
buildKeyValueLine(width, [
|
|
143
|
+
{ label: 'plugins', value: String(plugins.length), valueColor: C.info },
|
|
144
|
+
{ label: 'active', value: String(active), valueColor: active > 0 ? C.ok : C.dim },
|
|
145
|
+
{ label: 'untrusted', value: String(untrusted), valueColor: untrusted > 0 ? C.warn : C.dim },
|
|
146
|
+
{ label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.error : C.dim },
|
|
147
|
+
], C),
|
|
148
|
+
];
|
|
149
|
+
|
|
125
150
|
this.clampSelection();
|
|
126
151
|
const selected = plugins[this.selectedIndex]!;
|
|
127
152
|
const selectedCaps = this.manager.capabilities(selected.name);
|
|
128
153
|
const trustRecord = this.manager.getTrustRecord(selected.name);
|
|
129
154
|
const quarantineRecord = this.manager.getQuarantineRecord(selected.name);
|
|
130
|
-
const
|
|
155
|
+
const detailRows: Line[] = [
|
|
131
156
|
buildPanelLine(width, [
|
|
132
157
|
[' Plugin: ', C.label],
|
|
133
158
|
[selected.name, C.value],
|
|
159
|
+
[' v', C.label],
|
|
160
|
+
[selected.version, C.dim],
|
|
134
161
|
[' State: ', C.label],
|
|
135
162
|
[statusLabel(selected), statusColor(selected)],
|
|
136
163
|
[' Trust: ', C.label],
|
|
@@ -138,12 +165,12 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
138
165
|
]),
|
|
139
166
|
buildPanelLine(width, [
|
|
140
167
|
[' Description: ', C.label],
|
|
141
|
-
[selected.description
|
|
168
|
+
[truncateDisplay(selected.description, Math.max(0, width - 15)), C.dim],
|
|
142
169
|
]),
|
|
143
170
|
];
|
|
144
171
|
|
|
145
172
|
if (selectedCaps) {
|
|
146
|
-
|
|
173
|
+
detailRows.push(buildPanelLine(width, [
|
|
147
174
|
[' Capabilities: ', C.label],
|
|
148
175
|
[String(selectedCaps.requested.length), C.value],
|
|
149
176
|
[' High-risk: ', C.label],
|
|
@@ -154,25 +181,36 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
|
154
181
|
}
|
|
155
182
|
|
|
156
183
|
if (trustRecord?.signatureFingerprint) {
|
|
157
|
-
|
|
184
|
+
detailRows.push(buildPanelLine(width, [
|
|
158
185
|
[' Signature: ', C.label],
|
|
159
|
-
[trustRecord.signatureFingerprint, C.info],
|
|
186
|
+
[truncateDisplay(trustRecord.signatureFingerprint, Math.max(0, width - 14)), C.info],
|
|
160
187
|
]));
|
|
188
|
+
} else {
|
|
189
|
+
detailRows.push(buildPanelLine(width, [[' Signature: unsigned (no provenance fingerprint on record)', C.warn]]));
|
|
161
190
|
}
|
|
162
191
|
|
|
163
192
|
if (quarantineRecord) {
|
|
164
|
-
|
|
193
|
+
detailRows.push(buildPanelLine(width, [
|
|
165
194
|
[' Quarantine: ', C.label],
|
|
166
|
-
[quarantineRecord.reason
|
|
195
|
+
[truncateDisplay(quarantineRecord.reason, Math.max(0, width - 14)), C.error],
|
|
167
196
|
]));
|
|
168
197
|
}
|
|
169
198
|
|
|
170
|
-
|
|
171
|
-
|
|
199
|
+
const hints = this.filterActive
|
|
200
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
201
|
+
: [
|
|
202
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
203
|
+
{ keys: '/plugin', label: 'act' },
|
|
204
|
+
{ keys: '/', label: 'filter' },
|
|
205
|
+
];
|
|
172
206
|
|
|
173
207
|
return this.renderList(width, height, {
|
|
174
208
|
title: 'Plugin Control Room',
|
|
175
|
-
|
|
209
|
+
header: headerLines,
|
|
210
|
+
footer: [
|
|
211
|
+
...buildDetailBlock(width, `Plugin · ${selected.name}`, detailRows, C),
|
|
212
|
+
buildKeyboardHints(width, hints, C),
|
|
213
|
+
],
|
|
176
214
|
});
|
|
177
215
|
}
|
|
178
216
|
}
|