@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 +11 -7
- 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 +85 -7
- package/src/cli.js +2 -1
- package/src/global/dashboard-guidance.js +146 -17
- package/src/global/ink/cockpit/primitives.js +16 -9
- package/src/global/ink/cockpit-controller.js +84 -44
- package/src/global/ink/cockpit-focus.js +92 -0
- package/src/global/ink/cockpit-home.js +115 -0
- package/src/global/ink/cockpit-models.js +162 -60
- package/src/global/ink/cockpit-views.js +92 -40
- package/src/global/ink/launch-input.js +13 -3
- package/src/global/ink/orchestrator-app.js +132 -50
- package/src/global/ink/orchestrator-state.js +71 -14
- package/src/global/ink/theme.js +7 -1
- package/src/global/ink/use-orchestrator-data.js +6 -2
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
|
|
39
|
-
|
|
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 |
|
|
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
|
|
49
|
-
|
|
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
|
+
"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 (!
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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, /
|
|
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: ↑↓ ·
|
|
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
|
-
"
|
|
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
|
|
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
|
|
29
|
-
const
|
|
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
|
-
|
|
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 (
|
|
154
|
+
if (blocked && launchableCount === 0) {
|
|
42
155
|
return {
|
|
43
156
|
kind: NEXT_STEP_KINDS.REVIEW,
|
|
44
|
-
|
|
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 (
|
|
165
|
+
if (launchableCount > 0) {
|
|
49
166
|
return {
|
|
50
|
-
kind: NEXT_STEP_KINDS.
|
|
51
|
-
|
|
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 (
|
|
176
|
+
if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
|
|
56
177
|
return {
|
|
57
|
-
kind: NEXT_STEP_KINDS.
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
|
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,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:
|
|
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
|
-
|
|
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 {
|
|
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
|
|
130
|
+
if (state.helpOpen || state.view === ORCHESTRATOR_VIEWS.HELP) {
|
|
131
|
+
return goOverview(state);
|
|
104
132
|
}
|
|
105
|
-
if (state.view
|
|
106
|
-
const
|
|
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:
|
|
139
|
+
view: back,
|
|
110
140
|
listIndex: 0,
|
|
111
|
-
region:
|
|
112
|
-
|
|
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
|
|
130
|
-
return
|
|
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
|
}
|