@kal-elsam/kairo-runtime 0.9.0 → 0.11.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/brand/wordmark.js +50 -0
- package/src/global/ink/cockpit/primitives.js +72 -50
- 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 +70 -97
- package/src/global/ink/orchestrator-app.js +44 -4
- package/src/global/ink/setup-app.js +55 -72
- package/src/global/ink/setup-state.js +16 -0
- package/src/global/ink/theme.js +40 -8
- package/src/global/ink/ux/live-activity.js +191 -0
- package/src/global/ink/ux/live-alerts.js +156 -0
- package/src/global/ink/ux/live-governance.js +191 -0
- package/src/global/ink/ux/live-orchestration.js +185 -0
- package/src/global/ink/ux/live-overview.js +175 -0
- package/src/global/ink/ux/live-settings.js +186 -0
- package/src/global/ink/ux/live-setup.js +160 -0
- package/src/global/ink/ux/live-usage.js +49 -0
- package/src/global/ink/ux/semantic.js +101 -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import { stdout as output } from "node:process";
|
|
4
|
-
import { BRAND
|
|
4
|
+
import { BRAND } from "../brand/index.js";
|
|
5
5
|
import { DEFAULT_COMPONENT_IDS, describeComponentCatalog } from "../component-registry.js";
|
|
6
6
|
import { GLOBAL_AGENT_IDS, detectInstalledAdapters, listAdapters } from "../registry.js";
|
|
7
7
|
import { buildSetupPreview, resolveComponentSelection } from "../clack/setup-preview.js";
|
|
@@ -9,72 +9,57 @@ import {
|
|
|
9
9
|
SETUP_STEPS,
|
|
10
10
|
buildAgentOptions,
|
|
11
11
|
buildComponentOptions,
|
|
12
|
-
formatInkDetectPanel,
|
|
13
12
|
formatInkHeaderLines,
|
|
14
|
-
formatInkPreviewLines,
|
|
15
|
-
formatInkSelectList,
|
|
16
13
|
formatInkSplashLines,
|
|
17
14
|
INITIAL_SETUP_STEP,
|
|
18
15
|
shouldStartPreviewLoad,
|
|
19
16
|
shouldUseCompactSplashLogo,
|
|
17
|
+
setupLineKey,
|
|
20
18
|
toggleComponentSelection,
|
|
21
19
|
toggleSelection,
|
|
22
20
|
transitionFromSplash
|
|
23
21
|
} from "./setup-state.js";
|
|
24
|
-
import { COCKPIT_COLORS } from "./theme.js";
|
|
25
|
-
import {
|
|
22
|
+
import { COCKPIT_COLORS, resolveInkColor } from "./theme.js";
|
|
23
|
+
import { useTerminalSize } from "./use-terminal-size.js";
|
|
24
|
+
import { resolveTerminalCapabilities } from "./terminal-capabilities.js";
|
|
25
|
+
import { SemanticSetupPanel } from "./ux/live-setup.js";
|
|
26
26
|
|
|
27
|
-
|
|
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
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function Header() {
|
|
27
|
+
export function SetupHeader({ colorEnabled = true }) {
|
|
36
28
|
const lines = formatInkHeaderLines();
|
|
37
29
|
return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
38
|
-
React.createElement(Text, {
|
|
39
|
-
|
|
30
|
+
React.createElement(Text, {
|
|
31
|
+
bold: true,
|
|
32
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
|
|
33
|
+
}, `╭─ ${lines[0]}`),
|
|
34
|
+
React.createElement(Text, {
|
|
35
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
|
|
36
|
+
}, lines[1]),
|
|
40
37
|
React.createElement(Text, { dimColor: true }, lines[2])
|
|
41
38
|
);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
function
|
|
45
|
-
return React.createElement(CockpitPanel, {
|
|
46
|
-
title,
|
|
47
|
-
focused: true,
|
|
48
|
-
width: "100%"
|
|
49
|
-
}, children);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function Footer({ children }) {
|
|
53
|
-
return React.createElement(Text, { dimColor: true }, children);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function Splash({ compact, onboarding = false }) {
|
|
41
|
+
export function SetupSplash({ compact, onboarding = false, colorEnabled = true }) {
|
|
57
42
|
const lines = formatInkSplashLines({ compact, onboarding });
|
|
58
43
|
const logoLineCount = compact ? BRAND.compactLogo.length : BRAND.asciiLogo.length;
|
|
44
|
+
const accent = resolveInkColor(colorEnabled, COCKPIT_COLORS.primary);
|
|
45
|
+
const muted = resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
|
|
59
46
|
|
|
60
47
|
return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
61
48
|
lines.map((line, index) => {
|
|
49
|
+
const key = setupLineKey(index, line);
|
|
62
50
|
if (index < logoLineCount) {
|
|
63
|
-
return React.createElement(Text, { key
|
|
51
|
+
return React.createElement(Text, { key, bold: true, color: accent }, line);
|
|
64
52
|
}
|
|
65
53
|
if (line === BRAND.name) {
|
|
66
|
-
return React.createElement(Text, { key
|
|
54
|
+
return React.createElement(Text, { key, bold: true, color: accent }, line);
|
|
67
55
|
}
|
|
68
56
|
if (line === BRAND.tagline) {
|
|
69
|
-
return React.createElement(Text, { key
|
|
57
|
+
return React.createElement(Text, { key, color: muted }, line);
|
|
70
58
|
}
|
|
71
59
|
if (line === BRAND.splashHint || line.includes("Esc to exit") || line.includes("Press Enter")) {
|
|
72
|
-
return React.createElement(Text, { key
|
|
73
|
-
}
|
|
74
|
-
if (line === "") {
|
|
75
|
-
return React.createElement(Text, { key: `line-${index}` }, "");
|
|
60
|
+
return React.createElement(Text, { key, dimColor: true }, line);
|
|
76
61
|
}
|
|
77
|
-
return React.createElement(Text, { key
|
|
62
|
+
return React.createElement(Text, { key }, line);
|
|
78
63
|
})
|
|
79
64
|
);
|
|
80
65
|
}
|
|
@@ -90,6 +75,12 @@ export function SetupApp({
|
|
|
90
75
|
onComplete
|
|
91
76
|
}) {
|
|
92
77
|
const { exit } = useApp();
|
|
78
|
+
const { columns, rows, layoutMode } = useTerminalSize({
|
|
79
|
+
initialColumns: output.columns ?? 80,
|
|
80
|
+
initialRows: output.rows ?? 24
|
|
81
|
+
});
|
|
82
|
+
const caps = resolveTerminalCapabilities({ columns, rows, isTTY: true });
|
|
83
|
+
const colorEnabled = caps.color;
|
|
93
84
|
const adapters = listAdapters();
|
|
94
85
|
const detected = detectInstalledAdapters({ homeDir });
|
|
95
86
|
const componentCatalog = describeComponentCatalog({ workspaceRoot });
|
|
@@ -98,7 +89,7 @@ export function SetupApp({
|
|
|
98
89
|
const defaultAgents = detected.length > 0 ? detected : [...GLOBAL_AGENT_IDS];
|
|
99
90
|
|
|
100
91
|
const [step, setStep] = useState(INITIAL_SETUP_STEP);
|
|
101
|
-
const useCompactSplash = shouldUseCompactSplashLogo(
|
|
92
|
+
const useCompactSplash = shouldUseCompactSplashLogo(columns);
|
|
102
93
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
103
94
|
const [selectedAgents, setSelectedAgents] = useState(defaultAgents);
|
|
104
95
|
const [selectedComponents, setSelectedComponents] = useState([...DEFAULT_COMPONENT_IDS]);
|
|
@@ -238,42 +229,34 @@ export function SetupApp({
|
|
|
238
229
|
}
|
|
239
230
|
});
|
|
240
231
|
|
|
241
|
-
const detectPanel = formatInkDetectPanel({ adapters, detected });
|
|
242
|
-
|
|
243
232
|
return React.createElement(Box, { flexDirection: "column" },
|
|
244
|
-
step === SETUP_STEPS.SPLASH && React.createElement(
|
|
233
|
+
step === SETUP_STEPS.SPLASH && React.createElement(SetupSplash, {
|
|
245
234
|
compact: useCompactSplash,
|
|
246
|
-
onboarding
|
|
235
|
+
onboarding,
|
|
236
|
+
colorEnabled
|
|
247
237
|
}),
|
|
248
|
-
step
|
|
249
|
-
|
|
250
|
-
detectPanel.split("\n")
|
|
251
|
-
.map((line) => React.createElement(Text, { key: line }, line))
|
|
252
|
-
),
|
|
253
|
-
step === SETUP_STEPS.AGENTS && React.createElement(Panel, { title: WIZARD_COPY.agentsPrompt },
|
|
254
|
-
formatInkSelectList({ options: agentOptions, selected: selectedAgents, activeIndex })
|
|
255
|
-
.map((line) => React.createElement(Text, { key: line }, line))
|
|
256
|
-
),
|
|
257
|
-
step === SETUP_STEPS.COMPONENTS && React.createElement(Panel, { title: WIZARD_COPY.componentsPrompt },
|
|
258
|
-
formatInkSelectList({ options: componentOptions, selected: selectedComponents, activeIndex })
|
|
259
|
-
.map((line) => React.createElement(Text, { key: line }, line))
|
|
238
|
+
step === SETUP_STEPS.SPLASH && React.createElement(Text, { dimColor: true },
|
|
239
|
+
`${BRAND.splashHint} · Esc cancel`
|
|
260
240
|
),
|
|
261
|
-
step
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
241
|
+
step !== SETUP_STEPS.SPLASH && React.createElement(SetupHeader, { colorEnabled }),
|
|
242
|
+
step !== SETUP_STEPS.SPLASH && React.createElement(SemanticSetupPanel, {
|
|
243
|
+
step,
|
|
244
|
+
activeIndex,
|
|
245
|
+
agentOptions,
|
|
246
|
+
componentOptions,
|
|
247
|
+
componentCatalog,
|
|
248
|
+
selectedAgents,
|
|
249
|
+
selectedComponents,
|
|
250
|
+
adapters,
|
|
251
|
+
detected,
|
|
252
|
+
preview,
|
|
253
|
+
previewLoading,
|
|
254
|
+
previewError,
|
|
255
|
+
dryRun,
|
|
256
|
+
layoutMode,
|
|
257
|
+
colorEnabled,
|
|
258
|
+
unicode: caps.unicode,
|
|
259
|
+
columns
|
|
260
|
+
})
|
|
278
261
|
);
|
|
279
262
|
}
|
|
@@ -178,6 +178,22 @@ export function formatInkPreviewLines({ preview, componentCatalog }) {
|
|
|
178
178
|
return lines;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/** Cap preview lines for the active layout; keep remaining-count when truncated. */
|
|
182
|
+
export function setupPreviewLineLimit(layoutMode = "compact") {
|
|
183
|
+
if (layoutMode === "wide") return 12;
|
|
184
|
+
if (layoutMode === "minimal") return 5;
|
|
185
|
+
return 8;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function windowSetupLines(lines = [], limit = 8) {
|
|
189
|
+
if (lines.length <= limit) return lines;
|
|
190
|
+
return [...lines.slice(0, limit), `… ${lines.length - limit} more`];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function setupLineKey(index, line) {
|
|
194
|
+
return `${index}-${line}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
export function formatInkSuccessLines(result, { dryRun = false, cliName = PREFERRED_CLI } = {}) {
|
|
182
198
|
const agentLine = result.agents.map((id) => getAgentLabel(id)).join(", ");
|
|
183
199
|
const componentLine = result.components.length > 0
|
package/src/global/ink/theme.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Kairo Cockpit theme — amber brand, ice interactive, semantic status.
|
|
3
|
+
* Status always has a text label — never color alone.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
export const COCKPIT_COLORS = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
success: "
|
|
9
|
-
warning: "
|
|
10
|
-
danger: "
|
|
7
|
+
brand: "#E8A017",
|
|
8
|
+
interactive: "#7EC8E8",
|
|
9
|
+
success: "#3DDC97",
|
|
10
|
+
warning: "#F0C674",
|
|
11
|
+
danger: "#FF6B6B",
|
|
11
12
|
muted: "gray",
|
|
12
|
-
|
|
13
|
+
/** @deprecated alias — prefer interactive */
|
|
14
|
+
primary: "#7EC8E8",
|
|
15
|
+
/** @deprecated alias — prefer brand */
|
|
16
|
+
secondary: "#E8A017",
|
|
17
|
+
border: "#7EC8E8"
|
|
13
18
|
};
|
|
14
19
|
|
|
15
20
|
export const STATUS_LABELS = {
|
|
@@ -69,10 +74,37 @@ export function statusColor(kind, { colorEnabled = true } = {}) {
|
|
|
69
74
|
case "muted":
|
|
70
75
|
return COCKPIT_COLORS.muted;
|
|
71
76
|
default:
|
|
72
|
-
return COCKPIT_COLORS.
|
|
77
|
+
return COCKPIT_COLORS.interactive;
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
/** Ink `color` / `borderColor` prop — undefined when color is disabled. */
|
|
82
|
+
export function resolveInkColor(colorEnabled, color) {
|
|
83
|
+
return colorEnabled ? color : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect ANSI color SGR in a string.
|
|
88
|
+
* Bold (1), dim (2), and reset (0) alone are not color.
|
|
89
|
+
*/
|
|
90
|
+
export function hasColorSgr(text) {
|
|
91
|
+
const re = /\u001b\[([0-9;]*)m/g;
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = re.exec(String(text ?? ""))) !== null) {
|
|
94
|
+
const params = match[1].length === 0
|
|
95
|
+
? []
|
|
96
|
+
: match[1].split(";").map((part) => Number(part));
|
|
97
|
+
for (let i = 0; i < params.length; i += 1) {
|
|
98
|
+
const code = params[i];
|
|
99
|
+
if (!Number.isFinite(code)) continue;
|
|
100
|
+
if (code === 38 || code === 48) return true;
|
|
101
|
+
if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) return true;
|
|
102
|
+
if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
export function formatStatusBadge(kind, label = STATUS_LABELS[kind] ?? String(kind)) {
|
|
77
109
|
return label;
|
|
78
110
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/** Live semantic Activity/Recovery. Callout=status · Confirm=action · footer=keys · snapshots=focus. */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { formatConfirmPath } from "../cockpit-path-label.js";
|
|
5
|
+
import {
|
|
6
|
+
RECOVERY_PHASE, listRecoverySnapshots, formatWhen, formatResult, shortName
|
|
7
|
+
} from "../cockpit-recovery.js";
|
|
8
|
+
import { LAYOUT_MODES } from "../layout.js";
|
|
9
|
+
import { ActionList, Callout, Confirm, Details, SectionLabel, ViewTitle } from "./semantic.js";
|
|
10
|
+
import { detailsPathLimit } from "./live-governance.js";
|
|
11
|
+
|
|
12
|
+
export function activityContentLimits(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
13
|
+
return layoutMode === LAYOUT_MODES.WIDE
|
|
14
|
+
? { events: 4, snapshots: 8 }
|
|
15
|
+
: { events: 3, snapshots: 3 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function activitySnapshotLimit(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
19
|
+
return activityContentLimits(layoutMode).snapshots;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Visible window over full list; keeps `index` focused without truncating navigation. */
|
|
23
|
+
export function windowSlice(items = [], index = 0, limit = 3) {
|
|
24
|
+
const total = items.length;
|
|
25
|
+
if (total === 0) return { items: [], selectedIndex: -1, start: 0 };
|
|
26
|
+
const size = Math.max(1, Math.min(limit, total));
|
|
27
|
+
const safeIndex = Math.min(Math.max(0, index), total - 1);
|
|
28
|
+
const start = Math.min(Math.max(0, safeIndex - Math.floor((size - 1) / 2)), total - size);
|
|
29
|
+
return { items: items.slice(start, start + size), selectedIndex: safeIndex - start, start };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function collectRecentItems(snapshot, dashboard, eventLimit) {
|
|
33
|
+
const items = [];
|
|
34
|
+
for (const event of (snapshot?.history?.events ?? []).slice(0, 3)) {
|
|
35
|
+
items.push({
|
|
36
|
+
id: `e-${items.length}`,
|
|
37
|
+
label: `${formatWhen(event.timestamp)} · ${event.command ?? event.type ?? "event"} · ${formatResult(event.action)}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const run of (dashboard?.recentRuns ?? []).slice(0, 2)) {
|
|
41
|
+
items.push({
|
|
42
|
+
id: `r-${items.length}`,
|
|
43
|
+
label: `${formatWhen(run.updatedAt ?? run.endedAt ?? run.startedAt)} · ${run.agentId ?? "agent"} · ${formatResult(run.state)}`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return items.slice(0, eventLimit);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function phaseTone(phase) {
|
|
50
|
+
if (phase === RECOVERY_PHASE.FAILED) return "danger";
|
|
51
|
+
if (phase === RECOVERY_PHASE.COMPLETED) return "ready";
|
|
52
|
+
if (phase === RECOVERY_PHASE.CONFIRMING || phase === RECOVERY_PHASE.PREVIEWING || phase === RECOVERY_PHASE.APPLYING) {
|
|
53
|
+
return "warn";
|
|
54
|
+
}
|
|
55
|
+
return "info";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function phaseTitle(phase, snapCount, eventCount) {
|
|
59
|
+
if (phase === RECOVERY_PHASE.PREVIEWING) return "Previewing restore";
|
|
60
|
+
if (phase === RECOVERY_PHASE.CONFIRMING) return "Confirm restore";
|
|
61
|
+
if (phase === RECOVERY_PHASE.APPLYING) return "Restoring";
|
|
62
|
+
if (phase === RECOVERY_PHASE.COMPLETED) return "Restore complete";
|
|
63
|
+
if (phase === RECOVERY_PHASE.FAILED) return "Restore failed";
|
|
64
|
+
return `${snapCount} snapshot(s) · ${eventCount} recent`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function calloutBody(phase, recoveryAction) {
|
|
68
|
+
const msg = recoveryAction?.message ?? "";
|
|
69
|
+
if (/Y restore|N\/Esc/i.test(msg)) return "";
|
|
70
|
+
if (phase === RECOVERY_PHASE.FAILED) {
|
|
71
|
+
return msg || (recoveryAction?.error ? `Error · ${recoveryAction.error}` : "");
|
|
72
|
+
}
|
|
73
|
+
return phase === RECOVERY_PHASE.IDLE ? msg : "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildDetailsLines(preview, receipt, showPaths, homeDir, pathLimit) {
|
|
77
|
+
if (!showPaths) return [];
|
|
78
|
+
const files = preview?.files ?? [];
|
|
79
|
+
if (files.length > 0) {
|
|
80
|
+
const lines = files.slice(0, pathLimit).map((f) => formatConfirmPath(f.displayPath ?? f.path, homeDir));
|
|
81
|
+
if (files.length > pathLimit) lines.push(`… ${files.length - pathLimit} more`);
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
if (receipt?.safetyBackup) return ["Safety backup retained"];
|
|
85
|
+
if (receipt) return [`Result · ${receipt.action ?? "rollback"} · ${receipt.restored?.length ?? 0} restored`];
|
|
86
|
+
return ["No path evidence in this preview."];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function adaptActivityModel({
|
|
90
|
+
snapshot = null, recoveryAction = null, dashboard = null, listIndex = 0,
|
|
91
|
+
homeDir = null, detailsOpen = false, layoutMode = LAYOUT_MODES.COMPACT
|
|
92
|
+
} = {}) {
|
|
93
|
+
const phase = recoveryAction?.phase ?? RECOVERY_PHASE.IDLE;
|
|
94
|
+
const limits = activityContentLimits(layoutMode);
|
|
95
|
+
const allSnapshots = listRecoverySnapshots(snapshot);
|
|
96
|
+
const windowed = windowSlice(allSnapshots, listIndex, limits.snapshots);
|
|
97
|
+
const preview = recoveryAction?.preview ?? null;
|
|
98
|
+
const receipt = recoveryAction?.receipt ?? null;
|
|
99
|
+
const hasPreview = Boolean(preview);
|
|
100
|
+
const confirming = phase === RECOVERY_PHASE.CONFIRMING;
|
|
101
|
+
const working = phase === RECOVERY_PHASE.PREVIEWING || phase === RECOVERY_PHASE.APPLYING;
|
|
102
|
+
const showPaths = (detailsOpen && hasPreview) || confirming;
|
|
103
|
+
const recentItems = collectRecentItems(snapshot, dashboard, limits.events);
|
|
104
|
+
const recent = recentItems.length > 0
|
|
105
|
+
? recentItems
|
|
106
|
+
: [{ id: "empty-recent", label: "No recent agent activity." }];
|
|
107
|
+
const snapshotItems = windowed.items.length === 0
|
|
108
|
+
? [{ id: "empty-snap", label: "No global snapshots yet." }]
|
|
109
|
+
: windowed.items.map((entry, index) => ({
|
|
110
|
+
id: entry.name ?? `s-${windowed.start + index}`,
|
|
111
|
+
label: `${shortName(entry.name)} · ${entry.fileCount ?? "?"} files`
|
|
112
|
+
}));
|
|
113
|
+
const focused = allSnapshots[Math.min(Math.max(0, listIndex), Math.max(0, allSnapshots.length - 1))] ?? null;
|
|
114
|
+
const fileCount = preview?.files?.length ?? 0;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
title: "Activity",
|
|
118
|
+
phase,
|
|
119
|
+
hasPreview,
|
|
120
|
+
callout: {
|
|
121
|
+
tone: phaseTone(phase),
|
|
122
|
+
title: phaseTitle(phase, allSnapshots.length, recentItems.length),
|
|
123
|
+
body: calloutBody(phase, recoveryAction)
|
|
124
|
+
},
|
|
125
|
+
primary: confirming || working ? null : {
|
|
126
|
+
label: hasPreview
|
|
127
|
+
? `Restore preview · ${fileCount} file(s)`
|
|
128
|
+
: (allSnapshots.length > 0 ? "Select a snapshot to preview" : "No snapshots to restore"),
|
|
129
|
+
detail: receipt
|
|
130
|
+
? `Result · ${receipt.action ?? "rollback"} · ${receipt.restored?.length ?? 0} restored`
|
|
131
|
+
: null
|
|
132
|
+
},
|
|
133
|
+
confirm: confirming ? {
|
|
134
|
+
summary: fileCount > 0
|
|
135
|
+
? `Restore ${fileCount} file(s) from ${shortName(preview?.snapshot)}.`
|
|
136
|
+
: "Restore confirmed snapshot preview.",
|
|
137
|
+
primaryLabel: "Restore"
|
|
138
|
+
} : null,
|
|
139
|
+
recent,
|
|
140
|
+
snapshots: snapshotItems,
|
|
141
|
+
selectedIndex: windowed.items.length === 0 ? -1 : windowed.selectedIndex,
|
|
142
|
+
focusedSnapshot: focused?.name ?? null,
|
|
143
|
+
snapshotTotal: allSnapshots.length,
|
|
144
|
+
details: buildDetailsLines(preview, receipt, showPaths, homeDir, detailsPathLimit(layoutMode)),
|
|
145
|
+
detailsOpen: showPaths,
|
|
146
|
+
showDetails: hasPreview || confirming || Boolean(receipt)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function SemanticActivityPanel({
|
|
151
|
+
snapshot = null, recoveryAction = null, dashboard = null, listIndex = 0,
|
|
152
|
+
homeDir = null, detailsOpen = false, layoutMode = LAYOUT_MODES.COMPACT,
|
|
153
|
+
contentFocused = false, colorEnabled = true, unicode = true
|
|
154
|
+
}) {
|
|
155
|
+
const view = adaptActivityModel({
|
|
156
|
+
snapshot, recoveryAction, dashboard, listIndex, homeDir, detailsOpen, layoutMode
|
|
157
|
+
});
|
|
158
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
159
|
+
React.createElement(ViewTitle, { colorEnabled }, view.title),
|
|
160
|
+
React.createElement(Callout, {
|
|
161
|
+
tone: view.callout.tone, title: view.callout.title,
|
|
162
|
+
body: view.callout.body || undefined, colorEnabled, compact: true
|
|
163
|
+
}),
|
|
164
|
+
view.confirm
|
|
165
|
+
? React.createElement(Confirm, {
|
|
166
|
+
summary: view.confirm.summary, primaryLabel: view.confirm.primaryLabel,
|
|
167
|
+
focused: false, colorEnabled, mark: " "
|
|
168
|
+
})
|
|
169
|
+
: view.primary
|
|
170
|
+
? React.createElement(Text, { bold: true }, ` ${view.primary.label}`)
|
|
171
|
+
: null,
|
|
172
|
+
view.primary?.detail && !view.confirm
|
|
173
|
+
? React.createElement(Text, null, view.primary.detail) : null,
|
|
174
|
+
React.createElement(SectionLabel, { colorEnabled }, "Recent"),
|
|
175
|
+
React.createElement(ActionList, {
|
|
176
|
+
items: view.recent, selectedIndex: -1, focused: false, colorEnabled, unicode
|
|
177
|
+
}),
|
|
178
|
+
React.createElement(SectionLabel, { colorEnabled }, "Snapshots"),
|
|
179
|
+
React.createElement(ActionList, {
|
|
180
|
+
items: view.snapshots, selectedIndex: view.selectedIndex,
|
|
181
|
+
focused: contentFocused, colorEnabled, unicode
|
|
182
|
+
}),
|
|
183
|
+
view.showDetails
|
|
184
|
+
? React.createElement(Details, {
|
|
185
|
+
open: view.detailsOpen, summary: "Details",
|
|
186
|
+
lines: view.details.length > 0 ? view.details : ["No path evidence in this preview."],
|
|
187
|
+
colorEnabled, focused: false, mark: " "
|
|
188
|
+
})
|
|
189
|
+
: null
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live semantic Alerts inbox.
|
|
3
|
+
* ActionList owns the only focus mark; windowSlice keeps full open domain navigable.
|
|
4
|
+
* Enter resolve / D dismiss unchanged — adapter exposes focusedId aligned with selectAlertFromList.
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { LAYOUT_MODES } from "../layout.js";
|
|
9
|
+
import { ALERT_STATES } from "../../runtime/alerts/alert-types.js";
|
|
10
|
+
import { formatAlertsHeadline, selectAlertFromList } from "../cockpit-alerts.js";
|
|
11
|
+
import { ActionList, Callout, ViewTitle } from "./semantic.js";
|
|
12
|
+
import { windowSlice } from "./live-activity.js";
|
|
13
|
+
|
|
14
|
+
export function alertsListLimit(layoutMode = LAYOUT_MODES.COMPACT) {
|
|
15
|
+
return layoutMode === LAYOUT_MODES.WIDE ? 8 : 3;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatAlertWhen(alert) {
|
|
19
|
+
return String(alert.createdAt ?? "").slice(0, 16).replace("T", " ") || "unknown time";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function alertLabel(alert) {
|
|
23
|
+
return `${alert.severity} · ${alert.title} · ${formatAlertWhen(alert)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function openAlerts(alerts) {
|
|
27
|
+
if (!Array.isArray(alerts)) return [];
|
|
28
|
+
return alerts.filter((alert) => alert.state === ALERT_STATES.OPEN);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function unavailablePack() {
|
|
32
|
+
return {
|
|
33
|
+
items: [{ id: "unavailable", label: "Could not read the alert store." }],
|
|
34
|
+
selectedIndex: -1,
|
|
35
|
+
focusedId: null,
|
|
36
|
+
total: 0,
|
|
37
|
+
start: 0,
|
|
38
|
+
isEmpty: true,
|
|
39
|
+
isUnavailable: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emptyPack() {
|
|
44
|
+
return {
|
|
45
|
+
items: [{ id: "empty", label: "No pending alerts." }],
|
|
46
|
+
selectedIndex: -1,
|
|
47
|
+
focusedId: null,
|
|
48
|
+
total: 0,
|
|
49
|
+
start: 0,
|
|
50
|
+
isEmpty: true,
|
|
51
|
+
isUnavailable: false
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function populatedPack(open, listIndex, limit) {
|
|
56
|
+
const windowed = windowSlice(open, listIndex, limit);
|
|
57
|
+
const safe = Math.min(Math.max(0, listIndex), open.length - 1);
|
|
58
|
+
const focused = open[safe] ?? null;
|
|
59
|
+
return {
|
|
60
|
+
items: windowed.items.map((alert, i) => ({
|
|
61
|
+
id: alert.alertId ?? `alert-${windowed.start + i}`,
|
|
62
|
+
label: alertLabel(alert)
|
|
63
|
+
})),
|
|
64
|
+
selectedIndex: windowed.selectedIndex,
|
|
65
|
+
focusedId: focused?.alertId ?? null,
|
|
66
|
+
total: open.length,
|
|
67
|
+
start: windowed.start,
|
|
68
|
+
isEmpty: false,
|
|
69
|
+
isUnavailable: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Pure adapter: unavailable · empty · pending inbox. */
|
|
74
|
+
export function adaptAlertsModel({
|
|
75
|
+
alerts = null,
|
|
76
|
+
listIndex = 0,
|
|
77
|
+
layoutMode = LAYOUT_MODES.COMPACT
|
|
78
|
+
} = {}) {
|
|
79
|
+
const limit = alertsListLimit(layoutMode);
|
|
80
|
+
const headline = formatAlertsHeadline(alerts);
|
|
81
|
+
let list;
|
|
82
|
+
|
|
83
|
+
if (alerts == null) {
|
|
84
|
+
list = unavailablePack();
|
|
85
|
+
} else {
|
|
86
|
+
const open = openAlerts(alerts);
|
|
87
|
+
list = open.length === 0
|
|
88
|
+
? emptyPack()
|
|
89
|
+
: populatedPack(open, listIndex, limit);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const selected = selectAlertFromList(alerts, listIndex);
|
|
93
|
+
if (!list.isEmpty && selected?.alertId) {
|
|
94
|
+
list.focusedId = selected.alertId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const callout = list.isUnavailable
|
|
98
|
+
? {
|
|
99
|
+
tone: "danger",
|
|
100
|
+
title: headline.headline,
|
|
101
|
+
body: "Esc back · / Alerts"
|
|
102
|
+
}
|
|
103
|
+
: list.isEmpty
|
|
104
|
+
? {
|
|
105
|
+
tone: "info",
|
|
106
|
+
title: headline.headline,
|
|
107
|
+
body: "Esc back · / Alerts"
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
tone: "warn",
|
|
111
|
+
title: headline.headline,
|
|
112
|
+
body: "Enter resolves · D dismisses · Esc back"
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
title: "Alerts",
|
|
117
|
+
callout,
|
|
118
|
+
items: list.items,
|
|
119
|
+
selectedIndex: list.selectedIndex,
|
|
120
|
+
focusedId: list.focusedId,
|
|
121
|
+
total: list.total,
|
|
122
|
+
start: list.start,
|
|
123
|
+
isEmpty: list.isEmpty,
|
|
124
|
+
isUnavailable: list.isUnavailable,
|
|
125
|
+
listLimit: limit
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function SemanticAlertsPanel({
|
|
130
|
+
alerts = null,
|
|
131
|
+
listIndex = 0,
|
|
132
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
133
|
+
contentFocused = false,
|
|
134
|
+
colorEnabled = true,
|
|
135
|
+
unicode = true
|
|
136
|
+
}) {
|
|
137
|
+
const model = adaptAlertsModel({ alerts, listIndex, layoutMode });
|
|
138
|
+
const listFocused = contentFocused && !model.isEmpty && !model.isUnavailable;
|
|
139
|
+
return React.createElement(Box, { flexDirection: "column" },
|
|
140
|
+
React.createElement(ViewTitle, { colorEnabled }, model.title),
|
|
141
|
+
React.createElement(Callout, {
|
|
142
|
+
tone: model.callout.tone,
|
|
143
|
+
title: model.callout.title,
|
|
144
|
+
body: model.callout.body || undefined,
|
|
145
|
+
colorEnabled,
|
|
146
|
+
compact: true
|
|
147
|
+
}),
|
|
148
|
+
React.createElement(ActionList, {
|
|
149
|
+
items: model.items,
|
|
150
|
+
selectedIndex: model.selectedIndex,
|
|
151
|
+
focused: listFocused,
|
|
152
|
+
colorEnabled,
|
|
153
|
+
unicode
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|