@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,6 +1,5 @@
|
|
|
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 { TokenAuditResult } from '@pellux/goodvibes-sdk/platform/security/token-audit';
|
|
5
4
|
import type { UiReadModel, UiSecuritySnapshot } from '../runtime/ui-read-models.ts';
|
|
6
5
|
import {
|
|
@@ -8,10 +7,10 @@ import {
|
|
|
8
7
|
buildGuidanceLine,
|
|
9
8
|
buildPanelLine,
|
|
10
9
|
buildPanelWorkspace,
|
|
11
|
-
|
|
10
|
+
buildStatusPill,
|
|
12
11
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
type PanelWorkspaceSection,
|
|
14
12
|
} from './polish.ts';
|
|
13
|
+
import { createEmptyLine } from '../types/grid.ts';
|
|
15
14
|
|
|
16
15
|
const C = {
|
|
17
16
|
...DEFAULT_PANEL_PALETTE,
|
|
@@ -58,12 +57,12 @@ function severityColor(severity: 'low' | 'medium' | 'high' | 'critical'): string
|
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
export class SecurityPanel extends
|
|
62
|
-
private selectedIndex = 0;
|
|
60
|
+
export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
63
61
|
private readonly unsub: (() => void) | null;
|
|
64
62
|
|
|
65
63
|
public constructor(private readonly readModel: UiReadModel<UiSecuritySnapshot>) {
|
|
66
64
|
super('security', 'Security', 'U', 'monitoring');
|
|
65
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
67
66
|
this.unsub = this.readModel.subscribe(() => this.markDirty());
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -71,28 +70,41 @@ export class SecurityPanel extends BasePanel {
|
|
|
71
70
|
this.unsub?.();
|
|
72
71
|
}
|
|
73
72
|
|
|
73
|
+
protected override getPalette() { return C; }
|
|
74
|
+
protected override getEmptyStateMessage() { return ' No API tokens are registered with the security auditor yet.'; }
|
|
75
|
+
protected override getEmptyStateActions() {
|
|
76
|
+
return [
|
|
77
|
+
{ command: '/storage review', summary: 'inspect secure secret storage and environment overrides' },
|
|
78
|
+
{ command: '/policy preflight', summary: 'run a live preflight posture review' },
|
|
79
|
+
{ command: '/mcp trust', summary: 'inspect active MCP trust and quarantine posture' },
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected getItems(): readonly TokenAuditResult[] {
|
|
84
|
+
return this.readModel.getSnapshot().audit.results;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected renderItem(result: TokenAuditResult, index: number, selected: boolean, width: number): Line {
|
|
88
|
+
const bg = selected ? C.selectBg : undefined;
|
|
89
|
+
return buildPanelLine(width, [
|
|
90
|
+
[' ', C.label, bg],
|
|
91
|
+
[result.label.padEnd(22), C.value, bg],
|
|
92
|
+
[` ${result.tokenId.padEnd(12)}`, C.info, bg],
|
|
93
|
+
[` ${result.scope.policyId.padEnd(10)}`, C.label, bg],
|
|
94
|
+
[` ${resultSummary(result).slice(0, Math.max(0, width - 49))}`, resultColor(result), bg],
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
public handleInput(key: string): boolean {
|
|
75
|
-
const snapshot = this.readModel.getSnapshot();
|
|
76
99
|
if (key === 'r') {
|
|
77
100
|
this.markDirty();
|
|
78
101
|
return true;
|
|
79
102
|
}
|
|
80
|
-
|
|
81
|
-
if (key === 'up' || key === 'k') {
|
|
82
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
83
|
-
this.markDirty();
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
if (key === 'down' || key === 'j') {
|
|
87
|
-
this.selectedIndex = Math.min(snapshot.audit.results.length - 1, this.selectedIndex + 1);
|
|
88
|
-
this.markDirty();
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
103
|
+
return super.handleInput(key);
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
public render(width: number, height: number): Line[] {
|
|
95
|
-
this.
|
|
107
|
+
this.clampSelection();
|
|
96
108
|
const snapshot = this.readModel.getSnapshot();
|
|
97
109
|
const view = snapshot.audit;
|
|
98
110
|
const preflightStatus = snapshot.policy.preflightStatus;
|
|
@@ -105,9 +117,8 @@ export class SecurityPanel extends BasePanel {
|
|
|
105
117
|
const quarantinedPlugins = snapshot.quarantinedPlugins;
|
|
106
118
|
const untrustedPlugins = snapshot.untrustedPlugins;
|
|
107
119
|
const attackPathReview = snapshot.attackPathReview;
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
] as const;
|
|
120
|
+
const intro = 'Token audit, policy posture, MCP attack-path review, plugin trust, and incident pressure.';
|
|
121
|
+
const footerLine = buildGuidanceLine(width, '/policy preflight', 'run a proactive policy review before risky work starts', C);
|
|
111
122
|
|
|
112
123
|
const governanceLines: Line[] = [
|
|
113
124
|
buildPanelLine(width, [
|
|
@@ -116,214 +127,169 @@ export class SecurityPanel extends BasePanel {
|
|
|
116
127
|
[' tokens ', C.label],
|
|
117
128
|
[String(view.totalTokens), C.value],
|
|
118
129
|
[' blocked ', C.label],
|
|
119
|
-
|
|
130
|
+
...buildStatusPill(view.blocked.length > 0 ? 'bad' : 'good', String(view.blocked.length)),
|
|
120
131
|
[' scope violations ', C.label],
|
|
121
|
-
|
|
132
|
+
...buildStatusPill(view.scopeViolations.length > 0 ? 'bad' : 'good', String(view.scopeViolations.length)),
|
|
122
133
|
[' overdue ', C.label],
|
|
123
|
-
|
|
134
|
+
...buildStatusPill(view.rotationOverdue.length > 0 ? 'bad' : 'good', String(view.rotationOverdue.length)),
|
|
124
135
|
[' warnings ', C.label],
|
|
125
|
-
|
|
136
|
+
...buildStatusPill(view.rotationWarnings.length > 0 ? 'warn' : 'good', String(view.rotationWarnings.length)),
|
|
126
137
|
]),
|
|
127
138
|
buildPanelLine(width, [
|
|
128
139
|
[' preflight ', C.label],
|
|
129
|
-
|
|
140
|
+
...buildStatusPill(preflightStatus === 'block' ? 'bad' : preflightStatus === 'warn' ? 'warn' : preflightStatus === 'pass' ? 'good' : 'info', preflightStatus.toUpperCase()),
|
|
130
141
|
[' issues ', C.label],
|
|
131
|
-
|
|
142
|
+
...buildStatusPill(preflightIssueCount > 0 ? 'warn' : 'good', String(preflightIssueCount)),
|
|
132
143
|
[' lint ', C.label],
|
|
133
|
-
|
|
144
|
+
...buildStatusPill(lintFindingCount > 0 ? 'warn' : 'good', String(lintFindingCount)),
|
|
134
145
|
[' denied permissions ', C.label],
|
|
135
|
-
|
|
146
|
+
...buildStatusPill(snapshot.deniedPermissions > 0 ? 'warn' : 'good', String(snapshot.deniedPermissions)),
|
|
136
147
|
]),
|
|
137
|
-
];
|
|
138
|
-
|
|
139
|
-
const threatLines: Line[] = [
|
|
140
148
|
buildPanelLine(width, [
|
|
141
149
|
[' quarantined MCP ', C.label],
|
|
142
|
-
|
|
150
|
+
...buildStatusPill(quarantinedMcp.length > 0 ? 'bad' : 'good', String(quarantinedMcp.length)),
|
|
143
151
|
[' elevated MCP ', C.label],
|
|
144
|
-
|
|
152
|
+
...buildStatusPill(elevatedMcp.length > 0 ? 'warn' : 'good', String(elevatedMcp.length)),
|
|
145
153
|
[' quarantined plugins ', C.label],
|
|
146
|
-
|
|
154
|
+
...buildStatusPill(quarantinedPlugins.length > 0 ? 'bad' : 'good', String(quarantinedPlugins.length)),
|
|
147
155
|
[' untrusted plugins ', C.label],
|
|
148
|
-
|
|
156
|
+
...buildStatusPill(untrustedPlugins.length > 0 ? 'warn' : 'good', String(untrustedPlugins.length)),
|
|
149
157
|
]),
|
|
150
158
|
buildPanelLine(width, [
|
|
151
159
|
[' incidents ', C.label],
|
|
152
|
-
|
|
160
|
+
...buildStatusPill(incidents.length > 0 ? 'warn' : 'good', String(incidents.length)),
|
|
153
161
|
]),
|
|
154
162
|
];
|
|
155
163
|
|
|
156
164
|
const attackPathLines: Line[] = [
|
|
157
165
|
buildPanelLine(width, [
|
|
158
166
|
[' attack paths ', C.label],
|
|
159
|
-
|
|
167
|
+
...buildStatusPill(attackPathReview.criticalFindings > 0 ? 'bad' : 'good', String(attackPathReview.criticalFindings)),
|
|
160
168
|
[' critical ', C.label],
|
|
161
|
-
|
|
169
|
+
...buildStatusPill(attackPathReview.incoherentFindings > 0 ? 'warn' : 'good', String(attackPathReview.incoherentFindings)),
|
|
162
170
|
[' review ', C.label],
|
|
163
171
|
[attackPathReview.summary.slice(0, Math.max(0, width - 36)), C.dim],
|
|
164
172
|
]),
|
|
165
173
|
];
|
|
166
174
|
|
|
175
|
+
// Empty state: no token results yet — show governance + threat posture before base empty state
|
|
167
176
|
if (view.results.length === 0) {
|
|
168
|
-
const
|
|
177
|
+
const emptyStateLines = [
|
|
169
178
|
...governanceLines,
|
|
170
|
-
...threatLines,
|
|
171
179
|
...buildEmptyState(
|
|
172
180
|
width,
|
|
173
|
-
|
|
181
|
+
this.getEmptyStateMessage(),
|
|
174
182
|
'The security control room can already review policy, MCP, plugin, and incident posture, but token-specific scope and rotation audit data has not been registered.',
|
|
175
|
-
|
|
176
|
-
{ command: '/storage review', summary: 'inspect secure secret storage and environment overrides' },
|
|
177
|
-
{ command: '/policy preflight', summary: 'run a live preflight posture review' },
|
|
178
|
-
{ command: '/mcp trust', summary: 'inspect active MCP trust and quarantine posture' },
|
|
179
|
-
],
|
|
183
|
+
this.getEmptyStateActions(),
|
|
180
184
|
C,
|
|
181
185
|
),
|
|
182
186
|
];
|
|
183
187
|
if (quarantinedMcp.length > 0) {
|
|
184
|
-
|
|
188
|
+
emptyStateLines.push(buildPanelLine(width, [[' MCP quarantine still active despite no registered tokens.', C.warn]]));
|
|
185
189
|
}
|
|
186
190
|
const workspace = buildPanelWorkspace(width, height, {
|
|
187
191
|
title: 'Security Control Room',
|
|
188
|
-
intro
|
|
192
|
+
intro,
|
|
189
193
|
sections: [
|
|
190
|
-
{ title: 'Governance', lines:
|
|
194
|
+
{ title: 'Governance', lines: emptyStateLines },
|
|
191
195
|
{ title: 'Attack Paths', lines: attackPathLines },
|
|
192
196
|
],
|
|
193
|
-
footerLines,
|
|
197
|
+
footerLines: [footerLine],
|
|
194
198
|
palette: C,
|
|
195
199
|
});
|
|
196
200
|
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
197
201
|
return workspace.slice(0, height);
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
if (attackPathReview.findings.length > 0) {
|
|
205
|
+
attackPathLines.push(buildPanelLine(width, [[' MCP attack-path review', C.label]]));
|
|
206
|
+
for (const finding of attackPathReview.findings.slice(0, 3)) {
|
|
207
|
+
attackPathLines.push(buildPanelLine(width, [[
|
|
208
|
+
` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}`.slice(0, width),
|
|
209
|
+
severityColor(finding.severity),
|
|
210
|
+
]]));
|
|
211
|
+
attackPathLines.push(buildPanelLine(width, [[
|
|
212
|
+
` ${finding.reason}`.slice(0, width),
|
|
213
|
+
C.dim,
|
|
214
|
+
]]));
|
|
215
|
+
attackPathLines.push(buildPanelLine(width, [[
|
|
216
|
+
` evidence: ${finding.evidence.join(' | ')}`.slice(0, width),
|
|
217
|
+
C.dim,
|
|
218
|
+
]]));
|
|
219
|
+
}
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
const
|
|
216
|
-
|
|
222
|
+
const selected = view.results[this.selectedIndex];
|
|
223
|
+
const detailLines: Line[] = [];
|
|
224
|
+
if (selected) {
|
|
225
|
+
detailLines.push(buildPanelLine(width, [
|
|
217
226
|
[' Token: ', C.label],
|
|
218
227
|
[selected.label, C.value],
|
|
219
228
|
[' Policy: ', C.label],
|
|
220
229
|
[selected.scope.policyId, C.info],
|
|
221
230
|
[' Blocked: ', C.label],
|
|
222
231
|
[selected.blocked ? 'yes' : 'no', selected.blocked ? C.error : C.ok],
|
|
223
|
-
])
|
|
224
|
-
buildPanelLine(width, [
|
|
232
|
+
]));
|
|
233
|
+
detailLines.push(buildPanelLine(width, [
|
|
225
234
|
[' Scope: ', C.label],
|
|
226
235
|
[selected.scope.outcome, selected.scope.outcome === 'violation' ? C.error : C.ok],
|
|
227
236
|
[' Excess: ', C.label],
|
|
228
237
|
[(selected.scope.excessScopes.length > 0 ? selected.scope.excessScopes.join(', ') : 'none').slice(0, Math.max(0, width - 27)), selected.scope.excessScopes.length > 0 ? C.error : C.dim],
|
|
229
|
-
])
|
|
230
|
-
buildPanelLine(width, [
|
|
238
|
+
]));
|
|
239
|
+
detailLines.push(buildPanelLine(width, [
|
|
231
240
|
[' Rotation: ', C.label],
|
|
232
241
|
[selected.rotation.outcome, selected.rotation.outcome === 'ok' ? C.ok : selected.rotation.outcome === 'warning' ? C.warn : C.error],
|
|
233
242
|
[' Due: ', C.label],
|
|
234
243
|
[new Date(selected.rotation.dueAt).toISOString(), C.value],
|
|
235
244
|
[' Age(d): ', C.label],
|
|
236
245
|
[String(Math.floor(selected.rotation.ageMs / (24 * 60 * 60 * 1000))), C.value],
|
|
237
|
-
])
|
|
238
|
-
buildPanelLine(width, [[
|
|
246
|
+
]));
|
|
247
|
+
detailLines.push(buildPanelLine(width, [[
|
|
239
248
|
`Last audit: ${view.lastAuditAt ? new Date(view.lastAuditAt).toISOString() : 'never'} Press r to refresh.`,
|
|
240
249
|
C.dim,
|
|
241
|
-
]]),
|
|
242
|
-
];
|
|
243
|
-
|
|
244
|
-
if (preflightStatus !== 'n/a') {
|
|
245
|
-
detailLines.push(buildPanelLine(width, [[
|
|
246
|
-
`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})`.slice(0, width),
|
|
247
|
-
preflightStatus === 'block' ? C.error : preflightStatus === 'warn' ? C.warn : C.dim,
|
|
248
|
-
]]));
|
|
249
|
-
}
|
|
250
|
-
if (quarantinedMcp.length > 0) {
|
|
251
|
-
const server = quarantinedMcp[0]!;
|
|
252
|
-
detailLines.push(buildPanelLine(width, [[
|
|
253
|
-
`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`.slice(0, width),
|
|
254
|
-
C.error,
|
|
255
|
-
]]));
|
|
256
|
-
}
|
|
257
|
-
if (quarantinedPlugins.length > 0) {
|
|
258
|
-
const plugin = quarantinedPlugins[0]!;
|
|
259
|
-
detailLines.push(buildPanelLine(width, [[
|
|
260
|
-
`Plugin quarantine: ${plugin.name} (${plugin.trustTier})`.slice(0, width),
|
|
261
|
-
C.error,
|
|
262
|
-
]]));
|
|
263
|
-
} else if (untrustedPlugins.length > 0) {
|
|
264
|
-
const plugin = untrustedPlugins[0]!;
|
|
265
|
-
detailLines.push(buildPanelLine(width, [[
|
|
266
|
-
`Plugin trust warning: ${plugin.name} remains untrusted.`.slice(0, width),
|
|
267
|
-
C.warn,
|
|
268
250
|
]]));
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
C.warn,
|
|
274
|
-
]]));
|
|
275
|
-
}
|
|
276
|
-
if (attackPathReview.findings.length > 0) {
|
|
277
|
-
attackPathLines.push(buildPanelLine(width, [[' MCP attack-path review', C.label]]));
|
|
278
|
-
for (const finding of attackPathReview.findings.slice(0, 3)) {
|
|
279
|
-
attackPathLines.push(buildPanelLine(width, [[
|
|
280
|
-
` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}`.slice(0, width),
|
|
281
|
-
severityColor(finding.severity),
|
|
251
|
+
if (preflightStatus !== 'n/a') {
|
|
252
|
+
detailLines.push(buildPanelLine(width, [[
|
|
253
|
+
`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})`.slice(0, width),
|
|
254
|
+
preflightStatus === 'block' ? C.error : preflightStatus === 'warn' ? C.warn : C.dim,
|
|
282
255
|
]]));
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
256
|
+
}
|
|
257
|
+
if (quarantinedMcp.length > 0) {
|
|
258
|
+
const server = quarantinedMcp[0]!;
|
|
259
|
+
detailLines.push(buildPanelLine(width, [[
|
|
260
|
+
`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`.slice(0, width),
|
|
261
|
+
C.error,
|
|
286
262
|
]]));
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
263
|
+
}
|
|
264
|
+
if (quarantinedPlugins.length > 0) {
|
|
265
|
+
const plugin = quarantinedPlugins[0]!;
|
|
266
|
+
detailLines.push(buildPanelLine(width, [[
|
|
267
|
+
`Plugin quarantine: ${plugin.name} (${plugin.trustTier})`.slice(0, width),
|
|
268
|
+
C.error,
|
|
269
|
+
]]));
|
|
270
|
+
} else if (untrustedPlugins.length > 0) {
|
|
271
|
+
const plugin = untrustedPlugins[0]!;
|
|
272
|
+
detailLines.push(buildPanelLine(width, [[
|
|
273
|
+
`Plugin trust warning: ${plugin.name} remains untrusted.`.slice(0, width),
|
|
274
|
+
C.warn,
|
|
275
|
+
]]));
|
|
276
|
+
}
|
|
277
|
+
if (latestIncident) {
|
|
278
|
+
detailLines.push(buildPanelLine(width, [[
|
|
279
|
+
`Latest incident: ${latestIncident.classification} - ${latestIncident.summary}`.slice(0, width),
|
|
280
|
+
C.warn,
|
|
290
281
|
]]));
|
|
291
282
|
}
|
|
292
283
|
}
|
|
293
284
|
|
|
294
|
-
|
|
295
|
-
const trustSection: PanelWorkspaceSection = { title: 'Policy And Trust', lines: threatLines };
|
|
296
|
-
const selectedSection: PanelWorkspaceSection = { title: 'Selected Token', lines: detailLines };
|
|
297
|
-
const attackPathSection: PanelWorkspaceSection = { title: 'Attack Paths', lines: attackPathLines };
|
|
298
|
-
const tokenAuditSection = resolveScrollablePanelSection(width, height, {
|
|
299
|
-
intro: 'Token audit, policy posture, MCP attack-path review, plugin trust, and incident pressure.',
|
|
300
|
-
footerLines,
|
|
301
|
-
palette: C,
|
|
302
|
-
beforeSections: [governanceSection, trustSection],
|
|
303
|
-
section: {
|
|
304
|
-
title: 'Token Audit',
|
|
305
|
-
scrollableLines: tokenRows,
|
|
306
|
-
selectedIndex: this.selectedIndex,
|
|
307
|
-
scrollOffset: this.selectedIndex,
|
|
308
|
-
minRows: 1,
|
|
309
|
-
},
|
|
310
|
-
afterSections: [selectedSection, attackPathSection],
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
285
|
+
return this.renderList(width, height, {
|
|
314
286
|
title: 'Security Control Room',
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
attackPathSection,
|
|
322
|
-
] satisfies readonly PanelWorkspaceSection[],
|
|
323
|
-
footerLines,
|
|
324
|
-
palette: C,
|
|
287
|
+
header: governanceLines,
|
|
288
|
+
footer: [
|
|
289
|
+
...detailLines,
|
|
290
|
+
...attackPathLines,
|
|
291
|
+
footerLine,
|
|
292
|
+
],
|
|
325
293
|
});
|
|
326
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
327
|
-
return lines.slice(0, height);
|
|
328
294
|
}
|
|
329
295
|
}
|
|
@@ -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
|
type ServiceConfig,
|
|
6
6
|
type ServiceInspection,
|
|
@@ -11,9 +11,8 @@ import {
|
|
|
11
11
|
buildEmptyState,
|
|
12
12
|
buildPanelLine,
|
|
13
13
|
buildPanelWorkspace,
|
|
14
|
+
buildStatusPill,
|
|
14
15
|
DEFAULT_PANEL_PALETTE,
|
|
15
|
-
resolvePrimaryScrollableSection,
|
|
16
|
-
type PanelWorkspaceSection,
|
|
17
16
|
} from './polish.ts';
|
|
18
17
|
|
|
19
18
|
const C = {
|
|
@@ -54,7 +53,6 @@ function statusColor(entry: ServicePanelEntry): string {
|
|
|
54
53
|
|
|
55
54
|
function authSummary(config: ServiceConfig, manager: SubscriptionAccessQuery): string {
|
|
56
55
|
const provider = config.providerId ?? config.name;
|
|
57
|
-
const hasActiveSubscription = manager.get(provider) != null;
|
|
58
56
|
const hasOverride = manager.getAccessToken(provider) != null;
|
|
59
57
|
switch (config.authType) {
|
|
60
58
|
case 'bearer':
|
|
@@ -66,16 +64,14 @@ function authSummary(config: ServiceConfig, manager: SubscriptionAccessQuery): s
|
|
|
66
64
|
? 'oauth-override'
|
|
67
65
|
: config.apiKeyHeader ? `api-key:${config.apiKeyHeader}` : 'api-key';
|
|
68
66
|
case 'oauth':
|
|
69
|
-
return
|
|
67
|
+
return manager.get(provider) != null ? 'oauth(active)' : 'oauth';
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
export class ServicesPanel extends
|
|
71
|
+
export class ServicesPanel extends ScrollableListPanel<ServicePanelEntry> {
|
|
74
72
|
private readonly registry: ServiceInspectionQuery;
|
|
75
73
|
private readonly subscriptionManager: SubscriptionAccessQuery;
|
|
76
74
|
private entries: ServicePanelEntry[] = [];
|
|
77
|
-
private selectedIndex = 0;
|
|
78
|
-
private scrollOffset = 0;
|
|
79
75
|
private loading = false;
|
|
80
76
|
|
|
81
77
|
public constructor(
|
|
@@ -83,6 +79,7 @@ export class ServicesPanel extends BasePanel {
|
|
|
83
79
|
subscriptionManager: SubscriptionAccessQuery,
|
|
84
80
|
) {
|
|
85
81
|
super('services', 'Services', 'V', 'monitoring');
|
|
82
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
86
83
|
this.registry = registry;
|
|
87
84
|
this.subscriptionManager = subscriptionManager;
|
|
88
85
|
void this.refresh();
|
|
@@ -95,27 +92,40 @@ export class ServicesPanel extends BasePanel {
|
|
|
95
92
|
}
|
|
96
93
|
}
|
|
97
94
|
|
|
95
|
+
protected override getPalette() { return C; }
|
|
96
|
+
protected override getEmptyStateMessage() { return ' No services configured.'; }
|
|
97
|
+
protected override getEmptyStateActions() {
|
|
98
|
+
return [
|
|
99
|
+
{ command: '/services auth-review', summary: 'inspect service auth posture and registry config' },
|
|
100
|
+
{ command: '/subscription', summary: 'review provider login state and override posture' },
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected getItems(): readonly ServicePanelEntry[] {
|
|
105
|
+
return this.entries;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
protected renderItem(entry: ServicePanelEntry, index: number, selected: boolean, width: number): Line {
|
|
109
|
+
const bg = selected ? C.selectBg : undefined;
|
|
110
|
+
return buildPanelLine(width, [
|
|
111
|
+
[' ', C.label, bg],
|
|
112
|
+
[entry.name.padEnd(16), C.value, bg],
|
|
113
|
+
[` ${statusLabel(entry).padEnd(12)}`, statusColor(entry), bg],
|
|
114
|
+
[` ${authSummary(entry.inspection.config, this.subscriptionManager).padEnd(18)}`, C.info, bg],
|
|
115
|
+
[` ${entry.inspection.config.baseUrl ?? '(no baseUrl)'}`, C.dim, bg],
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
public handleInput(key: string): boolean {
|
|
99
120
|
if (key === 'r') {
|
|
100
121
|
void this.refresh();
|
|
101
122
|
return true;
|
|
102
123
|
}
|
|
103
|
-
if (this.entries.length === 0) return false;
|
|
104
|
-
if (key === 'up' || key === 'k') {
|
|
105
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
106
|
-
this.markDirty();
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
if (key === 'down' || key === 'j') {
|
|
110
|
-
this.selectedIndex = Math.min(this.entries.length - 1, this.selectedIndex + 1);
|
|
111
|
-
this.markDirty();
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
124
|
if (key === 't') {
|
|
115
125
|
void this.testSelected();
|
|
116
126
|
return true;
|
|
117
127
|
}
|
|
118
|
-
return
|
|
128
|
+
return super.handleInput(key);
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
private async refresh(): Promise<void> {
|
|
@@ -152,7 +162,7 @@ export class ServicesPanel extends BasePanel {
|
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
public render(width: number, height: number): Line[] {
|
|
155
|
-
this.
|
|
165
|
+
this.clampSelection();
|
|
156
166
|
const intro = 'Credential posture, subscription overrides, and live connection checks for configured services.';
|
|
157
167
|
|
|
158
168
|
if (this.loading && this.entries.length === 0) {
|
|
@@ -166,106 +176,54 @@ export class ServicesPanel extends BasePanel {
|
|
|
166
176
|
return lines;
|
|
167
177
|
}
|
|
168
178
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
lines: buildEmptyState(
|
|
175
|
-
width,
|
|
176
|
-
' No services configured.',
|
|
177
|
-
'Add entries to .goodvibes/tui/services.json and store secrets before using service-backed product flows.',
|
|
178
|
-
[
|
|
179
|
-
{ command: '/services auth-review', summary: 'inspect service auth posture and registry config' },
|
|
180
|
-
{ command: '/subscription', summary: 'review provider login state and override posture' },
|
|
181
|
-
],
|
|
182
|
-
C,
|
|
183
|
-
),
|
|
184
|
-
}],
|
|
185
|
-
palette: C,
|
|
186
|
-
});
|
|
187
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
188
|
-
return workspace;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const selected = this.entries[this.selectedIndex]!;
|
|
192
|
-
const inspect = selected.inspection;
|
|
193
|
-
const detailLines: Line[] = [
|
|
194
|
-
buildPanelLine(width, [
|
|
179
|
+
const selected = this.entries[this.selectedIndex];
|
|
180
|
+
const detailLines: Line[] = [];
|
|
181
|
+
if (selected) {
|
|
182
|
+
const inspect = selected.inspection;
|
|
183
|
+
detailLines.push(buildPanelLine(width, [
|
|
195
184
|
[' Service: ', C.label],
|
|
196
185
|
[selected.name, C.value],
|
|
197
186
|
[' State: ', C.label],
|
|
198
187
|
[statusLabel(selected), statusColor(selected)],
|
|
199
188
|
[' Auth: ', C.label],
|
|
200
189
|
[authSummary(inspect.config, this.subscriptionManager), C.info],
|
|
201
|
-
])
|
|
202
|
-
buildPanelLine(width, [
|
|
190
|
+
]));
|
|
191
|
+
detailLines.push(buildPanelLine(width, [
|
|
203
192
|
[' Primary credential: ', C.label],
|
|
204
|
-
|
|
193
|
+
...buildStatusPill(inspect.hasPrimaryCredential ? 'good' : 'bad', inspect.hasPrimaryCredential ? 'present' : 'missing'),
|
|
205
194
|
[' Webhook URL: ', C.label],
|
|
206
|
-
|
|
195
|
+
...buildStatusPill(inspect.hasWebhookUrl ? 'good' : 'info', inspect.hasWebhookUrl ? 'present' : 'missing'),
|
|
207
196
|
[' Signing secret: ', C.label],
|
|
208
|
-
|
|
209
|
-
]),
|
|
210
|
-
];
|
|
211
|
-
if (selected.lastTest) {
|
|
212
|
-
detailLines.push(buildPanelLine(width, [
|
|
213
|
-
[' Last test: ', C.label],
|
|
214
|
-
[selected.lastTest.ok ? 'ok' : 'failed', selected.lastTest.ok ? C.ok : C.error],
|
|
215
|
-
[' Status: ', C.label],
|
|
216
|
-
[selected.lastTest.status != null ? String(selected.lastTest.status) : 'n/a', C.value],
|
|
217
|
-
[' URL: ', C.label],
|
|
218
|
-
[(selected.lastTest.testedUrl ?? 'n/a').slice(0, Math.max(0, width - 34)), C.dim],
|
|
197
|
+
...buildStatusPill(inspect.hasSigningSecret ? 'good' : 'info', inspect.hasSigningSecret ? 'present' : 'missing'),
|
|
219
198
|
]));
|
|
220
|
-
if (selected.lastTest
|
|
199
|
+
if (selected.lastTest) {
|
|
221
200
|
detailLines.push(buildPanelLine(width, [
|
|
222
|
-
['
|
|
223
|
-
|
|
201
|
+
[' Last test: ', C.label],
|
|
202
|
+
...buildStatusPill(selected.lastTest.ok ? 'good' : 'bad', selected.lastTest.ok ? 'ok' : 'failed'),
|
|
203
|
+
[' Status: ', C.label],
|
|
204
|
+
[selected.lastTest.status != null ? String(selected.lastTest.status) : 'n/a', C.value],
|
|
205
|
+
[' URL: ', C.label],
|
|
206
|
+
[(selected.lastTest.testedUrl ?? 'n/a').slice(0, Math.max(0, width - 34)), C.dim],
|
|
224
207
|
]));
|
|
208
|
+
if (selected.lastTest.error) {
|
|
209
|
+
detailLines.push(buildPanelLine(width, [
|
|
210
|
+
[' Error: ', C.label],
|
|
211
|
+
[selected.lastTest.error.slice(0, Math.max(0, width - 10)), C.error],
|
|
212
|
+
]));
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
detailLines.push(buildPanelLine(width, [[' Press t to test the selected service or r to refresh credential status.', C.dim]]));
|
|
225
216
|
}
|
|
226
|
-
|
|
227
|
-
detailLines.push(buildPanelLine(width, [[' Press t to test the selected service or r to refresh credential status.', C.dim]]));
|
|
217
|
+
detailLines.push(buildPanelLine(width, [[' Services resolve credentials through hierarchy-aware secure storage, plaintext fallback policy, and project-local config.', C.dim]]));
|
|
228
218
|
}
|
|
229
|
-
detailLines.push(buildPanelLine(width, [[' Services resolve credentials through hierarchy-aware secure storage, plaintext fallback policy, and project-local config.', C.dim]]));
|
|
230
|
-
const detailSection: PanelWorkspaceSection = { title: 'Details', lines: detailLines };
|
|
231
|
-
const resolvedServicesSection = resolvePrimaryScrollableSection(width, height, {
|
|
232
|
-
intro,
|
|
233
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move t test selected service r refresh inspections', C.dim]])],
|
|
234
|
-
palette: C,
|
|
235
|
-
section: {
|
|
236
|
-
title: 'Services',
|
|
237
|
-
scrollableLines: this.entries.map((entry, absolute) => {
|
|
238
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
239
|
-
return buildPanelLine(width, [
|
|
240
|
-
[' ', C.label, bg],
|
|
241
|
-
[entry.name.padEnd(16), C.value, bg],
|
|
242
|
-
[` ${statusLabel(entry).padEnd(12)}`, statusColor(entry), bg],
|
|
243
|
-
[` ${authSummary(entry.inspection.config, this.subscriptionManager).padEnd(18)}`, C.info, bg],
|
|
244
|
-
[` ${entry.inspection.config.baseUrl ?? '(no baseUrl)'}`, C.dim, bg],
|
|
245
|
-
]);
|
|
246
|
-
}),
|
|
247
|
-
selectedIndex: this.selectedIndex,
|
|
248
|
-
scrollOffset: this.scrollOffset,
|
|
249
|
-
guardRows: 1,
|
|
250
|
-
minRows: 4,
|
|
251
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
252
|
-
},
|
|
253
|
-
afterSections: [detailSection],
|
|
254
|
-
});
|
|
255
|
-
this.scrollOffset = resolvedServicesSection.scrollOffset;
|
|
256
219
|
|
|
257
|
-
|
|
258
|
-
resolvedServicesSection.section,
|
|
259
|
-
detailSection,
|
|
260
|
-
];
|
|
261
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
220
|
+
return this.renderList(width, height, {
|
|
262
221
|
title: 'Service Control Room',
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
222
|
+
footer: [
|
|
223
|
+
...detailLines,
|
|
224
|
+
buildPanelLine(width, [[' Up/Down move t test selected service r refresh inspections', C.dim]]),
|
|
225
|
+
],
|
|
226
|
+
emptyMessage: intro,
|
|
267
227
|
});
|
|
268
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
269
|
-
return lines.slice(0, height);
|
|
270
228
|
}
|
|
271
229
|
}
|