@kal-elsam/kairo-runtime 0.2.3 → 0.3.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 CHANGED
@@ -33,10 +33,23 @@ kairo
33
33
  ```
34
34
 
35
35
  **First run** (no `~/.harness/state.json`): interactive onboarding → safe diagnosis →
36
- setup with confirmation → operations dashboard.
36
+ setup with confirmation → full-screen operations cockpit.
37
37
 
38
- **Later runs** (state present): operations dashboard with a stable purpose line and a
39
- contextual next step (configure, enable intelligence, launch a run, or review problems).
38
+ **Later runs** (state present): full-screen cockpit with mission control (recommended
39
+ action), navigation, and a system strip. Layout adapts to terminal size:
40
+
41
+ | Mode | Size | Layout |
42
+ |------|------|--------|
43
+ | Wide | ≥100 cols × ≥28 rows | Nav + content + system |
44
+ | Compact | ≥72×20 | Nav + content |
45
+ | Minimal | 60–71 cols or short height | Single panel + nav header |
46
+ | Below gate | <60 cols | Explicit TTY fallback (Ink disabled) |
47
+
48
+ Keys: `↑↓` navigate · `Tab` region · `Enter` open · `R` refresh · `C` cancel run ·
49
+ `?` help · `Esc` back (exit only from Home).
50
+
51
+ Respects `NO_COLOR`, `HARNESS_ASCII=1`, and `HARNESS_INK=0`. Status is always labeled
52
+ in text, never color alone.
40
53
 
41
54
  Explicit commands and setup flags keep their current behavior (`kairo setup`,
42
55
  `kairo --dry-run`, `kairo shell`, non-TTY scripts, etc.).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Gemini, Copilot, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
@@ -33,6 +33,7 @@
33
33
  "doctor": "node ./bin/kairo.js doctor",
34
34
  "smoke": "bash scripts/smoke-test.sh",
35
35
  "ux:smoke": "bash scripts/ux-smoke-test.sh",
36
+ "smoke:cockpit": "bash scripts/cockpit-smoke-test.sh",
36
37
  "smoke:registry": "bash scripts/registry-smoke-test.sh",
37
38
  "smoke:installer": "bash scripts/installer-smoke-test.sh",
38
39
  "smoke:bridge": "bash scripts/bridge-smoke-test.sh",
@@ -59,6 +60,7 @@
59
60
  },
60
61
  "dependencies": {
61
62
  "@clack/prompts": "^1.7.0",
63
+ "ansi-escapes": "^7.3.0",
62
64
  "ink": "^5.2.1",
63
65
  "react": "^18.3.1"
64
66
  }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Cockpit smoke — layout/capabilities + package wiring (no PTY required).
3
+ set -euo pipefail
4
+
5
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ cd "$ROOT"
7
+
8
+ echo "Kairo cockpit smoke"
9
+ node "$ROOT/scripts/cockpit-smoke.mjs"
10
+
11
+ node --test \
12
+ test/fullscreen-session.test.js \
13
+ test/layout.test.js \
14
+ test/terminal-capabilities.test.js \
15
+ test/cockpit-models.test.js \
16
+ test/cockpit-controller.test.js \
17
+ test/cockpit-frame.test.js
18
+
19
+ echo "Kairo cockpit smoke passed"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { resolveLayoutMode, LAYOUT_MODES } from "../src/global/ink/layout.js";
7
+ import { resolveTerminalCapabilities } from "../src/global/ink/terminal-capabilities.js";
8
+ import { createFullscreenSession } from "../src/global/ink/fullscreen-session.js";
9
+ import { buildTopBarModel, buildHomeMissionModel } from "../src/global/ink/cockpit-models.js";
10
+
11
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require(join(root, "package.json"));
14
+
15
+ assert.equal(pkg.version, "0.3.0");
16
+ assert.ok(pkg.dependencies["ansi-escapes"]);
17
+
18
+ assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
19
+ assert.equal(resolveLayoutMode({ columns: 80, rows: 24 }), LAYOUT_MODES.COMPACT);
20
+ assert.equal(resolveLayoutMode({ columns: 65, rows: 24 }), LAYOUT_MODES.MINIMAL);
21
+ assert.equal(resolveLayoutMode({ columns: 50, rows: 24 }), null);
22
+
23
+ const caps = resolveTerminalCapabilities({
24
+ columns: 80,
25
+ rows: 24,
26
+ isTTY: true,
27
+ term: "xterm-256color",
28
+ env: { NO_COLOR: "1" }
29
+ });
30
+ assert.equal(caps.color, false);
31
+ assert.equal(caps.canUseInk, true);
32
+
33
+ const session = createFullscreenSession({
34
+ stdout: { isTTY: true, write: () => true },
35
+ processRef: { on() {}, removeListener() {}, exit() {} },
36
+ onSignal: () => {}
37
+ });
38
+ assert.equal(session.enter(), true);
39
+ assert.equal(session.leave(), true);
40
+ assert.equal(session.leave(), false);
41
+
42
+ const top = buildTopBarModel({ projectName: "smoke" });
43
+ assert.match(top.status, /ONLINE|Offline/);
44
+ const mission = buildHomeMissionModel({
45
+ hasGlobalState: false,
46
+ diagnostics: { diagnostics: { detected: 0 } },
47
+ dashboard: { providers: [], recentRuns: [] }
48
+ });
49
+ assert.match(mission.title, /MISSION CONTROL/);
50
+
51
+ console.log("cockpit smoke OK");
package/src/cli.js CHANGED
@@ -696,11 +696,11 @@ sections, components, backups, and drift repair under ~/.harness.
696
696
  Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
697
697
 
698
698
  Usage:
699
- ${cli} First run: onboarding → setup → dashboard (TTY).
700
- Later: operations dashboard with next-step guidance.
699
+ ${cli} First run: onboarding → setup → cockpit (TTY).
700
+ Later: full-screen cockpit (wide/compact/minimal).
701
701
  ${cli} --dry-run Setup dry-run (scriptable)
702
702
  ${cli} --version
703
- ${cli} shell Operations dashboard (TTY)
703
+ ${cli} shell Operations cockpit (TTY)
704
704
  ${cli} run --agent <id> --task "..." [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
705
705
  ${cli} runs list [--json] [--limit <n>] [--active-only]
706
706
  ${cli} runs show <runId> [--json] [--limit <n>] [--follow]
@@ -741,8 +741,9 @@ Scopes:
741
741
  Explicit --scope=workspace only.
742
742
 
743
743
  Commands:
744
- shell Operations dashboard (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
745
- is missing, otherwise the dashboard. Explicit ${cli} shell always opens the dashboard.
744
+ shell Operations cockpit (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
745
+ is missing, otherwise the cockpit. Explicit ${cli} shell always opens the cockpit.
746
+ Keys: ↑↓ · Tab region · Enter · R refresh · C cancel · ? help · Esc back/exit.
746
747
  run Launch a managed agent run with local audit trail.
747
748
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
748
749
  orchestrator Read-only capability registry diagnostics (--json supported).
@@ -0,0 +1,131 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { COCKPIT_COLORS, statusColor } from "../theme.js";
4
+
5
+ export function CockpitBadge({ label, kind = "ready", colorEnabled = true }) {
6
+ return React.createElement(Text, {
7
+ color: statusColor(kind, { colorEnabled })
8
+ }, label);
9
+ }
10
+
11
+ export function CockpitEmptyState({ title, message, hint }) {
12
+ return React.createElement(Box, { flexDirection: "column", marginY: 1 },
13
+ title && React.createElement(Text, { bold: true, color: COCKPIT_COLORS.secondary }, title),
14
+ message && React.createElement(Text, null, message),
15
+ hint && React.createElement(Text, { color: COCKPIT_COLORS.muted }, hint)
16
+ );
17
+ }
18
+
19
+ export function CockpitPanel({ title, focused = false, width, children }) {
20
+ return React.createElement(Box, {
21
+ flexDirection: "column",
22
+ width,
23
+ borderStyle: "single",
24
+ borderColor: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted,
25
+ paddingX: 1,
26
+ flexGrow: 1
27
+ },
28
+ title && React.createElement(Text, {
29
+ bold: true,
30
+ color: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
31
+ }, title),
32
+ children
33
+ );
34
+ }
35
+
36
+ export function CockpitTopBar({ model, colorEnabled = true }) {
37
+ return React.createElement(Box, { justifyContent: "space-between", width: "100%" },
38
+ React.createElement(Text, {
39
+ bold: true,
40
+ color: colorEnabled ? COCKPIT_COLORS.primary : undefined
41
+ }, `╭─ ${model.brand} ─ ${model.status}`),
42
+ React.createElement(Text, {
43
+ color: colorEnabled ? COCKPIT_COLORS.muted : undefined
44
+ }, `${model.projectLabel} ─╮`)
45
+ );
46
+ }
47
+
48
+ export function CockpitNav({ model, colorEnabled = true }) {
49
+ return React.createElement(Box, { flexDirection: "column" },
50
+ model.items.map((item) =>
51
+ React.createElement(Text, {
52
+ key: item.id,
53
+ bold: item.focused || item.selected,
54
+ color: item.focused
55
+ ? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
56
+ : item.selected
57
+ ? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
58
+ : undefined
59
+ }, `${item.marker} ${item.label}`)
60
+ )
61
+ );
62
+ }
63
+
64
+ export function CockpitSystemStrip({ model, colorEnabled = true }) {
65
+ return React.createElement(Box, { flexDirection: "column" },
66
+ model.rows.map((row) =>
67
+ React.createElement(Text, { key: row.key },
68
+ React.createElement(Text, { color: COCKPIT_COLORS.muted }, `${row.key.padEnd(7)}`),
69
+ React.createElement(CockpitBadge, {
70
+ label: row.value,
71
+ kind: row.kind,
72
+ colorEnabled
73
+ })
74
+ )
75
+ )
76
+ );
77
+ }
78
+
79
+ export function CockpitFooter({ model }) {
80
+ return React.createElement(Box, { flexDirection: "column" },
81
+ React.createElement(Text, { color: COCKPIT_COLORS.muted },
82
+ `├${"─".repeat(62)}┤`
83
+ ),
84
+ React.createElement(Text, { color: COCKPIT_COLORS.muted }, `│ ${model.text}`),
85
+ React.createElement(Text, { color: COCKPIT_COLORS.muted },
86
+ `╰${"─".repeat(62)}╯`
87
+ )
88
+ );
89
+ }
90
+
91
+ export function CockpitShell({
92
+ topBar,
93
+ footer,
94
+ layoutMode,
95
+ nav,
96
+ system,
97
+ navFocused,
98
+ contentFocused,
99
+ systemFocused,
100
+ colorEnabled = true,
101
+ children
102
+ }) {
103
+ const showNav = layoutMode === "wide" || layoutMode === "compact";
104
+ const showSystem = layoutMode === "wide";
105
+
106
+ return React.createElement(Box, { flexDirection: "column", width: "100%" },
107
+ React.createElement(CockpitTopBar, { model: topBar, colorEnabled }),
108
+ layoutMode === "minimal" && nav && React.createElement(Box, { marginY: 0 },
109
+ React.createElement(Text, { color: COCKPIT_COLORS.muted }, "Nav: "),
110
+ React.createElement(CockpitNav, { model: nav, colorEnabled })
111
+ ),
112
+ React.createElement(Box, { flexDirection: "row", width: "100%" },
113
+ showNav && React.createElement(CockpitPanel, {
114
+ title: nav?.title ?? "NAVIGATION",
115
+ focused: navFocused,
116
+ width: 22
117
+ }, React.createElement(CockpitNav, { model: nav, colorEnabled })),
118
+ React.createElement(CockpitPanel, {
119
+ title: undefined,
120
+ focused: contentFocused,
121
+ width: showSystem ? 48 : showNav ? 56 : "100%"
122
+ }, children),
123
+ showSystem && React.createElement(CockpitPanel, {
124
+ title: system?.title ?? "SYSTEM",
125
+ focused: systemFocused,
126
+ width: 20
127
+ }, React.createElement(CockpitSystemStrip, { model: system, colorEnabled }))
128
+ ),
129
+ React.createElement(CockpitFooter, { model: footer })
130
+ );
131
+ }
@@ -0,0 +1,131 @@
1
+ import { COCKPIT_NAV, COCKPIT_REGIONS, regionsForLayout } from "./cockpit-models.js";
2
+ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
3
+ import { LAYOUT_MODES } from "./layout.js";
4
+
5
+ /**
6
+ * Pure cockpit focus / navigation reducer — no I/O, no React.
7
+ */
8
+
9
+ export function createCockpitUiState({
10
+ layoutMode = LAYOUT_MODES.COMPACT,
11
+ view = ORCHESTRATOR_VIEWS.HOME,
12
+ region = COCKPIT_REGIONS.NAV,
13
+ navIndex = 0,
14
+ listIndex = 0,
15
+ helpOpen = false
16
+ } = {}) {
17
+ const regions = regionsForLayout(layoutMode);
18
+ const safeRegion = regions.includes(region) ? region : regions[0];
19
+ return {
20
+ layoutMode,
21
+ view,
22
+ region: safeRegion,
23
+ navIndex: clamp(navIndex, 0, COCKPIT_NAV.length - 1),
24
+ listIndex: Math.max(0, listIndex),
25
+ helpOpen,
26
+ shouldExit: false
27
+ };
28
+ }
29
+
30
+ export function reduceCockpitUi(state, action) {
31
+ switch (action.type) {
32
+ case "resize": {
33
+ const regions = regionsForLayout(action.layoutMode);
34
+ const region = regions.includes(state.region) ? state.region : regions[0];
35
+ return { ...state, layoutMode: action.layoutMode, region };
36
+ }
37
+ case "tab": {
38
+ const regions = regionsForLayout(state.layoutMode);
39
+ if (regions.length < 2) return state;
40
+ const index = regions.indexOf(state.region);
41
+ const next = regions[(index + 1) % regions.length];
42
+ return { ...state, region: next };
43
+ }
44
+ case "arrow": {
45
+ if (state.helpOpen) return state;
46
+ if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
47
+
48
+ const navigatesNav = state.region === COCKPIT_REGIONS.NAV
49
+ || (state.view === ORCHESTRATOR_VIEWS.HOME
50
+ && state.layoutMode === LAYOUT_MODES.MINIMAL);
51
+
52
+ if (navigatesNav) {
53
+ const delta = action.direction === "up" ? -1 : 1;
54
+ return {
55
+ ...state,
56
+ navIndex: clamp(state.navIndex + delta, 0, COCKPIT_NAV.length - 1)
57
+ };
58
+ }
59
+
60
+ const delta = action.direction === "up" ? -1 : 1;
61
+ const max = Math.max(0, (action.listLength ?? 1) - 1);
62
+ return { ...state, listIndex: clamp(state.listIndex + delta, 0, max) };
63
+ }
64
+ case "enter-nav": {
65
+ const item = COCKPIT_NAV[state.navIndex];
66
+ if (!item) return state;
67
+ if (item.view === ORCHESTRATOR_VIEWS.HOME) {
68
+ return {
69
+ ...state,
70
+ view: ORCHESTRATOR_VIEWS.HOME,
71
+ listIndex: 0,
72
+ region: COCKPIT_REGIONS.CONTENT
73
+ };
74
+ }
75
+ return {
76
+ ...state,
77
+ view: item.view,
78
+ listIndex: 0,
79
+ region: COCKPIT_REGIONS.CONTENT,
80
+ helpOpen: false
81
+ };
82
+ }
83
+ case "set-view":
84
+ return {
85
+ ...state,
86
+ view: action.view,
87
+ listIndex: 0,
88
+ region: action.region ?? COCKPIT_REGIONS.CONTENT
89
+ };
90
+ case "toggle-help":
91
+ if (state.helpOpen) {
92
+ return {
93
+ ...state,
94
+ helpOpen: false,
95
+ view: state.view === ORCHESTRATOR_VIEWS.HELP
96
+ ? ORCHESTRATOR_VIEWS.HOME
97
+ : state.view
98
+ };
99
+ }
100
+ return { ...state, helpOpen: true, view: ORCHESTRATOR_VIEWS.HELP };
101
+ case "escape": {
102
+ if (state.helpOpen) {
103
+ return { ...state, helpOpen: false, view: ORCHESTRATOR_VIEWS.HOME };
104
+ }
105
+ if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
106
+ const regions = regionsForLayout(state.layoutMode);
107
+ return {
108
+ ...state,
109
+ view: ORCHESTRATOR_VIEWS.HOME,
110
+ listIndex: 0,
111
+ region: regions.includes(COCKPIT_REGIONS.NAV)
112
+ ? COCKPIT_REGIONS.NAV
113
+ : COCKPIT_REGIONS.CONTENT
114
+ };
115
+ }
116
+ return { ...state, shouldExit: true };
117
+ }
118
+ case "clear-exit":
119
+ return { ...state, shouldExit: false };
120
+ default:
121
+ return state;
122
+ }
123
+ }
124
+
125
+ export function resolveNavAction(navIndex) {
126
+ return COCKPIT_NAV[navIndex] ?? null;
127
+ }
128
+
129
+ function clamp(value, min, max) {
130
+ return Math.min(max, Math.max(min, value));
131
+ }
@@ -0,0 +1,153 @@
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
+ if (helpOpen) {
134
+ return {
135
+ text: `Esc close help ${glyphs.bullet} R refresh ${glyphs.bullet} C cancel run`
136
+ };
137
+ }
138
+ if (view === ORCHESTRATOR_VIEWS.HOME) {
139
+ return {
140
+ text: `↑↓ Navigate ${glyphs.bullet} Tab Region ${glyphs.bullet} Enter Open ${glyphs.bullet} ? Help ${glyphs.bullet} Esc Exit`
141
+ };
142
+ }
143
+ const cancel = canCancel ? ` ${glyphs.bullet} C cancel` : "";
144
+ return {
145
+ text: `↑↓ Navigate ${glyphs.bullet} Enter Open ${glyphs.bullet} R refresh${cancel} ${glyphs.bullet} Esc Back · focus:${region}`
146
+ };
147
+ }
148
+
149
+ export function resolveProjectName(workspaceRoot = "") {
150
+ if (!workspaceRoot) return "project";
151
+ const parts = String(workspaceRoot).split(/[/\\]/).filter(Boolean);
152
+ return parts[parts.length - 1] || "project";
153
+ }