@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
package/README.md CHANGED
@@ -16,8 +16,11 @@ as a technical alias).
16
16
  Kairo Runtime and prints a migration warning. Prefer `@kal-elsam/kairo-runtime` and the
17
17
  `kairo` CLI for new installs. See [`packages/harness-bridge/README.md`](packages/harness-bridge/README.md).
18
18
 
19
- Terminal UX aims for Pi-like clarity (clear modes, non-interactive flags, extensible
20
- commands) without depending on Pi as a runtime or adding a Pi adapter.
19
+ Terminal UX is **terminal-first**: bare `kairo` opens the Ink Control Center (or Setup
20
+ when unconfigured). Explicit commands (`setup`, `status`, `sync`, `doctor`, `monitor`,
21
+ `shell`, …) keep their current behavior. Progressive disclosure keeps paths and IDs in
22
+ Details, list caps honest (`… N more`), and Callout reserved for status — never for
23
+ primary values. No web loopback / `kairo ui` in this release.
21
24
 
22
25
  - **npm:** https://www.npmjs.com/package/@kal-elsam/kairo-runtime
23
26
  - **repo:** https://github.com/Kal-elSam/harness
@@ -32,13 +35,11 @@ npx @kal-elsam/kairo-runtime
32
35
  kairo
33
36
  ```
34
37
 
35
- **First run** (no `~/.harness/state.json`): interactive onboardingsafe diagnosis
36
- setup with confirmation → full-screen operations cockpit.
38
+ **First run** (no `~/.harness/state.json`): semantic Setup (Detect Agents
39
+ Components PreviewConfirm) → full-screen Control Center.
37
40
 
38
- **Later runs** (state present): full-screen Control Center oriented around user
39
- tasks overall health, one next action, activity, pending alerts, and guided
40
- governance (scan → evidence → preview → confirm → apply → re-scan → recovery).
41
- Runs stay secondary. Layout adapts to terminal size:
41
+ **Later runs** (state present): semantic Cockpit Overview, Governance, Activity,
42
+ Orchestration, Usage, Settings plus Alerts inbox. Layout adapts to terminal size:
42
43
 
43
44
  | Mode | Size | Layout |
44
45
  |------|------|--------|
@@ -73,6 +74,11 @@ Settings browses a curated catalog (pinned `pi-usage-widget@0.2.1`, MIT).
73
74
  Preview → confirm shows an in-session confirmation receipt (`wroteFiles: false`);
74
75
  it does not persist or install anything.
75
76
 
77
+ ### Usage evidence
78
+
79
+ Usage shows measured budget pairs, profile limits, and finite run `tokenUsage`
80
+ fields only. It never invents totals, costs, or savings.
81
+
76
82
  Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
77
83
  in text, never color alone.
78
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
@@ -26,7 +26,7 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
26
26
  const require = createRequire(import.meta.url);
27
27
  const pkg = require(join(root, "package.json"));
28
28
 
29
- assert.equal(pkg.version, "0.9.0");
29
+ assert.equal(pkg.version, "0.10.0");
30
30
  assert.ok(pkg.dependencies["ansi-escapes"]);
31
31
 
32
32
  assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
@@ -132,7 +132,7 @@ const proposalLines = formatProposalLines([
132
132
  id: "setup-local",
133
133
  severity: "high",
134
134
  title: "Finish local setup",
135
- destination: "changes",
135
+ destination: "setup",
136
136
  evidence: [{ type: "status", source: "status.overall", ref: "missing" }]
137
137
  }
138
138
  ], {
@@ -143,7 +143,7 @@ const proposalLines = formatProposalLines([
143
143
  requestBudgetTokens: 10
144
144
  }
145
145
  });
146
- assert.match(proposalLines.join("\n"), /\[HIGH\] Finish local setup → changes/);
146
+ assert.match(proposalLines.join("\n"), /\[HIGH\] Finish local setup → setup/);
147
147
  assert.match(proposalLines.join("\n"), /Budget · stable 1\/10/);
148
148
  assert.match(proposalLines.join("\n"), /evidence: status\.overall/);
149
149
 
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /** Manual TTY gate: Home → Setup → Overview. No writes. Esc exits. */
3
+ import React from "react";
4
+ import { render } from "ink";
5
+ import { TaskFlowApp } from "../src/global/ink/ux/task-flow-app.js";
6
+
7
+ const app = render(React.createElement(TaskFlowApp, {
8
+ onExit: () => app.unmount()
9
+ }));
@@ -12,6 +12,7 @@ export const PROPOSAL_SEVERITY = Object.freeze({
12
12
  });
13
13
 
14
14
  export const PROPOSAL_DESTINATION = Object.freeze({
15
+ SETUP: "setup",
15
16
  CHANGES: "changes",
16
17
  CONTROL_CENTER: "control-center",
17
18
  IDES: "ides",
@@ -48,7 +49,7 @@ export function buildControlPlaneProposals({
48
49
  PROPOSAL_SEVERITY.HIGH,
49
50
  "Finish local setup",
50
51
  status?.nextAction ?? "Configure the local ecosystem before applying repairs.",
51
- PROPOSAL_DESTINATION.CHANGES,
52
+ PROPOSAL_DESTINATION.SETUP,
52
53
  [
53
54
  ev("status", "status.overall", status?.overall ?? "missing"),
54
55
  ev("health", "control-plane.health", health ?? "NOT_CONFIGURED")
@@ -66,7 +66,7 @@ export function resolveControlPlaneCta({ health, status, diff } = {}) {
66
66
  title: "Finish local setup",
67
67
  detail: status?.nextAction
68
68
  ?? `Run ${formatCliCommand("setup")} to configure the local ecosystem.`,
69
- destination: "changes"
69
+ destination: "setup"
70
70
  };
71
71
  case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
72
72
  return {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { COCKPIT_COLORS, statusColor } from "../theme.js";
3
+ import { COCKPIT_COLORS, resolveInkColor, statusColor } from "../theme.js";
4
4
 
5
5
  export function CockpitBadge({ label, kind = "ready", colorEnabled = true }) {
6
6
  return React.createElement(Text, {
@@ -8,36 +8,47 @@ export function CockpitBadge({ label, kind = "ready", colorEnabled = true }) {
8
8
  }, label);
9
9
  }
10
10
 
11
- export function CockpitEmptyState({ title, message, hint }) {
11
+ export function CockpitEmptyState({ title, message, hint, colorEnabled = true }) {
12
12
  return React.createElement(Box, { flexDirection: "column", marginY: 1 },
13
- title && React.createElement(Text, { bold: true, color: COCKPIT_COLORS.secondary }, title),
13
+ title && React.createElement(Text, {
14
+ bold: true,
15
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
16
+ }, title),
14
17
  message && React.createElement(Text, null, message),
15
- hint && React.createElement(Text, { color: COCKPIT_COLORS.muted }, hint)
18
+ hint && React.createElement(Text, {
19
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
20
+ }, hint)
16
21
  );
17
22
  }
18
23
 
19
- export function CockpitPanel({ title, focused = false, width, children }) {
24
+ export function CockpitPanel({ title, focused = false, width, children, colorEnabled = true }) {
20
25
  return React.createElement(Box, {
21
26
  flexDirection: "column",
22
27
  width,
23
28
  borderStyle: "single",
24
- borderColor: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted,
29
+ borderColor: resolveInkColor(
30
+ colorEnabled,
31
+ focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted
32
+ ),
25
33
  paddingX: 1,
26
34
  flexGrow: 1
27
35
  },
28
36
  title && React.createElement(Text, {
29
37
  bold: true,
30
- color: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
38
+ color: resolveInkColor(
39
+ colorEnabled,
40
+ focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
41
+ )
31
42
  }, title),
32
43
  children
33
44
  );
34
45
  }
35
46
 
36
- export function CockpitSection({ title, children }) {
47
+ export function CockpitSection({ title, children, colorEnabled = true }) {
37
48
  return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
38
49
  title && React.createElement(Text, {
39
50
  bold: true,
40
- color: COCKPIT_COLORS.secondary
51
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
41
52
  }, title),
42
53
  children
43
54
  );
@@ -45,7 +56,7 @@ export function CockpitSection({ title, children }) {
45
56
 
46
57
  export function CockpitKeyHint({ keys, label, colorEnabled = true }) {
47
58
  return React.createElement(Text, {
48
- color: colorEnabled ? COCKPIT_COLORS.muted : undefined
59
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
49
60
  }, `${keys} ${label}`);
50
61
  }
51
62
 
@@ -53,10 +64,10 @@ export function CockpitTopBar({ model, colorEnabled = true }) {
53
64
  return React.createElement(Box, { justifyContent: "space-between", width: "100%" },
54
65
  React.createElement(Text, {
55
66
  bold: true,
56
- color: colorEnabled ? COCKPIT_COLORS.primary : undefined
67
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
57
68
  }, `╭─ ${model.brand} ─ ${model.status}`),
58
69
  React.createElement(Text, {
59
- color: colorEnabled ? COCKPIT_COLORS.muted : undefined
70
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
60
71
  }, `${model.projectLabel} ─╮`)
61
72
  );
62
73
  }
@@ -67,11 +78,11 @@ export function CockpitNav({ model, colorEnabled = true }) {
67
78
  model.items.map((item) => {
68
79
  const suffix = item.current && !item.selected ? " (open)" : "";
69
80
  const color = item.focused
70
- ? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
81
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
71
82
  : item.selected
72
- ? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
83
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
73
84
  : item.current
74
- ? (colorEnabled ? COCKPIT_COLORS.muted : undefined)
85
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
75
86
  : undefined;
76
87
  return React.createElement(Text, {
77
88
  key: item.id,
@@ -80,7 +91,7 @@ export function CockpitNav({ model, colorEnabled = true }) {
80
91
  }, `${item.marker} ${item.label}${suffix}`);
81
92
  }),
82
93
  model.explanation && React.createElement(Text, {
83
- color: COCKPIT_COLORS.muted
94
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
84
95
  }, model.explanation)
85
96
  );
86
97
  }
@@ -102,12 +113,10 @@ export function CockpitNavStrip({
102
113
  const label = stripLabel(item, { compact });
103
114
  const suffix = item.current && !item.selected ? "*" : "";
104
115
  const color = item.focused
105
- ? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
116
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
106
117
  : item.selected
107
- ? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
108
- : item.current
109
- ? (colorEnabled ? COCKPIT_COLORS.muted : undefined)
110
- : (colorEnabled ? COCKPIT_COLORS.muted : undefined);
118
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
119
+ : resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
111
120
  return React.createElement(Text, {
112
121
  key: item.id,
113
122
  bold: item.focused || item.selected,
@@ -120,7 +129,7 @@ export function CockpitNavStrip({
120
129
  if (index > 0) {
121
130
  joined.push(React.createElement(Text, {
122
131
  key: `sep-${index}`,
123
- color: colorEnabled ? COCKPIT_COLORS.muted : undefined
132
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
124
133
  }, " · "));
125
134
  }
126
135
  joined.push(node);
@@ -130,12 +139,15 @@ export function CockpitNavStrip({
130
139
  flexDirection: "column",
131
140
  width: "100%",
132
141
  borderStyle: "single",
133
- borderColor: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted,
142
+ borderColor: resolveInkColor(
143
+ colorEnabled,
144
+ focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted
145
+ ),
134
146
  paddingX: 1
135
147
  },
136
148
  React.createElement(Box, { flexDirection: "row", flexWrap: "wrap" }, ...joined),
137
149
  model.explanation && React.createElement(Text, {
138
- color: COCKPIT_COLORS.muted
150
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
139
151
  }, model.explanation)
140
152
  );
141
153
  }
@@ -145,7 +157,9 @@ export function CockpitSystemStrip({ model, colorEnabled = true }) {
145
157
  return React.createElement(Box, { flexDirection: "column" },
146
158
  model.rows.map((row) =>
147
159
  React.createElement(Text, { key: row.key },
148
- React.createElement(Text, { color: COCKPIT_COLORS.muted }, `${row.key.padEnd(7)}`),
160
+ React.createElement(Text, {
161
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
162
+ }, `${row.key.padEnd(7)}`),
149
163
  React.createElement(CockpitBadge, {
150
164
  label: row.value,
151
165
  kind: row.kind,
@@ -156,17 +170,14 @@ export function CockpitSystemStrip({ model, colorEnabled = true }) {
156
170
  );
157
171
  }
158
172
 
159
- export function CockpitFooter({ model, columns = 80 }) {
173
+ export function CockpitFooter({ model, columns = 80, colorEnabled = true }) {
160
174
  const width = Math.max(24, Math.min(Number(columns) || 80, 120));
161
175
  const bar = Math.max(20, width - 2);
176
+ const muted = resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
162
177
  return React.createElement(Box, { flexDirection: "column", width: "100%" },
163
- React.createElement(Text, { color: COCKPIT_COLORS.muted },
164
- `├${"─".repeat(bar)}┤`
165
- ),
166
- React.createElement(Text, { color: COCKPIT_COLORS.muted }, `│ ${model.text}`),
167
- React.createElement(Text, { color: COCKPIT_COLORS.muted },
168
- `╰${"─".repeat(bar)}╯`
169
- )
178
+ React.createElement(Text, { color: muted }, `├${"─".repeat(bar)}┤`),
179
+ React.createElement(Text, { color: muted }, `│ ${model.text}`),
180
+ React.createElement(Text, { color: muted }, `╰${"─".repeat(bar)}╯`)
170
181
  );
171
182
  }
172
183
 
@@ -196,8 +207,9 @@ export function CockpitShell({
196
207
  React.createElement(CockpitPanel, {
197
208
  title: undefined,
198
209
  focused: contentFocused,
199
- width: "100%"
210
+ width: "100%",
211
+ colorEnabled
200
212
  }, children),
201
- React.createElement(CockpitFooter, { model: footer, columns })
213
+ React.createElement(CockpitFooter, { model: footer, columns, colorEnabled })
202
214
  );
203
215
  }
@@ -143,6 +143,6 @@ export function formatChangesActionLines({
143
143
 
144
144
  export function buildChangesFooterParts(phase) {
145
145
  if (phase === CHANGES_PHASE.PREVIEWING || phase === CHANGES_PHASE.APPLYING) return ["Working…", "Esc Back"];
146
- if (phase === CHANGES_PHASE.CONFIRMING) return ["Y Apply", "N/Esc Cancel"];
147
- return ["A Preview", "R Re-scan", "Esc Back"];
146
+ if (phase === CHANGES_PHASE.CONFIRMING) return ["Y Apply", "N/Esc Cancel", "Space"];
147
+ return ["A Preview", "R Re-scan", "Space", "Esc Back"];
148
148
  }
@@ -1,5 +1,10 @@
1
1
  import { CONTROL_PLANE_HEALTH } from "../control-plane-snapshot.js";
2
2
  import { formatAlertsHeadline } from "./cockpit-alerts.js";
3
+ import {
4
+ adaptUsageModel,
5
+ formatMeasuredBudgets,
6
+ formatUsageLinesFromModel
7
+ } from "./cockpit-usage.js";
3
8
 
4
9
  export function buildControlCenterModel({
5
10
  projectName = "project",
@@ -112,73 +117,16 @@ function formatHealthLabel(kind) {
112
117
  }
113
118
 
114
119
  function formatTokenHeadline(budgets) {
115
- if (!budgets || typeof budgets !== "object") return "Data unavailable";
116
- const parts = [];
117
- if (Number.isFinite(budgets.stableUsedTokens) && Number.isFinite(budgets.stableBudgetTokens)) {
118
- parts.push(`stable ${budgets.stableUsedTokens}/${budgets.stableBudgetTokens}`);
119
- }
120
- if (Number.isFinite(budgets.requestUsedTokens) && Number.isFinite(budgets.requestBudgetTokens)) {
121
- parts.push(`request ${budgets.requestUsedTokens}/${budgets.requestBudgetTokens}`);
122
- }
123
- return parts.length > 0 ? parts.join(" · ") : "Data unavailable";
124
- }
125
-
126
- /**
127
- * Usage surface: measured budgets when present, configured profile limits,
128
- * and auditable run tokenUsage — never invent totals.
129
- */
130
- function resolveProfileLimits(dashboard) {
131
- const resolved = dashboard?.profile?.profile ?? {};
132
- const configured = [];
133
- if (Number.isFinite(resolved.tokenBudget)) configured.push(`token ${resolved.tokenBudget}`);
134
- if (Number.isFinite(resolved.stableContextBudget)) configured.push(`stable ${resolved.stableContextBudget}`);
135
- if (Number.isFinite(resolved.requestContextBudget)) configured.push(`request ${resolved.requestContextBudget}`);
136
- return configured;
120
+ return formatMeasuredBudgets(budgets) ?? "Data unavailable";
137
121
  }
138
122
 
139
- function hasFiniteUsage(usage) {
140
- if (!usage || typeof usage !== "object") return false;
141
- return Number.isFinite(usage.total)
142
- || Number.isFinite(usage.input)
143
- || Number.isFinite(usage.output);
144
- }
145
-
146
- function formatRunUsageLine(run) {
147
- const usage = run.tokenUsage;
148
- const parts = [];
149
- if (Number.isFinite(usage.input)) parts.push(`in ${usage.input}`);
150
- if (Number.isFinite(usage.output)) parts.push(`out ${usage.output}`);
151
- if (Number.isFinite(usage.total)) parts.push(`total ${usage.total}`);
152
- return `${run.agentId ?? "agent"} · ${parts.join(" · ")}`;
153
- }
154
-
155
- export function formatUsageLines({ snapshot = null, dashboard = null } = {}) {
156
- const lines = ["MEASURED"];
157
- const measured = formatTokenHeadline(snapshot?.budgets);
158
- lines.push(measured === "Data unavailable" ? "Data unavailable" : measured);
159
-
160
- const configured = resolveProfileLimits(dashboard);
161
- lines.push("", "CONFIGURED LIMITS");
162
- lines.push(configured.length > 0 ? configured.join(" · ") : "No profile token budgets configured.");
163
-
164
- const runs = [...(dashboard?.activeRuns ?? []), ...(dashboard?.recentRuns ?? [])]
165
- .filter((run) => hasFiniteUsage(run?.tokenUsage));
166
- lines.push("", "RUN USAGE");
167
- if (runs.length === 0) {
168
- lines.push("No auditable run tokenUsage yet.");
169
- } else {
170
- for (const run of runs.slice(0, 3)) {
171
- lines.push(formatRunUsageLine(run));
172
- }
173
- }
174
-
175
- lines.push("", "Auditable budgets only — no invented token savings.");
176
- return lines;
123
+ /** Usage surface via shared auditable model — never invent totals. */
124
+ export function formatUsageLines({
125
+ snapshot = null, dashboard = null, layoutMode = undefined
126
+ } = {}) {
127
+ return formatUsageLinesFromModel(adaptUsageModel({ snapshot, dashboard, layoutMode }));
177
128
  }
178
129
 
179
130
  export function hasAuditableUsage({ snapshot = null, dashboard = null } = {}) {
180
- if (formatTokenHeadline(snapshot?.budgets) !== "Data unavailable") return true;
181
- if (resolveProfileLimits(dashboard).length > 0) return true;
182
- return [...(dashboard?.activeRuns ?? []), ...(dashboard?.recentRuns ?? [])]
183
- .some((run) => hasFiniteUsage(run?.tokenUsage));
131
+ return adaptUsageModel({ snapshot, dashboard }).hasEvidence;
184
132
  }
@@ -35,7 +35,10 @@ export function createCockpitUiState({
35
35
  helpOpen = false,
36
36
  returnView = null,
37
37
  paletteOpen = false,
38
- paletteIndex = 0
38
+ paletteIndex = 0,
39
+ overviewDetailsOpen = false,
40
+ governanceDetailsOpen = false,
41
+ activityDetailsOpen = false
39
42
  } = {}) {
40
43
  const regions = regionsForLayout(layoutMode);
41
44
  const preferred = region ?? defaultRegionForView(view, layoutMode);
@@ -52,6 +55,9 @@ export function createCockpitUiState({
52
55
  returnView,
53
56
  paletteOpen,
54
57
  paletteIndex: Math.max(0, paletteIndex),
58
+ overviewDetailsOpen: Boolean(overviewDetailsOpen),
59
+ governanceDetailsOpen: Boolean(governanceDetailsOpen),
60
+ activityDetailsOpen: Boolean(activityDetailsOpen),
55
61
  shouldExit: false
56
62
  };
57
63
  }
@@ -145,7 +151,10 @@ export function reduceCockpitUi(state, action) {
145
151
  listIndex: 0,
146
152
  region: defaultRegionForView(item.view, state.layoutMode),
147
153
  helpOpen: false,
148
- returnView: null
154
+ returnView: null,
155
+ overviewDetailsOpen: false,
156
+ governanceDetailsOpen: false,
157
+ activityDetailsOpen: false
149
158
  });
150
159
  }
151
160
  case "set-view": {
@@ -159,9 +168,21 @@ export function reduceCockpitUi(state, action) {
159
168
  : clamp(action.navIndex, 0, COCKPIT_NAV.length - 1),
160
169
  region: action.region ?? defaultRegionForView(nextView, state.layoutMode),
161
170
  returnView: action.returnView ?? state.returnView,
162
- helpOpen: nextView === ORCHESTRATOR_VIEWS.HELP
171
+ helpOpen: nextView === ORCHESTRATOR_VIEWS.HELP,
172
+ overviewDetailsOpen: false,
173
+ governanceDetailsOpen: false,
174
+ activityDetailsOpen: false
163
175
  });
164
176
  }
177
+ case "toggle-overview-details":
178
+ if (state.view !== ORCHESTRATOR_VIEWS.HOME) return state;
179
+ return { ...state, overviewDetailsOpen: !state.overviewDetailsOpen };
180
+ case "toggle-governance-details":
181
+ if (state.view !== ORCHESTRATOR_VIEWS.CHANGES) return state;
182
+ return { ...state, governanceDetailsOpen: !state.governanceDetailsOpen };
183
+ case "toggle-activity-details":
184
+ if (state.view !== ORCHESTRATOR_VIEWS.ACTIVITY) return state;
185
+ return { ...state, activityDetailsOpen: !state.activityDetailsOpen };
165
186
  case "toggle-help":
166
187
  if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
167
188
  return goOverview(state);
@@ -170,12 +191,24 @@ export function reduceCockpitUi(state, action) {
170
191
  ...state,
171
192
  helpOpen: true,
172
193
  view: ORCHESTRATOR_VIEWS.HELP,
173
- region: defaultRegionForView(ORCHESTRATOR_VIEWS.HELP, state.layoutMode)
194
+ region: defaultRegionForView(ORCHESTRATOR_VIEWS.HELP, state.layoutMode),
195
+ overviewDetailsOpen: false,
196
+ governanceDetailsOpen: false,
197
+ activityDetailsOpen: false
174
198
  });
175
199
  case "escape": {
176
200
  if (state.paletteOpen) {
177
201
  return closePalette(state);
178
202
  }
203
+ if (state.view === ORCHESTRATOR_VIEWS.HOME && state.overviewDetailsOpen) {
204
+ return { ...state, overviewDetailsOpen: false };
205
+ }
206
+ if (state.view === ORCHESTRATOR_VIEWS.CHANGES && state.governanceDetailsOpen) {
207
+ return { ...state, governanceDetailsOpen: false };
208
+ }
209
+ if (state.view === ORCHESTRATOR_VIEWS.ACTIVITY && state.activityDetailsOpen) {
210
+ return { ...state, activityDetailsOpen: false };
211
+ }
179
212
  if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
180
213
  return goOverview(state);
181
214
  }
@@ -228,7 +261,10 @@ function goOverview(state) {
228
261
  navIndex: 0,
229
262
  listIndex: 0,
230
263
  region: defaultRegionForView(ORCHESTRATOR_VIEWS.HOME, state.layoutMode),
231
- returnView: null
264
+ returnView: null,
265
+ overviewDetailsOpen: false,
266
+ governanceDetailsOpen: false,
267
+ activityDetailsOpen: false
232
268
  });
233
269
  }
234
270
 
@@ -14,6 +14,7 @@ export function resolveEnterNavIntent({
14
14
  const isOverview = navItem.id === "overview" || navItem.view === ORCHESTRATOR_VIEWS.HOME;
15
15
  if (isOverview) {
16
16
  if (currentView === ORCHESTRATOR_VIEWS.HOME && ctaDestination) {
17
+ if (ctaDestination === "setup") return { kind: "activate-setup" };
17
18
  return { kind: "activate-cta", destination: ctaDestination };
18
19
  }
19
20
  return { kind: "open-nav", view: ORCHESTRATOR_VIEWS.HOME };
@@ -243,6 +243,7 @@ export function buildFooterModel({
243
243
  hasError = false,
244
244
  changesPhase = null,
245
245
  recoveryPhase = null,
246
+ recoveryHasPreview = false,
246
247
  settingsPhase = null,
247
248
  columns = 80
248
249
  } = {}) {
@@ -290,7 +291,8 @@ export function buildFooterModel({
290
291
 
291
292
  if (view === ORCHESTRATOR_VIEWS.ACTIVITY) {
292
293
  return {
293
- text: buildRecoveryFooterParts(recoveryPhase).join(` ${glyphs.bullet} `),
294
+ text: buildRecoveryFooterParts(recoveryPhase, { hasPreview: recoveryHasPreview })
295
+ .join(` ${glyphs.bullet} `),
294
296
  columns: footerColumns
295
297
  };
296
298
  }
@@ -315,16 +317,21 @@ export function buildFooterModel({
315
317
  parts.push("Tab Region");
316
318
  }
317
319
 
318
- const activatesOverviewCta = view === ORCHESTRATOR_VIEWS.HOME && navIndex === 0;
319
- if (activatesOverviewCta) {
320
- parts.push("Enter Activate");
321
- } else if (view === ORCHESTRATOR_VIEWS.IDES
320
+ // HOME footer must stay on one line at 80 cols (frame already near 24 rows).
321
+ if (view === ORCHESTRATOR_VIEWS.HOME) {
322
+ return {
323
+ text: ["↑↓", "Enter", "Space", "R", "?", "/", "Esc"].join(` ${glyphs.bullet} `),
324
+ columns: footerColumns
325
+ };
326
+ }
327
+
328
+ if (region === COCKPIT_REGIONS.NAV
329
+ || view === ORCHESTRATOR_VIEWS.IDES
322
330
  || view === ORCHESTRATOR_VIEWS.MODULES
323
331
  || view === ORCHESTRATOR_VIEWS.PROFILE
324
332
  || view === ORCHESTRATOR_VIEWS.PROVIDERS
325
333
  || view === ORCHESTRATOR_VIEWS.DIAGNOSTICS
326
334
  || view === ORCHESTRATOR_VIEWS.USAGE
327
- || region === COCKPIT_REGIONS.NAV
328
335
  || view === ORCHESTRATOR_VIEWS.RUNS
329
336
  || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
330
337
  || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
@@ -337,7 +344,7 @@ export function buildFooterModel({
337
344
 
338
345
  parts.push("? Help");
339
346
  parts.push("/ Actions");
340
- parts.push(view === ORCHESTRATOR_VIEWS.HOME ? "Esc Exit" : "Esc Back");
347
+ parts.push("Esc Back");
341
348
 
342
349
  return { text: parts.join(` ${glyphs.bullet} `), columns: footerColumns };
343
350
  }
@@ -4,7 +4,8 @@ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
4
4
  export const PALETTE_KINDS = Object.freeze({
5
5
  NAVIGATE: "navigate",
6
6
  REFRESH: "refresh",
7
- HELP: "help"
7
+ HELP: "help",
8
+ SETUP: "setup"
8
9
  });
9
10
 
10
11
  const DESTINATION_VIEWS = Object.freeze({
@@ -37,15 +38,25 @@ export function buildPaletteActions({
37
38
  navItems = COCKPIT_NAV
38
39
  } = {}) {
39
40
  const actions = [];
40
- const recommendedView = resolvePaletteDestination(ctaDestination);
41
- if (recommendedView) {
41
+ if (ctaDestination === "setup") {
42
42
  actions.push({
43
43
  id: "recommended",
44
- kind: PALETTE_KINDS.NAVIGATE,
45
- label: ctaTitle?.trim() || "Recommended next action",
46
- view: recommendedView,
47
- description: ctaDetail?.trim() || "Overview recommended destination."
44
+ kind: PALETTE_KINDS.SETUP,
45
+ label: ctaTitle?.trim() || "Finish local setup",
46
+ view: null,
47
+ description: ctaDetail?.trim() || "Open the local setup wizard."
48
48
  });
49
+ } else {
50
+ const recommendedView = resolvePaletteDestination(ctaDestination);
51
+ if (recommendedView) {
52
+ actions.push({
53
+ id: "recommended",
54
+ kind: PALETTE_KINDS.NAVIGATE,
55
+ label: ctaTitle?.trim() || "Recommended next action",
56
+ view: recommendedView,
57
+ description: ctaDetail?.trim() || "Overview recommended destination."
58
+ });
59
+ }
49
60
  }
50
61
  for (const item of navItems) {
51
62
  actions.push({
@@ -75,7 +75,7 @@ export function listRecoverySnapshots(snapshot) {
75
75
  return snapshot?.backups?.snapshots ?? [];
76
76
  }
77
77
 
78
- function formatWhen(timestamp) {
78
+ export function formatWhen(timestamp) {
79
79
  if (!timestamp) return "unknown time";
80
80
  const raw = String(timestamp);
81
81
  if (/^\d{4}-\d{2}-\d{2}/.test(raw)) return raw.slice(0, 16).replace("T", " ");
@@ -84,7 +84,7 @@ function formatWhen(timestamp) {
84
84
  return date.toISOString().slice(0, 16).replace("T", " ");
85
85
  }
86
86
 
87
- function formatResult(action) {
87
+ export function formatResult(action) {
88
88
  const value = String(action ?? "").toLowerCase();
89
89
  if (!value) return "done";
90
90
  if (value === "applied" || value === "ok" || value === "success") return "ok";
@@ -93,7 +93,7 @@ function formatResult(action) {
93
93
  return value;
94
94
  }
95
95
 
96
- function shortName(name) {
96
+ export function shortName(name) {
97
97
  const raw = String(name ?? "").trim();
98
98
  if (!raw) return "snapshot";
99
99
  const parts = raw.split(/[/\\]/).filter(Boolean);
@@ -176,8 +176,11 @@ export function formatRecoveryLines({
176
176
  return lines;
177
177
  }
178
178
 
179
- export function buildRecoveryFooterParts(phase) {
179
+ export function buildRecoveryFooterParts(phase, { hasPreview = false } = {}) {
180
180
  if (phase === RECOVERY_PHASE.PREVIEWING || phase === RECOVERY_PHASE.APPLYING) return ["Working…", "Esc Back"];
181
- if (phase === RECOVERY_PHASE.CONFIRMING) return ["Y Restore", "N/Esc Cancel"];
182
- return ["↑↓ Select", "Enter Preview", "R Re-scan", "Esc Back"];
181
+ if (phase === RECOVERY_PHASE.CONFIRMING) return ["Y Restore", "N/Esc Cancel", "Space"];
182
+ const parts = ["↑↓ Select", "Enter Preview", "R Re-scan"];
183
+ if (hasPreview) parts.push("Space");
184
+ parts.push("Esc Back");
185
+ return parts;
183
186
  }