@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.
Files changed (49) hide show
  1. package/README.md +41 -15
  2. package/package.json +1 -1
  3. package/scripts/cockpit-smoke.mjs +6 -6
  4. package/scripts/ux-prototype-tty.mjs +9 -0
  5. package/src/cli.js +24 -1
  6. package/src/global/control-plane-proposals.js +2 -1
  7. package/src/global/control-plane-snapshot.js +1 -1
  8. package/src/global/global-doctor.js +2 -0
  9. package/src/global/ink/cockpit/primitives.js +127 -50
  10. package/src/global/ink/cockpit-alerts.js +36 -0
  11. package/src/global/ink/cockpit-changes.js +61 -37
  12. package/src/global/ink/cockpit-control-center.js +79 -53
  13. package/src/global/ink/cockpit-controller.js +98 -15
  14. package/src/global/ink/cockpit-enter.js +1 -0
  15. package/src/global/ink/cockpit-focus.js +4 -2
  16. package/src/global/ink/cockpit-models.js +100 -51
  17. package/src/global/ink/cockpit-palette.js +109 -0
  18. package/src/global/ink/cockpit-path-label.js +19 -0
  19. package/src/global/ink/cockpit-recovery.js +84 -18
  20. package/src/global/ink/cockpit-reviews.js +14 -10
  21. package/src/global/ink/cockpit-runs.js +13 -4
  22. package/src/global/ink/cockpit-settings.js +194 -0
  23. package/src/global/ink/cockpit-usage.js +111 -0
  24. package/src/global/ink/cockpit-views.js +119 -116
  25. package/src/global/ink/orchestrator-app.js +169 -46
  26. package/src/global/ink/orchestrator-state.js +24 -14
  27. package/src/global/ink/setup-app.js +55 -72
  28. package/src/global/ink/setup-state.js +16 -0
  29. package/src/global/ink/theme.js +27 -0
  30. package/src/global/ink/use-orchestrator-data.js +58 -0
  31. package/src/global/ink/ux/live-activity.js +194 -0
  32. package/src/global/ink/ux/live-alerts.js +159 -0
  33. package/src/global/ink/ux/live-governance.js +195 -0
  34. package/src/global/ink/ux/live-orchestration.js +188 -0
  35. package/src/global/ink/ux/live-overview.js +125 -0
  36. package/src/global/ink/ux/live-settings.js +189 -0
  37. package/src/global/ink/ux/live-setup.js +160 -0
  38. package/src/global/ink/ux/live-usage.js +51 -0
  39. package/src/global/ink/ux/semantic.js +84 -0
  40. package/src/global/ink/ux/task-flow-app.js +85 -0
  41. package/src/global/ink/ux/task-flow.js +173 -0
  42. package/src/global/orchestrator.js +37 -20
  43. package/src/global/paths.js +3 -0
  44. package/src/global/runtime/alerts/alert-store.js +216 -0
  45. package/src/global/runtime/alerts/alert-types.js +59 -0
  46. package/src/global/runtime/alerts/alert-validate.js +117 -0
  47. package/src/global/runtime/monitor/monitor-cli.js +62 -0
  48. package/src/global/runtime/monitor/monitor-platform.js +95 -0
  49. 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
+ }