@kal-elsam/kairo-runtime 0.3.0 → 0.4.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
@@ -35,18 +35,22 @@ kairo
35
35
  **First run** (no `~/.harness/state.json`): interactive onboarding → safe diagnosis →
36
36
  setup with confirmation → full-screen operations cockpit.
37
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:
38
+ **Later runs** (state present): full-screen cockpit Home that explains what Kairo
39
+ does, shows real readiness, and recommends a useful next action. Layout adapts to
40
+ terminal size:
40
41
 
41
42
  | Mode | Size | Layout |
42
43
  |------|------|--------|
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 |
44
+ | Wide | ≥100 cols × ≥28 rows | Nav + Home/content + system |
45
+ | Compact | ≥72×20 | Nav + Home/content (readiness embedded) |
46
+ | Minimal | 60–71 cols or short height | Selected section + essential readiness/next/recent |
46
47
  | Below gate | <60 cols | Explicit TTY fallback (Ink disabled) |
47
48
 
48
- Keys: `↑↓` navigate · `Tab` region · `Enter` open · `R` refresh · `C` cancel run ·
49
- `?` help · `Esc` back (exit only from Home).
49
+ Keys: `↑↓` navigate (selection explanation updates) · `Enter` open ·
50
+ `Esc` back (exit only from Home) · `R` refresh/retry · `C` cancel run · `?` help.
51
+ `Tab` switches region only when content is interactive (run lists / New run).
52
+
53
+ Navigation labels: Home · Running now · History · Agents · New run · System health.
50
54
 
51
55
  Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
52
56
  in text, never color alone.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
@@ -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
  );
@@ -14,6 +14,8 @@ node --test \
14
14
  test/terminal-capabilities.test.js \
15
15
  test/cockpit-models.test.js \
16
16
  test/cockpit-controller.test.js \
17
- test/cockpit-frame.test.js
17
+ test/cockpit-frame.test.js \
18
+ test/cockpit-navigation.test.js \
19
+ test/cockpit-input-integration.test.js
18
20
 
19
21
  echo "Kairo cockpit smoke passed"
@@ -6,13 +6,26 @@ import { fileURLToPath } from "node:url";
6
6
  import { resolveLayoutMode, LAYOUT_MODES } from "../src/global/ink/layout.js";
7
7
  import { resolveTerminalCapabilities } from "../src/global/ink/terminal-capabilities.js";
8
8
  import { createFullscreenSession } from "../src/global/ink/fullscreen-session.js";
9
- import { buildTopBarModel, buildHomeMissionModel } from "../src/global/ink/cockpit-models.js";
9
+ import {
10
+ buildFooterModel,
11
+ buildHomeMissionModel,
12
+ buildNavModel,
13
+ buildTopBarModel,
14
+ COCKPIT_NAV,
15
+ COCKPIT_REGIONS
16
+ } from "../src/global/ink/cockpit-models.js";
17
+ import {
18
+ createCockpitUiState,
19
+ reduceCockpitUi,
20
+ routeCockpitKey
21
+ } from "../src/global/ink/cockpit-controller.js";
22
+ import { ORCHESTRATOR_VIEWS } from "../src/global/ink/orchestrator-state.js";
10
23
 
11
24
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
12
25
  const require = createRequire(import.meta.url);
13
26
  const pkg = require(join(root, "package.json"));
14
27
 
15
- assert.equal(pkg.version, "0.3.0");
28
+ assert.equal(pkg.version, "0.4.0");
16
29
  assert.ok(pkg.dependencies["ansi-escapes"]);
17
30
 
18
31
  assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
@@ -39,13 +52,78 @@ assert.equal(session.enter(), true);
39
52
  assert.equal(session.leave(), true);
40
53
  assert.equal(session.leave(), false);
41
54
 
42
- const top = buildTopBarModel({ projectName: "smoke" });
55
+ const top = buildTopBarModel({ projectName: "smoke", unicode: false });
43
56
  assert.match(top.status, /ONLINE|Offline/);
44
57
  const mission = buildHomeMissionModel({
45
- hasGlobalState: false,
46
- diagnostics: { diagnostics: { detected: 0 } },
47
- dashboard: { providers: [], recentRuns: [] }
58
+ projectName: "smoke",
59
+ hasGlobalState: true,
60
+ diagnostics: {
61
+ diagnostics: { detected: 2, errors: 0 },
62
+ intelligence: { summary: { localAvailable: false, cloudAuthenticated: false } },
63
+ recommendations: []
64
+ },
65
+ dashboard: {
66
+ providers: [{ launchable: true }],
67
+ recentRuns: [{ runId: "r1", agentId: "codex", state: "failed" }]
68
+ },
69
+ layoutMode: LAYOUT_MODES.MINIMAL
48
70
  });
49
- assert.match(mission.title, /MISSION CONTROL/);
71
+ assert.match(mission.title, /HOME — smoke/);
72
+ assert.match(mission.readiness.headline, /LIMITED|READY|NEEDS/i);
73
+ assert.equal(mission.next.targetAction, "launch");
74
+ assert.match(mission.recent.headline, /Codex · Failed/);
75
+
76
+ const asciiNav = buildNavModel({
77
+ navIndex: 1,
78
+ currentView: ORCHESTRATOR_VIEWS.HOME,
79
+ focused: true,
80
+ unicode: false,
81
+ dashboard: { activeRuns: [], recentRuns: [], providers: [{ launchable: true }] },
82
+ diagnostics: { diagnostics: { detected: 1, errors: 0 } }
83
+ });
84
+ assert.equal(asciiNav.items[1].marker, ">");
85
+ assert.equal(asciiNav.items[0].current, true);
86
+ assert.equal(asciiNav.items[1].selected, true);
87
+ assert.match(asciiNav.explanation, /Supervised runs|Running/i);
88
+
89
+ function applyKey(state, keyAction) {
90
+ const routed = routeCockpitKey(state, keyAction);
91
+ if (!routed) return state;
92
+ return reduceCockpitUi(state, routed);
93
+ }
94
+
95
+ function smokeNavigation(layoutMode) {
96
+ let state = createCockpitUiState({
97
+ layoutMode,
98
+ region: layoutMode === LAYOUT_MODES.MINIMAL ? COCKPIT_REGIONS.CONTENT : COCKPIT_REGIONS.NAV
99
+ });
100
+ const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "diagnostics");
101
+ while (state.navIndex < diagnosticsIndex) {
102
+ state = applyKey(state, { type: "arrow", direction: "down" });
103
+ }
104
+ state = applyKey(state, { type: "enter" });
105
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.DIAGNOSTICS);
106
+ assert.equal(state.region, COCKPIT_REGIONS.NAV);
107
+
108
+ state = applyKey(state, { type: "arrow", direction: "up" });
109
+ state = applyKey(state, { type: "enter" });
110
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.LAUNCH);
111
+ assert.equal(state.region, COCKPIT_REGIONS.CONTENT);
112
+
113
+ const footer = buildFooterModel({
114
+ view: ORCHESTRATOR_VIEWS.DIAGNOSTICS,
115
+ region: COCKPIT_REGIONS.NAV,
116
+ unicode: false
117
+ });
118
+ assert.doesNotMatch(footer.text, /Tab/);
119
+
120
+ state = applyKey(state, { type: "escape" });
121
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.HOME);
122
+ state = applyKey(state, { type: "escape" });
123
+ assert.equal(state.shouldExit, true);
124
+ }
125
+
126
+ smokeNavigation(LAYOUT_MODES.WIDE);
127
+ smokeNavigation(LAYOUT_MODES.COMPACT);
50
128
 
51
129
  console.log("cockpit smoke OK");
package/src/cli.js CHANGED
@@ -743,7 +743,8 @@ Scopes:
743
743
  Commands:
744
744
  shell Operations cockpit (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
745
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.
746
+ Keys: ↑↓ · Enter · Esc back/exit · R refresh · C cancel · ? help.
747
+ Tab switches region only when content is interactive (runs/launch).
747
748
  run Launch a managed agent run with local audit trail.
748
749
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
749
750
  orchestrator Read-only capability registry diagnostics (--json supported).
@@ -1,7 +1,15 @@
1
1
  import { formatCliCommand } from "./brand/cli.js";
2
2
 
3
+ /** Public cockpit destinations for recommended actions (match ORCHESTRATOR_VIEWS). */
4
+ export const RECOMMENDATION_TARGETS = {
5
+ DIAGNOSTICS: "diagnostics",
6
+ LAUNCH: "launch",
7
+ RECENT_RUNS: "recent-runs",
8
+ PROVIDERS: "providers"
9
+ };
10
+
3
11
  export const DASHBOARD_PURPOSE =
4
- "Detects, configures, and coordinates local AI agents no changes without confirmation.";
12
+ "Kairo coordinates installed AI agents for this project, with controlled execution and auditable results.";
5
13
 
6
14
  export const NEXT_STEP_KINDS = {
7
15
  CONFIGURE: "configure",
@@ -10,13 +18,117 @@ export const NEXT_STEP_KINDS = {
10
18
  REVIEW: "review"
11
19
  };
12
20
 
21
+ export const READINESS_KINDS = {
22
+ NEEDS_SETUP: "needs_setup",
23
+ NEEDS_ATTENTION: "needs_attention",
24
+ LIMITED: "limited",
25
+ READY: "ready"
26
+ };
27
+
28
+ export const READINESS_LABELS = {
29
+ [READINESS_KINDS.NEEDS_SETUP]: "Needs setup",
30
+ [READINESS_KINDS.NEEDS_ATTENTION]: "Needs attention",
31
+ [READINESS_KINDS.LIMITED]: "Limited",
32
+ [READINESS_KINDS.READY]: "Ready to work"
33
+ };
34
+
13
35
  export function formatDashboardPurpose() {
14
36
  return DASHBOARD_PURPOSE;
15
37
  }
16
38
 
39
+ export function countLaunchableAgents(dashboard = null) {
40
+ return (dashboard?.providers ?? []).filter((entry) => entry.launchable).length;
41
+ }
42
+
43
+ export function hasIntelligenceConfigured(diagnostics = null) {
44
+ const summary = diagnostics?.intelligence?.summary;
45
+ return Boolean(summary?.localAvailable || summary?.cloudAuthenticated);
46
+ }
47
+
48
+ function hasBlockingDiagnostics(diagnostics = null) {
49
+ const summary = diagnostics?.diagnostics ?? { errors: 0 };
50
+ const hasErrors = (summary.errors ?? 0) > 0;
51
+ const hasProblemRecommendation = (diagnostics?.recommendations ?? []).some((line) =>
52
+ /error|fix|drift|not detected|failed|problem/i.test(line)
53
+ );
54
+ return hasErrors || hasProblemRecommendation;
55
+ }
56
+
57
+ /**
58
+ * Derive project readiness from diagnostics + dashboard.
59
+ * Intelligence absence is Limited (optional), never Needs attention by itself.
60
+ */
61
+ export function resolveProjectReadiness({
62
+ hasGlobalState = false,
63
+ diagnostics = null,
64
+ dashboard = null
65
+ } = {}) {
66
+ const summary = diagnostics?.diagnostics ?? { detected: 0, errors: 0 };
67
+ const launchableCount = countLaunchableAgents(dashboard);
68
+ const detected = summary.detected ?? 0;
69
+ const agentsReady = launchableCount;
70
+ const activeRuns = dashboard?.activeRuns?.length ?? 0;
71
+ const intelConfigured = hasIntelligenceConfigured(diagnostics);
72
+ const summaryLine = `${agentsReady} agents ready · ${detected} detected · ${activeRuns} active runs`;
73
+
74
+ if (!hasGlobalState || detected === 0) {
75
+ return {
76
+ kind: READINESS_KINDS.NEEDS_SETUP,
77
+ label: READINESS_LABELS[READINESS_KINDS.NEEDS_SETUP],
78
+ headline: "NEEDS SETUP",
79
+ summaryLine,
80
+ capabilityLines: [
81
+ "Intelligence: Optional capability not configured"
82
+ ],
83
+ healthKind: "warn"
84
+ };
85
+ }
86
+
87
+ if (hasBlockingDiagnostics(diagnostics) || launchableCount === 0) {
88
+ return {
89
+ kind: READINESS_KINDS.NEEDS_ATTENTION,
90
+ label: READINESS_LABELS[READINESS_KINDS.NEEDS_ATTENTION],
91
+ headline: "NEEDS ATTENTION",
92
+ summaryLine,
93
+ capabilityLines: [
94
+ intelConfigured
95
+ ? "Intelligence: Configured"
96
+ : "Intelligence: Optional capability not configured"
97
+ ],
98
+ healthKind: "error"
99
+ };
100
+ }
101
+
102
+ if (!intelConfigured) {
103
+ return {
104
+ kind: READINESS_KINDS.LIMITED,
105
+ label: READINESS_LABELS[READINESS_KINDS.LIMITED],
106
+ headline: "LIMITED",
107
+ summaryLine,
108
+ capabilityLines: [
109
+ "Intelligence: Optional capability not configured"
110
+ ],
111
+ healthKind: "warn"
112
+ };
113
+ }
114
+
115
+ return {
116
+ kind: READINESS_KINDS.READY,
117
+ label: READINESS_LABELS[READINESS_KINDS.READY],
118
+ headline: "READY TO WORK",
119
+ summaryLine,
120
+ capabilityLines: [
121
+ "Intelligence: Configured"
122
+ ],
123
+ healthKind: "ready"
124
+ };
125
+ }
126
+
17
127
  /**
18
128
  * Contextual next step from existing diagnostics + dashboard snapshot.
19
- * Priority: configure → review problemsenable intelligence launch.
129
+ * Priority: configure → review when blocked launch when launchable
130
+ * enable intelligence → review.
131
+ * Launchable agents win over missing optional intelligence.
20
132
  */
21
133
  export function resolveDashboardRecommendation({
22
134
  hasGlobalState = false,
@@ -25,42 +137,59 @@ export function resolveDashboardRecommendation({
25
137
  } = {}) {
26
138
  const summary = diagnostics?.diagnostics ?? { detected: 0, errors: 0 };
27
139
  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
- );
140
+ const launchableCount = countLaunchableAgents(dashboard);
141
+ const blocked = hasBlockingDiagnostics(diagnostics);
33
142
 
34
143
  if (!hasGlobalState || (summary.detected ?? 0) === 0) {
35
144
  return {
36
145
  kind: NEXT_STEP_KINDS.CONFIGURE,
37
- message: `Configure the local environment with ${formatCliCommand("setup")}.`
146
+ title: "Finish local setup",
147
+ message: `Configure the local environment with ${formatCliCommand("setup")}.`,
148
+ detail: "Open System health to review what Kairo detected.",
149
+ targetView: RECOMMENDATION_TARGETS.DIAGNOSTICS,
150
+ targetAction: null
38
151
  };
39
152
  }
40
153
 
41
- if (hasErrors || hasProblemRecommendation) {
154
+ if (blocked && launchableCount === 0) {
42
155
  return {
43
156
  kind: NEXT_STEP_KINDS.REVIEW,
44
- message: "Review diagnostics for problems before launching a run."
157
+ title: "Review system health",
158
+ message: "Review System health for problems before launching a run.",
159
+ detail: "Fix agent or configuration issues, then try again.",
160
+ targetView: RECOMMENDATION_TARGETS.DIAGNOSTICS,
161
+ targetAction: null
45
162
  };
46
163
  }
47
164
 
48
- if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
165
+ if (launchableCount > 0) {
49
166
  return {
50
- kind: NEXT_STEP_KINDS.ENABLE_INTELLIGENCE,
51
- message: "Enable intelligence: start Ollama or set OPENROUTER_API_KEY, then retry."
167
+ kind: NEXT_STEP_KINDS.LAUNCH,
168
+ title: "Create a new run",
169
+ message: "Create a new run",
170
+ detail: "Delegate a task to Cursor, Codex or Claude.",
171
+ targetView: RECOMMENDATION_TARGETS.LAUNCH,
172
+ targetAction: "launch"
52
173
  };
53
174
  }
54
175
 
55
- if (launchableCount > 0) {
176
+ if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
56
177
  return {
57
- kind: NEXT_STEP_KINDS.LAUNCH,
58
- message: "Launch a supervised run from the menu or with kairo run."
178
+ kind: NEXT_STEP_KINDS.ENABLE_INTELLIGENCE,
179
+ title: "Enable optional intelligence",
180
+ message: "Enable intelligence: start Ollama or set OPENROUTER_API_KEY, then retry.",
181
+ detail: "Agents can still run without this optional capability.",
182
+ targetView: RECOMMENDATION_TARGETS.DIAGNOSTICS,
183
+ targetAction: null
59
184
  };
60
185
  }
61
186
 
62
187
  return {
63
188
  kind: NEXT_STEP_KINDS.REVIEW,
64
- message: "Review diagnostics for problems before launching a run."
189
+ title: "Review system health",
190
+ message: "Review System health for problems before launching a run.",
191
+ detail: "Open System health to inspect agents and configuration.",
192
+ targetView: RECOMMENDATION_TARGETS.DIAGNOSTICS,
193
+ targetAction: null
65
194
  };
66
195
  }
@@ -47,17 +47,24 @@ export function CockpitTopBar({ model, colorEnabled = true }) {
47
47
 
48
48
  export function CockpitNav({ model, colorEnabled = true }) {
49
49
  return React.createElement(Box, { flexDirection: "column" },
50
- model.items.map((item) =>
51
- React.createElement(Text, {
50
+ model.items.map((item) => {
51
+ const suffix = item.current && !item.selected ? " (open)" : "";
52
+ const color = item.focused
53
+ ? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
54
+ : item.selected
55
+ ? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
56
+ : item.current
57
+ ? (colorEnabled ? COCKPIT_COLORS.muted : undefined)
58
+ : undefined;
59
+ return React.createElement(Text, {
52
60
  key: item.id,
53
61
  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
- )
62
+ color
63
+ }, `${item.marker} ${item.label}${suffix}`);
64
+ }),
65
+ model.explanation && React.createElement(Text, {
66
+ color: COCKPIT_COLORS.muted
67
+ }, model.explanation)
61
68
  );
62
69
  }
63
70
 
@@ -1,21 +1,43 @@
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";
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
+ };
4
23
 
5
24
  /**
6
25
  * Pure cockpit focus / navigation reducer — no I/O, no React.
7
26
  */
8
-
9
27
  export function createCockpitUiState({
10
28
  layoutMode = LAYOUT_MODES.COMPACT,
11
29
  view = ORCHESTRATOR_VIEWS.HOME,
12
30
  region = COCKPIT_REGIONS.NAV,
13
31
  navIndex = 0,
14
32
  listIndex = 0,
15
- helpOpen = false
33
+ helpOpen = false,
34
+ returnView = null
16
35
  } = {}) {
17
36
  const regions = regionsForLayout(layoutMode);
18
- const safeRegion = regions.includes(region) ? region : regions[0];
37
+ const preferred = region ?? defaultRegionForView(view, layoutMode);
38
+ const safeRegion = regions.includes(preferred)
39
+ ? preferred
40
+ : defaultRegionForView(view, layoutMode);
19
41
  return {
20
42
  layoutMode,
21
43
  view,
@@ -23,6 +45,7 @@ export function createCockpitUiState({
23
45
  navIndex: clamp(navIndex, 0, COCKPIT_NAV.length - 1),
24
46
  listIndex: Math.max(0, listIndex),
25
47
  helpOpen,
48
+ returnView,
26
49
  shouldExit: false
27
50
  };
28
51
  }
@@ -31,21 +54,23 @@ export function reduceCockpitUi(state, action) {
31
54
  switch (action.type) {
32
55
  case "resize": {
33
56
  const regions = regionsForLayout(action.layoutMode);
34
- const region = regions.includes(state.region) ? state.region : regions[0];
35
- return { ...state, layoutMode: action.layoutMode, region };
57
+ const preferred = regions.includes(state.region)
58
+ ? state.region
59
+ : defaultRegionForView(state.view, action.layoutMode);
60
+ return { ...state, layoutMode: action.layoutMode, region: preferred };
36
61
  }
37
62
  case "tab": {
38
- const regions = regionsForLayout(state.layoutMode);
39
- if (regions.length < 2) return state;
63
+ if (!canTabBetweenRegions(state)) return state;
64
+ const regions = interactiveRegionsFor(state);
40
65
  const index = regions.indexOf(state.region);
41
- const next = regions[(index + 1) % regions.length];
66
+ const next = regions[(Math.max(index, 0) + 1) % regions.length];
42
67
  return { ...state, region: next };
43
68
  }
44
69
  case "arrow": {
45
- if (state.helpOpen) return state;
46
70
  if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
47
71
 
48
72
  const navigatesNav = state.region === COCKPIT_REGIONS.NAV
73
+ || isNavFocusedView(state.view)
49
74
  || (state.view === ORCHESTRATOR_VIEWS.HOME
50
75
  && state.layoutMode === LAYOUT_MODES.MINIMAL);
51
76
 
@@ -57,6 +82,10 @@ export function reduceCockpitUi(state, action) {
57
82
  };
58
83
  }
59
84
 
85
+ if (!isContentInteractiveView(state.view) || state.view === ORCHESTRATOR_VIEWS.LAUNCH) {
86
+ return state;
87
+ }
88
+
60
89
  const delta = action.direction === "up" ? -1 : 1;
61
90
  const max = Math.max(0, (action.listLength ?? 1) - 1);
62
91
  return { ...state, listIndex: clamp(state.listIndex + delta, 0, max) };
@@ -64,55 +93,58 @@ export function reduceCockpitUi(state, action) {
64
93
  case "enter-nav": {
65
94
  const item = COCKPIT_NAV[state.navIndex];
66
95
  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
96
  return {
76
97
  ...state,
77
98
  view: item.view,
78
99
  listIndex: 0,
79
- region: COCKPIT_REGIONS.CONTENT,
80
- helpOpen: false
100
+ region: defaultRegionForView(item.view, state.layoutMode),
101
+ helpOpen: false,
102
+ returnView: null
81
103
  };
82
104
  }
83
- case "set-view":
105
+ case "set-view": {
106
+ const nextView = action.view;
84
107
  return {
85
108
  ...state,
86
- view: action.view,
109
+ view: nextView,
87
110
  listIndex: 0,
88
- region: action.region ?? COCKPIT_REGIONS.CONTENT
111
+ navIndex: action.navIndex == null
112
+ ? state.navIndex
113
+ : clamp(action.navIndex, 0, COCKPIT_NAV.length - 1),
114
+ region: action.region ?? defaultRegionForView(nextView, state.layoutMode),
115
+ returnView: action.returnView ?? state.returnView,
116
+ helpOpen: nextView === ORCHESTRATOR_VIEWS.HELP
89
117
  };
118
+ }
90
119
  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
- };
120
+ if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
121
+ return goOverview(state);
99
122
  }
100
- return { ...state, helpOpen: true, view: ORCHESTRATOR_VIEWS.HELP };
123
+ return {
124
+ ...state,
125
+ helpOpen: true,
126
+ view: ORCHESTRATOR_VIEWS.HELP,
127
+ region: defaultRegionForView(ORCHESTRATOR_VIEWS.HELP, state.layoutMode)
128
+ };
101
129
  case "escape": {
102
- if (state.helpOpen) {
103
- return { ...state, helpOpen: false, view: ORCHESTRATOR_VIEWS.HOME };
130
+ if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
131
+ return goOverview(state);
104
132
  }
105
- if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
106
- const regions = regionsForLayout(state.layoutMode);
133
+ if (state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
134
+ const back = state.returnView && isContentInteractiveView(state.returnView)
135
+ ? state.returnView
136
+ : ORCHESTRATOR_VIEWS.ACTIVE_RUNS;
107
137
  return {
108
138
  ...state,
109
- view: ORCHESTRATOR_VIEWS.HOME,
139
+ view: back,
110
140
  listIndex: 0,
111
- region: regions.includes(COCKPIT_REGIONS.NAV)
112
- ? COCKPIT_REGIONS.NAV
113
- : COCKPIT_REGIONS.CONTENT
141
+ region: defaultRegionForView(back, state.layoutMode),
142
+ returnView: null
114
143
  };
115
144
  }
145
+ if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
146
+ return goOverview(state);
147
+ }
116
148
  return { ...state, shouldExit: true };
117
149
  }
118
150
  case "clear-exit":
@@ -126,6 +158,14 @@ export function resolveNavAction(navIndex) {
126
158
  return COCKPIT_NAV[navIndex] ?? null;
127
159
  }
128
160
 
129
- function clamp(value, min, max) {
130
- return Math.min(max, Math.max(min, value));
161
+ function goOverview(state) {
162
+ return {
163
+ ...state,
164
+ helpOpen: false,
165
+ view: ORCHESTRATOR_VIEWS.HOME,
166
+ navIndex: 0,
167
+ listIndex: 0,
168
+ region: defaultRegionForView(ORCHESTRATOR_VIEWS.HOME, state.layoutMode),
169
+ returnView: null
170
+ };
131
171
  }