@kal-elsam/kairo-runtime 0.3.0 → 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
@@ -45,8 +45,9 @@ action), navigation, and a system strip. Layout adapts to terminal size:
45
45
  | Minimal | 60–71 cols or short height | Single panel + nav header |
46
46
  | Below gate | <60 cols | Explicit TTY fallback (Ink disabled) |
47
47
 
48
- Keys: `↑↓` navigate · `Tab` region · `Enter` open · `R` refresh · `C` cancel run ·
49
- `?` help · `Esc` back (exit only from Home).
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).
50
51
 
51
52
  Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
52
53
  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.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",
@@ -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,25 @@ 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
+ 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";
10
22
 
11
23
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
12
24
  const require = createRequire(import.meta.url);
13
25
  const pkg = require(join(root, "package.json"));
14
26
 
15
- assert.equal(pkg.version, "0.3.0");
27
+ assert.equal(pkg.version, "0.3.1");
16
28
  assert.ok(pkg.dependencies["ansi-escapes"]);
17
29
 
18
30
  assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
@@ -48,4 +60,44 @@ const mission = buildHomeMissionModel({
48
60
  });
49
61
  assert.match(mission.title, /MISSION CONTROL/);
50
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
+
51
103
  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,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,55 @@ 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
+ region: action.region ?? defaultRegionForView(nextView, state.layoutMode),
112
+ returnView: action.returnView ?? state.returnView,
113
+ helpOpen: nextView === ORCHESTRATOR_VIEWS.HELP
89
114
  };
115
+ }
90
116
  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
- };
117
+ if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
118
+ return goOverview(state);
99
119
  }
100
- return { ...state, helpOpen: true, view: ORCHESTRATOR_VIEWS.HELP };
120
+ return {
121
+ ...state,
122
+ helpOpen: true,
123
+ view: ORCHESTRATOR_VIEWS.HELP,
124
+ region: defaultRegionForView(ORCHESTRATOR_VIEWS.HELP, state.layoutMode)
125
+ };
101
126
  case "escape": {
102
- if (state.helpOpen) {
103
- return { ...state, helpOpen: false, view: ORCHESTRATOR_VIEWS.HOME };
127
+ if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
128
+ return goOverview(state);
104
129
  }
105
- if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
106
- const regions = regionsForLayout(state.layoutMode);
130
+ if (state.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
131
+ const back = state.returnView && isContentInteractiveView(state.returnView)
132
+ ? state.returnView
133
+ : ORCHESTRATOR_VIEWS.ACTIVE_RUNS;
107
134
  return {
108
135
  ...state,
109
- view: ORCHESTRATOR_VIEWS.HOME,
136
+ view: back,
110
137
  listIndex: 0,
111
- region: regions.includes(COCKPIT_REGIONS.NAV)
112
- ? COCKPIT_REGIONS.NAV
113
- : COCKPIT_REGIONS.CONTENT
138
+ region: defaultRegionForView(back, state.layoutMode),
139
+ returnView: null
114
140
  };
115
141
  }
142
+ if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
143
+ return goOverview(state);
144
+ }
116
145
  return { ...state, shouldExit: true };
117
146
  }
118
147
  case "clear-exit":
@@ -126,6 +155,14 @@ export function resolveNavAction(navIndex) {
126
155
  return COCKPIT_NAV[navIndex] ?? null;
127
156
  }
128
157
 
129
- function clamp(value, min, max) {
130
- return Math.min(max, Math.max(min, value));
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
+ };
131
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 };
@@ -130,20 +130,47 @@ export function buildFooterModel({
130
130
  unicode = true
131
131
  } = {}) {
132
132
  const glyphs = resolveGlyphs(unicode);
133
- if (helpOpen) {
134
- return {
135
- text: `Esc close help ${glyphs.bullet} R refresh ${glyphs.bullet} C cancel run`
136
- };
133
+ const parts = [];
134
+
135
+ if (helpOpen || view === ORCHESTRATOR_VIEWS.HELP) {
136
+ parts.push("Esc close help");
137
+ parts.push("? Help");
138
+ return { text: parts.join(` ${glyphs.bullet} `) };
137
139
  }
138
- if (view === ORCHESTRATOR_VIEWS.HOME) {
139
- return {
140
- text: `↑↓ Navigate ${glyphs.bullet} Tab Region ${glyphs.bullet} Enter Open ${glyphs.bullet} ? Help ${glyphs.bullet} Esc Exit`
141
- };
140
+
141
+ if (view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
142
+ parts.push("R refresh");
143
+ if (canCancel) parts.push("C cancel");
144
+ parts.push("Esc Back");
145
+ return { text: parts.join(` ${glyphs.bullet} `) };
142
146
  }
143
- const cancel = canCancel ? ` ${glyphs.bullet} C cancel` : "";
144
- return {
145
- text: `↑↓ Navigate ${glyphs.bullet} Enter Open ${glyphs.bullet} R refresh${cancel} ${glyphs.bullet} Esc Back · focus:${region}`
146
- };
147
+
148
+ parts.push("↑↓ Navigate");
149
+
150
+ const showTab = view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
151
+ || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
152
+ || view === ORCHESTRATOR_VIEWS.LAUNCH;
153
+ if (showTab) {
154
+ parts.push("Tab Region");
155
+ }
156
+
157
+ if (view === ORCHESTRATOR_VIEWS.HOME
158
+ || view === ORCHESTRATOR_VIEWS.PROVIDERS
159
+ || view === ORCHESTRATOR_VIEWS.DIAGNOSTICS
160
+ || region === COCKPIT_REGIONS.NAV
161
+ || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
162
+ || view === ORCHESTRATOR_VIEWS.RECENT_RUNS) {
163
+ parts.push("Enter Open");
164
+ }
165
+
166
+ if (view !== ORCHESTRATOR_VIEWS.LAUNCH) {
167
+ parts.push("R refresh");
168
+ }
169
+
170
+ parts.push("? Help");
171
+ parts.push(view === ORCHESTRATOR_VIEWS.HOME ? "Esc Exit" : "Esc Back");
172
+
173
+ return { text: parts.join(` ${glyphs.bullet} `) };
147
174
  }
148
175
 
149
176
  export function resolveProjectName(workspaceRoot = "") {
@@ -5,7 +5,8 @@ import {
5
5
  } from "./orchestrator-state.js";
6
6
 
7
7
  /**
8
- * Launch-wizard key handler. Returns true when the key was consumed.
8
+ * Launch-wizard key handler.
9
+ * Returns true when consumed, "retreated" when Esc stepped back, false otherwise.
9
10
  */
10
11
  export function handleLaunchInput(ctx) {
11
12
  const {
@@ -22,9 +23,18 @@ export function handleLaunchInput(ctx) {
22
23
  setLaunchPermissionIndex,
23
24
  setError,
24
25
  handleLaunch,
25
- reload
26
+ reload,
27
+ allowEscapeRetreat = false
26
28
  } = ctx;
27
29
 
30
+ if (allowEscapeRetreat && key.escape) {
31
+ if (launchStep === LAUNCH_WIZARD_STEPS.AGENT) {
32
+ return false;
33
+ }
34
+ setLaunchStep(retreatLaunchWizardStep(launchStep));
35
+ return "retreated";
36
+ }
37
+
28
38
  if (launchStep === LAUNCH_WIZARD_STEPS.AGENT) {
29
39
  if (key.upArrow) {
30
40
  setLaunchAgentIndex((index) => Math.max(0, index - 1));
@@ -87,7 +97,7 @@ export function handleLaunchInput(ctx) {
87
97
  }
88
98
  if (key.escape) {
89
99
  setLaunchStep(retreatLaunchWizardStep(launchStep));
90
- return true;
100
+ return allowEscapeRetreat ? "retreated" : true;
91
101
  }
92
102
  }
93
103
 
@@ -8,7 +8,9 @@ import {
8
8
  import {
9
9
  createCockpitUiState,
10
10
  reduceCockpitUi,
11
- resolveNavAction
11
+ resolveNavAction,
12
+ routeCockpitKey,
13
+ isContentInteractiveView
12
14
  } from "./cockpit-controller.js";
13
15
  import {
14
16
  COCKPIT_REGIONS,
@@ -65,7 +67,31 @@ export function OrchestratorApp({
65
67
  };
66
68
 
67
69
  useInput((inputKey, key) => {
70
+ if (data.loading || data.error || data.busy) return;
71
+
68
72
  if (key.escape) {
73
+ // Launch wizard may retreat a step before leaving the view.
74
+ if (ui.view === ORCHESTRATOR_VIEWS.LAUNCH && data.launchableAgents.length > 0) {
75
+ const retreated = handleLaunchInput({
76
+ key,
77
+ inputKey,
78
+ launchStep: data.launchStep,
79
+ launchDraft: data.launchDraft,
80
+ launchableAgents: data.launchableAgents,
81
+ launchAgentIndex: data.launchAgentIndex,
82
+ launchPermissionIndex: data.launchPermissionIndex,
83
+ setLaunchAgentIndex: data.setLaunchAgentIndex,
84
+ setLaunchDraft: data.setLaunchDraft,
85
+ setLaunchStep: data.setLaunchStep,
86
+ setLaunchPermissionIndex: data.setLaunchPermissionIndex,
87
+ setError: data.setError,
88
+ handleLaunch: (draft) => data.handleLaunch(draft, data.dashboard?.profile, dispatch),
89
+ reload: data.reload,
90
+ allowEscapeRetreat: true
91
+ });
92
+ if (retreated === "retreated") return;
93
+ }
94
+
69
95
  const next = reduceCockpitUi(ui, { type: "escape" });
70
96
  if (next.shouldExit) {
71
97
  finish({ cancelled: true });
@@ -77,17 +103,44 @@ export function OrchestratorApp({
77
103
  return;
78
104
  }
79
105
 
80
- if (data.loading || data.error || data.busy) return;
81
-
82
106
  if (inputKey === "?") {
83
107
  dispatch({ type: "toggle-help" });
84
108
  return;
85
109
  }
110
+
111
+ const listLength = ui.view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
112
+ ? (data.dashboard?.activeRuns ?? []).length
113
+ : ui.view === ORCHESTRATOR_VIEWS.RECENT_RUNS
114
+ ? (data.dashboard?.recentRuns ?? []).length
115
+ : 0;
116
+
117
+ let routed = null;
86
118
  if (key.tab) {
87
- dispatch({ type: "tab" });
119
+ routed = routeCockpitKey(ui, { type: "tab" });
120
+ } else if (key.upArrow || key.downArrow) {
121
+ routed = routeCockpitKey(ui, {
122
+ type: "arrow",
123
+ direction: key.upArrow ? "up" : "down",
124
+ listLength
125
+ });
126
+ } else if (key.return) {
127
+ routed = routeCockpitKey(ui, { type: "enter" });
128
+ }
129
+
130
+ if (routed) {
131
+ if (routed.type === "enter-nav") {
132
+ const item = resolveNavAction(ui.navIndex);
133
+ if (item?.action === "launch") {
134
+ data.resetLaunchWizard();
135
+ dispatch({ type: "set-view", view: ORCHESTRATOR_VIEWS.LAUNCH });
136
+ return;
137
+ }
138
+ }
139
+ dispatch(routed);
88
140
  return;
89
141
  }
90
142
 
143
+ // View-specific handlers after centralized region routing.
91
144
  if (ui.view === ORCHESTRATOR_VIEWS.LAUNCH && data.launchableAgents.length > 0) {
92
145
  if (handleLaunchInput({
93
146
  key,
@@ -109,22 +162,14 @@ export function OrchestratorApp({
109
162
  }
110
163
  }
111
164
 
112
- if (ui.view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS || ui.view === ORCHESTRATOR_VIEWS.RECENT_RUNS) {
165
+ if (ui.region === COCKPIT_REGIONS.CONTENT
166
+ && (ui.view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS || ui.view === ORCHESTRATOR_VIEWS.RECENT_RUNS)
167
+ && key.return) {
113
168
  const runs = ui.view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
114
169
  ? data.dashboard?.activeRuns ?? []
115
170
  : data.dashboard?.recentRuns ?? [];
116
- if (key.upArrow || key.downArrow) {
117
- dispatch({
118
- type: "arrow",
119
- direction: key.upArrow ? "up" : "down",
120
- listLength: runs.length
121
- });
122
- return;
123
- }
124
- if (key.return) {
125
- data.openRunDetail(selectRunFromList(runs, ui.listIndex), dispatch);
126
- return;
127
- }
171
+ data.openRunDetail(selectRunFromList(runs, ui.listIndex), dispatch, ui.view);
172
+ return;
128
173
  }
129
174
 
130
175
  if (ui.view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
@@ -133,7 +178,7 @@ export function OrchestratorApp({
133
178
  return;
134
179
  }
135
180
  if (inputKey.toLowerCase() === "r") {
136
- data.openRunDetail(data.selectedRun, dispatch);
181
+ data.openRunDetail(data.selectedRun, dispatch, ui.returnView);
137
182
  return;
138
183
  }
139
184
  }
@@ -142,23 +187,6 @@ export function OrchestratorApp({
142
187
  data.reload().catch((reloadError) => {
143
188
  data.setError(reloadError instanceof Error ? reloadError.message : String(reloadError));
144
189
  });
145
- return;
146
- }
147
-
148
- if (ui.view === ORCHESTRATOR_VIEWS.HOME || ui.region === COCKPIT_REGIONS.NAV) {
149
- if (key.upArrow || key.downArrow) {
150
- dispatch({ type: "arrow", direction: key.upArrow ? "up" : "down" });
151
- return;
152
- }
153
- if (key.return) {
154
- const item = resolveNavAction(ui.navIndex);
155
- if (item?.action === "launch") {
156
- data.resetLaunchWizard();
157
- dispatch({ type: "set-view", view: ORCHESTRATOR_VIEWS.LAUNCH });
158
- return;
159
- }
160
- dispatch({ type: "enter-nav" });
161
- }
162
190
  }
163
191
  });
164
192
 
@@ -201,7 +229,7 @@ export function OrchestratorApp({
201
229
  layoutMode: mode,
202
230
  nav: buildNavModel({
203
231
  navIndex: ui.navIndex,
204
- focused: ui.region === COCKPIT_REGIONS.NAV,
232
+ focused: ui.region === COCKPIT_REGIONS.NAV || !isContentInteractiveView(ui.view),
205
233
  unicode
206
234
  }),
207
235
  system: buildSystemStripModel({
@@ -64,12 +64,16 @@ export function useOrchestratorData({
64
64
  setLaunchPermissionIndex(0);
65
65
  };
66
66
 
67
- const openRunDetail = async (run, dispatch) => {
67
+ const openRunDetail = async (run, dispatch, returnView = null) => {
68
68
  if (!run) return;
69
69
  const events = await readRunEvents(homeDir, run.runId, { limit: 20 });
70
70
  setSelectedRun(run);
71
71
  setSelectedEvents(events);
72
- dispatch({ type: "set-view", view: ORCHESTRATOR_VIEWS.RUN_DETAIL });
72
+ dispatch({
73
+ type: "set-view",
74
+ view: ORCHESTRATOR_VIEWS.RUN_DETAIL,
75
+ returnView: returnView ?? null
76
+ });
73
77
  };
74
78
 
75
79
  const handleLaunch = async (draft, profile, dispatch) => {