@kal-elsam/kairo-runtime 0.2.3 → 0.3.1

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
@@ -33,10 +33,24 @@ kairo
33
33
  ```
34
34
 
35
35
  **First run** (no `~/.harness/state.json`): interactive onboarding → safe diagnosis →
36
- setup with confirmation → operations dashboard.
36
+ setup with confirmation → full-screen operations cockpit.
37
37
 
38
- **Later runs** (state present): operations dashboard with a stable purpose line and a
39
- contextual next step (configure, enable intelligence, launch a run, or review problems).
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 · `Enter` open · `Esc` back (exit only from Overview) ·
49
+ `R` refresh · `C` cancel run · `?` help. `Tab` switches region only when content
50
+ is interactive (run lists / launch).
51
+
52
+ Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
53
+ in text, never color alone.
40
54
 
41
55
  Explicit commands and setup flags keep their current behavior (`kairo setup`,
42
56
  `kairo --dry-run`, `kairo shell`, non-TTY scripts, etc.).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
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
  }
@@ -113,8 +113,11 @@ export async function verifyPublishedRelease({
113
113
  }
114
114
 
115
115
  const remoteTagLine = runGit(`git ls-remote --tags origin refs/tags/${releaseTag}`).trim();
116
+ const remotePeeledLine = runGit(`git ls-remote origin refs/tags/${releaseTag}^{}`).trim();
117
+ const remotePointsAtGitHead = remoteTagLine.startsWith(gitHead)
118
+ || remotePeeledLine.startsWith(gitHead);
116
119
 
117
- if (!remoteTagLine.startsWith(gitHead)) {
120
+ if (!remotePointsAtGitHead) {
118
121
  throw new Error(
119
122
  `Remote tag ${releaseTag} on origin does not point to npm gitHead (${gitHead})`
120
123
  );
@@ -0,0 +1,21 @@
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
+ test/cockpit-navigation.test.js \
19
+ test/cockpit-input-integration.test.js
20
+
21
+ echo "Kairo cockpit smoke passed"
@@ -0,0 +1,103 @@
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 {
10
+ buildFooterModel,
11
+ buildHomeMissionModel,
12
+ buildTopBarModel,
13
+ COCKPIT_NAV,
14
+ COCKPIT_REGIONS
15
+ } from "../src/global/ink/cockpit-models.js";
16
+ import {
17
+ createCockpitUiState,
18
+ reduceCockpitUi,
19
+ routeCockpitKey
20
+ } from "../src/global/ink/cockpit-controller.js";
21
+ import { ORCHESTRATOR_VIEWS } from "../src/global/ink/orchestrator-state.js";
22
+
23
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
24
+ const require = createRequire(import.meta.url);
25
+ const pkg = require(join(root, "package.json"));
26
+
27
+ assert.equal(pkg.version, "0.3.1");
28
+ assert.ok(pkg.dependencies["ansi-escapes"]);
29
+
30
+ assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
31
+ assert.equal(resolveLayoutMode({ columns: 80, rows: 24 }), LAYOUT_MODES.COMPACT);
32
+ assert.equal(resolveLayoutMode({ columns: 65, rows: 24 }), LAYOUT_MODES.MINIMAL);
33
+ assert.equal(resolveLayoutMode({ columns: 50, rows: 24 }), null);
34
+
35
+ const caps = resolveTerminalCapabilities({
36
+ columns: 80,
37
+ rows: 24,
38
+ isTTY: true,
39
+ term: "xterm-256color",
40
+ env: { NO_COLOR: "1" }
41
+ });
42
+ assert.equal(caps.color, false);
43
+ assert.equal(caps.canUseInk, true);
44
+
45
+ const session = createFullscreenSession({
46
+ stdout: { isTTY: true, write: () => true },
47
+ processRef: { on() {}, removeListener() {}, exit() {} },
48
+ onSignal: () => {}
49
+ });
50
+ assert.equal(session.enter(), true);
51
+ assert.equal(session.leave(), true);
52
+ assert.equal(session.leave(), false);
53
+
54
+ const top = buildTopBarModel({ projectName: "smoke" });
55
+ assert.match(top.status, /ONLINE|Offline/);
56
+ const mission = buildHomeMissionModel({
57
+ hasGlobalState: false,
58
+ diagnostics: { diagnostics: { detected: 0 } },
59
+ dashboard: { providers: [], recentRuns: [] }
60
+ });
61
+ assert.match(mission.title, /MISSION CONTROL/);
62
+
63
+ function applyKey(state, keyAction) {
64
+ const routed = routeCockpitKey(state, keyAction);
65
+ if (!routed) return state;
66
+ return reduceCockpitUi(state, routed);
67
+ }
68
+
69
+ function smokeNavigation(layoutMode) {
70
+ let state = createCockpitUiState({
71
+ layoutMode,
72
+ region: layoutMode === LAYOUT_MODES.MINIMAL ? COCKPIT_REGIONS.CONTENT : COCKPIT_REGIONS.NAV
73
+ });
74
+ const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "diagnostics");
75
+ while (state.navIndex < diagnosticsIndex) {
76
+ state = applyKey(state, { type: "arrow", direction: "down" });
77
+ }
78
+ state = applyKey(state, { type: "enter" });
79
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.DIAGNOSTICS);
80
+ assert.equal(state.region, COCKPIT_REGIONS.NAV);
81
+
82
+ state = applyKey(state, { type: "arrow", direction: "up" });
83
+ state = applyKey(state, { type: "enter" });
84
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.LAUNCH);
85
+ assert.equal(state.region, COCKPIT_REGIONS.CONTENT);
86
+
87
+ const footer = buildFooterModel({
88
+ view: ORCHESTRATOR_VIEWS.DIAGNOSTICS,
89
+ region: COCKPIT_REGIONS.NAV,
90
+ unicode: false
91
+ });
92
+ assert.doesNotMatch(footer.text, /Tab/);
93
+
94
+ state = applyKey(state, { type: "escape" });
95
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.HOME);
96
+ state = applyKey(state, { type: "escape" });
97
+ assert.equal(state.shouldExit, true);
98
+ }
99
+
100
+ smokeNavigation(LAYOUT_MODES.WIDE);
101
+ smokeNavigation(LAYOUT_MODES.COMPACT);
102
+
103
+ console.log("cockpit smoke OK");
package/src/cli.js CHANGED
@@ -696,11 +696,11 @@ sections, components, backups, and drift repair under ~/.harness.
696
696
  Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
697
697
 
698
698
  Usage:
699
- ${cli} First run: onboarding → setup → dashboard (TTY).
700
- Later: operations dashboard with next-step guidance.
699
+ ${cli} First run: onboarding → setup → cockpit (TTY).
700
+ Later: full-screen cockpit (wide/compact/minimal).
701
701
  ${cli} --dry-run Setup dry-run (scriptable)
702
702
  ${cli} --version
703
- ${cli} shell Operations dashboard (TTY)
703
+ ${cli} shell Operations cockpit (TTY)
704
704
  ${cli} run --agent <id> --task "..." [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
705
705
  ${cli} runs list [--json] [--limit <n>] [--active-only]
706
706
  ${cli} runs show <runId> [--json] [--limit <n>] [--follow]
@@ -741,8 +741,10 @@ Scopes:
741
741
  Explicit --scope=workspace only.
742
742
 
743
743
  Commands:
744
- shell Operations dashboard (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
745
- is missing, otherwise the dashboard. Explicit ${cli} shell always opens the dashboard.
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: ↑↓ · Enter · Esc back/exit · R refresh · C cancel · ? help.
747
+ Tab switches region only when content is interactive (runs/launch).
746
748
  run Launch a managed agent run with local audit trail.
747
749
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
748
750
  orchestrator Read-only capability registry diagnostics (--json supported).
@@ -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,168 @@
1
+ import {
2
+ COCKPIT_NAV,
3
+ COCKPIT_REGIONS,
4
+ LAYOUT_MODES,
5
+ ORCHESTRATOR_VIEWS,
6
+ canTabBetweenRegions,
7
+ clamp,
8
+ defaultRegionForView,
9
+ interactiveRegionsFor,
10
+ isContentInteractiveView,
11
+ isNavFocusedView,
12
+ regionsForLayout,
13
+ routeCockpitKey
14
+ } from "./cockpit-focus.js";
15
+
16
+ export {
17
+ canTabBetweenRegions,
18
+ defaultRegionForView,
19
+ isContentInteractiveView,
20
+ isNavFocusedView,
21
+ routeCockpitKey
22
+ };
23
+
24
+ /**
25
+ * Pure cockpit focus / navigation reducer — no I/O, no React.
26
+ */
27
+ export function createCockpitUiState({
28
+ layoutMode = LAYOUT_MODES.COMPACT,
29
+ view = ORCHESTRATOR_VIEWS.HOME,
30
+ region = COCKPIT_REGIONS.NAV,
31
+ navIndex = 0,
32
+ listIndex = 0,
33
+ helpOpen = false,
34
+ returnView = null
35
+ } = {}) {
36
+ const regions = regionsForLayout(layoutMode);
37
+ const preferred = region ?? defaultRegionForView(view, layoutMode);
38
+ const safeRegion = regions.includes(preferred)
39
+ ? preferred
40
+ : defaultRegionForView(view, layoutMode);
41
+ return {
42
+ layoutMode,
43
+ view,
44
+ region: safeRegion,
45
+ navIndex: clamp(navIndex, 0, COCKPIT_NAV.length - 1),
46
+ listIndex: Math.max(0, listIndex),
47
+ helpOpen,
48
+ returnView,
49
+ shouldExit: false
50
+ };
51
+ }
52
+
53
+ export function reduceCockpitUi(state, action) {
54
+ switch (action.type) {
55
+ case "resize": {
56
+ const regions = regionsForLayout(action.layoutMode);
57
+ const preferred = regions.includes(state.region)
58
+ ? state.region
59
+ : defaultRegionForView(state.view, action.layoutMode);
60
+ return { ...state, layoutMode: action.layoutMode, region: preferred };
61
+ }
62
+ case "tab": {
63
+ if (!canTabBetweenRegions(state)) return state;
64
+ const regions = interactiveRegionsFor(state);
65
+ const index = regions.indexOf(state.region);
66
+ const next = regions[(Math.max(index, 0) + 1) % regions.length];
67
+ return { ...state, region: next };
68
+ }
69
+ case "arrow": {
70
+ if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
71
+
72
+ const navigatesNav = state.region === COCKPIT_REGIONS.NAV
73
+ || isNavFocusedView(state.view)
74
+ || (state.view === ORCHESTRATOR_VIEWS.HOME
75
+ && state.layoutMode === LAYOUT_MODES.MINIMAL);
76
+
77
+ if (navigatesNav) {
78
+ const delta = action.direction === "up" ? -1 : 1;
79
+ return {
80
+ ...state,
81
+ navIndex: clamp(state.navIndex + delta, 0, COCKPIT_NAV.length - 1)
82
+ };
83
+ }
84
+
85
+ if (!isContentInteractiveView(state.view) || state.view === ORCHESTRATOR_VIEWS.LAUNCH) {
86
+ return state;
87
+ }
88
+
89
+ const delta = action.direction === "up" ? -1 : 1;
90
+ const max = Math.max(0, (action.listLength ?? 1) - 1);
91
+ return { ...state, listIndex: clamp(state.listIndex + delta, 0, max) };
92
+ }
93
+ case "enter-nav": {
94
+ const item = COCKPIT_NAV[state.navIndex];
95
+ if (!item) return state;
96
+ return {
97
+ ...state,
98
+ view: item.view,
99
+ listIndex: 0,
100
+ region: defaultRegionForView(item.view, state.layoutMode),
101
+ helpOpen: false,
102
+ returnView: null
103
+ };
104
+ }
105
+ case "set-view": {
106
+ const nextView = action.view;
107
+ return {
108
+ ...state,
109
+ view: nextView,
110
+ listIndex: 0,
111
+ region: action.region ?? defaultRegionForView(nextView, state.layoutMode),
112
+ returnView: action.returnView ?? state.returnView,
113
+ helpOpen: nextView === ORCHESTRATOR_VIEWS.HELP
114
+ };
115
+ }
116
+ case "toggle-help":
117
+ if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
118
+ return goOverview(state);
119
+ }
120
+ return {
121
+ ...state,
122
+ helpOpen: true,
123
+ view: ORCHESTRATOR_VIEWS.HELP,
124
+ region: defaultRegionForView(ORCHESTRATOR_VIEWS.HELP, state.layoutMode)
125
+ };
126
+ case "escape": {
127
+ if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
128
+ return goOverview(state);
129
+ }
130
+ if (state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
131
+ const back = state.returnView && isContentInteractiveView(state.returnView)
132
+ ? state.returnView
133
+ : ORCHESTRATOR_VIEWS.ACTIVE_RUNS;
134
+ return {
135
+ ...state,
136
+ view: back,
137
+ listIndex: 0,
138
+ region: defaultRegionForView(back, state.layoutMode),
139
+ returnView: null
140
+ };
141
+ }
142
+ if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
143
+ return goOverview(state);
144
+ }
145
+ return { ...state, shouldExit: true };
146
+ }
147
+ case "clear-exit":
148
+ return { ...state, shouldExit: false };
149
+ default:
150
+ return state;
151
+ }
152
+ }
153
+
154
+ export function resolveNavAction(navIndex) {
155
+ return COCKPIT_NAV[navIndex] ?? null;
156
+ }
157
+
158
+ function goOverview(state) {
159
+ return {
160
+ ...state,
161
+ helpOpen: false,
162
+ view: ORCHESTRATOR_VIEWS.HOME,
163
+ navIndex: 0,
164
+ listIndex: 0,
165
+ region: defaultRegionForView(ORCHESTRATOR_VIEWS.HOME, state.layoutMode),
166
+ returnView: null
167
+ };
168
+ }
@@ -0,0 +1,92 @@
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
+ const NAV_FOCUSED_VIEWS = new Set([
6
+ ORCHESTRATOR_VIEWS.HOME,
7
+ ORCHESTRATOR_VIEWS.PROVIDERS,
8
+ ORCHESTRATOR_VIEWS.DIAGNOSTICS,
9
+ ORCHESTRATOR_VIEWS.HELP
10
+ ]);
11
+
12
+ const CONTENT_INTERACTIVE_VIEWS = new Set([
13
+ ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
14
+ ORCHESTRATOR_VIEWS.RECENT_RUNS,
15
+ ORCHESTRATOR_VIEWS.RUN_DETAIL,
16
+ ORCHESTRATOR_VIEWS.LAUNCH
17
+ ]);
18
+
19
+ export function isNavFocusedView(view) {
20
+ return NAV_FOCUSED_VIEWS.has(view);
21
+ }
22
+
23
+ export function isContentInteractiveView(view) {
24
+ return CONTENT_INTERACTIVE_VIEWS.has(view);
25
+ }
26
+
27
+ export function defaultRegionForView(view, layoutMode = LAYOUT_MODES.COMPACT) {
28
+ const regions = regionsForLayout(layoutMode);
29
+ if (isContentInteractiveView(view) && regions.includes(COCKPIT_REGIONS.CONTENT)) {
30
+ return COCKPIT_REGIONS.CONTENT;
31
+ }
32
+ if (regions.includes(COCKPIT_REGIONS.NAV)) return COCKPIT_REGIONS.NAV;
33
+ return regions[0] ?? COCKPIT_REGIONS.CONTENT;
34
+ }
35
+
36
+ export function interactiveRegionsFor(state) {
37
+ const regions = regionsForLayout(state.layoutMode);
38
+ if (!isContentInteractiveView(state.view) || state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
39
+ return regions.filter((region) => region === COCKPIT_REGIONS.NAV);
40
+ }
41
+ return regions.filter(
42
+ (region) => region === COCKPIT_REGIONS.NAV || region === COCKPIT_REGIONS.CONTENT
43
+ );
44
+ }
45
+
46
+ export function canTabBetweenRegions(state) {
47
+ if (!isContentInteractiveView(state.view)) return false;
48
+ if (state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) return false;
49
+ return interactiveRegionsFor(state).length >= 2;
50
+ }
51
+
52
+ /**
53
+ * Map raw key intent to a reducer action (or null when inactive / view-owned).
54
+ */
55
+ export function routeCockpitKey(state, keyAction) {
56
+ switch (keyAction.type) {
57
+ case "escape":
58
+ return { type: "escape" };
59
+ case "tab":
60
+ return canTabBetweenRegions(state) ? { type: "tab" } : null;
61
+ case "arrow": {
62
+ if (state.region === COCKPIT_REGIONS.SYSTEM) return null;
63
+ if (state.region === COCKPIT_REGIONS.NAV || isNavFocusedView(state.view)) {
64
+ return { type: "arrow", direction: keyAction.direction };
65
+ }
66
+ if (state.region === COCKPIT_REGIONS.CONTENT && isContentInteractiveView(state.view)) {
67
+ if (state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL || state.view === ORCHESTRATOR_VIEWS.LAUNCH) {
68
+ return null;
69
+ }
70
+ return {
71
+ type: "arrow",
72
+ direction: keyAction.direction,
73
+ listLength: keyAction.listLength
74
+ };
75
+ }
76
+ return null;
77
+ }
78
+ case "enter":
79
+ if (state.region === COCKPIT_REGIONS.NAV || isNavFocusedView(state.view)) {
80
+ return { type: "enter-nav" };
81
+ }
82
+ return null;
83
+ default:
84
+ return null;
85
+ }
86
+ }
87
+
88
+ export function clamp(value, min, max) {
89
+ return Math.min(max, Math.max(min, value));
90
+ }
91
+
92
+ export { COCKPIT_NAV, COCKPIT_REGIONS, LAYOUT_MODES, ORCHESTRATOR_VIEWS, regionsForLayout };