@kal-elsam/kairo-runtime 0.2.3 → 0.3.1
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 +17 -3
- package/package.json +3 -1
- package/scripts/check-published-release.mjs +4 -1
- package/scripts/cockpit-smoke-test.sh +21 -0
- package/scripts/cockpit-smoke.mjs +103 -0
- package/src/cli.js +7 -5
- package/src/global/ink/cockpit/primitives.js +131 -0
- package/src/global/ink/cockpit-controller.js +168 -0
- package/src/global/ink/cockpit-focus.js +92 -0
- package/src/global/ink/cockpit-models.js +180 -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 +109 -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 +203 -457
- package/src/global/ink/run-orchestrator-ink.js +36 -16
- package/src/global/ink/run-setup-ink.js +38 -17
- package/src/global/ink/setup-app.js +14 -17
- 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 +162 -0
- package/src/global/ink/use-terminal-size.js +39 -0
- package/src/global/orchestrator.js +56 -35
- package/src/global/setup.js +4 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { LAYOUT_MODES } from "./layout.js";
|
|
2
|
+
import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
3
|
+
import { resolveDashboardRecommendation } from "../dashboard-guidance.js";
|
|
4
|
+
import { STATUS_LABELS, resolveGlyphs } from "./theme.js";
|
|
5
|
+
import { windowList } from "./list-window.js";
|
|
6
|
+
import { resolveListLimit } from "./layout.js";
|
|
7
|
+
|
|
8
|
+
export const COCKPIT_REGIONS = {
|
|
9
|
+
NAV: "nav",
|
|
10
|
+
CONTENT: "content",
|
|
11
|
+
SYSTEM: "system"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const COCKPIT_NAV = [
|
|
15
|
+
{ id: "overview", label: "Overview", view: ORCHESTRATOR_VIEWS.HOME },
|
|
16
|
+
{ id: "active", label: "Active runs", view: ORCHESTRATOR_VIEWS.ACTIVE_RUNS },
|
|
17
|
+
{ id: "recent", label: "Recent runs", view: ORCHESTRATOR_VIEWS.RECENT_RUNS },
|
|
18
|
+
{ id: "providers", label: "Providers", view: ORCHESTRATOR_VIEWS.PROVIDERS },
|
|
19
|
+
{ id: "launch", label: "Launch run", view: ORCHESTRATOR_VIEWS.LAUNCH, action: "launch" },
|
|
20
|
+
{ id: "diagnostics", label: "Diagnostics", view: ORCHESTRATOR_VIEWS.DIAGNOSTICS }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function regionsForLayout(layoutMode) {
|
|
24
|
+
if (layoutMode === LAYOUT_MODES.WIDE) {
|
|
25
|
+
return [COCKPIT_REGIONS.NAV, COCKPIT_REGIONS.CONTENT, COCKPIT_REGIONS.SYSTEM];
|
|
26
|
+
}
|
|
27
|
+
if (layoutMode === LAYOUT_MODES.COMPACT) {
|
|
28
|
+
return [COCKPIT_REGIONS.NAV, COCKPIT_REGIONS.CONTENT];
|
|
29
|
+
}
|
|
30
|
+
return [COCKPIT_REGIONS.CONTENT];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildTopBarModel({
|
|
34
|
+
projectName = "project",
|
|
35
|
+
systemOnline = true,
|
|
36
|
+
unicode = true
|
|
37
|
+
} = {}) {
|
|
38
|
+
const glyphs = resolveGlyphs(unicode);
|
|
39
|
+
const status = systemOnline ? STATUS_LABELS.online : STATUS_LABELS.offline;
|
|
40
|
+
return {
|
|
41
|
+
brand: "KAIRO",
|
|
42
|
+
status,
|
|
43
|
+
statusKind: systemOnline ? "online" : "offline",
|
|
44
|
+
projectLabel: `Project: ${projectName}`,
|
|
45
|
+
separator: glyphs.bullet
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildNavModel({
|
|
50
|
+
navIndex = 0,
|
|
51
|
+
focused = false,
|
|
52
|
+
unicode = true,
|
|
53
|
+
items = COCKPIT_NAV
|
|
54
|
+
} = {}) {
|
|
55
|
+
const glyphs = resolveGlyphs(unicode);
|
|
56
|
+
return {
|
|
57
|
+
title: "NAVIGATION",
|
|
58
|
+
items: items.map((item, index) => ({
|
|
59
|
+
...item,
|
|
60
|
+
marker: index === navIndex ? glyphs.focus : " ",
|
|
61
|
+
selected: index === navIndex,
|
|
62
|
+
focused: focused && index === navIndex
|
|
63
|
+
}))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildSystemStripModel({
|
|
68
|
+
dashboard = null,
|
|
69
|
+
diagnostics = null,
|
|
70
|
+
healthKind = "ready"
|
|
71
|
+
} = {}) {
|
|
72
|
+
const agentsDetected = diagnostics?.diagnostics?.detected
|
|
73
|
+
?? (dashboard?.providers ?? []).filter((p) => p.available).length;
|
|
74
|
+
const agentsTotal = diagnostics?.capabilities?.length
|
|
75
|
+
?? dashboard?.providers?.length
|
|
76
|
+
?? 0;
|
|
77
|
+
const activeRuns = dashboard?.activeRuns?.length ?? 0;
|
|
78
|
+
const intelligence = diagnostics?.intelligence?.summary;
|
|
79
|
+
const intelLabel = intelligence?.localAvailable
|
|
80
|
+
? STATUS_LABELS.local
|
|
81
|
+
: intelligence?.cloudAuthenticated
|
|
82
|
+
? "Cloud"
|
|
83
|
+
: "None";
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
title: "SYSTEM",
|
|
87
|
+
rows: [
|
|
88
|
+
{ key: "Agents", value: `${agentsDetected}/${agentsTotal}`, kind: agentsDetected > 0 ? "ready" : "warn" },
|
|
89
|
+
{ key: "Runs", value: String(activeRuns), kind: activeRuns > 0 ? "ready" : "muted" },
|
|
90
|
+
{ key: "Intel", value: intelLabel, kind: intelLabel === "None" ? "warn" : "ready" },
|
|
91
|
+
{ key: "Health", value: STATUS_LABELS[healthKind] ?? healthKind, kind: healthKind }
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildHomeMissionModel({
|
|
97
|
+
hasGlobalState = false,
|
|
98
|
+
diagnostics = null,
|
|
99
|
+
dashboard = null,
|
|
100
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
101
|
+
activityLines = []
|
|
102
|
+
} = {}) {
|
|
103
|
+
const recommendation = resolveDashboardRecommendation({
|
|
104
|
+
hasGlobalState,
|
|
105
|
+
diagnostics,
|
|
106
|
+
dashboard
|
|
107
|
+
});
|
|
108
|
+
const limit = resolveListLimit(layoutMode, { contentRows: 10 });
|
|
109
|
+
const windowed = windowList(activityLines, limit);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
title: "MISSION CONTROL",
|
|
113
|
+
recommendedTitle: "Recommended action",
|
|
114
|
+
recommendedAction: recommendation.message,
|
|
115
|
+
recommendedKind: recommendation.kind,
|
|
116
|
+
activityTitle: activityLines.length ? "Activity" : "Activity / empty state",
|
|
117
|
+
activityLines: windowed.items,
|
|
118
|
+
moreLine: windowed.moreLine,
|
|
119
|
+
emptyHint: activityLines.length === 0
|
|
120
|
+
? "No recent activity. Launch a run when agents are ready."
|
|
121
|
+
: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildFooterModel({
|
|
126
|
+
view = ORCHESTRATOR_VIEWS.HOME,
|
|
127
|
+
region = COCKPIT_REGIONS.NAV,
|
|
128
|
+
helpOpen = false,
|
|
129
|
+
canCancel = false,
|
|
130
|
+
unicode = true
|
|
131
|
+
} = {}) {
|
|
132
|
+
const glyphs = resolveGlyphs(unicode);
|
|
133
|
+
const parts = [];
|
|
134
|
+
|
|
135
|
+
if (helpOpen || view === ORCHESTRATOR_VIEWS.HELP) {
|
|
136
|
+
parts.push("Esc close help");
|
|
137
|
+
parts.push("? Help");
|
|
138
|
+
return { text: parts.join(` ${glyphs.bullet} `) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (view === ORCHESTRATOR_VIEWS.RUN_DETAIL) {
|
|
142
|
+
parts.push("R refresh");
|
|
143
|
+
if (canCancel) parts.push("C cancel");
|
|
144
|
+
parts.push("Esc Back");
|
|
145
|
+
return { text: parts.join(` ${glyphs.bullet} `) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
parts.push("↑↓ Navigate");
|
|
149
|
+
|
|
150
|
+
const showTab = view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
|
|
151
|
+
|| view === ORCHESTRATOR_VIEWS.RECENT_RUNS
|
|
152
|
+
|| view === ORCHESTRATOR_VIEWS.LAUNCH;
|
|
153
|
+
if (showTab) {
|
|
154
|
+
parts.push("Tab Region");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (view === ORCHESTRATOR_VIEWS.HOME
|
|
158
|
+
|| view === ORCHESTRATOR_VIEWS.PROVIDERS
|
|
159
|
+
|| view === ORCHESTRATOR_VIEWS.DIAGNOSTICS
|
|
160
|
+
|| region === COCKPIT_REGIONS.NAV
|
|
161
|
+
|| view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
|
|
162
|
+
|| view === ORCHESTRATOR_VIEWS.RECENT_RUNS) {
|
|
163
|
+
parts.push("Enter Open");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (view !== ORCHESTRATOR_VIEWS.LAUNCH) {
|
|
167
|
+
parts.push("R refresh");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
parts.push("? Help");
|
|
171
|
+
parts.push(view === ORCHESTRATOR_VIEWS.HOME ? "Esc Exit" : "Esc Back");
|
|
172
|
+
|
|
173
|
+
return { text: parts.join(` ${glyphs.bullet} `) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function resolveProjectName(workspaceRoot = "") {
|
|
177
|
+
if (!workspaceRoot) return "project";
|
|
178
|
+
const parts = String(workspaceRoot).split(/[/\\]/).filter(Boolean);
|
|
179
|
+
return parts[parts.length - 1] || "project";
|
|
180
|
+
}
|
|
@@ -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,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LAUNCH_PERMISSION_OPTIONS,
|
|
3
|
+
LAUNCH_WIZARD_STEPS,
|
|
4
|
+
retreatLaunchWizardStep
|
|
5
|
+
} from "./orchestrator-state.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Launch-wizard key handler.
|
|
9
|
+
* Returns true when consumed, "retreated" when Esc stepped back, false otherwise.
|
|
10
|
+
*/
|
|
11
|
+
export function handleLaunchInput(ctx) {
|
|
12
|
+
const {
|
|
13
|
+
key,
|
|
14
|
+
inputKey,
|
|
15
|
+
launchStep,
|
|
16
|
+
launchDraft,
|
|
17
|
+
launchableAgents,
|
|
18
|
+
launchAgentIndex,
|
|
19
|
+
launchPermissionIndex,
|
|
20
|
+
setLaunchAgentIndex,
|
|
21
|
+
setLaunchDraft,
|
|
22
|
+
setLaunchStep,
|
|
23
|
+
setLaunchPermissionIndex,
|
|
24
|
+
setError,
|
|
25
|
+
handleLaunch,
|
|
26
|
+
reload,
|
|
27
|
+
allowEscapeRetreat = false
|
|
28
|
+
} = ctx;
|
|
29
|
+
|
|
30
|
+
if (allowEscapeRetreat && key.escape) {
|
|
31
|
+
if (launchStep === LAUNCH_WIZARD_STEPS.AGENT) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
setLaunchStep(retreatLaunchWizardStep(launchStep));
|
|
35
|
+
return "retreated";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (launchStep === LAUNCH_WIZARD_STEPS.AGENT) {
|
|
39
|
+
if (key.upArrow) {
|
|
40
|
+
setLaunchAgentIndex((index) => Math.max(0, index - 1));
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (key.downArrow) {
|
|
44
|
+
setLaunchAgentIndex((index) => Math.min(launchableAgents.length - 1, index + 1));
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (key.return) {
|
|
48
|
+
const agentId = launchableAgents[launchAgentIndex];
|
|
49
|
+
setLaunchDraft((draft) => ({ ...draft, agentId }));
|
|
50
|
+
setLaunchStep(LAUNCH_WIZARD_STEPS.TASK);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (launchStep === LAUNCH_WIZARD_STEPS.TASK || launchStep === LAUNCH_WIZARD_STEPS.MODEL) {
|
|
57
|
+
const field = launchStep === LAUNCH_WIZARD_STEPS.TASK ? "task" : "model";
|
|
58
|
+
if (key.return) {
|
|
59
|
+
if (field === "task" && !launchDraft.task.trim()) {
|
|
60
|
+
setError("Task cannot be empty.");
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
setLaunchStep(field === "task" ? LAUNCH_WIZARD_STEPS.MODEL : LAUNCH_WIZARD_STEPS.PERMISSIONS);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (key.backspace || key.delete) {
|
|
67
|
+
setLaunchDraft((draft) => ({ ...draft, [field]: draft[field].slice(0, -1) }));
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (inputKey && inputKey.length === 1 && !key.ctrl && !key.meta) {
|
|
71
|
+
setLaunchDraft((draft) => ({ ...draft, [field]: `${draft[field]}${inputKey}` }));
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (launchStep === LAUNCH_WIZARD_STEPS.PERMISSIONS) {
|
|
78
|
+
if (key.upArrow) {
|
|
79
|
+
setLaunchPermissionIndex((index) => Math.max(0, index - 1));
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (key.downArrow) {
|
|
83
|
+
setLaunchPermissionIndex((index) => Math.min(LAUNCH_PERMISSION_OPTIONS.length - 1, index + 1));
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (key.return) {
|
|
87
|
+
setLaunchStep(LAUNCH_WIZARD_STEPS.CONFIRM);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (launchStep === LAUNCH_WIZARD_STEPS.CONFIRM) {
|
|
94
|
+
if (key.return) {
|
|
95
|
+
handleLaunch({ ...launchDraft, permissionIndex: launchPermissionIndex });
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (key.escape) {
|
|
99
|
+
setLaunchStep(retreatLaunchWizardStep(launchStep));
|
|
100
|
+
return allowEscapeRetreat ? "retreated" : true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (inputKey.toLowerCase() === "r") {
|
|
105
|
+
reload().catch(() => {});
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
@@ -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
|
+
}
|