@kal-elsam/kairo-runtime 0.7.0 → 0.9.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 (42) hide show
  1. package/README.md +43 -11
  2. package/global-template/components/catalog.json +4 -1
  3. package/global-template/components/orchestrator/extensions/pi/kairo-minion.js +604 -0
  4. package/package.json +1 -1
  5. package/scripts/cockpit-smoke.mjs +4 -4
  6. package/src/cli.js +34 -2
  7. package/src/global/adapters/pi.js +1 -1
  8. package/src/global/global-doctor.js +2 -0
  9. package/src/global/ink/cockpit/primitives.js +96 -31
  10. package/src/global/ink/cockpit-alerts.js +36 -0
  11. package/src/global/ink/cockpit-changes.js +59 -35
  12. package/src/global/ink/cockpit-control-center.js +129 -51
  13. package/src/global/ink/cockpit-controller.js +58 -11
  14. package/src/global/ink/cockpit-focus.js +4 -2
  15. package/src/global/ink/cockpit-models.js +89 -47
  16. package/src/global/ink/cockpit-palette.js +98 -0
  17. package/src/global/ink/cockpit-path-label.js +19 -0
  18. package/src/global/ink/cockpit-recovery.js +78 -15
  19. package/src/global/ink/cockpit-reviews.js +14 -10
  20. package/src/global/ink/cockpit-runs.js +13 -4
  21. package/src/global/ink/cockpit-settings.js +194 -0
  22. package/src/global/ink/cockpit-views.js +88 -57
  23. package/src/global/ink/orchestrator-app.js +137 -46
  24. package/src/global/ink/orchestrator-state.js +24 -14
  25. package/src/global/ink/use-orchestrator-data.js +58 -0
  26. package/src/global/paths.js +3 -0
  27. package/src/global/runtime/alerts/alert-store.js +216 -0
  28. package/src/global/runtime/alerts/alert-types.js +59 -0
  29. package/src/global/runtime/alerts/alert-validate.js +117 -0
  30. package/src/global/runtime/execution-adapters/pi.js +12 -2
  31. package/src/global/runtime/monitor/monitor-cli.js +62 -0
  32. package/src/global/runtime/monitor/monitor-platform.js +95 -0
  33. package/src/global/runtime/monitor/monitor.js +249 -0
  34. package/src/global/runtime/orchestration/index.js +23 -0
  35. package/src/global/runtime/orchestration/orch-receipts.js +234 -0
  36. package/src/global/runtime/orchestration/orch-types.js +173 -0
  37. package/src/global/runtime/orchestration/orch-validate.js +63 -0
  38. package/src/global/runtime/run-cli.js +1 -0
  39. package/src/global/runtime/run-manager.js +59 -4
  40. package/src/global/runtime/run-strategy.js +71 -0
  41. package/src/global/runtime/run-supervisor.js +22 -2
  42. package/src/global/runtime/run-types.js +5 -1
@@ -0,0 +1,98 @@
1
+ import { COCKPIT_NAV } from "./cockpit-models.js";
2
+ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
3
+
4
+ export const PALETTE_KINDS = Object.freeze({
5
+ NAVIGATE: "navigate",
6
+ REFRESH: "refresh",
7
+ HELP: "help"
8
+ });
9
+
10
+ const DESTINATION_VIEWS = Object.freeze({
11
+ changes: ORCHESTRATOR_VIEWS.CHANGES,
12
+ governance: ORCHESTRATOR_VIEWS.CHANGES,
13
+ "control-center": ORCHESTRATOR_VIEWS.HOME,
14
+ ides: ORCHESTRATOR_VIEWS.IDES,
15
+ modules: ORCHESTRATOR_VIEWS.MODULES,
16
+ activity: ORCHESTRATOR_VIEWS.ACTIVITY,
17
+ profile: ORCHESTRATOR_VIEWS.PROFILE,
18
+ runs: ORCHESTRATOR_VIEWS.RUNS,
19
+ orchestration: ORCHESTRATOR_VIEWS.RUNS,
20
+ usage: ORCHESTRATOR_VIEWS.USAGE,
21
+ alerts: ORCHESTRATOR_VIEWS.ALERTS
22
+ });
23
+
24
+ export function resolvePaletteDestination(key) {
25
+ return DESTINATION_VIEWS[key] ?? null;
26
+ }
27
+
28
+ export function canOpenPalette({ loading = false, busy = false, confirming = false } = {}) {
29
+ return !loading && !busy && !confirming;
30
+ }
31
+
32
+ /** Optional CTA + six destinations + Refresh + Help. No write shortcuts. */
33
+ export function buildPaletteActions({
34
+ ctaDestination = null,
35
+ ctaTitle = null,
36
+ ctaDetail = null,
37
+ navItems = COCKPIT_NAV
38
+ } = {}) {
39
+ const actions = [];
40
+ const recommendedView = resolvePaletteDestination(ctaDestination);
41
+ if (recommendedView) {
42
+ actions.push({
43
+ id: "recommended",
44
+ kind: PALETTE_KINDS.NAVIGATE,
45
+ label: ctaTitle?.trim() || "Recommended next action",
46
+ view: recommendedView,
47
+ description: ctaDetail?.trim() || "Overview recommended destination."
48
+ });
49
+ }
50
+ for (const item of navItems) {
51
+ actions.push({
52
+ id: item.id,
53
+ kind: PALETTE_KINDS.NAVIGATE,
54
+ label: item.label,
55
+ view: item.view,
56
+ description: item.description
57
+ });
58
+ }
59
+ actions.push({
60
+ id: "alerts",
61
+ kind: PALETTE_KINDS.NAVIGATE,
62
+ label: "Alerts",
63
+ view: ORCHESTRATOR_VIEWS.ALERTS,
64
+ description: "Pending alert inbox."
65
+ });
66
+ actions.push({
67
+ id: "refresh",
68
+ kind: PALETTE_KINDS.REFRESH,
69
+ label: "Refresh",
70
+ view: null,
71
+ description: "Reload the read-only cockpit scan."
72
+ });
73
+ actions.push({
74
+ id: "help",
75
+ kind: PALETTE_KINDS.HELP,
76
+ label: "Help",
77
+ view: ORCHESTRATOR_VIEWS.HELP,
78
+ description: "Keyboard reference and primary flow."
79
+ });
80
+ return actions;
81
+ }
82
+
83
+ export function buildPaletteModel({ actions = buildPaletteActions(), index = 0, unicode = true } = {}) {
84
+ const max = Math.max(0, actions.length - 1);
85
+ const safeIndex = Math.min(Math.max(0, index), max);
86
+ const focus = unicode ? "›" : ">";
87
+ return {
88
+ title: "ACTIONS",
89
+ hint: "Navigate · Refresh · Help — no writes",
90
+ items: actions.map((action, i) => ({
91
+ ...action,
92
+ marker: i === safeIndex ? focus : " ",
93
+ selected: i === safeIndex
94
+ })),
95
+ selected: actions[safeIndex] ?? null,
96
+ index: safeIndex
97
+ };
98
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Distinguishable path for confirmation / detail disclosure.
3
+ * Prefer home-relative `~/...`; otherwise keep parent/basename.
4
+ */
5
+ export function formatConfirmPath(target, homeDir = null) {
6
+ const raw = String(target ?? "").trim();
7
+ if (!raw) return "target";
8
+ const normalized = raw.replaceAll("\\", "/");
9
+ if (normalized === "~" || normalized.startsWith("~/")) return normalized;
10
+
11
+ const home = homeDir ? String(homeDir).replaceAll("\\", "/").replace(/\/$/, "") : "";
12
+ if (home && (normalized === home || normalized.startsWith(`${home}/`))) {
13
+ return `~${normalized.slice(home.length)}`;
14
+ }
15
+
16
+ const parts = normalized.split("/").filter(Boolean);
17
+ if (parts.length >= 2) return parts.slice(-2).join("/");
18
+ return parts[0] || normalized;
19
+ }
@@ -1,3 +1,5 @@
1
+ import { formatConfirmPath } from "./cockpit-path-label.js";
2
+
1
3
  export const RECOVERY_PHASE = Object.freeze({
2
4
  IDLE: "idle",
3
5
  PREVIEWING: "previewing",
@@ -73,40 +75,101 @@ export function listRecoverySnapshots(snapshot) {
73
75
  return snapshot?.backups?.snapshots ?? [];
74
76
  }
75
77
 
76
- export function formatRecoveryLines({ snapshot, recoveryAction, listIndex = 0 }) {
78
+ function formatWhen(timestamp) {
79
+ if (!timestamp) return "unknown time";
80
+ const raw = String(timestamp);
81
+ if (/^\d{4}-\d{2}-\d{2}/.test(raw)) return raw.slice(0, 16).replace("T", " ");
82
+ const date = new Date(raw);
83
+ if (Number.isNaN(date.getTime())) return raw.slice(0, 16);
84
+ return date.toISOString().slice(0, 16).replace("T", " ");
85
+ }
86
+
87
+ function formatResult(action) {
88
+ const value = String(action ?? "").toLowerCase();
89
+ if (!value) return "done";
90
+ if (value === "applied" || value === "ok" || value === "success") return "ok";
91
+ if (value === "cancelled" || value === "canceled") return "cancelled";
92
+ if (value.includes("fail")) return "failed";
93
+ return value;
94
+ }
95
+
96
+ function shortName(name) {
97
+ const raw = String(name ?? "").trim();
98
+ if (!raw) return "snapshot";
99
+ const parts = raw.split(/[/\\]/).filter(Boolean);
100
+ return parts[parts.length - 1] || raw;
101
+ }
102
+
103
+ /**
104
+ * Activity surface: what agents did, when, and result.
105
+ * Snapshot restore stays secondary; paths only under DETAILS.
106
+ */
107
+ export function formatRecoveryLines({
108
+ snapshot,
109
+ recoveryAction,
110
+ listIndex = 0,
111
+ dashboard = null,
112
+ detail = false,
113
+ homeDir = null
114
+ } = {}) {
77
115
  const backups = listRecoverySnapshots(snapshot);
78
116
  const events = snapshot?.history?.events ?? [];
79
117
  const phase = recoveryAction?.phase ?? RECOVERY_PHASE.IDLE;
80
- const lines = [
81
- `Activity & recovery · ${phase}`,
82
- `History: ${events.length} recent event(s)`,
83
- ...events.slice(0, 3).map((event) => `${event.command ?? event.type ?? "event"} · ${event.action ?? ""} · ${event.timestamp ?? ""}`),
84
- "",
85
- `Snapshots: ${snapshot?.backups?.count ?? backups.length}`
86
- ];
118
+ const lines = ["RECENT"];
87
119
 
120
+ const recent = [];
121
+ for (const event of events.slice(0, 3)) {
122
+ recent.push(
123
+ `${formatWhen(event.timestamp)} · ${event.command ?? event.type ?? "event"} · ${formatResult(event.action)}`
124
+ );
125
+ }
126
+ for (const run of (dashboard?.recentRuns ?? []).slice(0, 2)) {
127
+ recent.push(
128
+ `${formatWhen(run.updatedAt ?? run.endedAt ?? run.startedAt)} · ${run.agentId ?? "agent"} · ${formatResult(run.state)}`
129
+ );
130
+ }
131
+ if (recent.length === 0) lines.push("No recent agent activity.");
132
+ else lines.push(...recent.slice(0, 4));
133
+
134
+ lines.push("", "SNAPSHOTS");
88
135
  if (backups.length === 0) {
89
136
  lines.push("No global snapshots yet.");
90
137
  } else {
91
138
  backups.forEach((entry, index) => {
92
139
  const mark = index === listIndex ? "›" : " ";
93
- lines.push(`${mark} ${entry.name} · ${entry.fileCount ?? "?"} files`);
140
+ lines.push(`${mark} ${shortName(entry.name)} · ${entry.fileCount ?? "?"} files`);
94
141
  });
95
142
  }
96
143
 
97
144
  if (recoveryAction?.message) lines.push("", recoveryAction.message);
145
+
98
146
  const preview = recoveryAction?.preview;
99
147
  if (preview) {
100
- lines.push(`Snapshot · ${preview.snapshot}`);
101
- for (const file of preview.files ?? []) lines.push(` restore · ${file.displayPath}`);
102
- if (preview.fingerprint) lines.push(`Fingerprint · ${preview.fingerprint.slice(0, 12)}…`);
148
+ const fileCount = preview.files?.length ?? 0;
149
+ lines.push(`Restore preview · ${fileCount} file(s)`);
150
+ if (detail || phase === RECOVERY_PHASE.CONFIRMING) {
151
+ lines.push("DETAILS");
152
+ const limit = detail ? 12 : 3;
153
+ for (const file of (preview.files ?? []).slice(0, limit)) {
154
+ lines.push(formatConfirmPath(file.displayPath ?? file.path, homeDir));
155
+ }
156
+ if ((preview.files?.length ?? 0) > limit) {
157
+ lines.push(`… ${preview.files.length - limit} more`);
158
+ }
159
+ }
103
160
  }
161
+
104
162
  const receipt = recoveryAction?.receipt;
105
163
  if (receipt) {
106
- lines.push("", `Receipt · ${receipt.action}`);
107
- if (receipt.safetyBackup) lines.push(`Safety backup · ${receipt.safetyBackup}`);
108
- for (const path of receipt.restored ?? []) lines.push(` restored · ${path}`);
164
+ const restored = receipt.restored?.length ?? 0;
165
+ lines.push("", `Result · ${receipt.action ?? "rollback"} · ${restored} restored`);
166
+ if (detail && receipt.safetyBackup) {
167
+ lines.push("DETAILS", `Safety backup retained`);
168
+ } else if (receipt.safetyBackup) {
169
+ lines.push("Safety backup retained");
170
+ }
109
171
  }
172
+
110
173
  if (phase === RECOVERY_PHASE.IDLE || phase === RECOVERY_PHASE.COMPLETED || phase === RECOVERY_PHASE.FAILED) {
111
174
  lines.push("", "Enter preview · Y restore · N/Esc cancel · R re-scan");
112
175
  }
@@ -22,8 +22,10 @@ export function formatReviewListLines(receipts = []) {
22
22
  return receipts.map((receipt) => {
23
23
  const counts = countFindingsBySeverity(receipt.findings);
24
24
  const findingTotal = (receipt.findings ?? []).length;
25
- const created = String(receipt.createdAt ?? "").slice(0, 19).replace("T", " ");
26
- return `${receipt.reviewId} ${String(receipt.state).padEnd(10)} ${String(receipt.agentId).padEnd(6)} ${findingTotal}f (h${counts.high}/m${counts.medium}/l${counts.low}) ${created}`;
25
+ const created = String(receipt.createdAt ?? "").slice(0, 16).replace("T", " ");
26
+ const agent = String(receipt.agentId ?? "agent");
27
+ const state = String(receipt.state ?? "unknown");
28
+ return `${agent} · ${state} · ${findingTotal} findings (h${counts.high}/m${counts.medium}/l${counts.low}) · ${created || "unknown time"}`;
27
29
  });
28
30
  }
29
31
 
@@ -33,15 +35,17 @@ export function formatReviewDetailLines(receipt) {
33
35
  const snapshot = receipt.snapshot ?? {};
34
36
  const totals = snapshot.totals ?? {};
35
37
  const lines = [
36
- `Id: ${receipt.reviewId}`,
37
- `Agent: ${receipt.agentId}${receipt.model ? ` · model ${receipt.model}` : ""}`,
38
- `State: ${receipt.state}`,
39
- `Created: ${receipt.createdAt ?? "n/a"}`,
40
- `Findings: ${(receipt.findings ?? []).length} (high ${counts.high}, medium ${counts.medium}, low ${counts.low})`,
41
- `Snapshot: ${snapshot.mode ?? "n/a"} · files ${totals.fileCount ?? 0} · fingerprint ${String(snapshot.fingerprint ?? "").slice(0, 12) || "n/a"}`
38
+ "SUMMARY",
39
+ `${receipt.agentId}${receipt.model ? ` · ${receipt.model}` : ""} · ${receipt.state}`,
40
+ `Findings · ${(receipt.findings ?? []).length} (high ${counts.high}, medium ${counts.medium}, low ${counts.low})`,
41
+ `Created · ${receipt.createdAt ?? "n/a"}`,
42
+ "",
43
+ "DETAILS",
44
+ `Id · ${receipt.reviewId}`,
45
+ `Snapshot · ${snapshot.mode ?? "n/a"} · ${totals.fileCount ?? 0} files`
42
46
  ];
43
47
  if ((receipt.warnings ?? []).length > 0) {
44
- lines.push(`Warnings: ${receipt.warnings.length}`);
48
+ lines.push(`Warnings · ${receipt.warnings.length}`);
45
49
  }
46
50
  const findings = receipt.findings ?? [];
47
51
  if (findings.length === 0) {
@@ -56,7 +60,7 @@ export function formatReviewDetailLines(receipt) {
56
60
  );
57
61
  }
58
62
  if (findings.length > 20) {
59
- lines.push(` … ${findings.length - 20} more (use kairo reviews show ${receipt.reviewId})`);
63
+ lines.push(` … ${findings.length - 20} more`);
60
64
  }
61
65
  return lines;
62
66
  }
@@ -5,19 +5,19 @@ export const RUNS_HUB_ITEMS = [
5
5
  id: "active",
6
6
  label: "Active runs",
7
7
  view: ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
8
- description: "Inspect and cancel supervised runs that are currently executing."
8
+ description: "Inspect and cancel supervised runs in flight."
9
9
  },
10
10
  {
11
11
  id: "history",
12
12
  label: "History",
13
13
  view: ORCHESTRATOR_VIEWS.RECENT_RUNS,
14
- description: "Review completed and failed run outcomes."
14
+ description: "Completed and failed outcomes."
15
15
  },
16
16
  {
17
17
  id: "reviews",
18
18
  label: "Reviews",
19
19
  view: ORCHESTRATOR_VIEWS.REVIEWS,
20
- description: "Browse secret-free bounded-review receipts (read-only)."
20
+ description: "Secret-free review receipts (read-only)."
21
21
  },
22
22
  {
23
23
  id: "launch",
@@ -51,6 +51,15 @@ export function resolveRunsHubItem(listIndex = 0, items = RUNS_HUB_ITEMS) {
51
51
  return items[index] ?? null;
52
52
  }
53
53
 
54
+ /** Selectable hub labels only — counts belong in the panel title/hint. */
54
55
  export function formatRunsHubLines(items = RUNS_HUB_ITEMS) {
55
- return items.map((item) => `${item.label} — ${item.description}`);
56
+ return items.map((item) => item.label);
57
+ }
58
+
59
+ export function formatOrchestrationStatus({
60
+ active = 0,
61
+ recent = 0,
62
+ reviews = 0
63
+ } = {}) {
64
+ return `${active} active · ${recent} recent · ${reviews} reviews`;
56
65
  }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Curated Settings catalog — browse → preview → confirm (no filesystem install).
3
+ */
4
+
5
+ export const SETTINGS_PHASE = Object.freeze({
6
+ BROWSE: "browse",
7
+ PREVIEW: "preview",
8
+ CONFIRMING: "confirming",
9
+ COMPLETED: "completed"
10
+ });
11
+
12
+ export const CURATED_INTEGRATIONS = Object.freeze([
13
+ Object.freeze({
14
+ id: "pi-usage-widget",
15
+ name: "Pi usage widget",
16
+ version: "0.2.1",
17
+ license: "MIT",
18
+ status: "available",
19
+ capabilities: Object.freeze(["usage-display", "local-only"]),
20
+ permissions: Object.freeze(["full-local-access-on-install"]),
21
+ audit: "pending-security-review",
22
+ summary: "Local Pi package for usage visibility in the agent UI.",
23
+ notes: "Explicit install only. Never auto-applied by Kairo."
24
+ })
25
+ ]);
26
+
27
+ export function createSettingsActionState() {
28
+ return {
29
+ phase: SETTINGS_PHASE.BROWSE,
30
+ selectedId: null,
31
+ message: null,
32
+ receipt: null
33
+ };
34
+ }
35
+
36
+ export function listCuratedIntegrations() {
37
+ return [...CURATED_INTEGRATIONS];
38
+ }
39
+
40
+ export function getCuratedIntegration(id) {
41
+ return CURATED_INTEGRATIONS.find((entry) => entry.id === id) ?? null;
42
+ }
43
+
44
+ export function reduceSettingsAction(state, action) {
45
+ switch (action.type) {
46
+ case "reset":
47
+ return createSettingsActionState();
48
+ case "preview": {
49
+ const entry = getCuratedIntegration(action.id);
50
+ if (!entry) {
51
+ return { ...state, phase: SETTINGS_PHASE.BROWSE, message: "Integration not found." };
52
+ }
53
+ return {
54
+ phase: SETTINGS_PHASE.PREVIEW,
55
+ selectedId: entry.id,
56
+ message: "Enter Confirm · Esc back — no files written yet.",
57
+ receipt: null
58
+ };
59
+ }
60
+ case "confirm-prompt":
61
+ if (state.phase !== SETTINGS_PHASE.PREVIEW || !state.selectedId) return state;
62
+ return {
63
+ ...state,
64
+ phase: SETTINGS_PHASE.CONFIRMING,
65
+ message: "Confirm explicit install intent? Y confirm · N/Esc cancel"
66
+ };
67
+ case "confirm": {
68
+ if (state.phase !== SETTINGS_PHASE.CONFIRMING || !state.selectedId) return state;
69
+ const entry = getCuratedIntegration(state.selectedId);
70
+ return {
71
+ phase: SETTINGS_PHASE.COMPLETED,
72
+ selectedId: state.selectedId,
73
+ message: "Confirmed — no auto-install. Apply via documented Pi/CLI path only.",
74
+ receipt: {
75
+ id: state.selectedId,
76
+ version: entry?.version ?? null,
77
+ confirmedAt: new Date().toISOString(),
78
+ wroteFiles: false
79
+ }
80
+ };
81
+ }
82
+ case "cancel":
83
+ return {
84
+ ...createSettingsActionState(),
85
+ message: "Cancelled — no files written."
86
+ };
87
+ default:
88
+ return state;
89
+ }
90
+ }
91
+
92
+ export function formatSettingsBrowseLines(
93
+ integrations = listCuratedIntegrations(),
94
+ listIndex = 0
95
+ ) {
96
+ if (integrations.length === 0) return ["No curated integrations available."];
97
+ return [
98
+ "CURATED INTEGRATIONS",
99
+ ...integrations.map((entry, index) => {
100
+ const mark = index === listIndex ? "›" : " ";
101
+ return `${mark} ${entry.status} · ${entry.name} · ${entry.version} · ${entry.license}`;
102
+ }),
103
+ "",
104
+ "POLICY",
105
+ "Browse → preview → confirm. Install stays explicit; never automatic.",
106
+ "Enter preview · Esc back"
107
+ ];
108
+ }
109
+
110
+ export function formatSettingsDetailLines(entry, settingsAction = null) {
111
+ if (!entry) return ["Integration not found."];
112
+ const phase = settingsAction?.phase ?? SETTINGS_PHASE.BROWSE;
113
+
114
+ // Completed: receipt first so compact TTY (80×24) shows wroteFiles above the fold.
115
+ if (phase === SETTINGS_PHASE.COMPLETED && settingsAction?.receipt) {
116
+ const receipt = settingsAction.receipt;
117
+ return [
118
+ "RESULT",
119
+ settingsAction.message ?? "Confirmed — no auto-install.",
120
+ "RECEIPT",
121
+ `Id · ${receipt.id} · wroteFiles · ${receipt.wroteFiles}`,
122
+ `Confirmed · ${receipt.confirmedAt}`,
123
+ `${entry.name} · ${entry.version} · ${entry.license}`,
124
+ "Esc back · install stays explicit"
125
+ ];
126
+ }
127
+
128
+ const lines = [
129
+ "INTEGRATION",
130
+ `${entry.name} · ${entry.version}`,
131
+ `Status · ${entry.status} · License · ${entry.license}`,
132
+ `Audit · ${entry.audit}`,
133
+ "",
134
+ "CAPABILITIES",
135
+ entry.capabilities.join(" · ") || "none",
136
+ "",
137
+ "PERMISSIONS",
138
+ entry.permissions.join(" · ") || "none",
139
+ "",
140
+ "SUMMARY",
141
+ entry.summary,
142
+ entry.notes
143
+ ];
144
+ if (phase === SETTINGS_PHASE.PREVIEW || phase === SETTINGS_PHASE.CONFIRMING) {
145
+ lines.push("", "PREVIEW", "No filesystem changes. Confirm only records explicit intent.");
146
+ }
147
+ if (settingsAction?.message) lines.push("", settingsAction.message);
148
+ return lines;
149
+ }
150
+
151
+ export function formatSettingsLines({
152
+ listIndex = 0,
153
+ settingsAction = null,
154
+ snapshot = null,
155
+ diagnostics = null
156
+ } = {}) {
157
+ const integrations = listCuratedIntegrations();
158
+ const phase = settingsAction?.phase ?? SETTINGS_PHASE.BROWSE;
159
+ if (phase !== SETTINGS_PHASE.BROWSE && settingsAction?.selectedId) {
160
+ return formatSettingsDetailLines(
161
+ getCuratedIntegration(settingsAction.selectedId),
162
+ settingsAction
163
+ );
164
+ }
165
+
166
+ const policy = snapshot?.policy;
167
+ const sources = diagnostics?.profile?.sources;
168
+ const sourceLabel = sources?.global || sources?.project
169
+ ? [sources.global ? "global" : null, sources.project ? "project" : null].filter(Boolean).join(", ")
170
+ : "none";
171
+ return [
172
+ ...formatSettingsBrowseLines(integrations, listIndex),
173
+ "",
174
+ "PROFILE & POLICY",
175
+ `Policy · ${policy?.profile ?? "none"} · apply ${policy?.applyMode ?? "n/a"}`,
176
+ `Preflight · ${policy?.preflight ?? "n/a"} · sources · ${sourceLabel}`,
177
+ "",
178
+ `Selected · ${integrations[listIndex]?.id ?? "none"}`
179
+ ];
180
+ }
181
+
182
+ export function buildSettingsFooterParts(phase = SETTINGS_PHASE.BROWSE) {
183
+ switch (phase) {
184
+ case SETTINGS_PHASE.PREVIEW:
185
+ return ["Enter Confirm", "Esc Back"];
186
+ case SETTINGS_PHASE.CONFIRMING:
187
+ return ["Y Confirm", "N/Esc Cancel"];
188
+ case SETTINGS_PHASE.COMPLETED:
189
+ return ["Esc Back", "/ Actions"];
190
+ case SETTINGS_PHASE.BROWSE:
191
+ default:
192
+ return ["↑↓ Select", "Enter Preview", "Esc Nav", "/ Actions"];
193
+ }
194
+ }