@pellux/goodvibes-tui 0.18.20 → 0.18.23
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 +120 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- 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 +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -82
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +45 -14
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/subscription-panel.ts +63 -86
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/version.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
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 {
|
|
5
5
|
buildDetailBlock,
|
|
6
6
|
buildGuidanceLine,
|
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
buildSummaryBlock,
|
|
10
10
|
buildPanelWorkspace,
|
|
11
11
|
DEFAULT_PANEL_PALETTE,
|
|
12
|
-
|
|
13
|
-
type PanelWorkspaceSection,
|
|
12
|
+
type PanelPalette,
|
|
14
13
|
} from './polish.ts';
|
|
15
14
|
import type { LocalAuthSnapshot } from '@pellux/goodvibes-sdk/platform/security/user-auth';
|
|
16
15
|
import type { LocalAuthInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
@@ -27,9 +26,9 @@ function formatRoles(roles: readonly string[]): string {
|
|
|
27
26
|
return roles.length > 0 ? roles.join(', ') : '(none)';
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
type LocalAuthUser = LocalAuthSnapshot['users'][number];
|
|
30
|
+
|
|
31
|
+
export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
33
32
|
private readonly authManager: LocalAuthInspectionQuery;
|
|
34
33
|
|
|
35
34
|
public constructor(authManager: LocalAuthInspectionQuery) {
|
|
@@ -37,110 +36,94 @@ export class LocalAuthPanel extends BasePanel {
|
|
|
37
36
|
this.authManager = authManager;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
39
|
+
protected override getPalette(): PanelPalette {
|
|
40
|
+
return C;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected getItems(): readonly LocalAuthUser[] {
|
|
44
|
+
return this.authManager.inspect().users;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
|
|
48
|
+
return buildPanelListRow(width, [
|
|
49
|
+
{ text: user.username.padEnd(20), fg: C.value },
|
|
50
|
+
{ text: ` roles=${formatRoles(user.roles)}`.slice(0, Math.max(0, width - 24)), fg: C.info },
|
|
51
|
+
], C, { selected });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected override getEmptyStateMessage(): string {
|
|
55
|
+
return ' No local auth users configured.';
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
public render(width: number, height: number): Line[] {
|
|
57
|
-
this.needsRender = false;
|
|
58
59
|
const intro = 'Manage local daemon and HTTP-listener auth users, bootstrap state, and active sessions.';
|
|
59
|
-
const footerLines = [buildPanelLine(width, [[' /auth local review /auth local add-user /auth local rotate-password /auth local revoke-session ', C.dim]])];
|
|
60
60
|
const snapshot = this.authManager.inspect();
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const users = this.getItems();
|
|
62
|
+
|
|
63
63
|
const issueMessages: string[] = [];
|
|
64
64
|
if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists and should be cleared after password rotation.');
|
|
65
65
|
if (snapshot.userCount <= 1) issueMessages.push('Only one local auth user is configured.');
|
|
66
66
|
if (snapshot.sessionCount === 0) issueMessages.push('No active local auth sessions are currently tracked.');
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
67
|
+
|
|
68
|
+
const headerLines: Line[] = [
|
|
69
|
+
...buildSummaryBlock(width, 'Local auth posture', [
|
|
70
|
+
buildPanelLine(width, [
|
|
71
|
+
[' users ', C.label],
|
|
72
|
+
[String(snapshot.userCount), C.value],
|
|
73
|
+
[' sessions ', C.label],
|
|
74
|
+
[String(snapshot.sessionCount), snapshot.sessionCount > 0 ? C.info : C.dim],
|
|
75
|
+
[' bootstrap ', C.label],
|
|
76
|
+
[snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
|
|
77
|
+
]),
|
|
78
|
+
buildPanelLine(width, [[' user store ', C.label], [snapshot.userStorePath.slice(0, Math.max(0, width - 13)), C.dim]]),
|
|
79
|
+
buildPanelLine(width, [[' bootstrap file ', C.label], [snapshot.bootstrapCredentialPath.slice(0, Math.max(0, width - 18)), C.dim]]),
|
|
80
|
+
...(issueMessages.length > 0
|
|
81
|
+
? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}`.slice(0, Math.max(0, width)), C.warn]]))
|
|
82
|
+
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
83
|
+
buildGuidanceLine(width, '/auth local rotate-password <user> <password>', 'rotate bootstrap/default credentials and revoke older sessions as needed', C),
|
|
84
|
+
], C),
|
|
86
85
|
];
|
|
87
86
|
|
|
88
|
-
if (
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
lines: buildDetailBlock(width, 'Selected user', [
|
|
92
|
-
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
|
|
93
|
-
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username} <password>`.slice(0, Math.max(0, width)), C.dim]]),
|
|
94
|
-
buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
|
|
95
|
-
], C),
|
|
96
|
-
}
|
|
97
|
-
: null;
|
|
98
|
-
const activeSessionsSection: PanelWorkspaceSection | null = snapshot.sessions.length > 0
|
|
99
|
-
? {
|
|
100
|
-
title: 'Active Sessions',
|
|
101
|
-
lines: snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
102
|
-
[' ', C.label],
|
|
103
|
-
[session.username.padEnd(18), C.value],
|
|
104
|
-
[` expires ${new Date(session.expiresAt).toLocaleString()}`.slice(0, Math.max(0, width - 20)), C.dim],
|
|
105
|
-
])),
|
|
106
|
-
}
|
|
107
|
-
: null;
|
|
108
|
-
const rawUserLines: Line[] = snapshot.users.map((user, absolute) => {
|
|
109
|
-
return buildPanelListRow(width, [
|
|
110
|
-
{ text: user.username.padEnd(20), fg: C.value },
|
|
111
|
-
{ text: ` roles=${formatRoles(user.roles)}`.slice(0, Math.max(0, width - 24)), fg: C.info },
|
|
112
|
-
], C, { selected: absolute === this.selectedIndex });
|
|
113
|
-
});
|
|
114
|
-
const resolvedUsersSection = resolvePrimaryScrollableSection(width, height, {
|
|
87
|
+
if (users.length === 0) {
|
|
88
|
+
const workspace = buildPanelWorkspace(width, height, {
|
|
89
|
+
title: 'Local Auth Control Room',
|
|
115
90
|
intro,
|
|
116
|
-
|
|
91
|
+
sections: [{ lines: headerLines }],
|
|
117
92
|
palette: C,
|
|
118
|
-
beforeSections: sections,
|
|
119
|
-
section: {
|
|
120
|
-
title: 'Users',
|
|
121
|
-
scrollableLines: rawUserLines,
|
|
122
|
-
selectedIndex: this.selectedIndex,
|
|
123
|
-
scrollOffset: this.scrollOffset,
|
|
124
|
-
guardRows: 1,
|
|
125
|
-
minRows: 4,
|
|
126
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
127
|
-
},
|
|
128
|
-
afterSections: [selectedUserSection, activeSessionsSection].filter(Boolean) as PanelWorkspaceSection[],
|
|
129
93
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
94
|
+
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
95
|
+
return workspace;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.clampSelection();
|
|
99
|
+
const selected = users[this.selectedIndex];
|
|
100
|
+
|
|
101
|
+
const footerLines: Line[] = [];
|
|
102
|
+
if (selected) {
|
|
103
|
+
footerLines.push(
|
|
104
|
+
...buildDetailBlock(width, 'Selected user', [
|
|
105
|
+
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
|
|
106
|
+
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username} <password>`.slice(0, Math.max(0, width)), C.dim]]),
|
|
107
|
+
buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
|
|
108
|
+
], C),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (snapshot.sessions.length > 0) {
|
|
113
|
+
footerLines.push(
|
|
114
|
+
...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
115
|
+
[' ', C.label],
|
|
116
|
+
[session.username.padEnd(18), C.value],
|
|
117
|
+
[` expires ${new Date(session.expiresAt).toLocaleString()}`.slice(0, Math.max(0, width - 20)), C.dim],
|
|
118
|
+
])),
|
|
119
|
+
);
|
|
134
120
|
}
|
|
121
|
+
footerLines.push(buildPanelLine(width, [[' /auth local review /auth local add-user /auth local rotate-password /auth local revoke-session ', C.dim]]));
|
|
135
122
|
|
|
136
|
-
|
|
123
|
+
return this.renderList(width, height, {
|
|
137
124
|
title: 'Local Auth Control Room',
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
footerLines,
|
|
141
|
-
palette: C,
|
|
125
|
+
header: headerLines,
|
|
126
|
+
footer: footerLines,
|
|
142
127
|
});
|
|
143
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
144
|
-
return lines.slice(0, height);
|
|
145
128
|
}
|
|
146
129
|
}
|
package/src/panels/mcp-panel.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
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
|
-
buildPanelWorkspace,
|
|
13
12
|
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
resolvePrimaryScrollableSection,
|
|
15
|
-
type PanelWorkspaceSection,
|
|
16
13
|
} from './polish.ts';
|
|
17
14
|
|
|
18
15
|
const C = {
|
|
@@ -60,93 +57,101 @@ function decisionColor(decision: McpDecisionRecord): string {
|
|
|
60
57
|
return decision.incoherent ? C.warn : C.ok;
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
export class McpPanel extends
|
|
60
|
+
export class McpPanel extends ScrollableListPanel<McpServerSecurityEntry> {
|
|
64
61
|
private readonly registry: McpRegistry;
|
|
65
|
-
private selectedIndex = 0;
|
|
66
|
-
private scrollOffset = 0;
|
|
67
62
|
|
|
68
63
|
public constructor(registry: McpRegistry) {
|
|
69
64
|
super('mcp', 'MCP', 'Z', 'monitoring');
|
|
70
65
|
this.registry = registry;
|
|
71
66
|
}
|
|
72
67
|
|
|
68
|
+
protected override getPalette() { return C; }
|
|
69
|
+
protected override getEmptyStateMessage() { return ' No MCP servers configured or connected.'; }
|
|
70
|
+
protected override getEmptyStateActions() {
|
|
71
|
+
return [
|
|
72
|
+
{ command: '/mcp', summary: 'list server state and security posture' },
|
|
73
|
+
{ command: '/settings', summary: 'open the MCP settings category for trust and scope controls' },
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected getItems(): readonly McpServerSecurityEntry[] {
|
|
78
|
+
return this.registry.listServerSecurity();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected renderItem(entry: McpServerSecurityEntry, index: number, selected: boolean, width: number): Line {
|
|
82
|
+
const bg = selected ? C.selectBg : undefined;
|
|
83
|
+
return buildPanelLine(width, [
|
|
84
|
+
[' ', C.label, bg],
|
|
85
|
+
[entry.name.padEnd(20), C.value, bg],
|
|
86
|
+
[` ${(entry.connected ? 'CONNECTED' : 'DISCONNECTED').padEnd(13)}`, entry.connected ? C.ok : C.error, bg],
|
|
87
|
+
[` ${entry.trustMode.padEnd(12)}`, modeColor(entry.trustMode), bg],
|
|
88
|
+
[` ${entry.role.padEnd(10)}`, C.info, bg],
|
|
89
|
+
[` ${entry.schemaFreshness}`, freshnessColor(entry.schemaFreshness), bg],
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
public handleInput(key: string): boolean {
|
|
74
|
-
const entries = this.registry.listServerSecurity();
|
|
75
94
|
if (key === 'r') {
|
|
76
95
|
this.markDirty();
|
|
77
96
|
return true;
|
|
78
97
|
}
|
|
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;
|
|
98
|
+
return super.handleInput(key);
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
public render(width: number, height: number): Line[] {
|
|
94
|
-
this.
|
|
95
|
-
const intro = 'Trust, quarantine, scope, and recent security decisions for configured MCP servers.';
|
|
102
|
+
this.clampSelection();
|
|
96
103
|
const entries = this.registry.listServerSecurity();
|
|
104
|
+
const intro = 'Trust, quarantine, scope, and recent security decisions for configured MCP servers.';
|
|
97
105
|
|
|
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;
|
|
106
|
+
const connected = entries.filter((e) => e.connected).length;
|
|
107
|
+
const quarantined = entries.filter((e) => e.schemaFreshness === 'quarantined').length;
|
|
125
108
|
const disconnected = entries.length - connected;
|
|
126
|
-
const staleSchemas = entries.filter((
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
const staleSchemas = entries.filter((e) => e.schemaFreshness !== 'fresh').length;
|
|
110
|
+
|
|
111
|
+
const headerLines: Line[] = [
|
|
112
|
+
buildPanelLine(width, [[' MCP posture', C.label]]),
|
|
113
|
+
buildKeyValueLine(width, [
|
|
114
|
+
{ label: 'servers', value: String(entries.length), valueColor: C.value },
|
|
115
|
+
{ label: 'connected', value: String(connected), valueColor: connected > 0 ? C.ok : C.dim },
|
|
116
|
+
{ label: 'disconnected', value: String(disconnected), valueColor: disconnected > 0 ? C.warn : C.dim },
|
|
117
|
+
{ label: 'stale schema', value: String(staleSchemas), valueColor: staleSchemas > 0 ? C.warn : C.dim },
|
|
118
|
+
{ label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.error : C.dim },
|
|
119
|
+
], C),
|
|
120
|
+
buildGuidanceLine(width, '/mcp review', 'inspect trust, freshness, and quarantine posture for configured servers', C),
|
|
121
|
+
buildGuidanceLine(width, '/mcp repair', 'review reconnect, auth, import, and startup remediation guidance', C),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const selected = entries[this.selectedIndex];
|
|
125
|
+
const detailLines: Line[] = [];
|
|
126
|
+
const repairLines: Line[] = [];
|
|
127
|
+
|
|
128
|
+
if (selected) {
|
|
129
|
+
const sandboxBinding = this.registry.listServerSandboxBindings().find((e) => e.name === selected.name);
|
|
130
|
+
const decisions = this.registry.listRecentSecurityDecisions?.(24) ?? [];
|
|
131
|
+
const selectedDecision = decisions.find((d) => d.serverName === selected.name);
|
|
132
|
+
|
|
133
|
+
detailLines.push(buildPanelLine(width, [
|
|
129
134
|
[' Server: ', C.label],
|
|
130
135
|
[selected.name, C.value],
|
|
131
136
|
[' Trust: ', C.label],
|
|
132
137
|
[selected.trustMode, modeColor(selected.trustMode)],
|
|
133
138
|
[' Role: ', C.label],
|
|
134
139
|
[selected.role, C.info],
|
|
135
|
-
])
|
|
136
|
-
buildPanelLine(width, [
|
|
140
|
+
]));
|
|
141
|
+
detailLines.push(buildPanelLine(width, [
|
|
137
142
|
[' Schema: ', C.label],
|
|
138
143
|
[selected.schemaFreshness, freshnessColor(selected.schemaFreshness)],
|
|
139
144
|
[' Approved by: ', C.label],
|
|
140
145
|
[truncateDisplay(selected.quarantineApprovedBy ?? 'n/a', Math.max(0, width - 31)), selected.quarantineApprovedBy ? C.info : C.dim],
|
|
141
|
-
])
|
|
142
|
-
buildPanelLine(width, [
|
|
146
|
+
]));
|
|
147
|
+
detailLines.push(buildPanelLine(width, [
|
|
143
148
|
[' Scope: ', C.label],
|
|
144
149
|
[truncateDisplay(
|
|
145
150
|
`paths ${selected.allowedPaths.length > 0 ? selected.allowedPaths.join(', ') : 'unbounded'} hosts ${selected.allowedHosts.length > 0 ? selected.allowedHosts.join(', ') : 'unbounded'}`,
|
|
146
151
|
Math.max(0, width - 10),
|
|
147
152
|
), (selected.allowedPaths.length > 0 || selected.allowedHosts.length > 0) ? C.value : C.dim],
|
|
148
|
-
])
|
|
149
|
-
buildPanelLine(width, [
|
|
153
|
+
]));
|
|
154
|
+
detailLines.push(buildPanelLine(width, [
|
|
150
155
|
[' Sandbox: ', C.label],
|
|
151
156
|
[truncateDisplay(
|
|
152
157
|
sandboxBinding?.sessionId
|
|
@@ -154,107 +159,55 @@ export class McpPanel extends BasePanel {
|
|
|
154
159
|
: 'not isolated',
|
|
155
160
|
Math.max(0, width - 13),
|
|
156
161
|
), 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
162
|
]));
|
|
164
|
-
|
|
163
|
+
if (selected.schemaFreshness === 'quarantined') {
|
|
164
|
+
detailLines.push(buildPanelLine(width, [
|
|
165
|
+
[' Quarantine: ', C.label],
|
|
166
|
+
[truncateDisplay(`${selected.quarantineReason ?? 'unknown'}${selected.quarantineDetail ? ` - ${selected.quarantineDetail}` : ''}`, Math.max(0, width - 15)), C.error],
|
|
167
|
+
]));
|
|
168
|
+
}
|
|
169
|
+
if (selectedDecision) {
|
|
170
|
+
const summary = `${selectedDecision.serverName}:${selectedDecision.toolName} ${selectedDecision.verdict.toUpperCase()} ${selectedDecision.capability}${selectedDecision.incoherent ? ' incoherent' : ''}`;
|
|
171
|
+
detailLines.push(buildPanelLine(width, [
|
|
172
|
+
[' Recent: ', C.label],
|
|
173
|
+
[truncateDisplay(summary, Math.max(0, width - 10)), decisionColor(selectedDecision)],
|
|
174
|
+
]));
|
|
175
|
+
}
|
|
165
176
|
|
|
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
|
-
});
|
|
177
|
+
if (!selected.connected) {
|
|
178
|
+
repairLines.push(buildPanelLine(width, [[' /mcp repair', C.warn], [' review reconnect and startup posture for this server', C.dim]]));
|
|
179
|
+
}
|
|
180
|
+
if (selected.schemaFreshness !== 'fresh') {
|
|
181
|
+
repairLines.push(buildPanelLine(width, [[' /mcp review', C.warn], [' inspect schema freshness, quarantine, and trust posture', C.dim]]));
|
|
182
|
+
}
|
|
183
|
+
if (sandboxBinding?.sessionId) {
|
|
184
|
+
repairLines.push(buildPanelLine(width, [[' /sandbox review', C.info], [' verify the bound MCP isolation session and startup status', C.dim]]));
|
|
185
|
+
}
|
|
186
|
+
if (repairLines.length === 0) {
|
|
187
|
+
repairLines.push(buildPanelLine(width, [[' No immediate MCP repair actions suggested for the selected server.', C.dim]]));
|
|
188
|
+
}
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
const allDecisions = this.registry.listRecentSecurityDecisions?.(24) ?? [];
|
|
191
|
+
const decisionLines: Line[] = allDecisions.length === 0
|
|
192
|
+
? [buildPanelLine(width, [[' No MCP decisions recorded yet.', C.dim]])]
|
|
193
|
+
: allDecisions.map((decision) => {
|
|
194
|
+
const summary = `${decision.serverName}:${decision.toolName} ${decision.verdict.toUpperCase()} ${decision.capability}${decision.incoherent ? ' incoherent' : ''}`;
|
|
195
|
+
return buildPanelLine(width, [
|
|
196
|
+
[' ', C.label],
|
|
197
|
+
[truncateDisplay(summary, Math.max(0, width - 2)), decisionColor(decision)],
|
|
198
|
+
]);
|
|
199
|
+
});
|
|
200
|
+
detailLines.push(...repairLines);
|
|
201
|
+
detailLines.push(...decisionLines);
|
|
194
202
|
}
|
|
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
203
|
|
|
243
|
-
|
|
244
|
-
postureSection,
|
|
245
|
-
resolvedServersSection.section,
|
|
246
|
-
selectedSection,
|
|
247
|
-
repairSection,
|
|
248
|
-
decisionsSection,
|
|
249
|
-
];
|
|
250
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
204
|
+
return this.renderList(width, height, {
|
|
251
205
|
title: 'MCP Control Room',
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
206
|
+
header: headerLines,
|
|
207
|
+
footer: [
|
|
208
|
+
...detailLines,
|
|
209
|
+
buildPanelLine(width, [[' Up/Down move r refresh', C.dim]]),
|
|
210
|
+
],
|
|
256
211
|
});
|
|
257
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
258
|
-
return lines.slice(0, height);
|
|
259
212
|
}
|
|
260
213
|
}
|