@iseoane/agent-depot 0.5.2 → 0.6.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 +19 -0
- package/README.es.md +51 -29
- package/README.md +44 -25
- package/dist/src/app-flow.js +56 -24
- package/dist/src/profile-export.js +72 -9
- package/dist/src/profile-import.js +3 -2
- package/dist/src/profile.js +71 -7
- package/dist/src/tui/app-actions.js +82 -33
- package/dist/src/tui/app.js +19 -14
- package/dist/src/tui/catalog-browse.js +6 -1
- package/dist/src/tui/catalog-panel.js +11 -11
- package/dist/src/tui/catalog-state.js +28 -0
- package/dist/src/tui/catalog-tree.js +22 -5
- package/dist/src/tui/catalog-view.js +59 -16
- package/dist/src/tui/header-layout.js +33 -0
- package/dist/src/tui/installations-panel.js +3 -3
- package/dist/src/tui/installations-rows.js +20 -5
- package/dist/src/tui/installations-tree.js +4 -7
- package/dist/src/tui/installations-view.js +15 -6
- package/dist/src/tui/installations.js +9 -7
- package/dist/src/tui/manage-panel.js +7 -6
- package/dist/src/tui/mode-keys.js +9 -0
- package/dist/src/tui/package-rows.js +17 -5
- package/dist/src/tui/panel-parts.js +38 -6
- package/dist/src/tui/preview-window.js +48 -0
- package/dist/src/tui/profile-view.js +261 -42
- package/dist/src/tui/sources-mode.js +3 -1
- package/dist/src/tui/sources-recipes.js +50 -19
- package/dist/src/tui/sources-view.js +50 -14
- package/dist/src/tui/theme.js +14 -7
- package/dist/src/tui/updates-flow.js +2 -2
- package/dist/src/tui/updates-keys.js +34 -5
- package/dist/src/tui/updates-model.js +2 -0
- package/dist/src/tui/updates-panel.js +40 -15
- package/dist/src/tui/updates-preview.js +65 -0
- package/dist/src/tui/updates-view.js +17 -2
- package/dist/src/tui/updates.js +14 -5
- package/dist/src/tui/window.js +6 -4
- package/package.json +4 -3
|
@@ -1,52 +1,176 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
3
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
4
|
+
import stringWidth from "string-width";
|
|
4
5
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
6
|
import { createAppOperations } from "../app-flow.js";
|
|
6
|
-
import { buildProfileExport, describeProfilePackage, profileExportPreview, writeProfileExport, } from "../profile-export.js";
|
|
7
|
+
import { buildProfileExport, describeProfileExportExclusion, describeProfilePackage, describeProfileReplica, describeProfileSkill, profileExportPreview, SKILL_EXPORT_EXPLANATION, writeProfileExport, } from "../profile-export.js";
|
|
7
8
|
import { applyProfileImport, buildProfileImport, } from "../profile-import.js";
|
|
8
|
-
import { PROFILE_FORMAT } from "../profile.js";
|
|
9
|
+
import { PROFILE_FORMAT, PROFILE_FORMAT_V2, parseProfile } from "../profile.js";
|
|
10
|
+
import { selectionKey } from "../package-model.js";
|
|
9
11
|
import { packageOperationsOf } from "./environment.js";
|
|
10
12
|
import { useKeys } from "./keys.js";
|
|
11
|
-
import { answerYesNo, moveCursor, toggleChoice } from "./mode-keys.js";
|
|
12
|
-
import {
|
|
13
|
+
import { answerYesNo, moveCursor, movePreviewCursor, toggleChoice } from "./mode-keys.js";
|
|
14
|
+
import { BorderPanel, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
15
|
+
import { theme } from "./theme.js";
|
|
13
16
|
import { computeWindow, useListHeight } from "./window.js";
|
|
14
17
|
function rowsFromExport(plan) {
|
|
15
18
|
return [
|
|
16
19
|
...plan.profile.sources.map((value, index) => ({
|
|
17
|
-
block: "sources", index,
|
|
20
|
+
id: `sources:${value.url}`, block: "sources", index, ordinal: index + 1,
|
|
21
|
+
label: value.url, selectable: true,
|
|
18
22
|
})),
|
|
19
23
|
...plan.profile.skills.map((value, index) => ({
|
|
20
|
-
block: "skills", index,
|
|
24
|
+
id: `skills:${JSON.stringify([value.source, value.path])}`, block: "skills", index, ordinal: index + 1,
|
|
25
|
+
label: describeProfileSkill(value), selectable: true,
|
|
21
26
|
})),
|
|
22
27
|
...plan.profile.apps.map((value, index) => ({
|
|
23
|
-
|
|
28
|
+
id: `apps:${JSON.stringify([value.name, value.platform ?? null])}`, block: "apps", index, ordinal: index + 1,
|
|
29
|
+
label: `App recipe ${value.name} (${value.platform ?? "all platforms"})`, selectable: true,
|
|
24
30
|
})),
|
|
25
31
|
...plan.profile.packages.map((value, index) => ({
|
|
26
|
-
block: "packages", index,
|
|
32
|
+
id: `packages:${JSON.stringify(value)}`, block: "packages", index, ordinal: index + 1,
|
|
33
|
+
label: describeProfilePackage(value), selectable: true,
|
|
34
|
+
})),
|
|
35
|
+
...plan.profile.replicas.map((value, index) => ({
|
|
36
|
+
id: `replicas:${JSON.stringify(value)}`, block: "replicas", index, ordinal: index + 1,
|
|
37
|
+
label: describeProfileReplica(value), selectable: true,
|
|
27
38
|
})),
|
|
28
39
|
...plan.exclusions.map((item, index) => ({
|
|
29
|
-
block: "excluded", index, label:
|
|
40
|
+
id: `excluded:${index}`, block: "excluded", index, label: describeProfileExportExclusion(item), selectable: false,
|
|
30
41
|
})),
|
|
42
|
+
{ id: "skill-export-explanation", block: "excluded", index: -1,
|
|
43
|
+
label: SKILL_EXPORT_EXPLANATION, selectable: false },
|
|
31
44
|
...plan.warnings.filter(warning => warning.kind === "load").map((warning, index) => ({
|
|
32
|
-
block: "excluded", index: plan.exclusions.length + index,
|
|
45
|
+
id: `warning:${index}`, block: "excluded", index: plan.exclusions.length + index,
|
|
46
|
+
label: warning.message, selectable: false,
|
|
33
47
|
})),
|
|
34
48
|
];
|
|
35
49
|
}
|
|
50
|
+
function exportDisplayRows(rows, expanded) {
|
|
51
|
+
const choices = (block) => rows.filter(row => row.block === block);
|
|
52
|
+
const group = (id, label, depth, children) => {
|
|
53
|
+
if (!children.length)
|
|
54
|
+
return [];
|
|
55
|
+
const header = { kind: "group", id, label, depth };
|
|
56
|
+
return expanded.has(id) ? [header, ...children] : [header];
|
|
57
|
+
};
|
|
58
|
+
const sourceChildren = [
|
|
59
|
+
...group("group:sources", `Source choices (${choices("sources").length})`, 1, choices("sources")),
|
|
60
|
+
...group("group:skills", `Skills (${choices("skills").length})`, 1, choices("skills")),
|
|
61
|
+
...group("group:apps", `Apps (${choices("apps").length})`, 1, choices("apps")),
|
|
62
|
+
...group("group:packages", `Packages (${choices("packages").length})`, 1, choices("packages")),
|
|
63
|
+
];
|
|
64
|
+
return [
|
|
65
|
+
...group("group:sources-root", "Sources", 0, sourceChildren),
|
|
66
|
+
...group("group:replicas", `Installed replicas (${choices("replicas").length})`, 0, choices("replicas")),
|
|
67
|
+
...group("group:excluded", `Export notes (${choices("excluded").length})`, 0, choices("excluded")),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
71
|
+
function wrapListText(value, width) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
let line = "";
|
|
74
|
+
let lineWidth = 0;
|
|
75
|
+
for (const { segment } of graphemes.segment(value)) {
|
|
76
|
+
const segmentWidth = stringWidth(segment);
|
|
77
|
+
if (line && lineWidth + segmentWidth > width) {
|
|
78
|
+
lines.push(line);
|
|
79
|
+
line = "";
|
|
80
|
+
lineWidth = 0;
|
|
81
|
+
}
|
|
82
|
+
line += segment;
|
|
83
|
+
lineWidth += segmentWidth;
|
|
84
|
+
}
|
|
85
|
+
return [...lines, line];
|
|
86
|
+
}
|
|
87
|
+
function fitListLines(lines, maxRows, width) {
|
|
88
|
+
if (maxRows <= 0)
|
|
89
|
+
return [];
|
|
90
|
+
if (lines.length <= maxRows)
|
|
91
|
+
return lines;
|
|
92
|
+
const visible = lines.slice(0, maxRows);
|
|
93
|
+
let last = "";
|
|
94
|
+
let used = 0;
|
|
95
|
+
for (const { segment } of graphemes.segment(visible[maxRows - 1])) {
|
|
96
|
+
const segmentWidth = stringWidth(segment);
|
|
97
|
+
if (used + segmentWidth > width - 1)
|
|
98
|
+
break;
|
|
99
|
+
last += segment;
|
|
100
|
+
used += segmentWidth;
|
|
101
|
+
}
|
|
102
|
+
visible[maxRows - 1] = `${last}…`;
|
|
103
|
+
return visible;
|
|
104
|
+
}
|
|
105
|
+
function physicalListWindow(rows, rowHeights, selected, height) {
|
|
106
|
+
if (!rows.length)
|
|
107
|
+
return { start: 0, end: 0, indicator: undefined, stickyGroup: false };
|
|
108
|
+
const cursor = Math.min(Math.max(selected, 0), rows.length - 1);
|
|
109
|
+
const build = (budget) => {
|
|
110
|
+
let start = cursor;
|
|
111
|
+
let end = cursor + 1;
|
|
112
|
+
let used = rowHeights[cursor] ?? 1;
|
|
113
|
+
while (start > 0 || end < rows.length) {
|
|
114
|
+
const previous = start > 0 ? rowHeights[start - 1] : Infinity;
|
|
115
|
+
const next = end < rows.length ? rowHeights[end] : Infinity;
|
|
116
|
+
if (used + Math.min(previous, next) > budget)
|
|
117
|
+
break;
|
|
118
|
+
if (previous <= next) {
|
|
119
|
+
start--;
|
|
120
|
+
used += previous;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
used += next;
|
|
124
|
+
end++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { start, end };
|
|
128
|
+
};
|
|
129
|
+
let range = build(height);
|
|
130
|
+
const needsStickyGroup = (start) => start > 0 && !("kind" in rows[start]) &&
|
|
131
|
+
(rowHeights[start] ?? 1) < height && rows.slice(0, start).some(row => "kind" in row);
|
|
132
|
+
if (needsStickyGroup(range.start))
|
|
133
|
+
range = build(Math.max(height - 1, 1));
|
|
134
|
+
const stickyGroup = needsStickyGroup(range.start);
|
|
135
|
+
const hidden = range.start > 0 || range.end < rows.length;
|
|
136
|
+
return { ...range, indicator: hidden ? `${range.start + 1}–${range.end} of ${rows.length}` : undefined, stickyGroup };
|
|
137
|
+
}
|
|
36
138
|
/** The four import blocks in one indexed list, so the checklist and the confirmation share positions. */
|
|
37
139
|
function planEntries(plan) {
|
|
38
140
|
return [...plan.items, ...plan.packages];
|
|
39
141
|
}
|
|
40
142
|
function rowsFromPlan(plan) {
|
|
41
143
|
return planEntries(plan).map((item, index) => ({
|
|
42
|
-
block: item.block, index,
|
|
144
|
+
id: `${item.block}:${index}`, block: item.block, index, ordinal: index + 1,
|
|
43
145
|
label: `${item.status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`,
|
|
44
146
|
selectable: item.status === "add",
|
|
45
147
|
}));
|
|
46
148
|
}
|
|
149
|
+
function hasChosenResource(profile) {
|
|
150
|
+
return profile.sources.length + profile.skills.length + profile.apps.length + profile.packages.length > 0;
|
|
151
|
+
}
|
|
47
152
|
function selectedPlanEntries(plan, selected) {
|
|
48
153
|
return planEntries(plan).filter((item, index) => item.status !== "add" || selected.some(row => row.index === index));
|
|
49
154
|
}
|
|
155
|
+
function selectedImportProfile(profile, entries) {
|
|
156
|
+
const sources = profile.sources.filter(value => entries.some(item => item.block === "sources" && item.value.url === value.url));
|
|
157
|
+
const skills = profile.skills.filter(value => entries.some(item => item.block === "skills" &&
|
|
158
|
+
item.value.path === value.path && JSON.stringify(item.value.source) === JSON.stringify(value.source)));
|
|
159
|
+
const apps = profile.apps.filter(value => entries.some(item => item.block === "apps" &&
|
|
160
|
+
item.value.name === value.name && item.value.platform === value.platform));
|
|
161
|
+
const packages = profile.packages.filter(value => entries.some(item => item.block === "packages" &&
|
|
162
|
+
selectionKey(item.value) === selectionKey(value)));
|
|
163
|
+
const replicas = profile.replicas.filter(replica => {
|
|
164
|
+
if (replica.kind === "skill")
|
|
165
|
+
return skills.some(skill => skill.path === replica.path &&
|
|
166
|
+
JSON.stringify(skill.source) === JSON.stringify(replica.source));
|
|
167
|
+
if (replica.kind === "app")
|
|
168
|
+
return apps.some(app => app.name === replica.name && app.platform === replica.platform);
|
|
169
|
+
return packages.some(selection => selectionKey(selection) === selectionKey(replica.selection));
|
|
170
|
+
});
|
|
171
|
+
return parseProfile({ ...profile, format: replicas.length ? PROFILE_FORMAT_V2 : PROFILE_FORMAT,
|
|
172
|
+
sources, skills, apps, packages, replicas });
|
|
173
|
+
}
|
|
50
174
|
/** A plan whose items are the chosen subset, split back into the block the writer consumes. */
|
|
51
175
|
function chosenPlan(plan, selected) {
|
|
52
176
|
const entries = selectedPlanEntries(plan, selected);
|
|
@@ -75,6 +199,12 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
75
199
|
setModeState(next);
|
|
76
200
|
};
|
|
77
201
|
const [rows, setRows] = useState([]);
|
|
202
|
+
const [expandedGroups, setExpandedGroupsState] = useState(new Set());
|
|
203
|
+
const expandedGroupsRef = useRef(new Set());
|
|
204
|
+
const setExpandedGroups = (next) => {
|
|
205
|
+
expandedGroupsRef.current = next;
|
|
206
|
+
setExpandedGroupsState(next);
|
|
207
|
+
};
|
|
78
208
|
const [selected, setSelectedState] = useState([]);
|
|
79
209
|
const selectedRef = useRef([]);
|
|
80
210
|
const select = (next) => {
|
|
@@ -90,12 +220,20 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
90
220
|
const [text, setText] = useState("");
|
|
91
221
|
const typed = useRef("");
|
|
92
222
|
const [lines, setLines] = useState([]);
|
|
223
|
+
const detailReturnId = useRef(undefined);
|
|
93
224
|
const [profile, setProfile] = useState();
|
|
94
225
|
const [plan, setPlan] = useState();
|
|
95
226
|
const diagnostics = useRef([]);
|
|
96
227
|
const action = useRef("export");
|
|
97
|
-
const
|
|
98
|
-
const
|
|
228
|
+
const { columns: terminalColumns } = useWindowSize();
|
|
229
|
+
const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
|
|
230
|
+
const listingHint = mode === "export-list"
|
|
231
|
+
? "j/k · Enter group/preview · ←/→ group · space · a all · Esc"
|
|
232
|
+
: "j/k move · space toggle · a all · Enter preview · Esc";
|
|
233
|
+
const renderedListingHint = mode === "export-list" ? `${listingHint} · preview shows full text` : listingHint;
|
|
234
|
+
const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
|
|
235
|
+
const hintRows = Math.max(Math.ceil(stringWidth(renderedListingHint) / panelWidth), 1);
|
|
236
|
+
const height = useListHeight(listHeight, 4 + titleRows + hintRows);
|
|
99
237
|
const showLines = (next, nextMode) => {
|
|
100
238
|
setLines(next);
|
|
101
239
|
cursorTo(0);
|
|
@@ -120,7 +258,12 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
120
258
|
const loadRows = (next, nextMode) => {
|
|
121
259
|
setRows(next);
|
|
122
260
|
select(next.filter(row => row.selectable));
|
|
123
|
-
|
|
261
|
+
const open = new Set();
|
|
262
|
+
setExpandedGroups(open);
|
|
263
|
+
const firstChoice = nextMode === "export-list"
|
|
264
|
+
? exportDisplayRows(next, open).findIndex(row => !("kind" in row) && row.selectable)
|
|
265
|
+
: 0;
|
|
266
|
+
cursorTo(Math.max(firstChoice, 0));
|
|
124
267
|
setMode(nextMode);
|
|
125
268
|
};
|
|
126
269
|
const chosenProfile = () => {
|
|
@@ -132,6 +275,15 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
132
275
|
skills: profile.skills.filter((_, index) => isChosen("skills", index)),
|
|
133
276
|
apps: profile.apps.filter((_, index) => isChosen("apps", index)),
|
|
134
277
|
packages: profile.packages.filter((_, index) => isChosen("packages", index)),
|
|
278
|
+
replicas: profile.replicas.filter((replica, index) => {
|
|
279
|
+
if (!isChosen("replicas", index))
|
|
280
|
+
return false;
|
|
281
|
+
if (replica.kind === "skill")
|
|
282
|
+
return profile.skills.some((skill, skillIndex) => isChosen("skills", skillIndex) && skill.path === replica.path && JSON.stringify(skill.source) === JSON.stringify(replica.source));
|
|
283
|
+
if (replica.kind === "app")
|
|
284
|
+
return profile.apps.some((app, appIndex) => isChosen("apps", appIndex) && app.name === replica.name && app.platform === replica.platform);
|
|
285
|
+
return profile.packages.some((selection, packageIndex) => isChosen("packages", packageIndex) && JSON.stringify(selection) === JSON.stringify(replica.selection));
|
|
286
|
+
}),
|
|
135
287
|
};
|
|
136
288
|
};
|
|
137
289
|
useKeys((input, key) => {
|
|
@@ -176,11 +328,11 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
176
328
|
}
|
|
177
329
|
else
|
|
178
330
|
void run(async () => {
|
|
179
|
-
const
|
|
180
|
-
const imported = await buildProfileImport(
|
|
181
|
-
profileVersion.current = raw.agentDepotVersion;
|
|
331
|
+
const importedProfile = parseProfile(JSON.parse(await readFile(typed.current, "utf8")));
|
|
332
|
+
const imported = await buildProfileImport(importedProfile, operations, apps, {}, packages);
|
|
182
333
|
if (!mounted.current)
|
|
183
334
|
return;
|
|
335
|
+
setProfile(importedProfile);
|
|
184
336
|
setPlan(imported);
|
|
185
337
|
loadRows(rowsFromPlan(imported), "import-list");
|
|
186
338
|
});
|
|
@@ -195,6 +347,20 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
195
347
|
}
|
|
196
348
|
return;
|
|
197
349
|
}
|
|
350
|
+
if (current === "detail") {
|
|
351
|
+
if (key.escape) {
|
|
352
|
+
const returnIndex = exportDisplayRows(rows, expandedGroupsRef.current)
|
|
353
|
+
.findIndex(row => !("kind" in row) && row.id === detailReturnId.current);
|
|
354
|
+
cursorTo(Math.max(returnIndex, 0));
|
|
355
|
+
detailReturnId.current = undefined;
|
|
356
|
+
setMode("export-list");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const next = movePreviewCursor(cursorRef.current, lines.length, input, key, height);
|
|
360
|
+
if (next !== undefined)
|
|
361
|
+
cursorTo(Math.max(0, next));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
198
364
|
if (current === "confirm") {
|
|
199
365
|
answerYesNo(input, key, () => {
|
|
200
366
|
void run(async () => {
|
|
@@ -215,7 +381,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
215
381
|
}, () => setMode("browse"));
|
|
216
382
|
if (input === "y" || input === "n" || key.escape)
|
|
217
383
|
return;
|
|
218
|
-
const next =
|
|
384
|
+
const next = movePreviewCursor(cursorRef.current, lines.length, input, key, height);
|
|
219
385
|
if (next !== undefined)
|
|
220
386
|
cursorTo(Math.max(0, next));
|
|
221
387
|
return;
|
|
@@ -224,20 +390,42 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
224
390
|
setMode("browse");
|
|
225
391
|
return;
|
|
226
392
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const next = moveCursor(cursorRef.current, checklistLength, input, key);
|
|
393
|
+
const visibleRows = current === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows;
|
|
394
|
+
const currentRow = visibleRows[cursorRef.current];
|
|
395
|
+
const next = moveCursor(cursorRef.current, visibleRows.length, input, key);
|
|
231
396
|
if (next !== undefined)
|
|
232
397
|
cursorTo(Math.max(0, next));
|
|
233
|
-
else if (
|
|
234
|
-
|
|
398
|
+
else if (current === "export-list" && currentRow && "kind" in currentRow && currentRow.kind === "group" &&
|
|
399
|
+
(key.return || key.rightArrow || key.leftArrow)) {
|
|
400
|
+
if (key.return && !hasChosenResource(chosenProfile())) {
|
|
401
|
+
showLines(["Nothing selected; nothing written."], "results");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const open = new Set(expandedGroupsRef.current);
|
|
405
|
+
if (key.return) {
|
|
406
|
+
if (open.has(currentRow.id))
|
|
407
|
+
open.delete(currentRow.id);
|
|
408
|
+
else
|
|
409
|
+
open.add(currentRow.id);
|
|
410
|
+
}
|
|
411
|
+
else if (key.rightArrow)
|
|
412
|
+
open.add(currentRow.id);
|
|
413
|
+
else
|
|
414
|
+
open.delete(currentRow.id);
|
|
415
|
+
setExpandedGroups(open);
|
|
416
|
+
}
|
|
417
|
+
else if (input === " " && currentRow && !("kind" in currentRow) && currentRow.selectable) {
|
|
418
|
+
select(toggleChoice(rows, selectedRef.current, currentRow));
|
|
235
419
|
}
|
|
236
420
|
else if (input === "a")
|
|
237
421
|
select(selectedRef.current.length ? [] : rows.filter(row => row.selectable));
|
|
422
|
+
else if (current === "export-list" && key.return && currentRow && !("kind" in currentRow) && currentRow.block === "excluded") {
|
|
423
|
+
detailReturnId.current = currentRow.id;
|
|
424
|
+
showLines(wrapListText(currentRow.label, Math.max((terminalColumns || 80) - 8, 1)), "detail");
|
|
425
|
+
}
|
|
238
426
|
else if (key.return) {
|
|
239
427
|
if (current === "export-list") {
|
|
240
|
-
if (!
|
|
428
|
+
if (!hasChosenResource(chosenProfile())) {
|
|
241
429
|
showLines(["Nothing selected; nothing written."], "results");
|
|
242
430
|
}
|
|
243
431
|
else
|
|
@@ -247,25 +435,56 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
247
435
|
void run(async () => {
|
|
248
436
|
// Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
|
|
249
437
|
const entries = selectedPlanEntries(plan, selectedRef.current);
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
sources: entries.filter(item => item.block === "sources").map(item => item.value),
|
|
253
|
-
skills: entries.filter(item => item.block === "skills").map(item => item.value),
|
|
254
|
-
apps: entries.filter(item => item.block === "apps").map(item => item.value),
|
|
255
|
-
packages: entries.filter(item => item.block === "packages").map(item => item.value),
|
|
256
|
-
}, operations, apps, {}, packages);
|
|
438
|
+
const subset = selectedImportProfile(profile, entries);
|
|
439
|
+
const preview = await buildProfileImport(subset, operations, apps, {}, packages);
|
|
257
440
|
if (mounted.current)
|
|
258
441
|
showLines(preview.preview, "confirm");
|
|
259
442
|
});
|
|
260
443
|
}
|
|
261
444
|
});
|
|
262
445
|
const listing = mode === "export-list" || mode === "import-list";
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
446
|
+
const displayRows = mode === "export-list" ? exportDisplayRows(rows, expandedGroups) : rows;
|
|
447
|
+
const rowHeights = displayRows.map(row => {
|
|
448
|
+
const prefixWidth = "kind" in row ? stringWidth(`> ${" ".repeat(row.depth * 2)}▸ `) : row.block === "excluded" ? 2
|
|
449
|
+
: stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
|
|
450
|
+
const label = !("kind" in row) && row.block !== "excluded" && !row.selectable
|
|
451
|
+
? `${row.label} (not selectable)` : row.label;
|
|
452
|
+
return wrapListText(label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
453
|
+
});
|
|
454
|
+
const listWindow = physicalListWindow(displayRows, rowHeights, cursor, height);
|
|
455
|
+
const window = listing ? listWindow : computeWindow(lines.length, cursor, height);
|
|
456
|
+
const visibleRows = displayRows.slice(window.start, window.end);
|
|
457
|
+
const precedingGroup = displayRows.slice(0, window.start).filter((row) => "kind" in row).at(-1);
|
|
458
|
+
const indexedRows = visibleRows.map((row, index) => ({ row, index: window.start + index }));
|
|
459
|
+
const renderedRows = listWindow.stickyGroup && precedingGroup
|
|
460
|
+
? [{ row: precedingGroup, index: undefined }, ...indexedRows]
|
|
461
|
+
: indexedRows;
|
|
462
|
+
let remainingRows = height;
|
|
463
|
+
const listContent = renderedRows.map(({ row, index }) => {
|
|
464
|
+
const marker = index === cursor ? "> " : " ";
|
|
465
|
+
if ("kind" in row) {
|
|
466
|
+
const open = expandedGroups.has(row.id);
|
|
467
|
+
const prefix = `${marker}${" ".repeat(row.depth * 2)}${open ? "▾" : "▸"} `;
|
|
468
|
+
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
469
|
+
const label = fitListLines(wrapListText(row.label, width), remainingRows, width)
|
|
470
|
+
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
471
|
+
remainingRows -= label.split("\n").length;
|
|
472
|
+
return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, label] }, row.id);
|
|
473
|
+
}
|
|
474
|
+
if (row.block === "excluded") {
|
|
475
|
+
const width = Math.max(panelWidth - stringWidth(marker), 1);
|
|
476
|
+
const label = fitListLines(wrapListText(row.label, width), remainingRows, width)
|
|
477
|
+
.join(`\n${" ".repeat(stringWidth(marker))}`);
|
|
478
|
+
remainingRows -= label.split("\n").length;
|
|
479
|
+
return _jsxs(Text, { color: theme.value, children: [marker, label] }, row.id);
|
|
480
|
+
}
|
|
481
|
+
const prefix = `${marker}[${selected.some(choice => choice.id === row.id) ? "x" : " "}] ${row.ordinal ?? row.index + 1} `;
|
|
482
|
+
const value = `${row.label}${row.selectable ? "" : " (not selectable)"}`;
|
|
483
|
+
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
484
|
+
const label = fitListLines(wrapListText(value, width), remainingRows, width)
|
|
485
|
+
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
486
|
+
remainingRows -= label.split("\n").length;
|
|
487
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: prefix }), _jsx(Text, { color: theme.value, children: label })] }, row.id);
|
|
488
|
+
});
|
|
489
|
+
return _jsxs(Box, { flexDirection: "column", children: [listing ? _jsxs(BorderPanel, { title: mode === "export-list" ? "Export Profile" : "Import Profile", children: [listContent, _jsx(Text, { color: theme.legend, children: renderedListingHint }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null] }) : null, mode !== "confirm" && mode !== "detail" && !listing ? _jsx(BorderPanel, { title: "Portable Profile", children: _jsx(Text, { color: theme.legend, children: "e export \u00B7 i import" }) }) : null, mode === "busy" ? _jsx(Text, { children: "Loading Profile\u2026" }) : null, mode.endsWith("-path") ? _jsxs(Text, { children: ["Profile path: ", text, "_"] }) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines, cursor: cursor, question: "Apply Profile? (y/n) \u00B7 arrows/PgUp/PgDn page \u00B7 j/k line" }) : null, mode === "detail" ? _jsx(PreviewConfirm, { title: "Export note", lines: lines, cursor: cursor, question: "Esc close \u00B7 arrows/PgUp/PgDn page \u00B7 j/k line" }) : null, mode === "results" ? _jsxs(_Fragment, { children: [_jsx(PreviewLines, { lines: lines.slice(window.start, window.end) }), _jsx(Text, { children: "j/k scroll" })] }) : null, window.indicator && mode === "results" ? _jsx(Text, { children: window.indicator }) : null] });
|
|
271
490
|
}
|
|
@@ -9,8 +9,10 @@ export function modeReducer(mode, event) {
|
|
|
9
9
|
return mode.kind === "input" ? { kind: "input", value: event.value } : mode;
|
|
10
10
|
case "confirm":
|
|
11
11
|
return event.action === "refresh"
|
|
12
|
-
? { kind: "confirm", action: "refresh", sources: event.sources }
|
|
12
|
+
? { kind: "confirm", action: "refresh", sources: event.sources, cursor: 0 }
|
|
13
13
|
: { kind: "confirm", action: "remove", source: event.source };
|
|
14
|
+
case "preview-cursor":
|
|
15
|
+
return mode.kind === "confirm" && mode.action === "refresh" ? { ...mode, cursor: event.cursor } : mode;
|
|
14
16
|
case "busy":
|
|
15
17
|
return { kind: "busy" };
|
|
16
18
|
case "done":
|
|
@@ -7,16 +7,31 @@ import { BUILT_IN_SOURCE } from "../sources.js";
|
|
|
7
7
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
8
8
|
import { ActionPanel } from "./catalog-panel.js";
|
|
9
9
|
import { hasUnmanagedCopy, isInstalled, loadInstalledSkills } from "./catalog-installs.js";
|
|
10
|
-
import { appName, appStepLines } from "./app-actions.js";
|
|
10
|
+
import { appName, appRecipePreviewLines, appStepLines } from "./app-actions.js";
|
|
11
11
|
import { errorText } from "./batch.js";
|
|
12
12
|
import { useMounted } from "./view-state.js";
|
|
13
|
-
import { PreviewConfirm } from "./panel-parts.js";
|
|
14
|
-
import { answerYesNo } from "./mode-keys.js";
|
|
13
|
+
import { BorderPanel, DecisionPrompt, PreviewConfirm } from "./panel-parts.js";
|
|
14
|
+
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
15
|
+
import { previewViewport } from "./preview-window.js";
|
|
15
16
|
import { theme } from "./theme.js";
|
|
17
|
+
function recipeApprovalChrome(entry) {
|
|
18
|
+
return {
|
|
19
|
+
title: "Recipe approval · does not install",
|
|
20
|
+
questions: [`Approve ${appName(entry)}? y/n · recipe approval does not install the App.`, "Scroll to end before y; n cancels."],
|
|
21
|
+
footerRows: 2,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function uninstallChrome(mode) {
|
|
25
|
+
return {
|
|
26
|
+
title: mode.kind === "uninstall-manual" ? "App manual uninstall" : "App uninstall confirmation",
|
|
27
|
+
questions: [mode.kind === "uninstall-manual" ? "Complete the manual uninstall, then check now? y/n" : `Uninstall ${appName(mode.removal.entry)}? y/n`, "Scroll to end before y; n cancels."],
|
|
28
|
+
footerRows: 2,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
16
31
|
/**
|
|
17
32
|
* Recipe loading and captured actions; Sources owns the single list and cursor.
|
|
18
33
|
*/
|
|
19
|
-
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, }) {
|
|
34
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, terminalRows, terminalColumns, }) {
|
|
20
35
|
const homeDirectory = environment?.homeDirectory;
|
|
21
36
|
const appEnvironment = environment?.appEnvironment;
|
|
22
37
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -48,13 +63,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
48
63
|
if (mounted.current) {
|
|
49
64
|
setMode({
|
|
50
65
|
kind: "approve",
|
|
51
|
-
entry, removeAfterApproval,
|
|
52
|
-
lines:
|
|
53
|
-
`File: ${entry.file}`,
|
|
54
|
-
`Platform: ${entry.recipe?.platform ?? "all"}`,
|
|
55
|
-
`Status: ${status}`,
|
|
56
|
-
...steps.flatMap(step => [step.step, ...appStepLines(step)]),
|
|
57
|
-
],
|
|
66
|
+
entry, removeAfterApproval, cursor: 0,
|
|
67
|
+
lines: appRecipePreviewLines(entry, status, steps),
|
|
58
68
|
});
|
|
59
69
|
}
|
|
60
70
|
}
|
|
@@ -183,7 +193,13 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
183
193
|
}
|
|
184
194
|
const cancel = () => setMode({ kind: "browse" });
|
|
185
195
|
if (current.kind === "approve") {
|
|
186
|
-
|
|
196
|
+
const viewport = previewViewport(current.lines, current.cursor, terminalRows, terminalColumns, recipeApprovalChrome(current.entry));
|
|
197
|
+
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
198
|
+
if (cursor !== undefined)
|
|
199
|
+
setMode({ ...current, cursor });
|
|
200
|
+
else if (input === "n" || key.escape)
|
|
201
|
+
cancel();
|
|
202
|
+
else if (input === "y" && viewport.reachedEnd) {
|
|
187
203
|
setMode({ kind: "busy" });
|
|
188
204
|
void apps.approve(current.entry).then(async () => {
|
|
189
205
|
await reload();
|
|
@@ -199,7 +215,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
199
215
|
cancel();
|
|
200
216
|
}
|
|
201
217
|
});
|
|
202
|
-
}
|
|
218
|
+
}
|
|
203
219
|
return true;
|
|
204
220
|
}
|
|
205
221
|
if (current.kind === "uninstall-question") {
|
|
@@ -207,7 +223,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
207
223
|
setMode({ kind: "busy" });
|
|
208
224
|
void apps.planLifecycle(current.removal.entry, "uninstall").then(plan => {
|
|
209
225
|
if (mounted.current)
|
|
210
|
-
setMode({ kind: !plan.argv && plan.manual ? "uninstall-manual" : "uninstall-confirm", removal: current.removal, plan });
|
|
226
|
+
setMode({ kind: !plan.argv && plan.manual ? "uninstall-manual" : "uninstall-confirm", removal: current.removal, plan, cursor: 0 });
|
|
211
227
|
}, error => { if (mounted.current) {
|
|
212
228
|
setError(errorText(error));
|
|
213
229
|
cancel();
|
|
@@ -216,7 +232,15 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
216
232
|
return true;
|
|
217
233
|
}
|
|
218
234
|
if (current.kind === "uninstall-confirm" || current.kind === "uninstall-manual") {
|
|
219
|
-
|
|
235
|
+
const lines = appStepLines(current.plan);
|
|
236
|
+
const viewport = previewViewport(lines, current.cursor, terminalRows, terminalColumns, uninstallChrome(current));
|
|
237
|
+
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
238
|
+
if (cursor !== undefined)
|
|
239
|
+
setMode({ ...current, cursor });
|
|
240
|
+
else if (input === "n" || key.escape)
|
|
241
|
+
cancel();
|
|
242
|
+
else if (input === "y" && viewport.reachedEnd)
|
|
243
|
+
void uninstall(current);
|
|
220
244
|
return true;
|
|
221
245
|
}
|
|
222
246
|
if (current.kind === "remove") {
|
|
@@ -272,8 +296,15 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
272
296
|
}
|
|
273
297
|
}, [apps, mounted]);
|
|
274
298
|
useEffect(() => { void reload(); }, [reload]);
|
|
275
|
-
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, notice ? _jsx(Text, { color: theme.success, children: notice }) : null, mode.kind === "guide" ? (_jsxs(Text, { children:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, notice ? _jsx(Text, { color: theme.success, children: notice }) : null, mode.kind === "guide" ? (_jsxs(BorderPanel, { title: "Add an App recipe", children: [_jsx(Text, { children: "1 With the AI skill \u00B7 install if missing, then copy the prompt" }), _jsx(Text, { children: "2 Manual \u00B7 write a recipe using the schema" }), _jsx(Text, { children: "n or Esc closes" })] })) : null, mode.kind === "manual" ? (_jsxs(BorderPanel, { title: "Write an App recipe", children: [_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory] }), _jsx(Text, { children: "Schema: agent-depot app schema \u00B7 n or Esc closes" })] })) : null, mode.kind === "prompt" ? (_jsxs(BorderPanel, { title: "App recipe authoring prompt", children: [_jsx(Text, { children: "Use the agent-depot-apprecipe skill to create the recipe for <app>" }), _jsxs(Text, { children: ["Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(BorderPanel, { title: "App recipe workflow", children: _jsx(Text, { children: "Working..." }) }) : null, mode.kind === "approve" ? (() => {
|
|
300
|
+
const chrome = recipeApprovalChrome(mode.entry);
|
|
301
|
+
const viewport = previewViewport(mode.lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
302
|
+
return _jsx(PreviewConfirm, { title: chrome.title, lines: mode.lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
303
|
+
})() : null, mode.kind === "uninstall-question" ? (_jsxs(BorderPanel, { title: "Uninstall before recipe removal", children: [_jsxs(Text, { children: ["App ", appName(mode.removal.entry), " is installed or tracked", mode.removal.installedVersion ? ` (${mode.removal.installedVersion})` : "", "."] }), _jsxs(Text, { children: [_jsx(DecisionPrompt, { children: "Uninstall it before removing the recipe?" }), _jsx(Text, { color: theme.legend, children: "(n cancels removal)" })] })] })) : null, mode.kind === "uninstall-confirm" || mode.kind === "uninstall-manual" ? (() => {
|
|
304
|
+
const chrome = uninstallChrome(mode);
|
|
305
|
+
const lines = appStepLines(mode.plan);
|
|
306
|
+
const viewport = previewViewport(lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
307
|
+
return _jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
308
|
+
})() : null, mode.kind === "remove" ? (_jsx(PreviewConfirm, { title: "Recipe removal", lines: [`Delete recipe: ${mode.removal.file}`, "Only this recipe file or symlink will be removed; App data and Host configuration are not deleted."], question: `Remove recipe for ${appName(mode.removal.entry)}? y/n` })) : null] }));
|
|
309
|
+
return { rows, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory, capturing: mode.kind !== "browse" };
|
|
279
310
|
}
|