@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -3,28 +3,26 @@ import { createEmptyLine } from '../types/grid.ts';
|
|
|
3
3
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import type { UiControlPlaneSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
5
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
|
+
import type { ApprovalBroker, ControlPlaneRecentEvent, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
7
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
8
|
import {
|
|
9
|
+
type ConfirmState,
|
|
10
|
+
handleConfirmInput,
|
|
11
|
+
renderConfirmLines,
|
|
12
|
+
} from './confirm-state.ts';
|
|
13
|
+
import {
|
|
14
|
+
buildDetailBlock,
|
|
7
15
|
buildEmptyState,
|
|
8
|
-
buildGuidanceLine,
|
|
9
16
|
buildKeyValueLine,
|
|
10
|
-
buildKeyboardHints,
|
|
11
17
|
buildPanelLine,
|
|
12
18
|
buildPanelWorkspace,
|
|
13
|
-
buildSectionHeader,
|
|
14
19
|
DEFAULT_PANEL_PALETTE,
|
|
15
20
|
type PanelPalette,
|
|
16
21
|
} from './polish.ts';
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
headerBg: '#1e293b',
|
|
22
|
-
ok: '#22c55e',
|
|
23
|
-
warn: '#eab308',
|
|
24
|
-
error: '#ef4444',
|
|
25
|
-
info: '#38bdf8',
|
|
26
|
-
selectBg: '#0f172a',
|
|
27
|
-
} as const;
|
|
23
|
+
// Base chrome only — title band, state colors, and text tokens all come
|
|
24
|
+
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
25
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
28
26
|
|
|
29
27
|
function formatTime(value?: number): string {
|
|
30
28
|
if (!value) return 'n/a';
|
|
@@ -32,22 +30,73 @@ function formatTime(value?: number): string {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
function connectionColor(state: string): string {
|
|
35
|
-
if (state === 'connected' || state === 'healthy') return C.
|
|
33
|
+
if (state === 'connected' || state === 'healthy') return C.good;
|
|
36
34
|
if (state === 'degraded' || state === 'connecting' || state === 'initializing') return C.warn;
|
|
37
|
-
if (state === 'terminal_failure') return C.
|
|
35
|
+
if (state === 'terminal_failure') return C.bad;
|
|
36
|
+
return C.dim;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function approvalStatusColor(status: string): string {
|
|
40
|
+
if (status === 'pending' || status === 'claimed') return C.warn;
|
|
41
|
+
if (status === 'approved') return C.good;
|
|
42
|
+
if (status === 'denied' || status === 'expired') return C.bad;
|
|
38
43
|
return C.dim;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
type ControlPlaneClient = UiControlPlaneSnapshot['clients'][number];
|
|
47
|
+
type ControlPlaneApproval = UiControlPlaneSnapshot['approvals'][number];
|
|
48
|
+
type ControlPlaneSession = UiControlPlaneSnapshot['sessions'][number];
|
|
49
|
+
type ControlPlaneEvent = UiControlPlaneSnapshot['recentEvents'][number];
|
|
50
|
+
|
|
51
|
+
type FocusSection = 'approvals' | 'clients' | 'sessions' | 'events';
|
|
52
|
+
|
|
53
|
+
const FOCUS_SECTIONS: readonly FocusSection[] = ['approvals', 'clients', 'sessions', 'events'];
|
|
42
54
|
|
|
43
|
-
|
|
55
|
+
type ControlPlaneItem =
|
|
56
|
+
| { readonly kind: 'client'; readonly client: ControlPlaneClient }
|
|
57
|
+
| { readonly kind: 'approval'; readonly approval: ControlPlaneApproval }
|
|
58
|
+
| { readonly kind: 'session'; readonly session: ControlPlaneSession }
|
|
59
|
+
| { readonly kind: 'event'; readonly event: ControlPlaneEvent };
|
|
60
|
+
|
|
61
|
+
type ApprovalConfirmSubject = { readonly approvalId: string; readonly approved: boolean };
|
|
62
|
+
|
|
63
|
+
/** Deps needed to drive real approve/deny and live session/event lookups (WO-121). */
|
|
64
|
+
export interface ControlPlanePanelDeps {
|
|
65
|
+
readonly approvalBroker?: ApprovalBroker;
|
|
66
|
+
readonly sessionBroker?: SharedSessionBroker;
|
|
67
|
+
readonly getControlPlaneRecentEvents?: (limit: number) => readonly ControlPlaneRecentEvent[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* ControlPlanePanel — daemon control-plane console (WO-121).
|
|
72
|
+
*
|
|
73
|
+
* Tab cycles focus across four selectable sections (approvals, clients,
|
|
74
|
+
* sessions, events); approvals is the primary/default section and supports
|
|
75
|
+
* a=approve / d=deny via ApprovalBroker.resolveApproval behind ConfirmState.
|
|
76
|
+
* Sessions and events pull from the live brokers when wired, falling back to
|
|
77
|
+
* the read-model snapshot when they are not.
|
|
78
|
+
*/
|
|
79
|
+
export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneItem> {
|
|
80
|
+
private readonly readModel?: UiReadModel<UiControlPlaneSnapshot>;
|
|
81
|
+
private readonly approvalBroker?: ApprovalBroker;
|
|
82
|
+
private readonly sessionBroker?: SharedSessionBroker;
|
|
83
|
+
private readonly getControlPlaneRecentEvents?: (limit: number) => readonly ControlPlaneRecentEvent[];
|
|
44
84
|
private readonly unsub: (() => void) | null;
|
|
85
|
+
private focusSection: FocusSection = 'approvals';
|
|
86
|
+
private confirmAction: ConfirmState<ApprovalConfirmSubject> | null = null;
|
|
45
87
|
|
|
46
|
-
public constructor(
|
|
47
|
-
|
|
88
|
+
public constructor(
|
|
89
|
+
readModel?: UiReadModel<UiControlPlaneSnapshot>,
|
|
90
|
+
deps: ControlPlanePanelDeps = {},
|
|
91
|
+
) {
|
|
92
|
+
super('control-plane', 'Control Plane', 'C', 'runtime-ops');
|
|
48
93
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
49
94
|
this.filterEnabled = true;
|
|
50
|
-
this.filterLabel = 'Filter
|
|
95
|
+
this.filterLabel = 'Filter approvals';
|
|
96
|
+
this.readModel = readModel;
|
|
97
|
+
this.approvalBroker = deps.approvalBroker;
|
|
98
|
+
this.sessionBroker = deps.sessionBroker;
|
|
99
|
+
this.getControlPlaneRecentEvents = deps.getControlPlaneRecentEvents;
|
|
51
100
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
52
101
|
}
|
|
53
102
|
|
|
@@ -59,18 +108,166 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
59
108
|
return C;
|
|
60
109
|
}
|
|
61
110
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
// Data
|
|
113
|
+
// -------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The read model's snapshot.approvals is capped upstream (SDK read model
|
|
117
|
+
* calls approvalBroker.listApprovals(6)). Sourcing directly from the
|
|
118
|
+
* broker when it's wired removes that cap so the panel can show and act
|
|
119
|
+
* on the full pending set, not just the first six.
|
|
120
|
+
*/
|
|
121
|
+
private _approvals(snapshot: UiControlPlaneSnapshot): readonly ControlPlaneApproval[] {
|
|
122
|
+
const approvals = this.approvalBroker ? this.approvalBroker.listApprovals(50) : snapshot.approvals;
|
|
123
|
+
const pending: ControlPlaneApproval[] = [];
|
|
124
|
+
const rest: ControlPlaneApproval[] = [];
|
|
125
|
+
for (const approval of approvals) {
|
|
126
|
+
(approval.status === 'pending' ? pending : rest).push(approval);
|
|
127
|
+
}
|
|
128
|
+
return [...pending, ...rest];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private _sessions(snapshot: UiControlPlaneSnapshot): readonly ControlPlaneSession[] {
|
|
132
|
+
return this.sessionBroker ? this.sessionBroker.listSessions(50) : snapshot.sessions;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private _recentEvents(snapshot: UiControlPlaneSnapshot): readonly ControlPlaneEvent[] {
|
|
136
|
+
return this.getControlPlaneRecentEvents ? this.getControlPlaneRecentEvents(50) : snapshot.recentEvents;
|
|
66
137
|
}
|
|
67
138
|
|
|
68
|
-
protected getItems(): readonly
|
|
139
|
+
protected getItems(): readonly ControlPlaneItem[] {
|
|
69
140
|
if (!this.readModel) return [];
|
|
70
|
-
|
|
141
|
+
const snapshot = this.readModel.getSnapshot();
|
|
142
|
+
switch (this.focusSection) {
|
|
143
|
+
case 'approvals':
|
|
144
|
+
return this._approvals(snapshot).map((approval): ControlPlaneItem => ({ kind: 'approval', approval }));
|
|
145
|
+
case 'sessions':
|
|
146
|
+
return this._sessions(snapshot).map((session): ControlPlaneItem => ({ kind: 'session', session }));
|
|
147
|
+
case 'events':
|
|
148
|
+
return this._recentEvents(snapshot).map((event): ControlPlaneItem => ({ kind: 'event', event }));
|
|
149
|
+
case 'clients':
|
|
150
|
+
default:
|
|
151
|
+
return snapshot.clients.map((client): ControlPlaneItem => ({ kind: 'client', client }));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
protected override filterMatches(item: ControlPlaneItem, q: string): boolean {
|
|
156
|
+
switch (item.kind) {
|
|
157
|
+
case 'client':
|
|
158
|
+
return item.client.kind.toLowerCase().includes(q)
|
|
159
|
+
|| item.client.label.toLowerCase().includes(q)
|
|
160
|
+
|| item.client.transport.toLowerCase().includes(q);
|
|
161
|
+
case 'approval':
|
|
162
|
+
return item.approval.status.toLowerCase().includes(q)
|
|
163
|
+
|| item.approval.request.tool.toLowerCase().includes(q)
|
|
164
|
+
|| (item.approval.sessionId ?? '').toLowerCase().includes(q);
|
|
165
|
+
case 'session':
|
|
166
|
+
return item.session.status.toLowerCase().includes(q)
|
|
167
|
+
|| item.session.title.toLowerCase().includes(q);
|
|
168
|
+
case 'event':
|
|
169
|
+
return item.event.event.toLowerCase().includes(q);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected override getEmptyStateMessage(): string {
|
|
174
|
+
switch (this.focusSection) {
|
|
175
|
+
case 'approvals': return ' No approval pressure recorded.';
|
|
176
|
+
case 'sessions': return ' No shared sessions recorded.';
|
|
177
|
+
case 'events': return ' No recent control-plane events.';
|
|
178
|
+
default: return ' No control-plane clients connected.';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
183
|
+
return [
|
|
184
|
+
{ command: '/cockpit', summary: 'watch operator posture from the terminal' },
|
|
185
|
+
{ command: '/schedule list', summary: 'run automation that creates surface and daemon traffic' },
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// -------------------------------------------------------------------------
|
|
190
|
+
// Input
|
|
191
|
+
// -------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
handleInput(key: string): boolean {
|
|
194
|
+
if (this.lastError !== null) this.clearError();
|
|
195
|
+
|
|
196
|
+
if (this.confirmAction) {
|
|
197
|
+
const result = handleConfirmInput(this.confirmAction, key);
|
|
198
|
+
if (result === 'confirmed') {
|
|
199
|
+
this._executeConfirmed(this.confirmAction.subject);
|
|
200
|
+
this.confirmAction = null;
|
|
201
|
+
this.markDirty();
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (result === 'cancelled') {
|
|
205
|
+
this.confirmAction = null;
|
|
206
|
+
this.markDirty();
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!this.filterActive) {
|
|
212
|
+
if (key === 'tab') {
|
|
213
|
+
this._cycleFocus();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (this.focusSection === 'approvals' && this.approvalBroker && (key === 'a' || key === 'd')) {
|
|
217
|
+
this._beginApprovalDecision(key === 'a');
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return super.handleInput(key);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private _cycleFocus(): void {
|
|
226
|
+
const idx = FOCUS_SECTIONS.indexOf(this.focusSection);
|
|
227
|
+
this.focusSection = FOCUS_SECTIONS[(idx + 1) % FOCUS_SECTIONS.length]!;
|
|
228
|
+
this.filterLabel = `Filter ${this.focusSection}`;
|
|
229
|
+
this.selectedIndex = 0;
|
|
230
|
+
this.markDirty();
|
|
71
231
|
}
|
|
72
232
|
|
|
73
|
-
|
|
233
|
+
private _beginApprovalDecision(approved: boolean): void {
|
|
234
|
+
const item = this.getSelectedItem();
|
|
235
|
+
if (!item || item.kind !== 'approval') return;
|
|
236
|
+
if (item.approval.status !== 'pending' && item.approval.status !== 'claimed') return;
|
|
237
|
+
this.confirmAction = {
|
|
238
|
+
subject: { approvalId: item.approval.id, approved },
|
|
239
|
+
label: `${item.approval.request.tool} · ${truncateDisplay(item.approval.sessionId ?? item.approval.id, 24)}`,
|
|
240
|
+
verb: approved ? 'Approve' : 'Deny',
|
|
241
|
+
};
|
|
242
|
+
this.markDirty();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private _executeConfirmed(subject: ApprovalConfirmSubject): void {
|
|
246
|
+
if (!this.approvalBroker) return;
|
|
247
|
+
void this.approvalBroker.resolveApproval(subject.approvalId, {
|
|
248
|
+
approved: subject.approved,
|
|
249
|
+
actor: 'operator',
|
|
250
|
+
actorSurface: 'tui',
|
|
251
|
+
}).catch((err) => {
|
|
252
|
+
this.setError(`${subject.approved ? 'Approve' : 'Deny'} failed: ${summarizeError(err)}`);
|
|
253
|
+
this.markDirty();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
// Rendering — item rows
|
|
259
|
+
// -------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
protected renderItem(item: ControlPlaneItem, _index: number, selected: boolean, width: number): Line {
|
|
262
|
+
switch (item.kind) {
|
|
263
|
+
case 'client': return this._renderClientRow(item.client, selected, width);
|
|
264
|
+
case 'approval': return this._renderApprovalRow(item.approval, selected, width);
|
|
265
|
+
case 'session': return this._renderSessionRow(item.session, selected, width);
|
|
266
|
+
case 'event': return this._renderEventRow(item.event, selected, width);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private _renderClientRow(client: ControlPlaneClient, selected: boolean, width: number): Line {
|
|
74
271
|
const bg = selected ? C.selectBg : undefined;
|
|
75
272
|
return buildPanelLine(width, [
|
|
76
273
|
[' ', C.label, bg],
|
|
@@ -81,17 +278,40 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
81
278
|
]);
|
|
82
279
|
}
|
|
83
280
|
|
|
84
|
-
|
|
85
|
-
|
|
281
|
+
private _renderApprovalRow(approval: ControlPlaneApproval, selected: boolean, width: number): Line {
|
|
282
|
+
const bg = selected ? C.selectBg : undefined;
|
|
283
|
+
return buildPanelLine(width, [
|
|
284
|
+
[' ', C.label, bg],
|
|
285
|
+
[approval.status.padEnd(10), approvalStatusColor(approval.status), bg],
|
|
286
|
+
[` ${truncateDisplay(approval.request.tool, 16).padEnd(16)}`, C.value, bg],
|
|
287
|
+
[` ${truncateDisplay(approval.sessionId ?? approval.id, Math.max(0, width - 30))}`, C.dim, bg],
|
|
288
|
+
]);
|
|
86
289
|
}
|
|
87
290
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
291
|
+
private _renderSessionRow(session: ControlPlaneSession, selected: boolean, width: number): Line {
|
|
292
|
+
const bg = selected ? C.selectBg : undefined;
|
|
293
|
+
return buildPanelLine(width, [
|
|
294
|
+
[' ', C.label, bg],
|
|
295
|
+
[session.status.padEnd(10), session.status === 'active' ? C.good : C.dim, bg],
|
|
296
|
+
[` ${truncateDisplay(session.title, 20).padEnd(20)}`, C.value, bg],
|
|
297
|
+
[` ${truncateDisplay(session.activeAgentId ?? session.id, Math.max(0, width - 34))}`, C.dim, bg],
|
|
298
|
+
]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private _renderEventRow(event: ControlPlaneEvent, selected: boolean, width: number): Line {
|
|
302
|
+
const bg = selected ? C.selectBg : undefined;
|
|
303
|
+
const payload = typeof event.payload === 'string' ? event.payload : (JSON.stringify(event.payload) ?? '');
|
|
304
|
+
return buildPanelLine(width, [
|
|
305
|
+
[' ', C.label, bg],
|
|
306
|
+
[truncateDisplay(event.event, 16).padEnd(16), C.info, bg],
|
|
307
|
+
[` ${truncateDisplay(payload, Math.max(0, width - 19))}`, C.dim, bg],
|
|
308
|
+
]);
|
|
93
309
|
}
|
|
94
310
|
|
|
311
|
+
// -------------------------------------------------------------------------
|
|
312
|
+
// Rendering — workspace
|
|
313
|
+
// -------------------------------------------------------------------------
|
|
314
|
+
|
|
95
315
|
public render(width: number, height: number): Line[] {
|
|
96
316
|
const intro = 'Shared daemon control plane state, live clients, approval pressure, and recent omnichannel session posture.';
|
|
97
317
|
|
|
@@ -115,28 +335,29 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
115
335
|
}
|
|
116
336
|
|
|
117
337
|
const snapshot = this.readModel.getSnapshot();
|
|
118
|
-
const approvals = snapshot
|
|
119
|
-
const sessions = snapshot
|
|
120
|
-
const recentEvents = snapshot
|
|
121
|
-
const clients =
|
|
338
|
+
const approvals = this._approvals(snapshot);
|
|
339
|
+
const sessions = this._sessions(snapshot);
|
|
340
|
+
const recentEvents = this._recentEvents(snapshot);
|
|
341
|
+
const clients = snapshot.clients;
|
|
342
|
+
const pendingApprovals = approvals.filter((entry) => entry.status === 'pending').length;
|
|
122
343
|
|
|
123
344
|
const headerLines: Line[] = [
|
|
124
345
|
buildKeyValueLine(width, [
|
|
125
346
|
{ label: 'state', value: snapshot.connectionState, valueColor: connectionColor(snapshot.connectionState) },
|
|
126
|
-
{ label: 'clients', value: String(snapshot.activeClientIds.length), valueColor: snapshot.activeClientIds.length > 0 ? C.
|
|
347
|
+
{ label: 'clients', value: String(snapshot.activeClientIds.length), valueColor: snapshot.activeClientIds.length > 0 ? C.good : C.dim },
|
|
127
348
|
{ label: 'requests', value: String(snapshot.requestCount), valueColor: snapshot.requestCount > 0 ? C.info : C.dim },
|
|
128
|
-
{ label: 'errors', value: String(snapshot.errorCount), valueColor: snapshot.errorCount > 0 ? C.
|
|
349
|
+
{ label: 'errors', value: String(snapshot.errorCount), valueColor: snapshot.errorCount > 0 ? C.bad : C.dim },
|
|
129
350
|
], C),
|
|
130
351
|
buildKeyValueLine(width, [
|
|
131
352
|
{ label: 'host', value: `${snapshot.host}:${snapshot.port}`, valueColor: C.value },
|
|
132
|
-
{ label: 'approvals', value:
|
|
353
|
+
{ label: 'approvals', value: `${pendingApprovals} pending / ${approvals.length}`, valueColor: pendingApprovals > 0 ? C.warn : C.dim },
|
|
133
354
|
{ label: 'sessions', value: String(sessions.length), valueColor: sessions.length > 0 ? C.info : C.dim },
|
|
134
355
|
{ label: 'events', value: String(recentEvents.length), valueColor: recentEvents.length > 0 ? C.info : C.dim },
|
|
135
356
|
], C),
|
|
136
|
-
|
|
357
|
+
this._buildFocusTabsLine(width),
|
|
137
358
|
];
|
|
138
359
|
|
|
139
|
-
if (clients.length === 0 && approvals.length === 0 && sessions.length === 0) {
|
|
360
|
+
if (clients.length === 0 && approvals.length === 0 && sessions.length === 0 && recentEvents.length === 0) {
|
|
140
361
|
return this.renderList(width, height, {
|
|
141
362
|
title: 'Control Plane',
|
|
142
363
|
header: headerLines,
|
|
@@ -145,93 +366,105 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
145
366
|
}
|
|
146
367
|
|
|
147
368
|
this.clampSelection();
|
|
148
|
-
const selected =
|
|
369
|
+
const selected = this.getSelectedItem();
|
|
149
370
|
|
|
150
371
|
const footerLines: Line[] = [];
|
|
151
372
|
if (selected) {
|
|
152
|
-
footerLines.push(
|
|
153
|
-
|
|
154
|
-
[' Client: ', C.label],
|
|
155
|
-
[selected.label, C.value],
|
|
156
|
-
[' Kind: ', C.label],
|
|
157
|
-
[selected.kind, C.info],
|
|
158
|
-
]),
|
|
159
|
-
buildPanelLine(width, [
|
|
160
|
-
[' Transport: ', C.label],
|
|
161
|
-
[selected.transport, C.value],
|
|
162
|
-
[' Connected: ', C.label],
|
|
163
|
-
[selected.connected ? 'yes' : 'no', selected.connected ? C.ok : C.warn],
|
|
164
|
-
]),
|
|
165
|
-
buildPanelLine(width, [
|
|
166
|
-
[' Route: ', C.label],
|
|
167
|
-
[selected.routeId ?? 'n/a', C.dim],
|
|
168
|
-
[' Session: ', C.label],
|
|
169
|
-
[selected.sessionId ?? 'n/a', C.dim],
|
|
170
|
-
]),
|
|
171
|
-
buildPanelLine(width, [
|
|
172
|
-
[' Last seen: ', C.label],
|
|
173
|
-
[formatTime(selected.lastSeenAt), C.dim],
|
|
174
|
-
[' Remote: ', C.label],
|
|
175
|
-
[truncateDisplay(selected.remoteAddress ?? 'n/a', Math.max(0, width - 36)), C.dim],
|
|
176
|
-
]),
|
|
177
|
-
);
|
|
373
|
+
footerLines.push(...this._buildDetailLines(width, selected));
|
|
374
|
+
if (this.confirmAction) footerLines.push(...renderConfirmLines(width, this.confirmAction));
|
|
178
375
|
} else {
|
|
179
|
-
footerLines.push(buildPanelLine(width, [['
|
|
376
|
+
footerLines.push(buildPanelLine(width, [[' Nothing selected in this section.', C.dim]]));
|
|
180
377
|
}
|
|
181
378
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
footerLines
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
379
|
+
return this.renderList(width, height, {
|
|
380
|
+
title: 'Control Plane',
|
|
381
|
+
header: headerLines,
|
|
382
|
+
footer: footerLines,
|
|
383
|
+
hints: this._buildHints(selected),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private _buildFocusTabsLine(width: number): Line {
|
|
388
|
+
const seg = (label: string, section: FocusSection): [string, string] => [
|
|
389
|
+
` ${label} `,
|
|
390
|
+
this.focusSection === section ? C.value : C.dim,
|
|
391
|
+
];
|
|
392
|
+
return buildPanelLine(width, [
|
|
393
|
+
seg('Approvals', 'approvals'),
|
|
394
|
+
seg('Clients', 'clients'),
|
|
395
|
+
seg('Sessions', 'sessions'),
|
|
396
|
+
seg('Events', 'events'),
|
|
397
|
+
[' (Tab to switch)', C.dim],
|
|
398
|
+
]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private _buildHints(selected: ControlPlaneItem | undefined): ReadonlyArray<{ keys: string; label: string }> {
|
|
402
|
+
if (this.filterActive) {
|
|
403
|
+
return [
|
|
404
|
+
{ keys: 'type', label: `filter ${this.focusSection}` },
|
|
405
|
+
{ keys: 'Enter', label: 'apply' },
|
|
406
|
+
{ keys: 'Esc', label: 'clear' },
|
|
407
|
+
];
|
|
193
408
|
}
|
|
409
|
+
const hints: Array<{ keys: string; label: string }> = [
|
|
410
|
+
{ keys: 'Up/Down', label: `select ${this.focusSection.slice(0, -1)}` },
|
|
411
|
+
{ keys: 'Tab', label: 'section' },
|
|
412
|
+
];
|
|
413
|
+
if (
|
|
414
|
+
this.focusSection === 'approvals'
|
|
415
|
+
&& this.approvalBroker
|
|
416
|
+
&& selected?.kind === 'approval'
|
|
417
|
+
&& (selected.approval.status === 'pending' || selected.approval.status === 'claimed')
|
|
418
|
+
) {
|
|
419
|
+
hints.push({ keys: 'a', label: 'approve' }, { keys: 'd', label: 'deny' });
|
|
420
|
+
}
|
|
421
|
+
hints.push({ keys: '/', label: 'filter' });
|
|
422
|
+
return hints;
|
|
423
|
+
}
|
|
194
424
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
425
|
+
private _buildDetailLines(width: number, item: ControlPlaneItem): Line[] {
|
|
426
|
+
if (item.kind === 'client') {
|
|
427
|
+
const client = item.client;
|
|
428
|
+
const rows: Line[] = [
|
|
429
|
+
buildPanelLine(width, [[' Client: ', C.label], [client.label, C.value], [' Kind: ', C.label], [client.kind, C.info]]),
|
|
430
|
+
buildPanelLine(width, [[' Transport: ', C.label], [client.transport, C.value], [' Connected: ', C.label], [client.connected ? 'yes' : 'no', client.connected ? C.good : C.warn]]),
|
|
431
|
+
buildPanelLine(width, [[' Route: ', C.label], [client.routeId ?? 'n/a', C.dim], [' Session: ', C.label], [client.sessionId ?? 'n/a', C.dim]]),
|
|
432
|
+
buildPanelLine(width, [[' Last seen: ', C.label], [formatTime(client.lastSeenAt), C.dim], [' Remote: ', C.label], [truncateDisplay(client.remoteAddress ?? 'n/a', Math.max(0, width - 36)), C.dim]]),
|
|
433
|
+
];
|
|
434
|
+
return buildDetailBlock(width, `Client · ${client.label}`, rows, C);
|
|
206
435
|
}
|
|
207
436
|
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
437
|
+
if (item.kind === 'approval') {
|
|
438
|
+
const approval = item.approval;
|
|
439
|
+
const rows: Line[] = [
|
|
440
|
+
buildPanelLine(width, [[' Approval: ', C.label], [approval.id, C.value], [' Status: ', C.label], [approval.status, approvalStatusColor(approval.status)]]),
|
|
441
|
+
buildPanelLine(width, [[' Tool: ', C.label], [approval.request.tool, C.info], [' Risk: ', C.label], [approval.request.analysis.riskLevel, C.warn]]),
|
|
442
|
+
buildPanelLine(width, [[' Session: ', C.label], [approval.sessionId ?? 'n/a', C.dim], [' Route: ', C.label], [approval.routeId ?? 'n/a', C.dim]]),
|
|
443
|
+
buildPanelLine(width, [[' Requested: ', C.label], [formatTime(approval.createdAt), C.dim], [' Updated: ', C.label], [formatTime(approval.updatedAt), C.dim]]),
|
|
444
|
+
];
|
|
445
|
+
if (approval.decision) {
|
|
446
|
+
rows.push(buildPanelLine(width, [[' Decision: ', C.label], [approval.decision.approved ? 'approved' : 'denied', approval.decision.approved ? C.good : C.bad]]));
|
|
447
|
+
}
|
|
448
|
+
return buildDetailBlock(width, `Approval · ${approval.request.tool}`, rows, C);
|
|
217
449
|
}
|
|
218
|
-
footerLines.push(
|
|
219
|
-
this.filterActive
|
|
220
|
-
? buildKeyboardHints(width, [
|
|
221
|
-
{ keys: 'type', label: 'filter clients' },
|
|
222
|
-
{ keys: 'Enter', label: 'apply' },
|
|
223
|
-
{ keys: 'Esc', label: 'clear' },
|
|
224
|
-
], C)
|
|
225
|
-
: buildKeyboardHints(width, [
|
|
226
|
-
{ keys: 'Up/Down', label: 'select client' },
|
|
227
|
-
{ keys: '/', label: 'filter' },
|
|
228
|
-
], C),
|
|
229
|
-
);
|
|
230
450
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
451
|
+
if (item.kind === 'session') {
|
|
452
|
+
const session = item.session;
|
|
453
|
+
const rows: Line[] = [
|
|
454
|
+
buildPanelLine(width, [[' Session: ', C.label], [session.title, C.value], [' Status: ', C.label], [session.status, session.status === 'active' ? C.good : C.dim]]),
|
|
455
|
+
buildPanelLine(width, [[' Kind: ', C.label], [session.kind, C.info], [' Messages: ', C.label], [String(session.messageCount), C.value]]),
|
|
456
|
+
buildPanelLine(width, [[' Active agent: ', C.label], [session.activeAgentId ?? 'none (unattached)', session.activeAgentId ? C.good : C.dim], [' Last agent: ', C.label], [session.lastAgentId ?? 'n/a', C.dim]]),
|
|
457
|
+
buildPanelLine(width, [[' Pending inputs: ', C.label], [String(session.pendingInputCount), session.pendingInputCount > 0 ? C.warn : C.dim], [' Last activity: ', C.label], [formatTime(session.lastActivityAt), C.dim]]),
|
|
458
|
+
];
|
|
459
|
+
return buildDetailBlock(width, `Session · ${session.title}`, rows, C);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const event = item.event;
|
|
463
|
+
const payload = typeof event.payload === 'string' ? event.payload : (JSON.stringify(event.payload) ?? '');
|
|
464
|
+
const rows: Line[] = [
|
|
465
|
+
buildPanelLine(width, [[' Event: ', C.label], [event.event, C.info], [' At: ', C.label], [formatTime(event.createdAt), C.dim]]),
|
|
466
|
+
buildPanelLine(width, [[' Payload: ', C.label], [truncateDisplay(payload, Math.max(0, width - 12)), C.dim]]),
|
|
467
|
+
];
|
|
468
|
+
return buildDetailBlock(width, `Event · ${event.event}`, rows, C);
|
|
236
469
|
}
|
|
237
470
|
}
|