@kal-elsam/kairo-runtime 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,13 +24,37 @@ commands) without depending on Pi as a runtime or adding a Pi adapter.
24
24
 
25
25
  ## Quick start
26
26
 
27
- Recommended entry — run Kairo Runtime in your terminal (interactive setup wizard in a TTY):
27
+ Recommended entry — run Kairo Runtime in your terminal:
28
28
 
29
29
  ```bash
30
30
  npx @kal-elsam/kairo-runtime
31
+ # or, after a global install:
32
+ kairo
31
33
  ```
32
34
 
33
- Preview without writing anything:
35
+ **First run** (no `~/.harness/state.json`): interactive onboarding → safe diagnosis →
36
+ setup with confirmation → full-screen operations cockpit.
37
+
38
+ **Later runs** (state present): full-screen cockpit with mission control (recommended
39
+ action), navigation, and a system strip. Layout adapts to terminal size:
40
+
41
+ | Mode | Size | Layout |
42
+ |------|------|--------|
43
+ | Wide | ≥100 cols × ≥28 rows | Nav + content + system |
44
+ | Compact | ≥72×20 | Nav + content |
45
+ | Minimal | 60–71 cols or short height | Single panel + nav header |
46
+ | Below gate | <60 cols | Explicit TTY fallback (Ink disabled) |
47
+
48
+ Keys: `↑↓` navigate · `Tab` region · `Enter` open · `R` refresh · `C` cancel run ·
49
+ `?` help · `Esc` back (exit only from Home).
50
+
51
+ Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
52
+ in text, never color alone.
53
+
54
+ Explicit commands and setup flags keep their current behavior (`kairo setup`,
55
+ `kairo --dry-run`, `kairo shell`, non-TTY scripts, etc.).
56
+
57
+ Preview setup without writing anything:
34
58
 
35
59
  ```bash
36
60
  npx @kal-elsam/kairo-runtime --dry-run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Gemini, Copilot, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
@@ -33,6 +33,7 @@
33
33
  "doctor": "node ./bin/kairo.js doctor",
34
34
  "smoke": "bash scripts/smoke-test.sh",
35
35
  "ux:smoke": "bash scripts/ux-smoke-test.sh",
36
+ "smoke:cockpit": "bash scripts/cockpit-smoke-test.sh",
36
37
  "smoke:registry": "bash scripts/registry-smoke-test.sh",
37
38
  "smoke:installer": "bash scripts/installer-smoke-test.sh",
38
39
  "smoke:bridge": "bash scripts/bridge-smoke-test.sh",
@@ -59,6 +60,7 @@
59
60
  },
60
61
  "dependencies": {
61
62
  "@clack/prompts": "^1.7.0",
63
+ "ansi-escapes": "^7.3.0",
62
64
  "ink": "^5.2.1",
63
65
  "react": "^18.3.1"
64
66
  }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Cockpit smoke — layout/capabilities + package wiring (no PTY required).
3
+ set -euo pipefail
4
+
5
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ cd "$ROOT"
7
+
8
+ echo "Kairo cockpit smoke"
9
+ node "$ROOT/scripts/cockpit-smoke.mjs"
10
+
11
+ node --test \
12
+ test/fullscreen-session.test.js \
13
+ test/layout.test.js \
14
+ test/terminal-capabilities.test.js \
15
+ test/cockpit-models.test.js \
16
+ test/cockpit-controller.test.js \
17
+ test/cockpit-frame.test.js
18
+
19
+ echo "Kairo cockpit smoke passed"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { resolveLayoutMode, LAYOUT_MODES } from "../src/global/ink/layout.js";
7
+ import { resolveTerminalCapabilities } from "../src/global/ink/terminal-capabilities.js";
8
+ import { createFullscreenSession } from "../src/global/ink/fullscreen-session.js";
9
+ import { buildTopBarModel, buildHomeMissionModel } from "../src/global/ink/cockpit-models.js";
10
+
11
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require(join(root, "package.json"));
14
+
15
+ assert.equal(pkg.version, "0.3.0");
16
+ assert.ok(pkg.dependencies["ansi-escapes"]);
17
+
18
+ assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
19
+ assert.equal(resolveLayoutMode({ columns: 80, rows: 24 }), LAYOUT_MODES.COMPACT);
20
+ assert.equal(resolveLayoutMode({ columns: 65, rows: 24 }), LAYOUT_MODES.MINIMAL);
21
+ assert.equal(resolveLayoutMode({ columns: 50, rows: 24 }), null);
22
+
23
+ const caps = resolveTerminalCapabilities({
24
+ columns: 80,
25
+ rows: 24,
26
+ isTTY: true,
27
+ term: "xterm-256color",
28
+ env: { NO_COLOR: "1" }
29
+ });
30
+ assert.equal(caps.color, false);
31
+ assert.equal(caps.canUseInk, true);
32
+
33
+ const session = createFullscreenSession({
34
+ stdout: { isTTY: true, write: () => true },
35
+ processRef: { on() {}, removeListener() {}, exit() {} },
36
+ onSignal: () => {}
37
+ });
38
+ assert.equal(session.enter(), true);
39
+ assert.equal(session.leave(), true);
40
+ assert.equal(session.leave(), false);
41
+
42
+ const top = buildTopBarModel({ projectName: "smoke" });
43
+ assert.match(top.status, /ONLINE|Offline/);
44
+ const mission = buildHomeMissionModel({
45
+ hasGlobalState: false,
46
+ diagnostics: { diagnostics: { detected: 0 } },
47
+ dashboard: { providers: [], recentRuns: [] }
48
+ });
49
+ assert.match(mission.title, /MISSION CONTROL/);
50
+
51
+ console.log("cockpit smoke OK");
package/src/cli.js CHANGED
@@ -42,6 +42,11 @@ import {
42
42
  resolveSuggestedInvocation
43
43
  } from "./global/brand/cli.js";
44
44
  import { BRAND } from "./global/brand/index.js";
45
+ import {
46
+ INITIAL_EXPERIENCE,
47
+ hasConfiguredGlobalState,
48
+ resolveInitialExperience
49
+ } from "./global/initial-experience.js";
45
50
 
46
51
  export { resolveSuggestedInvocation };
47
52
 
@@ -50,7 +55,7 @@ const packageRoot = resolve(__dirname, "..");
50
55
  const SCOPES = new Set(["agent-global", "workspace"]);
51
56
 
52
57
  export async function runCli(argv) {
53
- const { command, options } = parseArgs(argv);
58
+ const { command, options, isImplicitCommand } = parseArgs(argv);
54
59
  maybeWarnLegacyCli(process.argv, { json: options.json });
55
60
 
56
61
  if (options.help || command === "help") {
@@ -69,14 +74,22 @@ export async function runCli(argv) {
69
74
  const invoke = resolveSuggestedInvocation(packageManifest.name);
70
75
 
71
76
  switch (command) {
72
- case "shell":
77
+ case "shell": {
78
+ const homeDir = resolveHomeDir();
79
+ const resolvedMode = resolveInitialExperience({
80
+ interactive: optionsWithPolicy.interactive,
81
+ isImplicitCommand,
82
+ hasGlobalState: hasConfiguredGlobalState(homeDir)
83
+ });
73
84
  await runOrchestratorShell({
74
85
  packageRoot,
75
86
  packageManifest,
76
87
  workspaceRoot: optionsWithPolicy.cwd,
77
- interactive: optionsWithPolicy.interactive
88
+ interactive: optionsWithPolicy.interactive,
89
+ initialMode: resolvedMode ?? INITIAL_EXPERIENCE.DASHBOARD
78
90
  });
79
91
  return;
92
+ }
80
93
  case "orchestrator":
81
94
  await runOrchestratorDiagnostics({
82
95
  homeDir: resolveHomeDir(),
@@ -468,7 +481,7 @@ export function parseArgs(argv) {
468
481
  options.task = args.join(" ").trim();
469
482
  }
470
483
 
471
- return { command, options };
484
+ return { command, options, isImplicitCommand: implicitCommand };
472
485
  }
473
486
 
474
487
  function parseComponentsAction(args, options) {
@@ -683,10 +696,11 @@ sections, components, backups, and drift repair under ~/.harness.
683
696
  Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
684
697
 
685
698
  Usage:
686
- ${cli} Interactive orchestrator shell (TTY)
699
+ ${cli} First run: onboarding → setup → cockpit (TTY).
700
+ Later: full-screen cockpit (wide/compact/minimal).
687
701
  ${cli} --dry-run Setup dry-run (scriptable)
688
702
  ${cli} --version
689
- ${cli} shell Operations dashboard (TTY)
703
+ ${cli} shell Operations cockpit (TTY)
690
704
  ${cli} run --agent <id> --task "..." [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
691
705
  ${cli} runs list [--json] [--limit <n>] [--active-only]
692
706
  ${cli} runs show <runId> [--json] [--limit <n>] [--follow]
@@ -727,7 +741,9 @@ Scopes:
727
741
  Explicit --scope=workspace only.
728
742
 
729
743
  Commands:
730
- shell Operations dashboard (TTY). Bare ${cli} opens this in TTY sessions.
744
+ shell Operations cockpit (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
745
+ is missing, otherwise the cockpit. Explicit ${cli} shell always opens the cockpit.
746
+ Keys: ↑↓ · Tab region · Enter · R refresh · C cancel · ? help · Esc back/exit.
731
747
  run Launch a managed agent run with local audit trail.
732
748
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
733
749
  orchestrator Read-only capability registry diagnostics (--json supported).
@@ -70,6 +70,16 @@ export const WIZARD_COPY = {
70
70
  coreOnlyLabel: "Core only (no components)"
71
71
  };
72
72
 
73
+ /** First-run framing reused by onboarding → setup. */
74
+ export const ONBOARDING_COPY = {
75
+ welcomeTitle: `Welcome to ${BRAND.displayName}`,
76
+ purpose:
77
+ `${BRAND.displayName} detects, configures, and coordinates the local agents you already use.`,
78
+ safety:
79
+ "Diagnosis is read-only. Nothing is modified until you confirm a plan.",
80
+ continueHint: "Press Enter to diagnose and configure · Esc to exit"
81
+ };
82
+
73
83
  export function getAgentLabel(agentId) {
74
84
  return AGENT_LABELS[agentId] ?? agentId;
75
85
  }
@@ -0,0 +1,66 @@
1
+ import { formatCliCommand } from "./brand/cli.js";
2
+
3
+ export const DASHBOARD_PURPOSE =
4
+ "Detects, configures, and coordinates local AI agents — no changes without confirmation.";
5
+
6
+ export const NEXT_STEP_KINDS = {
7
+ CONFIGURE: "configure",
8
+ ENABLE_INTELLIGENCE: "enable_intelligence",
9
+ LAUNCH: "launch",
10
+ REVIEW: "review"
11
+ };
12
+
13
+ export function formatDashboardPurpose() {
14
+ return DASHBOARD_PURPOSE;
15
+ }
16
+
17
+ /**
18
+ * Contextual next step from existing diagnostics + dashboard snapshot.
19
+ * Priority: configure → review problems → enable intelligence → launch.
20
+ */
21
+ export function resolveDashboardRecommendation({
22
+ hasGlobalState = false,
23
+ diagnostics = null,
24
+ dashboard = null
25
+ } = {}) {
26
+ const summary = diagnostics?.diagnostics ?? { detected: 0, errors: 0 };
27
+ const intelligence = diagnostics?.intelligence?.summary;
28
+ const launchableCount = (dashboard?.providers ?? []).filter((entry) => entry.launchable).length;
29
+ const hasErrors = (summary.errors ?? 0) > 0;
30
+ const hasProblemRecommendation = (diagnostics?.recommendations ?? []).some((line) =>
31
+ /error|fix|drift|not detected|failed|problem/i.test(line)
32
+ );
33
+
34
+ if (!hasGlobalState || (summary.detected ?? 0) === 0) {
35
+ return {
36
+ kind: NEXT_STEP_KINDS.CONFIGURE,
37
+ message: `Configure the local environment with ${formatCliCommand("setup")}.`
38
+ };
39
+ }
40
+
41
+ if (hasErrors || hasProblemRecommendation) {
42
+ return {
43
+ kind: NEXT_STEP_KINDS.REVIEW,
44
+ message: "Review diagnostics for problems before launching a run."
45
+ };
46
+ }
47
+
48
+ if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
49
+ return {
50
+ kind: NEXT_STEP_KINDS.ENABLE_INTELLIGENCE,
51
+ message: "Enable intelligence: start Ollama or set OPENROUTER_API_KEY, then retry."
52
+ };
53
+ }
54
+
55
+ if (launchableCount > 0) {
56
+ return {
57
+ kind: NEXT_STEP_KINDS.LAUNCH,
58
+ message: "Launch a supervised run from the menu or with kairo run."
59
+ };
60
+ }
61
+
62
+ return {
63
+ kind: NEXT_STEP_KINDS.REVIEW,
64
+ message: "Review diagnostics for problems before launching a run."
65
+ };
66
+ }
@@ -0,0 +1,34 @@
1
+ import { existsSync } from "node:fs";
2
+ import { harnessHomePaths } from "./paths.js";
3
+
4
+ export const INITIAL_EXPERIENCE = {
5
+ ONBOARDING: "onboarding",
6
+ DASHBOARD: "dashboard"
7
+ };
8
+
9
+ /**
10
+ * First-run marker is ~/.harness/state.json only.
11
+ * profile.json does not participate in this decision.
12
+ */
13
+ export function hasConfiguredGlobalState(homeDir) {
14
+ return existsSync(harnessHomePaths(homeDir).statePath);
15
+ }
16
+
17
+ /**
18
+ * Pure resolver for the interactive bare-entry experience.
19
+ * Returns null when CLI should keep existing non-onboarding paths
20
+ * (non-TTY, explicit commands, setup flags already routed elsewhere).
21
+ */
22
+ export function resolveInitialExperience({
23
+ interactive = false,
24
+ isImplicitCommand = false,
25
+ hasGlobalState = false
26
+ } = {}) {
27
+ if (!interactive || !isImplicitCommand) {
28
+ return null;
29
+ }
30
+
31
+ return hasGlobalState
32
+ ? INITIAL_EXPERIENCE.DASHBOARD
33
+ : INITIAL_EXPERIENCE.ONBOARDING;
34
+ }
@@ -0,0 +1,131 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { COCKPIT_COLORS, statusColor } from "../theme.js";
4
+
5
+ export function CockpitBadge({ label, kind = "ready", colorEnabled = true }) {
6
+ return React.createElement(Text, {
7
+ color: statusColor(kind, { colorEnabled })
8
+ }, label);
9
+ }
10
+
11
+ export function CockpitEmptyState({ title, message, hint }) {
12
+ return React.createElement(Box, { flexDirection: "column", marginY: 1 },
13
+ title && React.createElement(Text, { bold: true, color: COCKPIT_COLORS.secondary }, title),
14
+ message && React.createElement(Text, null, message),
15
+ hint && React.createElement(Text, { color: COCKPIT_COLORS.muted }, hint)
16
+ );
17
+ }
18
+
19
+ export function CockpitPanel({ title, focused = false, width, children }) {
20
+ return React.createElement(Box, {
21
+ flexDirection: "column",
22
+ width,
23
+ borderStyle: "single",
24
+ borderColor: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted,
25
+ paddingX: 1,
26
+ flexGrow: 1
27
+ },
28
+ title && React.createElement(Text, {
29
+ bold: true,
30
+ color: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
31
+ }, title),
32
+ children
33
+ );
34
+ }
35
+
36
+ export function CockpitTopBar({ model, colorEnabled = true }) {
37
+ return React.createElement(Box, { justifyContent: "space-between", width: "100%" },
38
+ React.createElement(Text, {
39
+ bold: true,
40
+ color: colorEnabled ? COCKPIT_COLORS.primary : undefined
41
+ }, `╭─ ${model.brand} ─ ${model.status}`),
42
+ React.createElement(Text, {
43
+ color: colorEnabled ? COCKPIT_COLORS.muted : undefined
44
+ }, `${model.projectLabel} ─╮`)
45
+ );
46
+ }
47
+
48
+ export function CockpitNav({ model, colorEnabled = true }) {
49
+ return React.createElement(Box, { flexDirection: "column" },
50
+ model.items.map((item) =>
51
+ React.createElement(Text, {
52
+ key: item.id,
53
+ bold: item.focused || item.selected,
54
+ color: item.focused
55
+ ? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
56
+ : item.selected
57
+ ? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
58
+ : undefined
59
+ }, `${item.marker} ${item.label}`)
60
+ )
61
+ );
62
+ }
63
+
64
+ export function CockpitSystemStrip({ model, colorEnabled = true }) {
65
+ return React.createElement(Box, { flexDirection: "column" },
66
+ model.rows.map((row) =>
67
+ React.createElement(Text, { key: row.key },
68
+ React.createElement(Text, { color: COCKPIT_COLORS.muted }, `${row.key.padEnd(7)}`),
69
+ React.createElement(CockpitBadge, {
70
+ label: row.value,
71
+ kind: row.kind,
72
+ colorEnabled
73
+ })
74
+ )
75
+ )
76
+ );
77
+ }
78
+
79
+ export function CockpitFooter({ model }) {
80
+ return React.createElement(Box, { flexDirection: "column" },
81
+ React.createElement(Text, { color: COCKPIT_COLORS.muted },
82
+ `├${"─".repeat(62)}┤`
83
+ ),
84
+ React.createElement(Text, { color: COCKPIT_COLORS.muted }, `│ ${model.text}`),
85
+ React.createElement(Text, { color: COCKPIT_COLORS.muted },
86
+ `╰${"─".repeat(62)}╯`
87
+ )
88
+ );
89
+ }
90
+
91
+ export function CockpitShell({
92
+ topBar,
93
+ footer,
94
+ layoutMode,
95
+ nav,
96
+ system,
97
+ navFocused,
98
+ contentFocused,
99
+ systemFocused,
100
+ colorEnabled = true,
101
+ children
102
+ }) {
103
+ const showNav = layoutMode === "wide" || layoutMode === "compact";
104
+ const showSystem = layoutMode === "wide";
105
+
106
+ return React.createElement(Box, { flexDirection: "column", width: "100%" },
107
+ React.createElement(CockpitTopBar, { model: topBar, colorEnabled }),
108
+ layoutMode === "minimal" && nav && React.createElement(Box, { marginY: 0 },
109
+ React.createElement(Text, { color: COCKPIT_COLORS.muted }, "Nav: "),
110
+ React.createElement(CockpitNav, { model: nav, colorEnabled })
111
+ ),
112
+ React.createElement(Box, { flexDirection: "row", width: "100%" },
113
+ showNav && React.createElement(CockpitPanel, {
114
+ title: nav?.title ?? "NAVIGATION",
115
+ focused: navFocused,
116
+ width: 22
117
+ }, React.createElement(CockpitNav, { model: nav, colorEnabled })),
118
+ React.createElement(CockpitPanel, {
119
+ title: undefined,
120
+ focused: contentFocused,
121
+ width: showSystem ? 48 : showNav ? 56 : "100%"
122
+ }, children),
123
+ showSystem && React.createElement(CockpitPanel, {
124
+ title: system?.title ?? "SYSTEM",
125
+ focused: systemFocused,
126
+ width: 20
127
+ }, React.createElement(CockpitSystemStrip, { model: system, colorEnabled }))
128
+ ),
129
+ React.createElement(CockpitFooter, { model: footer })
130
+ );
131
+ }
@@ -0,0 +1,131 @@
1
+ import { COCKPIT_NAV, COCKPIT_REGIONS, regionsForLayout } from "./cockpit-models.js";
2
+ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
3
+ import { LAYOUT_MODES } from "./layout.js";
4
+
5
+ /**
6
+ * Pure cockpit focus / navigation reducer — no I/O, no React.
7
+ */
8
+
9
+ export function createCockpitUiState({
10
+ layoutMode = LAYOUT_MODES.COMPACT,
11
+ view = ORCHESTRATOR_VIEWS.HOME,
12
+ region = COCKPIT_REGIONS.NAV,
13
+ navIndex = 0,
14
+ listIndex = 0,
15
+ helpOpen = false
16
+ } = {}) {
17
+ const regions = regionsForLayout(layoutMode);
18
+ const safeRegion = regions.includes(region) ? region : regions[0];
19
+ return {
20
+ layoutMode,
21
+ view,
22
+ region: safeRegion,
23
+ navIndex: clamp(navIndex, 0, COCKPIT_NAV.length - 1),
24
+ listIndex: Math.max(0, listIndex),
25
+ helpOpen,
26
+ shouldExit: false
27
+ };
28
+ }
29
+
30
+ export function reduceCockpitUi(state, action) {
31
+ switch (action.type) {
32
+ case "resize": {
33
+ const regions = regionsForLayout(action.layoutMode);
34
+ const region = regions.includes(state.region) ? state.region : regions[0];
35
+ return { ...state, layoutMode: action.layoutMode, region };
36
+ }
37
+ case "tab": {
38
+ const regions = regionsForLayout(state.layoutMode);
39
+ if (regions.length < 2) return state;
40
+ const index = regions.indexOf(state.region);
41
+ const next = regions[(index + 1) % regions.length];
42
+ return { ...state, region: next };
43
+ }
44
+ case "arrow": {
45
+ if (state.helpOpen) return state;
46
+ if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
47
+
48
+ const navigatesNav = state.region === COCKPIT_REGIONS.NAV
49
+ || (state.view === ORCHESTRATOR_VIEWS.HOME
50
+ && state.layoutMode === LAYOUT_MODES.MINIMAL);
51
+
52
+ if (navigatesNav) {
53
+ const delta = action.direction === "up" ? -1 : 1;
54
+ return {
55
+ ...state,
56
+ navIndex: clamp(state.navIndex + delta, 0, COCKPIT_NAV.length - 1)
57
+ };
58
+ }
59
+
60
+ const delta = action.direction === "up" ? -1 : 1;
61
+ const max = Math.max(0, (action.listLength ?? 1) - 1);
62
+ return { ...state, listIndex: clamp(state.listIndex + delta, 0, max) };
63
+ }
64
+ case "enter-nav": {
65
+ const item = COCKPIT_NAV[state.navIndex];
66
+ if (!item) return state;
67
+ if (item.view === ORCHESTRATOR_VIEWS.HOME) {
68
+ return {
69
+ ...state,
70
+ view: ORCHESTRATOR_VIEWS.HOME,
71
+ listIndex: 0,
72
+ region: COCKPIT_REGIONS.CONTENT
73
+ };
74
+ }
75
+ return {
76
+ ...state,
77
+ view: item.view,
78
+ listIndex: 0,
79
+ region: COCKPIT_REGIONS.CONTENT,
80
+ helpOpen: false
81
+ };
82
+ }
83
+ case "set-view":
84
+ return {
85
+ ...state,
86
+ view: action.view,
87
+ listIndex: 0,
88
+ region: action.region ?? COCKPIT_REGIONS.CONTENT
89
+ };
90
+ case "toggle-help":
91
+ if (state.helpOpen) {
92
+ return {
93
+ ...state,
94
+ helpOpen: false,
95
+ view: state.view === ORCHESTRATOR_VIEWS.HELP
96
+ ? ORCHESTRATOR_VIEWS.HOME
97
+ : state.view
98
+ };
99
+ }
100
+ return { ...state, helpOpen: true, view: ORCHESTRATOR_VIEWS.HELP };
101
+ case "escape": {
102
+ if (state.helpOpen) {
103
+ return { ...state, helpOpen: false, view: ORCHESTRATOR_VIEWS.HOME };
104
+ }
105
+ if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
106
+ const regions = regionsForLayout(state.layoutMode);
107
+ return {
108
+ ...state,
109
+ view: ORCHESTRATOR_VIEWS.HOME,
110
+ listIndex: 0,
111
+ region: regions.includes(COCKPIT_REGIONS.NAV)
112
+ ? COCKPIT_REGIONS.NAV
113
+ : COCKPIT_REGIONS.CONTENT
114
+ };
115
+ }
116
+ return { ...state, shouldExit: true };
117
+ }
118
+ case "clear-exit":
119
+ return { ...state, shouldExit: false };
120
+ default:
121
+ return state;
122
+ }
123
+ }
124
+
125
+ export function resolveNavAction(navIndex) {
126
+ return COCKPIT_NAV[navIndex] ?? null;
127
+ }
128
+
129
+ function clamp(value, min, max) {
130
+ return Math.min(max, Math.max(min, value));
131
+ }