@kal-elsam/kairo-runtime 0.9.0 → 0.11.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 (33) 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/brand/wordmark.js +50 -0
  8. package/src/global/ink/cockpit/primitives.js +72 -50
  9. package/src/global/ink/cockpit-changes.js +2 -2
  10. package/src/global/ink/cockpit-control-center.js +12 -64
  11. package/src/global/ink/cockpit-controller.js +41 -5
  12. package/src/global/ink/cockpit-enter.js +1 -0
  13. package/src/global/ink/cockpit-models.js +14 -7
  14. package/src/global/ink/cockpit-palette.js +18 -7
  15. package/src/global/ink/cockpit-recovery.js +9 -6
  16. package/src/global/ink/cockpit-usage.js +111 -0
  17. package/src/global/ink/cockpit-views.js +70 -97
  18. package/src/global/ink/orchestrator-app.js +44 -4
  19. package/src/global/ink/setup-app.js +55 -72
  20. package/src/global/ink/setup-state.js +16 -0
  21. package/src/global/ink/theme.js +40 -8
  22. package/src/global/ink/ux/live-activity.js +191 -0
  23. package/src/global/ink/ux/live-alerts.js +156 -0
  24. package/src/global/ink/ux/live-governance.js +191 -0
  25. package/src/global/ink/ux/live-orchestration.js +185 -0
  26. package/src/global/ink/ux/live-overview.js +175 -0
  27. package/src/global/ink/ux/live-settings.js +186 -0
  28. package/src/global/ink/ux/live-setup.js +160 -0
  29. package/src/global/ink/ux/live-usage.js +49 -0
  30. package/src/global/ink/ux/semantic.js +101 -0
  31. package/src/global/ink/ux/task-flow-app.js +85 -0
  32. package/src/global/ink/ux/task-flow.js +173 -0
  33. package/src/global/orchestrator.js +37 -20
@@ -0,0 +1,186 @@
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 { LAYOUT_MODES } from "../layout.js";
8
+ import {
9
+ SETTINGS_PHASE, getCuratedIntegration, listCuratedIntegrations
10
+ } from "../cockpit-settings.js";
11
+ import { ActionList, Callout, Confirm, Details, Receipt, SectionLabel, ViewTitle } from "./semantic.js";
12
+ import { windowSlice } from "./live-activity.js";
13
+
14
+ export function settingsListLimit(layoutMode = LAYOUT_MODES.COMPACT) {
15
+ return layoutMode === LAYOUT_MODES.WIDE ? 8 : 3;
16
+ }
17
+
18
+ /** Key ownership lives in shell KeyBar/footer — Confirm never owns Y/N/Esc. */
19
+ export function settingsKeyHints(phase = SETTINGS_PHASE.BROWSE) {
20
+ if (phase === SETTINGS_PHASE.PREVIEW) {
21
+ return [{ keys: "Enter", label: "Confirm" }, { keys: "Esc", label: "Back" }];
22
+ }
23
+ if (phase === SETTINGS_PHASE.CONFIRMING) {
24
+ return [
25
+ { keys: "Y", label: "Confirm" }, { keys: "N", label: "Cancel" },
26
+ { keys: "Esc", label: "Cancel" }
27
+ ];
28
+ }
29
+ if (phase === SETTINGS_PHASE.COMPLETED) {
30
+ return [{ keys: "Esc", label: "Back" }, { keys: "/", label: "Actions" }];
31
+ }
32
+ return [
33
+ { keys: "↑↓", label: "Select" }, { keys: "Enter", label: "Preview" },
34
+ { keys: "Esc", label: "Nav" }, { keys: "/", label: "Actions" }
35
+ ];
36
+ }
37
+
38
+ function entryLabel(entry) {
39
+ return `${entry.status} · ${entry.name} · ${entry.version} · ${entry.license}`;
40
+ }
41
+
42
+ function resolveEntry(integrations, selectedId) {
43
+ if (!selectedId) return null;
44
+ return integrations.find((e) => e.id === selectedId) ?? getCuratedIntegration(selectedId);
45
+ }
46
+
47
+ function phaseTone(phase) {
48
+ if (phase === SETTINGS_PHASE.COMPLETED) return "ready";
49
+ if (phase === SETTINGS_PHASE.CONFIRMING || phase === SETTINGS_PHASE.PREVIEW) return "warn";
50
+ return "info";
51
+ }
52
+
53
+ function phaseTitle(phase, total) {
54
+ if (phase === SETTINGS_PHASE.PREVIEW) return "Preview integration";
55
+ if (phase === SETTINGS_PHASE.CONFIRMING) return "Confirm intent";
56
+ if (phase === SETTINGS_PHASE.COMPLETED) return "Intent recorded";
57
+ return total === 0 ? "No curated integrations" : `${total} curated integration${total === 1 ? "" : "s"}`;
58
+ }
59
+
60
+ function buildDetailsLines(entry) {
61
+ if (!entry) return ["Integration not found."];
62
+ return [
63
+ `License · ${entry.license}`, `Audit · ${entry.audit}`,
64
+ `Capabilities · ${entry.capabilities?.join(" · ") || "none"}`,
65
+ `Permissions · ${entry.permissions?.join(" · ") || "none"}`,
66
+ entry.summary, entry.notes
67
+ ].filter(Boolean);
68
+ }
69
+
70
+ function receiptLines(receipt, entry) {
71
+ if (!receipt) return [];
72
+ return [
73
+ `Id · ${receipt.id} · wroteFiles · ${receipt.wroteFiles}`,
74
+ `Confirmed · ${receipt.confirmedAt}`,
75
+ entry ? `${entry.name} · ${entry.version} · ${entry.license}` : null,
76
+ "Confirm records intent — does not install packages."
77
+ ].filter(Boolean);
78
+ }
79
+
80
+ /** Browse-only: profile · apply · preflight · sources. */
81
+ function profilePolicyItems(snapshot = null, diagnostics = null) {
82
+ const p = snapshot?.policy, s = diagnostics?.profile?.sources;
83
+ const src = [s?.global && "global", s?.project && "project"].filter(Boolean).join(", ") || "none";
84
+ return [
85
+ { id: "policy", label: `Policy · ${p?.profile ?? "none"} · apply ${p?.applyMode ?? "n/a"}` },
86
+ { id: "preflight", label: `Preflight · ${p?.preflight ?? "n/a"} · sources · ${src}` }
87
+ ];
88
+ }
89
+
90
+ /** Pure adapter: browse window · preview Details · confirm intent · receipt first. */
91
+ export function adaptSettingsModel({
92
+ integrations = listCuratedIntegrations(), listIndex = 0, settingsAction = null,
93
+ layoutMode = LAYOUT_MODES.COMPACT, snapshot = null, diagnostics = null
94
+ } = {}) {
95
+ const phase = settingsAction?.phase ?? SETTINGS_PHASE.BROWSE;
96
+ const catalog = Array.isArray(integrations) ? integrations : [];
97
+ const limit = settingsListLimit(layoutMode);
98
+ const windowed = windowSlice(catalog, listIndex, limit);
99
+ const browsing = phase === SETTINGS_PHASE.BROWSE;
100
+ const safe = catalog.length > 0
101
+ ? Math.min(Math.max(0, listIndex), catalog.length - 1) : -1;
102
+ const focused = browsing && safe >= 0 ? catalog[safe] : null;
103
+ const entry = resolveEntry(catalog, settingsAction?.selectedId)
104
+ ?? (browsing ? focused : null);
105
+ const detailing = phase === SETTINGS_PHASE.PREVIEW || phase === SETTINGS_PHASE.CONFIRMING;
106
+ const items = catalog.length === 0
107
+ ? [{ id: "empty", label: "No curated integrations available." }]
108
+ : windowed.items.map((item, i) => ({
109
+ id: item.id ?? `integration-${windowed.start + i}`,
110
+ label: entryLabel(item)
111
+ }));
112
+
113
+ return {
114
+ title: "Settings",
115
+ phase,
116
+ callout: {
117
+ tone: phaseTone(phase),
118
+ title: phaseTitle(phase, catalog.length),
119
+ body: browsing
120
+ ? "Browse → preview → confirm. Confirm records intent — does not install packages."
121
+ : (phase === SETTINGS_PHASE.PREVIEW ? "No filesystem changes. Enter opens confirm." : "")
122
+ },
123
+ items,
124
+ selectedIndex: browsing && catalog.length > 0 ? windowed.selectedIndex : -1,
125
+ focusedId: focused?.id ?? null,
126
+ total: catalog.length,
127
+ start: windowed.start,
128
+ listLimit: limit,
129
+ listFocused: browsing && catalog.length > 0,
130
+ entry,
131
+ details: detailing ? buildDetailsLines(entry) : [],
132
+ detailsOpen: detailing,
133
+ confirm: phase === SETTINGS_PHASE.CONFIRMING
134
+ ? {
135
+ summary: entry
136
+ ? `Record install intent for ${entry.name}. Does not install packages.`
137
+ : "Record install intent. Does not install packages.",
138
+ primaryLabel: "Confirm intent"
139
+ }
140
+ : null,
141
+ receipt: phase === SETTINGS_PHASE.COMPLETED && settingsAction?.receipt
142
+ ? { title: "Receipt", lines: receiptLines(settingsAction.receipt, entry) }
143
+ : null,
144
+ profilePolicy: browsing ? profilePolicyItems(snapshot, diagnostics) : [],
145
+ keyHints: settingsKeyHints(phase)
146
+ };
147
+ }
148
+
149
+ export function SemanticSettingsPanel({
150
+ integrations = listCuratedIntegrations(), listIndex = 0, settingsAction = null,
151
+ layoutMode = LAYOUT_MODES.COMPACT, contentFocused = false, colorEnabled = true, unicode = true,
152
+ snapshot = null, diagnostics = null
153
+ }) {
154
+ const model = adaptSettingsModel({
155
+ integrations, listIndex, settingsAction, layoutMode, snapshot, diagnostics
156
+ });
157
+ const listFocused = contentFocused && model.listFocused;
158
+ return React.createElement(Box, { flexDirection: "column" },
159
+ model.receipt && React.createElement(Receipt, {
160
+ title: model.receipt.title, lines: model.receipt.lines, colorEnabled
161
+ }),
162
+ React.createElement(ViewTitle, { colorEnabled }, model.title),
163
+ React.createElement(Callout, {
164
+ tone: model.callout.tone, title: model.callout.title,
165
+ body: model.callout.body || undefined, colorEnabled, compact: true
166
+ }),
167
+ model.confirm && React.createElement(Confirm, {
168
+ summary: model.confirm.summary, primaryLabel: model.confirm.primaryLabel,
169
+ focused: false, colorEnabled, mark: " "
170
+ }),
171
+ model.phase === SETTINGS_PHASE.BROWSE && React.createElement(ActionList, {
172
+ items: model.items, selectedIndex: model.selectedIndex,
173
+ focused: listFocused, colorEnabled, unicode
174
+ }),
175
+ model.profilePolicy.length > 0 && React.createElement(Box, { flexDirection: "column" },
176
+ React.createElement(SectionLabel, { colorEnabled }, "Profile & Policy"),
177
+ React.createElement(ActionList, {
178
+ items: model.profilePolicy, selectedIndex: -1, focused: false, colorEnabled, unicode
179
+ })
180
+ ),
181
+ model.detailsOpen && React.createElement(Details, {
182
+ open: true, summary: "Details", lines: model.details,
183
+ colorEnabled, focused: false, mark: " "
184
+ })
185
+ );
186
+ }
@@ -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
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Live semantic Usage panel — renderer only; model lives in cockpit-usage.js.
3
+ * ActionList never owns focus.
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 { adaptUsageModel } from "../cockpit-usage.js";
10
+ import { ActionList, Callout, SectionLabel, ViewTitle } from "./semantic.js";
11
+
12
+ export function SemanticUsagePanel({
13
+ snapshot = null,
14
+ dashboard = null,
15
+ layoutMode = LAYOUT_MODES.COMPACT,
16
+ colorEnabled = true,
17
+ unicode = true
18
+ }) {
19
+ const model = adaptUsageModel({ snapshot, dashboard, layoutMode });
20
+ const muted = colorEnabled ? COCKPIT_COLORS.muted : undefined;
21
+ return React.createElement(Box, { flexDirection: "column" },
22
+ React.createElement(ViewTitle, { colorEnabled }, model.title),
23
+ React.createElement(Callout, {
24
+ tone: model.callout.tone,
25
+ title: model.callout.title,
26
+ body: model.callout.body || undefined,
27
+ colorEnabled,
28
+ compact: true
29
+ }),
30
+ React.createElement(SectionLabel, { colorEnabled }, "Measured"),
31
+ React.createElement(Text, null, ` ${model.measured}`),
32
+ React.createElement(SectionLabel, { colorEnabled }, "Configured limits"),
33
+ React.createElement(Text, null, ` ${model.configured}`),
34
+ React.createElement(SectionLabel, { colorEnabled }, "Run usage"),
35
+ model.runs.length === 0
36
+ ? React.createElement(Text, null, " No auditable run tokenUsage yet.")
37
+ : React.createElement(ActionList, {
38
+ items: model.runs,
39
+ selectedIndex: -1,
40
+ focused: false,
41
+ colorEnabled,
42
+ unicode
43
+ }),
44
+ model.moreLine
45
+ ? React.createElement(Text, { color: muted }, model.moreLine)
46
+ : null,
47
+ React.createElement(Text, { color: muted }, model.footnote)
48
+ );
49
+ }
@@ -0,0 +1,101 @@
1
+ /** Semantic Ink primitives — structured models, not string-array panels. */
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { COCKPIT_COLORS, statusColor, resolveGlyphs } from "../theme.js";
5
+
6
+ const mute = (on) => (on ? COCKPIT_COLORS.muted : undefined);
7
+ const ice = (on) => (on ? COCKPIT_COLORS.interactive : undefined);
8
+
9
+ /** Short typographic view title — brand amber, no border. */
10
+ export function ViewTitle({ children, colorEnabled = true }) {
11
+ return React.createElement(Text, {
12
+ bold: true,
13
+ color: colorEnabled ? COCKPIT_COLORS.brand : undefined
14
+ }, children);
15
+ }
16
+
17
+ export function ActionList({ items = [], selectedIndex = 0, focused = true, colorEnabled = true, unicode = true }) {
18
+ const g = resolveGlyphs(unicode);
19
+ return React.createElement(Box, { flexDirection: "column" },
20
+ ...items.map((item, i) => {
21
+ const sel = i === selectedIndex;
22
+ return React.createElement(Text, {
23
+ key: item.id ?? String(i),
24
+ bold: sel,
25
+ color: sel && focused && colorEnabled ? COCKPIT_COLORS.interactive : undefined
26
+ }, `${sel && focused ? g.focus : " "} ${item.label}${item.hint ? ` ${item.hint}` : ""}`);
27
+ })
28
+ );
29
+ }
30
+
31
+ export function Stepper({ steps = [], currentIndex = 0, colorEnabled = true, unicode = true }) {
32
+ // Progress marks: not focus `>` and not failure-looking `x`.
33
+ const done = unicode ? "✓" : "+";
34
+ const current = unicode ? "●" : "*";
35
+ const idle = unicode ? "·" : "-";
36
+ return React.createElement(Box, { flexDirection: "column" },
37
+ ...steps.map((step, i) => {
38
+ const isDone = i < currentIndex;
39
+ const cur = i === currentIndex;
40
+ const color = isDone
41
+ ? (colorEnabled ? COCKPIT_COLORS.success : undefined)
42
+ : cur ? ice(colorEnabled) : mute(colorEnabled);
43
+ return React.createElement(Text, {
44
+ key: step.id ?? String(i), bold: cur, color
45
+ }, `${isDone ? done : cur ? current : idle} ${i + 1}. ${step.label}`);
46
+ })
47
+ );
48
+ }
49
+
50
+ export function Callout({ tone = "info", title, body, colorEnabled = true, compact = false }) {
51
+ const kind = tone === "danger" ? "danger" : tone === "warn" ? "warn" : "ready";
52
+ return React.createElement(Box, {
53
+ flexDirection: "column",
54
+ marginY: compact ? 0 : 1
55
+ },
56
+ React.createElement(Text, { bold: true, color: statusColor(kind, { colorEnabled }) }, title),
57
+ body ? React.createElement(Text, null, body) : null
58
+ );
59
+ }
60
+
61
+ export function Confirm({ summary, primaryLabel = "Confirm", focused = true, colorEnabled = true, mark = " " }) {
62
+ return React.createElement(Box, { flexDirection: "column", marginY: 1 },
63
+ summary ? React.createElement(Text, null, summary) : null,
64
+ React.createElement(Text, {
65
+ bold: focused,
66
+ color: focused && colorEnabled ? COCKPIT_COLORS.interactive : undefined
67
+ }, `${mark} ${primaryLabel}`)
68
+ );
69
+ }
70
+
71
+ export function Receipt({ title = "Receipt", lines = [], colorEnabled = true }) {
72
+ return React.createElement(Box, { flexDirection: "column", marginY: 1 },
73
+ React.createElement(Text, { bold: true, color: colorEnabled ? COCKPIT_COLORS.success : undefined }, title),
74
+ ...lines.map((line, i) => React.createElement(Text, { key: `r${i}`, color: mute(colorEnabled) }, line))
75
+ );
76
+ }
77
+
78
+ export function Details({ open = false, summary = "Details", lines = [], colorEnabled = true, focused = false, mark = " " }) {
79
+ const color = focused && colorEnabled ? COCKPIT_COLORS.interactive : mute(colorEnabled);
80
+ if (!open) return React.createElement(Text, { bold: focused, color }, `${mark} ${summary} · Space`);
81
+ return React.createElement(Box, { flexDirection: "column" },
82
+ React.createElement(Text, { bold: true, color }, `${mark} ${summary}`),
83
+ ...lines.map((line, i) => React.createElement(Text, { key: `d${i}`, color: mute(colorEnabled) }, line))
84
+ );
85
+ }
86
+
87
+ export function KeyBar({ hints = [], colorEnabled = true, columns = 80 }) {
88
+ const width = Math.max(24, Math.min(Number(columns) || 80, 120));
89
+ return React.createElement(Box, { width },
90
+ React.createElement(Text, { color: mute(colorEnabled) },
91
+ hints.map((h) => `${h.keys} ${h.label}`).join(" · "))
92
+ );
93
+ }
94
+
95
+ /** Section label inside a panel — muted brand hierarchy under ViewTitle. */
96
+ export function SectionLabel({ children, colorEnabled = true }) {
97
+ return React.createElement(Text, {
98
+ bold: true,
99
+ color: colorEnabled ? COCKPIT_COLORS.muted : undefined
100
+ }, children);
101
+ }
@@ -0,0 +1,85 @@
1
+ import React, { useEffect, useReducer } from "react";
2
+ import { Box, Text, useApp, useInput, useStdout } from "ink";
3
+ import { COCKPIT_COLORS, resolveGlyphs } from "../theme.js";
4
+ import { resolveTerminalCapabilities } from "../terminal-capabilities.js";
5
+ import { ActionList, Callout, Confirm, Details, KeyBar, Receipt, Stepper } from "./semantic.js";
6
+ import {
7
+ FOCUS, SCREENS, createTaskFlowState, keyHintsFor, modelForState, reduceTaskFlow, resolvePrimaryPresentation
8
+ } from "./task-flow.js";
9
+
10
+ export function TaskFlowApp({ columns: cols, rows: rowCount, onExit } = {}) {
11
+ const { exit } = useApp();
12
+ const { stdout } = useStdout();
13
+ const [state, dispatch] = useReducer(
14
+ reduceTaskFlow,
15
+ createTaskFlowState({ columns: cols ?? stdout?.columns ?? 80, rows: rowCount ?? stdout?.rows ?? 24 })
16
+ );
17
+ const caps = resolveTerminalCapabilities({
18
+ columns: state.columns, rows: state.rows, env: process.env, isTTY: true
19
+ });
20
+ const model = modelForState(state);
21
+ const primary = resolvePrimaryPresentation(model, state, caps.unicode);
22
+ const detailsMark = state.focus === FOCUS.DETAILS ? resolveGlyphs(caps.unicode).focus : " ";
23
+ const accent = caps.color ? COCKPIT_COLORS.primary : undefined;
24
+
25
+ useEffect(() => {
26
+ if (!stdout) return undefined;
27
+ const onResize = () => dispatch({ type: "resize", columns: stdout.columns ?? 80, rows: stdout.rows ?? 24 });
28
+ stdout.on("resize", onResize);
29
+ return () => stdout.off("resize", onResize);
30
+ }, [stdout]);
31
+
32
+ useEffect(() => {
33
+ if (!state.exited) return;
34
+ onExit?.({ reason: "escape" });
35
+ exit();
36
+ }, [state.exited, exit, onExit]);
37
+
38
+ useInput((input, key) => {
39
+ if (key.upArrow) dispatch({ type: "up" });
40
+ else if (key.downArrow) dispatch({ type: "down" });
41
+ else if (key.return) dispatch({ type: "enter" });
42
+ else if (key.escape) dispatch({ type: "escape" });
43
+ else if (input === " ") dispatch({ type: "space" });
44
+ else if (input === "/") dispatch({ type: "slash" });
45
+ });
46
+
47
+ return React.createElement(Box, { flexDirection: "column", width: state.columns },
48
+ React.createElement(Text, { bold: true, color: accent }, `Kairo · ${model.title} · ${state.layout}`),
49
+ React.createElement(Callout, { ...model.callout, colorEnabled: caps.color }),
50
+ state.screen === SCREENS.SETUP
51
+ ? React.createElement(Stepper, {
52
+ steps: model.steps, currentIndex: model.stepIndex, colorEnabled: caps.color, unicode: caps.unicode
53
+ })
54
+ : null,
55
+ primary.mode === "confirm"
56
+ ? React.createElement(Confirm, {
57
+ summary: primary.summary, primaryLabel: primary.label,
58
+ focused: state.focus === FOCUS.PRIMARY, colorEnabled: caps.color, mark: primary.mark
59
+ })
60
+ : React.createElement(Box, { flexDirection: "column" },
61
+ React.createElement(Text, {
62
+ bold: state.focus === FOCUS.PRIMARY,
63
+ color: state.focus === FOCUS.PRIMARY ? accent : undefined
64
+ }, `${primary.mark} ${primary.label}`),
65
+ primary.detail
66
+ ? React.createElement(Text, { color: caps.color ? COCKPIT_COLORS.muted : undefined }, primary.detail)
67
+ : null
68
+ ),
69
+ model.receipt ? React.createElement(Receipt, { ...model.receipt, colorEnabled: caps.color }) : null,
70
+ (model.secondary || model.metrics)
71
+ ? React.createElement(ActionList, {
72
+ items: model.secondary ?? model.metrics,
73
+ selectedIndex: state.listIndex,
74
+ focused: state.focus === FOCUS.LIST,
75
+ colorEnabled: caps.color,
76
+ unicode: caps.unicode
77
+ })
78
+ : null,
79
+ React.createElement(Details, {
80
+ open: state.detailsOpen, summary: "Details", lines: model.details,
81
+ colorEnabled: caps.color, focused: state.focus === FOCUS.DETAILS, mark: detailsMark
82
+ }),
83
+ React.createElement(KeyBar, { hints: keyHintsFor(state), colorEnabled: caps.color, columns: state.columns })
84
+ );
85
+ }