@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.
- package/README.md +14 -8
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +3 -3
- package/scripts/ux-prototype-tty.mjs +9 -0
- package/src/global/control-plane-proposals.js +2 -1
- package/src/global/control-plane-snapshot.js +1 -1
- package/src/global/ink/cockpit/primitives.js +47 -35
- package/src/global/ink/cockpit-changes.js +2 -2
- package/src/global/ink/cockpit-control-center.js +12 -64
- package/src/global/ink/cockpit-controller.js +41 -5
- package/src/global/ink/cockpit-enter.js +1 -0
- package/src/global/ink/cockpit-models.js +14 -7
- package/src/global/ink/cockpit-palette.js +18 -7
- package/src/global/ink/cockpit-recovery.js +9 -6
- package/src/global/ink/cockpit-usage.js +111 -0
- package/src/global/ink/cockpit-views.js +69 -97
- package/src/global/ink/orchestrator-app.js +32 -0
- package/src/global/ink/setup-app.js +55 -72
- package/src/global/ink/setup-state.js +16 -0
- package/src/global/ink/theme.js +27 -0
- package/src/global/ink/ux/live-activity.js +194 -0
- package/src/global/ink/ux/live-alerts.js +159 -0
- package/src/global/ink/ux/live-governance.js +195 -0
- package/src/global/ink/ux/live-orchestration.js +188 -0
- package/src/global/ink/ux/live-overview.js +125 -0
- package/src/global/ink/ux/live-settings.js +189 -0
- package/src/global/ink/ux/live-setup.js +160 -0
- package/src/global/ink/ux/live-usage.js +51 -0
- package/src/global/ink/ux/semantic.js +84 -0
- package/src/global/ink/ux/task-flow-app.js +85 -0
- package/src/global/ink/ux/task-flow.js +173 -0
- 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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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();
|