@kal-elsam/kairo-runtime 0.9.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 (32) hide show
  1. package/README.md +14 -8
  2. package/package.json +1 -1
  3. package/scripts/cockpit-smoke.mjs +3 -3
  4. package/scripts/ux-prototype-tty.mjs +9 -0
  5. package/src/global/control-plane-proposals.js +2 -1
  6. package/src/global/control-plane-snapshot.js +1 -1
  7. package/src/global/ink/cockpit/primitives.js +47 -35
  8. package/src/global/ink/cockpit-changes.js +2 -2
  9. package/src/global/ink/cockpit-control-center.js +12 -64
  10. package/src/global/ink/cockpit-controller.js +41 -5
  11. package/src/global/ink/cockpit-enter.js +1 -0
  12. package/src/global/ink/cockpit-models.js +14 -7
  13. package/src/global/ink/cockpit-palette.js +18 -7
  14. package/src/global/ink/cockpit-recovery.js +9 -6
  15. package/src/global/ink/cockpit-usage.js +111 -0
  16. package/src/global/ink/cockpit-views.js +69 -97
  17. package/src/global/ink/orchestrator-app.js +32 -0
  18. package/src/global/ink/setup-app.js +55 -72
  19. package/src/global/ink/setup-state.js +16 -0
  20. package/src/global/ink/theme.js +27 -0
  21. package/src/global/ink/ux/live-activity.js +194 -0
  22. package/src/global/ink/ux/live-alerts.js +159 -0
  23. package/src/global/ink/ux/live-governance.js +195 -0
  24. package/src/global/ink/ux/live-orchestration.js +188 -0
  25. package/src/global/ink/ux/live-overview.js +125 -0
  26. package/src/global/ink/ux/live-settings.js +189 -0
  27. package/src/global/ink/ux/live-setup.js +160 -0
  28. package/src/global/ink/ux/live-usage.js +51 -0
  29. package/src/global/ink/ux/semantic.js +84 -0
  30. package/src/global/ink/ux/task-flow-app.js +85 -0
  31. package/src/global/ink/ux/task-flow.js +173 -0
  32. package/src/global/orchestrator.js +37 -20
@@ -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
+ }