@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.
Files changed (49) hide show
  1. package/README.md +41 -15
  2. package/package.json +1 -1
  3. package/scripts/cockpit-smoke.mjs +6 -6
  4. package/scripts/ux-prototype-tty.mjs +9 -0
  5. package/src/cli.js +24 -1
  6. package/src/global/control-plane-proposals.js +2 -1
  7. package/src/global/control-plane-snapshot.js +1 -1
  8. package/src/global/global-doctor.js +2 -0
  9. package/src/global/ink/cockpit/primitives.js +127 -50
  10. package/src/global/ink/cockpit-alerts.js +36 -0
  11. package/src/global/ink/cockpit-changes.js +61 -37
  12. package/src/global/ink/cockpit-control-center.js +79 -53
  13. package/src/global/ink/cockpit-controller.js +98 -15
  14. package/src/global/ink/cockpit-enter.js +1 -0
  15. package/src/global/ink/cockpit-focus.js +4 -2
  16. package/src/global/ink/cockpit-models.js +100 -51
  17. package/src/global/ink/cockpit-palette.js +109 -0
  18. package/src/global/ink/cockpit-path-label.js +19 -0
  19. package/src/global/ink/cockpit-recovery.js +84 -18
  20. package/src/global/ink/cockpit-reviews.js +14 -10
  21. package/src/global/ink/cockpit-runs.js +13 -4
  22. package/src/global/ink/cockpit-settings.js +194 -0
  23. package/src/global/ink/cockpit-usage.js +111 -0
  24. package/src/global/ink/cockpit-views.js +119 -116
  25. package/src/global/ink/orchestrator-app.js +169 -46
  26. package/src/global/ink/orchestrator-state.js +24 -14
  27. package/src/global/ink/setup-app.js +55 -72
  28. package/src/global/ink/setup-state.js +16 -0
  29. package/src/global/ink/theme.js +27 -0
  30. package/src/global/ink/use-orchestrator-data.js +58 -0
  31. package/src/global/ink/ux/live-activity.js +194 -0
  32. package/src/global/ink/ux/live-alerts.js +159 -0
  33. package/src/global/ink/ux/live-governance.js +195 -0
  34. package/src/global/ink/ux/live-orchestration.js +188 -0
  35. package/src/global/ink/ux/live-overview.js +125 -0
  36. package/src/global/ink/ux/live-settings.js +189 -0
  37. package/src/global/ink/ux/live-setup.js +160 -0
  38. package/src/global/ink/ux/live-usage.js +51 -0
  39. package/src/global/ink/ux/semantic.js +84 -0
  40. package/src/global/ink/ux/task-flow-app.js +85 -0
  41. package/src/global/ink/ux/task-flow.js +173 -0
  42. package/src/global/orchestrator.js +37 -20
  43. package/src/global/paths.js +3 -0
  44. package/src/global/runtime/alerts/alert-store.js +216 -0
  45. package/src/global/runtime/alerts/alert-types.js +59 -0
  46. package/src/global/runtime/alerts/alert-validate.js +117 -0
  47. package/src/global/runtime/monitor/monitor-cli.js +62 -0
  48. package/src/global/runtime/monitor/monitor-platform.js +95 -0
  49. 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 aims for Pi-like clarity (clear modes, non-interactive flags, extensible
20
- commands) without depending on Pi as a runtime or adding a Pi adapter.
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`): interactive onboardingsafe diagnosis
36
- setup with confirmation → full-screen operations cockpit.
38
+ **First run** (no `~/.harness/state.json`): semantic Setup (Detect Agents
39
+ Components PreviewConfirm) → full-screen Control Center.
37
40
 
38
- **Later runs** (state present): full-screen Control Center that explains what Kairo
39
- does, shows coverage/integrity, evidence-backed proposals, and the next governance
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 | Nav + Home/content + system |
46
- | Compact | ≥72×20 | Nav + Home/content (readiness embedded) |
47
- | Minimal | 60–71 cols or short height | Selected section + essential readiness/next/recent |
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 (selection explanation updates) · `Enter` open ·
51
- `Esc` back (exit only from Home) · `R` refresh/retry · `C` cancel run · `?` help.
52
- `Tab` switches region only when content is interactive (run lists / New run).
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 labels: Home · Running now · History · Agents · New run · System health.
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.8.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.8.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, /Detected agents|IDEs|models|Harness|modules/i);
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 === "changes");
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.MODULES);
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: "changes",
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 → changes/);
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.CHANGES,
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: "changes"
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, { bold: true, color: COCKPIT_COLORS.secondary }, title),
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, { color: COCKPIT_COLORS.muted }, hint)
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: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted,
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: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
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 ? COCKPIT_COLORS.primary : undefined
67
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
41
68
  }, `╭─ ${model.brand} ─ ${model.status}`),
42
69
  React.createElement(Text, {
43
- color: colorEnabled ? COCKPIT_COLORS.muted : undefined
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 ? COCKPIT_COLORS.primary : undefined)
81
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
54
82
  : item.selected
55
- ? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
83
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
56
84
  : item.current
57
- ? (colorEnabled ? COCKPIT_COLORS.muted : undefined)
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, { color: COCKPIT_COLORS.muted }, `${row.key.padEnd(7)}`),
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
- return React.createElement(Box, { flexDirection: "column" },
88
- React.createElement(Text, { color: COCKPIT_COLORS.muted },
89
- `├${"─".repeat(62)}┤`
90
- ),
91
- React.createElement(Text, { color: COCKPIT_COLORS.muted }, `│ ${model.text}`),
92
- React.createElement(Text, { color: COCKPIT_COLORS.muted },
93
- `╰${"─".repeat(62)}╯`
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
- layoutMode === "minimal" && nav && React.createElement(Box, { marginY: 0 },
116
- React.createElement(Text, { color: COCKPIT_COLORS.muted }, "Nav: "),
117
- React.createElement(CockpitNav, { model: nav, colorEnabled })
118
- ),
119
- React.createElement(Box, { flexDirection: "row", width: "100%" },
120
- showNav && React.createElement(CockpitPanel, {
121
- title: nav?.title ?? "NAVIGATION",
122
- focused: navFocused,
123
- width: 22
124
- }, React.createElement(CockpitNav, { model: nav, colorEnabled })),
125
- React.createElement(CockpitPanel, {
126
- title: undefined,
127
- focused: contentFocused,
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
+ }