@kal-elsam/kairo-runtime 0.2.3 → 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.
@@ -0,0 +1,141 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { COCKPIT_COLORS } from "./theme.js";
4
+ import { CockpitEmptyState } from "./cockpit/primitives.js";
5
+ import {
6
+ formatDiagnosticsLines,
7
+ formatLaunchWizardLines,
8
+ formatProviderLines,
9
+ formatRunDetailLines,
10
+ formatRunLines,
11
+ ORCHESTRATOR_VIEWS,
12
+ LAUNCH_WIZARD_STEPS
13
+ } from "./orchestrator-state.js";
14
+
15
+ export function HomeMissionPanel({ model, colorEnabled = true }) {
16
+ return React.createElement(Box, { flexDirection: "column" },
17
+ React.createElement(Text, {
18
+ bold: true,
19
+ color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
20
+ }, model.title),
21
+ React.createElement(Text, { bold: true }, model.recommendedTitle),
22
+ React.createElement(Text, {
23
+ color: colorEnabled ? COCKPIT_COLORS.primary : undefined
24
+ }, model.recommendedAction),
25
+ React.createElement(Text, null, ""),
26
+ React.createElement(Text, { bold: true }, model.activityTitle),
27
+ model.emptyHint
28
+ ? React.createElement(CockpitEmptyState, {
29
+ message: model.emptyHint,
30
+ hint: "Enter on Launch run when ready."
31
+ })
32
+ : model.activityLines.map((line) =>
33
+ React.createElement(Text, { key: line }, line)
34
+ ),
35
+ model.moreLine && React.createElement(Text, {
36
+ color: COCKPIT_COLORS.muted
37
+ }, model.moreLine)
38
+ );
39
+ }
40
+
41
+ export function renderCockpitView({
42
+ view,
43
+ dashboard,
44
+ diagnostics,
45
+ listIndex,
46
+ launchStep,
47
+ launchDraft,
48
+ launchAgentIndex,
49
+ launchPermissionIndex,
50
+ launchableAgents,
51
+ selectedRun,
52
+ selectedEvents,
53
+ homeMission,
54
+ colorEnabled = true
55
+ }) {
56
+ switch (view) {
57
+ case ORCHESTRATOR_VIEWS.HOME:
58
+ return React.createElement(HomeMissionPanel, { model: homeMission, colorEnabled });
59
+ case ORCHESTRATOR_VIEWS.ACTIVE_RUNS:
60
+ return listBlock(
61
+ "Active runs",
62
+ formatRunLines(dashboard?.activeRuns ?? [], { emptyMessage: "No active runs." }),
63
+ listIndex,
64
+ colorEnabled
65
+ );
66
+ case ORCHESTRATOR_VIEWS.RECENT_RUNS:
67
+ return listBlock(
68
+ "Recent runs",
69
+ formatRunLines(dashboard?.recentRuns ?? [], { emptyMessage: "No completed runs yet." }),
70
+ listIndex,
71
+ colorEnabled
72
+ );
73
+ case ORCHESTRATOR_VIEWS.PROVIDERS:
74
+ return React.createElement(Box, { flexDirection: "column" },
75
+ React.createElement(Text, { bold: true }, "Providers"),
76
+ formatProviderLines(dashboard?.providers ?? [])
77
+ .map((line) => React.createElement(Text, { key: line }, line))
78
+ );
79
+ case ORCHESTRATOR_VIEWS.LAUNCH:
80
+ if (launchableAgents.length === 0) {
81
+ return React.createElement(CockpitEmptyState, {
82
+ title: "Launch run",
83
+ message: "No launchable agents detected.",
84
+ hint: "Esc to return · check Diagnostics"
85
+ });
86
+ }
87
+ return React.createElement(Box, { flexDirection: "column" },
88
+ React.createElement(Text, { bold: true }, "Launch run"),
89
+ formatLaunchWizardLines({
90
+ step: launchStep,
91
+ draft: launchDraft,
92
+ launchableAgents,
93
+ agentIndex: launchAgentIndex,
94
+ permissionIndex: launchPermissionIndex
95
+ }).map((line) => React.createElement(Text, {
96
+ key: line,
97
+ color: line.startsWith("›") || line.startsWith(">")
98
+ ? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
99
+ : undefined
100
+ }, line))
101
+ );
102
+ case ORCHESTRATOR_VIEWS.RUN_DETAIL:
103
+ return React.createElement(Box, { flexDirection: "column" },
104
+ React.createElement(Text, { bold: true }, "Run detail"),
105
+ formatRunDetailLines(selectedRun, selectedEvents)
106
+ .map((line) => React.createElement(Text, { key: line }, line))
107
+ );
108
+ case ORCHESTRATOR_VIEWS.DIAGNOSTICS:
109
+ return React.createElement(Box, { flexDirection: "column" },
110
+ React.createElement(Text, { bold: true }, "Diagnostics"),
111
+ formatDiagnosticsLines(diagnostics)
112
+ .map((line) => React.createElement(Text, { key: line }, line))
113
+ );
114
+ case ORCHESTRATOR_VIEWS.HELP:
115
+ return React.createElement(Box, { flexDirection: "column" },
116
+ React.createElement(Text, { bold: true }, "Help"),
117
+ React.createElement(Text, null, "Kairo runtime launches and audits agent CLIs you manage."),
118
+ React.createElement(Text, null, "↑↓ navigate · Tab region · Enter open · Esc back/exit"),
119
+ React.createElement(Text, null, "R refresh · C cancel active run · ? toggle help"),
120
+ React.createElement(Text, null, "CLI: kairo run --agent <id> --task \"...\""),
121
+ React.createElement(Text, null, "Audit trail: ~/.harness/runs/<runId>/")
122
+ );
123
+ default: {
124
+ const _exhaustive = view;
125
+ return React.createElement(Text, null, `Unknown view: ${_exhaustive}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ function listBlock(title, lines, listIndex, colorEnabled) {
131
+ return React.createElement(Box, { flexDirection: "column" },
132
+ React.createElement(Text, { bold: true }, title),
133
+ lines.map((line, index) => React.createElement(Text, {
134
+ key: `${index}-${line}`,
135
+ bold: index === listIndex,
136
+ color: index === listIndex && colorEnabled ? COCKPIT_COLORS.primary : undefined
137
+ }, `${index === listIndex ? "› " : " "}${line}`))
138
+ );
139
+ }
140
+
141
+ export { LAUNCH_WIZARD_STEPS };
@@ -0,0 +1,128 @@
1
+ import ansiEscapes from "ansi-escapes";
2
+ import { stdout as defaultStdout } from "node:process";
3
+
4
+ const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
5
+
6
+ /**
7
+ * Idempotent alternate-screen session for Ink TTY flows.
8
+ * Enter once for onboarding → setup → dashboard; leave exactly once on any exit.
9
+ *
10
+ * @param {{
11
+ * stdout?: { isTTY?: boolean, write?: Function },
12
+ * ansi?: { enterAlternativeScreen: string, exitAlternativeScreen: string, cursorHide: string, cursorShow: string },
13
+ * processRef?: NodeJS.Process,
14
+ * enabled?: boolean,
15
+ * onSignal?: (signal: string) => void
16
+ * }} [options]
17
+ */
18
+ export function createFullscreenSession({
19
+ stdout = defaultStdout,
20
+ ansi = ansiEscapes,
21
+ processRef = process,
22
+ enabled = Boolean(stdout?.isTTY),
23
+ onSignal = null
24
+ } = {}) {
25
+ let active = false;
26
+ let left = false;
27
+ const signalHandlers = new Map();
28
+
29
+ function write(sequence) {
30
+ if (!stdout || typeof stdout.write !== "function") return;
31
+ try {
32
+ stdout.write(sequence);
33
+ } catch {
34
+ // Best-effort restore; never throw from leave path.
35
+ }
36
+ }
37
+
38
+ function detachSignals() {
39
+ for (const [signal, handler] of signalHandlers) {
40
+ try {
41
+ processRef.removeListener(signal, handler);
42
+ } catch {
43
+ // ignore
44
+ }
45
+ }
46
+ signalHandlers.clear();
47
+ }
48
+
49
+ function leave() {
50
+ if (!active || left) return false;
51
+ left = true;
52
+ active = false;
53
+ detachSignals();
54
+ write(ansi.cursorShow);
55
+ write(ansi.exitAlternativeScreen);
56
+ return true;
57
+ }
58
+
59
+ function defaultSignalExit(signal) {
60
+ const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 129;
61
+ try {
62
+ processRef.exit(code);
63
+ } catch {
64
+ // ignore in constrained environments
65
+ }
66
+ }
67
+
68
+ function attachSignals() {
69
+ for (const signal of SIGNALS) {
70
+ const handler = () => {
71
+ leave();
72
+ if (typeof onSignal === "function") {
73
+ onSignal(signal);
74
+ return;
75
+ }
76
+ defaultSignalExit(signal);
77
+ };
78
+ signalHandlers.set(signal, handler);
79
+ processRef.on(signal, handler);
80
+ }
81
+ }
82
+
83
+ function enter() {
84
+ if (!enabled) return false;
85
+ if (active) return false;
86
+ left = false;
87
+ active = true;
88
+ write(ansi.enterAlternativeScreen);
89
+ write(ansi.cursorHide);
90
+ attachSignals();
91
+ return true;
92
+ }
93
+
94
+ function isActive() {
95
+ return active && !left;
96
+ }
97
+
98
+ return {
99
+ enter,
100
+ leave,
101
+ isActive,
102
+ get enabled() {
103
+ return enabled;
104
+ }
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Run an async body inside a fullscreen session (enter → finally leave).
110
+ * If `sessionOrOptions` is an existing session that is already active, does not leave it.
111
+ */
112
+ export async function withFullscreenSession(sessionOrOptions, body) {
113
+ const isExisting = typeof sessionOrOptions?.enter === "function";
114
+ const session = isExisting
115
+ ? sessionOrOptions
116
+ : createFullscreenSession(sessionOrOptions);
117
+
118
+ const wasActive = session.isActive();
119
+ const didEnter = wasActive ? false : session.enter();
120
+
121
+ try {
122
+ return await body(session);
123
+ } finally {
124
+ if (didEnter) {
125
+ session.leave();
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,99 @@
1
+ import {
2
+ LAUNCH_PERMISSION_OPTIONS,
3
+ LAUNCH_WIZARD_STEPS,
4
+ retreatLaunchWizardStep
5
+ } from "./orchestrator-state.js";
6
+
7
+ /**
8
+ * Launch-wizard key handler. Returns true when the key was consumed.
9
+ */
10
+ export function handleLaunchInput(ctx) {
11
+ const {
12
+ key,
13
+ inputKey,
14
+ launchStep,
15
+ launchDraft,
16
+ launchableAgents,
17
+ launchAgentIndex,
18
+ launchPermissionIndex,
19
+ setLaunchAgentIndex,
20
+ setLaunchDraft,
21
+ setLaunchStep,
22
+ setLaunchPermissionIndex,
23
+ setError,
24
+ handleLaunch,
25
+ reload
26
+ } = ctx;
27
+
28
+ if (launchStep === LAUNCH_WIZARD_STEPS.AGENT) {
29
+ if (key.upArrow) {
30
+ setLaunchAgentIndex((index) => Math.max(0, index - 1));
31
+ return true;
32
+ }
33
+ if (key.downArrow) {
34
+ setLaunchAgentIndex((index) => Math.min(launchableAgents.length - 1, index + 1));
35
+ return true;
36
+ }
37
+ if (key.return) {
38
+ const agentId = launchableAgents[launchAgentIndex];
39
+ setLaunchDraft((draft) => ({ ...draft, agentId }));
40
+ setLaunchStep(LAUNCH_WIZARD_STEPS.TASK);
41
+ return true;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ if (launchStep === LAUNCH_WIZARD_STEPS.TASK || launchStep === LAUNCH_WIZARD_STEPS.MODEL) {
47
+ const field = launchStep === LAUNCH_WIZARD_STEPS.TASK ? "task" : "model";
48
+ if (key.return) {
49
+ if (field === "task" && !launchDraft.task.trim()) {
50
+ setError("Task cannot be empty.");
51
+ return true;
52
+ }
53
+ setLaunchStep(field === "task" ? LAUNCH_WIZARD_STEPS.MODEL : LAUNCH_WIZARD_STEPS.PERMISSIONS);
54
+ return true;
55
+ }
56
+ if (key.backspace || key.delete) {
57
+ setLaunchDraft((draft) => ({ ...draft, [field]: draft[field].slice(0, -1) }));
58
+ return true;
59
+ }
60
+ if (inputKey && inputKey.length === 1 && !key.ctrl && !key.meta) {
61
+ setLaunchDraft((draft) => ({ ...draft, [field]: `${draft[field]}${inputKey}` }));
62
+ return true;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ if (launchStep === LAUNCH_WIZARD_STEPS.PERMISSIONS) {
68
+ if (key.upArrow) {
69
+ setLaunchPermissionIndex((index) => Math.max(0, index - 1));
70
+ return true;
71
+ }
72
+ if (key.downArrow) {
73
+ setLaunchPermissionIndex((index) => Math.min(LAUNCH_PERMISSION_OPTIONS.length - 1, index + 1));
74
+ return true;
75
+ }
76
+ if (key.return) {
77
+ setLaunchStep(LAUNCH_WIZARD_STEPS.CONFIRM);
78
+ return true;
79
+ }
80
+ return true;
81
+ }
82
+
83
+ if (launchStep === LAUNCH_WIZARD_STEPS.CONFIRM) {
84
+ if (key.return) {
85
+ handleLaunch({ ...launchDraft, permissionIndex: launchPermissionIndex });
86
+ return true;
87
+ }
88
+ if (key.escape) {
89
+ setLaunchStep(retreatLaunchWizardStep(launchStep));
90
+ return true;
91
+ }
92
+ }
93
+
94
+ if (inputKey.toLowerCase() === "r") {
95
+ reload().catch(() => {});
96
+ return true;
97
+ }
98
+ return false;
99
+ }
@@ -0,0 +1,34 @@
1
+ import { LAYOUT_MODES } from "./terminal-capabilities.js";
2
+
3
+ /**
4
+ * Pure layout mode from terminal size.
5
+ * @returns {"wide"|"compact"|"minimal"|null} null when below Ink gate (<60 cols)
6
+ */
7
+ export function resolveLayoutMode({ columns = 80, rows = 24 } = {}) {
8
+ const cols = Number.isFinite(columns) && columns > 0 ? columns : 0;
9
+ const r = Number.isFinite(rows) && rows > 0 ? rows : 0;
10
+
11
+ if (cols < 60) return null;
12
+
13
+ if (cols >= 100 && r >= 28) return LAYOUT_MODES.WIDE;
14
+ if (cols >= 72 && r >= 20) return LAYOUT_MODES.COMPACT;
15
+ return LAYOUT_MODES.MINIMAL;
16
+ }
17
+
18
+ /**
19
+ * Suggested visible list limit for the active layout.
20
+ */
21
+ export function resolveListLimit(layoutMode, { contentRows = 12 } = {}) {
22
+ switch (layoutMode) {
23
+ case LAYOUT_MODES.WIDE:
24
+ return Math.max(6, Math.min(16, contentRows - 4));
25
+ case LAYOUT_MODES.COMPACT:
26
+ return Math.max(4, Math.min(10, contentRows - 4));
27
+ case LAYOUT_MODES.MINIMAL:
28
+ return Math.max(3, Math.min(6, contentRows - 2));
29
+ default:
30
+ return 4;
31
+ }
32
+ }
33
+
34
+ export { LAYOUT_MODES };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Truncate a list for a fixed viewport with an overflow indicator.
3
+ *
4
+ * @template T
5
+ * @param {T[]} items
6
+ * @param {number} limit
7
+ * @param {{ moreLabel?: string }} [options]
8
+ * @returns {{ items: T[], hiddenCount: number, hasMore: boolean, moreLine: string | null }}
9
+ */
10
+ export function windowList(items = [], limit = 8, { moreLabel = "… more" } = {}) {
11
+ const safeLimit = Math.max(0, Number(limit) || 0);
12
+ if (safeLimit === 0) {
13
+ return {
14
+ items: [],
15
+ hiddenCount: items.length,
16
+ hasMore: items.length > 0,
17
+ moreLine: items.length > 0 ? `${moreLabel} (${items.length})` : null
18
+ };
19
+ }
20
+
21
+ if (items.length <= safeLimit) {
22
+ return {
23
+ items: [...items],
24
+ hiddenCount: 0,
25
+ hasMore: false,
26
+ moreLine: null
27
+ };
28
+ }
29
+
30
+ const visible = items.slice(0, safeLimit);
31
+ const hiddenCount = items.length - safeLimit;
32
+ return {
33
+ items: visible,
34
+ hiddenCount,
35
+ hasMore: true,
36
+ moreLine: `${moreLabel} (${hiddenCount})`
37
+ };
38
+ }