@kal-elsam/kairo-runtime 0.13.0 → 0.14.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/CHANGELOG.md +1202 -0
- package/LICENSE +21 -0
- package/README.md +64 -1192
- package/package.json +9 -3
- package/scripts/cockpit-smoke.mjs +14 -10
- package/src/cli.js +25 -142
- package/src/global/cli-help.js +182 -0
- package/src/global/ink/cockpit-controller.js +1 -3
- package/src/global/ink/cockpit-enter.js +3 -1
- package/src/global/ink/cockpit-focus.js +7 -1
- package/src/global/ink/cockpit-models.js +30 -21
- package/src/global/ink/cockpit-palette.js +8 -3
- package/src/global/ink/cockpit-views.js +9 -2
- package/src/global/ink/orchestrator-app.js +21 -2
- package/src/global/ink/ux/live-overview.js +120 -72
- package/src/global/ink/ux/overview-actions.js +80 -0
- package/src/global/ink/ux/overview-needs.js +160 -0
- package/src/global/self-update.js +216 -0
|
@@ -17,23 +17,33 @@ export const COCKPIT_REGIONS = {
|
|
|
17
17
|
|
|
18
18
|
export const COCKPIT_NAV = [
|
|
19
19
|
{
|
|
20
|
-
id: "
|
|
21
|
-
label: "
|
|
20
|
+
id: "home",
|
|
21
|
+
label: "Home",
|
|
22
22
|
view: ORCHESTRATOR_VIEWS.HOME,
|
|
23
|
-
description: "
|
|
23
|
+
description: "What needs you, and what to do about it."
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
id: "settings",
|
|
27
|
+
label: "Settings",
|
|
28
|
+
view: ORCHESTRATOR_VIEWS.PROFILE,
|
|
29
|
+
description: "Choose agents, connect Obsidian, add integrations."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "history",
|
|
33
|
+
label: "History",
|
|
34
|
+
view: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
35
|
+
description: "What Kairo changed, and how to undo it."
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Destinations reachable via the action palette, not the top nav. */
|
|
40
|
+
export const COCKPIT_SECONDARY = [
|
|
25
41
|
{
|
|
26
42
|
id: "governance",
|
|
27
43
|
label: "Governance",
|
|
28
44
|
view: ORCHESTRATOR_VIEWS.CHANGES,
|
|
29
45
|
description: "Repair drift and apply governed changes."
|
|
30
46
|
},
|
|
31
|
-
{
|
|
32
|
-
id: "activity",
|
|
33
|
-
label: "Activity",
|
|
34
|
-
view: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
35
|
-
description: "Operations, backups, and recovery."
|
|
36
|
-
},
|
|
37
47
|
{
|
|
38
48
|
id: "orchestration",
|
|
39
49
|
label: "Orchestration",
|
|
@@ -45,12 +55,6 @@ export const COCKPIT_NAV = [
|
|
|
45
55
|
label: "Usage",
|
|
46
56
|
view: ORCHESTRATOR_VIEWS.USAGE,
|
|
47
57
|
description: "Token and context pressure when auditable."
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: "settings",
|
|
51
|
-
label: "Settings",
|
|
52
|
-
view: ORCHESTRATOR_VIEWS.PROFILE,
|
|
53
|
-
description: "Profile, policy, and guided configuration."
|
|
54
58
|
}
|
|
55
59
|
];
|
|
56
60
|
|
|
@@ -64,8 +68,8 @@ export function regionsForLayout(layoutMode) {
|
|
|
64
68
|
|
|
65
69
|
export function navIndexForView(view, items = COCKPIT_NAV) {
|
|
66
70
|
if (isRunsBranchView(view)) {
|
|
67
|
-
const
|
|
68
|
-
return
|
|
71
|
+
const historyIndex = items.findIndex((item) => item.id === "history");
|
|
72
|
+
return historyIndex >= 0 ? historyIndex : 0;
|
|
69
73
|
}
|
|
70
74
|
const index = items.findIndex((item) => item.view === view);
|
|
71
75
|
return index >= 0 ? index : 0;
|
|
@@ -105,6 +109,7 @@ export function resolveNavStatusSummary(item, {
|
|
|
105
109
|
const backups = snapshot?.backups?.count ?? 0;
|
|
106
110
|
|
|
107
111
|
switch (item.id) {
|
|
112
|
+
case "home":
|
|
108
113
|
case "overview":
|
|
109
114
|
return snapshot?.health?.replaceAll("_", " ")
|
|
110
115
|
?? resolveProjectReadiness({
|
|
@@ -114,6 +119,7 @@ export function resolveNavStatusSummary(item, {
|
|
|
114
119
|
}).label;
|
|
115
120
|
case "governance":
|
|
116
121
|
return changes > 0 ? `${changes} pending` : "Clean";
|
|
122
|
+
case "history":
|
|
117
123
|
case "activity":
|
|
118
124
|
return backups > 0 ? `${backups} backups` : "No backups";
|
|
119
125
|
case "orchestration":
|
|
@@ -174,6 +180,7 @@ export function buildNavModel({
|
|
|
174
180
|
const mapped = items.map((item, index) => {
|
|
175
181
|
const isSelected = index === navIndex;
|
|
176
182
|
const isCurrent = item.view === currentView
|
|
183
|
+
|| (item.id === "history" && isRunsBranchView(currentView))
|
|
177
184
|
|| (item.id === "orchestration" && isRunsBranchView(currentView));
|
|
178
185
|
return {
|
|
179
186
|
...item,
|
|
@@ -185,11 +192,13 @@ export function buildNavModel({
|
|
|
185
192
|
};
|
|
186
193
|
});
|
|
187
194
|
|
|
195
|
+
const explanation = !selected || selected.id === "home" || selected.id === "overview"
|
|
196
|
+
? ""
|
|
197
|
+
: (selected.description ?? "");
|
|
198
|
+
|
|
188
199
|
return {
|
|
189
200
|
title: "NAVIGATION",
|
|
190
|
-
explanation
|
|
191
|
-
? `${selected.description} (${resolveNavStatusSummary(selected, { dashboard, diagnostics, snapshot })})`
|
|
192
|
-
: "",
|
|
201
|
+
explanation,
|
|
193
202
|
items: mapped
|
|
194
203
|
};
|
|
195
204
|
}
|
|
@@ -320,7 +329,7 @@ export function buildFooterModel({
|
|
|
320
329
|
// HOME footer must stay on one line at 80 cols (frame already near 24 rows).
|
|
321
330
|
if (view === ORCHESTRATOR_VIEWS.HOME) {
|
|
322
331
|
return {
|
|
323
|
-
text: ["↑↓", "Enter", "Space", "R", "?", "/", "Esc"].join(` ${glyphs.bullet} `),
|
|
332
|
+
text: ["↑↓", "Enter", "Tab", "Space", "R", "?", "/", "Esc"].join(` ${glyphs.bullet} `),
|
|
324
333
|
columns: footerColumns
|
|
325
334
|
};
|
|
326
335
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { COCKPIT_NAV } from "./cockpit-models.js";
|
|
1
|
+
import { COCKPIT_NAV, COCKPIT_SECONDARY } from "./cockpit-models.js";
|
|
2
2
|
import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
3
3
|
|
|
4
|
+
const PALETTE_NAV_ITEMS = [...COCKPIT_NAV, ...COCKPIT_SECONDARY];
|
|
5
|
+
|
|
4
6
|
export const PALETTE_KINDS = Object.freeze({
|
|
5
7
|
NAVIGATE: "navigate",
|
|
6
8
|
REFRESH: "refresh",
|
|
@@ -12,10 +14,13 @@ const DESTINATION_VIEWS = Object.freeze({
|
|
|
12
14
|
changes: ORCHESTRATOR_VIEWS.CHANGES,
|
|
13
15
|
governance: ORCHESTRATOR_VIEWS.CHANGES,
|
|
14
16
|
"control-center": ORCHESTRATOR_VIEWS.HOME,
|
|
17
|
+
home: ORCHESTRATOR_VIEWS.HOME,
|
|
15
18
|
ides: ORCHESTRATOR_VIEWS.IDES,
|
|
16
19
|
modules: ORCHESTRATOR_VIEWS.MODULES,
|
|
17
20
|
activity: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
21
|
+
history: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
18
22
|
profile: ORCHESTRATOR_VIEWS.PROFILE,
|
|
23
|
+
settings: ORCHESTRATOR_VIEWS.PROFILE,
|
|
19
24
|
runs: ORCHESTRATOR_VIEWS.RUNS,
|
|
20
25
|
orchestration: ORCHESTRATOR_VIEWS.RUNS,
|
|
21
26
|
usage: ORCHESTRATOR_VIEWS.USAGE,
|
|
@@ -30,12 +35,12 @@ export function canOpenPalette({ loading = false, busy = false, confirming = fal
|
|
|
30
35
|
return !loading && !busy && !confirming;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
/** Optional CTA +
|
|
38
|
+
/** Optional CTA + primary/secondary destinations + Refresh + Help. No write shortcuts. */
|
|
34
39
|
export function buildPaletteActions({
|
|
35
40
|
ctaDestination = null,
|
|
36
41
|
ctaTitle = null,
|
|
37
42
|
ctaDetail = null,
|
|
38
|
-
navItems =
|
|
43
|
+
navItems = PALETTE_NAV_ITEMS
|
|
39
44
|
} = {}) {
|
|
40
45
|
const actions = [];
|
|
41
46
|
if (ctaDestination === "setup") {
|
|
@@ -100,7 +100,8 @@ export function renderCockpitView({
|
|
|
100
100
|
governanceDetailsOpen = false,
|
|
101
101
|
activityDetailsOpen = false,
|
|
102
102
|
contentFocused = false,
|
|
103
|
-
homeDir = null
|
|
103
|
+
homeDir = null,
|
|
104
|
+
hasGlobalState = false
|
|
104
105
|
}) {
|
|
105
106
|
if (palette) {
|
|
106
107
|
return React.createElement(PalettePanel, { model: palette, colorEnabled });
|
|
@@ -112,7 +113,13 @@ export function renderCockpitView({
|
|
|
112
113
|
detailsOpen: overviewDetailsOpen,
|
|
113
114
|
colorEnabled,
|
|
114
115
|
unicode,
|
|
115
|
-
layoutMode
|
|
116
|
+
layoutMode,
|
|
117
|
+
selectedIndex: listIndex,
|
|
118
|
+
contentFocused,
|
|
119
|
+
hasGlobalState,
|
|
120
|
+
snapshot,
|
|
121
|
+
diagnostics,
|
|
122
|
+
dashboard
|
|
116
123
|
});
|
|
117
124
|
case ORCHESTRATOR_VIEWS.USAGE:
|
|
118
125
|
return React.createElement(SemanticUsagePanel, {
|
|
@@ -46,6 +46,7 @@ import { LAYOUT_MODES } from "./layout.js";
|
|
|
46
46
|
import { CHANGES_PHASE } from "./cockpit-changes.js";
|
|
47
47
|
import { RECOVERY_PHASE, listRecoverySnapshots } from "./cockpit-recovery.js";
|
|
48
48
|
import { SETTINGS_PHASE } from "./cockpit-settings.js";
|
|
49
|
+
import { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./ux/overview-actions.js";
|
|
49
50
|
|
|
50
51
|
export function OrchestratorApp({
|
|
51
52
|
homeDir,
|
|
@@ -245,7 +246,9 @@ export function OrchestratorApp({
|
|
|
245
246
|
return;
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
const listLength = ui.view === ORCHESTRATOR_VIEWS.
|
|
249
|
+
const listLength = ui.view === ORCHESTRATOR_VIEWS.HOME
|
|
250
|
+
? OVERVIEW_BUTTON_COUNT
|
|
251
|
+
: ui.view === ORCHESTRATOR_VIEWS.RUNS
|
|
249
252
|
? RUNS_HUB_ITEMS.length
|
|
250
253
|
: ui.view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
|
|
251
254
|
? (data.dashboard?.activeRuns ?? []).length
|
|
@@ -301,6 +304,21 @@ export function OrchestratorApp({
|
|
|
301
304
|
return;
|
|
302
305
|
}
|
|
303
306
|
}
|
|
307
|
+
if (routed.type === "enter-home-button") {
|
|
308
|
+
const buttons = buildOverviewButtons({
|
|
309
|
+
hasGlobalState,
|
|
310
|
+
snapshot: data.snapshot,
|
|
311
|
+
diagnostics: data.diagnostics,
|
|
312
|
+
dashboard: data.dashboard
|
|
313
|
+
});
|
|
314
|
+
const selected = buttons[Math.min(Math.max(0, ui.listIndex), buttons.length - 1)];
|
|
315
|
+
const intent = selected?.intent ?? null;
|
|
316
|
+
if (intent === "setup") {
|
|
317
|
+
finish({ cancelled: false, action: "setup" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (intent && openDestination(intent)) return;
|
|
321
|
+
}
|
|
304
322
|
dispatch(routed);
|
|
305
323
|
return;
|
|
306
324
|
}
|
|
@@ -583,7 +601,8 @@ export function OrchestratorApp({
|
|
|
583
601
|
governanceDetailsOpen: ui.governanceDetailsOpen,
|
|
584
602
|
activityDetailsOpen: ui.activityDetailsOpen,
|
|
585
603
|
contentFocused: ui.region === COCKPIT_REGIONS.CONTENT,
|
|
586
|
-
homeDir
|
|
604
|
+
homeDir,
|
|
605
|
+
hasGlobalState
|
|
587
606
|
})
|
|
588
607
|
)
|
|
589
608
|
);
|
|
@@ -1,99 +1,121 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live semantic Overview for Cockpit HOME — product cover.
|
|
3
|
-
*
|
|
3
|
+
* Content owns the button focus mark when region=content.
|
|
4
4
|
* ASCII wordmark only here (wide/compact); minimal is textual.
|
|
5
|
+
*
|
|
6
|
+
* Rule: show purpose + two buttons + a few plain-language needs.
|
|
7
|
+
* Machine/system noise stays out of the first screen (Details only).
|
|
5
8
|
*/
|
|
6
9
|
import React from "react";
|
|
7
10
|
import { Box, Text } from "ink";
|
|
8
|
-
import {
|
|
11
|
+
import { DASHBOARD_PURPOSE } from "../../dashboard-guidance.js";
|
|
9
12
|
import { LAYOUT_MODES } from "../layout.js";
|
|
10
|
-
import { COCKPIT_COLORS } from "../theme.js";
|
|
13
|
+
import { COCKPIT_COLORS, resolveGlyphs } from "../theme.js";
|
|
11
14
|
import {
|
|
12
15
|
overviewBrandTitle,
|
|
13
16
|
shouldShowWordmark,
|
|
14
17
|
wordmarkLines
|
|
15
18
|
} from "../brand/wordmark.js";
|
|
16
19
|
import { ActionList, Callout, Details } from "./semantic.js";
|
|
20
|
+
import {
|
|
21
|
+
humanizeDestination,
|
|
22
|
+
humanizeHealthTitle,
|
|
23
|
+
humanizePrimary,
|
|
24
|
+
mapHealthTone,
|
|
25
|
+
partitionCompanionLines
|
|
26
|
+
} from "./overview-needs.js";
|
|
27
|
+
import { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./overview-actions.js";
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
usage: "Usage",
|
|
26
|
-
profile: "Settings"
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function mapHealthTone(kind) {
|
|
30
|
-
switch (kind) {
|
|
31
|
-
case CONTROL_PLANE_HEALTH.CHECK_FAILED:
|
|
32
|
-
return "danger";
|
|
33
|
-
case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
|
|
34
|
-
case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
|
|
35
|
-
case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
|
|
36
|
-
return "warn";
|
|
37
|
-
case CONTROL_PLANE_HEALTH.HEALTHY:
|
|
38
|
-
return "ready";
|
|
39
|
-
default:
|
|
40
|
-
return "warn";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
29
|
+
export {
|
|
30
|
+
humanizeCompanionNeed,
|
|
31
|
+
humanizeHealthTitle,
|
|
32
|
+
humanizePrimary,
|
|
33
|
+
mapHealthTone,
|
|
34
|
+
partitionCompanionLines
|
|
35
|
+
} from "./overview-needs.js";
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
if (!destination) return null;
|
|
46
|
-
return DESTINATION_LABELS[destination] ?? null;
|
|
47
|
-
}
|
|
37
|
+
export { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./overview-actions.js";
|
|
48
38
|
|
|
49
|
-
/** Safe Details lines
|
|
50
|
-
export function buildOverviewDetails(model = {}) {
|
|
39
|
+
/** Safe Details lines — leftovers + raw signals; never invent paths/IDs. */
|
|
40
|
+
export function buildOverviewDetails(model = {}, companionRest = []) {
|
|
51
41
|
const lines = [];
|
|
52
42
|
const next = model.nextAction ?? model.cta ?? {};
|
|
53
43
|
const dest = humanizeDestination(next.destination);
|
|
54
|
-
if (dest) lines.push(`
|
|
55
|
-
if (typeof model.alerts?.count === "number") {
|
|
44
|
+
if (dest) lines.push(`Opens · ${dest}`);
|
|
45
|
+
if (typeof model.alerts?.count === "number" && model.alerts.count > 0) {
|
|
56
46
|
lines.push(`Open alerts · ${model.alerts.count}`);
|
|
57
47
|
}
|
|
48
|
+
const tokens = model.tokens?.headline;
|
|
49
|
+
if (typeof tokens === "string" && tokens && !/unavailable/i.test(tokens)) {
|
|
50
|
+
lines.push(`Tokens · ${tokens}`);
|
|
51
|
+
}
|
|
58
52
|
const secondary = model.companionNextAction;
|
|
59
53
|
if (secondary?.title && secondary.kind !== "idle") {
|
|
60
54
|
lines.push(`Companion · ${secondary.title}`);
|
|
61
55
|
}
|
|
62
|
-
|
|
56
|
+
for (const line of companionRest) {
|
|
57
|
+
if (typeof line === "string" && line.trim()) lines.push(line);
|
|
58
|
+
}
|
|
59
|
+
if (lines.length === 0) return ["Nothing else to show right now."];
|
|
63
60
|
return lines;
|
|
64
61
|
}
|
|
65
62
|
|
|
66
63
|
/**
|
|
67
64
|
* Pure adapter: buildControlCenterModel → semantic overview props.
|
|
68
|
-
*
|
|
69
|
-
* Primary action is always governance CTA — companion is secondary metrics/details only.
|
|
65
|
+
* First screen = purpose + two buttons + plain needs. Machine noise → Details.
|
|
70
66
|
*/
|
|
71
|
-
export function adaptControlCenterToOverview(model = {}) {
|
|
67
|
+
export function adaptControlCenterToOverview(model = {}, options = {}) {
|
|
72
68
|
const status = model.status ?? model.health ?? {};
|
|
73
69
|
const next = model.nextAction ?? model.cta ?? {};
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
const { needs, rest } = partitionCompanionLines(model.companion?.lines ?? []);
|
|
71
|
+
const primary = humanizePrimary(next);
|
|
72
|
+
const buttons = buildOverviewButtons({
|
|
73
|
+
hasGlobalState: options.hasGlobalState,
|
|
74
|
+
snapshot: options.snapshot,
|
|
75
|
+
diagnostics: options.diagnostics,
|
|
76
|
+
dashboard: options.dashboard
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const activity = model.activity?.headline;
|
|
80
|
+
const metrics = [];
|
|
81
|
+
const hasUsefulActivity = typeof activity === "string"
|
|
82
|
+
&& activity
|
|
83
|
+
&& activity !== "Idle"
|
|
84
|
+
&& activity !== "No activity yet";
|
|
85
|
+
if (hasUsefulActivity) {
|
|
86
|
+
metrics.push({ id: "activity", label: `Last activity · ${activity.replace(/^Last ·\s*/, "")}` });
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < needs.length; i += 1) {
|
|
89
|
+
metrics.push({ id: `need-${i}`, label: needs[i] });
|
|
90
|
+
}
|
|
91
|
+
if ((model.alerts?.count ?? 0) > 0) {
|
|
92
|
+
metrics.push({
|
|
93
|
+
id: "alerts",
|
|
94
|
+
label: `Alerts · ${model.alerts.headline ?? `${model.alerts.count} open`}`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (rest.length > 0) {
|
|
98
|
+
metrics.push({
|
|
99
|
+
id: "more",
|
|
100
|
+
label: `${rest.length} more in Details · Space`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (metrics.length === 0) {
|
|
104
|
+
metrics.push({ id: "quiet", label: "Nothing else needs you right now" });
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
return {
|
|
79
108
|
title: model.title ?? "Overview",
|
|
109
|
+
purpose: DASHBOARD_PURPOSE,
|
|
80
110
|
callout: {
|
|
81
111
|
tone: mapHealthTone(status.kind),
|
|
82
|
-
title: status.label
|
|
112
|
+
title: humanizeHealthTitle(status.kind, status.label),
|
|
83
113
|
body: status.summaryLine ?? ""
|
|
84
114
|
},
|
|
85
|
-
primary
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
},
|
|
90
|
-
metrics: [
|
|
91
|
-
{ id: "activity", label: `Activity · ${model.activity?.headline ?? "Idle"}` },
|
|
92
|
-
{ id: "alerts", label: `Alerts · ${model.alerts?.headline ?? "Alert data unavailable"}` },
|
|
93
|
-
{ id: "tokens", label: `Tokens · ${model.tokens?.headline ?? "Data unavailable"}` },
|
|
94
|
-
...companionMetrics
|
|
95
|
-
],
|
|
96
|
-
details: buildOverviewDetails(model)
|
|
115
|
+
primary,
|
|
116
|
+
buttons,
|
|
117
|
+
metrics,
|
|
118
|
+
details: buildOverviewDetails(model, rest)
|
|
97
119
|
};
|
|
98
120
|
}
|
|
99
121
|
|
|
@@ -124,14 +146,30 @@ export function SemanticOverviewPanel({
|
|
|
124
146
|
detailsOpen = false,
|
|
125
147
|
colorEnabled = true,
|
|
126
148
|
unicode = true,
|
|
127
|
-
layoutMode = LAYOUT_MODES.COMPACT
|
|
149
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
150
|
+
selectedIndex = 0,
|
|
151
|
+
contentFocused = false,
|
|
152
|
+
hasGlobalState = false,
|
|
153
|
+
snapshot = null,
|
|
154
|
+
diagnostics = null,
|
|
155
|
+
dashboard = null
|
|
128
156
|
}) {
|
|
129
|
-
const view = adaptControlCenterToOverview(model
|
|
157
|
+
const view = adaptControlCenterToOverview(model, {
|
|
158
|
+
hasGlobalState,
|
|
159
|
+
snapshot,
|
|
160
|
+
diagnostics,
|
|
161
|
+
dashboard
|
|
162
|
+
});
|
|
130
163
|
const showArt = shouldShowWordmark(layoutMode);
|
|
131
164
|
const brandTitle = overviewBrandTitle(layoutMode);
|
|
132
165
|
const isWide = layoutMode === LAYOUT_MODES.WIDE;
|
|
133
166
|
const mark = renderWordmark({ layoutMode, colorEnabled, unicode });
|
|
134
167
|
const status = renderCallout(view, colorEnabled);
|
|
168
|
+
const safeIndex = Math.min(
|
|
169
|
+
Math.max(0, selectedIndex),
|
|
170
|
+
Math.max(0, view.buttons.length - 1)
|
|
171
|
+
);
|
|
172
|
+
const glyphs = resolveGlyphs(unicode);
|
|
135
173
|
|
|
136
174
|
const hero = showArt
|
|
137
175
|
? (isWide
|
|
@@ -150,19 +188,29 @@ export function SemanticOverviewPanel({
|
|
|
150
188
|
|
|
151
189
|
return React.createElement(Box, { flexDirection: "column" },
|
|
152
190
|
hero,
|
|
191
|
+
React.createElement(Text, {
|
|
192
|
+
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
193
|
+
}, view.purpose),
|
|
153
194
|
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
195
|
+
...view.buttons.map((button, index) => {
|
|
196
|
+
const selected = index === safeIndex;
|
|
197
|
+
const focused = contentFocused && selected;
|
|
198
|
+
return React.createElement(Box, {
|
|
199
|
+
key: button.id,
|
|
200
|
+
flexDirection: "column",
|
|
201
|
+
marginBottom: index === view.buttons.length - 1 ? 0 : 1
|
|
202
|
+
},
|
|
203
|
+
React.createElement(Text, {
|
|
204
|
+
bold: true,
|
|
205
|
+
color: focused && colorEnabled ? COCKPIT_COLORS.interactive : undefined
|
|
206
|
+
}, `${focused ? glyphs.focus : " "} ${button.label}`),
|
|
207
|
+
button.detail
|
|
208
|
+
? React.createElement(Text, {
|
|
209
|
+
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
210
|
+
}, ` ${button.detail}`)
|
|
211
|
+
: null
|
|
212
|
+
);
|
|
213
|
+
})
|
|
166
214
|
),
|
|
167
215
|
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
168
216
|
React.createElement(ActionList, {
|
|
@@ -175,7 +223,7 @@ export function SemanticOverviewPanel({
|
|
|
175
223
|
),
|
|
176
224
|
React.createElement(Details, {
|
|
177
225
|
open: detailsOpen,
|
|
178
|
-
summary: "
|
|
226
|
+
summary: "More info",
|
|
179
227
|
lines: view.details,
|
|
180
228
|
colorEnabled,
|
|
181
229
|
focused: false,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two explicit Home buttons — prepare + configure.
|
|
3
|
+
* No writes here: intents only open existing consent-gated flows.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const OVERVIEW_BUTTON_COUNT = 2;
|
|
7
|
+
|
|
8
|
+
function changeCount(snapshot) {
|
|
9
|
+
if (!snapshot?.diff?.hasChanges) return 0;
|
|
10
|
+
return snapshot.diff.changeCount
|
|
11
|
+
?? snapshot.diff.changes?.length
|
|
12
|
+
?? 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function detectedAgentCount({ snapshot = null, diagnostics = null, dashboard = null } = {}) {
|
|
16
|
+
if (Number.isFinite(snapshot?.coverage?.detectedAgents)) {
|
|
17
|
+
return snapshot.coverage.detectedAgents;
|
|
18
|
+
}
|
|
19
|
+
if (Number.isFinite(diagnostics?.diagnostics?.detected)) {
|
|
20
|
+
return diagnostics.diagnostics.detected;
|
|
21
|
+
}
|
|
22
|
+
return (dashboard?.providers ?? []).filter((entry) => entry?.available).length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildPrepareButton({
|
|
26
|
+
hasGlobalState = false,
|
|
27
|
+
snapshot = null,
|
|
28
|
+
diagnostics = null,
|
|
29
|
+
dashboard = null
|
|
30
|
+
} = {}) {
|
|
31
|
+
const detected = detectedAgentCount({ snapshot, diagnostics, dashboard });
|
|
32
|
+
const needsSetup = hasGlobalState === false || detected === 0;
|
|
33
|
+
if (needsSetup) {
|
|
34
|
+
return {
|
|
35
|
+
id: "prepare",
|
|
36
|
+
label: "Set up Kairo",
|
|
37
|
+
detail: "Detect your agents and install what is missing.",
|
|
38
|
+
intent: "setup"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pending = changeCount(snapshot);
|
|
43
|
+
if (pending > 0) {
|
|
44
|
+
return {
|
|
45
|
+
id: "prepare",
|
|
46
|
+
label: `Repair ${pending} change${pending === 1 ? "" : "s"}`,
|
|
47
|
+
detail: "You will see the exact plan before anything is written.",
|
|
48
|
+
intent: "governance"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: "prepare",
|
|
54
|
+
label: "Everything is ready",
|
|
55
|
+
detail: "Open History to see what changed, or Configure to adjust.",
|
|
56
|
+
intent: "history"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CONFIGURE_BUTTON = Object.freeze({
|
|
61
|
+
id: "configure",
|
|
62
|
+
label: "Configure",
|
|
63
|
+
detail: "Agents, Obsidian vault, integrations.",
|
|
64
|
+
intent: "settings"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Exactly two buttons for Home. First button changes face by state.
|
|
69
|
+
*/
|
|
70
|
+
export function buildOverviewButtons({
|
|
71
|
+
hasGlobalState = false,
|
|
72
|
+
snapshot = null,
|
|
73
|
+
diagnostics = null,
|
|
74
|
+
dashboard = null
|
|
75
|
+
} = {}) {
|
|
76
|
+
return [
|
|
77
|
+
buildPrepareButton({ hasGlobalState, snapshot, diagnostics, dashboard }),
|
|
78
|
+
{ ...CONFIGURE_BUTTON }
|
|
79
|
+
];
|
|
80
|
+
}
|