@kal-elsam/kairo-runtime 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/cockpit/primitives.js +47 -35
- 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 +69 -97
- package/src/global/ink/orchestrator-app.js +32 -0
- 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/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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Usage / Tokens model — no React/Ink.
|
|
3
|
+
* Shared by formatUsageLines and SemanticUsagePanel.
|
|
4
|
+
*/
|
|
5
|
+
import { LAYOUT_MODES } from "./layout.js";
|
|
6
|
+
|
|
7
|
+
export function usageRunLimit(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
8
|
+
return layoutMode === LAYOUT_MODES.WIDE ? 8 : 3;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatMeasuredBudgets(budgets) {
|
|
12
|
+
if (!budgets || typeof budgets !== "object") return null;
|
|
13
|
+
const parts = [];
|
|
14
|
+
if (Number.isFinite(budgets.stableUsedTokens) && Number.isFinite(budgets.stableBudgetTokens)) {
|
|
15
|
+
parts.push(`stable ${budgets.stableUsedTokens}/${budgets.stableBudgetTokens}`);
|
|
16
|
+
}
|
|
17
|
+
if (Number.isFinite(budgets.requestUsedTokens) && Number.isFinite(budgets.requestBudgetTokens)) {
|
|
18
|
+
parts.push(`request ${budgets.requestUsedTokens}/${budgets.requestBudgetTokens}`);
|
|
19
|
+
}
|
|
20
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveConfiguredLimits(dashboard) {
|
|
24
|
+
const resolved = dashboard?.profile?.profile ?? {};
|
|
25
|
+
const configured = [];
|
|
26
|
+
if (Number.isFinite(resolved.tokenBudget)) configured.push(`token ${resolved.tokenBudget}`);
|
|
27
|
+
if (Number.isFinite(resolved.stableContextBudget)) configured.push(`stable ${resolved.stableContextBudget}`);
|
|
28
|
+
if (Number.isFinite(resolved.requestContextBudget)) configured.push(`request ${resolved.requestContextBudget}`);
|
|
29
|
+
return configured;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hasFiniteUsage(usage) {
|
|
33
|
+
if (!usage || typeof usage !== "object") return false;
|
|
34
|
+
return Number.isFinite(usage.total)
|
|
35
|
+
|| Number.isFinite(usage.input)
|
|
36
|
+
|| Number.isFinite(usage.output);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatRunUsageLabel(run) {
|
|
40
|
+
const usage = run?.tokenUsage;
|
|
41
|
+
if (!hasFiniteUsage(usage)) return null;
|
|
42
|
+
const parts = [];
|
|
43
|
+
if (Number.isFinite(usage.input)) parts.push(`in ${usage.input}`);
|
|
44
|
+
if (Number.isFinite(usage.output)) parts.push(`out ${usage.output}`);
|
|
45
|
+
if (Number.isFinite(usage.total)) parts.push(`total ${usage.total}`);
|
|
46
|
+
return `${run.agentId ?? "agent"} · ${parts.join(" · ")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectAuditableRuns(dashboard) {
|
|
50
|
+
return [...(dashboard?.activeRuns ?? []), ...(dashboard?.recentRuns ?? [])]
|
|
51
|
+
.filter((run) => hasFiniteUsage(run?.tokenUsage));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Pure adapter: Callout=status · Measured section owns the value. */
|
|
55
|
+
export function adaptUsageModel({
|
|
56
|
+
snapshot = null,
|
|
57
|
+
dashboard = null,
|
|
58
|
+
layoutMode = LAYOUT_MODES.COMPACT
|
|
59
|
+
} = {}) {
|
|
60
|
+
const measured = formatMeasuredBudgets(snapshot?.budgets);
|
|
61
|
+
const configured = resolveConfiguredLimits(dashboard);
|
|
62
|
+
const allRuns = collectAuditableRuns(dashboard);
|
|
63
|
+
const limit = usageRunLimit(layoutMode);
|
|
64
|
+
const visible = allRuns.slice(0, limit);
|
|
65
|
+
const hidden = Math.max(0, allRuns.length - visible.length);
|
|
66
|
+
const hasEvidence = Boolean(measured) || configured.length > 0 || allRuns.length > 0;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
title: "Usage",
|
|
70
|
+
callout: hasEvidence
|
|
71
|
+
? {
|
|
72
|
+
tone: "info",
|
|
73
|
+
title: "Usage evidence available",
|
|
74
|
+
body: "Budgets and run tokenUsage only — no invented totals."
|
|
75
|
+
}
|
|
76
|
+
: {
|
|
77
|
+
tone: "info",
|
|
78
|
+
title: "Data unavailable",
|
|
79
|
+
body: "No measured budgets, profile limits, or auditable run tokenUsage."
|
|
80
|
+
},
|
|
81
|
+
measured: measured ?? "Data unavailable",
|
|
82
|
+
configured: configured.length > 0
|
|
83
|
+
? configured.join(" · ")
|
|
84
|
+
: "No profile token budgets configured.",
|
|
85
|
+
runs: visible.map((run, i) => ({
|
|
86
|
+
id: `usage-run-${i}`,
|
|
87
|
+
label: formatRunUsageLabel(run)
|
|
88
|
+
})),
|
|
89
|
+
runTotal: allRuns.length,
|
|
90
|
+
runLimit: limit,
|
|
91
|
+
moreLine: hidden > 0 ? `… ${hidden} more` : null,
|
|
92
|
+
hasEvidence,
|
|
93
|
+
footnote: "Auditable budgets only — no invented token savings."
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatUsageLinesFromModel(model) {
|
|
98
|
+
const lines = [
|
|
99
|
+
"MEASURED", model.measured, "",
|
|
100
|
+
"CONFIGURED LIMITS", model.configured, "",
|
|
101
|
+
"RUN USAGE"
|
|
102
|
+
];
|
|
103
|
+
if (model.runs.length === 0) {
|
|
104
|
+
lines.push("No auditable run tokenUsage yet.");
|
|
105
|
+
} else {
|
|
106
|
+
for (const item of model.runs) lines.push(item.label);
|
|
107
|
+
if (model.moreLine) lines.push(model.moreLine);
|
|
108
|
+
}
|
|
109
|
+
lines.push("", model.footnote);
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
@@ -5,7 +5,6 @@ import { CockpitEmptyState } from "./cockpit/primitives.js";
|
|
|
5
5
|
import {
|
|
6
6
|
formatProviderLines,
|
|
7
7
|
formatRunDetailLines,
|
|
8
|
-
formatRunLines,
|
|
9
8
|
formatSystemHealthLines,
|
|
10
9
|
formatLaunchWizardLines,
|
|
11
10
|
ORCHESTRATOR_VIEWS,
|
|
@@ -13,14 +12,15 @@ import {
|
|
|
13
12
|
} from "./orchestrator-state.js";
|
|
14
13
|
import { windowLinesForLayout } from "./cockpit-models.js";
|
|
15
14
|
import { LAYOUT_MODES } from "./layout.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
15
|
+
import { SemanticOverviewPanel } from "./ux/live-overview.js";
|
|
16
|
+
import { SemanticGovernancePanel } from "./ux/live-governance.js";
|
|
17
|
+
import { SemanticActivityPanel } from "./ux/live-activity.js";
|
|
18
|
+
import { SemanticOrchestrationPanel } from "./ux/live-orchestration.js";
|
|
19
|
+
import { SemanticAlertsPanel } from "./ux/live-alerts.js";
|
|
20
|
+
import { SemanticSettingsPanel } from "./ux/live-settings.js";
|
|
21
|
+
import { SemanticUsagePanel } from "./ux/live-usage.js";
|
|
22
|
+
import { formatReviewDetailLines } from "./cockpit-reviews.js";
|
|
23
|
+
import { listCuratedIntegrations } from "./cockpit-settings.js";
|
|
24
24
|
|
|
25
25
|
export function PalettePanel({ model, colorEnabled = true }) {
|
|
26
26
|
return React.createElement(Box, { flexDirection: "column" },
|
|
@@ -95,6 +95,11 @@ export function renderCockpitView({
|
|
|
95
95
|
recoveryAction = null,
|
|
96
96
|
settingsAction = null,
|
|
97
97
|
colorEnabled = true,
|
|
98
|
+
unicode = true,
|
|
99
|
+
overviewDetailsOpen = false,
|
|
100
|
+
governanceDetailsOpen = false,
|
|
101
|
+
activityDetailsOpen = false,
|
|
102
|
+
contentFocused = false,
|
|
98
103
|
homeDir = null
|
|
99
104
|
}) {
|
|
100
105
|
if (palette) {
|
|
@@ -102,14 +107,20 @@ export function renderCockpitView({
|
|
|
102
107
|
}
|
|
103
108
|
switch (view) {
|
|
104
109
|
case ORCHESTRATOR_VIEWS.HOME:
|
|
105
|
-
return React.createElement(
|
|
110
|
+
return React.createElement(SemanticOverviewPanel, {
|
|
111
|
+
model: controlCenter,
|
|
112
|
+
detailsOpen: overviewDetailsOpen,
|
|
113
|
+
colorEnabled,
|
|
114
|
+
unicode
|
|
115
|
+
});
|
|
106
116
|
case ORCHESTRATOR_VIEWS.USAGE:
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
return React.createElement(SemanticUsagePanel, {
|
|
118
|
+
snapshot,
|
|
119
|
+
dashboard,
|
|
110
120
|
layoutMode,
|
|
111
|
-
colorEnabled
|
|
112
|
-
|
|
121
|
+
colorEnabled,
|
|
122
|
+
unicode
|
|
123
|
+
});
|
|
113
124
|
case ORCHESTRATOR_VIEWS.IDES:
|
|
114
125
|
case ORCHESTRATOR_VIEWS.PROVIDERS:
|
|
115
126
|
return governanceList("IDEs & models", [
|
|
@@ -120,73 +131,54 @@ export function renderCockpitView({
|
|
|
120
131
|
case ORCHESTRATOR_VIEWS.MODULES:
|
|
121
132
|
return governanceList("Harness modules", formatModuleLines(snapshot), layoutMode, colorEnabled);
|
|
122
133
|
case ORCHESTRATOR_VIEWS.CHANGES:
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
return React.createElement(SemanticGovernancePanel, {
|
|
135
|
+
snapshot,
|
|
136
|
+
changesAction,
|
|
137
|
+
homeDir,
|
|
138
|
+
detailsOpen: governanceDetailsOpen,
|
|
126
139
|
layoutMode,
|
|
127
|
-
colorEnabled
|
|
128
|
-
|
|
140
|
+
colorEnabled,
|
|
141
|
+
unicode
|
|
142
|
+
});
|
|
129
143
|
case ORCHESTRATOR_VIEWS.ACTIVITY:
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
return React.createElement(SemanticActivityPanel, {
|
|
145
|
+
snapshot,
|
|
146
|
+
recoveryAction,
|
|
147
|
+
dashboard,
|
|
148
|
+
listIndex,
|
|
149
|
+
homeDir,
|
|
150
|
+
detailsOpen: activityDetailsOpen,
|
|
133
151
|
layoutMode,
|
|
134
|
-
|
|
135
|
-
|
|
152
|
+
contentFocused,
|
|
153
|
+
colorEnabled,
|
|
154
|
+
unicode
|
|
155
|
+
});
|
|
136
156
|
case ORCHESTRATOR_VIEWS.PROFILE:
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
formatSettingsLines({
|
|
140
|
-
listIndex,
|
|
141
|
-
settingsAction,
|
|
142
|
-
snapshot,
|
|
143
|
-
diagnostics
|
|
144
|
-
}),
|
|
145
|
-
layoutMode,
|
|
146
|
-
colorEnabled
|
|
147
|
-
);
|
|
148
|
-
case ORCHESTRATOR_VIEWS.RUNS:
|
|
149
|
-
return listBlock(
|
|
150
|
-
`Orchestration · ${formatOrchestrationStatus({
|
|
151
|
-
active: (dashboard?.activeRuns ?? []).length,
|
|
152
|
-
recent: (dashboard?.recentRuns ?? []).length,
|
|
153
|
-
reviews: (reviews ?? []).length
|
|
154
|
-
})}`,
|
|
155
|
-
formatRunsHubLines(RUNS_HUB_ITEMS),
|
|
157
|
+
return React.createElement(SemanticSettingsPanel, {
|
|
158
|
+
integrations: listCuratedIntegrations(),
|
|
156
159
|
listIndex,
|
|
160
|
+
settingsAction,
|
|
161
|
+
snapshot,
|
|
162
|
+
diagnostics,
|
|
163
|
+
layoutMode,
|
|
164
|
+
contentFocused,
|
|
157
165
|
colorEnabled,
|
|
158
|
-
|
|
159
|
-
);
|
|
166
|
+
unicode
|
|
167
|
+
});
|
|
168
|
+
case ORCHESTRATOR_VIEWS.RUNS:
|
|
160
169
|
case ORCHESTRATOR_VIEWS.ACTIVE_RUNS:
|
|
161
|
-
return listBlock(
|
|
162
|
-
"Active runs",
|
|
163
|
-
formatRunLines(dashboard?.activeRuns ?? [], {
|
|
164
|
-
emptyMessage: "No runs executing. Governance first — launch only after setup/repairs.",
|
|
165
|
-
readable: true
|
|
166
|
-
}),
|
|
167
|
-
listIndex,
|
|
168
|
-
colorEnabled,
|
|
169
|
-
"Enter opens detail · Esc back to Orchestration"
|
|
170
|
-
);
|
|
171
170
|
case ORCHESTRATOR_VIEWS.RECENT_RUNS:
|
|
172
|
-
return listBlock(
|
|
173
|
-
"Run history",
|
|
174
|
-
formatRunLines(dashboard?.recentRuns ?? [], {
|
|
175
|
-
emptyMessage: "No completed runs yet.",
|
|
176
|
-
readable: true
|
|
177
|
-
}),
|
|
178
|
-
listIndex,
|
|
179
|
-
colorEnabled,
|
|
180
|
-
"Enter opens detail · Esc back to Orchestration"
|
|
181
|
-
);
|
|
182
171
|
case ORCHESTRATOR_VIEWS.REVIEWS:
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
|
|
172
|
+
return React.createElement(SemanticOrchestrationPanel, {
|
|
173
|
+
view,
|
|
174
|
+
dashboard,
|
|
175
|
+
reviews,
|
|
186
176
|
listIndex,
|
|
177
|
+
layoutMode,
|
|
178
|
+
contentFocused,
|
|
187
179
|
colorEnabled,
|
|
188
|
-
|
|
189
|
-
);
|
|
180
|
+
unicode
|
|
181
|
+
});
|
|
190
182
|
case ORCHESTRATOR_VIEWS.LAUNCH:
|
|
191
183
|
if (launchableAgents.length === 0) {
|
|
192
184
|
return React.createElement(CockpitEmptyState, {
|
|
@@ -223,13 +215,14 @@ export function renderCockpitView({
|
|
|
223
215
|
.map((line) => React.createElement(Text, { key: line }, line))
|
|
224
216
|
);
|
|
225
217
|
case ORCHESTRATOR_VIEWS.ALERTS:
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
formatAlertListLines(alerts),
|
|
218
|
+
return React.createElement(SemanticAlertsPanel, {
|
|
219
|
+
alerts,
|
|
229
220
|
listIndex,
|
|
221
|
+
layoutMode,
|
|
222
|
+
contentFocused,
|
|
230
223
|
colorEnabled,
|
|
231
|
-
|
|
232
|
-
);
|
|
224
|
+
unicode
|
|
225
|
+
});
|
|
233
226
|
case ORCHESTRATOR_VIEWS.DIAGNOSTICS:
|
|
234
227
|
return governanceList(
|
|
235
228
|
"System health",
|
|
@@ -287,25 +280,4 @@ function formatModuleLines(snapshot) {
|
|
|
287
280
|
];
|
|
288
281
|
}
|
|
289
282
|
|
|
290
|
-
function formatChangeLines(snapshot, changesAction, layoutMode = LAYOUT_MODES.COMPACT, homeDir = null) {
|
|
291
|
-
return formatChangesActionLines({ snapshot, changesAction, layoutMode, homeDir });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function listBlock(title, lines, listIndex, colorEnabled, emptyHint) {
|
|
295
|
-
const isEmpty = lines.length === 1 && /no |nothing |empty/i.test(lines[0]);
|
|
296
|
-
return React.createElement(Box, { flexDirection: "column" },
|
|
297
|
-
React.createElement(Text, { bold: true }, title),
|
|
298
|
-
isEmpty
|
|
299
|
-
? React.createElement(CockpitEmptyState, {
|
|
300
|
-
message: lines[0],
|
|
301
|
-
hint: emptyHint
|
|
302
|
-
})
|
|
303
|
-
: lines.map((line, index) => React.createElement(Text, {
|
|
304
|
-
key: `${index}-${line}`,
|
|
305
|
-
bold: index === listIndex,
|
|
306
|
-
color: index === listIndex && colorEnabled ? COCKPIT_COLORS.primary : undefined
|
|
307
|
-
}, `${index === listIndex ? "› " : " "}${line}`))
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
283
|
export { LAUNCH_WIZARD_STEPS };
|
|
@@ -146,6 +146,10 @@ export function OrchestratorApp({
|
|
|
146
146
|
data.reload().catch(() => {});
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
|
+
if (selected.kind === PALETTE_KINDS.SETUP) {
|
|
150
|
+
finish({ cancelled: false, action: "setup" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
149
153
|
dispatch({
|
|
150
154
|
type: "run-palette",
|
|
151
155
|
kind: selected.kind,
|
|
@@ -161,6 +165,24 @@ export function OrchestratorApp({
|
|
|
161
165
|
return;
|
|
162
166
|
}
|
|
163
167
|
|
|
168
|
+
if (inputKey === " " && ui.view === ORCHESTRATOR_VIEWS.HOME && !ui.paletteOpen) {
|
|
169
|
+
dispatch({ type: "toggle-overview-details" });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (inputKey === " " && ui.view === ORCHESTRATOR_VIEWS.CHANGES && !ui.paletteOpen) {
|
|
174
|
+
dispatch({ type: "toggle-governance-details" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (inputKey === " "
|
|
179
|
+
&& ui.view === ORCHESTRATOR_VIEWS.ACTIVITY
|
|
180
|
+
&& !ui.paletteOpen
|
|
181
|
+
&& data.recoveryAction?.preview) {
|
|
182
|
+
dispatch({ type: "toggle-activity-details" });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
164
186
|
if (key.escape) {
|
|
165
187
|
if (ui.view === ORCHESTRATOR_VIEWS.CHANGES
|
|
166
188
|
&& data.changesAction?.phase === CHANGES_PHASE.CONFIRMING) {
|
|
@@ -262,6 +284,10 @@ export function OrchestratorApp({
|
|
|
262
284
|
navItem: item,
|
|
263
285
|
ctaDestination: data.snapshot?.cta?.destination ?? null
|
|
264
286
|
});
|
|
287
|
+
if (intent.kind === "activate-setup") {
|
|
288
|
+
finish({ cancelled: false, action: "setup" });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
265
291
|
if (intent.kind === "activate-cta") {
|
|
266
292
|
if (openDestination(intent.destination)) return;
|
|
267
293
|
}
|
|
@@ -495,6 +521,7 @@ export function OrchestratorApp({
|
|
|
495
521
|
unicode,
|
|
496
522
|
changesPhase: data.changesAction?.phase ?? null,
|
|
497
523
|
recoveryPhase: data.recoveryAction?.phase ?? null,
|
|
524
|
+
recoveryHasPreview: Boolean(data.recoveryAction?.preview),
|
|
498
525
|
settingsPhase: data.settingsAction?.phase ?? null,
|
|
499
526
|
columns
|
|
500
527
|
}),
|
|
@@ -542,6 +569,11 @@ export function OrchestratorApp({
|
|
|
542
569
|
: null,
|
|
543
570
|
layoutMode: mode,
|
|
544
571
|
colorEnabled,
|
|
572
|
+
unicode,
|
|
573
|
+
overviewDetailsOpen: ui.overviewDetailsOpen,
|
|
574
|
+
governanceDetailsOpen: ui.governanceDetailsOpen,
|
|
575
|
+
activityDetailsOpen: ui.activityDetailsOpen,
|
|
576
|
+
contentFocused: ui.region === COCKPIT_REGIONS.CONTENT,
|
|
545
577
|
homeDir
|
|
546
578
|
})
|
|
547
579
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import { stdout as output } from "node:process";
|
|
4
|
-
import { BRAND
|
|
4
|
+
import { BRAND } from "../brand/index.js";
|
|
5
5
|
import { DEFAULT_COMPONENT_IDS, describeComponentCatalog } from "../component-registry.js";
|
|
6
6
|
import { GLOBAL_AGENT_IDS, detectInstalledAdapters, listAdapters } from "../registry.js";
|
|
7
7
|
import { buildSetupPreview, resolveComponentSelection } from "../clack/setup-preview.js";
|
|
@@ -9,72 +9,57 @@ import {
|
|
|
9
9
|
SETUP_STEPS,
|
|
10
10
|
buildAgentOptions,
|
|
11
11
|
buildComponentOptions,
|
|
12
|
-
formatInkDetectPanel,
|
|
13
12
|
formatInkHeaderLines,
|
|
14
|
-
formatInkPreviewLines,
|
|
15
|
-
formatInkSelectList,
|
|
16
13
|
formatInkSplashLines,
|
|
17
14
|
INITIAL_SETUP_STEP,
|
|
18
15
|
shouldStartPreviewLoad,
|
|
19
16
|
shouldUseCompactSplashLogo,
|
|
17
|
+
setupLineKey,
|
|
20
18
|
toggleComponentSelection,
|
|
21
19
|
toggleSelection,
|
|
22
20
|
transitionFromSplash
|
|
23
21
|
} from "./setup-state.js";
|
|
24
|
-
import { COCKPIT_COLORS } from "./theme.js";
|
|
25
|
-
import {
|
|
22
|
+
import { COCKPIT_COLORS, resolveInkColor } from "./theme.js";
|
|
23
|
+
import { useTerminalSize } from "./use-terminal-size.js";
|
|
24
|
+
import { resolveTerminalCapabilities } from "./terminal-capabilities.js";
|
|
25
|
+
import { SemanticSetupPanel } from "./ux/live-setup.js";
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
accent: COCKPIT_COLORS.primary,
|
|
29
|
-
success: COCKPIT_COLORS.success,
|
|
30
|
-
warning: COCKPIT_COLORS.warning,
|
|
31
|
-
danger: COCKPIT_COLORS.danger,
|
|
32
|
-
muted: COCKPIT_COLORS.muted
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function Header() {
|
|
27
|
+
export function SetupHeader({ colorEnabled = true }) {
|
|
36
28
|
const lines = formatInkHeaderLines();
|
|
37
29
|
return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
38
|
-
React.createElement(Text, {
|
|
39
|
-
|
|
30
|
+
React.createElement(Text, {
|
|
31
|
+
bold: true,
|
|
32
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
|
|
33
|
+
}, `╭─ ${lines[0]}`),
|
|
34
|
+
React.createElement(Text, {
|
|
35
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
|
|
36
|
+
}, lines[1]),
|
|
40
37
|
React.createElement(Text, { dimColor: true }, lines[2])
|
|
41
38
|
);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
function
|
|
45
|
-
return React.createElement(CockpitPanel, {
|
|
46
|
-
title,
|
|
47
|
-
focused: true,
|
|
48
|
-
width: "100%"
|
|
49
|
-
}, children);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function Footer({ children }) {
|
|
53
|
-
return React.createElement(Text, { dimColor: true }, children);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function Splash({ compact, onboarding = false }) {
|
|
41
|
+
export function SetupSplash({ compact, onboarding = false, colorEnabled = true }) {
|
|
57
42
|
const lines = formatInkSplashLines({ compact, onboarding });
|
|
58
43
|
const logoLineCount = compact ? BRAND.compactLogo.length : BRAND.asciiLogo.length;
|
|
44
|
+
const accent = resolveInkColor(colorEnabled, COCKPIT_COLORS.primary);
|
|
45
|
+
const muted = resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
|
|
59
46
|
|
|
60
47
|
return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
61
48
|
lines.map((line, index) => {
|
|
49
|
+
const key = setupLineKey(index, line);
|
|
62
50
|
if (index < logoLineCount) {
|
|
63
|
-
return React.createElement(Text, { key
|
|
51
|
+
return React.createElement(Text, { key, bold: true, color: accent }, line);
|
|
64
52
|
}
|
|
65
53
|
if (line === BRAND.name) {
|
|
66
|
-
return React.createElement(Text, { key
|
|
54
|
+
return React.createElement(Text, { key, bold: true, color: accent }, line);
|
|
67
55
|
}
|
|
68
56
|
if (line === BRAND.tagline) {
|
|
69
|
-
return React.createElement(Text, { key
|
|
57
|
+
return React.createElement(Text, { key, color: muted }, line);
|
|
70
58
|
}
|
|
71
59
|
if (line === BRAND.splashHint || line.includes("Esc to exit") || line.includes("Press Enter")) {
|
|
72
|
-
return React.createElement(Text, { key
|
|
73
|
-
}
|
|
74
|
-
if (line === "") {
|
|
75
|
-
return React.createElement(Text, { key: `line-${index}` }, "");
|
|
60
|
+
return React.createElement(Text, { key, dimColor: true }, line);
|
|
76
61
|
}
|
|
77
|
-
return React.createElement(Text, { key
|
|
62
|
+
return React.createElement(Text, { key }, line);
|
|
78
63
|
})
|
|
79
64
|
);
|
|
80
65
|
}
|
|
@@ -90,6 +75,12 @@ export function SetupApp({
|
|
|
90
75
|
onComplete
|
|
91
76
|
}) {
|
|
92
77
|
const { exit } = useApp();
|
|
78
|
+
const { columns, rows, layoutMode } = useTerminalSize({
|
|
79
|
+
initialColumns: output.columns ?? 80,
|
|
80
|
+
initialRows: output.rows ?? 24
|
|
81
|
+
});
|
|
82
|
+
const caps = resolveTerminalCapabilities({ columns, rows, isTTY: true });
|
|
83
|
+
const colorEnabled = caps.color;
|
|
93
84
|
const adapters = listAdapters();
|
|
94
85
|
const detected = detectInstalledAdapters({ homeDir });
|
|
95
86
|
const componentCatalog = describeComponentCatalog({ workspaceRoot });
|
|
@@ -98,7 +89,7 @@ export function SetupApp({
|
|
|
98
89
|
const defaultAgents = detected.length > 0 ? detected : [...GLOBAL_AGENT_IDS];
|
|
99
90
|
|
|
100
91
|
const [step, setStep] = useState(INITIAL_SETUP_STEP);
|
|
101
|
-
const useCompactSplash = shouldUseCompactSplashLogo(
|
|
92
|
+
const useCompactSplash = shouldUseCompactSplashLogo(columns);
|
|
102
93
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
103
94
|
const [selectedAgents, setSelectedAgents] = useState(defaultAgents);
|
|
104
95
|
const [selectedComponents, setSelectedComponents] = useState([...DEFAULT_COMPONENT_IDS]);
|
|
@@ -238,42 +229,34 @@ export function SetupApp({
|
|
|
238
229
|
}
|
|
239
230
|
});
|
|
240
231
|
|
|
241
|
-
const detectPanel = formatInkDetectPanel({ adapters, detected });
|
|
242
|
-
|
|
243
232
|
return React.createElement(Box, { flexDirection: "column" },
|
|
244
|
-
step === SETUP_STEPS.SPLASH && React.createElement(
|
|
233
|
+
step === SETUP_STEPS.SPLASH && React.createElement(SetupSplash, {
|
|
245
234
|
compact: useCompactSplash,
|
|
246
|
-
onboarding
|
|
235
|
+
onboarding,
|
|
236
|
+
colorEnabled
|
|
247
237
|
}),
|
|
248
|
-
step
|
|
249
|
-
|
|
250
|
-
detectPanel.split("\n")
|
|
251
|
-
.map((line) => React.createElement(Text, { key: line }, line))
|
|
252
|
-
),
|
|
253
|
-
step === SETUP_STEPS.AGENTS && React.createElement(Panel, { title: WIZARD_COPY.agentsPrompt },
|
|
254
|
-
formatInkSelectList({ options: agentOptions, selected: selectedAgents, activeIndex })
|
|
255
|
-
.map((line) => React.createElement(Text, { key: line }, line))
|
|
256
|
-
),
|
|
257
|
-
step === SETUP_STEPS.COMPONENTS && React.createElement(Panel, { title: WIZARD_COPY.componentsPrompt },
|
|
258
|
-
formatInkSelectList({ options: componentOptions, selected: selectedComponents, activeIndex })
|
|
259
|
-
.map((line) => React.createElement(Text, { key: line }, line))
|
|
238
|
+
step === SETUP_STEPS.SPLASH && React.createElement(Text, { dimColor: true },
|
|
239
|
+
`${BRAND.splashHint} · Esc cancel`
|
|
260
240
|
),
|
|
261
|
-
step
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
241
|
+
step !== SETUP_STEPS.SPLASH && React.createElement(SetupHeader, { colorEnabled }),
|
|
242
|
+
step !== SETUP_STEPS.SPLASH && React.createElement(SemanticSetupPanel, {
|
|
243
|
+
step,
|
|
244
|
+
activeIndex,
|
|
245
|
+
agentOptions,
|
|
246
|
+
componentOptions,
|
|
247
|
+
componentCatalog,
|
|
248
|
+
selectedAgents,
|
|
249
|
+
selectedComponents,
|
|
250
|
+
adapters,
|
|
251
|
+
detected,
|
|
252
|
+
preview,
|
|
253
|
+
previewLoading,
|
|
254
|
+
previewError,
|
|
255
|
+
dryRun,
|
|
256
|
+
layoutMode,
|
|
257
|
+
colorEnabled,
|
|
258
|
+
unicode: caps.unicode,
|
|
259
|
+
columns
|
|
260
|
+
})
|
|
278
261
|
);
|
|
279
262
|
}
|
|
@@ -178,6 +178,22 @@ export function formatInkPreviewLines({ preview, componentCatalog }) {
|
|
|
178
178
|
return lines;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/** Cap preview lines for the active layout; keep remaining-count when truncated. */
|
|
182
|
+
export function setupPreviewLineLimit(layoutMode = "compact") {
|
|
183
|
+
if (layoutMode === "wide") return 12;
|
|
184
|
+
if (layoutMode === "minimal") return 5;
|
|
185
|
+
return 8;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function windowSetupLines(lines = [], limit = 8) {
|
|
189
|
+
if (lines.length <= limit) return lines;
|
|
190
|
+
return [...lines.slice(0, limit), `… ${lines.length - limit} more`];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function setupLineKey(index, line) {
|
|
194
|
+
return `${index}-${line}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
export function formatInkSuccessLines(result, { dryRun = false, cliName = PREFERRED_CLI } = {}) {
|
|
182
198
|
const agentLine = result.agents.map((id) => getAgentLabel(id)).join(", ");
|
|
183
199
|
const componentLine = result.components.length > 0
|
package/src/global/ink/theme.js
CHANGED
|
@@ -73,6 +73,33 @@ export function statusColor(kind, { colorEnabled = true } = {}) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Ink `color` / `borderColor` prop — undefined when color is disabled. */
|
|
77
|
+
export function resolveInkColor(colorEnabled, color) {
|
|
78
|
+
return colorEnabled ? color : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect ANSI color SGR in a string.
|
|
83
|
+
* Bold (1), dim (2), and reset (0) alone are not color.
|
|
84
|
+
*/
|
|
85
|
+
export function hasColorSgr(text) {
|
|
86
|
+
const re = /\u001b\[([0-9;]*)m/g;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = re.exec(String(text ?? ""))) !== null) {
|
|
89
|
+
const params = match[1].length === 0
|
|
90
|
+
? []
|
|
91
|
+
: match[1].split(";").map((part) => Number(part));
|
|
92
|
+
for (let i = 0; i < params.length; i += 1) {
|
|
93
|
+
const code = params[i];
|
|
94
|
+
if (!Number.isFinite(code)) continue;
|
|
95
|
+
if (code === 38 || code === 48) return true;
|
|
96
|
+
if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) return true;
|
|
97
|
+
if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
export function formatStatusBadge(kind, label = STATUS_LABELS[kind] ?? String(kind)) {
|
|
77
104
|
return label;
|
|
78
105
|
}
|