@iseoane/agent-depot 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.es.md +137 -34
  3. package/README.md +120 -29
  4. package/dist/src/app-flow.js +56 -24
  5. package/dist/src/profile-export.js +72 -9
  6. package/dist/src/profile-import.js +3 -2
  7. package/dist/src/profile.js +71 -7
  8. package/dist/src/tui/app-actions.js +82 -33
  9. package/dist/src/tui/app.js +19 -14
  10. package/dist/src/tui/catalog-browse.js +6 -1
  11. package/dist/src/tui/catalog-panel.js +11 -11
  12. package/dist/src/tui/catalog-state.js +28 -0
  13. package/dist/src/tui/catalog-tree.js +22 -5
  14. package/dist/src/tui/catalog-view.js +59 -16
  15. package/dist/src/tui/file-picker-model.js +87 -0
  16. package/dist/src/tui/file-picker.js +142 -0
  17. package/dist/src/tui/header-layout.js +33 -0
  18. package/dist/src/tui/installations-panel.js +3 -3
  19. package/dist/src/tui/installations-rows.js +20 -5
  20. package/dist/src/tui/installations-tree.js +4 -7
  21. package/dist/src/tui/installations-view.js +15 -6
  22. package/dist/src/tui/installations.js +9 -7
  23. package/dist/src/tui/manage-panel.js +7 -6
  24. package/dist/src/tui/mode-keys.js +9 -0
  25. package/dist/src/tui/package-rows.js +17 -5
  26. package/dist/src/tui/panel-parts.js +119 -6
  27. package/dist/src/tui/preview-window.js +51 -0
  28. package/dist/src/tui/profile-view.js +394 -136
  29. package/dist/src/tui/sources-mode.js +3 -1
  30. package/dist/src/tui/sources-recipes.js +50 -19
  31. package/dist/src/tui/sources-view.js +50 -14
  32. package/dist/src/tui/text-wrap.js +53 -0
  33. package/dist/src/tui/theme.js +14 -7
  34. package/dist/src/tui/updates-flow.js +2 -2
  35. package/dist/src/tui/updates-keys.js +34 -5
  36. package/dist/src/tui/updates-model.js +2 -0
  37. package/dist/src/tui/updates-panel.js +40 -15
  38. package/dist/src/tui/updates-preview.js +65 -0
  39. package/dist/src/tui/updates-view.js +17 -2
  40. package/dist/src/tui/updates.js +14 -5
  41. package/dist/src/tui/window.js +8 -6
  42. package/package.json +6 -6
  43. package/skills/doctor-md-agents/README.md +3 -1
  44. package/skills/doctor-md-skill/README.md +2 -2
@@ -1,101 +1,189 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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";
4
- import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Text, useWindowSize } from "ink";
4
+ import stringWidth from "string-width";
5
+ import { useCallback, 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";
12
+ import { DEFAULT_PROFILE_FILE_NAME, exportDestination, fileNameNotice, pathExists, pickerStartDirectory, resolveExportFileName, } from "./file-picker-model.js";
13
+ import { FilePicker } from "./file-picker.js";
10
14
  import { useKeys } from "./keys.js";
11
- import { answerYesNo, moveCursor, toggleChoice } from "./mode-keys.js";
12
- import { ChecklistLines, PreviewConfirm, PreviewLines } from "./panel-parts.js";
15
+ import { answerYesNo, moveCursor, movePreviewCursor, toggleChoice } from "./mode-keys.js";
16
+ import { BorderPanel, PreviewConfirm, PreviewLines } from "./panel-parts.js";
17
+ import { fitLines, wrapText } from "./text-wrap.js";
18
+ import { theme } from "./theme.js";
19
+ import { useMirrored, useMounted } from "./view-state.js";
13
20
  import { computeWindow, useListHeight } from "./window.js";
14
21
  function rowsFromExport(plan) {
15
22
  return [
16
23
  ...plan.profile.sources.map((value, index) => ({
17
- block: "sources", index, label: `Sources: ${value.url}`, selectable: true,
24
+ id: `sources:${value.url}`, block: "sources", index, ordinal: index + 1,
25
+ label: value.url, selectable: true,
18
26
  })),
19
27
  ...plan.profile.skills.map((value, index) => ({
20
- block: "skills", index, label: `Skills: ${value.path}`, selectable: true,
28
+ id: `skills:${JSON.stringify([value.source, value.path])}`, block: "skills", index, ordinal: index + 1,
29
+ label: describeProfileSkill(value), selectable: true,
21
30
  })),
22
31
  ...plan.profile.apps.map((value, index) => ({
23
- block: "apps", index, label: `Apps: ${value.name} (${value.platform ?? "all platforms"})`, selectable: true,
32
+ id: `apps:${JSON.stringify([value.name, value.platform ?? null])}`, block: "apps", index, ordinal: index + 1,
33
+ label: `App recipe ${value.name} (${value.platform ?? "all platforms"})`, selectable: true,
24
34
  })),
25
35
  ...plan.profile.packages.map((value, index) => ({
26
- block: "packages", index, label: `Packages: ${describeProfilePackage(value)}`, selectable: true,
36
+ id: `packages:${JSON.stringify(value)}`, block: "packages", index, ordinal: index + 1,
37
+ label: describeProfilePackage(value), selectable: true,
38
+ })),
39
+ ...plan.profile.replicas.map((value, index) => ({
40
+ id: `replicas:${JSON.stringify(value)}`, block: "replicas", index, ordinal: index + 1,
41
+ label: describeProfileReplica(value), selectable: true,
27
42
  })),
28
43
  ...plan.exclusions.map((item, index) => ({
29
- block: "excluded", index, label: `Excluded ${item.label}: ${item.reason}`, selectable: false,
44
+ id: `excluded:${index}`, block: "excluded", index, label: describeProfileExportExclusion(item), selectable: false,
30
45
  })),
46
+ { id: "skill-export-explanation", block: "excluded", index: -1,
47
+ label: SKILL_EXPORT_EXPLANATION, selectable: false },
31
48
  ...plan.warnings.filter(warning => warning.kind === "load").map((warning, index) => ({
32
- block: "excluded", index: plan.exclusions.length + index, label: warning.message, selectable: false,
49
+ id: `warning:${index}`, block: "excluded", index: plan.exclusions.length + index,
50
+ label: warning.message, selectable: false,
33
51
  })),
34
52
  ];
35
53
  }
54
+ function exportDisplayRows(rows, expanded) {
55
+ const choices = (block) => rows.filter(row => row.block === block);
56
+ const group = (id, label, depth, children) => {
57
+ if (!children.length)
58
+ return [];
59
+ const header = { kind: "group", id, label, depth };
60
+ return expanded.has(id) ? [header, ...children] : [header];
61
+ };
62
+ const sourceChildren = [
63
+ ...group("group:sources", `Source choices (${choices("sources").length})`, 1, choices("sources")),
64
+ ...group("group:skills", `Skills (${choices("skills").length})`, 1, choices("skills")),
65
+ ...group("group:apps", `Apps (${choices("apps").length})`, 1, choices("apps")),
66
+ ...group("group:packages", `Packages (${choices("packages").length})`, 1, choices("packages")),
67
+ ];
68
+ return [
69
+ ...group("group:sources-root", "Sources", 0, sourceChildren),
70
+ ...group("group:replicas", `Installed replicas (${choices("replicas").length})`, 0, choices("replicas")),
71
+ ...group("group:excluded", `Export notes (${choices("excluded").length})`, 0, choices("excluded")),
72
+ ];
73
+ }
74
+ function physicalListWindow(rows, rowHeights, selected, height) {
75
+ if (!rows.length)
76
+ return { start: 0, end: 0, indicator: undefined, stickyGroup: false };
77
+ const cursor = Math.min(Math.max(selected, 0), rows.length - 1);
78
+ const build = (budget) => {
79
+ let start = cursor;
80
+ let end = cursor + 1;
81
+ let used = rowHeights[cursor] ?? 1;
82
+ while (start > 0 || end < rows.length) {
83
+ const previous = start > 0 ? rowHeights[start - 1] : Infinity;
84
+ const next = end < rows.length ? rowHeights[end] : Infinity;
85
+ if (used + Math.min(previous, next) > budget)
86
+ break;
87
+ if (previous <= next) {
88
+ start--;
89
+ used += previous;
90
+ }
91
+ else {
92
+ used += next;
93
+ end++;
94
+ }
95
+ }
96
+ return { start, end };
97
+ };
98
+ let range = build(height);
99
+ const needsStickyGroup = (start) => start > 0 && !("kind" in rows[start]) &&
100
+ (rowHeights[start] ?? 1) < height && rows.slice(0, start).some(row => "kind" in row);
101
+ if (needsStickyGroup(range.start))
102
+ range = build(Math.max(height - 1, 1));
103
+ const stickyGroup = needsStickyGroup(range.start);
104
+ const hidden = range.start > 0 || range.end < rows.length;
105
+ return { ...range, indicator: hidden ? `${range.start + 1}–${range.end} of ${rows.length}` : undefined, stickyGroup };
106
+ }
36
107
  /** The four import blocks in one indexed list, so the checklist and the confirmation share positions. */
37
108
  function planEntries(plan) {
38
109
  return [...plan.items, ...plan.packages];
39
110
  }
40
111
  function rowsFromPlan(plan) {
41
112
  return planEntries(plan).map((item, index) => ({
42
- block: item.block, index,
113
+ id: `${item.block}:${index}`, block: item.block, index, ordinal: index + 1,
43
114
  label: `${item.status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`,
44
115
  selectable: item.status === "add",
45
116
  }));
46
117
  }
118
+ function hasChosenResource(profile) {
119
+ return profile.sources.length + profile.skills.length + profile.apps.length + profile.packages.length > 0;
120
+ }
47
121
  function selectedPlanEntries(plan, selected) {
48
122
  return planEntries(plan).filter((item, index) => item.status !== "add" || selected.some(row => row.index === index));
49
123
  }
50
- /** A plan whose items are the chosen subset, split back into the block the writer consumes. */
51
- function chosenPlan(plan, selected) {
52
- const entries = selectedPlanEntries(plan, selected);
53
- return {
54
- ...plan,
55
- items: entries.filter((item) => item.block !== "packages"),
56
- packages: entries.filter((item) => item.block === "packages"),
57
- };
124
+ function selectedImportProfile(profile, entries) {
125
+ const sources = profile.sources.filter(value => entries.some(item => item.block === "sources" && item.value.url === value.url));
126
+ const skills = profile.skills.filter(value => entries.some(item => item.block === "skills" &&
127
+ item.value.path === value.path && JSON.stringify(item.value.source) === JSON.stringify(value.source)));
128
+ const apps = profile.apps.filter(value => entries.some(item => item.block === "apps" &&
129
+ item.value.name === value.name && item.value.platform === value.platform));
130
+ const packages = profile.packages.filter(value => entries.some(item => item.block === "packages" &&
131
+ selectionKey(item.value) === selectionKey(value)));
132
+ const replicas = profile.replicas.filter(replica => {
133
+ if (replica.kind === "skill")
134
+ return skills.some(skill => skill.path === replica.path &&
135
+ JSON.stringify(skill.source) === JSON.stringify(replica.source));
136
+ if (replica.kind === "app")
137
+ return apps.some(app => app.name === replica.name && app.platform === replica.platform);
138
+ return packages.some(selection => selectionKey(selection) === selectionKey(replica.selection));
139
+ });
140
+ return parseProfile({ ...profile, format: replicas.length ? PROFILE_FORMAT_V2 : PROFILE_FORMAT,
141
+ sources, skills, apps, packages, replicas });
58
142
  }
59
143
  /** Collect choices, display shared-core previews and confirm portable Profile transfers. */
60
- export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight }) {
144
+ export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight, existsCheck = pathExists }) {
61
145
  const apps = useMemo(() => createAppOperations({
62
146
  homeDirectory: environment.homeDirectory, ...environment.appEnvironment,
63
147
  }), [environment]);
64
148
  const packages = useMemo(() => packageOperationsOf(environment), [environment]);
65
- const [mode, setModeState] = useState("browse");
66
- const modeRef = useRef("browse");
67
- const mounted = useRef(true);
68
- useEffect(() => {
69
- mounted.current = true;
70
- return () => { mounted.current = false; };
71
- }, []);
72
- const setMode = (next) => {
73
- modeRef.current = next;
149
+ const startDirectory = useMemo(() => pickerStartDirectory(environment), [environment]);
150
+ const mounted = useMounted();
151
+ const [mode, modeRef, setModeState] = useMirrored("browse");
152
+ const setMode = useCallback((next) => {
74
153
  onCapturingChange?.(next !== "browse" && next !== "results");
75
154
  setModeState(next);
76
- };
155
+ }, [onCapturingChange, setModeState]);
77
156
  const [rows, setRows] = useState([]);
78
- const [selected, setSelectedState] = useState([]);
79
- const selectedRef = useRef([]);
80
- const select = (next) => {
81
- selectedRef.current = next;
82
- setSelectedState(next);
83
- };
84
- const [cursor, setCursorState] = useState(0);
85
- const cursorRef = useRef(0);
86
- const cursorTo = (next) => {
87
- cursorRef.current = next;
88
- setCursorState(next);
89
- };
90
- const [text, setText] = useState("");
91
- const typed = useRef("");
157
+ const [expandedGroups, expandedGroupsRef, setExpandedGroups] = useMirrored(new Set());
158
+ const [selected, selectedRef, select] = useMirrored([]);
159
+ const [cursor, cursorRef, cursorTo] = useMirrored(0);
160
+ const [text, typed, setText] = useMirrored("");
161
+ const [nameError, setNameError] = useState(undefined);
162
+ const [importNotice, setImportNotice] = useState(undefined);
163
+ const [exportDirectory, setExportDirectory] = useState(undefined);
164
+ const [importDirectory, setImportDirectory] = useState(undefined);
92
165
  const [lines, setLines] = useState([]);
166
+ const detailReturnId = useRef(undefined);
167
+ const confirmRequest = useRef(undefined);
168
+ const nameRequest = useRef(0);
169
+ const importRequest = useRef(0);
93
170
  const [profile, setProfile] = useState();
94
171
  const [plan, setPlan] = useState();
95
172
  const diagnostics = useRef([]);
96
- const action = useRef("export");
97
- const profileVersion = useRef("");
98
- const height = useListHeight(listHeight, 9);
173
+ const { columns: terminalColumns } = useWindowSize();
174
+ const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
175
+ const hintCursorRow = (mode === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows)[cursorRef.current];
176
+ const enterHint = hintCursorRow === undefined ? undefined
177
+ : "kind" in hintCursorRow ? "Enter open"
178
+ : hintCursorRow.block === "excluded" ? "Enter note" : undefined;
179
+ const groupHint = hintCursorRow !== undefined && "kind" in hintCursorRow ? " · ←/→ group" : "";
180
+ const listingHint = mode === "export-list"
181
+ ? `j/k move${groupHint}${enterHint === undefined ? "" : ` · ${enterHint}`} · space toggle · a all · e export · Esc back to menu`
182
+ : "j/k move · space toggle · a all · Enter preview · Esc back to menu";
183
+ const hintText = `${selected.length} selected · ${listingHint}`;
184
+ const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
185
+ const hintRows = Math.max(Math.ceil(stringWidth(hintText) / panelWidth), 1);
186
+ const height = useListHeight(listHeight, 4 + titleRows + hintRows);
99
187
  const showLines = (next, nextMode) => {
100
188
  setLines(next);
101
189
  cursorTo(0);
@@ -112,15 +200,15 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
112
200
  }
113
201
  }
114
202
  };
115
- const prompt = (next) => {
116
- typed.current = "";
117
- setText("");
118
- setMode(next);
119
- };
120
203
  const loadRows = (next, nextMode) => {
121
204
  setRows(next);
122
205
  select(next.filter(row => row.selectable));
123
- cursorTo(0);
206
+ const open = new Set();
207
+ setExpandedGroups(open);
208
+ const firstChoice = nextMode === "export-list"
209
+ ? exportDisplayRows(next, open).findIndex(row => !("kind" in row) && row.selectable)
210
+ : 0;
211
+ cursorTo(Math.max(firstChoice, 0));
124
212
  setMode(nextMode);
125
213
  };
126
214
  const chosenProfile = () => {
@@ -132,15 +220,133 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
132
220
  skills: profile.skills.filter((_, index) => isChosen("skills", index)),
133
221
  apps: profile.apps.filter((_, index) => isChosen("apps", index)),
134
222
  packages: profile.packages.filter((_, index) => isChosen("packages", index)),
223
+ replicas: profile.replicas.filter((replica, index) => {
224
+ if (!isChosen("replicas", index))
225
+ return false;
226
+ if (replica.kind === "skill")
227
+ return profile.skills.some((skill, skillIndex) => isChosen("skills", skillIndex) && skill.path === replica.path && JSON.stringify(skill.source) === JSON.stringify(replica.source));
228
+ if (replica.kind === "app")
229
+ return profile.apps.some((app, appIndex) => isChosen("apps", appIndex) && app.name === replica.name && app.platform === replica.platform);
230
+ return profile.packages.some((selection, packageIndex) => isChosen("packages", packageIndex) && JSON.stringify(selection) === JSON.stringify(replica.selection));
231
+ }),
135
232
  };
136
233
  };
234
+ const beginExport = () => {
235
+ if (!hasChosenResource(chosenProfile())) {
236
+ showLines(["Nothing selected; nothing written."], "results");
237
+ return;
238
+ }
239
+ setExportDirectory(undefined);
240
+ setNameError(undefined);
241
+ setMode("export-folder");
242
+ };
243
+ const abandonNameCheck = () => { nameRequest.current += 1; };
244
+ const chooseExportDirectory = (directory) => {
245
+ abandonNameCheck();
246
+ setExportDirectory(directory);
247
+ setNameError(undefined);
248
+ if (typed.current === "")
249
+ setText(DEFAULT_PROFILE_FILE_NAME);
250
+ setMode("export-name");
251
+ };
252
+ const submitExportName = () => {
253
+ const outcome = resolveExportFileName(typed.current);
254
+ if (outcome.kind === "invalid") {
255
+ setNameError(outcome.message);
256
+ return;
257
+ }
258
+ const directory = exportDirectory ?? startDirectory;
259
+ const fileName = outcome.fileName;
260
+ const finalPath = exportDestination(directory, fileName);
261
+ const id = ++nameRequest.current;
262
+ void (async () => {
263
+ let exists;
264
+ try {
265
+ exists = await existsCheck(finalPath);
266
+ }
267
+ catch (error) {
268
+ if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
269
+ return;
270
+ setNameError(`Error: ${error instanceof Error ? error.message : String(error)}`);
271
+ return;
272
+ }
273
+ const current = resolveExportFileName(typed.current);
274
+ if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
275
+ return;
276
+ if (current.kind !== "ok" || current.fileName !== fileName)
277
+ return;
278
+ if (exists) {
279
+ setNameError(`${finalPath} already exists; choose another name`);
280
+ return;
281
+ }
282
+ confirmRequest.current = { kind: "export", file: finalPath };
283
+ setNameError(undefined);
284
+ showLines([`Destination: ${finalPath}`, ...profileExportPreview(chosenProfile(), diagnostics.current)], "confirm");
285
+ })();
286
+ };
287
+ const confirmApply = () => {
288
+ const request = confirmRequest.current;
289
+ if (request === undefined)
290
+ return;
291
+ void run(async () => {
292
+ let result;
293
+ if (request.kind === "export") {
294
+ await writeProfileExport(request.file, chosenProfile());
295
+ result = [`Exported profile to ${request.file}`];
296
+ }
297
+ else {
298
+ const outcomes = await applyProfileImport(request.plan, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, packageOperations: packages, ...environment.installationOptions });
299
+ result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
300
+ }
301
+ if (mounted.current) {
302
+ showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
303
+ }
304
+ });
305
+ };
306
+ const previewImport = () => {
307
+ void run(async () => {
308
+ // Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
309
+ const entries = selectedPlanEntries(plan, selectedRef.current);
310
+ const subset = selectedImportProfile(profile, entries);
311
+ const imported = await buildProfileImport(subset, operations, apps, {}, packages);
312
+ if (!mounted.current)
313
+ return;
314
+ confirmRequest.current = { kind: "import", plan: imported };
315
+ showLines(imported.preview, "confirm");
316
+ });
317
+ };
318
+ const chooseImportFile = (selection) => {
319
+ if (selection.kind !== "file")
320
+ return;
321
+ setImportDirectory(selection.directory);
322
+ const id = ++importRequest.current;
323
+ setMode("busy");
324
+ void (async () => {
325
+ try {
326
+ const importedProfile = parseProfile(JSON.parse(await readFile(selection.file, "utf8")));
327
+ const imported = await buildProfileImport(importedProfile, operations, apps, {}, packages);
328
+ if (!mounted.current || importRequest.current !== id)
329
+ return;
330
+ setProfile(importedProfile);
331
+ setPlan(imported);
332
+ setImportNotice(undefined);
333
+ loadRows(rowsFromPlan(imported), "import-list");
334
+ }
335
+ catch (error) {
336
+ if (!mounted.current || importRequest.current !== id)
337
+ return;
338
+ setImportNotice(`Error: ${error instanceof Error ? error.message : String(error)}`);
339
+ setMode("import-browse");
340
+ }
341
+ })();
342
+ };
137
343
  useKeys((input, key) => {
138
344
  const current = modeRef.current;
139
- if (current === "busy")
345
+ // The FilePicker owns its keys while it is mounted, and async work ignores every key.
346
+ if (current === "export-folder" || current === "import-browse" || current === "busy")
140
347
  return;
141
348
  if (current === "browse" || current === "results") {
142
349
  if (input === "e") {
143
- action.current = "export";
144
350
  void run(async () => {
145
351
  const exported = await buildProfileExport(operations, apps, {
146
352
  homeDirectory: environment.homeDirectory, packageOperations: packages,
@@ -153,8 +359,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
153
359
  });
154
360
  }
155
361
  else if (input === "i") {
156
- action.current = "import";
157
- prompt("import-path");
362
+ setImportNotice(undefined);
363
+ setMode("import-browse");
158
364
  }
159
365
  else if (current === "results") {
160
366
  const next = moveCursor(cursorRef.current, lines.length, input, key);
@@ -163,59 +369,54 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
163
369
  }
164
370
  return;
165
371
  }
166
- if (current === "export-path" || current === "import-path") {
372
+ if (current === "export-name") {
167
373
  if (key.escape) {
168
- setMode("browse");
374
+ abandonNameCheck();
375
+ setMode("export-folder");
376
+ return;
377
+ }
378
+ if (key.ctrl && input === "u") {
379
+ abandonNameCheck();
380
+ setText("");
381
+ setNameError(undefined);
169
382
  return;
170
383
  }
171
384
  if (key.return) {
172
- if (!typed.current.trim())
173
- return;
174
- if (current === "export-path") {
175
- showLines(profileExportPreview(chosenProfile(), diagnostics.current), "confirm");
176
- }
177
- else
178
- void run(async () => {
179
- const raw = JSON.parse(await readFile(typed.current, "utf8"));
180
- const imported = await buildProfileImport(raw, operations, apps, {}, packages);
181
- profileVersion.current = raw.agentDepotVersion;
182
- if (!mounted.current)
183
- return;
184
- setPlan(imported);
185
- loadRows(rowsFromPlan(imported), "import-list");
186
- });
385
+ submitExportName();
386
+ return;
387
+ }
388
+ if (key.backspace || key.delete) {
389
+ abandonNameCheck();
390
+ setText(typed.current.slice(0, -1));
391
+ setNameError(undefined);
392
+ return;
187
393
  }
188
- else if (key.backspace || key.delete) {
189
- typed.current = typed.current.slice(0, -1);
190
- setText(typed.current);
394
+ if (input && !key.ctrl && !key.meta) {
395
+ abandonNameCheck();
396
+ setText(typed.current + input);
397
+ setNameError(undefined);
191
398
  }
192
- else if (input && !key.ctrl && !key.meta) {
193
- typed.current += input;
194
- setText(typed.current);
399
+ return;
400
+ }
401
+ if (current === "detail") {
402
+ if (key.escape) {
403
+ const returnIndex = exportDisplayRows(rows, expandedGroupsRef.current)
404
+ .findIndex(row => !("kind" in row) && row.id === detailReturnId.current);
405
+ cursorTo(Math.max(returnIndex, 0));
406
+ detailReturnId.current = undefined;
407
+ setMode("export-list");
408
+ return;
195
409
  }
410
+ const next = movePreviewCursor(cursorRef.current, lines.length, input, key, height);
411
+ if (next !== undefined)
412
+ cursorTo(Math.max(0, next));
196
413
  return;
197
414
  }
198
415
  if (current === "confirm") {
199
- answerYesNo(input, key, () => {
200
- void run(async () => {
201
- let result;
202
- if (action.current === "export") {
203
- await writeProfileExport(typed.current, chosenProfile());
204
- result = [`Exported profile to ${typed.current}`];
205
- }
206
- else {
207
- const confirmed = chosenPlan(plan, selectedRef.current);
208
- const outcomes = await applyProfileImport(confirmed, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, packageOperations: packages, ...environment.installationOptions });
209
- result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
210
- }
211
- if (mounted.current) {
212
- showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
213
- }
214
- });
215
- }, () => setMode("browse"));
416
+ answerYesNo(input, key, confirmApply, () => setMode(confirmRequest.current?.kind === "import" ? "import-list" : "export-name"));
216
417
  if (input === "y" || input === "n" || key.escape)
217
418
  return;
218
- const next = moveCursor(cursorRef.current, lines.length, input, key);
419
+ const next = movePreviewCursor(cursorRef.current, lines.length, input, key, height);
219
420
  if (next !== undefined)
220
421
  cursorTo(Math.max(0, next));
221
422
  return;
@@ -224,48 +425,105 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
224
425
  setMode("browse");
225
426
  return;
226
427
  }
227
- // Export diagnostics follow the checklist, but never own its cursor.
228
- // Import same/conflict rows still draw a cursor so their differences can be reviewed.
229
- const checklistLength = rows.filter(row => row.block !== "excluded").length;
230
- const next = moveCursor(cursorRef.current, checklistLength, input, key);
231
- if (next !== undefined)
428
+ // export-list and import-list
429
+ const visibleRows = current === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows;
430
+ const currentRow = visibleRows[cursorRef.current];
431
+ const next = moveCursor(cursorRef.current, visibleRows.length, input, key);
432
+ if (next !== undefined) {
232
433
  cursorTo(Math.max(0, next));
233
- else if (input === " " && rows[cursorRef.current]?.selectable) {
234
- select(toggleChoice(rows, selectedRef.current, rows[cursorRef.current]));
434
+ return;
435
+ }
436
+ if (input === " ") {
437
+ if (currentRow && !("kind" in currentRow) && currentRow.selectable)
438
+ select(toggleChoice(rows, selectedRef.current, currentRow));
439
+ return;
235
440
  }
236
- else if (input === "a")
441
+ if (input === "a") {
237
442
  select(selectedRef.current.length ? [] : rows.filter(row => row.selectable));
238
- else if (key.return) {
239
- if (current === "export-list") {
240
- if (!selectedRef.current.length) {
241
- showLines(["Nothing selected; nothing written."], "results");
443
+ return;
444
+ }
445
+ if (current === "export-list" && input === "e") {
446
+ beginExport();
447
+ return;
448
+ }
449
+ if (currentRow && "kind" in currentRow && currentRow.kind === "group") {
450
+ if (key.return || key.rightArrow || key.leftArrow) {
451
+ const open = new Set(expandedGroupsRef.current);
452
+ if (key.return) {
453
+ if (open.has(currentRow.id))
454
+ open.delete(currentRow.id);
455
+ else
456
+ open.add(currentRow.id);
242
457
  }
458
+ else if (key.rightArrow)
459
+ open.add(currentRow.id);
243
460
  else
244
- prompt("export-path");
461
+ open.delete(currentRow.id);
462
+ setExpandedGroups(open);
245
463
  }
246
- else
247
- void run(async () => {
248
- // Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
249
- const entries = selectedPlanEntries(plan, selectedRef.current);
250
- const preview = await buildProfileImport({
251
- format: PROFILE_FORMAT, agentDepotVersion: profileVersion.current,
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);
257
- if (mounted.current)
258
- showLines(preview.preview, "confirm");
259
- });
464
+ return;
465
+ }
466
+ if (key.return && currentRow && !("kind" in currentRow) && currentRow.block === "excluded") {
467
+ detailReturnId.current = currentRow.id;
468
+ showLines(wrapText(currentRow.label, Math.max((terminalColumns || 80) - 8, 1)), "detail");
469
+ return;
470
+ }
471
+ if (key.return && current === "import-list") {
472
+ previewImport();
473
+ return;
260
474
  }
261
475
  });
262
476
  const listing = mode === "export-list" || mode === "import-list";
263
- const window = computeWindow(listing ? rows.length : lines.length, cursor, height);
264
- return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Portable user-global Profile \u00B7 e export \u00B7 i import" }), mode === "busy" ? _jsx(Text, { children: "Loading Profile\u2026" }) : null, mode.endsWith("-path") ? _jsxs(Text, { children: ["Profile path: ", text, "_"] }) : null, listing ? ["sources", "skills", "apps", "packages", "excluded"].map(block => {
265
- const choices = rows.slice(window.start, window.end).filter(row => row.block === block);
266
- if (!choices.length)
267
- return null;
268
- return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: block === "excluded" ? "Excluded" : block[0].toUpperCase() + block.slice(1) }), block === "excluded" ? _jsx(PreviewLines, { lines: choices.map(row => `${row.label} (not selectable)`) }) :
269
- _jsx(ChecklistLines, { choices: choices, cursor: choices.indexOf(rows[cursor]), isSelected: row => selected.includes(row), keyOf: row => `${row.block}:${row.index}`, label: row => `${row.label}${row.selectable ? "" : " (not selectable)"}` })] }, block);
270
- }) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines.slice(window.start, window.end), question: "Apply Profile? (y/n) \u00B7 j/k scroll" }) : null, mode === "results" ? _jsxs(_Fragment, { children: [_jsx(PreviewLines, { lines: lines.slice(window.start, window.end) }), _jsx(Text, { children: "j/k scroll" })] }) : null, window.indicator && (listing || mode === "confirm" || mode === "results") ? _jsx(Text, { children: window.indicator }) : null, listing ? _jsx(Text, { children: "j/k move \u00B7 space toggle \u00B7 a all \u00B7 Enter preview \u00B7 Esc cancel" }) : null] });
477
+ const displayRows = mode === "export-list" ? exportDisplayRows(rows, expandedGroups) : rows;
478
+ const rowHeights = displayRows.map(row => {
479
+ const prefixWidth = "kind" in row ? stringWidth(`> ${" ".repeat(row.depth * 2)}▸ `) : row.block === "excluded" ? 2
480
+ : stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
481
+ const label = !("kind" in row) && row.block !== "excluded" && !row.selectable
482
+ ? `${row.label} (not selectable)` : row.label;
483
+ return wrapText(label, Math.max(panelWidth - prefixWidth, 1)).length;
484
+ });
485
+ const listWindow = physicalListWindow(displayRows, rowHeights, cursor, height);
486
+ const window = listing ? listWindow : computeWindow(lines.length, cursor, height);
487
+ const visibleRows = displayRows.slice(window.start, window.end);
488
+ const precedingGroup = displayRows.slice(0, window.start).filter((row) => "kind" in row).at(-1);
489
+ const indexedRows = visibleRows.map((row, index) => ({ row, index: window.start + index }));
490
+ const renderedRows = listWindow.stickyGroup && precedingGroup
491
+ ? [{ row: precedingGroup, index: undefined }, ...indexedRows]
492
+ : indexedRows;
493
+ let remainingRows = height;
494
+ const listContent = renderedRows.map(({ row, index }) => {
495
+ const marker = index === cursor ? "> " : " ";
496
+ if ("kind" in row) {
497
+ const open = expandedGroups.has(row.id);
498
+ const prefix = `${marker}${" ".repeat(row.depth * 2)}${open ? "▾" : "▸"} `;
499
+ const width = Math.max(panelWidth - stringWidth(prefix), 1);
500
+ const label = fitLines(wrapText(row.label, width), remainingRows, width)
501
+ .join(`\n${" ".repeat(stringWidth(prefix))}`);
502
+ remainingRows -= label.split("\n").length;
503
+ return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, label] }, row.id);
504
+ }
505
+ if (row.block === "excluded") {
506
+ const width = Math.max(panelWidth - stringWidth(marker), 1);
507
+ const label = fitLines(wrapText(row.label, width), remainingRows, width)
508
+ .join(`\n${" ".repeat(stringWidth(marker))}`);
509
+ remainingRows -= label.split("\n").length;
510
+ return _jsxs(Text, { color: theme.value, children: [marker, label] }, row.id);
511
+ }
512
+ const prefix = `${marker}[${selected.some(choice => choice.id === row.id) ? "x" : " "}] ${row.ordinal ?? row.index + 1} `;
513
+ const value = `${row.label}${row.selectable ? "" : " (not selectable)"}`;
514
+ const width = Math.max(panelWidth - stringWidth(prefix), 1);
515
+ const label = fitLines(wrapText(value, width), remainingRows, width)
516
+ .join(`\n${" ".repeat(stringWidth(prefix))}`);
517
+ remainingRows -= label.split("\n").length;
518
+ return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: prefix }), _jsx(Text, { color: theme.value, children: label })] }, row.id);
519
+ });
520
+ const nameOutcome = resolveExportFileName(text);
521
+ const liveDestination = nameOutcome.kind === "ok"
522
+ ? exportDestination(exportDirectory ?? startDirectory, nameOutcome.fileName) : undefined;
523
+ const nameNote = nameOutcome.kind === "ok" ? fileNameNotice(nameOutcome.fileName) : undefined;
524
+ return _jsxs(Box, { flexDirection: "column", children: [mode === "export-folder" ? _jsx(FilePicker, { purpose: "directory", listHeight: listHeight, startDirectory: exportDirectory ?? startDirectory, onCancel: () => setMode("export-list"), onChoose: selection => { if (selection.kind === "directory")
525
+ chooseExportDirectory(selection.directory); } }) : null, mode === "import-browse" ? _jsx(FilePicker, { purpose: "file", listHeight: listHeight, startDirectory: importDirectory ?? startDirectory, notice: importNotice, onCancel: () => setMode("browse"), onChoose: chooseImportFile }) : null, listing ? _jsxs(BorderPanel, { title: mode === "export-list" ? "Export Profile" : "Import Profile", children: [listContent, _jsx(Text, { color: theme.legend, children: hintText }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null] }) : null, mode === "browse" ? _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 === "export-name" ? _jsxs(BorderPanel, { title: "Export file name", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Folder: " }), _jsx(Text, { color: theme.value, children: exportDirectory ?? startDirectory })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Name: " }), _jsxs(Text, { color: theme.value, children: [text, "_"] })] }), liveDestination === undefined ? null : _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Destination: " }), _jsx(Text, { color: theme.value, children: liveDestination })] }), nameNote === undefined ? null : _jsx(Text, { color: theme.warning, children: nameNote }), nameOutcome.kind === "ok" && nameOutcome.appendedExtension
526
+ ? _jsxs(Text, { color: theme.muted, children: ["A name without an extension is saved as ", nameOutcome.fileName, "."] }) : null, nameError === undefined ? null : _jsx(Text, { color: theme.error, children: nameError }), _jsx(Text, { color: theme.legend, children: "Type to edit \u00B7 Ctrl+U clear \u00B7 Enter continue \u00B7 Esc back to folder" })] }) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines, cursor: cursor, question: confirmRequest.current?.kind === "export"
527
+ ? "Save/Export Profile? (y/n) · arrows/PgUp/PgDn page · j/k line"
528
+ : "Apply Profile? (y/n) · arrows/PgUp/PgDn page · 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
529
  }