@pellux/goodvibes-tui 0.19.98 → 0.20.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.
@@ -1,215 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
- import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
4
- import type { McpDecisionRecord } from '@/runtime/index.ts';
5
-
6
- type McpServerSecurityEntry = ReturnType<McpRegistry['listServerSecurity']>[number];
7
- import { truncateDisplay } from '../utils/terminal-width.ts';
8
- import {
9
- buildGuidanceLine,
10
- buildKeyValueLine,
11
- buildPanelLine,
12
- buildStatusPill,
13
- DEFAULT_PANEL_PALETTE,
14
- } from './polish.ts';
15
-
16
- const C = {
17
- ...DEFAULT_PANEL_PALETTE,
18
- header: '#94a3b8',
19
- headerBg: '#1e293b',
20
- ok: '#22c55e',
21
- warn: '#eab308',
22
- error: '#ef4444',
23
- selectBg: '#0f172a',
24
- } as const;
25
-
26
- function modeColor(mode: string): string {
27
- switch (mode) {
28
- case 'allow-all':
29
- return C.error;
30
- case 'ask-on-risk':
31
- return C.warn;
32
- case 'constrained':
33
- return C.ok;
34
- case 'blocked':
35
- return C.error;
36
- default:
37
- return C.dim;
38
- }
39
- }
40
-
41
- function freshnessColor(freshness: string): string {
42
- switch (freshness) {
43
- case 'fresh':
44
- return C.ok;
45
- case 'stale':
46
- case 'fetch_failed':
47
- return C.warn;
48
- case 'quarantined':
49
- return C.error;
50
- default:
51
- return C.dim;
52
- }
53
- }
54
-
55
- function decisionColor(decision: McpDecisionRecord): string {
56
- if (decision.verdict === 'deny') return C.error;
57
- if (decision.verdict === 'ask') return C.warn;
58
- return decision.incoherent ? C.warn : C.ok;
59
- }
60
-
61
- export class McpPanel extends ScrollableListPanel<McpServerSecurityEntry> {
62
- private readonly registry: McpRegistry;
63
-
64
- public constructor(registry: McpRegistry) {
65
- super('mcp', 'MCP', 'Z', 'monitoring');
66
- this.showSelectionGutter = true; // I5: non-color selection affordance
67
- this.registry = registry;
68
- }
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
-
95
- public handleInput(key: string): boolean {
96
- if (key === 'r') {
97
- this.markDirty();
98
- return true;
99
- }
100
- return super.handleInput(key);
101
- }
102
-
103
- public render(width: number, height: number): Line[] {
104
- this.clampSelection();
105
- const entries = this.registry.listServerSecurity();
106
- const intro = 'Trust, quarantine, scope, and recent security decisions for configured MCP servers.';
107
-
108
- const connected = entries.filter((e) => e.connected).length;
109
- const quarantined = entries.filter((e) => e.schemaFreshness === 'quarantined').length;
110
- const disconnected = entries.length - connected;
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, [
136
- [' Server: ', C.label],
137
- [selected.name, C.value],
138
- [' Trust: ', C.label],
139
- [selected.trustMode, modeColor(selected.trustMode)],
140
- [' Role: ', C.label],
141
- [selected.role, C.info],
142
- ]));
143
- detailLines.push(buildPanelLine(width, [
144
- [' Schema: ', C.label],
145
- [selected.schemaFreshness, freshnessColor(selected.schemaFreshness)],
146
- [' Approved by: ', C.label],
147
- [truncateDisplay(selected.quarantineApprovedBy ?? 'n/a', Math.max(0, width - 31)), selected.quarantineApprovedBy ? C.info : C.dim],
148
- ]));
149
- detailLines.push(buildPanelLine(width, [
150
- [' Scope: ', C.label],
151
- [truncateDisplay(
152
- `paths ${selected.allowedPaths.length > 0 ? selected.allowedPaths.join(', ') : 'unbounded'} hosts ${selected.allowedHosts.length > 0 ? selected.allowedHosts.join(', ') : 'unbounded'}`,
153
- Math.max(0, width - 10),
154
- ), (selected.allowedPaths.length > 0 || selected.allowedHosts.length > 0) ? C.value : C.dim],
155
- ]));
156
- detailLines.push(buildPanelLine(width, [
157
- [' Sandbox: ', C.label],
158
- [truncateDisplay(
159
- sandboxBinding?.sessionId
160
- ? `${sandboxBinding.profileId ?? 'mcp'} ${sandboxBinding.state ?? 'unknown'} ${sandboxBinding.backend ?? 'n/a'} ${sandboxBinding.startupStatus ?? 'n/a'} (${sandboxBinding.sessionId})`
161
- : 'not isolated',
162
- Math.max(0, width - 13),
163
- ), sandboxBinding?.sessionId ? C.info : C.dim],
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
- }
178
-
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
- }
191
-
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);
204
- }
205
-
206
- return this.renderList(width, height, {
207
- title: 'MCP Control Room',
208
- header: headerLines,
209
- footer: [
210
- ...detailLines,
211
- buildPanelLine(width, [[' Up/Down move r refresh', C.dim]]),
212
- ],
213
- });
214
- }
215
- }