@kal-elsam/kairo-runtime 0.13.1 → 0.15.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 (53) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +12 -10
  3. package/bin/kairo-runtime.js +0 -0
  4. package/bin/kairo.js +0 -0
  5. package/package.json +5 -1
  6. package/scripts/cockpit-smoke.mjs +14 -10
  7. package/src/cli.js +182 -11
  8. package/src/global/check-resolutions.js +31 -0
  9. package/src/global/cli-help.js +23 -4
  10. package/src/global/component-ecosystem-checks.js +2 -0
  11. package/src/global/component-integration-cli.js +29 -10
  12. package/src/global/components-resolve-cli.js +246 -0
  13. package/src/global/connection-actions.js +147 -0
  14. package/src/global/connections.js +269 -0
  15. package/src/global/fleet-configure-plan.js +123 -0
  16. package/src/global/fleet-configure.js +303 -0
  17. package/src/global/fleet-models.js +188 -0
  18. package/src/global/fleet-set.js +219 -0
  19. package/src/global/fleet-shared.js +38 -0
  20. package/src/global/ink/cockpit-controller.js +2 -4
  21. package/src/global/ink/cockpit-enter.js +3 -1
  22. package/src/global/ink/cockpit-focus.js +7 -1
  23. package/src/global/ink/cockpit-models.js +33 -21
  24. package/src/global/ink/cockpit-palette.js +8 -3
  25. package/src/global/ink/cockpit-views.js +9 -2
  26. package/src/global/ink/orchestrator-app.js +42 -4
  27. package/src/global/ink/ux/live-overview.js +53 -26
  28. package/src/global/ink/ux/overview-actions.js +80 -0
  29. package/src/global/ink/ux/overview-needs.js +1 -1
  30. package/src/global/integrations/engram-evidence.js +7 -2
  31. package/src/global/integrations/sdd-apply.js +17 -7
  32. package/src/global/integrations/sdd-evidence.js +22 -3
  33. package/src/global/integrations/sdd-plan.js +21 -3
  34. package/src/global/integrations/sdd-resolutions.js +73 -0
  35. package/src/global/integrations/sdd-state.js +69 -0
  36. package/src/global/integrations/sdd-verify.js +9 -4
  37. package/src/global/mcp/kairo-mcp.js +56 -5
  38. package/src/global/mcp/resolve-mcp-workspace.js +51 -0
  39. package/src/global/mcp/work-snapshot-rule.js +89 -0
  40. package/src/global/mcp/work-snapshot-tool.js +49 -0
  41. package/src/global/mcp-install.js +239 -0
  42. package/src/global/next/next-cli.js +35 -0
  43. package/src/global/next/next-report.js +145 -0
  44. package/src/global/next/project-key.js +36 -0
  45. package/src/global/next/publish-work-snapshot.js +116 -0
  46. package/src/global/next/work-enroll.js +91 -0
  47. package/src/global/next/work-snapshot.js +216 -0
  48. package/src/global/observability/fleet-activity.js +197 -0
  49. package/src/global/observability/fleet-models-catalog.js +137 -0
  50. package/src/global/observability/fleet-platforms.js +166 -0
  51. package/src/global/observability/fleet-probe.js +229 -0
  52. package/src/global/paths.js +2 -1
  53. package/src/global/self-update.js +216 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Unified fleet model profile (~/.harness/fleet-models.json).
3
+ * Multi-agent platforms (Claude + OpenCode) share phase keys.
4
+ * Codex keeps a single default model. Cursor Auto stays IDE-managed.
5
+ */
6
+ import { readFile, mkdir } from "node:fs/promises";
7
+ import { dirname, join } from "node:path";
8
+ import { resolveHomeDir } from "./paths.js";
9
+ import { writeAtomicJson } from "./runtime/write-atomic-json.js";
10
+ import {
11
+ SDD_PHASES,
12
+ loadGentleClaudeAssignments
13
+ } from "./fleet-shared.js";
14
+ import { parseFrontmatterModel, parseCodexDefaultModel } from "./observability/fleet-platforms.js";
15
+
16
+ export const FLEET_MODELS_VERSION = 1;
17
+
18
+ export function fleetModelsPath(homeDir = resolveHomeDir()) {
19
+ return join(homeDir, ".harness", "fleet-models.json");
20
+ }
21
+
22
+ export function emptyFleetProfile() {
23
+ const phases = {};
24
+ for (const id of SDD_PHASES) {
25
+ phases[id] = { claude: null, opencode: null };
26
+ }
27
+ return {
28
+ version: FLEET_MODELS_VERSION,
29
+ claudeDefault: null,
30
+ codexDefault: null,
31
+ cursorAgentModel: "inherit",
32
+ phases,
33
+ note: "Multi-agent phases for Claude + OpenCode. Codex uses codexDefault only. Cursor agents use inherit (Auto is IDE-managed)."
34
+ };
35
+ }
36
+
37
+ async function readJsonSafe(path, read) {
38
+ try {
39
+ return JSON.parse(await read(path, "utf8"));
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Seed profile from Gentle + current on-disk OpenCode/Claude/Codex.
47
+ * Preserves tuned OpenCode models when present.
48
+ */
49
+ export async function seedFleetProfile({
50
+ homeDir = resolveHomeDir(),
51
+ read = readFile
52
+ } = {}) {
53
+ const profile = emptyFleetProfile();
54
+ const gentle = await loadGentleClaudeAssignments(homeDir, read);
55
+ if (gentle) {
56
+ profile.claudeDefault = gentle.default ?? "sonnet";
57
+ for (const id of SDD_PHASES) {
58
+ if (gentle[id]) profile.phases[id].claude = gentle[id];
59
+ }
60
+ }
61
+
62
+ const settings = await readJsonSafe(join(homeDir, ".claude", "settings.json"), read);
63
+ if (settings?.model && !profile.claudeDefault) profile.claudeDefault = settings.model;
64
+
65
+ for (const id of SDD_PHASES) {
66
+ try {
67
+ const raw = await read(join(homeDir, ".claude", "agents", `${id}.md`), "utf8");
68
+ const model = parseFrontmatterModel(raw).model;
69
+ if (model && model !== "inherit") profile.phases[id].claude = model;
70
+ } catch { /* missing */ }
71
+ }
72
+
73
+ const oc = await readJsonSafe(join(homeDir, ".config", "opencode", "opencode.json"), read);
74
+ const agents = oc?.agent ?? oc?.agents ?? {};
75
+ for (const id of SDD_PHASES) {
76
+ const model = agents[id]?.model;
77
+ // Only declare OpenCode models that exist on disk — never invent from Claude tiers.
78
+ if (typeof model === "string") profile.phases[id].opencode = model;
79
+ }
80
+
81
+ try {
82
+ const toml = await read(join(homeDir, ".codex", "config.toml"), "utf8");
83
+ profile.codexDefault = parseCodexDefaultModel(toml);
84
+ } catch { /* missing */ }
85
+
86
+ return profile;
87
+ }
88
+
89
+ export async function loadFleetProfile({
90
+ homeDir = resolveHomeDir(),
91
+ read = readFile,
92
+ seedIfMissing = true
93
+ } = {}) {
94
+ const path = fleetModelsPath(homeDir);
95
+ const existing = await readJsonSafe(path, read);
96
+ if (existing?.phases) {
97
+ return { profile: existing, path, seeded: false };
98
+ }
99
+ if (!seedIfMissing) {
100
+ return { profile: emptyFleetProfile(), path, seeded: false };
101
+ }
102
+ const profile = await seedFleetProfile({ homeDir, read });
103
+ return { profile, path, seeded: true };
104
+ }
105
+
106
+ export async function saveFleetProfile(profile, {
107
+ homeDir = resolveHomeDir(),
108
+ writeAtomicJsonFn = writeAtomicJson,
109
+ mkdirFn = mkdir
110
+ } = {}) {
111
+ const path = fleetModelsPath(homeDir);
112
+ await mkdirFn(dirname(path), { recursive: true });
113
+ const next = {
114
+ ...profile,
115
+ version: FLEET_MODELS_VERSION,
116
+ updatedAt: new Date().toISOString()
117
+ };
118
+ await writeAtomicJsonFn(path, next);
119
+ return { path, profile: next };
120
+ }
121
+
122
+ /** Expand profile → assignment maps used by apply. */
123
+ export function profileToPlatformAssignments(profile) {
124
+ const claude = {};
125
+ const opencode = {};
126
+ if (profile.claudeDefault) claude.default = profile.claudeDefault;
127
+ for (const id of SDD_PHASES) {
128
+ const row = profile.phases?.[id] ?? {};
129
+ if (row.claude) claude[id] = row.claude;
130
+ if (row.opencode) opencode[id] = row.opencode;
131
+ }
132
+ return {
133
+ claude,
134
+ opencode,
135
+ codex: profile.codexDefault ? { codex_default: profile.codexDefault } : {},
136
+ cursorAgentModel: profile.cursorAgentModel ?? "inherit"
137
+ };
138
+ }
139
+
140
+ export function formatFleetProfileText(profile, { path = null } = {}) {
141
+ const lines = ["Fleet profile (multi-agent)", ""];
142
+ if (path) lines.push(`Path · ${path}`);
143
+ lines.push(`Claude default · ${profile.claudeDefault ?? "—"}`);
144
+ lines.push(`Codex default · ${profile.codexDefault ?? "—"} (single-model tool)`);
145
+ lines.push(`Cursor agents · ${profile.cursorAgentModel ?? "inherit"} (Auto IDE-managed)`);
146
+ lines.push("");
147
+ for (const id of SDD_PHASES) {
148
+ const row = profile.phases?.[id] ?? {};
149
+ lines.push(`${id} · claude ${row.claude ?? "—"} · opencode ${row.opencode ?? "—"}`);
150
+ }
151
+ if (profile.note) {
152
+ lines.push("");
153
+ lines.push(profile.note);
154
+ }
155
+ return lines.join("\n").trimEnd();
156
+ }
157
+
158
+ export async function runFleetModels({
159
+ json = false,
160
+ profile = false,
161
+ homeDir = resolveHomeDir()
162
+ } = {}) {
163
+ const { printJson } = await import("./json-output.js");
164
+ const { commandHeader } = await import("./brand/index.js");
165
+ const { buildFleetModelsCatalog, formatFleetModelsText } = await import(
166
+ "./observability/fleet-models-catalog.js"
167
+ );
168
+ const catalog = await buildFleetModelsCatalog({ homeDir });
169
+ if (!profile) {
170
+ if (json) printJson(catalog);
171
+ else {
172
+ console.log(commandHeader("Fleet models"));
173
+ console.log(formatFleetModelsText(catalog));
174
+ }
175
+ return catalog;
176
+ }
177
+
178
+ const loaded = await loadFleetProfile({ homeDir, seedIfMissing: true });
179
+ const payload = { ok: true, profile: loaded.profile, path: loaded.path, catalog };
180
+ if (json) printJson(payload);
181
+ else {
182
+ console.log(commandHeader("Fleet models"));
183
+ console.log(formatFleetProfileText(loaded.profile, { path: loaded.path }));
184
+ console.log("");
185
+ console.log(formatFleetModelsText(catalog));
186
+ }
187
+ return payload;
188
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Consent-gated fleet model writes (OpenCode / Claude / Codex).
3
+ * Plan by default; --yes applies with backup. Never touches Cursor Auto / state.vscdb.
4
+ */
5
+ import { copyFile, readFile, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { resolveHomeDir } from "./paths.js";
8
+ import { writeAtomicJson } from "./runtime/write-atomic-json.js";
9
+ import { printJson } from "./json-output.js";
10
+ import { commandHeader } from "./brand/index.js";
11
+ import { formatCliCommand } from "./brand/cli.js";
12
+ import {
13
+ parseFrontmatterModel,
14
+ replaceFrontmatterModel,
15
+ replaceCodexDefaultModel
16
+ } from "./observability/fleet-platforms.js";
17
+
18
+ const PLATFORMS = new Set(["opencode", "claude", "codex"]);
19
+
20
+ function backupPath(path, stamp = Date.now()) {
21
+ return `${path}.kairo-backup.${stamp}`;
22
+ }
23
+
24
+ function requirePlatform(platform) {
25
+ const p = String(platform ?? "").toLowerCase();
26
+ if (!PLATFORMS.has(p)) {
27
+ throw new Error(`Unsupported platform "${platform}". Use opencode, claude, or codex.`);
28
+ }
29
+ return p;
30
+ }
31
+
32
+ function requireAgent(agent) {
33
+ const id = String(agent ?? "").trim();
34
+ if (!id) throw new Error("Missing --agent <id>.");
35
+ return id;
36
+ }
37
+
38
+ function requireModel(model) {
39
+ const m = String(model ?? "").trim();
40
+ if (!m) throw new Error("Missing --model <id>.");
41
+ return m;
42
+ }
43
+
44
+ export function buildOpenCodeFleetSetPlan({ config, agent, model, configPath }) {
45
+ const agents = config?.agent ?? config?.agents;
46
+ if (!agents || typeof agents !== "object" || !(agent in agents)) {
47
+ throw new Error(`OpenCode agent "${agent}" not found in ${configPath}.`);
48
+ }
49
+ const prev = agents[agent];
50
+ const previousModel = typeof prev?.model === "string" ? prev.model : (config?.model ?? null);
51
+ const nextAgents = {
52
+ ...agents,
53
+ [agent]: { ...prev, model }
54
+ };
55
+ const next = { ...config, agent: nextAgents };
56
+ if (config.agents && !config.agent) {
57
+ delete next.agent;
58
+ next.agents = nextAgents;
59
+ }
60
+ return {
61
+ platform: "opencode",
62
+ agent,
63
+ model,
64
+ previousModel,
65
+ path: configPath,
66
+ wouldWrite: previousModel !== model,
67
+ next,
68
+ note: `Set OpenCode agent.${agent}.model → ${model}`
69
+ };
70
+ }
71
+
72
+ export async function runFleetSet({
73
+ platform,
74
+ agent,
75
+ model,
76
+ yes = false,
77
+ json = false,
78
+ dryRun = false,
79
+ homeDir = resolveHomeDir(),
80
+ read = readFile,
81
+ writeText = writeFile,
82
+ copyFileFn = copyFile,
83
+ writeAtomicJsonFn = writeAtomicJson,
84
+ now = () => Date.now()
85
+ } = {}) {
86
+ const p = requirePlatform(platform);
87
+ const nextModel = requireModel(model);
88
+ const agentId = p === "codex"
89
+ ? (String(agent ?? "default").trim() || "default")
90
+ : requireAgent(agent);
91
+ const apply = yes === true && dryRun !== true;
92
+
93
+ let plan;
94
+ let path;
95
+ let applyFn;
96
+
97
+ if (p === "opencode") {
98
+ path = join(homeDir, ".config", "opencode", "opencode.json");
99
+ const raw = await read(path, "utf8");
100
+ const config = JSON.parse(raw);
101
+ plan = buildOpenCodeFleetSetPlan({
102
+ config, agent: agentId, model: nextModel, configPath: path
103
+ });
104
+ applyFn = async () => {
105
+ await writeAtomicJsonFn(path, plan.next);
106
+ };
107
+ } else if (p === "claude") {
108
+ if (agentId === "default") {
109
+ path = join(homeDir, ".claude", "settings.json");
110
+ const existing = JSON.parse(await read(path, "utf8"));
111
+ const previousModel = typeof existing.model === "string" ? existing.model : null;
112
+ plan = {
113
+ platform: "claude",
114
+ agent: agentId,
115
+ model: nextModel,
116
+ previousModel,
117
+ path,
118
+ wouldWrite: previousModel !== nextModel,
119
+ next: { ...existing, model: nextModel },
120
+ note: `Set Claude settings.model → ${nextModel}`
121
+ };
122
+ applyFn = async () => {
123
+ await writeAtomicJsonFn(path, plan.next);
124
+ };
125
+ } else {
126
+ path = join(homeDir, ".claude", "agents", `${agentId}.md`);
127
+ const raw = await read(path, "utf8");
128
+ const previousModel = parseFrontmatterModel(raw).model;
129
+ const nextText = replaceFrontmatterModel(raw, nextModel);
130
+ plan = {
131
+ platform: "claude",
132
+ agent: agentId,
133
+ model: nextModel,
134
+ previousModel,
135
+ path,
136
+ wouldWrite: previousModel !== nextModel,
137
+ nextText,
138
+ note: `Set Claude agent ${agentId} frontmatter model → ${nextModel}`
139
+ };
140
+ applyFn = async () => {
141
+ await writeText(path, plan.nextText, "utf8");
142
+ };
143
+ }
144
+ } else {
145
+ path = join(homeDir, ".codex", "config.toml");
146
+ const raw = await read(path, "utf8");
147
+ const previousMatch = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
148
+ const previousModel = previousMatch ? previousMatch[1] : null;
149
+ const nextText = replaceCodexDefaultModel(raw, nextModel);
150
+ plan = {
151
+ platform: "codex",
152
+ agent: "default",
153
+ model: nextModel,
154
+ previousModel,
155
+ path,
156
+ wouldWrite: previousModel !== nextModel,
157
+ nextText,
158
+ note: `Set Codex config.toml model → ${nextModel}`
159
+ };
160
+ applyFn = async () => {
161
+ await writeText(path, plan.nextText, "utf8");
162
+ };
163
+ }
164
+
165
+ plan.backupPath = backupPath(path, now());
166
+ plan.applyWith = formatCliCommand(
167
+ `fleet set --platform ${p} --agent ${plan.agent} --model ${nextModel} --yes`
168
+ );
169
+
170
+ if (!apply) {
171
+ const payload = {
172
+ ok: true,
173
+ applied: false,
174
+ plan: {
175
+ platform: plan.platform,
176
+ agent: plan.agent,
177
+ model: plan.model,
178
+ previousModel: plan.previousModel,
179
+ path: plan.path,
180
+ wouldWrite: plan.wouldWrite,
181
+ note: plan.note,
182
+ applyWith: plan.applyWith
183
+ }
184
+ };
185
+ if (json) printJson(payload);
186
+ else {
187
+ console.log(commandHeader("Fleet set"));
188
+ console.log(plan.note);
189
+ console.log(`Path · ${plan.path}`);
190
+ console.log(`Was · ${plan.previousModel ?? "—"}`);
191
+ console.log(`Now · ${plan.model}`);
192
+ console.log(`Apply · ${plan.applyWith}`);
193
+ }
194
+ return payload;
195
+ }
196
+
197
+ await copyFileFn(path, plan.backupPath);
198
+ await applyFn();
199
+
200
+ const receipt = {
201
+ ok: true,
202
+ applied: true,
203
+ platform: plan.platform,
204
+ agent: plan.agent,
205
+ model: plan.model,
206
+ previousModel: plan.previousModel,
207
+ path: plan.path,
208
+ backupPath: plan.backupPath,
209
+ note: "Model assignment updated. Refresh Kairo Fleet to see declared changes."
210
+ };
211
+ if (json) printJson(receipt);
212
+ else {
213
+ console.log(commandHeader("Fleet set"));
214
+ console.log(`Wrote · ${receipt.path}`);
215
+ console.log(`Backup · ${receipt.backupPath}`);
216
+ console.log(receipt.note);
217
+ }
218
+ return receipt;
219
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared fleet constants + Gentle assignment loader (no circular imports).
3
+ */
4
+ import { readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+
7
+ export const SDD_PHASES = Object.freeze([
8
+ "sdd-apply", "sdd-archive", "sdd-design", "sdd-explore", "sdd-init",
9
+ "sdd-onboard", "sdd-propose", "sdd-spec", "sdd-tasks", "sdd-verify"
10
+ ]);
11
+
12
+ /** Map Gentle/Claude tier names → OpenCode provider/model ids. */
13
+ export const CLAUDE_TO_OPENCODE = Object.freeze({
14
+ opus: "opencode-go/deepseek-v4-pro",
15
+ sonnet: "opencode-go/qwen3.5-plus",
16
+ haiku: "opencode-go/deepseek-v4-flash"
17
+ });
18
+
19
+ export async function loadGentleClaudeAssignments(homeDir, read = readFile) {
20
+ try {
21
+ const state = JSON.parse(await read(join(homeDir, ".gentle-ai", "state.json"), "utf8"));
22
+ const map = state?.claude_model_assignments;
23
+ if (!map || typeof map !== "object") return null;
24
+ return { ...map };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function mapClaudeAssignmentsToOpenCode(assignments = {}) {
31
+ const out = {};
32
+ for (const [agent, tier] of Object.entries(assignments)) {
33
+ if (agent === "default" || agent === "codex_default") continue;
34
+ const key = String(tier ?? "").toLowerCase();
35
+ out[agent] = CLAUDE_TO_OPENCODE[key] ?? CLAUDE_TO_OPENCODE.sonnet;
36
+ }
37
+ return out;
38
+ }
@@ -29,7 +29,7 @@ export {
29
29
  export function createCockpitUiState({
30
30
  layoutMode = LAYOUT_MODES.COMPACT,
31
31
  view = ORCHESTRATOR_VIEWS.HOME,
32
- region = COCKPIT_REGIONS.NAV,
32
+ region = null,
33
33
  navIndex = 0,
34
34
  listIndex = 0,
35
35
  helpOpen = false,
@@ -122,9 +122,7 @@ export function reduceCockpitUi(state, action) {
122
122
  if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
123
123
 
124
124
  const navigatesNav = state.region === COCKPIT_REGIONS.NAV
125
- || isNavFocusedView(state.view)
126
- || (state.view === ORCHESTRATOR_VIEWS.HOME
127
- && state.layoutMode === LAYOUT_MODES.MINIMAL);
125
+ || isNavFocusedView(state.view);
128
126
 
129
127
  if (navigatesNav) {
130
128
  const delta = action.direction === "up" ? -1 : 1;
@@ -11,7 +11,9 @@ export function resolveEnterNavIntent({
11
11
  } = {}) {
12
12
  if (!navItem) return { kind: "noop" };
13
13
 
14
- const isOverview = navItem.id === "overview" || navItem.view === ORCHESTRATOR_VIEWS.HOME;
14
+ const isOverview = navItem.id === "home"
15
+ || navItem.id === "overview"
16
+ || navItem.view === ORCHESTRATOR_VIEWS.HOME;
15
17
  if (isOverview) {
16
18
  if (currentView === ORCHESTRATOR_VIEWS.HOME && ctaDestination) {
17
19
  if (ctaDestination === "setup") return { kind: "activate-setup" };
@@ -3,7 +3,6 @@ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
3
3
  import { LAYOUT_MODES } from "./layout.js";
4
4
 
5
5
  const NAV_FOCUSED_VIEWS = new Set([
6
- ORCHESTRATOR_VIEWS.HOME,
7
6
  ORCHESTRATOR_VIEWS.IDES,
8
7
  ORCHESTRATOR_VIEWS.MODULES,
9
8
  ORCHESTRATOR_VIEWS.CHANGES,
@@ -14,6 +13,7 @@ const NAV_FOCUSED_VIEWS = new Set([
14
13
  ]);
15
14
 
16
15
  const CONTENT_INTERACTIVE_VIEWS = new Set([
16
+ ORCHESTRATOR_VIEWS.HOME,
17
17
  ORCHESTRATOR_VIEWS.RUNS,
18
18
  ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
19
19
  ORCHESTRATOR_VIEWS.RECENT_RUNS,
@@ -102,6 +102,12 @@ export function routeCockpitKey(state, keyAction) {
102
102
  if (state.region === COCKPIT_REGIONS.NAV || isNavFocusedView(state.view)) {
103
103
  return { type: "enter-nav" };
104
104
  }
105
+ if (
106
+ state.region === COCKPIT_REGIONS.CONTENT
107
+ && state.view === ORCHESTRATOR_VIEWS.HOME
108
+ ) {
109
+ return { type: "enter-home-button" };
110
+ }
105
111
  return null;
106
112
  default:
107
113
  return null;
@@ -17,23 +17,33 @@ export const COCKPIT_REGIONS = {
17
17
 
18
18
  export const COCKPIT_NAV = [
19
19
  {
20
- id: "overview",
21
- label: "Overview",
20
+ id: "home",
21
+ label: "Home",
22
22
  view: ORCHESTRATOR_VIEWS.HOME,
23
- description: "Status, next action, activity, alerts, and tokens."
23
+ description: "What needs you, and what to do about it."
24
24
  },
25
+ {
26
+ id: "settings",
27
+ label: "Settings",
28
+ view: ORCHESTRATOR_VIEWS.PROFILE,
29
+ description: "Choose agents, connect Obsidian, add integrations."
30
+ },
31
+ {
32
+ id: "history",
33
+ label: "History",
34
+ view: ORCHESTRATOR_VIEWS.ACTIVITY,
35
+ description: "What Kairo changed, and how to undo it."
36
+ }
37
+ ];
38
+
39
+ /** Destinations reachable via the action palette, not the top nav. */
40
+ export const COCKPIT_SECONDARY = [
25
41
  {
26
42
  id: "governance",
27
43
  label: "Governance",
28
44
  view: ORCHESTRATOR_VIEWS.CHANGES,
29
45
  description: "Repair drift and apply governed changes."
30
46
  },
31
- {
32
- id: "activity",
33
- label: "Activity",
34
- view: ORCHESTRATOR_VIEWS.ACTIVITY,
35
- description: "Operations, backups, and recovery."
36
- },
37
47
  {
38
48
  id: "orchestration",
39
49
  label: "Orchestration",
@@ -45,12 +55,6 @@ export const COCKPIT_NAV = [
45
55
  label: "Usage",
46
56
  view: ORCHESTRATOR_VIEWS.USAGE,
47
57
  description: "Token and context pressure when auditable."
48
- },
49
- {
50
- id: "settings",
51
- label: "Settings",
52
- view: ORCHESTRATOR_VIEWS.PROFILE,
53
- description: "Profile, policy, and guided configuration."
54
58
  }
55
59
  ];
56
60
 
@@ -64,8 +68,8 @@ export function regionsForLayout(layoutMode) {
64
68
 
65
69
  export function navIndexForView(view, items = COCKPIT_NAV) {
66
70
  if (isRunsBranchView(view)) {
67
- const orchIndex = items.findIndex((item) => item.id === "orchestration");
68
- return orchIndex >= 0 ? orchIndex : 0;
71
+ const historyIndex = items.findIndex((item) => item.id === "history");
72
+ return historyIndex >= 0 ? historyIndex : 0;
69
73
  }
70
74
  const index = items.findIndex((item) => item.view === view);
71
75
  return index >= 0 ? index : 0;
@@ -105,6 +109,7 @@ export function resolveNavStatusSummary(item, {
105
109
  const backups = snapshot?.backups?.count ?? 0;
106
110
 
107
111
  switch (item.id) {
112
+ case "home":
108
113
  case "overview":
109
114
  return snapshot?.health?.replaceAll("_", " ")
110
115
  ?? resolveProjectReadiness({
@@ -114,6 +119,7 @@ export function resolveNavStatusSummary(item, {
114
119
  }).label;
115
120
  case "governance":
116
121
  return changes > 0 ? `${changes} pending` : "Clean";
122
+ case "history":
117
123
  case "activity":
118
124
  return backups > 0 ? `${backups} backups` : "No backups";
119
125
  case "orchestration":
@@ -174,6 +180,7 @@ export function buildNavModel({
174
180
  const mapped = items.map((item, index) => {
175
181
  const isSelected = index === navIndex;
176
182
  const isCurrent = item.view === currentView
183
+ || (item.id === "history" && isRunsBranchView(currentView))
177
184
  || (item.id === "orchestration" && isRunsBranchView(currentView));
178
185
  return {
179
186
  ...item,
@@ -185,11 +192,13 @@ export function buildNavModel({
185
192
  };
186
193
  });
187
194
 
195
+ const explanation = !selected || selected.id === "home" || selected.id === "overview"
196
+ ? ""
197
+ : (selected.description ?? "");
198
+
188
199
  return {
189
200
  title: "NAVIGATION",
190
- explanation: selected
191
- ? `${selected.description} (${resolveNavStatusSummary(selected, { dashboard, diagnostics, snapshot })})`
192
- : "",
201
+ explanation,
193
202
  items: mapped
194
203
  };
195
204
  }
@@ -319,8 +328,11 @@ export function buildFooterModel({
319
328
 
320
329
  // HOME footer must stay on one line at 80 cols (frame already near 24 rows).
321
330
  if (view === ORCHESTRATOR_VIEWS.HOME) {
331
+ const homeParts = region === COCKPIT_REGIONS.NAV
332
+ ? ["↑↓ Section", "Enter Open", "? Help", "Esc Exit"]
333
+ : ["1·2 Select", "Enter Run", "? Help", "Esc Exit"];
322
334
  return {
323
- text: ["↑↓", "Enter", "Space", "R", "?", "/", "Esc"].join(` ${glyphs.bullet} `),
335
+ text: homeParts.join(` ${glyphs.bullet} `),
324
336
  columns: footerColumns
325
337
  };
326
338
  }
@@ -1,6 +1,8 @@
1
- import { COCKPIT_NAV } from "./cockpit-models.js";
1
+ import { COCKPIT_NAV, COCKPIT_SECONDARY } from "./cockpit-models.js";
2
2
  import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
3
3
 
4
+ const PALETTE_NAV_ITEMS = [...COCKPIT_NAV, ...COCKPIT_SECONDARY];
5
+
4
6
  export const PALETTE_KINDS = Object.freeze({
5
7
  NAVIGATE: "navigate",
6
8
  REFRESH: "refresh",
@@ -12,10 +14,13 @@ const DESTINATION_VIEWS = Object.freeze({
12
14
  changes: ORCHESTRATOR_VIEWS.CHANGES,
13
15
  governance: ORCHESTRATOR_VIEWS.CHANGES,
14
16
  "control-center": ORCHESTRATOR_VIEWS.HOME,
17
+ home: ORCHESTRATOR_VIEWS.HOME,
15
18
  ides: ORCHESTRATOR_VIEWS.IDES,
16
19
  modules: ORCHESTRATOR_VIEWS.MODULES,
17
20
  activity: ORCHESTRATOR_VIEWS.ACTIVITY,
21
+ history: ORCHESTRATOR_VIEWS.ACTIVITY,
18
22
  profile: ORCHESTRATOR_VIEWS.PROFILE,
23
+ settings: ORCHESTRATOR_VIEWS.PROFILE,
19
24
  runs: ORCHESTRATOR_VIEWS.RUNS,
20
25
  orchestration: ORCHESTRATOR_VIEWS.RUNS,
21
26
  usage: ORCHESTRATOR_VIEWS.USAGE,
@@ -30,12 +35,12 @@ export function canOpenPalette({ loading = false, busy = false, confirming = fal
30
35
  return !loading && !busy && !confirming;
31
36
  }
32
37
 
33
- /** Optional CTA + six destinations + Refresh + Help. No write shortcuts. */
38
+ /** Optional CTA + primary/secondary destinations + Refresh + Help. No write shortcuts. */
34
39
  export function buildPaletteActions({
35
40
  ctaDestination = null,
36
41
  ctaTitle = null,
37
42
  ctaDetail = null,
38
- navItems = COCKPIT_NAV
43
+ navItems = PALETTE_NAV_ITEMS
39
44
  } = {}) {
40
45
  const actions = [];
41
46
  if (ctaDestination === "setup") {
@@ -100,7 +100,8 @@ export function renderCockpitView({
100
100
  governanceDetailsOpen = false,
101
101
  activityDetailsOpen = false,
102
102
  contentFocused = false,
103
- homeDir = null
103
+ homeDir = null,
104
+ hasGlobalState = false
104
105
  }) {
105
106
  if (palette) {
106
107
  return React.createElement(PalettePanel, { model: palette, colorEnabled });
@@ -112,7 +113,13 @@ export function renderCockpitView({
112
113
  detailsOpen: overviewDetailsOpen,
113
114
  colorEnabled,
114
115
  unicode,
115
- layoutMode
116
+ layoutMode,
117
+ selectedIndex: listIndex,
118
+ contentFocused,
119
+ hasGlobalState,
120
+ snapshot,
121
+ diagnostics,
122
+ dashboard
116
123
  });
117
124
  case ORCHESTRATOR_VIEWS.USAGE:
118
125
  return React.createElement(SemanticUsagePanel, {