@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
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Live semantic Orchestration lists (Hub / Active / History / Reviews).
3
+ * ActionList owns the only focus mark; windowSlice keeps full domain navigable.
4
+ * Enter destinations unchanged — adapter exposes focused identity for listIndex.
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 { ORCHESTRATOR_VIEWS, formatRunLines } from "../orchestrator-state.js";
11
+ import {
12
+ RUNS_HUB_ITEMS, formatOrchestrationStatus, formatRunsHubLines
13
+ } from "../cockpit-runs.js";
14
+ import { formatReviewListLines } from "../cockpit-reviews.js";
15
+ import { ActionList, Callout } from "./semantic.js";
16
+ import { windowSlice } from "./live-activity.js";
17
+
18
+ export function orchestrationListLimit(layoutMode = LAYOUT_MODES.COMPACT) {
19
+ return layoutMode === LAYOUT_MODES.WIDE ? 8 : 3;
20
+ }
21
+
22
+ function emptyPack(label) {
23
+ return {
24
+ items: [{ id: "empty", label }],
25
+ selectedIndex: -1,
26
+ focusedId: null,
27
+ total: 0,
28
+ start: 0,
29
+ isEmpty: true
30
+ };
31
+ }
32
+
33
+ function windowDomain(domain, listIndex, limit, toItem, emptyLabel) {
34
+ const windowed = windowSlice(domain, listIndex, limit);
35
+ if (windowed.items.length === 0) return emptyPack(emptyLabel);
36
+ const safe = Math.min(Math.max(0, listIndex), domain.length - 1);
37
+ return {
38
+ items: windowed.items.map((entry, i) => toItem(entry, windowed.start + i)),
39
+ selectedIndex: windowed.selectedIndex,
40
+ focusedId: toItem(domain[safe], safe).id,
41
+ total: domain.length,
42
+ start: windowed.start,
43
+ isEmpty: false
44
+ };
45
+ }
46
+
47
+ function hubPack(listIndex) {
48
+ const labels = formatRunsHubLines(RUNS_HUB_ITEMS);
49
+ const items = RUNS_HUB_ITEMS.map((item, i) => ({
50
+ id: item.id,
51
+ label: labels[i] ?? item.label
52
+ }));
53
+ const safe = Math.min(Math.max(0, listIndex), items.length - 1);
54
+ return {
55
+ items,
56
+ selectedIndex: safe,
57
+ focusedId: items[safe]?.id ?? null,
58
+ total: items.length,
59
+ start: 0,
60
+ isEmpty: false
61
+ };
62
+ }
63
+
64
+ function runPack(runs, listIndex, limit, emptyLabel) {
65
+ return windowDomain(runs, listIndex, limit, (run, absoluteIndex) => ({
66
+ id: run.runId ?? `run-${absoluteIndex}`,
67
+ label: formatRunLines([run], { readable: true })[0]
68
+ }), emptyLabel);
69
+ }
70
+
71
+ function reviewPack(reviews, listIndex, limit) {
72
+ const empty = "No review receipts yet. Run kairo review --agent codex|pi.";
73
+ return windowDomain(reviews, listIndex, limit, (receipt, absoluteIndex) => ({
74
+ id: receipt.reviewId ?? `review-${absoluteIndex}`,
75
+ label: formatReviewListLines([receipt])[0]
76
+ }), empty);
77
+ }
78
+
79
+ /** Pure adapter: Hub · Active · History · Reviews. */
80
+ export function adaptOrchestrationModel({
81
+ view = ORCHESTRATOR_VIEWS.RUNS,
82
+ dashboard = null,
83
+ reviews = [],
84
+ listIndex = 0,
85
+ layoutMode = LAYOUT_MODES.COMPACT
86
+ } = {}) {
87
+ const limit = orchestrationListLimit(layoutMode);
88
+ const active = dashboard?.activeRuns ?? [];
89
+ const recent = dashboard?.recentRuns ?? [];
90
+ const reviewList = Array.isArray(reviews) ? reviews : [];
91
+ const status = formatOrchestrationStatus({
92
+ active: active.length, recent: recent.length, reviews: reviewList.length
93
+ });
94
+
95
+ let title = "Orchestration";
96
+ let callout = { tone: active.length > 0 ? "warn" : "info", title: status, body: "" };
97
+ let list = hubPack(listIndex);
98
+
99
+ if (view === ORCHESTRATOR_VIEWS.RUNS) {
100
+ callout = {
101
+ tone: active.length > 0 ? "warn" : "info",
102
+ title: status,
103
+ body: "Choose Active runs, History, Reviews, or New run."
104
+ };
105
+ list = hubPack(listIndex);
106
+ } else if (view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS) {
107
+ title = "Active runs";
108
+ list = runPack(
109
+ active, listIndex, limit,
110
+ "No runs executing. Governance first — launch only after setup/repairs."
111
+ );
112
+ callout = {
113
+ tone: list.isEmpty ? "info" : "warn",
114
+ title: list.isEmpty ? "None active" : `${list.total} active`,
115
+ body: list.isEmpty
116
+ ? "Esc back to Orchestration"
117
+ : "Enter opens detail"
118
+ };
119
+ } else if (view === ORCHESTRATOR_VIEWS.RECENT_RUNS) {
120
+ title = "Run history";
121
+ list = runPack(recent, listIndex, limit, "No completed runs yet.");
122
+ callout = {
123
+ tone: "info",
124
+ title: list.isEmpty ? "None completed" : `${list.total} completed`,
125
+ body: list.isEmpty
126
+ ? "Esc back to Orchestration"
127
+ : "Enter opens detail"
128
+ };
129
+ } else if (view === ORCHESTRATOR_VIEWS.REVIEWS) {
130
+ title = "Reviews";
131
+ list = reviewPack(reviewList, listIndex, limit);
132
+ callout = {
133
+ tone: "info",
134
+ title: list.isEmpty ? "None yet" : `${list.total} receipts`,
135
+ body: list.isEmpty
136
+ ? "Launch via kairo review --agent codex|pi."
137
+ : "Read-only · Enter opens detail"
138
+ };
139
+ }
140
+
141
+ return {
142
+ title,
143
+ view,
144
+ callout,
145
+ items: list.items,
146
+ selectedIndex: list.selectedIndex,
147
+ focusedId: list.focusedId,
148
+ total: list.total,
149
+ start: list.start,
150
+ isEmpty: list.isEmpty,
151
+ listLimit: limit
152
+ };
153
+ }
154
+
155
+ export function SemanticOrchestrationPanel({
156
+ view = ORCHESTRATOR_VIEWS.RUNS,
157
+ dashboard = null,
158
+ reviews = [],
159
+ listIndex = 0,
160
+ layoutMode = LAYOUT_MODES.COMPACT,
161
+ contentFocused = false,
162
+ colorEnabled = true,
163
+ unicode = true
164
+ }) {
165
+ const model = adaptOrchestrationModel({
166
+ view, dashboard, reviews, listIndex, layoutMode
167
+ });
168
+ const listFocused = contentFocused && !model.isEmpty;
169
+ return React.createElement(Box, { flexDirection: "column" },
170
+ React.createElement(Text, {
171
+ bold: true, color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
172
+ }, model.title),
173
+ React.createElement(Callout, {
174
+ tone: model.callout.tone,
175
+ title: model.callout.title,
176
+ body: model.callout.body || undefined,
177
+ colorEnabled,
178
+ compact: true
179
+ }),
180
+ React.createElement(ActionList, {
181
+ items: model.items,
182
+ selectedIndex: model.selectedIndex,
183
+ focused: listFocused,
184
+ colorEnabled,
185
+ unicode
186
+ })
187
+ );
188
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Live semantic Overview for Cockpit HOME.
3
+ * Nav owns the sole focus mark — this panel never renders `>`.
4
+ */
5
+ import React from "react";
6
+ import { Box, Text } from "ink";
7
+ import { CONTROL_PLANE_HEALTH } from "../../control-plane-snapshot.js";
8
+ import { COCKPIT_COLORS } from "../theme.js";
9
+ import { ActionList, Callout, Details } from "./semantic.js";
10
+
11
+ const DESTINATION_LABELS = {
12
+ setup: "Setup",
13
+ changes: "Governance",
14
+ ides: "Agents",
15
+ runs: "Orchestration",
16
+ "control-center": "Overview",
17
+ activity: "Activity",
18
+ usage: "Usage",
19
+ profile: "Settings"
20
+ };
21
+
22
+ export function mapHealthTone(kind) {
23
+ switch (kind) {
24
+ case CONTROL_PLANE_HEALTH.CHECK_FAILED:
25
+ return "danger";
26
+ case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
27
+ case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
28
+ case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
29
+ return "warn";
30
+ case CONTROL_PLANE_HEALTH.HEALTHY:
31
+ return "ready";
32
+ default:
33
+ return "warn";
34
+ }
35
+ }
36
+
37
+ function humanizeDestination(destination) {
38
+ if (!destination) return null;
39
+ return DESTINATION_LABELS[destination] ?? null;
40
+ }
41
+
42
+ /** Safe Details lines only — never invent paths/IDs; honest empty when none. */
43
+ export function buildOverviewDetails(model = {}) {
44
+ const lines = [];
45
+ const next = model.nextAction ?? model.cta ?? {};
46
+ const dest = humanizeDestination(next.destination);
47
+ if (dest) lines.push(`Next destination · ${dest}`);
48
+ if (typeof model.alerts?.count === "number") {
49
+ lines.push(`Open alerts · ${model.alerts.count}`);
50
+ }
51
+ if (lines.length === 0) return ["No extra evidence beyond the metrics above."];
52
+ return lines;
53
+ }
54
+
55
+ /**
56
+ * Pure adapter: buildControlCenterModel → semantic overview props.
57
+ * Callout / CTA / metrics never include paths or IDs.
58
+ */
59
+ export function adaptControlCenterToOverview(model = {}) {
60
+ const status = model.status ?? model.health ?? {};
61
+ const next = model.nextAction ?? model.cta ?? {};
62
+ return {
63
+ title: model.title ?? "Overview",
64
+ callout: {
65
+ tone: mapHealthTone(status.kind),
66
+ title: status.label ?? "Unknown",
67
+ body: status.summaryLine ?? ""
68
+ },
69
+ primary: {
70
+ label: next.actionTitle ?? "Review control plane",
71
+ detail: next.actionDetail || null,
72
+ hint: next.enterHint ?? null
73
+ },
74
+ metrics: [
75
+ { id: "activity", label: `Activity · ${model.activity?.headline ?? "Idle"}` },
76
+ { id: "alerts", label: `Alerts · ${model.alerts?.headline ?? "Alert data unavailable"}` },
77
+ { id: "tokens", label: `Tokens · ${model.tokens?.headline ?? "Data unavailable"}` }
78
+ ],
79
+ details: buildOverviewDetails(model)
80
+ };
81
+ }
82
+
83
+ export function SemanticOverviewPanel({
84
+ model,
85
+ detailsOpen = false,
86
+ colorEnabled = true,
87
+ unicode = true
88
+ }) {
89
+ const view = adaptControlCenterToOverview(model);
90
+ return React.createElement(Box, { flexDirection: "column" },
91
+ React.createElement(Text, {
92
+ bold: true,
93
+ color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
94
+ }, view.title),
95
+ React.createElement(Callout, {
96
+ tone: view.callout.tone,
97
+ title: view.callout.title,
98
+ body: view.callout.body,
99
+ colorEnabled,
100
+ compact: true
101
+ }),
102
+ React.createElement(Text, { bold: true }, ` ${view.primary.label}`),
103
+ view.primary.detail
104
+ ? React.createElement(Text, null, view.primary.detail)
105
+ : null,
106
+ view.primary.hint
107
+ ? React.createElement(Text, { color: colorEnabled ? COCKPIT_COLORS.muted : undefined }, view.primary.hint)
108
+ : null,
109
+ React.createElement(ActionList, {
110
+ items: view.metrics,
111
+ selectedIndex: -1,
112
+ focused: false,
113
+ colorEnabled,
114
+ unicode
115
+ }),
116
+ React.createElement(Details, {
117
+ open: detailsOpen,
118
+ summary: "Details",
119
+ lines: view.details,
120
+ colorEnabled,
121
+ focused: false,
122
+ mark: " "
123
+ })
124
+ );
125
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Live semantic Settings. Browse → preview → confirm → receipt (no filesystem install).
3
+ * Ownership: ActionList=browse focus · Callout=status · Confirm=intent · footer/KeyBar=keys · Receipt=result.
4
+ */
5
+ import React from "react";
6
+ import { Box, Text } from "ink";
7
+ import { COCKPIT_COLORS } from "../theme.js";
8
+ import { LAYOUT_MODES } from "../layout.js";
9
+ import {
10
+ SETTINGS_PHASE, getCuratedIntegration, listCuratedIntegrations
11
+ } from "../cockpit-settings.js";
12
+ import { ActionList, Callout, Confirm, Details, Receipt } from "./semantic.js";
13
+ import { windowSlice } from "./live-activity.js";
14
+
15
+ export function settingsListLimit(layoutMode = LAYOUT_MODES.COMPACT) {
16
+ return layoutMode === LAYOUT_MODES.WIDE ? 8 : 3;
17
+ }
18
+
19
+ /** Key ownership lives in shell KeyBar/footer — Confirm never owns Y/N/Esc. */
20
+ export function settingsKeyHints(phase = SETTINGS_PHASE.BROWSE) {
21
+ if (phase === SETTINGS_PHASE.PREVIEW) {
22
+ return [{ keys: "Enter", label: "Confirm" }, { keys: "Esc", label: "Back" }];
23
+ }
24
+ if (phase === SETTINGS_PHASE.CONFIRMING) {
25
+ return [
26
+ { keys: "Y", label: "Confirm" }, { keys: "N", label: "Cancel" },
27
+ { keys: "Esc", label: "Cancel" }
28
+ ];
29
+ }
30
+ if (phase === SETTINGS_PHASE.COMPLETED) {
31
+ return [{ keys: "Esc", label: "Back" }, { keys: "/", label: "Actions" }];
32
+ }
33
+ return [
34
+ { keys: "↑↓", label: "Select" }, { keys: "Enter", label: "Preview" },
35
+ { keys: "Esc", label: "Nav" }, { keys: "/", label: "Actions" }
36
+ ];
37
+ }
38
+
39
+ function entryLabel(entry) {
40
+ return `${entry.status} · ${entry.name} · ${entry.version} · ${entry.license}`;
41
+ }
42
+
43
+ function resolveEntry(integrations, selectedId) {
44
+ if (!selectedId) return null;
45
+ return integrations.find((e) => e.id === selectedId) ?? getCuratedIntegration(selectedId);
46
+ }
47
+
48
+ function phaseTone(phase) {
49
+ if (phase === SETTINGS_PHASE.COMPLETED) return "ready";
50
+ if (phase === SETTINGS_PHASE.CONFIRMING || phase === SETTINGS_PHASE.PREVIEW) return "warn";
51
+ return "info";
52
+ }
53
+
54
+ function phaseTitle(phase, total) {
55
+ if (phase === SETTINGS_PHASE.PREVIEW) return "Preview integration";
56
+ if (phase === SETTINGS_PHASE.CONFIRMING) return "Confirm intent";
57
+ if (phase === SETTINGS_PHASE.COMPLETED) return "Intent recorded";
58
+ return total === 0 ? "No curated integrations" : `${total} curated integration${total === 1 ? "" : "s"}`;
59
+ }
60
+
61
+ function buildDetailsLines(entry) {
62
+ if (!entry) return ["Integration not found."];
63
+ return [
64
+ `License · ${entry.license}`, `Audit · ${entry.audit}`,
65
+ `Capabilities · ${entry.capabilities?.join(" · ") || "none"}`,
66
+ `Permissions · ${entry.permissions?.join(" · ") || "none"}`,
67
+ entry.summary, entry.notes
68
+ ].filter(Boolean);
69
+ }
70
+
71
+ function receiptLines(receipt, entry) {
72
+ if (!receipt) return [];
73
+ return [
74
+ `Id · ${receipt.id} · wroteFiles · ${receipt.wroteFiles}`,
75
+ `Confirmed · ${receipt.confirmedAt}`,
76
+ entry ? `${entry.name} · ${entry.version} · ${entry.license}` : null,
77
+ "Confirm records intent — does not install packages."
78
+ ].filter(Boolean);
79
+ }
80
+
81
+ /** Browse-only: profile · apply · preflight · sources. */
82
+ function profilePolicyItems(snapshot = null, diagnostics = null) {
83
+ const p = snapshot?.policy, s = diagnostics?.profile?.sources;
84
+ const src = [s?.global && "global", s?.project && "project"].filter(Boolean).join(", ") || "none";
85
+ return [
86
+ { id: "policy", label: `Policy · ${p?.profile ?? "none"} · apply ${p?.applyMode ?? "n/a"}` },
87
+ { id: "preflight", label: `Preflight · ${p?.preflight ?? "n/a"} · sources · ${src}` }
88
+ ];
89
+ }
90
+
91
+ /** Pure adapter: browse window · preview Details · confirm intent · receipt first. */
92
+ export function adaptSettingsModel({
93
+ integrations = listCuratedIntegrations(), listIndex = 0, settingsAction = null,
94
+ layoutMode = LAYOUT_MODES.COMPACT, snapshot = null, diagnostics = null
95
+ } = {}) {
96
+ const phase = settingsAction?.phase ?? SETTINGS_PHASE.BROWSE;
97
+ const catalog = Array.isArray(integrations) ? integrations : [];
98
+ const limit = settingsListLimit(layoutMode);
99
+ const windowed = windowSlice(catalog, listIndex, limit);
100
+ const browsing = phase === SETTINGS_PHASE.BROWSE;
101
+ const safe = catalog.length > 0
102
+ ? Math.min(Math.max(0, listIndex), catalog.length - 1) : -1;
103
+ const focused = browsing && safe >= 0 ? catalog[safe] : null;
104
+ const entry = resolveEntry(catalog, settingsAction?.selectedId)
105
+ ?? (browsing ? focused : null);
106
+ const detailing = phase === SETTINGS_PHASE.PREVIEW || phase === SETTINGS_PHASE.CONFIRMING;
107
+ const items = catalog.length === 0
108
+ ? [{ id: "empty", label: "No curated integrations available." }]
109
+ : windowed.items.map((item, i) => ({
110
+ id: item.id ?? `integration-${windowed.start + i}`,
111
+ label: entryLabel(item)
112
+ }));
113
+
114
+ return {
115
+ title: "Settings",
116
+ phase,
117
+ callout: {
118
+ tone: phaseTone(phase),
119
+ title: phaseTitle(phase, catalog.length),
120
+ body: browsing
121
+ ? "Browse → preview → confirm. Confirm records intent — does not install packages."
122
+ : (phase === SETTINGS_PHASE.PREVIEW ? "No filesystem changes. Enter opens confirm." : "")
123
+ },
124
+ items,
125
+ selectedIndex: browsing && catalog.length > 0 ? windowed.selectedIndex : -1,
126
+ focusedId: focused?.id ?? null,
127
+ total: catalog.length,
128
+ start: windowed.start,
129
+ listLimit: limit,
130
+ listFocused: browsing && catalog.length > 0,
131
+ entry,
132
+ details: detailing ? buildDetailsLines(entry) : [],
133
+ detailsOpen: detailing,
134
+ confirm: phase === SETTINGS_PHASE.CONFIRMING
135
+ ? {
136
+ summary: entry
137
+ ? `Record install intent for ${entry.name}. Does not install packages.`
138
+ : "Record install intent. Does not install packages.",
139
+ primaryLabel: "Confirm intent"
140
+ }
141
+ : null,
142
+ receipt: phase === SETTINGS_PHASE.COMPLETED && settingsAction?.receipt
143
+ ? { title: "Receipt", lines: receiptLines(settingsAction.receipt, entry) }
144
+ : null,
145
+ profilePolicy: browsing ? profilePolicyItems(snapshot, diagnostics) : [],
146
+ keyHints: settingsKeyHints(phase)
147
+ };
148
+ }
149
+
150
+ export function SemanticSettingsPanel({
151
+ integrations = listCuratedIntegrations(), listIndex = 0, settingsAction = null,
152
+ layoutMode = LAYOUT_MODES.COMPACT, contentFocused = false, colorEnabled = true, unicode = true,
153
+ snapshot = null, diagnostics = null
154
+ }) {
155
+ const model = adaptSettingsModel({
156
+ integrations, listIndex, settingsAction, layoutMode, snapshot, diagnostics
157
+ });
158
+ const listFocused = contentFocused && model.listFocused;
159
+ return React.createElement(Box, { flexDirection: "column" },
160
+ model.receipt && React.createElement(Receipt, {
161
+ title: model.receipt.title, lines: model.receipt.lines, colorEnabled
162
+ }),
163
+ React.createElement(Text, {
164
+ bold: true, color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
165
+ }, model.title),
166
+ React.createElement(Callout, {
167
+ tone: model.callout.tone, title: model.callout.title,
168
+ body: model.callout.body || undefined, colorEnabled, compact: true
169
+ }),
170
+ model.confirm && React.createElement(Confirm, {
171
+ summary: model.confirm.summary, primaryLabel: model.confirm.primaryLabel,
172
+ focused: false, colorEnabled, mark: " "
173
+ }),
174
+ model.phase === SETTINGS_PHASE.BROWSE && React.createElement(ActionList, {
175
+ items: model.items, selectedIndex: model.selectedIndex,
176
+ focused: listFocused, colorEnabled, unicode
177
+ }),
178
+ model.profilePolicy.length > 0 && React.createElement(Box, { flexDirection: "column" },
179
+ React.createElement(Text, { bold: true }, "Profile & Policy"),
180
+ React.createElement(ActionList, {
181
+ items: model.profilePolicy, selectedIndex: -1, focused: false, colorEnabled, unicode
182
+ })
183
+ ),
184
+ model.detailsOpen && React.createElement(Details, {
185
+ open: true, summary: "Details", lines: model.details,
186
+ colorEnabled, focused: false, mark: " "
187
+ })
188
+ );
189
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Live semantic Setup. Splash stays separate.
3
+ * Ownership: Callout=status · list/Confirm=decision · KeyBar=keys.
4
+ * Focus mark only on ActionList (Enter/Space executable); Confirm is Y/N/Esc via KeyBar.
5
+ */
6
+ import React from "react";
7
+ import { Box, Text } from "ink";
8
+ import { AGENT_HINTS, WIZARD_COPY, getAgentLabel } from "../../brand/index.js";
9
+ import { LAYOUT_MODES } from "../layout.js";
10
+ import { COCKPIT_COLORS } from "../theme.js";
11
+ import {
12
+ SETUP_STEPS, formatInkPreviewLines, setupPreviewLineLimit, windowSetupLines
13
+ } from "../setup-state.js";
14
+ import { ActionList, Callout, Confirm, KeyBar, Stepper } from "./semantic.js";
15
+
16
+ export const SETUP_STEPPER_STEPS = [
17
+ { id: SETUP_STEPS.DETECT, label: "Detect" },
18
+ { id: SETUP_STEPS.AGENTS, label: "Agents" },
19
+ { id: SETUP_STEPS.COMPONENTS, label: "Components" },
20
+ { id: SETUP_STEPS.PREVIEW, label: "Preview" },
21
+ { id: SETUP_STEPS.CONFIRM, label: "Confirm" }
22
+ ];
23
+
24
+ export function setupStepperIndex(step) {
25
+ return SETUP_STEPPER_STEPS.findIndex((entry) => entry.id === step);
26
+ }
27
+
28
+ export function setupKeyHints(step, { previewReady = false, dryRun = false } = {}) {
29
+ if (step === SETUP_STEPS.AGENTS || step === SETUP_STEPS.COMPONENTS) {
30
+ return [
31
+ { keys: "↑↓", label: "Move" }, { keys: "Space", label: "Toggle" },
32
+ { keys: "Enter", label: "Continue" }, { keys: "Esc", label: "Cancel" }
33
+ ];
34
+ }
35
+ if (step === SETUP_STEPS.CONFIRM) {
36
+ return [
37
+ { keys: "Y", label: dryRun ? "Continue" : "Apply" },
38
+ { keys: "N", label: "Cancel" },
39
+ { keys: "Esc", label: "Cancel" }
40
+ ];
41
+ }
42
+ if (step === SETUP_STEPS.PREVIEW && !previewReady) return [{ keys: "Esc", label: "Cancel" }];
43
+ if (step === SETUP_STEPS.DETECT || step === SETUP_STEPS.PREVIEW) {
44
+ return [{ keys: "Enter", label: "Continue" }, { keys: "Esc", label: "Cancel" }];
45
+ }
46
+ return [{ keys: "Esc", label: "Cancel" }];
47
+ }
48
+
49
+ function buildCallout(step, { adapters = [], detected = [], previewLoading = false, previewError = null } = {}) {
50
+ if (step === SETUP_STEPS.DETECT) {
51
+ return {
52
+ tone: "info", title: WIZARD_COPY.detectTitle,
53
+ body: `Your agents · ${detected.length}/${adapters.length} roots found`
54
+ };
55
+ }
56
+ if (step === SETUP_STEPS.AGENTS) {
57
+ return { tone: "info", title: "Agents", body: WIZARD_COPY.agentsPrompt };
58
+ }
59
+ if (step === SETUP_STEPS.COMPONENTS) {
60
+ return { tone: "info", title: "Components", body: WIZARD_COPY.componentsPrompt };
61
+ }
62
+ if (step === SETUP_STEPS.PREVIEW) {
63
+ if (previewLoading) return { tone: "warn", title: WIZARD_COPY.previewTitle, body: "Building preview…" };
64
+ if (previewError) return { tone: "danger", title: WIZARD_COPY.previewTitle, body: String(previewError) };
65
+ return { tone: "info", title: WIZARD_COPY.previewTitle, body: "" };
66
+ }
67
+ if (step === SETUP_STEPS.CONFIRM) return { tone: "warn", title: "Confirm", body: "" };
68
+ return { tone: "info", title: "Setup", body: "" };
69
+ }
70
+
71
+ function buildListItems(step, {
72
+ agentOptions, componentOptions, selectedAgents, selectedComponents, adapters, detected
73
+ }) {
74
+ if (step === SETUP_STEPS.AGENTS || step === SETUP_STEPS.COMPONENTS) {
75
+ const options = step === SETUP_STEPS.AGENTS ? agentOptions : componentOptions;
76
+ const selected = step === SETUP_STEPS.AGENTS ? selectedAgents : selectedComponents;
77
+ return options.map((option) => ({
78
+ id: option.id,
79
+ label: `${selected.includes(option.id) ? "[x]" : "[ ]"} ${option.label}`,
80
+ hint: option.hint
81
+ }));
82
+ }
83
+ if (step !== SETUP_STEPS.DETECT) return [];
84
+ return adapters.map((adapter) => ({
85
+ id: adapter.id,
86
+ label: `${getAgentLabel(adapter.id)} · ${
87
+ detected.includes(adapter.id) ? AGENT_HINTS.ready : AGENT_HINTS.notDetected
88
+ }`
89
+ }));
90
+ }
91
+
92
+ /** Pure adapter for Detect→Agents→Components→Preview→Confirm (no splash). */
93
+ export function adaptSetupModel({
94
+ step = SETUP_STEPS.DETECT, activeIndex = 0, agentOptions = [], componentOptions = [],
95
+ componentCatalog = [], selectedAgents = [], selectedComponents = [], adapters = [],
96
+ detected = [], preview = null, previewLoading = false, previewError = null,
97
+ dryRun = false, layoutMode = LAYOUT_MODES.COMPACT
98
+ } = {}) {
99
+ const listFocused = step === SETUP_STEPS.AGENTS || step === SETUP_STEPS.COMPONENTS;
100
+ const previewReady = Boolean(preview) && !previewLoading && !previewError;
101
+ const catalog = componentCatalog.length > 0 ? componentCatalog : componentOptions;
102
+ return {
103
+ steps: SETUP_STEPPER_STEPS,
104
+ stepIndex: Math.max(0, setupStepperIndex(step)),
105
+ callout: buildCallout(step, { adapters, detected, previewLoading, previewError }),
106
+ listItems: buildListItems(step, {
107
+ agentOptions, componentOptions, selectedAgents, selectedComponents, adapters, detected
108
+ }),
109
+ listSelectedIndex: listFocused ? activeIndex : -1,
110
+ listFocused,
111
+ previewLines: previewReady
112
+ ? windowSetupLines(
113
+ formatInkPreviewLines({ preview, componentCatalog: catalog }),
114
+ setupPreviewLineLimit(layoutMode)
115
+ )
116
+ : [],
117
+ confirm: step === SETUP_STEPS.CONFIRM
118
+ ? {
119
+ summary: dryRun ? WIZARD_COPY.confirmDryRun : WIZARD_COPY.confirmApply,
120
+ primaryLabel: dryRun ? "Continue dry run" : "Apply plan"
121
+ }
122
+ : null,
123
+ keyHints: setupKeyHints(step, { previewReady, dryRun }),
124
+ // Confirm is Y/N/Esc only — no focus mark (Enter is not executable here).
125
+ focusSurface: listFocused ? "list" : "none"
126
+ };
127
+ }
128
+
129
+ export function SemanticSetupPanel(props) {
130
+ const {
131
+ colorEnabled = true, unicode = true, columns = 80, layoutMode = LAYOUT_MODES.COMPACT, ...rest
132
+ } = props;
133
+ const view = adaptSetupModel({ ...rest, layoutMode });
134
+ const muted = colorEnabled ? COCKPIT_COLORS.muted : undefined;
135
+ return React.createElement(Box, { flexDirection: "column" },
136
+ React.createElement(Stepper, {
137
+ steps: view.steps, currentIndex: view.stepIndex, colorEnabled, unicode
138
+ }),
139
+ React.createElement(Callout, {
140
+ tone: view.callout.tone, title: view.callout.title,
141
+ body: view.callout.body || undefined, colorEnabled, compact: true
142
+ }),
143
+ view.listItems.length > 0
144
+ ? React.createElement(ActionList, {
145
+ items: view.listItems, selectedIndex: view.listSelectedIndex,
146
+ focused: view.listFocused, colorEnabled, unicode
147
+ })
148
+ : null,
149
+ ...view.previewLines.map((line, index) =>
150
+ React.createElement(Text, { key: `p${index}`, color: muted }, line || " ")
151
+ ),
152
+ view.confirm
153
+ ? React.createElement(Confirm, {
154
+ summary: view.confirm.summary, primaryLabel: view.confirm.primaryLabel,
155
+ focused: false, colorEnabled, mark: " "
156
+ })
157
+ : null,
158
+ React.createElement(KeyBar, { hints: view.keyHints, colorEnabled, columns })
159
+ );
160
+ }