@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 +3 -2
- package/package.json +1 -1
- package/scripts/check-published-release.mjs +4 -1
- package/scripts/cockpit-smoke-test.sh +3 -1
- package/scripts/cockpit-smoke.mjs +54 -2
- package/src/cli.js +2 -1
- package/src/global/ink/cockpit-controller.js +81 -44
- package/src/global/ink/cockpit-focus.js +92 -0
- package/src/global/ink/cockpit-models.js +39 -12
- package/src/global/ink/launch-input.js +13 -3
- package/src/global/ink/orchestrator-app.js +64 -36
- package/src/global/ink/use-orchestrator-data.js +6 -2
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 · `
|
|
49
|
-
|
|
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.
|
|
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 (!
|
|
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 {
|
|
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.
|
|
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: ↑↓ ·
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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:
|
|
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:
|
|
109
|
+
view: nextView,
|
|
87
110
|
listIndex: 0,
|
|
88
|
-
region: action.region ??
|
|
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 {
|
|
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
|
|
127
|
+
if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
|
|
128
|
+
return goOverview(state);
|
|
104
129
|
}
|
|
105
|
-
if (state.view
|
|
106
|
-
const
|
|
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:
|
|
136
|
+
view: back,
|
|
110
137
|
listIndex: 0,
|
|
111
|
-
region:
|
|
112
|
-
|
|
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
|
|
130
|
-
return
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
117
|
-
|
|
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({
|
|
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) => {
|