@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.
@@ -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
- renderImpl = render
12
+ hasGlobalState = false,
13
+ renderImpl = render,
14
+ fullscreenSession = null,
15
+ stdout = process.stdout
12
16
  }) {
13
- return new Promise((resolve) => {
14
- const { waitUntilExit } = renderImpl(
15
- React.createElement(OrchestratorApp, {
16
- homeDir,
17
- workspaceRoot,
18
- packageRoot,
19
- packageName,
20
- cliVersion,
21
- onComplete: resolve
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
- waitUntilExit().catch((error) => {
26
- resolve({ cancelled: true, error });
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 outcome = await new Promise((resolve) => {
28
- const { waitUntilExit } = renderImpl(
29
- React.createElement(SetupApp, {
30
- homeDir,
31
- workspaceRoot,
32
- packageRoot,
33
- packageName,
34
- cliVersion,
35
- dryRun,
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
- waitUntilExit().catch((error) => {
41
- resolve({ cancelled: true, usedWizard: true, error });
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: "cyan",
27
- success: "green",
28
- warning: "yellow",
29
- danger: "red",
30
- muted: "gray"
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: INK_COLORS.muted }, lines[1]),
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(Box, {
44
- flexDirection: "column",
45
- borderStyle: "round",
46
- borderColor: INK_COLORS.accent,
47
- paddingX: 1,
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, { compact: useCompactSplash }),
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 { AGENT_HINTS, BRAND, formatCliCommand, getAgentLabel, PREFERRED_CLI, WIZARD_COPY } from "../brand/index.js";
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
- forceInk = process.env.HARNESS_INK !== "0"
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
- if (!interactive || !forceInk) return false;
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
+ }