@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.
- package/README.md +41 -15
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +6 -6
- package/scripts/ux-prototype-tty.mjs +9 -0
- package/src/cli.js +24 -1
- package/src/global/control-plane-proposals.js +2 -1
- package/src/global/control-plane-snapshot.js +1 -1
- package/src/global/global-doctor.js +2 -0
- package/src/global/ink/cockpit/primitives.js +127 -50
- package/src/global/ink/cockpit-alerts.js +36 -0
- package/src/global/ink/cockpit-changes.js +61 -37
- package/src/global/ink/cockpit-control-center.js +79 -53
- package/src/global/ink/cockpit-controller.js +98 -15
- package/src/global/ink/cockpit-enter.js +1 -0
- package/src/global/ink/cockpit-focus.js +4 -2
- package/src/global/ink/cockpit-models.js +100 -51
- package/src/global/ink/cockpit-palette.js +109 -0
- package/src/global/ink/cockpit-path-label.js +19 -0
- package/src/global/ink/cockpit-recovery.js +84 -18
- package/src/global/ink/cockpit-reviews.js +14 -10
- package/src/global/ink/cockpit-runs.js +13 -4
- package/src/global/ink/cockpit-settings.js +194 -0
- package/src/global/ink/cockpit-usage.js +111 -0
- package/src/global/ink/cockpit-views.js +119 -116
- package/src/global/ink/orchestrator-app.js +169 -46
- package/src/global/ink/orchestrator-state.js +24 -14
- package/src/global/ink/setup-app.js +55 -72
- package/src/global/ink/setup-state.js +16 -0
- package/src/global/ink/theme.js +27 -0
- package/src/global/ink/use-orchestrator-data.js +58 -0
- package/src/global/ink/ux/live-activity.js +194 -0
- package/src/global/ink/ux/live-alerts.js +159 -0
- package/src/global/ink/ux/live-governance.js +195 -0
- package/src/global/ink/ux/live-orchestration.js +188 -0
- package/src/global/ink/ux/live-overview.js +125 -0
- package/src/global/ink/ux/live-settings.js +189 -0
- package/src/global/ink/ux/live-setup.js +160 -0
- package/src/global/ink/ux/live-usage.js +51 -0
- package/src/global/ink/ux/semantic.js +84 -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
- package/src/global/paths.js +3 -0
- package/src/global/runtime/alerts/alert-store.js +216 -0
- package/src/global/runtime/alerts/alert-types.js +59 -0
- package/src/global/runtime/alerts/alert-validate.js +117 -0
- package/src/global/runtime/monitor/monitor-cli.js +62 -0
- package/src/global/runtime/monitor/monitor-platform.js +95 -0
- 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
|
+
}
|