@pellux/goodvibes-tui 0.18.20 → 0.19.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 +154 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +22 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/session.ts +0 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +119 -119
- package/src/input/keybindings.ts +30 -0
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +55 -82
- package/src/panels/automation-control-panel.ts +120 -161
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +69 -107
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +117 -172
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +103 -138
- package/src/panels/incident-review-panel.ts +59 -109
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +77 -93
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +110 -155
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +51 -85
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +25 -2
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +37 -60
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +91 -141
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +64 -16
- package/src/panels/security-panel.ts +118 -152
- package/src/panels/services-panel.ts +63 -105
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +79 -123
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +64 -86
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +130 -179
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +89 -137
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +23 -1
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +48 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrchestrationPanel — displays task graphs, node contracts, recursion guards,
|
|
3
|
+
* and WRFC-visible orchestration state.
|
|
4
|
+
*
|
|
5
|
+
* Migrated (Wave B2): extends ScrollableListPanel<OrchestrationGraphRecord>.
|
|
6
|
+
* Navigation (up/down/j/k) is handled by the base class.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import type { Line } from '../types/grid.ts';
|
|
2
10
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
11
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
12
|
import type { UiOrchestrationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
13
|
+
import type { OrchestrationGraphRecord } from '@pellux/goodvibes-sdk/platform/runtime/store/domains/orchestration';
|
|
5
14
|
import {
|
|
6
15
|
buildEmptyState,
|
|
7
16
|
buildGuidanceLine,
|
|
@@ -10,11 +19,12 @@ import {
|
|
|
10
19
|
buildPanelWorkspace,
|
|
11
20
|
resolveScrollablePanelSection,
|
|
12
21
|
DEFAULT_PANEL_PALETTE,
|
|
22
|
+
extendPalette,
|
|
23
|
+
type PanelPalette,
|
|
13
24
|
type PanelWorkspaceSection,
|
|
14
25
|
} from './polish.ts';
|
|
15
26
|
|
|
16
|
-
const C = {
|
|
17
|
-
...DEFAULT_PANEL_PALETTE,
|
|
27
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
18
28
|
header: '#94a3b8',
|
|
19
29
|
headerBg: '#1e293b',
|
|
20
30
|
running: '#22c55e',
|
|
@@ -23,30 +33,22 @@ const C = {
|
|
|
23
33
|
failed: '#ef4444',
|
|
24
34
|
completed: '#a78bfa',
|
|
25
35
|
selectBg: '#0f172a',
|
|
26
|
-
} as const;
|
|
36
|
+
} as const);
|
|
27
37
|
|
|
28
38
|
function statusColor(status: string): string {
|
|
29
39
|
switch (status) {
|
|
30
|
-
case 'ready':
|
|
31
|
-
|
|
32
|
-
case '
|
|
33
|
-
|
|
34
|
-
case '
|
|
35
|
-
|
|
36
|
-
case 'failed':
|
|
37
|
-
return C.failed;
|
|
38
|
-
case 'completed':
|
|
39
|
-
return C.completed;
|
|
40
|
-
default:
|
|
41
|
-
return C.dim;
|
|
40
|
+
case 'ready': return C.ready;
|
|
41
|
+
case 'running': return C.running;
|
|
42
|
+
case 'blocked': return C.blocked;
|
|
43
|
+
case 'failed': return C.failed;
|
|
44
|
+
case 'completed': return C.completed;
|
|
45
|
+
default: return C.dim;
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
export class OrchestrationPanel extends
|
|
49
|
+
export class OrchestrationPanel extends ScrollableListPanel<OrchestrationGraphRecord> {
|
|
46
50
|
private readonly readModel?: UiReadModel<UiOrchestrationSnapshot>;
|
|
47
51
|
private readonly unsub: (() => void) | null;
|
|
48
|
-
private selectedIndex = 0;
|
|
49
|
-
private scrollOffset = 0;
|
|
50
52
|
|
|
51
53
|
public constructor(readModel?: UiReadModel<UiOrchestrationSnapshot>) {
|
|
52
54
|
super('orchestration', 'Orchestration', 'Q', 'monitoring');
|
|
@@ -58,32 +60,52 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
58
60
|
this.unsub?.();
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (key === 'up' || key === 'k') {
|
|
65
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
66
|
-
this.markDirty();
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
if (key === 'down' || key === 'j') {
|
|
70
|
-
this.selectedIndex = Math.min(graphs.length - 1, this.selectedIndex + 1);
|
|
71
|
-
this.markDirty();
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// ScrollableListPanel contract
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
76
66
|
|
|
77
|
-
|
|
67
|
+
protected getItems(): readonly OrchestrationGraphRecord[] {
|
|
78
68
|
if (!this.readModel) return [];
|
|
79
69
|
return [...this.readModel.getSnapshot().graphs].sort((a, b) => b.createdAt - a.createdAt);
|
|
80
70
|
}
|
|
81
71
|
|
|
72
|
+
protected renderItem(
|
|
73
|
+
graph: OrchestrationGraphRecord,
|
|
74
|
+
index: number,
|
|
75
|
+
selected: boolean,
|
|
76
|
+
width: number,
|
|
77
|
+
): Line {
|
|
78
|
+
const bg = selected ? C.selectBg : undefined;
|
|
79
|
+
return buildPanelLine(width, [
|
|
80
|
+
[' ', C.label, bg],
|
|
81
|
+
[graph.status.padEnd(10), statusColor(graph.status), bg],
|
|
82
|
+
[` ${graph.mode.padEnd(17)}`, C.value, bg],
|
|
83
|
+
[` ${graph.id.slice(0, 8)} `, C.dim, bg],
|
|
84
|
+
[graph.title.slice(0, Math.max(0, width - 39)), C.value, bg],
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected override getPalette(): PanelPalette {
|
|
89
|
+
return C;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected override getEmptyStateMessage(): string {
|
|
93
|
+
return ' No orchestration graphs recorded yet.';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Input — base class handles all navigation (up/down/j/k/pageup/pagedown/g/G)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Render — multi-section layout (posture + scrollable graphs + detail + nodes)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
82
104
|
public render(width: number, height: number): Line[] {
|
|
83
|
-
this.needsRender = false;
|
|
84
105
|
const intro = 'Task graphs, node contracts, recursion guards, and WRFC-visible orchestration state.';
|
|
85
106
|
|
|
86
107
|
if (!this.readModel) {
|
|
108
|
+
this.needsRender = false;
|
|
87
109
|
const workspace = buildPanelWorkspace(width, height, {
|
|
88
110
|
title: 'Orchestration Control Room',
|
|
89
111
|
intro,
|
|
@@ -103,7 +125,7 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
const snapshot = this.readModel.getSnapshot();
|
|
106
|
-
const graphs = this.
|
|
128
|
+
const graphs = this.getItems();
|
|
107
129
|
const postureLines = [
|
|
108
130
|
buildKeyValueLine(width, [
|
|
109
131
|
{ label: 'graphs', value: String(snapshot.totalGraphs), valueColor: snapshot.totalGraphs > 0 ? C.value : C.dim },
|
|
@@ -115,6 +137,7 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
115
137
|
buildGuidanceLine(width, '/orchestration', 'inspect recursive execution posture, graph health, and node contract flow', C),
|
|
116
138
|
];
|
|
117
139
|
if (graphs.length === 0) {
|
|
140
|
+
this.needsRender = false;
|
|
118
141
|
const workspace = buildPanelWorkspace(width, height, {
|
|
119
142
|
title: 'Orchestration Control Room',
|
|
120
143
|
intro,
|
|
@@ -124,7 +147,7 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
124
147
|
...postureLines,
|
|
125
148
|
...buildEmptyState(
|
|
126
149
|
width,
|
|
127
|
-
|
|
150
|
+
this.getEmptyStateMessage(),
|
|
128
151
|
'Graphs, nodes, child contracts, and recursion guard trips will appear here as orchestration starts.',
|
|
129
152
|
[
|
|
130
153
|
{ command: '/tasks', summary: 'create or inspect task flows that feed orchestration graphs' },
|
|
@@ -140,7 +163,7 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
140
163
|
return workspace;
|
|
141
164
|
}
|
|
142
165
|
|
|
143
|
-
this.
|
|
166
|
+
this.clampSelection();
|
|
144
167
|
const selected = graphs[this.selectedIndex]!;
|
|
145
168
|
const detailLines: Line[] = [
|
|
146
169
|
buildPanelLine(width, [
|
|
@@ -208,6 +231,10 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
208
231
|
]);
|
|
209
232
|
});
|
|
210
233
|
|
|
234
|
+
const scrollableLines: Line[] = graphs.map((graph, index) =>
|
|
235
|
+
this.renderItem(graph, index, index === this.selectedIndex, width),
|
|
236
|
+
);
|
|
237
|
+
|
|
211
238
|
const postureSection: PanelWorkspaceSection = { title: 'Orchestration posture', lines: postureLines };
|
|
212
239
|
const selectedGraphSection: PanelWorkspaceSection = { title: 'Selected Graph', lines: detailLines };
|
|
213
240
|
const nodesSection: PanelWorkspaceSection = { title: 'Nodes', lines: nodeLines };
|
|
@@ -217,24 +244,15 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
217
244
|
beforeSections: [postureSection],
|
|
218
245
|
section: {
|
|
219
246
|
title: 'Graphs',
|
|
220
|
-
scrollableLines
|
|
221
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
222
|
-
return buildPanelLine(width, [
|
|
223
|
-
[' ', C.label, bg],
|
|
224
|
-
[graph.status.padEnd(10), statusColor(graph.status), bg],
|
|
225
|
-
[` ${graph.mode.padEnd(17)}`, C.value, bg],
|
|
226
|
-
[` ${graph.id.slice(0, 8)} `, C.dim, bg],
|
|
227
|
-
[graph.title.slice(0, Math.max(0, width - 39)), C.value, bg],
|
|
228
|
-
]);
|
|
229
|
-
}),
|
|
247
|
+
scrollableLines,
|
|
230
248
|
selectedIndex: this.selectedIndex,
|
|
231
|
-
scrollOffset: this.
|
|
249
|
+
scrollOffset: this.scrollStart,
|
|
232
250
|
minRows: 4,
|
|
233
251
|
appendWindowSummary: { dimColor: C.dim },
|
|
234
252
|
},
|
|
235
253
|
afterSections: [selectedGraphSection, nodesSection],
|
|
236
254
|
});
|
|
237
|
-
this.
|
|
255
|
+
this.scrollStart = graphsSection.scrollOffset;
|
|
238
256
|
|
|
239
257
|
const sections: PanelWorkspaceSection[] = [
|
|
240
258
|
postureSection,
|
|
@@ -242,6 +260,7 @@ export class OrchestrationPanel extends BasePanel {
|
|
|
242
260
|
selectedGraphSection,
|
|
243
261
|
nodesSection,
|
|
244
262
|
];
|
|
263
|
+
this.needsRender = false;
|
|
245
264
|
const lines = buildPanelWorkspace(width, height, {
|
|
246
265
|
title: 'Orchestration Control Room',
|
|
247
266
|
intro,
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
isPanelSearchCommit,
|
|
40
40
|
isPanelSearchPrintable,
|
|
41
41
|
} from './search-focus.ts';
|
|
42
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
|
|
42
43
|
|
|
43
44
|
// ── Colour palette ────────────────────────────────────────────────────────────
|
|
44
45
|
const C = {
|
|
@@ -163,7 +164,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
163
164
|
try {
|
|
164
165
|
this.panelManager.open(selectedPanel.reg.id);
|
|
165
166
|
} catch (err) {
|
|
166
|
-
|
|
167
|
+
logger.warn(`[panel-list] failed to open panel: ${err}`);
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
this.markDirty();
|
|
@@ -205,7 +206,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
205
206
|
try {
|
|
206
207
|
this.panelManager.open(selectedPanel.reg.id);
|
|
207
208
|
} catch (err) {
|
|
208
|
-
|
|
209
|
+
logger.warn(`[panel-list] failed to open panel: ${err}`);
|
|
209
210
|
}
|
|
210
211
|
this.markDirty();
|
|
211
212
|
}
|
|
@@ -221,7 +222,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
221
222
|
pm.open(selectedPanel.reg.id, pane);
|
|
222
223
|
pm.show();
|
|
223
224
|
} catch (err) {
|
|
224
|
-
|
|
225
|
+
logger.warn(`[panel-list] failed to place panel: ${err}`);
|
|
225
226
|
}
|
|
226
227
|
this.markDirty();
|
|
227
228
|
}
|
|
@@ -234,7 +235,7 @@ export class PanelListPanel extends BasePanel {
|
|
|
234
235
|
try {
|
|
235
236
|
this.panelManager.moveToOtherPane(selectedPanel.reg.id);
|
|
236
237
|
} catch (err) {
|
|
237
|
-
|
|
238
|
+
logger.warn(`[panel-list] failed to move panel: ${err}`);
|
|
238
239
|
}
|
|
239
240
|
this.markDirty();
|
|
240
241
|
}
|
|
@@ -39,6 +39,9 @@ export class PanelManager {
|
|
|
39
39
|
private _verticalSplitRatio: number = 0.5; // top gets 50% of panel height
|
|
40
40
|
private _bottomPaneVisible: boolean = false;
|
|
41
41
|
|
|
42
|
+
// Cache for getWorkspaceTabs() — invalidated on every panel lifecycle event
|
|
43
|
+
private _cachedWorkspaceTabs: readonly WorkspaceTab[] | null = null;
|
|
44
|
+
|
|
42
45
|
// -------------------------------------------------------------------------
|
|
43
46
|
// Registration
|
|
44
47
|
// -------------------------------------------------------------------------
|
|
@@ -79,6 +82,11 @@ export class PanelManager {
|
|
|
79
82
|
// Panel lifecycle — operates on a specific pane (defaults to focused)
|
|
80
83
|
// -------------------------------------------------------------------------
|
|
81
84
|
|
|
85
|
+
/** Invalidate the workspace tab cache. Call on every panel lifecycle mutation. */
|
|
86
|
+
private _invalidateWorkspaceTabs(): void {
|
|
87
|
+
this._cachedWorkspaceTabs = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
open(panelId: string, pane?: 'top' | 'bottom'): Panel {
|
|
83
91
|
const existingPane = this._findPaneOf(panelId);
|
|
84
92
|
if (existingPane) {
|
|
@@ -107,6 +115,7 @@ export class PanelManager {
|
|
|
107
115
|
this._focusedPane = 'top';
|
|
108
116
|
}
|
|
109
117
|
panel.onActivate();
|
|
118
|
+
this._invalidateWorkspaceTabs();
|
|
110
119
|
return panel;
|
|
111
120
|
}
|
|
112
121
|
|
|
@@ -146,6 +155,7 @@ export class PanelManager {
|
|
|
146
155
|
if (this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
|
|
147
156
|
this._visible = false;
|
|
148
157
|
}
|
|
158
|
+
this._invalidateWorkspaceTabs();
|
|
149
159
|
return;
|
|
150
160
|
}
|
|
151
161
|
}
|
|
@@ -187,6 +197,7 @@ export class PanelManager {
|
|
|
187
197
|
p.activeIndex = (p.activeIndex + 1) % p.panels.length;
|
|
188
198
|
const newPanel = p.panels[p.activeIndex];
|
|
189
199
|
if (newPanel) newPanel.onActivate();
|
|
200
|
+
this._invalidateWorkspaceTabs();
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
nextWorkspaceTab(): void {
|
|
@@ -205,6 +216,7 @@ export class PanelManager {
|
|
|
205
216
|
p.activeIndex = (p.activeIndex - 1 + p.panels.length) % p.panels.length;
|
|
206
217
|
const newPanel = p.panels[p.activeIndex];
|
|
207
218
|
if (newPanel) newPanel.onActivate();
|
|
219
|
+
this._invalidateWorkspaceTabs();
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
activateByIndex(index: number): void {
|
|
@@ -216,6 +228,7 @@ export class PanelManager {
|
|
|
216
228
|
p.activeIndex = index;
|
|
217
229
|
const newPanel = p.panels[p.activeIndex];
|
|
218
230
|
if (newPanel) newPanel.onActivate();
|
|
231
|
+
this._invalidateWorkspaceTabs();
|
|
219
232
|
}
|
|
220
233
|
|
|
221
234
|
activateById(panelId: string): void {
|
|
@@ -231,6 +244,7 @@ export class PanelManager {
|
|
|
231
244
|
focusPane(pane: 'top' | 'bottom'): void {
|
|
232
245
|
if (pane === 'bottom' && !this._bottomPaneVisible) return;
|
|
233
246
|
this._focusedPane = pane;
|
|
247
|
+
this._invalidateWorkspaceTabs();
|
|
234
248
|
}
|
|
235
249
|
|
|
236
250
|
getFocusedPane(): 'top' | 'bottom' {
|
|
@@ -246,6 +260,7 @@ export class PanelManager {
|
|
|
246
260
|
togglePaneFocus(): void {
|
|
247
261
|
if (!this._bottomPaneVisible || this.bottomPane.panels.length === 0) return;
|
|
248
262
|
this._focusedPane = this._focusedPane === 'top' ? 'bottom' : 'top';
|
|
263
|
+
this._invalidateWorkspaceTabs();
|
|
249
264
|
}
|
|
250
265
|
|
|
251
266
|
// -------------------------------------------------------------------------
|
|
@@ -253,6 +268,7 @@ export class PanelManager {
|
|
|
253
268
|
// -------------------------------------------------------------------------
|
|
254
269
|
|
|
255
270
|
toggleBottomPane(): void {
|
|
271
|
+
this._invalidateWorkspaceTabs();
|
|
256
272
|
if (this._bottomPaneVisible) {
|
|
257
273
|
this._bottomPaneVisible = false;
|
|
258
274
|
if (this._focusedPane === 'bottom') this._focusedPane = 'top';
|
|
@@ -329,7 +345,8 @@ export class PanelManager {
|
|
|
329
345
|
return this._findPaneOf(panelId);
|
|
330
346
|
}
|
|
331
347
|
|
|
332
|
-
getWorkspaceTabs(): WorkspaceTab[] {
|
|
348
|
+
getWorkspaceTabs(): readonly WorkspaceTab[] {
|
|
349
|
+
if (this._cachedWorkspaceTabs !== null) return this._cachedWorkspaceTabs;
|
|
333
350
|
const focusedPanelId = this.getActivePanel()?.id;
|
|
334
351
|
const topTabs = this.topPane.panels.map((panel) => ({
|
|
335
352
|
id: panel.id,
|
|
@@ -347,7 +364,9 @@ export class PanelManager {
|
|
|
347
364
|
active: panel.id === focusedPanelId,
|
|
348
365
|
focused: panel.id === focusedPanelId,
|
|
349
366
|
}));
|
|
350
|
-
|
|
367
|
+
const tabs = [...topTabs, ...bottomTabs] as WorkspaceTab[];
|
|
368
|
+
this._cachedWorkspaceTabs = tabs;
|
|
369
|
+
return tabs;
|
|
351
370
|
}
|
|
352
371
|
|
|
353
372
|
activateWorkspaceIndex(index: number): void {
|
|
@@ -357,6 +376,7 @@ export class PanelManager {
|
|
|
357
376
|
this._focusedPane = tab.pane;
|
|
358
377
|
if (tab.pane === 'bottom') this._bottomPaneVisible = true;
|
|
359
378
|
this._activateByIdInPane(tab.id, tab.pane);
|
|
379
|
+
this._invalidateWorkspaceTabs();
|
|
360
380
|
}
|
|
361
381
|
|
|
362
382
|
// -------------------------------------------------------------------------
|
|
@@ -439,6 +459,7 @@ export class PanelManager {
|
|
|
439
459
|
this._focusedPane = 'top';
|
|
440
460
|
this._bottomPaneVisible = false;
|
|
441
461
|
this._visible = false;
|
|
462
|
+
this._invalidateWorkspaceTabs();
|
|
442
463
|
}
|
|
443
464
|
|
|
444
465
|
// -------------------------------------------------------------------------
|
|
@@ -491,6 +512,7 @@ export class PanelManager {
|
|
|
491
512
|
this._bottomPaneVisible = true;
|
|
492
513
|
}
|
|
493
514
|
this._focusedPane = dstPaneName;
|
|
515
|
+
this._invalidateWorkspaceTabs();
|
|
494
516
|
}
|
|
495
517
|
|
|
496
518
|
private _cycleWorkspaceTab(direction: 1 | -1): void {
|
|
@@ -533,6 +555,7 @@ export class PanelManager {
|
|
|
533
555
|
p.activeIndex = index;
|
|
534
556
|
const newPanel = p.panels[p.activeIndex];
|
|
535
557
|
if (newPanel) newPanel.onActivate();
|
|
558
|
+
this._invalidateWorkspaceTabs();
|
|
536
559
|
}
|
|
537
560
|
}
|
|
538
561
|
}
|
|
@@ -80,6 +80,7 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
80
80
|
// --------------------------------------------------------------------------
|
|
81
81
|
|
|
82
82
|
render(width: number, height: number): Line[] {
|
|
83
|
+
return this.trackedRender(() => {
|
|
83
84
|
const plan = this.planManager.getActive();
|
|
84
85
|
if (!plan) {
|
|
85
86
|
return buildPanelWorkspace(width, height, {
|
|
@@ -100,6 +101,7 @@ export class PlanDashboardPanel extends BasePanel {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
return this.renderPlan(plan, width, height);
|
|
104
|
+
});
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
// --------------------------------------------------------------------------
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import type { PluginManagerObserver, PluginStatus } from '@pellux/goodvibes-sdk/platform/plugins/manager';
|
|
5
5
|
import {
|
|
6
6
|
buildEmptyState,
|
|
7
7
|
buildPanelLine,
|
|
8
8
|
buildPanelWorkspace,
|
|
9
9
|
DEFAULT_PANEL_PALETTE,
|
|
10
|
-
|
|
11
|
-
type PanelWorkspaceSection,
|
|
10
|
+
type PanelPalette,
|
|
12
11
|
} from './polish.ts';
|
|
13
12
|
|
|
14
13
|
const C = {
|
|
@@ -47,14 +46,13 @@ function statusLabel(status: PluginStatus): string {
|
|
|
47
46
|
return 'DISABLED';
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
export class PluginsPanel extends
|
|
49
|
+
export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
|
|
51
50
|
private readonly manager: PluginManagerObserver;
|
|
52
51
|
private readonly unsub: (() => void) | null;
|
|
53
|
-
private selectedIndex = 0;
|
|
54
|
-
private scrollOffset = 0;
|
|
55
52
|
|
|
56
53
|
public constructor(manager: PluginManagerObserver) {
|
|
57
54
|
super('plugins', 'Plugins', 'P', 'monitoring');
|
|
55
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
58
56
|
this.manager = manager;
|
|
59
57
|
this.unsub = manager.subscribe(() => this.markDirty());
|
|
60
58
|
}
|
|
@@ -68,26 +66,39 @@ export class PluginsPanel extends BasePanel {
|
|
|
68
66
|
this.unsub?.();
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
69
|
+
protected override getPalette(): PanelPalette {
|
|
70
|
+
return C;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected getItems(): readonly PluginStatus[] {
|
|
74
|
+
return this.manager.list();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected renderItem(plugin: PluginStatus, _index: number, selected: boolean, width: number): Line {
|
|
78
|
+
const bg = selected ? C.selectBg : undefined;
|
|
79
|
+
return buildPanelLine(width, [
|
|
80
|
+
[' ', C.label, bg],
|
|
81
|
+
[plugin.name.padEnd(22), C.value, bg],
|
|
82
|
+
[` ${statusLabel(plugin).padEnd(11)}`, statusColor(plugin), bg],
|
|
83
|
+
[` ${plugin.trustTier.toUpperCase().padEnd(10)}`, trustColor(plugin.trustTier), bg],
|
|
84
|
+
[` ${plugin.version}`, C.dim, bg],
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected override getEmptyStateMessage(): string {
|
|
89
|
+
return ' No plugins discovered.';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
93
|
+
return [
|
|
94
|
+
{ command: '/plugin list', summary: 'inspect plugin discovery paths and current registry state' },
|
|
95
|
+
{ command: '/marketplace', summary: 'review curated ecosystem entries and provenance posture' },
|
|
96
|
+
];
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
public render(width: number, height: number): Line[] {
|
|
88
|
-
this.needsRender = false;
|
|
89
100
|
const intro = 'Plugin trust, capabilities, signatures, and quarantine posture for the active ecosystem surface.';
|
|
90
|
-
const plugins = this.
|
|
101
|
+
const plugins = this.getItems();
|
|
91
102
|
|
|
92
103
|
if (plugins.length === 0) {
|
|
93
104
|
const workspace = buildPanelWorkspace(width, height, {
|
|
@@ -111,7 +122,7 @@ export class PluginsPanel extends BasePanel {
|
|
|
111
122
|
return workspace;
|
|
112
123
|
}
|
|
113
124
|
|
|
114
|
-
this.
|
|
125
|
+
this.clampSelection();
|
|
115
126
|
const selected = plugins[this.selectedIndex]!;
|
|
116
127
|
const selectedCaps = this.manager.capabilities(selected.name);
|
|
117
128
|
const trustRecord = this.manager.getTrustRecord(selected.name);
|
|
@@ -157,45 +168,11 @@ export class PluginsPanel extends BasePanel {
|
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
detailLines.push(buildPanelLine(width, [[' Inspect trust and capability state here, then use /plugin to take action.', C.dim]]));
|
|
160
|
-
|
|
161
|
-
const resolvedPluginsSection = resolvePrimaryScrollableSection(width, height, {
|
|
162
|
-
intro,
|
|
163
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through discovered plugins', C.dim]])],
|
|
164
|
-
palette: C,
|
|
165
|
-
section: {
|
|
166
|
-
title: 'Plugins',
|
|
167
|
-
scrollableLines: plugins.map((plugin, absolute) => {
|
|
168
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
169
|
-
return buildPanelLine(width, [
|
|
170
|
-
[' ', C.label, bg],
|
|
171
|
-
[plugin.name.padEnd(22), C.value, bg],
|
|
172
|
-
[` ${statusLabel(plugin).padEnd(11)}`, statusColor(plugin), bg],
|
|
173
|
-
[` ${plugin.trustTier.toUpperCase().padEnd(10)}`, trustColor(plugin.trustTier), bg],
|
|
174
|
-
[` ${plugin.version}`, C.dim, bg],
|
|
175
|
-
]);
|
|
176
|
-
}),
|
|
177
|
-
selectedIndex: this.selectedIndex,
|
|
178
|
-
scrollOffset: this.scrollOffset,
|
|
179
|
-
guardRows: 1,
|
|
180
|
-
minRows: 4,
|
|
181
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
182
|
-
},
|
|
183
|
-
afterSections: [detailSection],
|
|
184
|
-
});
|
|
185
|
-
this.scrollOffset = resolvedPluginsSection.scrollOffset;
|
|
171
|
+
detailLines.push(buildPanelLine(width, [[' Up/Down move through discovered plugins', C.dim]]));
|
|
186
172
|
|
|
187
|
-
|
|
188
|
-
resolvedPluginsSection.section,
|
|
189
|
-
detailSection,
|
|
190
|
-
];
|
|
191
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
173
|
+
return this.renderList(width, height, {
|
|
192
174
|
title: 'Plugin Control Room',
|
|
193
|
-
|
|
194
|
-
sections,
|
|
195
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through discovered plugins', C.dim]])],
|
|
196
|
-
palette: C,
|
|
175
|
+
footer: detailLines,
|
|
197
176
|
});
|
|
198
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
199
|
-
return lines.slice(0, height);
|
|
200
177
|
}
|
|
201
178
|
}
|
package/src/panels/polish.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
|
3
3
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
4
4
|
import { getSurfaceContentRows, getTrackedVisibleWindow, getVisibleWindow, type VisibleWindow } from '../renderer/surface-layout.ts';
|
|
5
5
|
import { GLYPHS, UI_TONES } from '../renderer/ui-primitives.ts';
|
|
6
|
+
import { type StatusState, STATE_GLYPHS } from '../renderer/status-glyphs.ts';
|
|
6
7
|
|
|
7
8
|
export interface PanelPalette {
|
|
8
9
|
readonly label: string;
|
|
@@ -42,13 +43,36 @@ export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = {
|
|
|
42
43
|
selectBg: UI_TONES.bg.selected,
|
|
43
44
|
} as const;
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Extend the base panel palette with domain-specific colors.
|
|
48
|
+
*
|
|
49
|
+
* Convention: raw hex colors may only live inside a palette constant declared
|
|
50
|
+
* at the top of a panel file, not inline in render calls.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
55
|
+
* decision: '#38bdf8',
|
|
56
|
+
* incident: '#ef4444',
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function extendPalette<T extends Record<string, string>>(
|
|
61
|
+
base: typeof DEFAULT_PANEL_PALETTE,
|
|
62
|
+
extras: T,
|
|
63
|
+
): typeof DEFAULT_PANEL_PALETTE & T {
|
|
64
|
+
return { ...base, ...extras };
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
export function buildPanelLine(
|
|
46
68
|
width: number,
|
|
47
|
-
segments: Array<[string, string, string?]>,
|
|
69
|
+
segments: Array<StyledPanelSegment | [string, string, string?]>,
|
|
48
70
|
): Line {
|
|
49
71
|
return buildStyledPanelLine(
|
|
50
72
|
width,
|
|
51
|
-
segments.map((
|
|
73
|
+
segments.map((seg) =>
|
|
74
|
+
Array.isArray(seg) ? { text: seg[0], fg: seg[1], bg: seg[2] } : seg,
|
|
75
|
+
),
|
|
52
76
|
);
|
|
53
77
|
}
|
|
54
78
|
|
|
@@ -256,6 +280,31 @@ export function buildSearchInputLine(
|
|
|
256
280
|
], { fillBg: bg });
|
|
257
281
|
}
|
|
258
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Build a status pill segment (glyph + label) for use in buildPanelLine.
|
|
285
|
+
*
|
|
286
|
+
* Returns StyledPanelSegment[] — spread directly into a buildPanelLine segments
|
|
287
|
+
* array:
|
|
288
|
+
* buildPanelLine(width, [[' count ', C.label], ...buildStatusPill('bad', '3')])
|
|
289
|
+
*
|
|
290
|
+
* Convention: raw hex colors may only live inside a palette constant declared at
|
|
291
|
+
* the top of a panel file; buildStatusPill derives its color from the palette.
|
|
292
|
+
*/
|
|
293
|
+
export function buildStatusPill(
|
|
294
|
+
state: StatusState,
|
|
295
|
+
label: string,
|
|
296
|
+
opts?: { glyph?: string; bg?: string; count?: number },
|
|
297
|
+
): StyledPanelSegment[] {
|
|
298
|
+
const glyph = opts?.glyph ?? STATE_GLYPHS[state];
|
|
299
|
+
const color = state === 'good' ? DEFAULT_PANEL_PALETTE.good
|
|
300
|
+
: state === 'warn' ? DEFAULT_PANEL_PALETTE.warn
|
|
301
|
+
: state === 'bad' ? DEFAULT_PANEL_PALETTE.bad
|
|
302
|
+
: DEFAULT_PANEL_PALETTE.info;
|
|
303
|
+
const bg = opts?.bg;
|
|
304
|
+
const text = opts?.count !== undefined ? `${glyph} ${label} (${opts.count})` : `${glyph} ${label}`;
|
|
305
|
+
return [{ text, fg: color, bg }];
|
|
306
|
+
}
|
|
307
|
+
|
|
259
308
|
export function buildStatPill(
|
|
260
309
|
label: string,
|
|
261
310
|
value: string,
|
|
@@ -36,6 +36,7 @@ export class ProviderAccountsPanel extends ScrollableListPanel<ProviderAccountRe
|
|
|
36
36
|
|
|
37
37
|
public constructor(deps: ProviderAccountsPanelDeps) {
|
|
38
38
|
super('accounts', 'Accounts', 'Q', 'monitoring');
|
|
39
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
39
40
|
this.providerAccounts = deps.providerAccounts;
|
|
40
41
|
void this.refresh();
|
|
41
42
|
}
|