@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import { OrchestratorApp } from "./orchestrator-app.js";
|
|
4
|
+
import { createFullscreenSession } from "./fullscreen-session.js";
|
|
4
5
|
|
|
5
6
|
export async function runOrchestratorInk({
|
|
6
7
|
homeDir,
|
|
@@ -8,22 +9,43 @@ export async function runOrchestratorInk({
|
|
|
8
9
|
packageRoot,
|
|
9
10
|
packageName,
|
|
10
11
|
cliVersion,
|
|
11
|
-
|
|
12
|
+
hasGlobalState = false,
|
|
13
|
+
renderImpl = render,
|
|
14
|
+
fullscreenSession = null,
|
|
15
|
+
stdout = process.stdout
|
|
12
16
|
}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
);
|
|
17
|
+
const ownsSession = !fullscreenSession;
|
|
18
|
+
const session = fullscreenSession ?? createFullscreenSession({
|
|
19
|
+
stdout,
|
|
20
|
+
enabled: Boolean(stdout?.isTTY)
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (ownsSession) {
|
|
24
|
+
session.enter();
|
|
25
|
+
}
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
try {
|
|
28
|
+
return await new Promise((resolve) => {
|
|
29
|
+
const { waitUntilExit } = renderImpl(
|
|
30
|
+
React.createElement(OrchestratorApp, {
|
|
31
|
+
homeDir,
|
|
32
|
+
workspaceRoot,
|
|
33
|
+
packageRoot,
|
|
34
|
+
packageName,
|
|
35
|
+
cliVersion,
|
|
36
|
+
hasGlobalState,
|
|
37
|
+
onComplete: resolve
|
|
38
|
+
}),
|
|
39
|
+
stdout ? { stdout } : undefined
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
waitUntilExit().catch((error) => {
|
|
43
|
+
resolve({ cancelled: true, error });
|
|
44
|
+
});
|
|
27
45
|
});
|
|
28
|
-
}
|
|
46
|
+
} finally {
|
|
47
|
+
if (ownsSession) {
|
|
48
|
+
session.leave();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
29
51
|
}
|
|
@@ -5,6 +5,7 @@ import { loadConsentAudit } from "../policy.js";
|
|
|
5
5
|
import { formatResultNote } from "../clack/theme.js";
|
|
6
6
|
import { SetupWizardCancelledError } from "../clack/setup-wizard-constants.js";
|
|
7
7
|
import { SetupApp } from "./setup-app.js";
|
|
8
|
+
import { createFullscreenSession } from "./fullscreen-session.js";
|
|
8
9
|
|
|
9
10
|
export { SetupWizardCancelledError };
|
|
10
11
|
|
|
@@ -15,6 +16,7 @@ export async function runSetupInk({
|
|
|
15
16
|
packageName,
|
|
16
17
|
cliVersion,
|
|
17
18
|
dryRun = false,
|
|
19
|
+
onboarding = false,
|
|
18
20
|
preflight = true,
|
|
19
21
|
yes = false,
|
|
20
22
|
confirm = false,
|
|
@@ -22,25 +24,46 @@ export async function runSetupInk({
|
|
|
22
24
|
yesExplicit = false,
|
|
23
25
|
confirmExplicit = false,
|
|
24
26
|
interactive = true,
|
|
25
|
-
renderImpl = render
|
|
27
|
+
renderImpl = render,
|
|
28
|
+
fullscreenSession = null,
|
|
29
|
+
stdout = process.stdout
|
|
26
30
|
}) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
onComplete: resolve
|
|
37
|
-
})
|
|
38
|
-
);
|
|
31
|
+
const ownsSession = !fullscreenSession;
|
|
32
|
+
const session = fullscreenSession ?? createFullscreenSession({
|
|
33
|
+
stdout,
|
|
34
|
+
enabled: Boolean(stdout?.isTTY)
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (ownsSession) {
|
|
38
|
+
session.enter();
|
|
39
|
+
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
let outcome;
|
|
42
|
+
try {
|
|
43
|
+
outcome = await new Promise((resolve) => {
|
|
44
|
+
const { waitUntilExit } = renderImpl(
|
|
45
|
+
React.createElement(SetupApp, {
|
|
46
|
+
homeDir,
|
|
47
|
+
workspaceRoot,
|
|
48
|
+
packageRoot,
|
|
49
|
+
packageName,
|
|
50
|
+
cliVersion,
|
|
51
|
+
dryRun,
|
|
52
|
+
onboarding,
|
|
53
|
+
onComplete: resolve
|
|
54
|
+
}),
|
|
55
|
+
stdout ? { stdout } : undefined
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
waitUntilExit().catch((error) => {
|
|
59
|
+
resolve({ cancelled: true, usedWizard: true, error });
|
|
60
|
+
});
|
|
42
61
|
});
|
|
43
|
-
}
|
|
62
|
+
} finally {
|
|
63
|
+
if (ownsSession) {
|
|
64
|
+
session.leave();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
44
67
|
|
|
45
68
|
if (outcome.error) {
|
|
46
69
|
throw outcome.error;
|
|
@@ -21,43 +21,40 @@ import {
|
|
|
21
21
|
toggleSelection,
|
|
22
22
|
transitionFromSplash
|
|
23
23
|
} from "./setup-state.js";
|
|
24
|
+
import { COCKPIT_COLORS } from "./theme.js";
|
|
25
|
+
import { CockpitPanel } from "./cockpit/primitives.js";
|
|
24
26
|
|
|
25
27
|
const INK_COLORS = {
|
|
26
|
-
accent:
|
|
27
|
-
success:
|
|
28
|
-
warning:
|
|
29
|
-
danger:
|
|
30
|
-
muted:
|
|
28
|
+
accent: COCKPIT_COLORS.primary,
|
|
29
|
+
success: COCKPIT_COLORS.success,
|
|
30
|
+
warning: COCKPIT_COLORS.warning,
|
|
31
|
+
danger: COCKPIT_COLORS.danger,
|
|
32
|
+
muted: COCKPIT_COLORS.muted
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
function Header() {
|
|
34
36
|
const lines = formatInkHeaderLines();
|
|
35
37
|
return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
36
|
-
React.createElement(Text, { bold: true, color: INK_COLORS.accent }, lines[0]),
|
|
37
|
-
React.createElement(Text, { color:
|
|
38
|
+
React.createElement(Text, { bold: true, color: INK_COLORS.accent }, `╭─ ${lines[0]}`),
|
|
39
|
+
React.createElement(Text, { color: COCKPIT_COLORS.secondary }, lines[1]),
|
|
38
40
|
React.createElement(Text, { dimColor: true }, lines[2])
|
|
39
41
|
);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function Panel({ title, children }) {
|
|
43
|
-
return React.createElement(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
marginBottom: 1
|
|
49
|
-
},
|
|
50
|
-
React.createElement(Text, { bold: true, color: INK_COLORS.accent }, title),
|
|
51
|
-
children
|
|
52
|
-
);
|
|
45
|
+
return React.createElement(CockpitPanel, {
|
|
46
|
+
title,
|
|
47
|
+
focused: true,
|
|
48
|
+
width: "100%"
|
|
49
|
+
}, children);
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
function Footer({ children }) {
|
|
56
53
|
return React.createElement(Text, { dimColor: true }, children);
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
function Splash({ compact }) {
|
|
60
|
-
const lines = formatInkSplashLines({ compact });
|
|
56
|
+
function Splash({ compact, onboarding = false }) {
|
|
57
|
+
const lines = formatInkSplashLines({ compact, onboarding });
|
|
61
58
|
const logoLineCount = compact ? BRAND.compactLogo.length : BRAND.asciiLogo.length;
|
|
62
59
|
|
|
63
60
|
return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
@@ -71,7 +68,7 @@ function Splash({ compact }) {
|
|
|
71
68
|
if (line === BRAND.tagline) {
|
|
72
69
|
return React.createElement(Text, { key: `line-${index}`, color: INK_COLORS.muted }, line);
|
|
73
70
|
}
|
|
74
|
-
if (line === BRAND.splashHint) {
|
|
71
|
+
if (line === BRAND.splashHint || line.includes("Esc to exit") || line.includes("Press Enter")) {
|
|
75
72
|
return React.createElement(Text, { key: `line-${index}`, dimColor: true }, line);
|
|
76
73
|
}
|
|
77
74
|
if (line === "") {
|
|
@@ -89,6 +86,7 @@ export function SetupApp({
|
|
|
89
86
|
packageName,
|
|
90
87
|
cliVersion,
|
|
91
88
|
dryRun = false,
|
|
89
|
+
onboarding = false,
|
|
92
90
|
onComplete
|
|
93
91
|
}) {
|
|
94
92
|
const { exit } = useApp();
|
|
@@ -243,7 +241,10 @@ export function SetupApp({
|
|
|
243
241
|
const detectPanel = formatInkDetectPanel({ adapters, detected });
|
|
244
242
|
|
|
245
243
|
return React.createElement(Box, { flexDirection: "column" },
|
|
246
|
-
step === SETUP_STEPS.SPLASH && React.createElement(Splash, {
|
|
244
|
+
step === SETUP_STEPS.SPLASH && React.createElement(Splash, {
|
|
245
|
+
compact: useCompactSplash,
|
|
246
|
+
onboarding
|
|
247
|
+
}),
|
|
247
248
|
step !== SETUP_STEPS.SPLASH && React.createElement(Header),
|
|
248
249
|
step === SETUP_STEPS.DETECT && React.createElement(Panel, { title: WIZARD_COPY.detectTitle },
|
|
249
250
|
detectPanel.split("\n")
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AGENT_HINTS,
|
|
3
|
+
BRAND,
|
|
4
|
+
ONBOARDING_COPY,
|
|
5
|
+
formatCliCommand,
|
|
6
|
+
getAgentLabel,
|
|
7
|
+
PREFERRED_CLI,
|
|
8
|
+
WIZARD_COPY
|
|
9
|
+
} from "../brand/index.js";
|
|
2
10
|
import { formatAgentMultiselectHint } from "../clack/theme.js";
|
|
3
11
|
|
|
4
12
|
export const SETUP_STEPS = {
|
|
@@ -16,8 +24,22 @@ export function shouldUseCompactSplashLogo(columns) {
|
|
|
16
24
|
return columns < fullWidth + 4;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
export function formatInkSplashLines({ compact = false } = {}) {
|
|
27
|
+
export function formatInkSplashLines({ compact = false, onboarding = false } = {}) {
|
|
20
28
|
const logo = compact ? BRAND.compactLogo : BRAND.asciiLogo;
|
|
29
|
+
if (onboarding) {
|
|
30
|
+
return [
|
|
31
|
+
...logo,
|
|
32
|
+
"",
|
|
33
|
+
BRAND.name,
|
|
34
|
+
BRAND.tagline,
|
|
35
|
+
"",
|
|
36
|
+
ONBOARDING_COPY.purpose,
|
|
37
|
+
ONBOARDING_COPY.safety,
|
|
38
|
+
"",
|
|
39
|
+
ONBOARDING_COPY.continueHint
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
return [
|
|
22
44
|
...logo,
|
|
23
45
|
"",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure terminal capability resolution for Ink cockpit / setup.
|
|
3
|
+
* Never communicates state via color alone — callers must keep text labels.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LAYOUT_MODES = {
|
|
7
|
+
WIDE: "wide",
|
|
8
|
+
COMPACT: "compact",
|
|
9
|
+
MINIMAL: "minimal"
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {{
|
|
14
|
+
* columns?: number,
|
|
15
|
+
* rows?: number,
|
|
16
|
+
* env?: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
17
|
+
* isTTY?: boolean,
|
|
18
|
+
* term?: string
|
|
19
|
+
* }} [options]
|
|
20
|
+
*/
|
|
21
|
+
export function resolveTerminalCapabilities({
|
|
22
|
+
columns = 80,
|
|
23
|
+
rows = 24,
|
|
24
|
+
env = process.env,
|
|
25
|
+
isTTY = true,
|
|
26
|
+
term = env.TERM ?? ""
|
|
27
|
+
} = {}) {
|
|
28
|
+
const forceInk = env.HARNESS_INK !== "0";
|
|
29
|
+
const noColor = Boolean(env.NO_COLOR) || env.FORCE_COLOR === "0";
|
|
30
|
+
const unicode = !(
|
|
31
|
+
env.HARNESS_ASCII === "1"
|
|
32
|
+
|| env.LC_ALL === "C"
|
|
33
|
+
|| term === "dumb"
|
|
34
|
+
|| /^(linux|vt100|vt220)$/i.test(term)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const safeColumns = Number.isFinite(columns) && columns > 0 ? columns : 80;
|
|
38
|
+
const safeRows = Number.isFinite(rows) && rows > 0 ? rows : 24;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
isTTY: Boolean(isTTY),
|
|
42
|
+
term: term || "",
|
|
43
|
+
columns: safeColumns,
|
|
44
|
+
rows: safeRows,
|
|
45
|
+
forceInk,
|
|
46
|
+
color: !noColor,
|
|
47
|
+
unicode,
|
|
48
|
+
canUseInk: Boolean(isTTY) && forceInk && term !== "dumb" && safeColumns >= 60
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { resolveTerminalCapabilities } from "./terminal-capabilities.js";
|
|
3
|
+
import { resolveLayoutMode } from "./layout.js";
|
|
4
|
+
|
|
5
|
+
export { resolveTerminalCapabilities } from "./terminal-capabilities.js";
|
|
6
|
+
export { resolveLayoutMode, resolveListLimit, LAYOUT_MODES } from "./layout.js";
|
|
7
|
+
export { windowList } from "./list-window.js";
|
|
2
8
|
|
|
3
9
|
export function canUseSetupInk({
|
|
4
10
|
interactive = Boolean(input.isTTY && output.isTTY),
|
|
5
11
|
term = process.env.TERM ?? "",
|
|
6
12
|
columns = output.columns ?? 80,
|
|
7
|
-
|
|
13
|
+
rows = output.rows ?? 24,
|
|
14
|
+
forceInk = process.env.HARNESS_INK !== "0",
|
|
15
|
+
env = process.env
|
|
16
|
+
} = {}) {
|
|
17
|
+
const caps = resolveTerminalCapabilities({
|
|
18
|
+
columns,
|
|
19
|
+
rows,
|
|
20
|
+
env: { ...env, HARNESS_INK: forceInk ? env.HARNESS_INK : "0", TERM: term },
|
|
21
|
+
isTTY: interactive,
|
|
22
|
+
term
|
|
23
|
+
});
|
|
24
|
+
return caps.canUseInk;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Snapshot of layout mode from live stdout dimensions.
|
|
29
|
+
*/
|
|
30
|
+
export function readTerminalLayout({
|
|
31
|
+
columns = output.columns ?? 80,
|
|
32
|
+
rows = output.rows ?? 24
|
|
8
33
|
} = {}) {
|
|
9
|
-
|
|
10
|
-
if (term === "dumb") return false;
|
|
11
|
-
if (columns > 0 && columns < 60) return false;
|
|
12
|
-
return true;
|
|
34
|
+
return resolveLayoutMode({ columns, rows });
|
|
13
35
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-space cockpit theme. Status always has a text label — never color alone.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const COCKPIT_COLORS = {
|
|
6
|
+
primary: "cyan",
|
|
7
|
+
secondary: "magenta",
|
|
8
|
+
success: "green",
|
|
9
|
+
warning: "yellow",
|
|
10
|
+
danger: "red",
|
|
11
|
+
muted: "gray",
|
|
12
|
+
border: "cyan"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const STATUS_LABELS = {
|
|
16
|
+
ready: "Ready",
|
|
17
|
+
warn: "Warn",
|
|
18
|
+
error: "Error",
|
|
19
|
+
offline: "Offline",
|
|
20
|
+
local: "Local",
|
|
21
|
+
online: "ONLINE",
|
|
22
|
+
loading: "Loading"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const COCKPIT_GLYPHS = {
|
|
26
|
+
focus: "›",
|
|
27
|
+
focusAscii: ">",
|
|
28
|
+
bullet: "·",
|
|
29
|
+
bulletAscii: "-",
|
|
30
|
+
more: "…",
|
|
31
|
+
moreAscii: "..."
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function resolveGlyphs(unicode = true) {
|
|
35
|
+
if (unicode) {
|
|
36
|
+
return {
|
|
37
|
+
focus: COCKPIT_GLYPHS.focus,
|
|
38
|
+
bullet: COCKPIT_GLYPHS.bullet,
|
|
39
|
+
more: COCKPIT_GLYPHS.more
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
focus: COCKPIT_GLYPHS.focusAscii,
|
|
44
|
+
bullet: COCKPIT_GLYPHS.bulletAscii,
|
|
45
|
+
more: COCKPIT_GLYPHS.moreAscii
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function statusColor(kind, { colorEnabled = true } = {}) {
|
|
50
|
+
if (!colorEnabled) return undefined;
|
|
51
|
+
switch (kind) {
|
|
52
|
+
case "ready":
|
|
53
|
+
case "success":
|
|
54
|
+
case "online":
|
|
55
|
+
return COCKPIT_COLORS.success;
|
|
56
|
+
case "warn":
|
|
57
|
+
case "warning":
|
|
58
|
+
return COCKPIT_COLORS.warning;
|
|
59
|
+
case "error":
|
|
60
|
+
case "danger":
|
|
61
|
+
return COCKPIT_COLORS.danger;
|
|
62
|
+
case "offline":
|
|
63
|
+
case "muted":
|
|
64
|
+
return COCKPIT_COLORS.muted;
|
|
65
|
+
default:
|
|
66
|
+
return COCKPIT_COLORS.primary;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatStatusBadge(kind, label = STATUS_LABELS[kind] ?? String(kind)) {
|
|
71
|
+
return label;
|
|
72
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { buildReadOnlyDiagnostics } from "../action-planner.js";
|
|
3
|
+
import { buildRuntimeDashboardData } from "../runtime/run-cli.js";
|
|
4
|
+
import { readRunEvents } from "../runtime/run-store.js";
|
|
5
|
+
import { startRun, stopRun } from "../runtime/run-manager.js";
|
|
6
|
+
import {
|
|
7
|
+
createLaunchDraft,
|
|
8
|
+
isRunCancellable,
|
|
9
|
+
resolveLaunchPermissions,
|
|
10
|
+
resolveLaunchableAgents
|
|
11
|
+
} from "./orchestrator-state.js";
|
|
12
|
+
import { LAUNCH_WIZARD_STEPS, ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
13
|
+
|
|
14
|
+
export function useOrchestratorData({
|
|
15
|
+
homeDir,
|
|
16
|
+
workspaceRoot,
|
|
17
|
+
packageName,
|
|
18
|
+
packageRoot,
|
|
19
|
+
cliVersion
|
|
20
|
+
}) {
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [busy, setBusy] = useState(false);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
const [dashboard, setDashboard] = useState(null);
|
|
25
|
+
const [diagnostics, setDiagnostics] = useState(null);
|
|
26
|
+
const [selectedRun, setSelectedRun] = useState(null);
|
|
27
|
+
const [selectedEvents, setSelectedEvents] = useState([]);
|
|
28
|
+
const [statusMessage, setStatusMessage] = useState(null);
|
|
29
|
+
const [launchAgentIndex, setLaunchAgentIndex] = useState(0);
|
|
30
|
+
const [launchStep, setLaunchStep] = useState(LAUNCH_WIZARD_STEPS.AGENT);
|
|
31
|
+
const [launchDraft, setLaunchDraft] = useState(createLaunchDraft);
|
|
32
|
+
const [launchPermissionIndex, setLaunchPermissionIndex] = useState(0);
|
|
33
|
+
|
|
34
|
+
const reload = async () => {
|
|
35
|
+
const [dash, diag] = await Promise.all([
|
|
36
|
+
buildRuntimeDashboardData({ homeDir, workspaceRoot, cliVersion }),
|
|
37
|
+
buildReadOnlyDiagnostics({ homeDir, workspaceRoot, packageName, packageRoot, cliVersion })
|
|
38
|
+
]);
|
|
39
|
+
setDashboard(dash);
|
|
40
|
+
setDiagnostics(diag);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let cancelled = false;
|
|
45
|
+
async function load() {
|
|
46
|
+
try {
|
|
47
|
+
await reload();
|
|
48
|
+
if (!cancelled) setLoading(false);
|
|
49
|
+
} catch (loadError) {
|
|
50
|
+
if (!cancelled) {
|
|
51
|
+
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
|
52
|
+
setLoading(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
load();
|
|
57
|
+
return () => { cancelled = true; };
|
|
58
|
+
}, [homeDir, workspaceRoot, packageName, packageRoot, cliVersion]);
|
|
59
|
+
|
|
60
|
+
const resetLaunchWizard = () => {
|
|
61
|
+
setLaunchStep(LAUNCH_WIZARD_STEPS.AGENT);
|
|
62
|
+
setLaunchDraft(createLaunchDraft());
|
|
63
|
+
setLaunchAgentIndex(0);
|
|
64
|
+
setLaunchPermissionIndex(0);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const openRunDetail = async (run, dispatch) => {
|
|
68
|
+
if (!run) return;
|
|
69
|
+
const events = await readRunEvents(homeDir, run.runId, { limit: 20 });
|
|
70
|
+
setSelectedRun(run);
|
|
71
|
+
setSelectedEvents(events);
|
|
72
|
+
dispatch({ type: "set-view", view: ORCHESTRATOR_VIEWS.RUN_DETAIL });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleLaunch = async (draft, profile, dispatch) => {
|
|
76
|
+
if (!draft.agentId || !draft.task.trim()) {
|
|
77
|
+
setError("Agent and task are required.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setBusy(true);
|
|
81
|
+
setStatusMessage(`Launching ${draft.agentId}…`);
|
|
82
|
+
try {
|
|
83
|
+
const permissions = resolveLaunchPermissions({
|
|
84
|
+
...draft,
|
|
85
|
+
permissionIndex: launchPermissionIndex
|
|
86
|
+
});
|
|
87
|
+
const { runId } = await startRun({
|
|
88
|
+
homeDir,
|
|
89
|
+
agentId: draft.agentId,
|
|
90
|
+
task: draft.task.trim(),
|
|
91
|
+
cwd: workspaceRoot,
|
|
92
|
+
model: draft.model.trim() || null,
|
|
93
|
+
permissions,
|
|
94
|
+
cliVersion,
|
|
95
|
+
profile: profile ?? null,
|
|
96
|
+
follow: false,
|
|
97
|
+
wait: false
|
|
98
|
+
});
|
|
99
|
+
await reload();
|
|
100
|
+
const run = (await buildRuntimeDashboardData({ homeDir, workspaceRoot, cliVersion }))
|
|
101
|
+
.runs.find((entry) => entry.runId === runId);
|
|
102
|
+
setStatusMessage(`Run started: ${runId}`);
|
|
103
|
+
resetLaunchWizard();
|
|
104
|
+
dispatch({ type: "set-view", view: ORCHESTRATOR_VIEWS.HOME });
|
|
105
|
+
if (run) await openRunDetail(run, dispatch);
|
|
106
|
+
} catch (launchError) {
|
|
107
|
+
setError(launchError instanceof Error ? launchError.message : String(launchError));
|
|
108
|
+
} finally {
|
|
109
|
+
setBusy(false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleCancelRun = async () => {
|
|
114
|
+
if (!selectedRun || !isRunCancellable(selectedRun)) return;
|
|
115
|
+
setBusy(true);
|
|
116
|
+
try {
|
|
117
|
+
await stopRun(homeDir, selectedRun.runId);
|
|
118
|
+
await reload();
|
|
119
|
+
const refreshed = await buildRuntimeDashboardData({ homeDir, workspaceRoot, cliVersion });
|
|
120
|
+
const run = refreshed.runs.find((entry) => entry.runId === selectedRun.runId);
|
|
121
|
+
const events = await readRunEvents(homeDir, selectedRun.runId, { limit: 20 });
|
|
122
|
+
setSelectedRun(run ?? selectedRun);
|
|
123
|
+
setSelectedEvents(events);
|
|
124
|
+
setStatusMessage(`Cancelled ${selectedRun.runId}`);
|
|
125
|
+
} catch (cancelError) {
|
|
126
|
+
setError(cancelError instanceof Error ? cancelError.message : String(cancelError));
|
|
127
|
+
} finally {
|
|
128
|
+
setBusy(false);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
loading,
|
|
134
|
+
busy,
|
|
135
|
+
error,
|
|
136
|
+
setError,
|
|
137
|
+
dashboard,
|
|
138
|
+
diagnostics,
|
|
139
|
+
selectedRun,
|
|
140
|
+
setSelectedRun,
|
|
141
|
+
selectedEvents,
|
|
142
|
+
statusMessage,
|
|
143
|
+
launchAgentIndex,
|
|
144
|
+
setLaunchAgentIndex,
|
|
145
|
+
launchStep,
|
|
146
|
+
setLaunchStep,
|
|
147
|
+
launchDraft,
|
|
148
|
+
setLaunchDraft,
|
|
149
|
+
launchPermissionIndex,
|
|
150
|
+
setLaunchPermissionIndex,
|
|
151
|
+
launchableAgents: resolveLaunchableAgents(dashboard?.providers ?? []),
|
|
152
|
+
reload,
|
|
153
|
+
resetLaunchWizard,
|
|
154
|
+
openRunDetail,
|
|
155
|
+
handleLaunch,
|
|
156
|
+
handleCancelRun
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useStdout } from "ink";
|
|
3
|
+
import { resolveLayoutMode } from "./layout.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Live terminal size + layout mode. Updates on stdout resize.
|
|
7
|
+
*/
|
|
8
|
+
export function useTerminalSize({
|
|
9
|
+
initialColumns = 80,
|
|
10
|
+
initialRows = 24
|
|
11
|
+
} = {}) {
|
|
12
|
+
const { stdout } = useStdout();
|
|
13
|
+
const [size, setSize] = useState(() => ({
|
|
14
|
+
columns: stdout?.columns ?? initialColumns,
|
|
15
|
+
rows: stdout?.rows ?? initialRows
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!stdout || typeof stdout.on !== "function") return undefined;
|
|
20
|
+
|
|
21
|
+
const sync = () => {
|
|
22
|
+
setSize({
|
|
23
|
+
columns: stdout.columns ?? initialColumns,
|
|
24
|
+
rows: stdout.rows ?? initialRows
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
sync();
|
|
29
|
+
stdout.on("resize", sync);
|
|
30
|
+
return () => {
|
|
31
|
+
stdout.off("resize", sync);
|
|
32
|
+
};
|
|
33
|
+
}, [stdout, initialColumns, initialRows]);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
...size,
|
|
37
|
+
layoutMode: resolveLayoutMode(size)
|
|
38
|
+
};
|
|
39
|
+
}
|