@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.
Files changed (32) 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/cockpit/primitives.js +47 -35
  8. package/src/global/ink/cockpit-changes.js +2 -2
  9. package/src/global/ink/cockpit-control-center.js +12 -64
  10. package/src/global/ink/cockpit-controller.js +41 -5
  11. package/src/global/ink/cockpit-enter.js +1 -0
  12. package/src/global/ink/cockpit-models.js +14 -7
  13. package/src/global/ink/cockpit-palette.js +18 -7
  14. package/src/global/ink/cockpit-recovery.js +9 -6
  15. package/src/global/ink/cockpit-usage.js +111 -0
  16. package/src/global/ink/cockpit-views.js +69 -97
  17. package/src/global/ink/orchestrator-app.js +32 -0
  18. package/src/global/ink/setup-app.js +55 -72
  19. package/src/global/ink/setup-state.js +16 -0
  20. package/src/global/ink/theme.js +27 -0
  21. package/src/global/ink/ux/live-activity.js +194 -0
  22. package/src/global/ink/ux/live-alerts.js +159 -0
  23. package/src/global/ink/ux/live-governance.js +195 -0
  24. package/src/global/ink/ux/live-orchestration.js +188 -0
  25. package/src/global/ink/ux/live-overview.js +125 -0
  26. package/src/global/ink/ux/live-settings.js +189 -0
  27. package/src/global/ink/ux/live-setup.js +160 -0
  28. package/src/global/ink/ux/live-usage.js +51 -0
  29. package/src/global/ink/ux/semantic.js +84 -0
  30. package/src/global/ink/ux/task-flow-app.js +85 -0
  31. package/src/global/ink/ux/task-flow.js +173 -0
  32. 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 { formatRunsHubLines, RUNS_HUB_ITEMS } from "./cockpit-runs.js";
17
- import { formatReviewDetailLines, formatReviewListLines } from "./cockpit-reviews.js";
18
- import { formatChangesActionLines } from "./cockpit-changes.js";
19
- import { formatRecoveryLines } from "./cockpit-recovery.js";
20
- import { formatUsageLines } from "./cockpit-control-center.js";
21
- import { formatOrchestrationStatus } from "./cockpit-runs.js";
22
- import { formatAlertListLines } from "./cockpit-alerts.js";
23
- import { formatSettingsLines } from "./cockpit-settings.js";
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(ControlCenterPanel, { model: controlCenter, colorEnabled });
110
+ return React.createElement(SemanticOverviewPanel, {
111
+ model: controlCenter,
112
+ detailsOpen: overviewDetailsOpen,
113
+ colorEnabled,
114
+ unicode
115
+ });
106
116
  case ORCHESTRATOR_VIEWS.USAGE:
107
- return governanceList(
108
- "Usage",
109
- formatUsageLines({ snapshot, dashboard }),
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 governanceList(
124
- "Governance",
125
- formatChangeLines(snapshot, changesAction, layoutMode, homeDir),
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 governanceList(
131
- "Activity",
132
- formatRecoveryLines({ snapshot, recoveryAction, listIndex, dashboard, homeDir }),
144
+ return React.createElement(SemanticActivityPanel, {
145
+ snapshot,
146
+ recoveryAction,
147
+ dashboard,
148
+ listIndex,
149
+ homeDir,
150
+ detailsOpen: activityDetailsOpen,
133
151
  layoutMode,
134
- colorEnabled
135
- );
152
+ contentFocused,
153
+ colorEnabled,
154
+ unicode
155
+ });
136
156
  case ORCHESTRATOR_VIEWS.PROFILE:
137
- return governanceList(
138
- "Settings",
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
- "Choose Active runs, History, Reviews, or New run."
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 listBlock(
184
- "Reviews",
185
- formatReviewListLines(reviews),
172
+ return React.createElement(SemanticOrchestrationPanel, {
173
+ view,
174
+ dashboard,
175
+ reviews,
186
176
  listIndex,
177
+ layoutMode,
178
+ contentFocused,
187
179
  colorEnabled,
188
- "Receipts are read-only. Launch reviews via kairo review --agent codex|pi."
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 listBlock(
227
- "Alerts",
228
- formatAlertListLines(alerts),
218
+ return React.createElement(SemanticAlertsPanel, {
219
+ alerts,
229
220
  listIndex,
221
+ layoutMode,
222
+ contentFocused,
230
223
  colorEnabled,
231
- "Enter resolves · D dismisses · Esc back · / Alerts"
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, WIZARD_COPY } from "../brand/index.js";
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 { CockpitPanel } from "./cockpit/primitives.js";
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
- const INK_COLORS = {
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, { bold: true, color: INK_COLORS.accent }, `╭─ ${lines[0]}`),
39
- React.createElement(Text, { color: COCKPIT_COLORS.secondary }, lines[1]),
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 Panel({ title, children }) {
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: `logo-${index}`, bold: true, color: INK_COLORS.accent }, line);
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: `line-${index}`, bold: true, color: INK_COLORS.accent }, line);
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: `line-${index}`, color: INK_COLORS.muted }, line);
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: `line-${index}`, dimColor: true }, line);
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: `line-${index}` }, line);
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(output.columns);
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(Splash, {
233
+ step === SETUP_STEPS.SPLASH && React.createElement(SetupSplash, {
245
234
  compact: useCompactSplash,
246
- onboarding
235
+ onboarding,
236
+ colorEnabled
247
237
  }),
248
- step !== SETUP_STEPS.SPLASH && React.createElement(Header),
249
- step === SETUP_STEPS.DETECT && React.createElement(Panel, { title: WIZARD_COPY.detectTitle },
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 === SETUP_STEPS.PREVIEW && React.createElement(Panel, { title: WIZARD_COPY.previewTitle },
262
- previewLoading && React.createElement(Text, { color: INK_COLORS.warning }, "Building preview…"),
263
- previewError && React.createElement(Text, { color: INK_COLORS.danger }, previewError),
264
- preview && formatInkPreviewLines({ preview, componentCatalog })
265
- .map((line) => React.createElement(Text, { key: line }, line))
266
- ),
267
- step === SETUP_STEPS.CONFIRM && React.createElement(Panel, { title: "Confirm" },
268
- React.createElement(Text, null, dryRun ? WIZARD_COPY.confirmDryRun : WIZARD_COPY.confirmApply)
269
- ),
270
- React.createElement(Footer, null,
271
- step === SETUP_STEPS.SPLASH && `${BRAND.splashHint} · Esc cancel`,
272
- step === SETUP_STEPS.DETECT && "Enter continue · Esc cancel",
273
- step === SETUP_STEPS.AGENTS && "↑↓ move · Space toggle · Enter continue · Esc cancel",
274
- step === SETUP_STEPS.COMPONENTS && "↑↓ move · Space toggle · Enter continue · Esc cancel",
275
- step === SETUP_STEPS.PREVIEW && preview && !previewLoading && "Enter continue · Esc cancel",
276
- step === SETUP_STEPS.CONFIRM && "Y apply · N cancel · Esc cancel"
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
@@ -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
  }