@pellux/goodvibes-tui 0.18.19 → 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 +170 -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-routes.ts +10 -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 -74
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/base-panel.ts +71 -0
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/confirm-state.ts +61 -0
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/git-panel.ts +9 -0
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/knowledge-panel.ts +63 -14
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/marketplace-panel.ts +19 -12
- 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 +71 -16
- 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/skills-panel.ts +44 -0
- package/src/panels/subscription-panel.ts +69 -80
- 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/renderer/status-token.ts +71 -0
- 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 { UiCommunicationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
4
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
5
|
import {
|
|
@@ -11,10 +10,9 @@ import {
|
|
|
11
10
|
buildPanelLine,
|
|
12
11
|
buildPanelWorkspace,
|
|
13
12
|
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
|
|
15
|
-
type PanelWorkspaceSection,
|
|
13
|
+
type PanelPalette,
|
|
16
14
|
} from './polish.ts';
|
|
17
|
-
import {
|
|
15
|
+
import { createEmptyLine } from '../types/grid.ts';
|
|
18
16
|
|
|
19
17
|
const C = {
|
|
20
18
|
...DEFAULT_PANEL_PALETTE,
|
|
@@ -26,11 +24,11 @@ const C = {
|
|
|
26
24
|
selectBg: '#0f172a',
|
|
27
25
|
} as const;
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
type CommunicationRecord = UiCommunicationSnapshot['records'][number];
|
|
28
|
+
|
|
29
|
+
export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord> {
|
|
30
30
|
private readonly readModel?: UiReadModel<UiCommunicationSnapshot>;
|
|
31
31
|
private readonly unsub: (() => void) | null;
|
|
32
|
-
private selectedIndex = 0;
|
|
33
|
-
private scrollOffset = 0;
|
|
34
32
|
|
|
35
33
|
public constructor(readModel?: UiReadModel<UiCommunicationSnapshot>) {
|
|
36
34
|
super('communication', 'Communication', 'Y', 'monitoring');
|
|
@@ -42,31 +40,40 @@ export class CommunicationPanel extends BasePanel {
|
|
|
42
40
|
this.unsub?.();
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (records.length === 0) return false;
|
|
48
|
-
if (key === 'up' || key === 'k') {
|
|
49
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
50
|
-
this.markDirty();
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
if (key === 'down' || key === 'j') {
|
|
54
|
-
this.selectedIndex = Math.min(records.length - 1, this.selectedIndex + 1);
|
|
55
|
-
this.markDirty();
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
43
|
+
protected override getPalette(): PanelPalette {
|
|
44
|
+
return C;
|
|
59
45
|
}
|
|
60
46
|
|
|
61
|
-
|
|
47
|
+
protected getItems(): readonly CommunicationRecord[] {
|
|
62
48
|
if (!this.readModel) return [];
|
|
63
|
-
return
|
|
49
|
+
return this.readModel.getSnapshot().records;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected renderItem(record: CommunicationRecord, index: number, selected: boolean, width: number): Line {
|
|
53
|
+
const bg = selected ? C.selectBg : undefined;
|
|
54
|
+
const color = record.status === 'blocked' ? C.error : record.status === 'delivered' ? C.ok : C.info;
|
|
55
|
+
return buildPanelLine(width, [
|
|
56
|
+
[' ', C.label, bg],
|
|
57
|
+
[record.status.padEnd(10), color, bg],
|
|
58
|
+
[` ${record.kind.padEnd(10)}`, C.info, bg],
|
|
59
|
+
[` ${truncateDisplay(`${record.fromId} -> ${record.toId}`, 28).padEnd(28)}`, C.value, bg],
|
|
60
|
+
[` ${truncateDisplay(record.content, Math.max(0, width - 53))}`, C.dim, bg],
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected override getEmptyStateMessage(): string {
|
|
65
|
+
return ' No structured communication recorded yet.';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
69
|
+
return [
|
|
70
|
+
{ command: '/orchestration', summary: 'review graphs and recursive agent activity' },
|
|
71
|
+
{ command: '/communication', summary: 'reopen this workspace once the runtime emits message traffic' },
|
|
72
|
+
];
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
public render(width: number, height: number): Line[] {
|
|
67
|
-
this.needsRender = false;
|
|
68
76
|
const intro = 'Structured agent communication, routing policy outcomes, and delivery status across orchestration trees.';
|
|
69
|
-
const footerLines = [buildPanelLine(width, [[' Up/Down move through messages', C.dim]])];
|
|
70
77
|
|
|
71
78
|
if (!this.readModel) {
|
|
72
79
|
const workspace = buildPanelWorkspace(width, height, {
|
|
@@ -88,104 +95,58 @@ export class CommunicationPanel extends BasePanel {
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
const snapshot = this.readModel.getSnapshot();
|
|
91
|
-
const records = this.
|
|
98
|
+
const records = this.getItems();
|
|
99
|
+
|
|
100
|
+
const postureLines: Line[] = [
|
|
101
|
+
buildPanelLine(width, [[' Communication posture', C.label]]),
|
|
102
|
+
buildKeyValueLine(width, [
|
|
103
|
+
{ label: 'sent', value: String(snapshot.totalSent), valueColor: snapshot.totalSent > 0 ? C.info : C.dim },
|
|
104
|
+
{ label: 'delivered', value: String(snapshot.totalDelivered), valueColor: snapshot.totalDelivered > 0 ? C.ok : C.dim },
|
|
105
|
+
{ label: 'blocked', value: String(snapshot.totalBlocked), valueColor: snapshot.totalBlocked > 0 ? C.error : C.dim },
|
|
106
|
+
], C),
|
|
107
|
+
buildGuidanceLine(width, '/orchestration', 'inspect recursive routing, message handoff, and blocked broadcast posture', C),
|
|
108
|
+
];
|
|
92
109
|
|
|
93
110
|
if (records.length === 0) {
|
|
94
|
-
|
|
111
|
+
return this.renderList(width, height, {
|
|
95
112
|
title: 'Communication Control Room',
|
|
96
|
-
|
|
97
|
-
sections: [{
|
|
98
|
-
title: 'Communication posture',
|
|
99
|
-
lines: [
|
|
100
|
-
buildKeyValueLine(width, [
|
|
101
|
-
{ label: 'sent', value: String(snapshot.totalSent), valueColor: snapshot.totalSent > 0 ? C.info : C.dim },
|
|
102
|
-
{ label: 'delivered', value: String(snapshot.totalDelivered), valueColor: snapshot.totalDelivered > 0 ? C.ok : C.dim },
|
|
103
|
-
{ label: 'blocked', value: String(snapshot.totalBlocked), valueColor: snapshot.totalBlocked > 0 ? C.error : C.dim },
|
|
104
|
-
], C),
|
|
105
|
-
buildGuidanceLine(width, '/communication', 'review structured message flow, delivery posture, and blocked routing decisions', C),
|
|
106
|
-
...buildEmptyState(
|
|
107
|
-
width,
|
|
108
|
-
' No structured communication recorded yet.',
|
|
109
|
-
'Messages, escalations, findings, and handoffs will appear here once orchestration starts routing them through the communication policy.',
|
|
110
|
-
[
|
|
111
|
-
{ command: '/orchestration', summary: 'review graphs and recursive agent activity' },
|
|
112
|
-
{ command: '/communication', summary: 'reopen this workspace once the runtime emits message traffic' },
|
|
113
|
-
],
|
|
114
|
-
C,
|
|
115
|
-
),
|
|
116
|
-
],
|
|
117
|
-
}],
|
|
118
|
-
palette: C,
|
|
113
|
+
header: postureLines,
|
|
119
114
|
});
|
|
120
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
121
|
-
return workspace;
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
this.
|
|
125
|
-
const
|
|
117
|
+
this.clampSelection();
|
|
118
|
+
const selected = records[this.selectedIndex];
|
|
119
|
+
|
|
120
|
+
// Update posture with selected info
|
|
121
|
+
const postureWithSelected: Line[] = [
|
|
122
|
+
buildPanelLine(width, [[' Communication posture', C.label]]),
|
|
126
123
|
buildKeyValueLine(width, [
|
|
127
124
|
{ label: 'sent', value: String(snapshot.totalSent), valueColor: snapshot.totalSent > 0 ? C.info : C.dim },
|
|
128
125
|
{ label: 'delivered', value: String(snapshot.totalDelivered), valueColor: snapshot.totalDelivered > 0 ? C.ok : C.dim },
|
|
129
126
|
{ label: 'blocked', value: String(snapshot.totalBlocked), valueColor: snapshot.totalBlocked > 0 ? C.error : C.dim },
|
|
130
|
-
{ label: 'selected', value: `${
|
|
127
|
+
{ label: 'selected', value: `${selected?.fromId ?? 'n/a'} -> ${selected?.toId ?? 'n/a'}`, valueColor: C.value },
|
|
131
128
|
], C),
|
|
132
129
|
buildGuidanceLine(width, '/orchestration', 'inspect recursive routing, message handoff, and blocked broadcast posture', C),
|
|
133
130
|
];
|
|
134
131
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
const footerLines: Line[] = [];
|
|
133
|
+
if (selected) {
|
|
134
|
+
footerLines.push(
|
|
135
|
+
buildPanelLine(width, [[' Route: ', C.label], [`${selected.scope} / ${selected.kind}`, C.value], [' Status: ', C.label], [selected.status, selected.status === 'blocked' ? C.error : C.ok]]),
|
|
136
|
+
buildPanelLine(width, [[' From: ', C.label], [selected.fromId, C.value], [' To: ', C.label], [selected.toId, C.value]]),
|
|
137
|
+
buildPanelLine(width, [[' Roles: ', C.label], [`${selected.fromRole ?? 'unknown'} -> ${selected.toRole ?? 'unknown'}`, C.dim]]),
|
|
138
|
+
);
|
|
139
|
+
if (selected.reason) {
|
|
140
|
+
footerLines.push(buildPanelLine(width, [[' Reason: ', C.label], [truncateDisplay(selected.reason, Math.max(0, width - 11)), C.warn]]));
|
|
141
|
+
}
|
|
142
|
+
footerLines.push(...buildBodyText(width, ` Content: ${selected.content}`, C));
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
const postureSection: PanelWorkspaceSection = { title: 'Communication posture', lines: postureLines };
|
|
146
|
-
const detailSection: PanelWorkspaceSection = { title: 'Selected Message', lines: detailLines };
|
|
147
|
-
const rawOverviewLines: Line[] = records.map((record, absolute) => {
|
|
148
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
149
|
-
const color = record.status === 'blocked' ? C.error : record.status === 'delivered' ? C.ok : C.info;
|
|
150
|
-
return buildPanelLine(width, [
|
|
151
|
-
[' ', C.label, bg],
|
|
152
|
-
[record.status.padEnd(10), color, bg],
|
|
153
|
-
[` ${record.kind.padEnd(10)}`, C.info, bg],
|
|
154
|
-
[` ${truncateDisplay(`${record.fromId} -> ${record.toId}`, 28).padEnd(28)}`, C.value, bg],
|
|
155
|
-
[` ${truncateDisplay(record.content, Math.max(0, width - 53))}`, C.dim, bg],
|
|
156
|
-
]);
|
|
157
|
-
});
|
|
158
|
-
const resolvedMessagesSection = resolvePrimaryScrollableSection(width, height, {
|
|
159
|
-
intro,
|
|
160
|
-
footerLines,
|
|
161
|
-
palette: C,
|
|
162
|
-
beforeSections: [postureSection],
|
|
163
|
-
section: {
|
|
164
|
-
title: 'Recent Messages',
|
|
165
|
-
scrollableLines: rawOverviewLines,
|
|
166
|
-
selectedIndex: this.selectedIndex,
|
|
167
|
-
scrollOffset: this.scrollOffset,
|
|
168
|
-
guardRows: 1,
|
|
169
|
-
minRows: 4,
|
|
170
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
171
|
-
},
|
|
172
|
-
afterSections: [detailSection],
|
|
173
|
-
});
|
|
174
|
-
this.scrollOffset = resolvedMessagesSection.scrollOffset;
|
|
144
|
+
footerLines.push(buildPanelLine(width, [[' Up/Down move through messages', C.dim]]));
|
|
175
145
|
|
|
176
|
-
|
|
177
|
-
postureSection,
|
|
178
|
-
resolvedMessagesSection.section,
|
|
179
|
-
detailSection,
|
|
180
|
-
];
|
|
181
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
146
|
+
return this.renderList(width, height, {
|
|
182
147
|
title: 'Communication Control Room',
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
footerLines,
|
|
186
|
-
palette: C,
|
|
148
|
+
header: postureWithSelected,
|
|
149
|
+
footer: footerLines,
|
|
187
150
|
});
|
|
188
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
189
|
-
return lines.slice(0, height);
|
|
190
151
|
}
|
|
191
152
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// useConfirmState<T> — reusable inline y/n confirmation helper
|
|
3
|
+
//
|
|
4
|
+
// Pattern (chosen over ConfirmableListPanel base class):
|
|
5
|
+
// - Composable: any panel holds a ConfirmState field, not a new base class
|
|
6
|
+
// - Identical y/n UX everywhere: y confirms, n/Esc cancels, any other key
|
|
7
|
+
// is absorbed (does nothing) while confirm is active
|
|
8
|
+
// - Render: caller calls renderConfirmLines(width, state) to get the two
|
|
9
|
+
// lines that replace the normal content area when confirming
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
import type { Line } from '../types/grid.ts';
|
|
13
|
+
import { buildPanelLine } from './polish.ts';
|
|
14
|
+
import { DEFAULT_PANEL_PALETTE } from './polish.ts';
|
|
15
|
+
|
|
16
|
+
export interface ConfirmState<T = string> {
|
|
17
|
+
/** The subject of the confirmation (e.g. item name or id). */
|
|
18
|
+
readonly subject: T;
|
|
19
|
+
/** Human-readable label for the item being destroyed. */
|
|
20
|
+
readonly label: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Call this from a panel's handleInput() BEFORE any other key handling.
|
|
25
|
+
*
|
|
26
|
+
* Returns:
|
|
27
|
+
* - `'confirmed'` — user pressed y; caller must execute the action and
|
|
28
|
+
* clear state (set confirm to null)
|
|
29
|
+
* - `'cancelled'` — user pressed n or Esc; caller must clear state
|
|
30
|
+
* - `'absorbed'` — any other key while confirm is active; caller returns true
|
|
31
|
+
* - `'inactive'` — no confirm pending; caller continues normal dispatch
|
|
32
|
+
*/
|
|
33
|
+
export function handleConfirmInput<T = string>(
|
|
34
|
+
confirm: ConfirmState<T> | null,
|
|
35
|
+
key: string,
|
|
36
|
+
): 'confirmed' | 'cancelled' | 'absorbed' | 'inactive' {
|
|
37
|
+
if (!confirm) return 'inactive';
|
|
38
|
+
if (key === 'y') return 'confirmed';
|
|
39
|
+
if (key === 'n' || key === 'escape') return 'cancelled';
|
|
40
|
+
return 'absorbed';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the two confirmation lines to show in place of the normal list body.
|
|
45
|
+
* Callers embed these lines in a workspace section titled 'Confirmation'.
|
|
46
|
+
*/
|
|
47
|
+
export function renderConfirmLines<T = string>(width: number, state: ConfirmState<T>): Line[] {
|
|
48
|
+
const palette = DEFAULT_PANEL_PALETTE;
|
|
49
|
+
return [
|
|
50
|
+
buildPanelLine(width, [[
|
|
51
|
+
` Delete "${state.label}"?`,
|
|
52
|
+
palette.warn,
|
|
53
|
+
]]),
|
|
54
|
+
buildPanelLine(width, [
|
|
55
|
+
[' y', palette.info],
|
|
56
|
+
[' confirm delete', palette.dim],
|
|
57
|
+
[' n / Esc', palette.info],
|
|
58
|
+
[' cancel', palette.dim],
|
|
59
|
+
]),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
@@ -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 type { UiControlPlaneSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
5
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
6
|
import {
|
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
buildPanelLine,
|
|
11
11
|
buildPanelWorkspace,
|
|
12
12
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
|
|
14
|
-
type PanelWorkspaceSection,
|
|
13
|
+
type PanelPalette,
|
|
15
14
|
} from './polish.ts';
|
|
16
15
|
|
|
17
16
|
const C = {
|
|
@@ -37,10 +36,10 @@ function connectionColor(state: string): string {
|
|
|
37
36
|
return C.dim;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
type ControlPlaneClient = UiControlPlaneSnapshot['clients'][number];
|
|
40
|
+
|
|
41
|
+
export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
41
42
|
private readonly unsub: (() => void) | null;
|
|
42
|
-
private selectedIndex = 0;
|
|
43
|
-
private scrollOffset = 0;
|
|
44
43
|
|
|
45
44
|
public constructor(private readonly readModel?: UiReadModel<UiControlPlaneSnapshot>) {
|
|
46
45
|
super('control-plane', 'Control Plane', 'C', 'monitoring');
|
|
@@ -51,29 +50,38 @@ export class ControlPlanePanel extends BasePanel {
|
|
|
51
50
|
this.unsub?.();
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (clients.length === 0) return false;
|
|
57
|
-
if (key === 'up' || key === 'k') {
|
|
58
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
59
|
-
this.markDirty();
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
if (key === 'down' || key === 'j') {
|
|
63
|
-
this.selectedIndex = Math.min(clients.length - 1, this.selectedIndex + 1);
|
|
64
|
-
this.markDirty();
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
53
|
+
protected override getPalette(): PanelPalette {
|
|
54
|
+
return C;
|
|
68
55
|
}
|
|
69
56
|
|
|
70
|
-
|
|
57
|
+
protected getItems(): readonly ControlPlaneClient[] {
|
|
71
58
|
if (!this.readModel) return [];
|
|
72
|
-
return
|
|
59
|
+
return this.readModel.getSnapshot().clients;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected renderItem(client: ControlPlaneClient, _index: number, selected: boolean, width: number): Line {
|
|
63
|
+
const bg = selected ? C.selectBg : undefined;
|
|
64
|
+
return buildPanelLine(width, [
|
|
65
|
+
[' ', C.label, bg],
|
|
66
|
+
[client.kind.padEnd(10), C.info, bg],
|
|
67
|
+
[` ${truncateDisplay(client.label, 20).padEnd(20)}`, C.value, bg],
|
|
68
|
+
[` ${client.transport.padEnd(12)}`, C.dim, bg],
|
|
69
|
+
[` ${truncateDisplay(formatTime(client.lastSeenAt), Math.max(0, width - 46))}`, C.dim, bg],
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected override getEmptyStateMessage(): string {
|
|
74
|
+
return ' No control-plane activity recorded.';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
78
|
+
return [
|
|
79
|
+
{ command: '/cockpit', summary: 'watch operator posture from the terminal' },
|
|
80
|
+
{ command: '/schedule list', summary: 'run automation that creates surface and daemon traffic' },
|
|
81
|
+
];
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
public render(width: number, height: number): Line[] {
|
|
76
|
-
this.needsRender = false;
|
|
77
85
|
const intro = 'Shared daemon control plane state, live clients, approval pressure, and recent omnichannel session posture.';
|
|
78
86
|
|
|
79
87
|
if (!this.readModel) {
|
|
@@ -99,168 +107,104 @@ export class ControlPlanePanel extends BasePanel {
|
|
|
99
107
|
const approvals = snapshot.approvals;
|
|
100
108
|
const sessions = snapshot.sessions;
|
|
101
109
|
const recentEvents = snapshot.recentEvents;
|
|
102
|
-
const clients = this.
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
buildGuidanceLine(width, '/cockpit', 'use the web operator surface or daemon APIs for direct interventions while this panel tracks overall posture', C),
|
|
120
|
-
],
|
|
121
|
-
};
|
|
110
|
+
const clients = this.getItems();
|
|
111
|
+
|
|
112
|
+
const headerLines: Line[] = [
|
|
113
|
+
buildKeyValueLine(width, [
|
|
114
|
+
{ label: 'state', value: snapshot.connectionState, valueColor: connectionColor(snapshot.connectionState) },
|
|
115
|
+
{ label: 'clients', value: String(snapshot.activeClientIds.length), valueColor: snapshot.activeClientIds.length > 0 ? C.ok : C.dim },
|
|
116
|
+
{ label: 'requests', value: String(snapshot.requestCount), valueColor: snapshot.requestCount > 0 ? C.info : C.dim },
|
|
117
|
+
{ label: 'errors', value: String(snapshot.errorCount), valueColor: snapshot.errorCount > 0 ? C.error : C.dim },
|
|
118
|
+
], C),
|
|
119
|
+
buildKeyValueLine(width, [
|
|
120
|
+
{ label: 'host', value: `${snapshot.host}:${snapshot.port}`, valueColor: C.value },
|
|
121
|
+
{ label: 'approvals', value: String(approvals.filter((entry) => entry.status === 'pending').length), valueColor: approvals.some((entry) => entry.status === 'pending') ? C.warn : C.dim },
|
|
122
|
+
{ label: 'sessions', value: String(sessions.length), valueColor: sessions.length > 0 ? C.info : C.dim },
|
|
123
|
+
{ label: 'events', value: String(recentEvents.length), valueColor: recentEvents.length > 0 ? C.info : C.dim },
|
|
124
|
+
], C),
|
|
125
|
+
buildGuidanceLine(width, '/cockpit', 'use the web operator surface or daemon APIs for direct interventions while this panel tracks overall posture', C),
|
|
126
|
+
];
|
|
122
127
|
|
|
123
128
|
if (clients.length === 0 && approvals.length === 0 && sessions.length === 0) {
|
|
124
|
-
|
|
129
|
+
return this.renderList(width, height, {
|
|
125
130
|
title: 'Control Plane',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
summarySection,
|
|
129
|
-
{
|
|
130
|
-
lines: buildEmptyState(
|
|
131
|
-
width,
|
|
132
|
-
' No control-plane activity recorded.',
|
|
133
|
-
'Start the daemon, connect a surface, or trigger an approval to populate this operator panel.',
|
|
134
|
-
[
|
|
135
|
-
{ command: '/cockpit', summary: 'watch operator posture from the terminal' },
|
|
136
|
-
{ command: '/schedule list', summary: 'run automation that creates surface and daemon traffic' },
|
|
137
|
-
],
|
|
138
|
-
C,
|
|
139
|
-
),
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
palette: C,
|
|
131
|
+
header: headerLines,
|
|
132
|
+
emptyMessage: ' No control-plane activity recorded.',
|
|
143
133
|
});
|
|
144
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
145
|
-
return workspace;
|
|
146
134
|
}
|
|
147
135
|
|
|
148
|
-
this.
|
|
136
|
+
this.clampSelection();
|
|
149
137
|
const selected = clients[this.selectedIndex];
|
|
150
138
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
title: 'Selected Client',
|
|
183
|
-
lines: [buildPanelLine(width, [[' No connected client selected.', C.dim]])],
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const approvalsSection: PanelWorkspaceSection = {
|
|
187
|
-
title: 'Approvals',
|
|
188
|
-
lines: approvals.length > 0
|
|
189
|
-
? approvals.slice(0, 6).map((approval) => buildPanelLine(width, [
|
|
190
|
-
[' ', C.label],
|
|
191
|
-
[approval.status.padEnd(10), approval.status === 'pending' ? C.warn : approval.status === 'approved' ? C.ok : approval.status === 'denied' ? C.error : C.dim],
|
|
192
|
-
[` ${truncateDisplay(approval.request.tool, 16).padEnd(16)}`, C.value],
|
|
193
|
-
[` ${truncateDisplay(approval.sessionId ?? approval.id, Math.max(0, width - 30))}`, C.dim],
|
|
194
|
-
]))
|
|
195
|
-
: [buildPanelLine(width, [[' No recent approvals.', C.dim]])],
|
|
196
|
-
};
|
|
139
|
+
const footerLines: Line[] = [];
|
|
140
|
+
if (selected) {
|
|
141
|
+
footerLines.push(
|
|
142
|
+
buildPanelLine(width, [
|
|
143
|
+
[' Client: ', C.label],
|
|
144
|
+
[selected.label, C.value],
|
|
145
|
+
[' Kind: ', C.label],
|
|
146
|
+
[selected.kind, C.info],
|
|
147
|
+
]),
|
|
148
|
+
buildPanelLine(width, [
|
|
149
|
+
[' Transport: ', C.label],
|
|
150
|
+
[selected.transport, C.value],
|
|
151
|
+
[' Connected: ', C.label],
|
|
152
|
+
[selected.connected ? 'yes' : 'no', selected.connected ? C.ok : C.warn],
|
|
153
|
+
]),
|
|
154
|
+
buildPanelLine(width, [
|
|
155
|
+
[' Route: ', C.label],
|
|
156
|
+
[selected.routeId ?? 'n/a', C.dim],
|
|
157
|
+
[' Session: ', C.label],
|
|
158
|
+
[selected.sessionId ?? 'n/a', C.dim],
|
|
159
|
+
]),
|
|
160
|
+
buildPanelLine(width, [
|
|
161
|
+
[' Last seen: ', C.label],
|
|
162
|
+
[formatTime(selected.lastSeenAt), C.dim],
|
|
163
|
+
[' Remote: ', C.label],
|
|
164
|
+
[truncateDisplay(selected.remoteAddress ?? 'n/a', Math.max(0, width - 36)), C.dim],
|
|
165
|
+
]),
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
footerLines.push(buildPanelLine(width, [[' No connected client selected.', C.dim]]));
|
|
169
|
+
}
|
|
197
170
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
171
|
+
if (approvals.length > 0) {
|
|
172
|
+
footerLines.push(
|
|
173
|
+
...approvals.slice(0, 6).map((approval) => buildPanelLine(width, [
|
|
174
|
+
[' ', C.label],
|
|
175
|
+
[approval.status.padEnd(10), approval.status === 'pending' ? C.warn : approval.status === 'approved' ? C.ok : approval.status === 'denied' ? C.error : C.dim],
|
|
176
|
+
[` ${truncateDisplay(approval.request.tool, 16).padEnd(16)}`, C.value],
|
|
177
|
+
[` ${truncateDisplay(approval.sessionId ?? approval.id, Math.max(0, width - 30))}`, C.dim],
|
|
178
|
+
])),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
209
181
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
182
|
+
if (sessions.length > 0) {
|
|
183
|
+
footerLines.push(
|
|
184
|
+
...sessions.slice(0, 6).map((session) => buildPanelLine(width, [
|
|
185
|
+
[' ', C.label],
|
|
186
|
+
[session.status.padEnd(10), session.status === 'active' ? C.ok : C.dim],
|
|
187
|
+
[` ${truncateDisplay(session.title, 20).padEnd(20)}`, C.value],
|
|
188
|
+
[` ${truncateDisplay(session.activeAgentId ?? session.id, Math.max(0, width - 34))}`, C.dim],
|
|
189
|
+
])),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
220
192
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
[' ', C.label, bg],
|
|
232
|
-
[client.kind.padEnd(10), C.info, bg],
|
|
233
|
-
[` ${truncateDisplay(client.label, 20).padEnd(20)}`, C.value, bg],
|
|
234
|
-
[` ${client.transport.padEnd(12)}`, C.dim, bg],
|
|
235
|
-
[` ${truncateDisplay(formatTime(client.lastSeenAt), Math.max(0, width - 46))}`, C.dim, bg],
|
|
236
|
-
]);
|
|
237
|
-
}),
|
|
238
|
-
selectedIndex: this.selectedIndex,
|
|
239
|
-
scrollOffset: this.scrollOffset,
|
|
240
|
-
guardRows: 1,
|
|
241
|
-
minRows: 4,
|
|
242
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
243
|
-
},
|
|
244
|
-
afterSections: [detailSection, approvalsSection, sessionsSection, eventsSection],
|
|
245
|
-
});
|
|
246
|
-
this.scrollOffset = resolvedClients.scrollOffset;
|
|
193
|
+
if (recentEvents.length > 0) {
|
|
194
|
+
footerLines.push(
|
|
195
|
+
...recentEvents.slice(0, 6).map((event) => buildPanelLine(width, [
|
|
196
|
+
[' ', C.label],
|
|
197
|
+
[truncateDisplay(event.event, 16).padEnd(16), C.info],
|
|
198
|
+
[` ${truncateDisplay(typeof event.payload === 'string' ? event.payload : JSON.stringify(event.payload) ?? '', Math.max(0, width - 19))}`, C.dim],
|
|
199
|
+
])),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
footerLines.push(buildPanelLine(width, [[' Up/Down move through connected clients', C.dim]]));
|
|
247
203
|
|
|
248
|
-
|
|
249
|
-
summarySection,
|
|
250
|
-
resolvedClients.section,
|
|
251
|
-
detailSection,
|
|
252
|
-
approvalsSection,
|
|
253
|
-
sessionsSection,
|
|
254
|
-
eventsSection,
|
|
255
|
-
];
|
|
256
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
204
|
+
return this.renderList(width, height, {
|
|
257
205
|
title: 'Control Plane',
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through connected clients', C.dim]])],
|
|
261
|
-
palette: C,
|
|
206
|
+
header: headerLines,
|
|
207
|
+
footer: footerLines,
|
|
262
208
|
});
|
|
263
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
264
|
-
return lines.slice(0, height);
|
|
265
209
|
}
|
|
266
210
|
}
|