@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.
Files changed (49) hide show
  1. package/README.md +41 -15
  2. package/package.json +1 -1
  3. package/scripts/cockpit-smoke.mjs +6 -6
  4. package/scripts/ux-prototype-tty.mjs +9 -0
  5. package/src/cli.js +24 -1
  6. package/src/global/control-plane-proposals.js +2 -1
  7. package/src/global/control-plane-snapshot.js +1 -1
  8. package/src/global/global-doctor.js +2 -0
  9. package/src/global/ink/cockpit/primitives.js +127 -50
  10. package/src/global/ink/cockpit-alerts.js +36 -0
  11. package/src/global/ink/cockpit-changes.js +61 -37
  12. package/src/global/ink/cockpit-control-center.js +79 -53
  13. package/src/global/ink/cockpit-controller.js +98 -15
  14. package/src/global/ink/cockpit-enter.js +1 -0
  15. package/src/global/ink/cockpit-focus.js +4 -2
  16. package/src/global/ink/cockpit-models.js +100 -51
  17. package/src/global/ink/cockpit-palette.js +109 -0
  18. package/src/global/ink/cockpit-path-label.js +19 -0
  19. package/src/global/ink/cockpit-recovery.js +84 -18
  20. package/src/global/ink/cockpit-reviews.js +14 -10
  21. package/src/global/ink/cockpit-runs.js +13 -4
  22. package/src/global/ink/cockpit-settings.js +194 -0
  23. package/src/global/ink/cockpit-usage.js +111 -0
  24. package/src/global/ink/cockpit-views.js +119 -116
  25. package/src/global/ink/orchestrator-app.js +169 -46
  26. package/src/global/ink/orchestrator-state.js +24 -14
  27. package/src/global/ink/setup-app.js +55 -72
  28. package/src/global/ink/setup-state.js +16 -0
  29. package/src/global/ink/theme.js +27 -0
  30. package/src/global/ink/use-orchestrator-data.js +58 -0
  31. package/src/global/ink/ux/live-activity.js +194 -0
  32. package/src/global/ink/ux/live-alerts.js +159 -0
  33. package/src/global/ink/ux/live-governance.js +195 -0
  34. package/src/global/ink/ux/live-orchestration.js +188 -0
  35. package/src/global/ink/ux/live-overview.js +125 -0
  36. package/src/global/ink/ux/live-settings.js +189 -0
  37. package/src/global/ink/ux/live-setup.js +160 -0
  38. package/src/global/ink/ux/live-usage.js +51 -0
  39. package/src/global/ink/ux/semantic.js +84 -0
  40. package/src/global/ink/ux/task-flow-app.js +85 -0
  41. package/src/global/ink/ux/task-flow.js +173 -0
  42. package/src/global/orchestrator.js +37 -20
  43. package/src/global/paths.js +3 -0
  44. package/src/global/runtime/alerts/alert-store.js +216 -0
  45. package/src/global/runtime/alerts/alert-types.js +59 -0
  46. package/src/global/runtime/alerts/alert-validate.js +117 -0
  47. package/src/global/runtime/monitor/monitor-cli.js +62 -0
  48. package/src/global/runtime/monitor/monitor-platform.js +95 -0
  49. package/src/global/runtime/monitor/monitor.js +249 -0
@@ -13,19 +13,26 @@ import {
13
13
  isContentInteractiveView
14
14
  } from "./cockpit-controller.js";
15
15
  import {
16
- COCKPIT_NAV,
17
16
  COCKPIT_REGIONS,
18
17
  buildFooterModel,
19
18
  buildNavModel,
20
- buildSystemStripModel,
21
19
  buildTopBarModel,
22
20
  navIndexForView,
23
21
  resolveProjectName
24
22
  } from "./cockpit-models.js";
25
23
  import { buildControlCenterModel } from "./cockpit-control-center.js";
24
+ import {
25
+ buildPaletteActions,
26
+ buildPaletteModel,
27
+ canOpenPalette,
28
+ PALETTE_KINDS,
29
+ resolvePaletteDestination
30
+ } from "./cockpit-palette.js";
26
31
  import { resolveEnterNavIntent } from "./cockpit-enter.js";
27
32
  import { resolveRunsHubItem, RUNS_HUB_ITEMS } from "./cockpit-runs.js";
28
33
  import { selectReviewFromList } from "./cockpit-reviews.js";
34
+ import { selectAlertFromList } from "./cockpit-alerts.js";
35
+ import { ALERT_STATES } from "../runtime/alerts/alert-types.js";
29
36
  import { resolveProjectReadiness } from "../dashboard-guidance.js";
30
37
  import { CONTROL_PLANE_HEALTH } from "../control-plane-snapshot.js";
31
38
  import { CockpitShell } from "./cockpit/primitives.js";
@@ -38,6 +45,7 @@ import { COCKPIT_COLORS } from "./theme.js";
38
45
  import { LAYOUT_MODES } from "./layout.js";
39
46
  import { CHANGES_PHASE } from "./cockpit-changes.js";
40
47
  import { RECOVERY_PHASE, listRecoverySnapshots } from "./cockpit-recovery.js";
48
+ import { SETTINGS_PHASE } from "./cockpit-settings.js";
41
49
 
42
50
  export function OrchestratorApp({
43
51
  homeDir,
@@ -76,7 +84,7 @@ export function OrchestratorApp({
76
84
  };
77
85
 
78
86
  const openDestination = (destinationKey) => {
79
- const view = resolveCtaDestinationView(destinationKey);
87
+ const view = resolvePaletteDestination(destinationKey);
80
88
  if (!view) return false;
81
89
  dispatch({
82
90
  type: "set-view",
@@ -86,6 +94,15 @@ export function OrchestratorApp({
86
94
  return true;
87
95
  };
88
96
 
97
+ const confirming = data.changesAction?.phase === CHANGES_PHASE.CONFIRMING
98
+ || data.recoveryAction?.phase === RECOVERY_PHASE.CONFIRMING
99
+ || data.settingsAction?.phase === SETTINGS_PHASE.CONFIRMING;
100
+ const paletteActions = buildPaletteActions({
101
+ ctaDestination: data.snapshot?.cta?.destination ?? null,
102
+ ctaTitle: data.snapshot?.cta?.title ?? null,
103
+ ctaDetail: data.snapshot?.cta?.detail ?? null
104
+ });
105
+
89
106
  useInput((inputKey, key) => {
90
107
  if (data.loading) return;
91
108
 
@@ -104,6 +121,68 @@ export function OrchestratorApp({
104
121
 
105
122
  if (data.busy) return;
106
123
 
124
+ if (ui.paletteOpen) {
125
+ if (key.escape) {
126
+ dispatch({ type: "close-palette" });
127
+ return;
128
+ }
129
+ if (inputKey === "/") {
130
+ dispatch({ type: "toggle-palette" });
131
+ return;
132
+ }
133
+ if (key.upArrow || key.downArrow) {
134
+ dispatch({
135
+ type: "palette-arrow",
136
+ direction: key.upArrow ? "up" : "down",
137
+ listLength: paletteActions.length
138
+ });
139
+ return;
140
+ }
141
+ if (key.return) {
142
+ const selected = paletteActions[ui.paletteIndex] ?? null;
143
+ if (!selected) return;
144
+ if (selected.kind === PALETTE_KINDS.REFRESH) {
145
+ dispatch({ type: "run-palette", kind: selected.kind });
146
+ data.reload().catch(() => {});
147
+ return;
148
+ }
149
+ if (selected.kind === PALETTE_KINDS.SETUP) {
150
+ finish({ cancelled: false, action: "setup" });
151
+ return;
152
+ }
153
+ dispatch({
154
+ type: "run-palette",
155
+ kind: selected.kind,
156
+ view: selected.view
157
+ });
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (inputKey === "/"
163
+ && canOpenPalette({ loading: data.loading, busy: data.busy, confirming })) {
164
+ dispatch({ type: "toggle-palette" });
165
+ return;
166
+ }
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
+
107
186
  if (key.escape) {
108
187
  if (ui.view === ORCHESTRATOR_VIEWS.CHANGES
109
188
  && data.changesAction?.phase === CHANGES_PHASE.CONFIRMING) {
@@ -115,6 +194,18 @@ export function OrchestratorApp({
115
194
  data.cancelRecovery();
116
195
  return;
117
196
  }
197
+ if (ui.view === ORCHESTRATOR_VIEWS.PROFILE) {
198
+ const settingsPhase = data.settingsAction?.phase;
199
+ if (settingsPhase === SETTINGS_PHASE.CONFIRMING) {
200
+ data.cancelSettings();
201
+ return;
202
+ }
203
+ if (settingsPhase === SETTINGS_PHASE.PREVIEW
204
+ || settingsPhase === SETTINGS_PHASE.COMPLETED) {
205
+ data.resetSettings();
206
+ return;
207
+ }
208
+ }
118
209
 
119
210
  if (ui.view === ORCHESTRATOR_VIEWS.LAUNCH && data.launchableAgents.length > 0) {
120
211
  const retreated = handleLaunchInput({
@@ -162,9 +253,15 @@ export function OrchestratorApp({
162
253
  ? (data.dashboard?.recentRuns ?? []).length
163
254
  : ui.view === ORCHESTRATOR_VIEWS.REVIEWS
164
255
  ? (data.reviews ?? []).length
165
- : ui.view === ORCHESTRATOR_VIEWS.ACTIVITY
166
- ? listRecoverySnapshots(data.snapshot).length
167
- : 0;
256
+ : ui.view === ORCHESTRATOR_VIEWS.ALERTS
257
+ ? (Array.isArray(data.alerts) ? data.alerts : [])
258
+ .filter((alert) => alert.state === ALERT_STATES.OPEN).length
259
+ : ui.view === ORCHESTRATOR_VIEWS.ACTIVITY
260
+ ? listRecoverySnapshots(data.snapshot).length
261
+ : ui.view === ORCHESTRATOR_VIEWS.PROFILE
262
+ && data.settingsAction?.phase === SETTINGS_PHASE.BROWSE
263
+ ? data.curatedIntegrations.length
264
+ : 0;
168
265
 
169
266
  let routed = null;
170
267
  if (key.tab) {
@@ -187,6 +284,10 @@ export function OrchestratorApp({
187
284
  navItem: item,
188
285
  ctaDestination: data.snapshot?.cta?.destination ?? null
189
286
  });
287
+ if (intent.kind === "activate-setup") {
288
+ finish({ cancelled: false, action: "setup" });
289
+ return;
290
+ }
190
291
  if (intent.kind === "activate-cta") {
191
292
  if (openDestination(intent.destination)) return;
192
293
  }
@@ -261,6 +362,19 @@ export function OrchestratorApp({
261
362
  return;
262
363
  }
263
364
 
365
+ if (ui.region === COCKPIT_REGIONS.CONTENT
366
+ && ui.view === ORCHESTRATOR_VIEWS.ALERTS) {
367
+ const selected = selectAlertFromList(data.alerts, ui.listIndex);
368
+ if (key.return) {
369
+ data.handleAlertTransition(selected, "resolve").catch(() => {});
370
+ return;
371
+ }
372
+ if (inputKey.toLowerCase() === "d") {
373
+ data.handleAlertTransition(selected, "dismiss").catch(() => {});
374
+ return;
375
+ }
376
+ }
377
+
264
378
  if (ui.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
265
379
  if (inputKey.toLowerCase() === "c" && isRunCancellable(data.selectedRun)) {
266
380
  data.handleCancelRun();
@@ -317,6 +431,30 @@ export function OrchestratorApp({
317
431
  }
318
432
  }
319
433
 
434
+ if (ui.view === ORCHESTRATOR_VIEWS.PROFILE) {
435
+ const keyName = inputKey.toLowerCase();
436
+ const phase = data.settingsAction?.phase ?? SETTINGS_PHASE.BROWSE;
437
+ if (key.return && ui.region === COCKPIT_REGIONS.CONTENT) {
438
+ if (phase === SETTINGS_PHASE.BROWSE) {
439
+ const entry = data.curatedIntegrations[ui.listIndex];
440
+ if (entry?.id) data.previewSettings(entry.id);
441
+ return;
442
+ }
443
+ if (phase === SETTINGS_PHASE.PREVIEW) {
444
+ data.promptConfirmSettings();
445
+ return;
446
+ }
447
+ }
448
+ if (keyName === "y" && phase === SETTINGS_PHASE.CONFIRMING) {
449
+ data.confirmSettings();
450
+ return;
451
+ }
452
+ if (keyName === "n" && phase === SETTINGS_PHASE.CONFIRMING) {
453
+ data.cancelSettings();
454
+ return;
455
+ }
456
+ }
457
+
320
458
  if (inputKey.toLowerCase() === "r" && ui.view !== ORCHESTRATOR_VIEWS.LAUNCH) {
321
459
  if (ui.view === ORCHESTRATOR_VIEWS.REVIEWS || ui.view === ORCHESTRATOR_VIEWS.REVIEW_DETAIL) {
322
460
  data.loadReviews().catch(() => {});
@@ -354,7 +492,9 @@ export function OrchestratorApp({
354
492
  const controlCenter = buildControlCenterModel({
355
493
  projectName,
356
494
  snapshot: data.snapshot,
357
- layoutMode: mode
495
+ dashboard: data.dashboard,
496
+ layoutMode: mode,
497
+ alerts: data.alerts
358
498
  });
359
499
  const systemOnline = data.snapshot
360
500
  ? data.snapshot.health !== CONTROL_PLANE_HEALTH.NOT_CONFIGURED
@@ -376,12 +516,17 @@ export function OrchestratorApp({
376
516
  region: ui.region,
377
517
  navIndex: ui.navIndex,
378
518
  helpOpen: ui.helpOpen,
519
+ paletteOpen: ui.paletteOpen,
379
520
  canCancel: isRunCancellable(data.selectedRun),
380
521
  unicode,
381
522
  changesPhase: data.changesAction?.phase ?? null,
382
- recoveryPhase: data.recoveryAction?.phase ?? null
523
+ recoveryPhase: data.recoveryAction?.phase ?? null,
524
+ recoveryHasPreview: Boolean(data.recoveryAction?.preview),
525
+ settingsPhase: data.settingsAction?.phase ?? null,
526
+ columns
383
527
  }),
384
528
  layoutMode: mode,
529
+ columns,
385
530
  nav: buildNavModel({
386
531
  navIndex: ui.navIndex,
387
532
  currentView: ui.view,
@@ -391,24 +536,8 @@ export function OrchestratorApp({
391
536
  diagnostics: data.diagnostics,
392
537
  snapshot: data.snapshot
393
538
  }),
394
- system: buildSystemStripModel({
395
- dashboard: data.dashboard,
396
- diagnostics: data.diagnostics,
397
- readiness: data.snapshot
398
- ? {
399
- kind: data.snapshot.health.toLowerCase(),
400
- label: controlCenter.health.label,
401
- healthKind: data.snapshot.health === CONTROL_PLANE_HEALTH.HEALTHY
402
- ? "ready"
403
- : data.snapshot.health === CONTROL_PLANE_HEALTH.CHECK_FAILED
404
- ? "error"
405
- : "warn"
406
- }
407
- : readiness
408
- }),
409
539
  navFocused: ui.region === COCKPIT_REGIONS.NAV,
410
540
  contentFocused: ui.region === COCKPIT_REGIONS.CONTENT,
411
- systemFocused: ui.region === COCKPIT_REGIONS.SYSTEM,
412
541
  colorEnabled
413
542
  },
414
543
  renderCockpitView({
@@ -426,33 +555,27 @@ export function OrchestratorApp({
426
555
  selectedEvents: data.selectedEvents,
427
556
  reviews: data.reviews,
428
557
  selectedReview: data.selectedReview,
558
+ alerts: data.alerts,
429
559
  changesAction: data.changesAction,
430
560
  recoveryAction: data.recoveryAction,
561
+ settingsAction: data.settingsAction,
431
562
  controlCenter,
563
+ palette: ui.paletteOpen
564
+ ? buildPaletteModel({
565
+ actions: paletteActions,
566
+ index: ui.paletteIndex,
567
+ unicode
568
+ })
569
+ : null,
432
570
  layoutMode: mode,
433
- colorEnabled
571
+ colorEnabled,
572
+ unicode,
573
+ overviewDetailsOpen: ui.overviewDetailsOpen,
574
+ governanceDetailsOpen: ui.governanceDetailsOpen,
575
+ activityDetailsOpen: ui.activityDetailsOpen,
576
+ contentFocused: ui.region === COCKPIT_REGIONS.CONTENT,
577
+ homeDir
434
578
  })
435
579
  )
436
580
  );
437
581
  }
438
-
439
- function resolveCtaDestinationView(destinationKey) {
440
- switch (destinationKey) {
441
- case "changes":
442
- return ORCHESTRATOR_VIEWS.CHANGES;
443
- case "control-center":
444
- return ORCHESTRATOR_VIEWS.HOME;
445
- case "ides":
446
- return ORCHESTRATOR_VIEWS.IDES;
447
- case "modules":
448
- return ORCHESTRATOR_VIEWS.MODULES;
449
- case "activity":
450
- return ORCHESTRATOR_VIEWS.ACTIVITY;
451
- case "profile":
452
- return ORCHESTRATOR_VIEWS.PROFILE;
453
- case "runs":
454
- return ORCHESTRATOR_VIEWS.RUNS;
455
- default:
456
- return null;
457
- }
458
- }
@@ -1,6 +1,7 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
2
  import { canUseSetupInk } from "./terminal.js";
3
3
  import { isActiveRunState, formatTaskLabel } from "../runtime/run-types.js";
4
+ import { formatConfirmPath } from "./cockpit-path-label.js";
4
5
 
5
6
  export function canUseOrchestratorShell({
6
7
  interactive = Boolean(input.isTTY && output.isTTY),
@@ -27,7 +28,9 @@ export const ORCHESTRATOR_VIEWS = {
27
28
  PROVIDERS: "providers",
28
29
  LAUNCH: "launch",
29
30
  DIAGNOSTICS: "diagnostics",
30
- HELP: "help"
31
+ HELP: "help",
32
+ USAGE: "usage",
33
+ ALERTS: "alerts"
31
34
  };
32
35
 
33
36
  export const ORCHESTRATOR_MENU = [
@@ -175,8 +178,9 @@ export function formatRunLines(runs, {
175
178
  const agent = humanizeToken(run.agentId);
176
179
  const state = humanizeRunState(run.state);
177
180
  const task = formatTaskLabel(run);
181
+ const strategy = run.strategy ? ` · ${run.strategy}` : "";
178
182
  if (readable) {
179
- return `${agent} · ${state} · ${task}`;
183
+ return `${agent} · ${state}${strategy} · ${task}`;
180
184
  }
181
185
  const statePad = run.state.padEnd(12);
182
186
  const agentPad = run.agentId.padEnd(10);
@@ -270,23 +274,29 @@ export function formatAgentStatusLines(capabilities) {
270
274
  });
271
275
  }
272
276
 
273
- export function formatRunDetailLines(run, events = []) {
277
+ export function formatRunDetailLines(run, events = [], { homeDir = null } = {}) {
274
278
  if (!run) return ["Run not found."];
275
279
 
276
280
  const lines = [
277
- `Run: ${run.runId}`,
278
- `Agent: ${run.agentId} (${run.provider})`,
279
- `State: ${run.state}`,
280
- `Model: ${run.model ?? "default"}`,
281
- `Cwd: ${run.cwd}`,
282
- `Started: ${run.startedAt}`,
283
- `Updated: ${run.updatedAt}`
281
+ "SUMMARY",
282
+ `${humanizeToken(run.agentId)} · ${humanizeRunState(run.state)}${run.strategy ? ` · ${run.strategy}` : ""}`,
283
+ `Model · ${run.model ?? "default"}`,
284
+ `Started · ${run.startedAt ?? "n/a"}`
284
285
  ];
286
+ if (run.completedAt) lines.push(`Completed · ${run.completedAt}`);
287
+ if (run.tokenUsage && typeof run.tokenUsage === "object") {
288
+ const parts = [];
289
+ if (Number.isFinite(run.tokenUsage.input)) parts.push(`in ${run.tokenUsage.input}`);
290
+ if (Number.isFinite(run.tokenUsage.output)) parts.push(`out ${run.tokenUsage.output}`);
291
+ if (Number.isFinite(run.tokenUsage.total)) parts.push(`total ${run.tokenUsage.total}`);
292
+ if (parts.length > 0) lines.push(`Tokens · ${parts.join(" · ")}`);
293
+ }
294
+ if (run.error) lines.push(`Error · ${run.error}`);
285
295
 
286
- if (run.completedAt) lines.push(`Completed: ${run.completedAt}`);
287
- if (run.tokenUsage) lines.push(`Tokens: ${JSON.stringify(run.tokenUsage)}`);
288
- if (run.error) lines.push(`Error: ${run.error}`);
289
- if (run.tools?.length) lines.push(`Tools: ${run.tools.join(", ")}`);
296
+ lines.push("", "DETAILS");
297
+ lines.push(`Run id · ${run.runId}`);
298
+ if (run.cwd) lines.push(`Cwd · ${formatConfirmPath(run.cwd, homeDir)}`);
299
+ if (run.tools?.length) lines.push(`Tools · ${run.tools.join(", ")}`);
290
300
 
291
301
  if (events.length > 0) {
292
302
  lines.push("", "Recent events");
@@ -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
  }