@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,12 +1,11 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
3
|
import {
|
|
4
4
|
buildEmptyState,
|
|
5
5
|
buildGuidanceLine,
|
|
6
6
|
buildKeyValueLine,
|
|
7
7
|
buildPanelLine,
|
|
8
8
|
buildPanelWorkspace,
|
|
9
|
-
resolveScrollablePanelSection,
|
|
10
9
|
DEFAULT_PANEL_PALETTE,
|
|
11
10
|
type PanelWorkspaceSection,
|
|
12
11
|
} from './polish.ts';
|
|
@@ -36,10 +35,8 @@ function statusColor(installed: boolean): string {
|
|
|
36
35
|
return installed ? C.good : C.dim;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
export class MarketplacePanel extends
|
|
38
|
+
export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
40
39
|
private rows: MarketplaceRow[] = [];
|
|
41
|
-
private selectedIndex = 0;
|
|
42
|
-
private scrollOffset = 0;
|
|
43
40
|
private readonly unsub: (() => void) | null;
|
|
44
41
|
|
|
45
42
|
public constructor(
|
|
@@ -59,26 +56,45 @@ export class MarketplacePanel extends BasePanel {
|
|
|
59
56
|
this.refresh();
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// ScrollableListPanel implementation
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
protected getItems(): readonly MarketplaceRow[] {
|
|
64
|
+
return this.rows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected renderItem(row: MarketplaceRow, index: number, selected: boolean, width: number): Line {
|
|
68
|
+
const bg = selected ? C.selectBg : undefined;
|
|
69
|
+
const provenance = row.entry.provenance ?? 'local';
|
|
70
|
+
return buildPanelLine(width, [
|
|
71
|
+
[' ', C.label, bg],
|
|
72
|
+
[row.kind.padEnd(11), C.info, bg],
|
|
73
|
+
[row.entry.name.slice(0, 20).padEnd(20), C.value, bg],
|
|
74
|
+
[` ${provenance.slice(0, 16).padEnd(16)}`, provenance === 'local' ? C.dim : C.info, bg],
|
|
75
|
+
[` ${(row.installed ? 'INSTALLED' : 'CURATED').padEnd(9)} `, statusColor(row.installed), bg],
|
|
76
|
+
[` ${row.entry.version ?? 'n/a'}`, C.dim, bg],
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected override getPalette() { return C; }
|
|
81
|
+
protected override getEmptyStateMessage() {
|
|
82
|
+
return this.ecosystemPaths
|
|
83
|
+
? ' No curated marketplace entries found yet.'
|
|
84
|
+
: ' Marketplace catalog paths are not wired into this panel yet.';
|
|
85
|
+
}
|
|
86
|
+
protected override getEmptyStateActions() {
|
|
87
|
+
return [
|
|
88
|
+
{ command: '/marketplace bundle import <path>', summary: 'import a curated marketplace bundle' },
|
|
89
|
+
{ command: '/marketplace catalog review', summary: 'inspect the current local catalog posture' },
|
|
90
|
+
{ command: '/marketplace publish <kind> <path>', summary: 'publish local ecosystem entries back into the curated catalog' },
|
|
91
|
+
];
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
private refresh(): void {
|
|
78
95
|
if (!this.ecosystemPaths) {
|
|
79
96
|
this.rows = [];
|
|
80
|
-
this.
|
|
81
|
-
this.scrollOffset = 0;
|
|
97
|
+
this.clampSelection();
|
|
82
98
|
return;
|
|
83
99
|
}
|
|
84
100
|
try {
|
|
@@ -93,7 +109,7 @@ export class MarketplacePanel extends BasePanel {
|
|
|
93
109
|
...loadEcosystemCatalog('policy-pack', this.ecosystemPaths).map((entry) => ({ kind: 'policy-pack' as const, entry, installed: installedPolicyPacks.has(entry.id) })),
|
|
94
110
|
];
|
|
95
111
|
this.rows = rows.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
|
|
96
|
-
this.
|
|
112
|
+
this.clampSelection();
|
|
97
113
|
// I2: clear any previous catalog load error on successful refresh
|
|
98
114
|
this.clearError();
|
|
99
115
|
} catch (e) {
|
|
@@ -103,7 +119,7 @@ export class MarketplacePanel extends BasePanel {
|
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
public render(width: number, height: number): Line[] {
|
|
106
|
-
this.
|
|
122
|
+
this.clampSelection();
|
|
107
123
|
this.refresh();
|
|
108
124
|
|
|
109
125
|
const intro = 'Curated local-first ecosystem with provenance, compatibility, rollback history, and receipt-aware lifecycle review.';
|
|
@@ -159,22 +175,22 @@ export class MarketplacePanel extends BasePanel {
|
|
|
159
175
|
? startupIssues.slice(0, 4).map((issue) => buildPanelLine(width, [[' ', C.label], [issue.slice(0, Math.max(0, width - 2)), C.warn]]))
|
|
160
176
|
: [buildPanelLine(width, [[' No startup or lifecycle issues are currently pushing marketplace repair recommendations.', C.dim]])];
|
|
161
177
|
|
|
162
|
-
const
|
|
178
|
+
const selectedRow = this.rows[this.selectedIndex];
|
|
163
179
|
const selectedLines: Line[] = [];
|
|
164
|
-
if (
|
|
165
|
-
const review = reviewEcosystemCatalogEntry(
|
|
180
|
+
if (selectedRow) {
|
|
181
|
+
const review = reviewEcosystemCatalogEntry(selectedRow.entry, this.ecosystemPaths!);
|
|
166
182
|
selectedLines.push(buildPanelLine(width, [
|
|
167
183
|
[' Provenance: ', C.label],
|
|
168
|
-
[(
|
|
184
|
+
[(selectedRow.entry.provenance ?? '(none)').slice(0, Math.max(0, width - 15)), selectedRow.entry.provenance ? C.info : C.dim],
|
|
169
185
|
]));
|
|
170
186
|
selectedLines.push(buildPanelLine(width, [
|
|
171
187
|
[' Source: ', C.label],
|
|
172
|
-
[
|
|
188
|
+
[selectedRow.entry.source.slice(0, Math.max(0, width - 11)), C.value],
|
|
173
189
|
]));
|
|
174
190
|
selectedLines.push(buildKeyValueLine(width, [
|
|
175
191
|
{ label: 'Compatibility', value: review.compatibility.status, valueColor: review.compatibility.status === 'compatible' ? C.good : C.warn },
|
|
176
192
|
{ label: 'Risk', value: review.riskLevel, valueColor: review.riskLevel === 'low' ? C.good : C.warn },
|
|
177
|
-
{ label: 'State', value:
|
|
193
|
+
{ label: 'State', value: selectedRow.installed ? 'installed' : 'curated', valueColor: statusColor(selectedRow.installed) },
|
|
178
194
|
], C));
|
|
179
195
|
selectedLines.push(buildGuidanceLine(width, '/marketplace review <id>', 'inspect full compatibility and receipt detail for the selected entry', C));
|
|
180
196
|
}
|
|
@@ -182,49 +198,15 @@ export class MarketplacePanel extends BasePanel {
|
|
|
182
198
|
const postureSection: PanelWorkspaceSection = { title: 'Marketplace posture', lines: postureLines };
|
|
183
199
|
const startupIssuesSection: PanelWorkspaceSection = { title: 'Startup Issues', lines: startupIssueLines };
|
|
184
200
|
const recommendationsSection: PanelWorkspaceSection = { title: 'Recommendations', lines: recommendationLines };
|
|
185
|
-
const selectedSection: PanelWorkspaceSection = { title: 'Selected', lines: selectedLines };
|
|
186
|
-
const catalogSection = resolveScrollablePanelSection(width, height, {
|
|
187
|
-
intro,
|
|
188
|
-
palette: C,
|
|
189
|
-
beforeSections: [postureSection, startupIssuesSection, recommendationsSection],
|
|
190
|
-
section: {
|
|
191
|
-
title: 'Catalog',
|
|
192
|
-
scrollableLines: this.rows.map((row, globalIndex) => {
|
|
193
|
-
const bg = globalIndex === this.selectedIndex ? C.selectBg : undefined;
|
|
194
|
-
const provenance = row.entry.provenance ?? 'local';
|
|
195
|
-
return buildPanelLine(width, [
|
|
196
|
-
[' ', C.label, bg],
|
|
197
|
-
[row.kind.padEnd(11), C.info, bg],
|
|
198
|
-
[row.entry.name.slice(0, 20).padEnd(20), C.value, bg],
|
|
199
|
-
[` ${provenance.slice(0, 16).padEnd(16)}`, provenance === 'local' ? C.dim : C.info, bg],
|
|
200
|
-
[` ${(row.installed ? 'INSTALLED' : 'CURATED').padEnd(9)} `, statusColor(row.installed), bg],
|
|
201
|
-
[` ${row.entry.version ?? 'n/a'}`, C.dim, bg],
|
|
202
|
-
]);
|
|
203
|
-
}),
|
|
204
|
-
selectedIndex: this.selectedIndex,
|
|
205
|
-
scrollOffset: this.scrollOffset,
|
|
206
|
-
minRows: 4,
|
|
207
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
208
|
-
},
|
|
209
|
-
afterSections: selectedLines.length > 0 && height >= 20 ? [selectedSection] : [],
|
|
210
|
-
});
|
|
211
|
-
this.scrollOffset = catalogSection.scrollOffset;
|
|
212
|
-
|
|
213
|
-
const sections: PanelWorkspaceSection[] = [
|
|
214
|
-
postureSection,
|
|
215
|
-
startupIssuesSection,
|
|
216
|
-
recommendationsSection,
|
|
217
|
-
catalogSection.section,
|
|
218
|
-
];
|
|
219
|
-
if (selectedLines.length > 0 && height >= 20) {
|
|
220
|
-
sections.push(selectedSection);
|
|
221
|
-
}
|
|
222
201
|
|
|
223
|
-
return
|
|
202
|
+
return this.renderList(width, height, {
|
|
224
203
|
title: 'Marketplace Control Room',
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
204
|
+
header: [
|
|
205
|
+
...postureLines,
|
|
206
|
+
...startupIssueLines,
|
|
207
|
+
...recommendationLines,
|
|
208
|
+
],
|
|
209
|
+
footer: selectedLines.length > 0 && height >= 20 ? selectedLines : [],
|
|
228
210
|
});
|
|
229
211
|
}
|
|
230
212
|
}
|
package/src/panels/mcp-panel.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
3
|
-
import { BasePanel } from './base-panel.ts';
|
|
2
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
3
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp/registry';
|
|
5
4
|
import type { McpDecisionRecord } from '@pellux/goodvibes-sdk/platform/runtime/mcp/types';
|
|
5
|
+
|
|
6
|
+
type McpServerSecurityEntry = ReturnType<McpRegistry['listServerSecurity']>[number];
|
|
6
7
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
8
|
import {
|
|
8
|
-
buildEmptyState,
|
|
9
9
|
buildGuidanceLine,
|
|
10
10
|
buildKeyValueLine,
|
|
11
11
|
buildPanelLine,
|
|
12
|
-
|
|
12
|
+
buildStatusPill,
|
|
13
13
|
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
resolvePrimaryScrollableSection,
|
|
15
|
-
type PanelWorkspaceSection,
|
|
16
14
|
} from './polish.ts';
|
|
17
15
|
|
|
18
16
|
const C = {
|
|
@@ -60,93 +58,102 @@ function decisionColor(decision: McpDecisionRecord): string {
|
|
|
60
58
|
return decision.incoherent ? C.warn : C.ok;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
export class McpPanel extends
|
|
61
|
+
export class McpPanel extends ScrollableListPanel<McpServerSecurityEntry> {
|
|
64
62
|
private readonly registry: McpRegistry;
|
|
65
|
-
private selectedIndex = 0;
|
|
66
|
-
private scrollOffset = 0;
|
|
67
63
|
|
|
68
64
|
public constructor(registry: McpRegistry) {
|
|
69
65
|
super('mcp', 'MCP', 'Z', 'monitoring');
|
|
66
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
70
67
|
this.registry = registry;
|
|
71
68
|
}
|
|
72
69
|
|
|
70
|
+
protected override getPalette() { return C; }
|
|
71
|
+
protected override getEmptyStateMessage() { return ' No MCP servers configured or connected.'; }
|
|
72
|
+
protected override getEmptyStateActions() {
|
|
73
|
+
return [
|
|
74
|
+
{ command: '/mcp', summary: 'list server state and security posture' },
|
|
75
|
+
{ command: '/settings', summary: 'open the MCP settings category for trust and scope controls' },
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected getItems(): readonly McpServerSecurityEntry[] {
|
|
80
|
+
return this.registry.listServerSecurity();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected renderItem(entry: McpServerSecurityEntry, index: number, selected: boolean, width: number): Line {
|
|
84
|
+
const bg = selected ? C.selectBg : undefined;
|
|
85
|
+
return buildPanelLine(width, [
|
|
86
|
+
[' ', C.label, bg],
|
|
87
|
+
[entry.name.padEnd(20), C.value, bg],
|
|
88
|
+
...buildStatusPill(entry.connected ? 'good' : 'bad', ` ${(entry.connected ? 'CONNECTED' : 'DISCONNECTED').padEnd(13)}`, { bg }),
|
|
89
|
+
[` ${entry.trustMode.padEnd(12)}`, modeColor(entry.trustMode), bg],
|
|
90
|
+
[` ${entry.role.padEnd(10)}`, C.info, bg],
|
|
91
|
+
[` ${entry.schemaFreshness}`, freshnessColor(entry.schemaFreshness), bg],
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
public handleInput(key: string): boolean {
|
|
74
|
-
const entries = this.registry.listServerSecurity();
|
|
75
96
|
if (key === 'r') {
|
|
76
97
|
this.markDirty();
|
|
77
98
|
return true;
|
|
78
99
|
}
|
|
79
|
-
|
|
80
|
-
if (key === 'up' || key === 'k') {
|
|
81
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
82
|
-
this.markDirty();
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
if (key === 'down' || key === 'j') {
|
|
86
|
-
this.selectedIndex = Math.min(entries.length - 1, this.selectedIndex + 1);
|
|
87
|
-
this.markDirty();
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
return false;
|
|
100
|
+
return super.handleInput(key);
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
public render(width: number, height: number): Line[] {
|
|
94
|
-
this.
|
|
95
|
-
const intro = 'Trust, quarantine, scope, and recent security decisions for configured MCP servers.';
|
|
104
|
+
this.clampSelection();
|
|
96
105
|
const entries = this.registry.listServerSecurity();
|
|
106
|
+
const intro = 'Trust, quarantine, scope, and recent security decisions for configured MCP servers.';
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
title: 'MCP Control Room',
|
|
101
|
-
intro,
|
|
102
|
-
sections: [{
|
|
103
|
-
lines: buildEmptyState(
|
|
104
|
-
width,
|
|
105
|
-
' No MCP servers configured or connected.',
|
|
106
|
-
'Add MCP servers, inspect trust posture, and review risk-scoped policies here once the registry is populated.',
|
|
107
|
-
[
|
|
108
|
-
{ command: '/mcp', summary: 'list server state and security posture' },
|
|
109
|
-
{ command: '/settings', summary: 'open the MCP settings category for trust and scope controls' },
|
|
110
|
-
],
|
|
111
|
-
C,
|
|
112
|
-
),
|
|
113
|
-
}],
|
|
114
|
-
palette: C,
|
|
115
|
-
});
|
|
116
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
117
|
-
return workspace;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
this.selectedIndex = Math.min(this.selectedIndex, entries.length - 1);
|
|
121
|
-
const selected = entries[this.selectedIndex]!;
|
|
122
|
-
const sandboxBinding = this.registry.listServerSandboxBindings().find((entry) => entry.name === selected.name);
|
|
123
|
-
const connected = entries.filter((entry) => entry.connected).length;
|
|
124
|
-
const quarantined = entries.filter((entry) => entry.schemaFreshness === 'quarantined').length;
|
|
108
|
+
const connected = entries.filter((e) => e.connected).length;
|
|
109
|
+
const quarantined = entries.filter((e) => e.schemaFreshness === 'quarantined').length;
|
|
125
110
|
const disconnected = entries.length - connected;
|
|
126
|
-
const staleSchemas = entries.filter((
|
|
127
|
-
|
|
128
|
-
|
|
111
|
+
const staleSchemas = entries.filter((e) => e.schemaFreshness !== 'fresh').length;
|
|
112
|
+
|
|
113
|
+
const headerLines: Line[] = [
|
|
114
|
+
buildPanelLine(width, [[' MCP posture', C.label]]),
|
|
115
|
+
buildKeyValueLine(width, [
|
|
116
|
+
{ label: 'servers', value: String(entries.length), valueColor: C.value },
|
|
117
|
+
{ label: 'connected', value: String(connected), valueColor: connected > 0 ? C.ok : C.dim },
|
|
118
|
+
{ label: 'disconnected', value: String(disconnected), valueColor: disconnected > 0 ? C.warn : C.dim },
|
|
119
|
+
{ label: 'stale schema', value: String(staleSchemas), valueColor: staleSchemas > 0 ? C.warn : C.dim },
|
|
120
|
+
{ label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.error : C.dim },
|
|
121
|
+
], C),
|
|
122
|
+
buildGuidanceLine(width, '/mcp review', 'inspect trust, freshness, and quarantine posture for configured servers', C),
|
|
123
|
+
buildGuidanceLine(width, '/mcp repair', 'review reconnect, auth, import, and startup remediation guidance', C),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const selected = entries[this.selectedIndex];
|
|
127
|
+
const detailLines: Line[] = [];
|
|
128
|
+
const repairLines: Line[] = [];
|
|
129
|
+
|
|
130
|
+
if (selected) {
|
|
131
|
+
const sandboxBinding = this.registry.listServerSandboxBindings().find((e) => e.name === selected.name);
|
|
132
|
+
const decisions = this.registry.listRecentSecurityDecisions?.(24) ?? [];
|
|
133
|
+
const selectedDecision = decisions.find((d) => d.serverName === selected.name);
|
|
134
|
+
|
|
135
|
+
detailLines.push(buildPanelLine(width, [
|
|
129
136
|
[' Server: ', C.label],
|
|
130
137
|
[selected.name, C.value],
|
|
131
138
|
[' Trust: ', C.label],
|
|
132
139
|
[selected.trustMode, modeColor(selected.trustMode)],
|
|
133
140
|
[' Role: ', C.label],
|
|
134
141
|
[selected.role, C.info],
|
|
135
|
-
])
|
|
136
|
-
buildPanelLine(width, [
|
|
142
|
+
]));
|
|
143
|
+
detailLines.push(buildPanelLine(width, [
|
|
137
144
|
[' Schema: ', C.label],
|
|
138
145
|
[selected.schemaFreshness, freshnessColor(selected.schemaFreshness)],
|
|
139
146
|
[' Approved by: ', C.label],
|
|
140
147
|
[truncateDisplay(selected.quarantineApprovedBy ?? 'n/a', Math.max(0, width - 31)), selected.quarantineApprovedBy ? C.info : C.dim],
|
|
141
|
-
])
|
|
142
|
-
buildPanelLine(width, [
|
|
148
|
+
]));
|
|
149
|
+
detailLines.push(buildPanelLine(width, [
|
|
143
150
|
[' Scope: ', C.label],
|
|
144
151
|
[truncateDisplay(
|
|
145
152
|
`paths ${selected.allowedPaths.length > 0 ? selected.allowedPaths.join(', ') : 'unbounded'} hosts ${selected.allowedHosts.length > 0 ? selected.allowedHosts.join(', ') : 'unbounded'}`,
|
|
146
153
|
Math.max(0, width - 10),
|
|
147
154
|
), (selected.allowedPaths.length > 0 || selected.allowedHosts.length > 0) ? C.value : C.dim],
|
|
148
|
-
])
|
|
149
|
-
buildPanelLine(width, [
|
|
155
|
+
]));
|
|
156
|
+
detailLines.push(buildPanelLine(width, [
|
|
150
157
|
[' Sandbox: ', C.label],
|
|
151
158
|
[truncateDisplay(
|
|
152
159
|
sandboxBinding?.sessionId
|
|
@@ -154,107 +161,55 @@ export class McpPanel extends BasePanel {
|
|
|
154
161
|
: 'not isolated',
|
|
155
162
|
Math.max(0, width - 13),
|
|
156
163
|
), sandboxBinding?.sessionId ? C.info : C.dim],
|
|
157
|
-
]),
|
|
158
|
-
];
|
|
159
|
-
if (selected.schemaFreshness === 'quarantined') {
|
|
160
|
-
detailLines.push(buildPanelLine(width, [
|
|
161
|
-
[' Quarantine: ', C.label],
|
|
162
|
-
[truncateDisplay(`${selected.quarantineReason ?? 'unknown'}${selected.quarantineDetail ? ` - ${selected.quarantineDetail}` : ''}`, Math.max(0, width - 15)), C.error],
|
|
163
164
|
]));
|
|
164
|
-
|
|
165
|
+
if (selected.schemaFreshness === 'quarantined') {
|
|
166
|
+
detailLines.push(buildPanelLine(width, [
|
|
167
|
+
[' Quarantine: ', C.label],
|
|
168
|
+
[truncateDisplay(`${selected.quarantineReason ?? 'unknown'}${selected.quarantineDetail ? ` - ${selected.quarantineDetail}` : ''}`, Math.max(0, width - 15)), C.error],
|
|
169
|
+
]));
|
|
170
|
+
}
|
|
171
|
+
if (selectedDecision) {
|
|
172
|
+
const summary = `${selectedDecision.serverName}:${selectedDecision.toolName} ${selectedDecision.verdict.toUpperCase()} ${selectedDecision.capability}${selectedDecision.incoherent ? ' incoherent' : ''}`;
|
|
173
|
+
detailLines.push(buildPanelLine(width, [
|
|
174
|
+
[' Recent: ', C.label],
|
|
175
|
+
[truncateDisplay(summary, Math.max(0, width - 10)), decisionColor(selectedDecision)],
|
|
176
|
+
]));
|
|
177
|
+
}
|
|
165
178
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const summary = `${decision.serverName}:${decision.toolName} ${decision.verdict.toUpperCase()} ${decision.capability}${decision.incoherent ? ' incoherent' : ''}`;
|
|
179
|
-
return buildPanelLine(width, [
|
|
180
|
-
[' ', C.label],
|
|
181
|
-
[truncateDisplay(summary, Math.max(0, width - 2)), decisionColor(decision)],
|
|
182
|
-
]);
|
|
183
|
-
});
|
|
179
|
+
if (!selected.connected) {
|
|
180
|
+
repairLines.push(buildPanelLine(width, [[' /mcp repair', C.warn], [' review reconnect and startup posture for this server', C.dim]]));
|
|
181
|
+
}
|
|
182
|
+
if (selected.schemaFreshness !== 'fresh') {
|
|
183
|
+
repairLines.push(buildPanelLine(width, [[' /mcp review', C.warn], [' inspect schema freshness, quarantine, and trust posture', C.dim]]));
|
|
184
|
+
}
|
|
185
|
+
if (sandboxBinding?.sessionId) {
|
|
186
|
+
repairLines.push(buildPanelLine(width, [[' /sandbox review', C.info], [' verify the bound MCP isolation session and startup status', C.dim]]));
|
|
187
|
+
}
|
|
188
|
+
if (repairLines.length === 0) {
|
|
189
|
+
repairLines.push(buildPanelLine(width, [[' No immediate MCP repair actions suggested for the selected server.', C.dim]]));
|
|
190
|
+
}
|
|
184
191
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
const allDecisions = this.registry.listRecentSecurityDecisions?.(24) ?? [];
|
|
193
|
+
const decisionLines: Line[] = allDecisions.length === 0
|
|
194
|
+
? [buildPanelLine(width, [[' No MCP decisions recorded yet.', C.dim]])]
|
|
195
|
+
: allDecisions.map((decision) => {
|
|
196
|
+
const summary = `${decision.serverName}:${decision.toolName} ${decision.verdict.toUpperCase()} ${decision.capability}${decision.incoherent ? ' incoherent' : ''}`;
|
|
197
|
+
return buildPanelLine(width, [
|
|
198
|
+
[' ', C.label],
|
|
199
|
+
[truncateDisplay(summary, Math.max(0, width - 2)), decisionColor(decision)],
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
detailLines.push(...repairLines);
|
|
203
|
+
detailLines.push(...decisionLines);
|
|
194
204
|
}
|
|
195
|
-
if (repairLines.length === 0) {
|
|
196
|
-
repairLines.push(buildPanelLine(width, [[' No immediate MCP repair actions suggested for the selected server.', C.dim]]));
|
|
197
|
-
}
|
|
198
|
-
const postureSection: PanelWorkspaceSection = {
|
|
199
|
-
title: 'MCP posture',
|
|
200
|
-
lines: [
|
|
201
|
-
buildKeyValueLine(width, [
|
|
202
|
-
{ label: 'servers', value: String(entries.length), valueColor: C.value },
|
|
203
|
-
{ label: 'connected', value: String(connected), valueColor: connected > 0 ? C.ok : C.dim },
|
|
204
|
-
{ label: 'disconnected', value: String(disconnected), valueColor: disconnected > 0 ? C.warn : C.dim },
|
|
205
|
-
{ label: 'stale schema', value: String(staleSchemas), valueColor: staleSchemas > 0 ? C.warn : C.dim },
|
|
206
|
-
{ label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.error : C.dim },
|
|
207
|
-
], C),
|
|
208
|
-
buildGuidanceLine(width, '/mcp review', 'inspect trust, freshness, and quarantine posture for configured servers', C),
|
|
209
|
-
buildGuidanceLine(width, '/mcp repair', 'review reconnect, auth, import, and startup remediation guidance', C),
|
|
210
|
-
],
|
|
211
|
-
};
|
|
212
|
-
const selectedSection: PanelWorkspaceSection = { title: 'Selected Server', lines: detailLines };
|
|
213
|
-
const repairSection: PanelWorkspaceSection = { title: 'Repair', lines: repairLines };
|
|
214
|
-
const decisionsSection: PanelWorkspaceSection = { title: 'Recent Decisions', lines: decisionLines };
|
|
215
|
-
const resolvedServersSection = resolvePrimaryScrollableSection(width, height, {
|
|
216
|
-
intro,
|
|
217
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move r refresh', C.dim]])],
|
|
218
|
-
palette: C,
|
|
219
|
-
beforeSections: [postureSection],
|
|
220
|
-
section: {
|
|
221
|
-
title: 'Servers',
|
|
222
|
-
scrollableLines: entries.map((entry, absolute) => {
|
|
223
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
224
|
-
return buildPanelLine(width, [
|
|
225
|
-
[' ', C.label, bg],
|
|
226
|
-
[entry.name.padEnd(20), C.value, bg],
|
|
227
|
-
[` ${(entry.connected ? 'CONNECTED' : 'DISCONNECTED').padEnd(13)}`, entry.connected ? C.ok : C.error, bg],
|
|
228
|
-
[` ${entry.trustMode.padEnd(12)}`, modeColor(entry.trustMode), bg],
|
|
229
|
-
[` ${entry.role.padEnd(10)}`, C.info, bg],
|
|
230
|
-
[` ${entry.schemaFreshness}`, freshnessColor(entry.schemaFreshness), bg],
|
|
231
|
-
]);
|
|
232
|
-
}),
|
|
233
|
-
selectedIndex: this.selectedIndex,
|
|
234
|
-
scrollOffset: this.scrollOffset,
|
|
235
|
-
guardRows: 1,
|
|
236
|
-
minRows: 4,
|
|
237
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
238
|
-
},
|
|
239
|
-
afterSections: [selectedSection, repairSection, decisionsSection],
|
|
240
|
-
});
|
|
241
|
-
this.scrollOffset = resolvedServersSection.scrollOffset;
|
|
242
205
|
|
|
243
|
-
|
|
244
|
-
postureSection,
|
|
245
|
-
resolvedServersSection.section,
|
|
246
|
-
selectedSection,
|
|
247
|
-
repairSection,
|
|
248
|
-
decisionsSection,
|
|
249
|
-
];
|
|
250
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
206
|
+
return this.renderList(width, height, {
|
|
251
207
|
title: 'MCP Control Room',
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
208
|
+
header: headerLines,
|
|
209
|
+
footer: [
|
|
210
|
+
...detailLines,
|
|
211
|
+
buildPanelLine(width, [[' Up/Down move r refresh', C.dim]]),
|
|
212
|
+
],
|
|
256
213
|
});
|
|
257
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
258
|
-
return lines.slice(0, height);
|
|
259
214
|
}
|
|
260
215
|
}
|