@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.
- package/README.md +14 -8
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +3 -3
- package/scripts/ux-prototype-tty.mjs +9 -0
- package/src/global/control-plane-proposals.js +2 -1
- package/src/global/control-plane-snapshot.js +1 -1
- package/src/global/ink/brand/wordmark.js +50 -0
- package/src/global/ink/cockpit/primitives.js +72 -50
- package/src/global/ink/cockpit-changes.js +2 -2
- package/src/global/ink/cockpit-control-center.js +12 -64
- package/src/global/ink/cockpit-controller.js +41 -5
- package/src/global/ink/cockpit-enter.js +1 -0
- package/src/global/ink/cockpit-models.js +14 -7
- package/src/global/ink/cockpit-palette.js +18 -7
- package/src/global/ink/cockpit-recovery.js +9 -6
- package/src/global/ink/cockpit-usage.js +111 -0
- package/src/global/ink/cockpit-views.js +70 -97
- package/src/global/ink/orchestrator-app.js +44 -4
- package/src/global/ink/setup-app.js +55 -72
- package/src/global/ink/setup-state.js +16 -0
- package/src/global/ink/theme.js +40 -8
- package/src/global/ink/ux/live-activity.js +191 -0
- package/src/global/ink/ux/live-alerts.js +156 -0
- package/src/global/ink/ux/live-governance.js +191 -0
- package/src/global/ink/ux/live-orchestration.js +185 -0
- package/src/global/ink/ux/live-overview.js +175 -0
- package/src/global/ink/ux/live-settings.js +186 -0
- package/src/global/ink/ux/live-setup.js +160 -0
- package/src/global/ink/ux/live-usage.js +49 -0
- package/src/global/ink/ux/semantic.js +101 -0
- package/src/global/ink/ux/task-flow-app.js +85 -0
- package/src/global/ink/ux/task-flow.js +173 -0
- package/src/global/orchestrator.js +37 -20
|
@@ -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
|
+
}
|