@kal-elsam/kairo-runtime 0.8.0 → 0.10.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/README.md +41 -15
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +6 -6
- package/scripts/ux-prototype-tty.mjs +9 -0
- package/src/cli.js +24 -1
- package/src/global/control-plane-proposals.js +2 -1
- package/src/global/control-plane-snapshot.js +1 -1
- package/src/global/global-doctor.js +2 -0
- package/src/global/ink/cockpit/primitives.js +127 -50
- package/src/global/ink/cockpit-alerts.js +36 -0
- package/src/global/ink/cockpit-changes.js +61 -37
- package/src/global/ink/cockpit-control-center.js +79 -53
- package/src/global/ink/cockpit-controller.js +98 -15
- package/src/global/ink/cockpit-enter.js +1 -0
- package/src/global/ink/cockpit-focus.js +4 -2
- package/src/global/ink/cockpit-models.js +100 -51
- package/src/global/ink/cockpit-palette.js +109 -0
- package/src/global/ink/cockpit-path-label.js +19 -0
- package/src/global/ink/cockpit-recovery.js +84 -18
- package/src/global/ink/cockpit-reviews.js +14 -10
- package/src/global/ink/cockpit-runs.js +13 -4
- package/src/global/ink/cockpit-settings.js +194 -0
- package/src/global/ink/cockpit-usage.js +111 -0
- package/src/global/ink/cockpit-views.js +119 -116
- package/src/global/ink/orchestrator-app.js +169 -46
- package/src/global/ink/orchestrator-state.js +24 -14
- package/src/global/ink/setup-app.js +55 -72
- package/src/global/ink/setup-state.js +16 -0
- package/src/global/ink/theme.js +27 -0
- package/src/global/ink/use-orchestrator-data.js +58 -0
- package/src/global/ink/ux/live-activity.js +194 -0
- package/src/global/ink/ux/live-alerts.js +159 -0
- package/src/global/ink/ux/live-governance.js +195 -0
- package/src/global/ink/ux/live-orchestration.js +188 -0
- package/src/global/ink/ux/live-overview.js +125 -0
- package/src/global/ink/ux/live-settings.js +189 -0
- package/src/global/ink/ux/live-setup.js +160 -0
- package/src/global/ink/ux/live-usage.js +51 -0
- package/src/global/ink/ux/semantic.js +84 -0
- package/src/global/ink/ux/task-flow-app.js +85 -0
- package/src/global/ink/ux/task-flow.js +173 -0
- package/src/global/orchestrator.js +37 -20
- package/src/global/paths.js +3 -0
- package/src/global/runtime/alerts/alert-store.js +216 -0
- package/src/global/runtime/alerts/alert-types.js +59 -0
- package/src/global/runtime/alerts/alert-validate.js +117 -0
- package/src/global/runtime/monitor/monitor-cli.js +62 -0
- package/src/global/runtime/monitor/monitor-platform.js +95 -0
- package/src/global/runtime/monitor/monitor.js +249 -0
|
@@ -33,8 +33,14 @@ import {
|
|
|
33
33
|
listRecoverySnapshots,
|
|
34
34
|
reduceRecoveryAction
|
|
35
35
|
} from "./cockpit-recovery.js";
|
|
36
|
+
import {
|
|
37
|
+
createSettingsActionState,
|
|
38
|
+
listCuratedIntegrations,
|
|
39
|
+
reduceSettingsAction
|
|
40
|
+
} from "./cockpit-settings.js";
|
|
36
41
|
import { listReviewReceipts } from "../runtime/review/review-receipts.js";
|
|
37
42
|
import { assertReceiptSecretFree } from "../runtime/review/review-validate.js";
|
|
43
|
+
import { listAlerts, resolveAlert, dismissAlert } from "../runtime/alerts/alert-store.js";
|
|
38
44
|
|
|
39
45
|
export function useOrchestratorData({
|
|
40
46
|
homeDir,
|
|
@@ -54,6 +60,7 @@ export function useOrchestratorData({
|
|
|
54
60
|
const [selectedEvents, setSelectedEvents] = useState([]);
|
|
55
61
|
const [reviews, setReviews] = useState([]);
|
|
56
62
|
const [selectedReview, setSelectedReview] = useState(null);
|
|
63
|
+
const [alerts, setAlerts] = useState(null);
|
|
57
64
|
const [statusMessage, setStatusMessage] = useState(null);
|
|
58
65
|
const [launchAgentIndex, setLaunchAgentIndex] = useState(0);
|
|
59
66
|
const [launchStep, setLaunchStep] = useState(LAUNCH_WIZARD_STEPS.AGENT);
|
|
@@ -61,6 +68,7 @@ export function useOrchestratorData({
|
|
|
61
68
|
const [launchPermissionIndex, setLaunchPermissionIndex] = useState(0);
|
|
62
69
|
const [changesAction, setChangesAction] = useState(createChangesActionState);
|
|
63
70
|
const [recoveryAction, setRecoveryAction] = useState(createRecoveryActionState);
|
|
71
|
+
const [settingsAction, setSettingsAction] = useState(createSettingsActionState);
|
|
64
72
|
|
|
65
73
|
const serializedReload = useMemo(() => createSerializedReloader(() => loadCockpitScanBundle({
|
|
66
74
|
homeDir,
|
|
@@ -90,6 +98,11 @@ export function useOrchestratorData({
|
|
|
90
98
|
setDashboard(outcome.result.dashboard);
|
|
91
99
|
setDiagnostics(outcome.result.diagnostics);
|
|
92
100
|
setSnapshot(outcome.result.snapshot);
|
|
101
|
+
try {
|
|
102
|
+
setAlerts(await listAlerts({ homeDir, limit: 50 }));
|
|
103
|
+
} catch {
|
|
104
|
+
setAlerts(null);
|
|
105
|
+
}
|
|
93
106
|
setError(null);
|
|
94
107
|
setLoading(false);
|
|
95
108
|
setRetrying(false);
|
|
@@ -149,6 +162,22 @@ export function useOrchestratorData({
|
|
|
149
162
|
});
|
|
150
163
|
};
|
|
151
164
|
|
|
165
|
+
const handleAlertTransition = async (alert, action) => {
|
|
166
|
+
if (!alert) return;
|
|
167
|
+
setBusy(true);
|
|
168
|
+
try {
|
|
169
|
+
if (action === "dismiss") await dismissAlert(alert.alertId, { homeDir });
|
|
170
|
+
else await resolveAlert(alert.alertId, { homeDir });
|
|
171
|
+
setAlerts(await listAlerts({ homeDir, limit: 50 }));
|
|
172
|
+
setStatusMessage(action === "dismiss" ? "Alert dismissed" : "Alert resolved");
|
|
173
|
+
} catch (error) {
|
|
174
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
175
|
+
setAlerts(null);
|
|
176
|
+
} finally {
|
|
177
|
+
setBusy(false);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
152
181
|
const handleLaunch = async (draft, profile, dispatch) => {
|
|
153
182
|
if (!draft.agentId || !draft.task.trim()) {
|
|
154
183
|
setError("Agent and task are required.");
|
|
@@ -366,6 +395,26 @@ export function useOrchestratorData({
|
|
|
366
395
|
await reload();
|
|
367
396
|
};
|
|
368
397
|
|
|
398
|
+
const previewSettings = (id) => {
|
|
399
|
+
setSettingsAction((prev) => reduceSettingsAction(prev, { type: "preview", id }));
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const promptConfirmSettings = () => {
|
|
403
|
+
setSettingsAction((prev) => reduceSettingsAction(prev, { type: "confirm-prompt" }));
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const confirmSettings = () => {
|
|
407
|
+
setSettingsAction((prev) => reduceSettingsAction(prev, { type: "confirm" }));
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const cancelSettings = () => {
|
|
411
|
+
setSettingsAction((prev) => reduceSettingsAction(prev, { type: "cancel" }));
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const resetSettings = () => {
|
|
415
|
+
setSettingsAction(() => createSettingsActionState());
|
|
416
|
+
};
|
|
417
|
+
|
|
369
418
|
return {
|
|
370
419
|
loading,
|
|
371
420
|
busy,
|
|
@@ -381,6 +430,7 @@ export function useOrchestratorData({
|
|
|
381
430
|
reviews,
|
|
382
431
|
selectedReview,
|
|
383
432
|
setSelectedReview,
|
|
433
|
+
alerts,
|
|
384
434
|
statusMessage,
|
|
385
435
|
launchAgentIndex,
|
|
386
436
|
setLaunchAgentIndex,
|
|
@@ -394,11 +444,14 @@ export function useOrchestratorData({
|
|
|
394
444
|
scanOptions: CONTROL_PLANE_AUTO_SCAN,
|
|
395
445
|
changesAction,
|
|
396
446
|
recoveryAction,
|
|
447
|
+
settingsAction,
|
|
448
|
+
curatedIntegrations: listCuratedIntegrations(),
|
|
397
449
|
reload,
|
|
398
450
|
resetLaunchWizard,
|
|
399
451
|
openRunDetail,
|
|
400
452
|
loadReviews,
|
|
401
453
|
openReviewDetail,
|
|
454
|
+
handleAlertTransition,
|
|
402
455
|
handleLaunch,
|
|
403
456
|
handleCancelRun,
|
|
404
457
|
previewChanges,
|
|
@@ -409,6 +462,11 @@ export function useOrchestratorData({
|
|
|
409
462
|
cancelRecovery,
|
|
410
463
|
confirmApplyRecovery,
|
|
411
464
|
rescanRecovery,
|
|
465
|
+
previewSettings,
|
|
466
|
+
promptConfirmSettings,
|
|
467
|
+
confirmSettings,
|
|
468
|
+
cancelSettings,
|
|
469
|
+
resetSettings,
|
|
412
470
|
recoverySnapshots: listRecoverySnapshots(snapshot)
|
|
413
471
|
};
|
|
414
472
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/** Live semantic Activity/Recovery. Callout=status · Confirm=action · footer=keys · snapshots=focus. */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { COCKPIT_COLORS } from "../theme.js";
|
|
5
|
+
import { formatConfirmPath } from "../cockpit-path-label.js";
|
|
6
|
+
import {
|
|
7
|
+
RECOVERY_PHASE, listRecoverySnapshots, formatWhen, formatResult, shortName
|
|
8
|
+
} from "../cockpit-recovery.js";
|
|
9
|
+
import { LAYOUT_MODES } from "../layout.js";
|
|
10
|
+
import { ActionList, Callout, Confirm, Details } from "./semantic.js";
|
|
11
|
+
import { detailsPathLimit } from "./live-governance.js";
|
|
12
|
+
|
|
13
|
+
export function activityContentLimits(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
14
|
+
return layoutMode === LAYOUT_MODES.WIDE
|
|
15
|
+
? { events: 4, snapshots: 8 }
|
|
16
|
+
: { events: 3, snapshots: 3 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function activitySnapshotLimit(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
20
|
+
return activityContentLimits(layoutMode).snapshots;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Visible window over full list; keeps `index` focused without truncating navigation. */
|
|
24
|
+
export function windowSlice(items = [], index = 0, limit = 3) {
|
|
25
|
+
const total = items.length;
|
|
26
|
+
if (total === 0) return { items: [], selectedIndex: -1, start: 0 };
|
|
27
|
+
const size = Math.max(1, Math.min(limit, total));
|
|
28
|
+
const safeIndex = Math.min(Math.max(0, index), total - 1);
|
|
29
|
+
const start = Math.min(Math.max(0, safeIndex - Math.floor((size - 1) / 2)), total - size);
|
|
30
|
+
return { items: items.slice(start, start + size), selectedIndex: safeIndex - start, start };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectRecentItems(snapshot, dashboard, eventLimit) {
|
|
34
|
+
const items = [];
|
|
35
|
+
for (const event of (snapshot?.history?.events ?? []).slice(0, 3)) {
|
|
36
|
+
items.push({
|
|
37
|
+
id: `e-${items.length}`,
|
|
38
|
+
label: `${formatWhen(event.timestamp)} · ${event.command ?? event.type ?? "event"} · ${formatResult(event.action)}`
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
for (const run of (dashboard?.recentRuns ?? []).slice(0, 2)) {
|
|
42
|
+
items.push({
|
|
43
|
+
id: `r-${items.length}`,
|
|
44
|
+
label: `${formatWhen(run.updatedAt ?? run.endedAt ?? run.startedAt)} · ${run.agentId ?? "agent"} · ${formatResult(run.state)}`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return items.slice(0, eventLimit);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function phaseTone(phase) {
|
|
51
|
+
if (phase === RECOVERY_PHASE.FAILED) return "danger";
|
|
52
|
+
if (phase === RECOVERY_PHASE.COMPLETED) return "ready";
|
|
53
|
+
if (phase === RECOVERY_PHASE.CONFIRMING || phase === RECOVERY_PHASE.PREVIEWING || phase === RECOVERY_PHASE.APPLYING) {
|
|
54
|
+
return "warn";
|
|
55
|
+
}
|
|
56
|
+
return "info";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function phaseTitle(phase, snapCount, eventCount) {
|
|
60
|
+
if (phase === RECOVERY_PHASE.PREVIEWING) return "Previewing restore";
|
|
61
|
+
if (phase === RECOVERY_PHASE.CONFIRMING) return "Confirm restore";
|
|
62
|
+
if (phase === RECOVERY_PHASE.APPLYING) return "Restoring";
|
|
63
|
+
if (phase === RECOVERY_PHASE.COMPLETED) return "Restore complete";
|
|
64
|
+
if (phase === RECOVERY_PHASE.FAILED) return "Restore failed";
|
|
65
|
+
return `${snapCount} snapshot(s) · ${eventCount} recent`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function calloutBody(phase, recoveryAction) {
|
|
69
|
+
const msg = recoveryAction?.message ?? "";
|
|
70
|
+
if (/Y restore|N\/Esc/i.test(msg)) return "";
|
|
71
|
+
if (phase === RECOVERY_PHASE.FAILED) {
|
|
72
|
+
return msg || (recoveryAction?.error ? `Error · ${recoveryAction.error}` : "");
|
|
73
|
+
}
|
|
74
|
+
return phase === RECOVERY_PHASE.IDLE ? msg : "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildDetailsLines(preview, receipt, showPaths, homeDir, pathLimit) {
|
|
78
|
+
if (!showPaths) return [];
|
|
79
|
+
const files = preview?.files ?? [];
|
|
80
|
+
if (files.length > 0) {
|
|
81
|
+
const lines = files.slice(0, pathLimit).map((f) => formatConfirmPath(f.displayPath ?? f.path, homeDir));
|
|
82
|
+
if (files.length > pathLimit) lines.push(`… ${files.length - pathLimit} more`);
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
if (receipt?.safetyBackup) return ["Safety backup retained"];
|
|
86
|
+
if (receipt) return [`Result · ${receipt.action ?? "rollback"} · ${receipt.restored?.length ?? 0} restored`];
|
|
87
|
+
return ["No path evidence in this preview."];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function adaptActivityModel({
|
|
91
|
+
snapshot = null, recoveryAction = null, dashboard = null, listIndex = 0,
|
|
92
|
+
homeDir = null, detailsOpen = false, layoutMode = LAYOUT_MODES.COMPACT
|
|
93
|
+
} = {}) {
|
|
94
|
+
const phase = recoveryAction?.phase ?? RECOVERY_PHASE.IDLE;
|
|
95
|
+
const limits = activityContentLimits(layoutMode);
|
|
96
|
+
const allSnapshots = listRecoverySnapshots(snapshot);
|
|
97
|
+
const windowed = windowSlice(allSnapshots, listIndex, limits.snapshots);
|
|
98
|
+
const preview = recoveryAction?.preview ?? null;
|
|
99
|
+
const receipt = recoveryAction?.receipt ?? null;
|
|
100
|
+
const hasPreview = Boolean(preview);
|
|
101
|
+
const confirming = phase === RECOVERY_PHASE.CONFIRMING;
|
|
102
|
+
const working = phase === RECOVERY_PHASE.PREVIEWING || phase === RECOVERY_PHASE.APPLYING;
|
|
103
|
+
const showPaths = (detailsOpen && hasPreview) || confirming;
|
|
104
|
+
const recentItems = collectRecentItems(snapshot, dashboard, limits.events);
|
|
105
|
+
const recent = recentItems.length > 0
|
|
106
|
+
? recentItems
|
|
107
|
+
: [{ id: "empty-recent", label: "No recent agent activity." }];
|
|
108
|
+
const snapshotItems = windowed.items.length === 0
|
|
109
|
+
? [{ id: "empty-snap", label: "No global snapshots yet." }]
|
|
110
|
+
: windowed.items.map((entry, index) => ({
|
|
111
|
+
id: entry.name ?? `s-${windowed.start + index}`,
|
|
112
|
+
label: `${shortName(entry.name)} · ${entry.fileCount ?? "?"} files`
|
|
113
|
+
}));
|
|
114
|
+
const focused = allSnapshots[Math.min(Math.max(0, listIndex), Math.max(0, allSnapshots.length - 1))] ?? null;
|
|
115
|
+
const fileCount = preview?.files?.length ?? 0;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
title: "Activity",
|
|
119
|
+
phase,
|
|
120
|
+
hasPreview,
|
|
121
|
+
callout: {
|
|
122
|
+
tone: phaseTone(phase),
|
|
123
|
+
title: phaseTitle(phase, allSnapshots.length, recentItems.length),
|
|
124
|
+
body: calloutBody(phase, recoveryAction)
|
|
125
|
+
},
|
|
126
|
+
primary: confirming || working ? null : {
|
|
127
|
+
label: hasPreview
|
|
128
|
+
? `Restore preview · ${fileCount} file(s)`
|
|
129
|
+
: (allSnapshots.length > 0 ? "Select a snapshot to preview" : "No snapshots to restore"),
|
|
130
|
+
detail: receipt
|
|
131
|
+
? `Result · ${receipt.action ?? "rollback"} · ${receipt.restored?.length ?? 0} restored`
|
|
132
|
+
: null
|
|
133
|
+
},
|
|
134
|
+
confirm: confirming ? {
|
|
135
|
+
summary: fileCount > 0
|
|
136
|
+
? `Restore ${fileCount} file(s) from ${shortName(preview?.snapshot)}.`
|
|
137
|
+
: "Restore confirmed snapshot preview.",
|
|
138
|
+
primaryLabel: "Restore"
|
|
139
|
+
} : null,
|
|
140
|
+
recent,
|
|
141
|
+
snapshots: snapshotItems,
|
|
142
|
+
selectedIndex: windowed.items.length === 0 ? -1 : windowed.selectedIndex,
|
|
143
|
+
focusedSnapshot: focused?.name ?? null,
|
|
144
|
+
snapshotTotal: allSnapshots.length,
|
|
145
|
+
details: buildDetailsLines(preview, receipt, showPaths, homeDir, detailsPathLimit(layoutMode)),
|
|
146
|
+
detailsOpen: showPaths,
|
|
147
|
+
showDetails: hasPreview || confirming || Boolean(receipt)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function SemanticActivityPanel({
|
|
152
|
+
snapshot = null, recoveryAction = null, dashboard = null, listIndex = 0,
|
|
153
|
+
homeDir = null, detailsOpen = false, layoutMode = LAYOUT_MODES.COMPACT,
|
|
154
|
+
contentFocused = false, colorEnabled = true, unicode = true
|
|
155
|
+
}) {
|
|
156
|
+
const view = adaptActivityModel({
|
|
157
|
+
snapshot, recoveryAction, dashboard, listIndex, homeDir, detailsOpen, layoutMode
|
|
158
|
+
});
|
|
159
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
160
|
+
React.createElement(Text, {
|
|
161
|
+
bold: true, color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
|
|
162
|
+
}, view.title),
|
|
163
|
+
React.createElement(Callout, {
|
|
164
|
+
tone: view.callout.tone, title: view.callout.title,
|
|
165
|
+
body: view.callout.body || undefined, colorEnabled, compact: true
|
|
166
|
+
}),
|
|
167
|
+
view.confirm
|
|
168
|
+
? React.createElement(Confirm, {
|
|
169
|
+
summary: view.confirm.summary, primaryLabel: view.confirm.primaryLabel,
|
|
170
|
+
focused: false, colorEnabled, mark: " "
|
|
171
|
+
})
|
|
172
|
+
: view.primary
|
|
173
|
+
? React.createElement(Text, { bold: true }, ` ${view.primary.label}`)
|
|
174
|
+
: null,
|
|
175
|
+
view.primary?.detail && !view.confirm
|
|
176
|
+
? React.createElement(Text, null, view.primary.detail) : null,
|
|
177
|
+
React.createElement(Text, { bold: true }, "Recent"),
|
|
178
|
+
React.createElement(ActionList, {
|
|
179
|
+
items: view.recent, selectedIndex: -1, focused: false, colorEnabled, unicode
|
|
180
|
+
}),
|
|
181
|
+
React.createElement(Text, { bold: true }, "Snapshots"),
|
|
182
|
+
React.createElement(ActionList, {
|
|
183
|
+
items: view.snapshots, selectedIndex: view.selectedIndex,
|
|
184
|
+
focused: contentFocused, colorEnabled, unicode
|
|
185
|
+
}),
|
|
186
|
+
view.showDetails
|
|
187
|
+
? React.createElement(Details, {
|
|
188
|
+
open: view.detailsOpen, summary: "Details",
|
|
189
|
+
lines: view.details.length > 0 ? view.details : ["No path evidence in this preview."],
|
|
190
|
+
colorEnabled, focused: false, mark: " "
|
|
191
|
+
})
|
|
192
|
+
: null
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live semantic Alerts inbox.
|
|
3
|
+
* ActionList owns the only focus mark; windowSlice keeps full open domain navigable.
|
|
4
|
+
* Enter resolve / D dismiss unchanged — adapter exposes focusedId aligned with selectAlertFromList.
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { COCKPIT_COLORS } from "../theme.js";
|
|
9
|
+
import { LAYOUT_MODES } from "../layout.js";
|
|
10
|
+
import { ALERT_STATES } from "../../runtime/alerts/alert-types.js";
|
|
11
|
+
import { formatAlertsHeadline, selectAlertFromList } from "../cockpit-alerts.js";
|
|
12
|
+
import { ActionList, Callout } from "./semantic.js";
|
|
13
|
+
import { windowSlice } from "./live-activity.js";
|
|
14
|
+
|
|
15
|
+
export function alertsListLimit(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
16
|
+
return layoutMode === LAYOUT_MODES.WIDE ? 8 : 3;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatAlertWhen(alert) {
|
|
20
|
+
return String(alert.createdAt ?? "").slice(0, 16).replace("T", " ") || "unknown time";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function alertLabel(alert) {
|
|
24
|
+
return `${alert.severity} · ${alert.title} · ${formatAlertWhen(alert)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function openAlerts(alerts) {
|
|
28
|
+
if (!Array.isArray(alerts)) return [];
|
|
29
|
+
return alerts.filter((alert) => alert.state === ALERT_STATES.OPEN);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function unavailablePack() {
|
|
33
|
+
return {
|
|
34
|
+
items: [{ id: "unavailable", label: "Could not read the alert store." }],
|
|
35
|
+
selectedIndex: -1,
|
|
36
|
+
focusedId: null,
|
|
37
|
+
total: 0,
|
|
38
|
+
start: 0,
|
|
39
|
+
isEmpty: true,
|
|
40
|
+
isUnavailable: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function emptyPack() {
|
|
45
|
+
return {
|
|
46
|
+
items: [{ id: "empty", label: "No pending alerts." }],
|
|
47
|
+
selectedIndex: -1,
|
|
48
|
+
focusedId: null,
|
|
49
|
+
total: 0,
|
|
50
|
+
start: 0,
|
|
51
|
+
isEmpty: true,
|
|
52
|
+
isUnavailable: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function populatedPack(open, listIndex, limit) {
|
|
57
|
+
const windowed = windowSlice(open, listIndex, limit);
|
|
58
|
+
const safe = Math.min(Math.max(0, listIndex), open.length - 1);
|
|
59
|
+
const focused = open[safe] ?? null;
|
|
60
|
+
return {
|
|
61
|
+
items: windowed.items.map((alert, i) => ({
|
|
62
|
+
id: alert.alertId ?? `alert-${windowed.start + i}`,
|
|
63
|
+
label: alertLabel(alert)
|
|
64
|
+
})),
|
|
65
|
+
selectedIndex: windowed.selectedIndex,
|
|
66
|
+
focusedId: focused?.alertId ?? null,
|
|
67
|
+
total: open.length,
|
|
68
|
+
start: windowed.start,
|
|
69
|
+
isEmpty: false,
|
|
70
|
+
isUnavailable: false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Pure adapter: unavailable · empty · pending inbox. */
|
|
75
|
+
export function adaptAlertsModel({
|
|
76
|
+
alerts = null,
|
|
77
|
+
listIndex = 0,
|
|
78
|
+
layoutMode = LAYOUT_MODES.COMPACT
|
|
79
|
+
} = {}) {
|
|
80
|
+
const limit = alertsListLimit(layoutMode);
|
|
81
|
+
const headline = formatAlertsHeadline(alerts);
|
|
82
|
+
let list;
|
|
83
|
+
|
|
84
|
+
if (alerts == null) {
|
|
85
|
+
list = unavailablePack();
|
|
86
|
+
} else {
|
|
87
|
+
const open = openAlerts(alerts);
|
|
88
|
+
list = open.length === 0
|
|
89
|
+
? emptyPack()
|
|
90
|
+
: populatedPack(open, listIndex, limit);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const selected = selectAlertFromList(alerts, listIndex);
|
|
94
|
+
if (!list.isEmpty && selected?.alertId) {
|
|
95
|
+
list.focusedId = selected.alertId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const callout = list.isUnavailable
|
|
99
|
+
? {
|
|
100
|
+
tone: "danger",
|
|
101
|
+
title: headline.headline,
|
|
102
|
+
body: "Esc back · / Alerts"
|
|
103
|
+
}
|
|
104
|
+
: list.isEmpty
|
|
105
|
+
? {
|
|
106
|
+
tone: "info",
|
|
107
|
+
title: headline.headline,
|
|
108
|
+
body: "Esc back · / Alerts"
|
|
109
|
+
}
|
|
110
|
+
: {
|
|
111
|
+
tone: "warn",
|
|
112
|
+
title: headline.headline,
|
|
113
|
+
body: "Enter resolves · D dismisses · Esc back"
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
title: "Alerts",
|
|
118
|
+
callout,
|
|
119
|
+
items: list.items,
|
|
120
|
+
selectedIndex: list.selectedIndex,
|
|
121
|
+
focusedId: list.focusedId,
|
|
122
|
+
total: list.total,
|
|
123
|
+
start: list.start,
|
|
124
|
+
isEmpty: list.isEmpty,
|
|
125
|
+
isUnavailable: list.isUnavailable,
|
|
126
|
+
listLimit: limit
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function SemanticAlertsPanel({
|
|
131
|
+
alerts = null,
|
|
132
|
+
listIndex = 0,
|
|
133
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
134
|
+
contentFocused = false,
|
|
135
|
+
colorEnabled = true,
|
|
136
|
+
unicode = true
|
|
137
|
+
}) {
|
|
138
|
+
const model = adaptAlertsModel({ alerts, listIndex, layoutMode });
|
|
139
|
+
const listFocused = contentFocused && !model.isEmpty && !model.isUnavailable;
|
|
140
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
141
|
+
React.createElement(Text, {
|
|
142
|
+
bold: true, color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
|
|
143
|
+
}, model.title),
|
|
144
|
+
React.createElement(Callout, {
|
|
145
|
+
tone: model.callout.tone,
|
|
146
|
+
title: model.callout.title,
|
|
147
|
+
body: model.callout.body || undefined,
|
|
148
|
+
colorEnabled,
|
|
149
|
+
compact: true
|
|
150
|
+
}),
|
|
151
|
+
React.createElement(ActionList, {
|
|
152
|
+
items: model.items,
|
|
153
|
+
selectedIndex: model.selectedIndex,
|
|
154
|
+
focused: listFocused,
|
|
155
|
+
colorEnabled,
|
|
156
|
+
unicode
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/** Live semantic Governance. Ownership: Callout=status · Confirm/primary=action · footer=keys. */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { COCKPIT_COLORS } from "../theme.js";
|
|
5
|
+
import { formatConfirmPath } from "../cockpit-path-label.js";
|
|
6
|
+
import { CHANGES_PHASE } from "../cockpit-changes.js";
|
|
7
|
+
import { LAYOUT_MODES } from "../layout.js";
|
|
8
|
+
import { ActionList, Callout, Confirm, Details } from "./semantic.js";
|
|
9
|
+
import { mapHealthTone } from "./live-overview.js";
|
|
10
|
+
|
|
11
|
+
function healthLabel(kind) {
|
|
12
|
+
return String(kind ?? "unknown").replaceAll("_", " ");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function plannedChanges(snapshot, changesAction) {
|
|
16
|
+
const preview = changesAction?.preview;
|
|
17
|
+
const diff = snapshot?.diff;
|
|
18
|
+
if (preview?.hasChanges) return preview.changes ?? [];
|
|
19
|
+
if (diff?.hasChanges && !preview) return diff.changes ?? [];
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function phaseTone(phase, healthKind) {
|
|
24
|
+
if (phase === CHANGES_PHASE.FAILED) return "danger";
|
|
25
|
+
if (phase === CHANGES_PHASE.COMPLETED) return "ready";
|
|
26
|
+
if (phase === CHANGES_PHASE.CONFIRMING || phase === CHANGES_PHASE.PREVIEWING || phase === CHANGES_PHASE.APPLYING) {
|
|
27
|
+
return "warn";
|
|
28
|
+
}
|
|
29
|
+
return mapHealthTone(healthKind);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function phaseTitle(phase, snapshot, changesAction) {
|
|
33
|
+
if (phase === CHANGES_PHASE.PREVIEWING) return "Previewing";
|
|
34
|
+
if (phase === CHANGES_PHASE.CONFIRMING) return "Confirm apply";
|
|
35
|
+
if (phase === CHANGES_PHASE.APPLYING) return "Applying";
|
|
36
|
+
if (phase === CHANGES_PHASE.COMPLETED) return "Apply complete";
|
|
37
|
+
if (phase === CHANGES_PHASE.FAILED) {
|
|
38
|
+
return changesAction?.error === "setup-required" ? "Setup required" : "Governance failed";
|
|
39
|
+
}
|
|
40
|
+
const pending = snapshot?.diff?.hasChanges
|
|
41
|
+
? (snapshot.diff.changeCount ?? snapshot.diff.changes?.length ?? 0)
|
|
42
|
+
: 0;
|
|
43
|
+
return `${healthLabel(snapshot?.health)}${pending > 0 ? ` · ${pending} pending` : " · drift clean"}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Compact/minimal: 3 paths; wide: 12. */
|
|
47
|
+
export function detailsPathLimit(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
48
|
+
return layoutMode === LAYOUT_MODES.WIDE ? 12 : 3;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function calloutBody(phase, changesAction) {
|
|
52
|
+
if (phase !== CHANGES_PHASE.FAILED) return "";
|
|
53
|
+
if (changesAction?.error === "setup-required") return "Not configured — open Overview and run setup.";
|
|
54
|
+
if (changesAction?.error) return `Error · ${changesAction.error}`;
|
|
55
|
+
return changesAction?.message ?? "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildDetailsLines(planned, showPaths, changesAction, homeDir, pathLimit) {
|
|
59
|
+
if (!showPaths) return [];
|
|
60
|
+
if (planned.length > 0) {
|
|
61
|
+
const lines = planned.slice(0, pathLimit).map((c) =>
|
|
62
|
+
`${c.action ?? c.kind} · ${formatConfirmPath(c.target, homeDir)}`
|
|
63
|
+
);
|
|
64
|
+
if (planned.length > pathLimit) lines.push(`… ${planned.length - pathLimit} more`);
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
const receipt = changesAction?.receipt;
|
|
68
|
+
if (receipt?.checksBefore && receipt?.checksAfter) {
|
|
69
|
+
return [`Checks · before ok=${receipt.checksBefore.ok} → after ok=${receipt.checksAfter.ok}`];
|
|
70
|
+
}
|
|
71
|
+
return ["No path evidence on this scan."];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Pure adapter: paths only in detailsLines (confirming or Details open). */
|
|
75
|
+
export function adaptGovernanceModel({
|
|
76
|
+
snapshot = null,
|
|
77
|
+
changesAction = null,
|
|
78
|
+
homeDir = null,
|
|
79
|
+
detailsOpen = false,
|
|
80
|
+
layoutMode = LAYOUT_MODES.COMPACT
|
|
81
|
+
} = {}) {
|
|
82
|
+
const phase = changesAction?.phase ?? CHANGES_PHASE.IDLE;
|
|
83
|
+
const coverage = snapshot?.coverage ?? {};
|
|
84
|
+
const cta = snapshot?.cta;
|
|
85
|
+
const planned = plannedChanges(snapshot, changesAction);
|
|
86
|
+
const showPaths = detailsOpen || phase === CHANGES_PHASE.CONFIRMING;
|
|
87
|
+
const confirming = phase === CHANGES_PHASE.CONFIRMING;
|
|
88
|
+
const working = phase === CHANGES_PHASE.PREVIEWING || phase === CHANGES_PHASE.APPLYING;
|
|
89
|
+
const actionable = phase === CHANGES_PHASE.IDLE
|
|
90
|
+
|| phase === CHANGES_PHASE.COMPLETED
|
|
91
|
+
|| phase === CHANGES_PHASE.FAILED;
|
|
92
|
+
|
|
93
|
+
const metrics = [
|
|
94
|
+
{
|
|
95
|
+
id: "coverage",
|
|
96
|
+
label: `Coverage · ${coverage.governedAgents ?? 0}/${coverage.detectedAgents ?? 0} agents · ${coverage.components ?? 0} components`
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "planned",
|
|
100
|
+
label: planned.length > 0
|
|
101
|
+
? `Planned · ${planned.length} change(s)`
|
|
102
|
+
: (snapshot?.diff?.summary ?? "No pending governance changes.")
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
if (changesAction?.receipt) {
|
|
106
|
+
const r = changesAction.receipt;
|
|
107
|
+
metrics.push({
|
|
108
|
+
id: "receipt",
|
|
109
|
+
label: `Result · ${r.action}${r.partial ? " (partial)" : ""}${r.backups?.length ? ` · ${r.backups.length} backup(s)` : ""}`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
title: "Governance",
|
|
115
|
+
phase,
|
|
116
|
+
callout: {
|
|
117
|
+
tone: phaseTone(phase, snapshot?.health),
|
|
118
|
+
title: phaseTitle(phase, snapshot, changesAction),
|
|
119
|
+
body: calloutBody(phase, changesAction)
|
|
120
|
+
},
|
|
121
|
+
primary: confirming || working
|
|
122
|
+
? null
|
|
123
|
+
: {
|
|
124
|
+
label: cta?.title ?? "Review governance when ready",
|
|
125
|
+
detail: actionable ? (cta?.detail ?? null) : null
|
|
126
|
+
},
|
|
127
|
+
confirm: confirming
|
|
128
|
+
? {
|
|
129
|
+
summary: planned.length > 0
|
|
130
|
+
? `Apply ${planned.length} planned change(s).`
|
|
131
|
+
: "Apply confirmed governance preview.",
|
|
132
|
+
primaryLabel: "Apply"
|
|
133
|
+
}
|
|
134
|
+
: null,
|
|
135
|
+
metrics,
|
|
136
|
+
details: buildDetailsLines(planned, showPaths, changesAction, homeDir, detailsPathLimit(layoutMode)),
|
|
137
|
+
detailsOpen: showPaths
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function SemanticGovernancePanel({
|
|
142
|
+
snapshot = null,
|
|
143
|
+
changesAction = null,
|
|
144
|
+
homeDir = null,
|
|
145
|
+
detailsOpen = false,
|
|
146
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
147
|
+
colorEnabled = true,
|
|
148
|
+
unicode = true
|
|
149
|
+
}) {
|
|
150
|
+
const view = adaptGovernanceModel({
|
|
151
|
+
snapshot, changesAction, homeDir, detailsOpen, layoutMode
|
|
152
|
+
});
|
|
153
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
154
|
+
React.createElement(Text, {
|
|
155
|
+
bold: true,
|
|
156
|
+
color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
|
|
157
|
+
}, view.title),
|
|
158
|
+
React.createElement(Callout, {
|
|
159
|
+
tone: view.callout.tone,
|
|
160
|
+
title: view.callout.title,
|
|
161
|
+
body: view.callout.body || undefined,
|
|
162
|
+
colorEnabled,
|
|
163
|
+
compact: true
|
|
164
|
+
}),
|
|
165
|
+
view.confirm
|
|
166
|
+
? React.createElement(Confirm, {
|
|
167
|
+
summary: view.confirm.summary,
|
|
168
|
+
primaryLabel: view.confirm.primaryLabel,
|
|
169
|
+
focused: false,
|
|
170
|
+
colorEnabled,
|
|
171
|
+
mark: " "
|
|
172
|
+
})
|
|
173
|
+
: view.primary
|
|
174
|
+
? React.createElement(Box, { flexDirection: "column" },
|
|
175
|
+
React.createElement(Text, { bold: true }, ` ${view.primary.label}`),
|
|
176
|
+
view.primary.detail ? React.createElement(Text, null, view.primary.detail) : null
|
|
177
|
+
)
|
|
178
|
+
: null,
|
|
179
|
+
React.createElement(ActionList, {
|
|
180
|
+
items: view.metrics,
|
|
181
|
+
selectedIndex: -1,
|
|
182
|
+
focused: false,
|
|
183
|
+
colorEnabled,
|
|
184
|
+
unicode
|
|
185
|
+
}),
|
|
186
|
+
React.createElement(Details, {
|
|
187
|
+
open: view.detailsOpen,
|
|
188
|
+
summary: "Details",
|
|
189
|
+
lines: view.details.length > 0 ? view.details : ["No path evidence on this scan."],
|
|
190
|
+
colorEnabled,
|
|
191
|
+
focused: false,
|
|
192
|
+
mark: " "
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
}
|