@kal-elsam/kairo-runtime 0.2.2 → 0.3.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 +26 -2
- package/package.json +3 -1
- package/scripts/cockpit-smoke-test.sh +19 -0
- package/scripts/cockpit-smoke.mjs +51 -0
- package/src/cli.js +23 -7
- package/src/global/brand/index.js +10 -0
- package/src/global/dashboard-guidance.js +66 -0
- package/src/global/initial-experience.js +34 -0
- package/src/global/ink/cockpit/primitives.js +131 -0
- package/src/global/ink/cockpit-controller.js +131 -0
- package/src/global/ink/cockpit-models.js +153 -0
- package/src/global/ink/cockpit-views.js +141 -0
- package/src/global/ink/fullscreen-session.js +128 -0
- package/src/global/ink/launch-input.js +99 -0
- package/src/global/ink/layout.js +34 -0
- package/src/global/ink/list-window.js +38 -0
- package/src/global/ink/orchestrator-app.js +174 -440
- package/src/global/ink/run-orchestrator-ink.js +37 -15
- package/src/global/ink/run-setup-ink.js +39 -16
- package/src/global/ink/setup-app.js +22 -21
- package/src/global/ink/setup-state.js +24 -2
- package/src/global/ink/terminal-capabilities.js +50 -0
- package/src/global/ink/terminal.js +27 -5
- package/src/global/ink/theme.js +72 -0
- package/src/global/ink/use-orchestrator-data.js +158 -0
- package/src/global/ink/use-terminal-size.js +39 -0
- package/src/global/orchestrator.js +71 -15
- package/src/global/setup.js +17 -7
package/README.md
CHANGED
|
@@ -24,13 +24,37 @@ commands) without depending on Pi as a runtime or adding a Pi adapter.
|
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
|
-
Recommended entry — run Kairo Runtime in your terminal
|
|
27
|
+
Recommended entry — run Kairo Runtime in your terminal:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
npx @kal-elsam/kairo-runtime
|
|
31
|
+
# or, after a global install:
|
|
32
|
+
kairo
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
**First run** (no `~/.harness/state.json`): interactive onboarding → safe diagnosis →
|
|
36
|
+
setup with confirmation → full-screen operations cockpit.
|
|
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:
|
|
40
|
+
|
|
41
|
+
| Mode | Size | Layout |
|
|
42
|
+
|------|------|--------|
|
|
43
|
+
| Wide | ≥100 cols × ≥28 rows | Nav + content + system |
|
|
44
|
+
| Compact | ≥72×20 | Nav + content |
|
|
45
|
+
| Minimal | 60–71 cols or short height | Single panel + nav header |
|
|
46
|
+
| Below gate | <60 cols | Explicit TTY fallback (Ink disabled) |
|
|
47
|
+
|
|
48
|
+
Keys: `↑↓` navigate · `Tab` region · `Enter` open · `R` refresh · `C` cancel run ·
|
|
49
|
+
`?` help · `Esc` back (exit only from Home).
|
|
50
|
+
|
|
51
|
+
Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
|
|
52
|
+
in text, never color alone.
|
|
53
|
+
|
|
54
|
+
Explicit commands and setup flags keep their current behavior (`kairo setup`,
|
|
55
|
+
`kairo --dry-run`, `kairo shell`, non-TTY scripts, etc.).
|
|
56
|
+
|
|
57
|
+
Preview setup without writing anything:
|
|
34
58
|
|
|
35
59
|
```bash
|
|
36
60
|
npx @kal-elsam/kairo-runtime --dry-run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"doctor": "node ./bin/kairo.js doctor",
|
|
34
34
|
"smoke": "bash scripts/smoke-test.sh",
|
|
35
35
|
"ux:smoke": "bash scripts/ux-smoke-test.sh",
|
|
36
|
+
"smoke:cockpit": "bash scripts/cockpit-smoke-test.sh",
|
|
36
37
|
"smoke:registry": "bash scripts/registry-smoke-test.sh",
|
|
37
38
|
"smoke:installer": "bash scripts/installer-smoke-test.sh",
|
|
38
39
|
"smoke:bridge": "bash scripts/bridge-smoke-test.sh",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
62
|
"@clack/prompts": "^1.7.0",
|
|
63
|
+
"ansi-escapes": "^7.3.0",
|
|
62
64
|
"ink": "^5.2.1",
|
|
63
65
|
"react": "^18.3.1"
|
|
64
66
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cockpit smoke — layout/capabilities + package wiring (no PTY required).
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
cd "$ROOT"
|
|
7
|
+
|
|
8
|
+
echo "Kairo cockpit smoke"
|
|
9
|
+
node "$ROOT/scripts/cockpit-smoke.mjs"
|
|
10
|
+
|
|
11
|
+
node --test \
|
|
12
|
+
test/fullscreen-session.test.js \
|
|
13
|
+
test/layout.test.js \
|
|
14
|
+
test/terminal-capabilities.test.js \
|
|
15
|
+
test/cockpit-models.test.js \
|
|
16
|
+
test/cockpit-controller.test.js \
|
|
17
|
+
test/cockpit-frame.test.js
|
|
18
|
+
|
|
19
|
+
echo "Kairo cockpit smoke passed"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveLayoutMode, LAYOUT_MODES } from "../src/global/ink/layout.js";
|
|
7
|
+
import { resolveTerminalCapabilities } from "../src/global/ink/terminal-capabilities.js";
|
|
8
|
+
import { createFullscreenSession } from "../src/global/ink/fullscreen-session.js";
|
|
9
|
+
import { buildTopBarModel, buildHomeMissionModel } from "../src/global/ink/cockpit-models.js";
|
|
10
|
+
|
|
11
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkg = require(join(root, "package.json"));
|
|
14
|
+
|
|
15
|
+
assert.equal(pkg.version, "0.3.0");
|
|
16
|
+
assert.ok(pkg.dependencies["ansi-escapes"]);
|
|
17
|
+
|
|
18
|
+
assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
|
|
19
|
+
assert.equal(resolveLayoutMode({ columns: 80, rows: 24 }), LAYOUT_MODES.COMPACT);
|
|
20
|
+
assert.equal(resolveLayoutMode({ columns: 65, rows: 24 }), LAYOUT_MODES.MINIMAL);
|
|
21
|
+
assert.equal(resolveLayoutMode({ columns: 50, rows: 24 }), null);
|
|
22
|
+
|
|
23
|
+
const caps = resolveTerminalCapabilities({
|
|
24
|
+
columns: 80,
|
|
25
|
+
rows: 24,
|
|
26
|
+
isTTY: true,
|
|
27
|
+
term: "xterm-256color",
|
|
28
|
+
env: { NO_COLOR: "1" }
|
|
29
|
+
});
|
|
30
|
+
assert.equal(caps.color, false);
|
|
31
|
+
assert.equal(caps.canUseInk, true);
|
|
32
|
+
|
|
33
|
+
const session = createFullscreenSession({
|
|
34
|
+
stdout: { isTTY: true, write: () => true },
|
|
35
|
+
processRef: { on() {}, removeListener() {}, exit() {} },
|
|
36
|
+
onSignal: () => {}
|
|
37
|
+
});
|
|
38
|
+
assert.equal(session.enter(), true);
|
|
39
|
+
assert.equal(session.leave(), true);
|
|
40
|
+
assert.equal(session.leave(), false);
|
|
41
|
+
|
|
42
|
+
const top = buildTopBarModel({ projectName: "smoke" });
|
|
43
|
+
assert.match(top.status, /ONLINE|Offline/);
|
|
44
|
+
const mission = buildHomeMissionModel({
|
|
45
|
+
hasGlobalState: false,
|
|
46
|
+
diagnostics: { diagnostics: { detected: 0 } },
|
|
47
|
+
dashboard: { providers: [], recentRuns: [] }
|
|
48
|
+
});
|
|
49
|
+
assert.match(mission.title, /MISSION CONTROL/);
|
|
50
|
+
|
|
51
|
+
console.log("cockpit smoke OK");
|
package/src/cli.js
CHANGED
|
@@ -42,6 +42,11 @@ import {
|
|
|
42
42
|
resolveSuggestedInvocation
|
|
43
43
|
} from "./global/brand/cli.js";
|
|
44
44
|
import { BRAND } from "./global/brand/index.js";
|
|
45
|
+
import {
|
|
46
|
+
INITIAL_EXPERIENCE,
|
|
47
|
+
hasConfiguredGlobalState,
|
|
48
|
+
resolveInitialExperience
|
|
49
|
+
} from "./global/initial-experience.js";
|
|
45
50
|
|
|
46
51
|
export { resolveSuggestedInvocation };
|
|
47
52
|
|
|
@@ -50,7 +55,7 @@ const packageRoot = resolve(__dirname, "..");
|
|
|
50
55
|
const SCOPES = new Set(["agent-global", "workspace"]);
|
|
51
56
|
|
|
52
57
|
export async function runCli(argv) {
|
|
53
|
-
const { command, options } = parseArgs(argv);
|
|
58
|
+
const { command, options, isImplicitCommand } = parseArgs(argv);
|
|
54
59
|
maybeWarnLegacyCli(process.argv, { json: options.json });
|
|
55
60
|
|
|
56
61
|
if (options.help || command === "help") {
|
|
@@ -69,14 +74,22 @@ export async function runCli(argv) {
|
|
|
69
74
|
const invoke = resolveSuggestedInvocation(packageManifest.name);
|
|
70
75
|
|
|
71
76
|
switch (command) {
|
|
72
|
-
case "shell":
|
|
77
|
+
case "shell": {
|
|
78
|
+
const homeDir = resolveHomeDir();
|
|
79
|
+
const resolvedMode = resolveInitialExperience({
|
|
80
|
+
interactive: optionsWithPolicy.interactive,
|
|
81
|
+
isImplicitCommand,
|
|
82
|
+
hasGlobalState: hasConfiguredGlobalState(homeDir)
|
|
83
|
+
});
|
|
73
84
|
await runOrchestratorShell({
|
|
74
85
|
packageRoot,
|
|
75
86
|
packageManifest,
|
|
76
87
|
workspaceRoot: optionsWithPolicy.cwd,
|
|
77
|
-
interactive: optionsWithPolicy.interactive
|
|
88
|
+
interactive: optionsWithPolicy.interactive,
|
|
89
|
+
initialMode: resolvedMode ?? INITIAL_EXPERIENCE.DASHBOARD
|
|
78
90
|
});
|
|
79
91
|
return;
|
|
92
|
+
}
|
|
80
93
|
case "orchestrator":
|
|
81
94
|
await runOrchestratorDiagnostics({
|
|
82
95
|
homeDir: resolveHomeDir(),
|
|
@@ -468,7 +481,7 @@ export function parseArgs(argv) {
|
|
|
468
481
|
options.task = args.join(" ").trim();
|
|
469
482
|
}
|
|
470
483
|
|
|
471
|
-
return { command, options };
|
|
484
|
+
return { command, options, isImplicitCommand: implicitCommand };
|
|
472
485
|
}
|
|
473
486
|
|
|
474
487
|
function parseComponentsAction(args, options) {
|
|
@@ -683,10 +696,11 @@ sections, components, backups, and drift repair under ~/.harness.
|
|
|
683
696
|
Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
|
|
684
697
|
|
|
685
698
|
Usage:
|
|
686
|
-
${cli}
|
|
699
|
+
${cli} First run: onboarding → setup → cockpit (TTY).
|
|
700
|
+
Later: full-screen cockpit (wide/compact/minimal).
|
|
687
701
|
${cli} --dry-run Setup dry-run (scriptable)
|
|
688
702
|
${cli} --version
|
|
689
|
-
${cli} shell Operations
|
|
703
|
+
${cli} shell Operations cockpit (TTY)
|
|
690
704
|
${cli} run --agent <id> --task "..." [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
|
|
691
705
|
${cli} runs list [--json] [--limit <n>] [--active-only]
|
|
692
706
|
${cli} runs show <runId> [--json] [--limit <n>] [--follow]
|
|
@@ -727,7 +741,9 @@ Scopes:
|
|
|
727
741
|
Explicit --scope=workspace only.
|
|
728
742
|
|
|
729
743
|
Commands:
|
|
730
|
-
shell Operations
|
|
744
|
+
shell Operations cockpit (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
|
|
745
|
+
is missing, otherwise the cockpit. Explicit ${cli} shell always opens the cockpit.
|
|
746
|
+
Keys: ↑↓ · Tab region · Enter · R refresh · C cancel · ? help · Esc back/exit.
|
|
731
747
|
run Launch a managed agent run with local audit trail.
|
|
732
748
|
runs List, inspect, or cancel agent runs under ~/.harness/runs/.
|
|
733
749
|
orchestrator Read-only capability registry diagnostics (--json supported).
|
|
@@ -70,6 +70,16 @@ export const WIZARD_COPY = {
|
|
|
70
70
|
coreOnlyLabel: "Core only (no components)"
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/** First-run framing reused by onboarding → setup. */
|
|
74
|
+
export const ONBOARDING_COPY = {
|
|
75
|
+
welcomeTitle: `Welcome to ${BRAND.displayName}`,
|
|
76
|
+
purpose:
|
|
77
|
+
`${BRAND.displayName} detects, configures, and coordinates the local agents you already use.`,
|
|
78
|
+
safety:
|
|
79
|
+
"Diagnosis is read-only. Nothing is modified until you confirm a plan.",
|
|
80
|
+
continueHint: "Press Enter to diagnose and configure · Esc to exit"
|
|
81
|
+
};
|
|
82
|
+
|
|
73
83
|
export function getAgentLabel(agentId) {
|
|
74
84
|
return AGENT_LABELS[agentId] ?? agentId;
|
|
75
85
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
2
|
+
|
|
3
|
+
export const DASHBOARD_PURPOSE =
|
|
4
|
+
"Detects, configures, and coordinates local AI agents — no changes without confirmation.";
|
|
5
|
+
|
|
6
|
+
export const NEXT_STEP_KINDS = {
|
|
7
|
+
CONFIGURE: "configure",
|
|
8
|
+
ENABLE_INTELLIGENCE: "enable_intelligence",
|
|
9
|
+
LAUNCH: "launch",
|
|
10
|
+
REVIEW: "review"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function formatDashboardPurpose() {
|
|
14
|
+
return DASHBOARD_PURPOSE;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Contextual next step from existing diagnostics + dashboard snapshot.
|
|
19
|
+
* Priority: configure → review problems → enable intelligence → launch.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveDashboardRecommendation({
|
|
22
|
+
hasGlobalState = false,
|
|
23
|
+
diagnostics = null,
|
|
24
|
+
dashboard = null
|
|
25
|
+
} = {}) {
|
|
26
|
+
const summary = diagnostics?.diagnostics ?? { detected: 0, errors: 0 };
|
|
27
|
+
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
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!hasGlobalState || (summary.detected ?? 0) === 0) {
|
|
35
|
+
return {
|
|
36
|
+
kind: NEXT_STEP_KINDS.CONFIGURE,
|
|
37
|
+
message: `Configure the local environment with ${formatCliCommand("setup")}.`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (hasErrors || hasProblemRecommendation) {
|
|
42
|
+
return {
|
|
43
|
+
kind: NEXT_STEP_KINDS.REVIEW,
|
|
44
|
+
message: "Review diagnostics for problems before launching a run."
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
|
|
49
|
+
return {
|
|
50
|
+
kind: NEXT_STEP_KINDS.ENABLE_INTELLIGENCE,
|
|
51
|
+
message: "Enable intelligence: start Ollama or set OPENROUTER_API_KEY, then retry."
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (launchableCount > 0) {
|
|
56
|
+
return {
|
|
57
|
+
kind: NEXT_STEP_KINDS.LAUNCH,
|
|
58
|
+
message: "Launch a supervised run from the menu or with kairo run."
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
kind: NEXT_STEP_KINDS.REVIEW,
|
|
64
|
+
message: "Review diagnostics for problems before launching a run."
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { harnessHomePaths } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
export const INITIAL_EXPERIENCE = {
|
|
5
|
+
ONBOARDING: "onboarding",
|
|
6
|
+
DASHBOARD: "dashboard"
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* First-run marker is ~/.harness/state.json only.
|
|
11
|
+
* profile.json does not participate in this decision.
|
|
12
|
+
*/
|
|
13
|
+
export function hasConfiguredGlobalState(homeDir) {
|
|
14
|
+
return existsSync(harnessHomePaths(homeDir).statePath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pure resolver for the interactive bare-entry experience.
|
|
19
|
+
* Returns null when CLI should keep existing non-onboarding paths
|
|
20
|
+
* (non-TTY, explicit commands, setup flags already routed elsewhere).
|
|
21
|
+
*/
|
|
22
|
+
export function resolveInitialExperience({
|
|
23
|
+
interactive = false,
|
|
24
|
+
isImplicitCommand = false,
|
|
25
|
+
hasGlobalState = false
|
|
26
|
+
} = {}) {
|
|
27
|
+
if (!interactive || !isImplicitCommand) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return hasGlobalState
|
|
32
|
+
? INITIAL_EXPERIENCE.DASHBOARD
|
|
33
|
+
: INITIAL_EXPERIENCE.ONBOARDING;
|
|
34
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { COCKPIT_COLORS, statusColor } from "../theme.js";
|
|
4
|
+
|
|
5
|
+
export function CockpitBadge({ label, kind = "ready", colorEnabled = true }) {
|
|
6
|
+
return React.createElement(Text, {
|
|
7
|
+
color: statusColor(kind, { colorEnabled })
|
|
8
|
+
}, label);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CockpitEmptyState({ title, message, hint }) {
|
|
12
|
+
return React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
13
|
+
title && React.createElement(Text, { bold: true, color: COCKPIT_COLORS.secondary }, title),
|
|
14
|
+
message && React.createElement(Text, null, message),
|
|
15
|
+
hint && React.createElement(Text, { color: COCKPIT_COLORS.muted }, hint)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CockpitPanel({ title, focused = false, width, children }) {
|
|
20
|
+
return React.createElement(Box, {
|
|
21
|
+
flexDirection: "column",
|
|
22
|
+
width,
|
|
23
|
+
borderStyle: "single",
|
|
24
|
+
borderColor: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted,
|
|
25
|
+
paddingX: 1,
|
|
26
|
+
flexGrow: 1
|
|
27
|
+
},
|
|
28
|
+
title && React.createElement(Text, {
|
|
29
|
+
bold: true,
|
|
30
|
+
color: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
|
|
31
|
+
}, title),
|
|
32
|
+
children
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function CockpitTopBar({ model, colorEnabled = true }) {
|
|
37
|
+
return React.createElement(Box, { justifyContent: "space-between", width: "100%" },
|
|
38
|
+
React.createElement(Text, {
|
|
39
|
+
bold: true,
|
|
40
|
+
color: colorEnabled ? COCKPIT_COLORS.primary : undefined
|
|
41
|
+
}, `╭─ ${model.brand} ─ ${model.status}`),
|
|
42
|
+
React.createElement(Text, {
|
|
43
|
+
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
44
|
+
}, `${model.projectLabel} ─╮`)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function CockpitNav({ model, colorEnabled = true }) {
|
|
49
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
50
|
+
model.items.map((item) =>
|
|
51
|
+
React.createElement(Text, {
|
|
52
|
+
key: item.id,
|
|
53
|
+
bold: item.focused || item.selected,
|
|
54
|
+
color: item.focused
|
|
55
|
+
? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
|
|
56
|
+
: item.selected
|
|
57
|
+
? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
|
|
58
|
+
: undefined
|
|
59
|
+
}, `${item.marker} ${item.label}`)
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function CockpitSystemStrip({ model, colorEnabled = true }) {
|
|
65
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
66
|
+
model.rows.map((row) =>
|
|
67
|
+
React.createElement(Text, { key: row.key },
|
|
68
|
+
React.createElement(Text, { color: COCKPIT_COLORS.muted }, `${row.key.padEnd(7)}`),
|
|
69
|
+
React.createElement(CockpitBadge, {
|
|
70
|
+
label: row.value,
|
|
71
|
+
kind: row.kind,
|
|
72
|
+
colorEnabled
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function CockpitFooter({ model }) {
|
|
80
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
81
|
+
React.createElement(Text, { color: COCKPIT_COLORS.muted },
|
|
82
|
+
`├${"─".repeat(62)}┤`
|
|
83
|
+
),
|
|
84
|
+
React.createElement(Text, { color: COCKPIT_COLORS.muted }, `│ ${model.text}`),
|
|
85
|
+
React.createElement(Text, { color: COCKPIT_COLORS.muted },
|
|
86
|
+
`╰${"─".repeat(62)}╯`
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function CockpitShell({
|
|
92
|
+
topBar,
|
|
93
|
+
footer,
|
|
94
|
+
layoutMode,
|
|
95
|
+
nav,
|
|
96
|
+
system,
|
|
97
|
+
navFocused,
|
|
98
|
+
contentFocused,
|
|
99
|
+
systemFocused,
|
|
100
|
+
colorEnabled = true,
|
|
101
|
+
children
|
|
102
|
+
}) {
|
|
103
|
+
const showNav = layoutMode === "wide" || layoutMode === "compact";
|
|
104
|
+
const showSystem = layoutMode === "wide";
|
|
105
|
+
|
|
106
|
+
return React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
107
|
+
React.createElement(CockpitTopBar, { model: topBar, colorEnabled }),
|
|
108
|
+
layoutMode === "minimal" && nav && React.createElement(Box, { marginY: 0 },
|
|
109
|
+
React.createElement(Text, { color: COCKPIT_COLORS.muted }, "Nav: "),
|
|
110
|
+
React.createElement(CockpitNav, { model: nav, colorEnabled })
|
|
111
|
+
),
|
|
112
|
+
React.createElement(Box, { flexDirection: "row", width: "100%" },
|
|
113
|
+
showNav && React.createElement(CockpitPanel, {
|
|
114
|
+
title: nav?.title ?? "NAVIGATION",
|
|
115
|
+
focused: navFocused,
|
|
116
|
+
width: 22
|
|
117
|
+
}, React.createElement(CockpitNav, { model: nav, colorEnabled })),
|
|
118
|
+
React.createElement(CockpitPanel, {
|
|
119
|
+
title: undefined,
|
|
120
|
+
focused: contentFocused,
|
|
121
|
+
width: showSystem ? 48 : showNav ? 56 : "100%"
|
|
122
|
+
}, children),
|
|
123
|
+
showSystem && React.createElement(CockpitPanel, {
|
|
124
|
+
title: system?.title ?? "SYSTEM",
|
|
125
|
+
focused: systemFocused,
|
|
126
|
+
width: 20
|
|
127
|
+
}, React.createElement(CockpitSystemStrip, { model: system, colorEnabled }))
|
|
128
|
+
),
|
|
129
|
+
React.createElement(CockpitFooter, { model: footer })
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Pure cockpit focus / navigation reducer — no I/O, no React.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function createCockpitUiState({
|
|
10
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
11
|
+
view = ORCHESTRATOR_VIEWS.HOME,
|
|
12
|
+
region = COCKPIT_REGIONS.NAV,
|
|
13
|
+
navIndex = 0,
|
|
14
|
+
listIndex = 0,
|
|
15
|
+
helpOpen = false
|
|
16
|
+
} = {}) {
|
|
17
|
+
const regions = regionsForLayout(layoutMode);
|
|
18
|
+
const safeRegion = regions.includes(region) ? region : regions[0];
|
|
19
|
+
return {
|
|
20
|
+
layoutMode,
|
|
21
|
+
view,
|
|
22
|
+
region: safeRegion,
|
|
23
|
+
navIndex: clamp(navIndex, 0, COCKPIT_NAV.length - 1),
|
|
24
|
+
listIndex: Math.max(0, listIndex),
|
|
25
|
+
helpOpen,
|
|
26
|
+
shouldExit: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function reduceCockpitUi(state, action) {
|
|
31
|
+
switch (action.type) {
|
|
32
|
+
case "resize": {
|
|
33
|
+
const regions = regionsForLayout(action.layoutMode);
|
|
34
|
+
const region = regions.includes(state.region) ? state.region : regions[0];
|
|
35
|
+
return { ...state, layoutMode: action.layoutMode, region };
|
|
36
|
+
}
|
|
37
|
+
case "tab": {
|
|
38
|
+
const regions = regionsForLayout(state.layoutMode);
|
|
39
|
+
if (regions.length < 2) return state;
|
|
40
|
+
const index = regions.indexOf(state.region);
|
|
41
|
+
const next = regions[(index + 1) % regions.length];
|
|
42
|
+
return { ...state, region: next };
|
|
43
|
+
}
|
|
44
|
+
case "arrow": {
|
|
45
|
+
if (state.helpOpen) return state;
|
|
46
|
+
if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
|
|
47
|
+
|
|
48
|
+
const navigatesNav = state.region === COCKPIT_REGIONS.NAV
|
|
49
|
+
|| (state.view === ORCHESTRATOR_VIEWS.HOME
|
|
50
|
+
&& state.layoutMode === LAYOUT_MODES.MINIMAL);
|
|
51
|
+
|
|
52
|
+
if (navigatesNav) {
|
|
53
|
+
const delta = action.direction === "up" ? -1 : 1;
|
|
54
|
+
return {
|
|
55
|
+
...state,
|
|
56
|
+
navIndex: clamp(state.navIndex + delta, 0, COCKPIT_NAV.length - 1)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const delta = action.direction === "up" ? -1 : 1;
|
|
61
|
+
const max = Math.max(0, (action.listLength ?? 1) - 1);
|
|
62
|
+
return { ...state, listIndex: clamp(state.listIndex + delta, 0, max) };
|
|
63
|
+
}
|
|
64
|
+
case "enter-nav": {
|
|
65
|
+
const item = COCKPIT_NAV[state.navIndex];
|
|
66
|
+
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
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
view: item.view,
|
|
78
|
+
listIndex: 0,
|
|
79
|
+
region: COCKPIT_REGIONS.CONTENT,
|
|
80
|
+
helpOpen: false
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
case "set-view":
|
|
84
|
+
return {
|
|
85
|
+
...state,
|
|
86
|
+
view: action.view,
|
|
87
|
+
listIndex: 0,
|
|
88
|
+
region: action.region ?? COCKPIT_REGIONS.CONTENT
|
|
89
|
+
};
|
|
90
|
+
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
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { ...state, helpOpen: true, view: ORCHESTRATOR_VIEWS.HELP };
|
|
101
|
+
case "escape": {
|
|
102
|
+
if (state.helpOpen) {
|
|
103
|
+
return { ...state, helpOpen: false, view: ORCHESTRATOR_VIEWS.HOME };
|
|
104
|
+
}
|
|
105
|
+
if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
|
|
106
|
+
const regions = regionsForLayout(state.layoutMode);
|
|
107
|
+
return {
|
|
108
|
+
...state,
|
|
109
|
+
view: ORCHESTRATOR_VIEWS.HOME,
|
|
110
|
+
listIndex: 0,
|
|
111
|
+
region: regions.includes(COCKPIT_REGIONS.NAV)
|
|
112
|
+
? COCKPIT_REGIONS.NAV
|
|
113
|
+
: COCKPIT_REGIONS.CONTENT
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { ...state, shouldExit: true };
|
|
117
|
+
}
|
|
118
|
+
case "clear-exit":
|
|
119
|
+
return { ...state, shouldExit: false };
|
|
120
|
+
default:
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveNavAction(navIndex) {
|
|
126
|
+
return COCKPIT_NAV[navIndex] ?? null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function clamp(value, min, max) {
|
|
130
|
+
return Math.min(max, Math.max(min, value));
|
|
131
|
+
}
|