@kal-elsam/kairo-runtime 0.8.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 +41 -15
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +6 -6
- package/scripts/ux-prototype-tty.mjs +9 -0
- package/src/cli.js +24 -1
- package/src/global/control-plane-proposals.js +2 -1
- package/src/global/control-plane-snapshot.js +1 -1
- package/src/global/global-doctor.js +2 -0
- package/src/global/ink/cockpit/primitives.js +127 -50
- package/src/global/ink/cockpit-alerts.js +36 -0
- package/src/global/ink/cockpit-changes.js +61 -37
- package/src/global/ink/cockpit-control-center.js +79 -53
- package/src/global/ink/cockpit-controller.js +98 -15
- package/src/global/ink/cockpit-enter.js +1 -0
- package/src/global/ink/cockpit-focus.js +4 -2
- package/src/global/ink/cockpit-models.js +100 -51
- package/src/global/ink/cockpit-palette.js +109 -0
- package/src/global/ink/cockpit-path-label.js +19 -0
- package/src/global/ink/cockpit-recovery.js +84 -18
- package/src/global/ink/cockpit-reviews.js +14 -10
- package/src/global/ink/cockpit-runs.js +13 -4
- package/src/global/ink/cockpit-settings.js +194 -0
- package/src/global/ink/cockpit-usage.js +111 -0
- package/src/global/ink/cockpit-views.js +119 -116
- package/src/global/ink/orchestrator-app.js +169 -46
- package/src/global/ink/orchestrator-state.js +24 -14
- 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/use-orchestrator-data.js +58 -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
- package/src/global/paths.js +3 -0
- package/src/global/runtime/alerts/alert-store.js +216 -0
- package/src/global/runtime/alerts/alert-types.js +59 -0
- package/src/global/runtime/alerts/alert-validate.js +117 -0
- package/src/global/runtime/monitor/monitor-cli.js +62 -0
- package/src/global/runtime/monitor/monitor-platform.js +95 -0
- package/src/global/runtime/monitor/monitor.js +249 -0
package/README.md
CHANGED
|
@@ -16,8 +16,11 @@ as a technical alias).
|
|
|
16
16
|
Kairo Runtime and prints a migration warning. Prefer `@kal-elsam/kairo-runtime` and the
|
|
17
17
|
`kairo` CLI for new installs. See [`packages/harness-bridge/README.md`](packages/harness-bridge/README.md).
|
|
18
18
|
|
|
19
|
-
Terminal UX
|
|
20
|
-
|
|
19
|
+
Terminal UX is **terminal-first**: bare `kairo` opens the Ink Control Center (or Setup
|
|
20
|
+
when unconfigured). Explicit commands (`setup`, `status`, `sync`, `doctor`, `monitor`,
|
|
21
|
+
`shell`, …) keep their current behavior. Progressive disclosure keeps paths and IDs in
|
|
22
|
+
Details, list caps honest (`… N more`), and Callout reserved for status — never for
|
|
23
|
+
primary values. No web loopback / `kairo ui` in this release.
|
|
21
24
|
|
|
22
25
|
- **npm:** https://www.npmjs.com/package/@kal-elsam/kairo-runtime
|
|
23
26
|
- **repo:** https://github.com/Kal-elSam/harness
|
|
@@ -32,26 +35,49 @@ npx @kal-elsam/kairo-runtime
|
|
|
32
35
|
kairo
|
|
33
36
|
```
|
|
34
37
|
|
|
35
|
-
**First run** (no `~/.harness/state.json`):
|
|
36
|
-
|
|
38
|
+
**First run** (no `~/.harness/state.json`): semantic Setup (Detect → Agents →
|
|
39
|
+
Components → Preview → Confirm) → full-screen Control Center.
|
|
37
40
|
|
|
38
|
-
**Later runs** (state present):
|
|
39
|
-
|
|
40
|
-
action (scan → evidence → preview → confirm → apply → re-scan → recovery). Runs stay
|
|
41
|
-
secondary. Layout adapts to terminal size:
|
|
41
|
+
**Later runs** (state present): semantic Cockpit — Overview, Governance, Activity,
|
|
42
|
+
Orchestration, Usage, Settings — plus Alerts inbox. Layout adapts to terminal size:
|
|
42
43
|
|
|
43
44
|
| Mode | Size | Layout |
|
|
44
45
|
|------|------|--------|
|
|
45
|
-
| Wide | ≥100 cols × ≥28 rows |
|
|
46
|
-
| Compact | ≥72×20 |
|
|
47
|
-
| Minimal | 60–71 cols or short height | Selected section + essential readiness/next
|
|
46
|
+
| Wide | ≥100 cols × ≥28 rows | TopBar + nav strip + main panel + footer |
|
|
47
|
+
| Compact | ≥72×20 | Same single-panel shell (tighter lists) |
|
|
48
|
+
| Minimal | 60–71 cols or short height | Selected section + essential readiness/next |
|
|
48
49
|
| Below gate | <60 cols | Explicit TTY fallback (Ink disabled) |
|
|
49
50
|
|
|
50
|
-
Keys: `↑↓` navigate
|
|
51
|
-
`Esc` back (exit only from
|
|
52
|
-
`Tab` switches region
|
|
51
|
+
Keys: `↑↓` navigate · `Enter` open/activate · `/` actions palette ·
|
|
52
|
+
`Esc` back (exit only from Overview) · `R` refresh/retry · `C` cancel run ·
|
|
53
|
+
`?` help. `Tab` switches region when content is interactive (runs, alerts,
|
|
54
|
+
Activity, Settings, launch).
|
|
53
55
|
|
|
54
|
-
Navigation
|
|
56
|
+
Navigation: Overview · Governance · Activity · Orchestration · Usage · Settings.
|
|
57
|
+
|
|
58
|
+
### Monitor (opt-in)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
kairo monitor enable
|
|
62
|
+
kairo monitor status
|
|
63
|
+
kairo monitor tick
|
|
64
|
+
kairo monitor disable
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Scans governance drift and orphan/failed runs into the alert store. On macOS,
|
|
68
|
+
`enable` can install a LaunchAgent; other OS degrade honestly. Notifications fire
|
|
69
|
+
only when a new alert claim succeeds.
|
|
70
|
+
|
|
71
|
+
### Settings (curated integrations)
|
|
72
|
+
|
|
73
|
+
Settings browses a curated catalog (pinned `pi-usage-widget@0.2.1`, MIT).
|
|
74
|
+
Preview → confirm shows an in-session confirmation receipt (`wroteFiles: false`);
|
|
75
|
+
it does not persist or install anything.
|
|
76
|
+
|
|
77
|
+
### Usage evidence
|
|
78
|
+
|
|
79
|
+
Usage shows measured budget pairs, profile limits, and finite run `tokenUsage`
|
|
80
|
+
fields only. It never invents totals, costs, or savings.
|
|
55
81
|
|
|
56
82
|
Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
|
|
57
83
|
in text, never color alone.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
@@ -26,7 +26,7 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
26
26
|
const require = createRequire(import.meta.url);
|
|
27
27
|
const pkg = require(join(root, "package.json"));
|
|
28
28
|
|
|
29
|
-
assert.equal(pkg.version, "0.
|
|
29
|
+
assert.equal(pkg.version, "0.10.0");
|
|
30
30
|
assert.ok(pkg.dependencies["ansi-escapes"]);
|
|
31
31
|
|
|
32
32
|
assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
|
|
@@ -85,7 +85,7 @@ const asciiNav = buildNavModel({
|
|
|
85
85
|
assert.equal(asciiNav.items[1].marker, ">");
|
|
86
86
|
assert.equal(asciiNav.items[0].current, true);
|
|
87
87
|
assert.equal(asciiNav.items[1].selected, true);
|
|
88
|
-
assert.match(asciiNav.explanation, /
|
|
88
|
+
assert.match(asciiNav.explanation, /Repair|Govern|Activity|Usage|Settings|Overview/i);
|
|
89
89
|
|
|
90
90
|
function applyKey(state, keyAction) {
|
|
91
91
|
const routed = routeCockpitKey(state, keyAction);
|
|
@@ -98,7 +98,7 @@ function smokeNavigation(layoutMode) {
|
|
|
98
98
|
layoutMode,
|
|
99
99
|
region: layoutMode === LAYOUT_MODES.MINIMAL ? COCKPIT_REGIONS.CONTENT : COCKPIT_REGIONS.NAV
|
|
100
100
|
});
|
|
101
|
-
const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "
|
|
101
|
+
const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "governance");
|
|
102
102
|
while (state.navIndex < diagnosticsIndex) {
|
|
103
103
|
state = applyKey(state, { type: "arrow", direction: "down" });
|
|
104
104
|
}
|
|
@@ -108,7 +108,7 @@ function smokeNavigation(layoutMode) {
|
|
|
108
108
|
|
|
109
109
|
state = applyKey(state, { type: "arrow", direction: "up" });
|
|
110
110
|
state = applyKey(state, { type: "enter" });
|
|
111
|
-
assert.equal(state.view, ORCHESTRATOR_VIEWS.
|
|
111
|
+
assert.equal(state.view, ORCHESTRATOR_VIEWS.HOME);
|
|
112
112
|
assert.equal(state.region, COCKPIT_REGIONS.NAV);
|
|
113
113
|
|
|
114
114
|
const footer = buildFooterModel({
|
|
@@ -132,7 +132,7 @@ const proposalLines = formatProposalLines([
|
|
|
132
132
|
id: "setup-local",
|
|
133
133
|
severity: "high",
|
|
134
134
|
title: "Finish local setup",
|
|
135
|
-
destination: "
|
|
135
|
+
destination: "setup",
|
|
136
136
|
evidence: [{ type: "status", source: "status.overall", ref: "missing" }]
|
|
137
137
|
}
|
|
138
138
|
], {
|
|
@@ -143,7 +143,7 @@ const proposalLines = formatProposalLines([
|
|
|
143
143
|
requestBudgetTokens: 10
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
|
-
assert.match(proposalLines.join("\n"), /\[HIGH\] Finish local setup →
|
|
146
|
+
assert.match(proposalLines.join("\n"), /\[HIGH\] Finish local setup → setup/);
|
|
147
147
|
assert.match(proposalLines.join("\n"), /Budget · stable 1\/10/);
|
|
148
148
|
assert.match(proposalLines.join("\n"), /evidence: status\.overall/);
|
|
149
149
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Manual TTY gate: Home → Setup → Overview. No writes. Esc exits. */
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import { TaskFlowApp } from "../src/global/ink/ux/task-flow-app.js";
|
|
6
|
+
|
|
7
|
+
const app = render(React.createElement(TaskFlowApp, {
|
|
8
|
+
onExit: () => app.unmount()
|
|
9
|
+
}));
|
package/src/cli.js
CHANGED
|
@@ -39,6 +39,7 @@ import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orche
|
|
|
39
39
|
import { runIntelligenceCli } from "./global/intelligence-cli.js";
|
|
40
40
|
import { runGlobalRun, runGlobalRuns } from "./global/runtime/run-cli.js";
|
|
41
41
|
import { runGlobalReview, runGlobalReviews } from "./global/runtime/review/review-cli.js";
|
|
42
|
+
import { runGlobalMonitor } from "./global/runtime/monitor/monitor-cli.js";
|
|
42
43
|
import { normalizeRunStrategy } from "./global/runtime/run-strategy.js";
|
|
43
44
|
import {
|
|
44
45
|
LEGACY_PACKAGE_NAME,
|
|
@@ -119,6 +120,9 @@ export async function runCli(argv) {
|
|
|
119
120
|
case "reviews":
|
|
120
121
|
await runGlobalReviews(optionsWithPolicy, packageManifest);
|
|
121
122
|
return;
|
|
123
|
+
case "monitor":
|
|
124
|
+
await runGlobalMonitor(optionsWithPolicy, packageManifest, { packageRoot });
|
|
125
|
+
return;
|
|
122
126
|
case "intelligence":
|
|
123
127
|
await runIntelligenceCli(optionsWithPolicy, packageManifest);
|
|
124
128
|
return;
|
|
@@ -423,6 +427,10 @@ export function parseArgs(argv) {
|
|
|
423
427
|
parseIntelligenceAction(args, options);
|
|
424
428
|
}
|
|
425
429
|
|
|
430
|
+
if (command === "monitor") {
|
|
431
|
+
parseMonitorAction(args, options);
|
|
432
|
+
}
|
|
433
|
+
|
|
426
434
|
for (let index = 0; index < args.length; index += 1) {
|
|
427
435
|
const arg = args[index];
|
|
428
436
|
|
|
@@ -678,6 +686,19 @@ function parseReviewsAction(args, options) {
|
|
|
678
686
|
}
|
|
679
687
|
}
|
|
680
688
|
|
|
689
|
+
function parseMonitorAction(args, options) {
|
|
690
|
+
const action = args[0];
|
|
691
|
+
if (!action || action.startsWith("-")) {
|
|
692
|
+
options.monitorAction = "status";
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (!new Set(["enable", "disable", "status", "tick"]).has(action)) {
|
|
696
|
+
throw new Error(`Unknown monitor action "${action}". Use enable, disable, status, or tick.`);
|
|
697
|
+
}
|
|
698
|
+
args.shift();
|
|
699
|
+
options.monitorAction = action;
|
|
700
|
+
}
|
|
701
|
+
|
|
681
702
|
function parseIntelligenceAction(args, options) {
|
|
682
703
|
const action = args[0];
|
|
683
704
|
if (!action || action.startsWith("-")) {
|
|
@@ -736,6 +757,7 @@ function normalizeCommand(command) {
|
|
|
736
757
|
if (command === "runs") return "runs";
|
|
737
758
|
if (command === "review") return "review";
|
|
738
759
|
if (command === "reviews") return "reviews";
|
|
760
|
+
if (command === "monitor") return "monitor";
|
|
739
761
|
if (command === "intelligence" || command === "intel") return "intelligence";
|
|
740
762
|
if (command === "setup") return "setup";
|
|
741
763
|
if (command === "status") return "status";
|
|
@@ -863,6 +885,7 @@ Commands:
|
|
|
863
885
|
runs List, inspect, or cancel agent runs under ~/.harness/runs/.
|
|
864
886
|
review Bounded read-only review via Codex or Pi; receipts under ~/.harness/reviews/.
|
|
865
887
|
reviews List or show prior review receipts (secret-free).
|
|
888
|
+
monitor Opt-in anomaly monitor (enable|disable|status|tick). macOS LaunchAgent; notify shell:false.
|
|
866
889
|
orchestrator Read-only capability registry diagnostics (--json supported).
|
|
867
890
|
intelligence Harness Engineering layer: backends, context packs, routing, budgets.
|
|
868
891
|
Local-first (Ollama). Cloud only with --cloud-consent.
|
|
@@ -891,7 +914,7 @@ Commands:
|
|
|
891
914
|
|
|
892
915
|
JSON output (--json on supported commands):
|
|
893
916
|
status, sync, doctor, adapters, explain, diff, history, history last,
|
|
894
|
-
policy (show/set/reset), report
|
|
917
|
+
policy (show/set/reset), report, monitor
|
|
895
918
|
Human text remains the default. See README.md for examples and field notes.
|
|
896
919
|
|
|
897
920
|
Version:
|
|
@@ -12,6 +12,7 @@ export const PROPOSAL_SEVERITY = Object.freeze({
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
export const PROPOSAL_DESTINATION = Object.freeze({
|
|
15
|
+
SETUP: "setup",
|
|
15
16
|
CHANGES: "changes",
|
|
16
17
|
CONTROL_CENTER: "control-center",
|
|
17
18
|
IDES: "ides",
|
|
@@ -48,7 +49,7 @@ export function buildControlPlaneProposals({
|
|
|
48
49
|
PROPOSAL_SEVERITY.HIGH,
|
|
49
50
|
"Finish local setup",
|
|
50
51
|
status?.nextAction ?? "Configure the local ecosystem before applying repairs.",
|
|
51
|
-
PROPOSAL_DESTINATION.
|
|
52
|
+
PROPOSAL_DESTINATION.SETUP,
|
|
52
53
|
[
|
|
53
54
|
ev("status", "status.overall", status?.overall ?? "missing"),
|
|
54
55
|
ev("health", "control-plane.health", health ?? "NOT_CONFIGURED")
|
|
@@ -66,7 +66,7 @@ export function resolveControlPlaneCta({ health, status, diff } = {}) {
|
|
|
66
66
|
title: "Finish local setup",
|
|
67
67
|
detail: status?.nextAction
|
|
68
68
|
?? `Run ${formatCliCommand("setup")} to configure the local ecosystem.`,
|
|
69
|
-
destination: "
|
|
69
|
+
destination: "setup"
|
|
70
70
|
};
|
|
71
71
|
case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
|
|
72
72
|
return {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
BACKEND_IDS,
|
|
12
12
|
inspectIntelligenceBackends
|
|
13
13
|
} from "./intelligence/index.js";
|
|
14
|
+
import { monitorDoctorCheck } from "./runtime/monitor/monitor.js";
|
|
14
15
|
|
|
15
16
|
export async function runGlobalDoctorChecks(homeDir, {
|
|
16
17
|
packageRoot,
|
|
@@ -43,6 +44,7 @@ export async function runGlobalDoctorChecks(homeDir, {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
checks.push(await intelligenceProvidersCheck({ env, fetchImpl, inspectBackends }));
|
|
47
|
+
checks.push(await monitorDoctorCheck(homeDir));
|
|
46
48
|
|
|
47
49
|
const hasMissing = checks.some((check) => check.status === "missing");
|
|
48
50
|
const hasStale = checks.some((check) => check.status === "stale");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { COCKPIT_COLORS, statusColor } from "../theme.js";
|
|
3
|
+
import { COCKPIT_COLORS, resolveInkColor, statusColor } from "../theme.js";
|
|
4
4
|
|
|
5
5
|
export function CockpitBadge({ label, kind = "ready", colorEnabled = true }) {
|
|
6
6
|
return React.createElement(Text, {
|
|
@@ -8,53 +8,81 @@ export function CockpitBadge({ label, kind = "ready", colorEnabled = true }) {
|
|
|
8
8
|
}, label);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function CockpitEmptyState({ title, message, hint }) {
|
|
11
|
+
export function CockpitEmptyState({ title, message, hint, colorEnabled = true }) {
|
|
12
12
|
return React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
13
|
-
title && React.createElement(Text, {
|
|
13
|
+
title && React.createElement(Text, {
|
|
14
|
+
bold: true,
|
|
15
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
|
|
16
|
+
}, title),
|
|
14
17
|
message && React.createElement(Text, null, message),
|
|
15
|
-
hint && React.createElement(Text, {
|
|
18
|
+
hint && React.createElement(Text, {
|
|
19
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
20
|
+
}, hint)
|
|
16
21
|
);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
export function CockpitPanel({ title, focused = false, width, children }) {
|
|
24
|
+
export function CockpitPanel({ title, focused = false, width, children, colorEnabled = true }) {
|
|
20
25
|
return React.createElement(Box, {
|
|
21
26
|
flexDirection: "column",
|
|
22
27
|
width,
|
|
23
28
|
borderStyle: "single",
|
|
24
|
-
borderColor:
|
|
29
|
+
borderColor: resolveInkColor(
|
|
30
|
+
colorEnabled,
|
|
31
|
+
focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted
|
|
32
|
+
),
|
|
25
33
|
paddingX: 1,
|
|
26
34
|
flexGrow: 1
|
|
27
35
|
},
|
|
28
36
|
title && React.createElement(Text, {
|
|
29
37
|
bold: true,
|
|
30
|
-
color:
|
|
38
|
+
color: resolveInkColor(
|
|
39
|
+
colorEnabled,
|
|
40
|
+
focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
|
|
41
|
+
)
|
|
42
|
+
}, title),
|
|
43
|
+
children
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function CockpitSection({ title, children, colorEnabled = true }) {
|
|
48
|
+
return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
49
|
+
title && React.createElement(Text, {
|
|
50
|
+
bold: true,
|
|
51
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
|
|
31
52
|
}, title),
|
|
32
53
|
children
|
|
33
54
|
);
|
|
34
55
|
}
|
|
35
56
|
|
|
57
|
+
export function CockpitKeyHint({ keys, label, colorEnabled = true }) {
|
|
58
|
+
return React.createElement(Text, {
|
|
59
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
60
|
+
}, `${keys} ${label}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
export function CockpitTopBar({ model, colorEnabled = true }) {
|
|
37
64
|
return React.createElement(Box, { justifyContent: "space-between", width: "100%" },
|
|
38
65
|
React.createElement(Text, {
|
|
39
66
|
bold: true,
|
|
40
|
-
color: colorEnabled
|
|
67
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
|
|
41
68
|
}, `╭─ ${model.brand} ─ ${model.status}`),
|
|
42
69
|
React.createElement(Text, {
|
|
43
|
-
color: colorEnabled
|
|
70
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
44
71
|
}, `${model.projectLabel} ─╮`)
|
|
45
72
|
);
|
|
46
73
|
}
|
|
47
74
|
|
|
75
|
+
/** Vertical nav list (legacy / tests). Prefer CockpitNavStrip in the shell. */
|
|
48
76
|
export function CockpitNav({ model, colorEnabled = true }) {
|
|
49
77
|
return React.createElement(Box, { flexDirection: "column" },
|
|
50
78
|
model.items.map((item) => {
|
|
51
79
|
const suffix = item.current && !item.selected ? " (open)" : "";
|
|
52
80
|
const color = item.focused
|
|
53
|
-
? (colorEnabled
|
|
81
|
+
? resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
|
|
54
82
|
: item.selected
|
|
55
|
-
? (colorEnabled
|
|
83
|
+
? resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
|
|
56
84
|
: item.current
|
|
57
|
-
? (colorEnabled
|
|
85
|
+
? resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
58
86
|
: undefined;
|
|
59
87
|
return React.createElement(Text, {
|
|
60
88
|
key: item.id,
|
|
@@ -63,16 +91,75 @@ export function CockpitNav({ model, colorEnabled = true }) {
|
|
|
63
91
|
}, `${item.marker} ${item.label}${suffix}`);
|
|
64
92
|
}),
|
|
65
93
|
model.explanation && React.createElement(Text, {
|
|
66
|
-
color: COCKPIT_COLORS.muted
|
|
94
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
67
95
|
}, model.explanation)
|
|
68
96
|
);
|
|
69
97
|
}
|
|
70
98
|
|
|
99
|
+
function stripLabel(item, { compact }) {
|
|
100
|
+
if (!compact) return item.label;
|
|
101
|
+
return item.label.split(/[&·]/)[0].trim().split(/\s+/)[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Horizontal navigation strip under the top bar. */
|
|
105
|
+
export function CockpitNavStrip({
|
|
106
|
+
model,
|
|
107
|
+
colorEnabled = true,
|
|
108
|
+
layoutMode = "compact",
|
|
109
|
+
focused = false
|
|
110
|
+
}) {
|
|
111
|
+
const compact = layoutMode !== "wide";
|
|
112
|
+
const parts = model.items.map((item) => {
|
|
113
|
+
const label = stripLabel(item, { compact });
|
|
114
|
+
const suffix = item.current && !item.selected ? "*" : "";
|
|
115
|
+
const color = item.focused
|
|
116
|
+
? resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
|
|
117
|
+
: item.selected
|
|
118
|
+
? resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
|
|
119
|
+
: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
|
|
120
|
+
return React.createElement(Text, {
|
|
121
|
+
key: item.id,
|
|
122
|
+
bold: item.focused || item.selected,
|
|
123
|
+
color
|
|
124
|
+
}, `${item.marker}${label}${suffix}`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const joined = [];
|
|
128
|
+
parts.forEach((node, index) => {
|
|
129
|
+
if (index > 0) {
|
|
130
|
+
joined.push(React.createElement(Text, {
|
|
131
|
+
key: `sep-${index}`,
|
|
132
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
133
|
+
}, " · "));
|
|
134
|
+
}
|
|
135
|
+
joined.push(node);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return React.createElement(Box, {
|
|
139
|
+
flexDirection: "column",
|
|
140
|
+
width: "100%",
|
|
141
|
+
borderStyle: "single",
|
|
142
|
+
borderColor: resolveInkColor(
|
|
143
|
+
colorEnabled,
|
|
144
|
+
focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted
|
|
145
|
+
),
|
|
146
|
+
paddingX: 1
|
|
147
|
+
},
|
|
148
|
+
React.createElement(Box, { flexDirection: "row", flexWrap: "wrap" }, ...joined),
|
|
149
|
+
model.explanation && React.createElement(Text, {
|
|
150
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
151
|
+
}, model.explanation)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Kept for model tests; no longer rendered in the shell. */
|
|
71
156
|
export function CockpitSystemStrip({ model, colorEnabled = true }) {
|
|
72
157
|
return React.createElement(Box, { flexDirection: "column" },
|
|
73
158
|
model.rows.map((row) =>
|
|
74
159
|
React.createElement(Text, { key: row.key },
|
|
75
|
-
React.createElement(Text, {
|
|
160
|
+
React.createElement(Text, {
|
|
161
|
+
color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
|
|
162
|
+
}, `${row.key.padEnd(7)}`),
|
|
76
163
|
React.createElement(CockpitBadge, {
|
|
77
164
|
label: row.value,
|
|
78
165
|
kind: row.kind,
|
|
@@ -83,56 +170,46 @@ export function CockpitSystemStrip({ model, colorEnabled = true }) {
|
|
|
83
170
|
);
|
|
84
171
|
}
|
|
85
172
|
|
|
86
|
-
export function CockpitFooter({ model }) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
React.createElement(Text, { color:
|
|
92
|
-
React.createElement(Text, { color:
|
|
93
|
-
|
|
94
|
-
)
|
|
173
|
+
export function CockpitFooter({ model, columns = 80, colorEnabled = true }) {
|
|
174
|
+
const width = Math.max(24, Math.min(Number(columns) || 80, 120));
|
|
175
|
+
const bar = Math.max(20, width - 2);
|
|
176
|
+
const muted = resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
|
|
177
|
+
return React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
178
|
+
React.createElement(Text, { color: muted }, `├${"─".repeat(bar)}┤`),
|
|
179
|
+
React.createElement(Text, { color: muted }, `│ ${model.text}`),
|
|
180
|
+
React.createElement(Text, { color: muted }, `╰${"─".repeat(bar)}╯`)
|
|
95
181
|
);
|
|
96
182
|
}
|
|
97
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Responsive single-panel shell: TopBar → NavStrip → Main → Footer.
|
|
186
|
+
* SYSTEM side column removed (diagnostics via Enter/detail in later slices).
|
|
187
|
+
*/
|
|
98
188
|
export function CockpitShell({
|
|
99
189
|
topBar,
|
|
100
190
|
footer,
|
|
101
191
|
layoutMode,
|
|
102
192
|
nav,
|
|
103
|
-
system,
|
|
104
193
|
navFocused,
|
|
105
194
|
contentFocused,
|
|
106
|
-
systemFocused,
|
|
107
195
|
colorEnabled = true,
|
|
196
|
+
columns = 80,
|
|
108
197
|
children
|
|
109
198
|
}) {
|
|
110
|
-
const showNav = layoutMode === "wide" || layoutMode === "compact";
|
|
111
|
-
const showSystem = layoutMode === "wide";
|
|
112
|
-
|
|
113
199
|
return React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
114
200
|
React.createElement(CockpitTopBar, { model: topBar, colorEnabled }),
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
width: showSystem ? 48 : showNav ? 56 : "100%"
|
|
129
|
-
}, children),
|
|
130
|
-
showSystem && React.createElement(CockpitPanel, {
|
|
131
|
-
title: system?.title ?? "SYSTEM",
|
|
132
|
-
focused: systemFocused,
|
|
133
|
-
width: 20
|
|
134
|
-
}, React.createElement(CockpitSystemStrip, { model: system, colorEnabled }))
|
|
135
|
-
),
|
|
136
|
-
React.createElement(CockpitFooter, { model: footer })
|
|
201
|
+
nav && React.createElement(CockpitNavStrip, {
|
|
202
|
+
model: nav,
|
|
203
|
+
colorEnabled,
|
|
204
|
+
layoutMode,
|
|
205
|
+
focused: navFocused
|
|
206
|
+
}),
|
|
207
|
+
React.createElement(CockpitPanel, {
|
|
208
|
+
title: undefined,
|
|
209
|
+
focused: contentFocused,
|
|
210
|
+
width: "100%",
|
|
211
|
+
colorEnabled
|
|
212
|
+
}, children),
|
|
213
|
+
React.createElement(CockpitFooter, { model: footer, columns, colorEnabled })
|
|
137
214
|
);
|
|
138
215
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ALERT_STATES } from "../runtime/alerts/alert-types.js";
|
|
2
|
+
|
|
3
|
+
export function formatAlertsHeadline(alerts = null) {
|
|
4
|
+
if (alerts == null) return { count: null, headline: "Alert data unavailable" };
|
|
5
|
+
const count = alerts.filter((alert) => alert.state === ALERT_STATES.OPEN).length;
|
|
6
|
+
return { count, headline: count === 0 ? "None pending" : `${count} pending` };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatAlertListLines(alerts = null) {
|
|
10
|
+
if (alerts == null) return ["Alert data unavailable"];
|
|
11
|
+
const open = alerts.filter((alert) => alert.state === ALERT_STATES.OPEN);
|
|
12
|
+
if (open.length === 0) return ["No pending alerts."];
|
|
13
|
+
return open.map((alert) => {
|
|
14
|
+
const when = String(alert.createdAt ?? "").slice(0, 16).replace("T", " ");
|
|
15
|
+
return `${alert.severity} · ${alert.title} · ${when || "unknown time"}`;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatAlertDetailLines(alert) {
|
|
20
|
+
if (!alert) return ["Alert not found."];
|
|
21
|
+
return [
|
|
22
|
+
"SUMMARY",
|
|
23
|
+
`${alert.severity} · ${alert.state} · ${alert.kind}`,
|
|
24
|
+
alert.title,
|
|
25
|
+
alert.summary || "No summary.",
|
|
26
|
+
"",
|
|
27
|
+
"DETAILS",
|
|
28
|
+
`Id · ${alert.alertId}`,
|
|
29
|
+
`Source · ${alert.source ?? "n/a"} · Created · ${alert.createdAt ?? "n/a"}`
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function selectAlertFromList(alerts = null, index = 0) {
|
|
34
|
+
if (!Array.isArray(alerts)) return null;
|
|
35
|
+
return alerts.filter((alert) => alert.state === ALERT_STATES.OPEN)[index] ?? null;
|
|
36
|
+
}
|