@kal-elsam/kairo-runtime 0.15.0 → 0.17.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 +76 -0
- package/package.json +2 -1
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/ux-smoke-test.sh +3 -3
- package/src/cli.js +106 -11
- package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
- package/src/global/architect/architect-cli.js +76 -0
- package/src/global/architect/architect-codex.js +146 -0
- package/src/global/architect/architect-manager.js +125 -0
- package/src/global/architect/architect-store.js +377 -0
- package/src/global/architect/architect-types.js +47 -0
- package/src/global/cli-help.js +12 -1
- package/src/global/cockpit/app.js +475 -0
- package/src/global/cockpit/card.js +111 -0
- package/src/global/cockpit/cli.js +33 -0
- package/src/global/cockpit/gauge.js +31 -0
- package/src/global/cockpit/project-overlay.js +683 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1263 -0
- package/src/global/control-plane/attention.js +141 -0
- package/src/global/control-plane/build-report.js +146 -0
- package/src/global/control-plane/cli.js +36 -0
- package/src/global/control-plane/constants.js +38 -0
- package/src/global/control-plane/gentle-adapters.js +183 -0
- package/src/global/control-plane/provider.js +69 -0
- package/src/global/control-plane/review-status.js +115 -0
- package/src/global/control-plane/sdd-status.js +49 -0
- package/src/global/control-plane/team.js +63 -0
- package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
- package/src/global/conversation/cli.js +53 -0
- package/src/global/conversation/codex-sandbox.js +230 -0
- package/src/global/conversation/cursor-sandbox.js +215 -0
- package/src/global/conversation/project-analysis.js +204 -0
- package/src/global/conversation/project-profile.js +178 -0
- package/src/global/conversation/project-router.js +149 -0
- package/src/global/conversation/project-strategy-store.js +64 -0
- package/src/global/conversation/project-strategy.js +514 -0
- package/src/global/conversation/sanitized-snapshot.js +169 -0
- package/src/global/conversation/secret-scanner.js +71 -0
- package/src/global/conversation/service.js +1063 -0
- package/src/global/conversation/session-store.js +75 -0
- package/src/global/conversation/transcript-store.js +79 -0
- package/src/global/conversation/ui.js +195 -0
- package/src/global/intelligence/capability-scoring.js +480 -0
- package/src/global/intelligence/execution-router.js +444 -0
- package/src/global/intelligence/kairo-telemetry-source.js +59 -0
- package/src/global/intelligence/kairobench-runner.js +85 -0
- package/src/global/intelligence/kairobench-source.js +34 -0
- package/src/global/intelligence/kairobench-tasks.js +47 -0
- package/src/global/intelligence/model-candidate-catalog.js +456 -0
- package/src/global/intelligence/model-capability-registry-sources.js +145 -0
- package/src/global/intelligence/model-capability-registry.js +125 -0
- package/src/global/intelligence/model-intelligence.js +1646 -0
- package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
- package/src/global/intelligence/quick-ask.js +149 -0
- package/src/global/intelligence/role-profiles.js +251 -0
- package/src/global/intelligence/skill-catalog.js +67 -0
- package/src/global/intelligence/subscription-pressure-source.js +41 -0
- package/src/global/mcp/kairo-mcp.js +51 -18
- package/src/global/mcp/work-snapshot-rule.js +4 -2
- package/src/global/mcp/workspace-binding.js +88 -0
- package/src/global/mcp/workspace-mcp-entry.js +74 -0
- package/src/global/mcp-install.js +8 -1
- package/src/global/observability/artificial-analysis-models.js +118 -0
- package/src/global/observability/claude-models.js +31 -0
- package/src/global/observability/claude-usage.js +112 -0
- package/src/global/observability/codex-models.js +96 -0
- package/src/global/observability/codex-usage.js +160 -0
- package/src/global/observability/cursor-auth.js +88 -0
- package/src/global/observability/cursor-models.js +101 -0
- package/src/global/observability/gentle-probe.js +30 -2
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/index.js +2 -1
- package/src/global/observability/opencode-models.js +101 -0
- package/src/global/observability/opencode-usage.js +162 -0
- package/src/global/paths.js +49 -2
- package/src/global/profile.js +23 -1
- package/src/global/runtime/execution-adapters/claude.js +63 -30
- package/src/global/runtime/execution-adapters/codex.js +9 -2
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
- package/src/global/runtime/execution-adapters/opencode.js +83 -18
- package/src/global/runtime/execution-worktree-manager.js +924 -0
- package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
- package/src/global/runtime/execution-worktree-store.js +83 -0
- package/src/global/runtime/execution-worktree-types.js +45 -0
- package/src/global/runtime/run-events.js +38 -0
- package/src/global/runtime/run-manager.js +22 -6
- package/src/global/runtime/run-supervisor.js +41 -12
- package/src/global/runtime/usage-manager.js +96 -0
- package/src/global/runtime/usage-store.js +69 -0
- package/src/global/runtime/usage-types.js +62 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { Box, SelectList, Text, Input, Key, matchesKey, fuzzyFilter } from "@earendil-works/pi-tui";
|
|
2
|
+
import { editorTheme, theme } from "./theme.js";
|
|
3
|
+
import { CARD_TONE, cardInnerWidth, renderPanel } from "./card.js";
|
|
4
|
+
|
|
5
|
+
// /project's interactive overlay — the real preflight -> select analyst ->
|
|
6
|
+
// confirm -> analyze -> result -> approve loop, reusing the exact same
|
|
7
|
+
// service functions the plain-text /project subcommands already use
|
|
8
|
+
// (preflightProject, runBootstrapAnalysis, approveProjectStrategy,
|
|
9
|
+
// refreshProjectStrategy, getProjectTeamEditCatalog, setProjectTeamAssignment).
|
|
10
|
+
// No new persistence, no new ProjectStrategy schema — this is a different
|
|
11
|
+
// way to drive the same real state machine, never a parallel one. The
|
|
12
|
+
// plain-text subcommands (/project status|analyze|analyst|approve|refresh)
|
|
13
|
+
// keep working unchanged; this overlay is what bare `/project` (no
|
|
14
|
+
// subcommand) now opens.
|
|
15
|
+
|
|
16
|
+
export const PROJECT_OVERLAY_STATE = {
|
|
17
|
+
LOADING_PREFLIGHT: "loading-preflight",
|
|
18
|
+
NO_ANALYST: "no-analyst",
|
|
19
|
+
SELECT_ANALYST: "select-analyst",
|
|
20
|
+
CONFIRM_ANALYST: "confirm-analyst",
|
|
21
|
+
ANALYZING: "analyzing",
|
|
22
|
+
RESULT: "result",
|
|
23
|
+
EDIT_LOADING: "edit-loading",
|
|
24
|
+
EDIT_MODEL_SEARCH: "edit-model-search",
|
|
25
|
+
EDIT_CONFIRM: "edit-confirm",
|
|
26
|
+
EDIT_SAVING: "edit-saving",
|
|
27
|
+
APPROVING: "approving",
|
|
28
|
+
REFRESHING: "refreshing",
|
|
29
|
+
ACTIVE: "active",
|
|
30
|
+
STALE: "stale",
|
|
31
|
+
ERROR: "error"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const S = PROJECT_OVERLAY_STATE;
|
|
35
|
+
|
|
36
|
+
function teamLines(label, team, aiTeamLabel, tone = "bold") {
|
|
37
|
+
const lines = [tone === "bold" ? theme.bold(label) : theme.fg("muted", label)];
|
|
38
|
+
if (!team?.length) {
|
|
39
|
+
lines.push(theme.fg("muted", " (no active roles)"));
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of team) {
|
|
43
|
+
const modelText = entry.model ? aiTeamLabel(entry.model) : theme.fg("warning", "no eligible option");
|
|
44
|
+
lines.push(theme.fg("muted", ` ${entry.role.padEnd(10)} ${modelText}`));
|
|
45
|
+
}
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A pi-tui Component implementing /project's interactive overlay. State is
|
|
51
|
+
* driven entirely by the real, already-existing service calls — this
|
|
52
|
+
* component only sequences them and renders their real results; it never
|
|
53
|
+
* invents team data, approval state, or staleness on its own.
|
|
54
|
+
*/
|
|
55
|
+
export class ProjectOverlay {
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} deps
|
|
58
|
+
* @param {object} deps.service - conversation service (preflightProject, runBootstrapAnalysis, approveProjectStrategy, refreshProjectStrategy, getProjectTeamEditCatalog, setProjectTeamAssignment)
|
|
59
|
+
* @param {import("./view.js").CockpitView} deps.view
|
|
60
|
+
* @param {string} deps.cwd
|
|
61
|
+
* @param {() => void} deps.onClose - called when the overlay should close and focus should return to the editor
|
|
62
|
+
* @param {() => void} [deps.requestRender]
|
|
63
|
+
* @param {(text: string) => void} [deps.onNarrate] - mirrors real milestones
|
|
64
|
+
* (analysis started, result ready, approved, refreshed, errors) into the
|
|
65
|
+
* main conversation transcript, the same way the plain-text /project
|
|
66
|
+
* subcommands already narrate their own steps there — the interactive
|
|
67
|
+
* overlay is the rich detail view, but the chat history should still
|
|
68
|
+
* show that an analysis happened and what it decided, matching how a
|
|
69
|
+
* real CLI (Claude, Codex) narrates its own process. Never called for
|
|
70
|
+
* pure navigation (opening a picker, moving a selection) — only for a
|
|
71
|
+
* real state change the overlay's own service calls produced.
|
|
72
|
+
*/
|
|
73
|
+
constructor({ service, view, cwd, onClose, requestRender = () => {}, onNarrate = () => {} }) {
|
|
74
|
+
this.service = service;
|
|
75
|
+
this.view = view;
|
|
76
|
+
this.cwd = cwd;
|
|
77
|
+
this.onClose = onClose;
|
|
78
|
+
this.requestRender = requestRender;
|
|
79
|
+
this.onNarrate = onNarrate;
|
|
80
|
+
|
|
81
|
+
this.state = S.LOADING_PREFLIGHT;
|
|
82
|
+
// The real Box render() builds each frame, kept here (not local to
|
|
83
|
+
// render()) so handleMouse can forward a real click into it — pi-tui
|
|
84
|
+
// dispatches mouse events against whatever the component's own last
|
|
85
|
+
// render() actually laid out, never a freshly-built one.
|
|
86
|
+
this.box = null;
|
|
87
|
+
this.lastWidth = 76;
|
|
88
|
+
this.preflight = null;
|
|
89
|
+
this.selectedAnalyst = null;
|
|
90
|
+
this.selectList = null;
|
|
91
|
+
this.suggestedStrategy = null;
|
|
92
|
+
this.activeStrategy = null;
|
|
93
|
+
this.errorMessage = null;
|
|
94
|
+
|
|
95
|
+
// projectTeam editing (section 4).
|
|
96
|
+
this.resultSelectList = null;
|
|
97
|
+
this.editingRole = null;
|
|
98
|
+
this.editCatalog = null;
|
|
99
|
+
this.editModels = [];
|
|
100
|
+
this.editQuery = "";
|
|
101
|
+
this.editInput = null;
|
|
102
|
+
this.editSelectList = null;
|
|
103
|
+
this.pendingEditCandidate = null;
|
|
104
|
+
|
|
105
|
+
const existing = view.snapshot?.projectStrategy ?? null;
|
|
106
|
+
if (existing?.status === "active") {
|
|
107
|
+
this.state = S.ACTIVE;
|
|
108
|
+
this.activeStrategy = existing;
|
|
109
|
+
} else if (existing?.status === "stale") {
|
|
110
|
+
this.state = S.STALE;
|
|
111
|
+
this.activeStrategy = existing;
|
|
112
|
+
} else if (existing?.status === "suggested") {
|
|
113
|
+
this.state = S.RESULT;
|
|
114
|
+
this.suggestedStrategy = existing;
|
|
115
|
+
this.buildResultRoleList();
|
|
116
|
+
} else {
|
|
117
|
+
// LOCAL_PREFLIGHT: real, read-only evidence — no provider call, no
|
|
118
|
+
// quota consumed, nothing persisted yet.
|
|
119
|
+
void this.loadPreflight();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
invalidate() {
|
|
124
|
+
this.selectList?.invalidate();
|
|
125
|
+
this.resultSelectList?.invalidate();
|
|
126
|
+
this.editSelectList?.invalidate();
|
|
127
|
+
this.editInput?.invalidate();
|
|
128
|
+
this.box?.invalidate();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Forwards a real mouse event into the real Box render() built — the
|
|
133
|
+
* missing half of the real contract: SelectList/Box already implement
|
|
134
|
+
* handleMouse (a click can select an analyst or a PROJECT TEAM role
|
|
135
|
+
* row), but nothing here ever called it, so those real clicks never
|
|
136
|
+
* reached the list at all. Coordinates are translated from this real
|
|
137
|
+
* bordered panel's own frame (render()'s renderPanel border + padding)
|
|
138
|
+
* back into the Box's own render(cardInnerWidth(width)) coordinate
|
|
139
|
+
* space — the exact inverse of how render() below lays the frame out.
|
|
140
|
+
* @param {import("@earendil-works/pi-tui").TuiMouseEvent} event
|
|
141
|
+
*/
|
|
142
|
+
handleMouse(event) {
|
|
143
|
+
if (!this.box) return undefined;
|
|
144
|
+
const innerWidth = cardInnerWidth(this.lastWidth);
|
|
145
|
+
const x = event.x - 2; // real frame's own left border + one padding column (see card.js's cardLine)
|
|
146
|
+
const y = event.y - 1; // real frame's own top border row (see card.js's cardTop)
|
|
147
|
+
if (x < 0 || y < 0 || x >= innerWidth) return undefined;
|
|
148
|
+
return this.box.handleMouse({ ...event, x, y, width: innerWidth });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async loadPreflight() {
|
|
152
|
+
this.view.beginAction("Reading project evidence locally");
|
|
153
|
+
try {
|
|
154
|
+
this.preflight = await this.service.preflightProject({ cwd: this.cwd });
|
|
155
|
+
const models = this.preflight.analystCatalog?.models ?? [];
|
|
156
|
+
if (!models.length) {
|
|
157
|
+
this.state = S.NO_ANALYST;
|
|
158
|
+
} else {
|
|
159
|
+
this.buildSelectList();
|
|
160
|
+
this.state = S.SELECT_ANALYST;
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.errorMessage = error.message ?? String(error);
|
|
164
|
+
this.state = S.ERROR;
|
|
165
|
+
}
|
|
166
|
+
this.view.endAction();
|
|
167
|
+
this.requestRender();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A short, honest label for a catalog entry's own real recommendationTags/evidenceStatus — never a fabricated "Quality"/"Efficient" claim for a model that doesn't actually carry that tag. */
|
|
171
|
+
static tagLabel(model) {
|
|
172
|
+
if (model.evidenceStatus === "unscored") return "Unscored";
|
|
173
|
+
const tags = [];
|
|
174
|
+
if (model.recommendationTags.includes("quality")) tags.push("Quality fit");
|
|
175
|
+
if (model.recommendationTags.includes("efficient")) tags.push("Efficient fit");
|
|
176
|
+
return tags.join(" · ");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Builds the real, full analyst catalog picker — every real ask-
|
|
181
|
+
* supported model (scored AND unscored), the real recommended one
|
|
182
|
+
* listed first and pre-selected (index 0), matching every OTHER real
|
|
183
|
+
* candidate's own real tag/evidence state honestly instead of
|
|
184
|
+
* collapsing the catalog back down to just two picks.
|
|
185
|
+
*/
|
|
186
|
+
buildSelectList() {
|
|
187
|
+
const catalog = this.preflight.analystCatalog;
|
|
188
|
+
const models = catalog.models ?? [];
|
|
189
|
+
const recommendedKey = catalog.recommendedModel?.candidateKey ?? null;
|
|
190
|
+
const ordered = recommendedKey
|
|
191
|
+
? [...models.filter((m) => m.candidateKey === recommendedKey), ...models.filter((m) => m.candidateKey !== recommendedKey)]
|
|
192
|
+
: models;
|
|
193
|
+
// Model-first rows: "<display name> <provider> <real tag>" —
|
|
194
|
+
// never provider-first, matching the plan's own mockup. The primary
|
|
195
|
+
// column (model + provider) is explicitly the real, bright `text`
|
|
196
|
+
// color — never left to the terminal's own default or muted, so it
|
|
197
|
+
// stays legible regardless of terminal theme; muted stays reserved
|
|
198
|
+
// for the real secondary tag in `description`.
|
|
199
|
+
const items = ordered.map((model) => ({
|
|
200
|
+
value: model.candidateKey,
|
|
201
|
+
label: theme.fg("text", `${model.displayName} ${model.adapterId}`),
|
|
202
|
+
description: ProjectOverlay.tagLabel(model)
|
|
203
|
+
}));
|
|
204
|
+
this.selectList = new SelectList(items, 8, editorTheme.selectList);
|
|
205
|
+
this.selectList.onSelect = (item) => {
|
|
206
|
+
const picked = models.find((model) => model.candidateKey === item.value);
|
|
207
|
+
if (!picked) return;
|
|
208
|
+
const selectionSource = picked.candidateKey === recommendedKey ? "recommended" : "manual";
|
|
209
|
+
const recommendationTags = picked.recommendationTags ?? [];
|
|
210
|
+
this.selectedAnalyst = {
|
|
211
|
+
// A clean modelRef shape — no UI-only fields leak into what
|
|
212
|
+
// eventually gets persisted verbatim as ProjectStrategy's own
|
|
213
|
+
// bootstrapAnalyst (see buildProjectStrategy). available/
|
|
214
|
+
// evidenceStatus below are UI-only, read by this overlay's own
|
|
215
|
+
// CONFIRM_ANALYST render, never sent to the service or persisted.
|
|
216
|
+
model: { adapterId: picked.adapterId, modelId: picked.modelId, displayName: picked.displayName },
|
|
217
|
+
selectionSource,
|
|
218
|
+
recommendationTags,
|
|
219
|
+
// choice stays ONLY for the legacy plain-text subcommand's own
|
|
220
|
+
// persisted field — never fabricated for a real manual/unscored
|
|
221
|
+
// pick that fits neither bucket.
|
|
222
|
+
choice: recommendationTags.includes("quality") ? "quality" : recommendationTags.includes("efficient") ? "efficient" : null,
|
|
223
|
+
available: picked.available,
|
|
224
|
+
evidenceStatus: picked.evidenceStatus
|
|
225
|
+
};
|
|
226
|
+
this.state = S.CONFIRM_ANALYST;
|
|
227
|
+
this.requestRender();
|
|
228
|
+
};
|
|
229
|
+
this.selectList.onCancel = () => this.close();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async confirmAnalyst() {
|
|
233
|
+
this.state = S.ANALYZING;
|
|
234
|
+
this.view.beginAction(`${this.view.aiTeamLabelWithProvider(this.selectedAnalyst.model)} is investigating this project`);
|
|
235
|
+
this.requestRender();
|
|
236
|
+
this.onNarrate(`${this.view.aiTeamLabelWithProvider(this.selectedAnalyst.model)} is investigating this project (read-only)…`);
|
|
237
|
+
try {
|
|
238
|
+
const result = await this.service.runBootstrapAnalysis({
|
|
239
|
+
cwd: this.cwd, profile: this.preflight.profile, candidates: this.preflight.candidates, analyst: this.selectedAnalyst
|
|
240
|
+
});
|
|
241
|
+
this.suggestedStrategy = result;
|
|
242
|
+
this.buildResultRoleList();
|
|
243
|
+
this.state = S.RESULT;
|
|
244
|
+
const roleCount = result.activeRoles?.length ?? 0;
|
|
245
|
+
this.onNarrate(`Suggested project team ready (${roleCount} real role${roleCount === 1 ? "" : "s"}). Open /project to review, edit, or approve it.`);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.errorMessage = error.message ?? String(error);
|
|
248
|
+
this.state = S.ERROR;
|
|
249
|
+
this.onNarrate(`Project analysis failed: ${this.errorMessage}`);
|
|
250
|
+
}
|
|
251
|
+
this.view.endAction();
|
|
252
|
+
this.requestRender();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The RESULT screen's own real, interactive PROJECT TEAM list — one row
|
|
257
|
+
* per real role in strategy.projectTeam, showing its real current
|
|
258
|
+
* model and whether it's a real override. Enter on a row opens that
|
|
259
|
+
* role's real edit picker (see openRolePicker); Esc closes the overlay
|
|
260
|
+
* without approving (unchanged). Approval is a separate, explicit key
|
|
261
|
+
* ("a") — Enter here edits, it never silently approves.
|
|
262
|
+
*/
|
|
263
|
+
buildResultRoleList() {
|
|
264
|
+
const team = this.suggestedStrategy?.projectTeam ?? [];
|
|
265
|
+
const items = team.map((entry) => {
|
|
266
|
+
const modelText = entry.model ? this.view.aiTeamLabel(entry.model) : "no eligible option";
|
|
267
|
+
const overrideNote = entry.assignmentSource === "override" ? theme.fg("accent", " (override)") : "";
|
|
268
|
+
// Role + model are the real primary information here — explicit
|
|
269
|
+
// `text` color, never left to default/muted.
|
|
270
|
+
// WHY this model was picked — the real, human-readable evidence
|
|
271
|
+
// buildProjectStrategy already carries (entry.reason, sourced from
|
|
272
|
+
// efficientTeam's own describeEfficiencyDecision), never a
|
|
273
|
+
// fabricated justification. An override has no ranking reason of
|
|
274
|
+
// its own (see applyProjectTeamOverride) — honestly say so instead
|
|
275
|
+
// of silently reusing the old recommendation's reason for a
|
|
276
|
+
// different model.
|
|
277
|
+
const description = entry.assignmentSource === "override"
|
|
278
|
+
? "Manual override — not the automatic ranking's own pick."
|
|
279
|
+
: (entry.reason ?? "");
|
|
280
|
+
return { value: entry.role, label: theme.fg("text", `${entry.role.padEnd(10)} ${modelText}`) + overrideNote, description };
|
|
281
|
+
});
|
|
282
|
+
this.resultSelectList = new SelectList(items, 6, editorTheme.selectList);
|
|
283
|
+
this.resultSelectList.onSelect = (item) => void this.openRolePicker(item.value);
|
|
284
|
+
this.resultSelectList.onCancel = () => this.close();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Opens the real edit picker for one projectTeam role — a real, read-
|
|
289
|
+
* only catalog fetch (getProjectTeamEditCatalog), no quota, no write.
|
|
290
|
+
*/
|
|
291
|
+
async openRolePicker(role) {
|
|
292
|
+
this.editingRole = role;
|
|
293
|
+
this.editCatalog = null;
|
|
294
|
+
this.state = S.EDIT_LOADING;
|
|
295
|
+
this.view.beginAction(`Reading the real current catalog for ${role}`);
|
|
296
|
+
this.requestRender();
|
|
297
|
+
try {
|
|
298
|
+
this.editCatalog = await this.service.getProjectTeamEditCatalog({ cwd: this.cwd, role });
|
|
299
|
+
this.buildEditPicker();
|
|
300
|
+
this.state = S.EDIT_MODEL_SEARCH;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
this.errorMessage = error.message ?? String(error);
|
|
303
|
+
this.state = S.ERROR;
|
|
304
|
+
}
|
|
305
|
+
this.view.endAction();
|
|
306
|
+
this.requestRender();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Orders the real edit catalog per the plan's own contract: 1) the
|
|
311
|
+
* role's real current operational model, 2) its real original
|
|
312
|
+
* recommendation (when different from the current), 3) the rest of the
|
|
313
|
+
* real scored candidates, 4) the real unscored ones — never re-ranking
|
|
314
|
+
* anything, just ordering what the catalog already returned.
|
|
315
|
+
*/
|
|
316
|
+
orderedEditModels() {
|
|
317
|
+
const entry = this.suggestedStrategy?.projectTeam?.find((e) => e.role === this.editingRole);
|
|
318
|
+
const currentKey = entry?.model?.candidateKey ?? null;
|
|
319
|
+
const recommendedKey = entry?.recommendedAssignment?.model?.candidateKey ?? currentKey;
|
|
320
|
+
const models = this.editCatalog?.models ?? [];
|
|
321
|
+
const byKey = new Map(models.map((m) => [m.candidateKey, m]));
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
const ordered = [];
|
|
324
|
+
const pushIfPresent = (key) => {
|
|
325
|
+
if (key && byKey.has(key) && !seen.has(key)) {
|
|
326
|
+
ordered.push(byKey.get(key));
|
|
327
|
+
seen.add(key);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
pushIfPresent(currentKey);
|
|
331
|
+
pushIfPresent(recommendedKey);
|
|
332
|
+
for (const model of models) {
|
|
333
|
+
if (seen.has(model.candidateKey) || model.evidenceStatus === "unscored") continue;
|
|
334
|
+
ordered.push(model);
|
|
335
|
+
seen.add(model.candidateKey);
|
|
336
|
+
}
|
|
337
|
+
for (const model of models) {
|
|
338
|
+
if (seen.has(model.candidateKey)) continue;
|
|
339
|
+
ordered.push(model);
|
|
340
|
+
seen.add(model.candidateKey);
|
|
341
|
+
}
|
|
342
|
+
return ordered;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
editItemTag(model, currentKey, recommendedKey) {
|
|
346
|
+
const tags = [];
|
|
347
|
+
if (model.candidateKey === currentKey) tags.push("current");
|
|
348
|
+
if (model.candidateKey === recommendedKey) tags.push("recommended");
|
|
349
|
+
if (model.evidenceStatus === "unscored") tags.push("unscored");
|
|
350
|
+
if (model.accessMode === "manual") tags.push("manual");
|
|
351
|
+
return tags.join(" · ");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
buildEditPicker() {
|
|
355
|
+
this.editQuery = "";
|
|
356
|
+
this.editInput = new Input({ placeholder: "type to search…" });
|
|
357
|
+
this.editModels = this.orderedEditModels();
|
|
358
|
+
this.rebuildEditSelectList();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Real fuzzy filtering (pi-tui's own fuzzyFilter) against each real candidate's own display name + provider — never SelectList's built-in setFilter, which only prefix-matches a plain `value` (here, candidateKey), not what a human actually types. */
|
|
362
|
+
rebuildEditSelectList() {
|
|
363
|
+
const entry = this.suggestedStrategy?.projectTeam?.find((e) => e.role === this.editingRole);
|
|
364
|
+
const currentKey = entry?.model?.candidateKey ?? null;
|
|
365
|
+
const recommendedKey = entry?.recommendedAssignment?.model?.candidateKey ?? currentKey;
|
|
366
|
+
const filtered = this.editQuery
|
|
367
|
+
? fuzzyFilter(this.editModels, this.editQuery, (model) => `${model.displayName} ${model.adapterId}`)
|
|
368
|
+
: this.editModels;
|
|
369
|
+
const items = filtered.map((model) => ({
|
|
370
|
+
value: model.candidateKey,
|
|
371
|
+
label: theme.fg("text", `${model.displayName} ${model.adapterId}`),
|
|
372
|
+
description: this.editItemTag(model, currentKey, recommendedKey)
|
|
373
|
+
}));
|
|
374
|
+
this.editSelectList = new SelectList(items, 8, editorTheme.selectList);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
beginConfirmEdit(candidateKey) {
|
|
378
|
+
const candidate = this.editCatalog?.models?.find((m) => m.candidateKey === candidateKey);
|
|
379
|
+
if (!candidate) return;
|
|
380
|
+
this.pendingEditCandidate = candidate;
|
|
381
|
+
this.state = S.EDIT_CONFIRM;
|
|
382
|
+
this.requestRender();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async commitEdit() {
|
|
386
|
+
this.state = S.EDIT_SAVING;
|
|
387
|
+
this.view.beginAction("Saving the real assignment");
|
|
388
|
+
this.requestRender();
|
|
389
|
+
try {
|
|
390
|
+
this.suggestedStrategy = await this.service.setProjectTeamAssignment({
|
|
391
|
+
cwd: this.cwd, role: this.editingRole, candidateKey: this.pendingEditCandidate.candidateKey
|
|
392
|
+
});
|
|
393
|
+
this.buildResultRoleList();
|
|
394
|
+
this.state = S.RESULT;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
this.errorMessage = error.message ?? String(error);
|
|
397
|
+
this.state = S.ERROR;
|
|
398
|
+
}
|
|
399
|
+
this.view.endAction();
|
|
400
|
+
this.requestRender();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async approve() {
|
|
404
|
+
this.state = S.APPROVING;
|
|
405
|
+
this.view.beginAction("Activating the project team");
|
|
406
|
+
this.requestRender();
|
|
407
|
+
try {
|
|
408
|
+
this.activeStrategy = await this.service.approveProjectStrategy({ cwd: this.cwd });
|
|
409
|
+
this.state = S.ACTIVE;
|
|
410
|
+
this.onNarrate("Project team is now ACTIVE.");
|
|
411
|
+
} catch (error) {
|
|
412
|
+
this.errorMessage = error.message ?? String(error);
|
|
413
|
+
this.state = S.ERROR;
|
|
414
|
+
this.onNarrate(`Approval failed: ${this.errorMessage}`);
|
|
415
|
+
}
|
|
416
|
+
this.view.endAction();
|
|
417
|
+
this.requestRender();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async refresh() {
|
|
421
|
+
this.state = S.REFRESHING;
|
|
422
|
+
this.view.beginAction("Re-checking the real project evidence");
|
|
423
|
+
this.requestRender();
|
|
424
|
+
try {
|
|
425
|
+
const result = await this.service.refreshProjectStrategy({ cwd: this.cwd });
|
|
426
|
+
this.activeStrategy = result;
|
|
427
|
+
this.state = result?.status === "stale" ? S.STALE : S.ACTIVE;
|
|
428
|
+
this.onNarrate(result ? `Project strategy is now ${result.status.toUpperCase()}.` : "Nothing to refresh yet.");
|
|
429
|
+
} catch (error) {
|
|
430
|
+
this.errorMessage = error.message ?? String(error);
|
|
431
|
+
this.state = S.ERROR;
|
|
432
|
+
this.onNarrate(`Refresh failed: ${this.errorMessage}`);
|
|
433
|
+
}
|
|
434
|
+
this.view.endAction();
|
|
435
|
+
this.requestRender();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
close() {
|
|
439
|
+
this.onClose?.();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
handleInput(data) {
|
|
443
|
+
if (this.state === S.SELECT_ANALYST && this.selectList) {
|
|
444
|
+
this.selectList.handleInput(data);
|
|
445
|
+
this.requestRender();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (this.state === S.RESULT && this.resultSelectList) {
|
|
450
|
+
// "a" approves and activates — a distinct key from Enter, which
|
|
451
|
+
// edits the highlighted role instead. Enter must never silently
|
|
452
|
+
// approve just because a role row happens to be focused.
|
|
453
|
+
if (data === "a" || data === "A") return void this.approve();
|
|
454
|
+
this.resultSelectList.handleInput(data);
|
|
455
|
+
this.requestRender();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (this.state === S.EDIT_MODEL_SEARCH) {
|
|
460
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.esc)) {
|
|
461
|
+
this.state = S.RESULT;
|
|
462
|
+
this.requestRender();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
466
|
+
const item = this.editSelectList?.getSelectedItem?.();
|
|
467
|
+
if (item) this.beginConfirmEdit(item.value);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) {
|
|
471
|
+
this.editSelectList?.handleInput(data);
|
|
472
|
+
this.requestRender();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this.editInput.handleInput(data);
|
|
476
|
+
this.editQuery = this.editInput.getValue();
|
|
477
|
+
this.rebuildEditSelectList();
|
|
478
|
+
this.requestRender();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (this.state === S.EDIT_CONFIRM) {
|
|
483
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.esc)) {
|
|
484
|
+
this.state = S.EDIT_MODEL_SEARCH;
|
|
485
|
+
this.requestRender();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) return void this.commitEdit();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.esc)) {
|
|
493
|
+
if (this.state === S.CONFIRM_ANALYST) {
|
|
494
|
+
this.state = S.SELECT_ANALYST;
|
|
495
|
+
this.requestRender();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.close();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
502
|
+
if (this.state === S.CONFIRM_ANALYST) return void this.confirmAnalyst();
|
|
503
|
+
if (this.state === S.STALE) return void this.refresh();
|
|
504
|
+
if (this.state === S.NO_ANALYST || this.state === S.ERROR || this.state === S.ACTIVE) {
|
|
505
|
+
this.close();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Real border tone per state — never a fixed color, so ERROR/WARNING states read as visually distinct as their own content already claims to be. */
|
|
511
|
+
panelTone() {
|
|
512
|
+
if (this.state === S.ERROR) return CARD_TONE.ERROR;
|
|
513
|
+
if (this.state === S.NO_ANALYST || this.state === S.STALE) return CARD_TONE.WARNING;
|
|
514
|
+
if (this.state === S.RESULT || this.state === S.ACTIVE) return CARD_TONE.SUCCESS;
|
|
515
|
+
return CARD_TONE.INFO;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
render(width) {
|
|
519
|
+
this.lastWidth = width;
|
|
520
|
+
// No background applied to the whole box — only the SelectList's own
|
|
521
|
+
// active row gets a highlight (editorTheme.selectList's own
|
|
522
|
+
// selectedPrefix/selectedText), so the overlay reads as a real modal
|
|
523
|
+
// over the dashboard, not a solid color block. The real bordered
|
|
524
|
+
// frame below (renderPanel) is what actually keeps this from getting
|
|
525
|
+
// visually lost against the dashboard behind it — a background alone
|
|
526
|
+
// isn't a modal boundary a human eye reliably notices.
|
|
527
|
+
const box = new Box(2, 1);
|
|
528
|
+
this.box = box;
|
|
529
|
+
const aiTeamLabel = (model) => this.view.aiTeamLabel(model);
|
|
530
|
+
const push = (text) => box.addChild(new Text(text));
|
|
531
|
+
// The real, ticking spinner+elapsed-time line every other in-flight
|
|
532
|
+
// action in the cockpit already uses (view.beginAction/tickSpinner/
|
|
533
|
+
// actionStatusLine — app.js's own fast timer keeps it live) — never a
|
|
534
|
+
// static "…" string that just sits there unchanged. Falls back to the
|
|
535
|
+
// static text only if no real action happens to be running yet (the
|
|
536
|
+
// render right before beginAction's own first call).
|
|
537
|
+
const spinnerLine = (fallback) => theme.fg("muted", this.view.actionStatusLine() ?? fallback);
|
|
538
|
+
|
|
539
|
+
switch (this.state) {
|
|
540
|
+
case S.LOADING_PREFLIGHT:
|
|
541
|
+
push(theme.bold("Analyze Project"));
|
|
542
|
+
push(spinnerLine("Reading project evidence locally — no provider call, no quota consumed…"));
|
|
543
|
+
break;
|
|
544
|
+
case S.NO_ANALYST:
|
|
545
|
+
push(theme.bold("Select Project Analyst"));
|
|
546
|
+
push(theme.fg("warning", "No real Project Analyst candidate is available right now (ASK only supports Codex/Claude today)."));
|
|
547
|
+
push(theme.fg("muted", "Esc / Enter to close."));
|
|
548
|
+
break;
|
|
549
|
+
case S.SELECT_ANALYST:
|
|
550
|
+
push(theme.bold("Select Project Analyst"));
|
|
551
|
+
// The Project Analyst investigates and reports on this project's
|
|
552
|
+
// real architecture and risks — it never joins the team it
|
|
553
|
+
// recommends (see BOOTSTRAP_ANALYST_PROFILE), so it's shown
|
|
554
|
+
// distinctly from Architect and every other role, and only ever
|
|
555
|
+
// as one of the two real models Kairo can actually invoke,
|
|
556
|
+
// isolated, read-only (Codex/Claude today).
|
|
557
|
+
push(theme.fg("muted", "Architecture & systems analysis — read-only, no quota consumed until you confirm."));
|
|
558
|
+
box.addChild(this.selectList);
|
|
559
|
+
push(theme.fg("muted", "Enter Select · Esc Cancel"));
|
|
560
|
+
break;
|
|
561
|
+
case S.CONFIRM_ANALYST: {
|
|
562
|
+
const { model, selectionSource, available, evidenceStatus } = this.selectedAnalyst;
|
|
563
|
+
push(theme.bold("Confirm Project Analyst"));
|
|
564
|
+
const sourceNote = selectionSource === "manual" ? theme.fg("muted", " (manual selection)") : "";
|
|
565
|
+
push(` ${this.view.aiTeamLabelWithProvider(model)}${sourceNote}${available === false ? theme.fg("warning", " (not available)") : ""}`);
|
|
566
|
+
if (evidenceStatus === "unscored") {
|
|
567
|
+
push(theme.fg("warning", "This model has no real benchmark evidence — Kairo isn't recommending it, you're choosing it manually."));
|
|
568
|
+
}
|
|
569
|
+
push(theme.fg("warning", "This will run a real, read-only investigation against your project and consume real quota from this provider."));
|
|
570
|
+
push(theme.fg("muted", "Enter confirm and run · Esc back"));
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case S.ANALYZING:
|
|
574
|
+
push(theme.bold("ANALYZING"));
|
|
575
|
+
push(spinnerLine(`${this.view.aiTeamLabelWithProvider(this.selectedAnalyst.model)} is investigating this project (read-only)…`));
|
|
576
|
+
break;
|
|
577
|
+
case S.RESULT: {
|
|
578
|
+
const strategy = this.suggestedStrategy;
|
|
579
|
+
push(theme.bold("Suggested Project Team"));
|
|
580
|
+
const choiceNote = strategy.bootstrapAnalystChoice ?? (strategy.bootstrapAnalystSelectionSource === "manual" ? "manual pick" : "recommended");
|
|
581
|
+
push(theme.fg("muted", `Project Analyst: ${choiceNote} — ${this.view.aiTeamLabelWithProvider(strategy.bootstrapAnalyst)}`));
|
|
582
|
+
push(theme.bold("PROJECT TEAM"));
|
|
583
|
+
box.addChild(this.resultSelectList);
|
|
584
|
+
// Quality/Efficient stay real, comparative REFERENCE — muted, and
|
|
585
|
+
// rendered strictly below the real operational PROJECT TEAM list
|
|
586
|
+
// above, never replacing it visually.
|
|
587
|
+
for (const line of teamLines("Quality (reference)", strategy.qualityTeam, aiTeamLabel, "muted")) push(line);
|
|
588
|
+
for (const line of teamLines("Efficient (reference)", strategy.efficientTeam, aiTeamLabel, "muted")) push(line);
|
|
589
|
+
push(theme.fg("muted", "Enter edit role · a approve & activate · Esc close without approving"));
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case S.EDIT_LOADING:
|
|
593
|
+
push(theme.bold(`Edit ${this.editingRole}`));
|
|
594
|
+
push(spinnerLine("Reading the real current catalog for this role — no quota consumed…"));
|
|
595
|
+
break;
|
|
596
|
+
case S.EDIT_MODEL_SEARCH:
|
|
597
|
+
push(theme.bold(`Edit ${this.editingRole}`));
|
|
598
|
+
push(theme.fg("muted", "current · recommended · manual · unscored — real state, never fabricated."));
|
|
599
|
+
box.addChild(this.editInput);
|
|
600
|
+
box.addChild(this.editSelectList);
|
|
601
|
+
push(theme.fg("muted", "Enter select · Esc cancel"));
|
|
602
|
+
break;
|
|
603
|
+
case S.EDIT_CONFIRM: {
|
|
604
|
+
const entry = this.suggestedStrategy.projectTeam.find((e) => e.role === this.editingRole);
|
|
605
|
+
const candidate = this.pendingEditCandidate;
|
|
606
|
+
const oldLabel = entry?.model ? this.view.aiTeamLabelWithProvider(entry.model) : "(none)";
|
|
607
|
+
const newLabel = this.view.aiTeamLabelWithProvider({ adapterId: candidate.adapterId, modelId: candidate.modelId, displayName: candidate.displayName });
|
|
608
|
+
const recommendedKey = entry?.recommendedAssignment?.model?.candidateKey ?? entry?.model?.candidateKey ?? null;
|
|
609
|
+
const isRecommended = candidate.candidateKey === recommendedKey;
|
|
610
|
+
push(theme.bold(`Confirm ${this.editingRole}`));
|
|
611
|
+
push(` ${oldLabel} → ${newLabel}`);
|
|
612
|
+
push(theme.fg("muted", isRecommended ? "restores the real recommendation" : "manual override"));
|
|
613
|
+
if (candidate.evidenceStatus === "unscored") {
|
|
614
|
+
push(theme.fg("warning", "This model has no real benchmark evidence for this role."));
|
|
615
|
+
}
|
|
616
|
+
if (candidate.accessMode === "manual") {
|
|
617
|
+
push(theme.fg("warning", `${candidate.adapterId} isn't executable by Kairo automatically — this role will need a manual handoff.`));
|
|
618
|
+
}
|
|
619
|
+
push(theme.fg("muted", "Enter confirm and save (still suggested, not yet approved) · Esc back"));
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case S.EDIT_SAVING:
|
|
623
|
+
push(theme.bold(`Edit ${this.editingRole}`));
|
|
624
|
+
push(spinnerLine("Saving the real assignment…"));
|
|
625
|
+
break;
|
|
626
|
+
case S.APPROVING:
|
|
627
|
+
push(theme.bold("Approving"));
|
|
628
|
+
push(spinnerLine("Activating the project team…"));
|
|
629
|
+
break;
|
|
630
|
+
case S.REFRESHING:
|
|
631
|
+
push(theme.bold("Refreshing"));
|
|
632
|
+
push(spinnerLine("Re-checking whether the active project team still matches the real evidence…"));
|
|
633
|
+
break;
|
|
634
|
+
case S.ACTIVE: {
|
|
635
|
+
const strategy = this.activeStrategy;
|
|
636
|
+
push(theme.fg("success", "ACTIVE"));
|
|
637
|
+
push(theme.fg("muted", `Approved ${strategy.approvedAt ?? "?"}`));
|
|
638
|
+
for (const line of teamLines("PROJECT TEAM", strategy.projectTeam ?? strategy.qualityTeam, aiTeamLabel)) push(line);
|
|
639
|
+
push(theme.fg("muted", "Esc close"));
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case S.STALE: {
|
|
643
|
+
const strategy = this.activeStrategy;
|
|
644
|
+
push(theme.fg("warning", "STALE"));
|
|
645
|
+
push(theme.fg("muted", "The real project evidence has changed since this team was approved — previous assignments are kept until refreshed."));
|
|
646
|
+
for (const line of teamLines("PROJECT TEAM (previous)", strategy.projectTeam ?? strategy.qualityTeam, aiTeamLabel)) push(line);
|
|
647
|
+
push(theme.fg("muted", "Enter refresh · Esc close"));
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case S.ERROR:
|
|
651
|
+
default:
|
|
652
|
+
push(theme.fg("error", "Error"));
|
|
653
|
+
push(theme.fg("muted", this.errorMessage ?? "Unknown error."));
|
|
654
|
+
push(theme.fg("muted", "Esc / Enter to close."));
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
const innerWidth = cardInnerWidth(width);
|
|
658
|
+
return renderPanel("Project", this.panelTone(), theme, width, box.render(innerWidth));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Opens the real /project overlay on top of `tui`, focus-managed by pi-tui
|
|
664
|
+
* itself (showOverlay records the currently-focused component — the
|
|
665
|
+
* editor — and restores it automatically when the overlay is hidden, per
|
|
666
|
+
* pi-tui's own OverlayHandle contract).
|
|
667
|
+
* @param {object} args
|
|
668
|
+
* @param {object} args.tui
|
|
669
|
+
* @param {object} args.service
|
|
670
|
+
* @param {import("./view.js").CockpitView} args.view
|
|
671
|
+
* @param {string} args.cwd
|
|
672
|
+
* @param {(text: string) => void} [args.onNarrate] - see ProjectOverlay's own doc
|
|
673
|
+
*/
|
|
674
|
+
export function openProjectOverlay({ tui, service, view, cwd, onNarrate }) {
|
|
675
|
+
let handle;
|
|
676
|
+
const overlay = new ProjectOverlay({
|
|
677
|
+
service, view, cwd, onNarrate,
|
|
678
|
+
onClose: () => handle?.hide(),
|
|
679
|
+
requestRender: () => tui.requestRender()
|
|
680
|
+
});
|
|
681
|
+
handle = tui.showOverlay(overlay, { width: 76, maxHeight: "70%", anchor: "center" });
|
|
682
|
+
return handle;
|
|
683
|
+
}
|