@kal-elsam/kairo-runtime 0.9.0 → 0.10.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.
Files changed (32) hide show
  1. package/README.md +14 -8
  2. package/package.json +1 -1
  3. package/scripts/cockpit-smoke.mjs +3 -3
  4. package/scripts/ux-prototype-tty.mjs +9 -0
  5. package/src/global/control-plane-proposals.js +2 -1
  6. package/src/global/control-plane-snapshot.js +1 -1
  7. package/src/global/ink/cockpit/primitives.js +47 -35
  8. package/src/global/ink/cockpit-changes.js +2 -2
  9. package/src/global/ink/cockpit-control-center.js +12 -64
  10. package/src/global/ink/cockpit-controller.js +41 -5
  11. package/src/global/ink/cockpit-enter.js +1 -0
  12. package/src/global/ink/cockpit-models.js +14 -7
  13. package/src/global/ink/cockpit-palette.js +18 -7
  14. package/src/global/ink/cockpit-recovery.js +9 -6
  15. package/src/global/ink/cockpit-usage.js +111 -0
  16. package/src/global/ink/cockpit-views.js +69 -97
  17. package/src/global/ink/orchestrator-app.js +32 -0
  18. package/src/global/ink/setup-app.js +55 -72
  19. package/src/global/ink/setup-state.js +16 -0
  20. package/src/global/ink/theme.js +27 -0
  21. package/src/global/ink/ux/live-activity.js +194 -0
  22. package/src/global/ink/ux/live-alerts.js +159 -0
  23. package/src/global/ink/ux/live-governance.js +195 -0
  24. package/src/global/ink/ux/live-orchestration.js +188 -0
  25. package/src/global/ink/ux/live-overview.js +125 -0
  26. package/src/global/ink/ux/live-settings.js +189 -0
  27. package/src/global/ink/ux/live-setup.js +160 -0
  28. package/src/global/ink/ux/live-usage.js +51 -0
  29. package/src/global/ink/ux/semantic.js +84 -0
  30. package/src/global/ink/ux/task-flow-app.js +85 -0
  31. package/src/global/ink/ux/task-flow.js +173 -0
  32. package/src/global/orchestrator.js +37 -20
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Live semantic Usage panel — renderer only; model lives in cockpit-usage.js.
3
+ * ActionList never owns focus.
4
+ */
5
+ import React from "react";
6
+ import { Box, Text } from "ink";
7
+ import { COCKPIT_COLORS } from "../theme.js";
8
+ import { LAYOUT_MODES } from "../layout.js";
9
+ import { adaptUsageModel } from "../cockpit-usage.js";
10
+ import { ActionList, Callout } from "./semantic.js";
11
+
12
+ export function SemanticUsagePanel({
13
+ snapshot = null,
14
+ dashboard = null,
15
+ layoutMode = LAYOUT_MODES.COMPACT,
16
+ colorEnabled = true,
17
+ unicode = true
18
+ }) {
19
+ const model = adaptUsageModel({ snapshot, dashboard, layoutMode });
20
+ const muted = colorEnabled ? COCKPIT_COLORS.muted : undefined;
21
+ return React.createElement(Box, { flexDirection: "column" },
22
+ React.createElement(Text, {
23
+ bold: true, color: colorEnabled ? COCKPIT_COLORS.secondary : undefined
24
+ }, model.title),
25
+ React.createElement(Callout, {
26
+ tone: model.callout.tone,
27
+ title: model.callout.title,
28
+ body: model.callout.body || undefined,
29
+ colorEnabled,
30
+ compact: true
31
+ }),
32
+ React.createElement(Text, { bold: true }, "Measured"),
33
+ React.createElement(Text, null, ` ${model.measured}`),
34
+ React.createElement(Text, { bold: true }, "Configured limits"),
35
+ React.createElement(Text, null, ` ${model.configured}`),
36
+ React.createElement(Text, { bold: true }, "Run usage"),
37
+ model.runs.length === 0
38
+ ? React.createElement(Text, null, " No auditable run tokenUsage yet.")
39
+ : React.createElement(ActionList, {
40
+ items: model.runs,
41
+ selectedIndex: -1,
42
+ focused: false,
43
+ colorEnabled,
44
+ unicode
45
+ }),
46
+ model.moreLine
47
+ ? React.createElement(Text, { color: muted }, model.moreLine)
48
+ : null,
49
+ React.createElement(Text, { color: muted }, model.footnote)
50
+ );
51
+ }
@@ -0,0 +1,84 @@
1
+ /** Semantic Ink primitives — structured models, not string-array panels. */
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { COCKPIT_COLORS, statusColor, resolveGlyphs } from "../theme.js";
5
+
6
+ const mute = (on) => (on ? COCKPIT_COLORS.muted : undefined);
7
+
8
+ export function ActionList({ items = [], selectedIndex = 0, focused = true, colorEnabled = true, unicode = true }) {
9
+ const g = resolveGlyphs(unicode);
10
+ return React.createElement(Box, { flexDirection: "column" },
11
+ ...items.map((item, i) => {
12
+ const sel = i === selectedIndex;
13
+ return React.createElement(Text, {
14
+ key: item.id ?? String(i),
15
+ bold: sel,
16
+ color: sel && focused && colorEnabled ? COCKPIT_COLORS.primary : undefined
17
+ }, `${sel && focused ? g.focus : " "} ${item.label}${item.hint ? ` ${item.hint}` : ""}`);
18
+ })
19
+ );
20
+ }
21
+
22
+ export function Stepper({ steps = [], currentIndex = 0, colorEnabled = true, unicode = true }) {
23
+ // Progress marks: not focus `>` and not failure-looking `x`.
24
+ const done = unicode ? "✓" : "+";
25
+ const current = unicode ? "●" : "*";
26
+ const idle = unicode ? "·" : "-";
27
+ return React.createElement(Box, { flexDirection: "column" },
28
+ ...steps.map((step, i) => {
29
+ const isDone = i < currentIndex;
30
+ const cur = i === currentIndex;
31
+ const color = isDone
32
+ ? (colorEnabled ? COCKPIT_COLORS.success : undefined)
33
+ : cur ? (colorEnabled ? COCKPIT_COLORS.primary : undefined) : mute(colorEnabled);
34
+ return React.createElement(Text, {
35
+ key: step.id ?? String(i), bold: cur, color
36
+ }, `${isDone ? done : cur ? current : idle} ${i + 1}. ${step.label}`);
37
+ })
38
+ );
39
+ }
40
+
41
+ export function Callout({ tone = "info", title, body, colorEnabled = true, compact = false }) {
42
+ const kind = tone === "danger" ? "danger" : tone === "warn" ? "warn" : "ready";
43
+ return React.createElement(Box, {
44
+ flexDirection: "column",
45
+ marginY: compact ? 0 : 1
46
+ },
47
+ React.createElement(Text, { bold: true, color: statusColor(kind, { colorEnabled }) }, title),
48
+ body ? React.createElement(Text, null, body) : null
49
+ );
50
+ }
51
+
52
+ export function Confirm({ summary, primaryLabel = "Confirm", focused = true, colorEnabled = true, mark = " " }) {
53
+ return React.createElement(Box, { flexDirection: "column" },
54
+ summary ? React.createElement(Text, null, summary) : null,
55
+ React.createElement(Text, {
56
+ bold: focused,
57
+ color: focused && colorEnabled ? COCKPIT_COLORS.primary : undefined
58
+ }, `${mark} ${primaryLabel}`)
59
+ );
60
+ }
61
+
62
+ export function Receipt({ title = "Receipt", lines = [], colorEnabled = true }) {
63
+ return React.createElement(Box, { flexDirection: "column" },
64
+ React.createElement(Text, { bold: true, color: colorEnabled ? COCKPIT_COLORS.success : undefined }, title),
65
+ ...lines.map((line, i) => React.createElement(Text, { key: `r${i}`, color: mute(colorEnabled) }, line))
66
+ );
67
+ }
68
+
69
+ export function Details({ open = false, summary = "Details", lines = [], colorEnabled = true, focused = false, mark = " " }) {
70
+ const color = focused && colorEnabled ? COCKPIT_COLORS.primary : mute(colorEnabled);
71
+ if (!open) return React.createElement(Text, { bold: focused, color }, `${mark} ${summary} · Space`);
72
+ return React.createElement(Box, { flexDirection: "column" },
73
+ React.createElement(Text, { bold: true, color }, `${mark} ${summary}`),
74
+ ...lines.map((line, i) => React.createElement(Text, { key: `d${i}`, color: mute(colorEnabled) }, line))
75
+ );
76
+ }
77
+
78
+ export function KeyBar({ hints = [], colorEnabled = true, columns = 80 }) {
79
+ const width = Math.max(24, Math.min(Number(columns) || 80, 120));
80
+ return React.createElement(Box, { width },
81
+ React.createElement(Text, { color: mute(colorEnabled) },
82
+ hints.map((h) => `${h.keys} ${h.label}`).join(" · "))
83
+ );
84
+ }
@@ -0,0 +1,85 @@
1
+ import React, { useEffect, useReducer } from "react";
2
+ import { Box, Text, useApp, useInput, useStdout } from "ink";
3
+ import { COCKPIT_COLORS, resolveGlyphs } from "../theme.js";
4
+ import { resolveTerminalCapabilities } from "../terminal-capabilities.js";
5
+ import { ActionList, Callout, Confirm, Details, KeyBar, Receipt, Stepper } from "./semantic.js";
6
+ import {
7
+ FOCUS, SCREENS, createTaskFlowState, keyHintsFor, modelForState, reduceTaskFlow, resolvePrimaryPresentation
8
+ } from "./task-flow.js";
9
+
10
+ export function TaskFlowApp({ columns: cols, rows: rowCount, onExit } = {}) {
11
+ const { exit } = useApp();
12
+ const { stdout } = useStdout();
13
+ const [state, dispatch] = useReducer(
14
+ reduceTaskFlow,
15
+ createTaskFlowState({ columns: cols ?? stdout?.columns ?? 80, rows: rowCount ?? stdout?.rows ?? 24 })
16
+ );
17
+ const caps = resolveTerminalCapabilities({
18
+ columns: state.columns, rows: state.rows, env: process.env, isTTY: true
19
+ });
20
+ const model = modelForState(state);
21
+ const primary = resolvePrimaryPresentation(model, state, caps.unicode);
22
+ const detailsMark = state.focus === FOCUS.DETAILS ? resolveGlyphs(caps.unicode).focus : " ";
23
+ const accent = caps.color ? COCKPIT_COLORS.primary : undefined;
24
+
25
+ useEffect(() => {
26
+ if (!stdout) return undefined;
27
+ const onResize = () => dispatch({ type: "resize", columns: stdout.columns ?? 80, rows: stdout.rows ?? 24 });
28
+ stdout.on("resize", onResize);
29
+ return () => stdout.off("resize", onResize);
30
+ }, [stdout]);
31
+
32
+ useEffect(() => {
33
+ if (!state.exited) return;
34
+ onExit?.({ reason: "escape" });
35
+ exit();
36
+ }, [state.exited, exit, onExit]);
37
+
38
+ useInput((input, key) => {
39
+ if (key.upArrow) dispatch({ type: "up" });
40
+ else if (key.downArrow) dispatch({ type: "down" });
41
+ else if (key.return) dispatch({ type: "enter" });
42
+ else if (key.escape) dispatch({ type: "escape" });
43
+ else if (input === " ") dispatch({ type: "space" });
44
+ else if (input === "/") dispatch({ type: "slash" });
45
+ });
46
+
47
+ return React.createElement(Box, { flexDirection: "column", width: state.columns },
48
+ React.createElement(Text, { bold: true, color: accent }, `Kairo · ${model.title} · ${state.layout}`),
49
+ React.createElement(Callout, { ...model.callout, colorEnabled: caps.color }),
50
+ state.screen === SCREENS.SETUP
51
+ ? React.createElement(Stepper, {
52
+ steps: model.steps, currentIndex: model.stepIndex, colorEnabled: caps.color, unicode: caps.unicode
53
+ })
54
+ : null,
55
+ primary.mode === "confirm"
56
+ ? React.createElement(Confirm, {
57
+ summary: primary.summary, primaryLabel: primary.label,
58
+ focused: state.focus === FOCUS.PRIMARY, colorEnabled: caps.color, mark: primary.mark
59
+ })
60
+ : React.createElement(Box, { flexDirection: "column" },
61
+ React.createElement(Text, {
62
+ bold: state.focus === FOCUS.PRIMARY,
63
+ color: state.focus === FOCUS.PRIMARY ? accent : undefined
64
+ }, `${primary.mark} ${primary.label}`),
65
+ primary.detail
66
+ ? React.createElement(Text, { color: caps.color ? COCKPIT_COLORS.muted : undefined }, primary.detail)
67
+ : null
68
+ ),
69
+ model.receipt ? React.createElement(Receipt, { ...model.receipt, colorEnabled: caps.color }) : null,
70
+ (model.secondary || model.metrics)
71
+ ? React.createElement(ActionList, {
72
+ items: model.secondary ?? model.metrics,
73
+ selectedIndex: state.listIndex,
74
+ focused: state.focus === FOCUS.LIST,
75
+ colorEnabled: caps.color,
76
+ unicode: caps.unicode
77
+ })
78
+ : null,
79
+ React.createElement(Details, {
80
+ open: state.detailsOpen, summary: "Details", lines: model.details,
81
+ colorEnabled: caps.color, focused: state.focus === FOCUS.DETAILS, mark: detailsMark
82
+ }),
83
+ React.createElement(KeyBar, { hints: keyHintsFor(state), colorEnabled: caps.color, columns: state.columns })
84
+ );
85
+ }
@@ -0,0 +1,173 @@
1
+ /** Pure task-flow prototype: Home → Setup → Overview. Mock only — no writes. */
2
+
3
+ export const SCREENS = { HOME: "home", SETUP: "setup", OVERVIEW: "overview" };
4
+ export const FOCUS = { PRIMARY: "primary", LIST: "list", DETAILS: "details" };
5
+ export const SETUP_STEPS = [
6
+ { id: "detect", label: "Detect agents" },
7
+ { id: "select", label: "Select governance" },
8
+ { id: "review", label: "Review changes" },
9
+ { id: "confirm", label: "Confirm" },
10
+ { id: "receipt", label: "Receipt" }
11
+ ];
12
+
13
+ export function resolveLayout(c = 80, r = 24) {
14
+ if (c >= 120 && r >= 40) return "wide";
15
+ if (c < 80 || r < 24) return "minimal";
16
+ return "compact";
17
+ }
18
+
19
+ export function createTaskFlowState(init = {}) {
20
+ const columns = init.columns ?? 80;
21
+ const rows = init.rows ?? 24;
22
+ return {
23
+ screen: init.screen ?? SCREENS.HOME,
24
+ setupStep: init.setupStep ?? 0,
25
+ listIndex: init.listIndex ?? 0,
26
+ focus: init.focus ?? FOCUS.PRIMARY,
27
+ detailsOpen: Boolean(init.detailsOpen),
28
+ columns,
29
+ rows,
30
+ exited: Boolean(init.exited),
31
+ layout: resolveLayout(columns, rows)
32
+ };
33
+ }
34
+
35
+ export function buildHomeModel() {
36
+ return {
37
+ title: "Home",
38
+ callout: { tone: "warn", title: "Needs attention", body: "2 open alerts · monitor idle · 1 active run" },
39
+ primary: { id: "start-setup", label: "Start setup · Cursor drift", detail: "Opens guided setup. Stale hooks detected." },
40
+ secondary: [
41
+ { id: "overview", label: "Open overview" },
42
+ { id: "details", label: "Show details", hint: "paths · ids" }
43
+ ],
44
+ details: ["project: agentic-harness", "path hidden until Details"]
45
+ };
46
+ }
47
+
48
+ export function buildSetupModel(stepIndex = 0) {
49
+ const step = SETUP_STEPS[stepIndex] ?? SETUP_STEPS[0];
50
+ const isConfirm = step.id === "confirm";
51
+ const isReceipt = step.id === "receipt";
52
+ return {
53
+ title: "Setup",
54
+ steps: SETUP_STEPS,
55
+ stepIndex,
56
+ callout: {
57
+ tone: "info",
58
+ title: step.label,
59
+ body: isReceipt ? "Prototype receipt — no files written." : "Enter advances · Esc leaves setup."
60
+ },
61
+ primary: { label: isReceipt ? "Open overview" : isConfirm ? "Confirm (no write)" : "Continue" },
62
+ confirm: isConfirm ? { summary: "Apply governance preview to Cursor hooks.", primaryLabel: "Confirm (no write)" } : null,
63
+ receipt: isReceipt ? { title: "Receipt", lines: ["status: simulated", "wrote: none", "planId: proto-1"] } : null,
64
+ details: [`step: ${step.id}`, "write: disabled"]
65
+ };
66
+ }
67
+
68
+ export function buildOverviewModel() {
69
+ return {
70
+ title: "Overview",
71
+ callout: { tone: "warn", title: "Attention first", body: "2 items need a decision before secondary metrics." },
72
+ primary: { label: "Back to home priority" },
73
+ metrics: [
74
+ { id: "agents", label: "Protected agents · Cursor · Codex · Pi" },
75
+ { id: "monitor", label: "Monitor · Disabled" },
76
+ { id: "runs", label: "Active runs · 1" },
77
+ { id: "usage", label: "Usage · Pi evidence only" }
78
+ ],
79
+ details: ["alert: alt-1", "run: run-1"]
80
+ };
81
+ }
82
+
83
+ export function modelForState(state) {
84
+ if (state.screen === SCREENS.SETUP) return buildSetupModel(state.setupStep);
85
+ if (state.screen === SCREENS.OVERVIEW) return buildOverviewModel();
86
+ return buildHomeModel();
87
+ }
88
+
89
+ /** Focus mark only when PRIMARY — list/details own their own markers. */
90
+ export function focusMarkFor(state, unicode = true) {
91
+ return state.focus === FOCUS.PRIMARY ? (unicode ? "›" : ">") : " ";
92
+ }
93
+
94
+ /** Exactly one primary surface; confirm steps own the action (no duplicate line). */
95
+ export function resolvePrimaryPresentation(model, state, unicode = true) {
96
+ const mark = focusMarkFor(state, unicode);
97
+ if (model.confirm) {
98
+ return { mode: "confirm", summary: model.confirm.summary, label: model.confirm.primaryLabel, mark };
99
+ }
100
+ return { mode: "primary", label: model.primary.label, detail: model.primary.detail ?? null, mark };
101
+ }
102
+
103
+ export function keyHintsFor(state) {
104
+ const hints = [
105
+ { keys: "↑↓", label: "Move" },
106
+ { keys: "Enter", label: "Primary" },
107
+ { keys: "Space", label: "Details" },
108
+ { keys: "/", label: "Home" },
109
+ { keys: "Esc", label: state.screen === SCREENS.SETUP ? "Home" : "Exit" }
110
+ ];
111
+ return state.layout === "minimal" ? hints.slice(0, 3) : hints;
112
+ }
113
+
114
+ export function reduceTaskFlow(state, event) {
115
+ switch (event.type) {
116
+ case "resize": {
117
+ const columns = event.columns ?? state.columns;
118
+ const rows = event.rows ?? state.rows;
119
+ return { ...state, columns, rows, layout: resolveLayout(columns, rows) };
120
+ }
121
+ case "up": return moveList(state, -1);
122
+ case "down": return moveList(state, 1);
123
+ case "space": return { ...state, detailsOpen: !state.detailsOpen, focus: FOCUS.DETAILS };
124
+ case "slash":
125
+ return { ...state, screen: SCREENS.HOME, focus: FOCUS.PRIMARY, listIndex: 0, detailsOpen: false };
126
+ case "escape":
127
+ if (state.detailsOpen) return { ...state, detailsOpen: false, focus: FOCUS.PRIMARY };
128
+ if (state.screen === SCREENS.SETUP) {
129
+ return { ...state, screen: SCREENS.HOME, focus: FOCUS.PRIMARY, listIndex: 0 };
130
+ }
131
+ return { ...state, exited: true };
132
+ case "enter": return activate(state);
133
+ default: return state;
134
+ }
135
+ }
136
+
137
+ function listLength(state) {
138
+ if (state.screen === SCREENS.HOME) return 2;
139
+ if (state.screen === SCREENS.OVERVIEW) return 4;
140
+ return 0;
141
+ }
142
+
143
+ function moveList(state, delta) {
144
+ const len = listLength(state);
145
+ if (len <= 0) return { ...state, focus: FOCUS.PRIMARY };
146
+ if (state.focus !== FOCUS.LIST) {
147
+ return { ...state, listIndex: delta > 0 ? 0 : len - 1, focus: FOCUS.LIST };
148
+ }
149
+ return { ...state, listIndex: (state.listIndex + delta + len) % len, focus: FOCUS.LIST };
150
+ }
151
+
152
+ function activate(state) {
153
+ if (state.screen === SCREENS.HOME) {
154
+ if (state.focus === FOCUS.LIST) {
155
+ const id = buildHomeModel().secondary[state.listIndex]?.id;
156
+ if (id === "overview") {
157
+ return { ...state, screen: SCREENS.OVERVIEW, focus: FOCUS.PRIMARY, listIndex: 0, detailsOpen: false };
158
+ }
159
+ if (id === "details") return { ...state, detailsOpen: !state.detailsOpen, focus: FOCUS.DETAILS };
160
+ }
161
+ return { ...state, screen: SCREENS.SETUP, setupStep: 0, focus: FOCUS.PRIMARY, listIndex: 0, detailsOpen: false };
162
+ }
163
+ if (state.screen === SCREENS.SETUP) {
164
+ if (state.setupStep >= SETUP_STEPS.length - 1) {
165
+ return { ...state, screen: SCREENS.OVERVIEW, focus: FOCUS.PRIMARY, listIndex: 0, detailsOpen: false };
166
+ }
167
+ return { ...state, setupStep: state.setupStep + 1, focus: FOCUS.PRIMARY, detailsOpen: false };
168
+ }
169
+ if (state.screen === SCREENS.OVERVIEW) {
170
+ return { ...state, screen: SCREENS.HOME, focus: FOCUS.PRIMARY, listIndex: 0, detailsOpen: false };
171
+ }
172
+ return state;
173
+ }
@@ -85,27 +85,44 @@ export async function runOrchestratorShell({
85
85
  }
86
86
  }
87
87
 
88
- const outcome = await runOrchestratorInkImpl({
89
- homeDir,
90
- workspaceRoot,
91
- packageRoot,
92
- packageName: packageManifest.name,
93
- cliVersion: packageManifest.version,
94
- hasGlobalState: hasConfiguredGlobalState(homeDir),
95
- fullscreenSession: session
96
- });
97
-
98
- if (outcome.error) {
99
- throw outcome.error;
100
- }
88
+ for (;;) {
89
+ const outcome = await runOrchestratorInkImpl({
90
+ homeDir,
91
+ workspaceRoot,
92
+ packageRoot,
93
+ packageName: packageManifest.name,
94
+ cliVersion: packageManifest.version,
95
+ hasGlobalState: hasConfiguredGlobalState(homeDir),
96
+ fullscreenSession: session
97
+ });
98
+
99
+ if (outcome.error) {
100
+ throw outcome.error;
101
+ }
101
102
 
102
- return {
103
- cancelled: Boolean(outcome.cancelled),
104
- wrote: Boolean(setupOutcome && !setupOutcome.cancelled),
105
- action: outcome.action ?? null,
106
- initialMode,
107
- setup: setupOutcome
108
- };
103
+ if (outcome?.action === "setup") {
104
+ const mid = await runHarnessSetupImpl({
105
+ packageRoot,
106
+ packageName: packageManifest.name,
107
+ cliVersion: packageManifest.version,
108
+ homeDir,
109
+ workspaceRoot,
110
+ onboarding: false,
111
+ interactive: true,
112
+ fullscreenSession: session
113
+ });
114
+ if (!mid?.cancelled) setupOutcome = mid;
115
+ continue;
116
+ }
117
+
118
+ return {
119
+ cancelled: Boolean(outcome.cancelled),
120
+ wrote: Boolean(setupOutcome && !setupOutcome.cancelled),
121
+ action: outcome.action ?? null,
122
+ initialMode,
123
+ setup: setupOutcome
124
+ };
125
+ }
109
126
  } finally {
110
127
  if (ownsSession) {
111
128
  session.leave();