@iseoane/agent-depot 0.6.9 → 0.6.11
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 +52 -0
- package/README.md +25 -3
- package/dist/src/app-flow.js +3 -3
- package/dist/src/app-recipe-removal.js +3 -1
- package/dist/src/atomic-file.js +24 -1
- package/dist/src/cli.js +49 -15
- package/dist/src/package-cli.js +1 -18
- package/dist/src/package-flow.js +8 -8
- package/dist/src/package-model.js +51 -0
- package/dist/src/path-safety.js +4 -1
- package/dist/src/profile-export.js +4 -4
- package/dist/src/profile-import.js +94 -18
- package/dist/src/project-installation.js +2 -1
- package/dist/src/project-manifest.js +7 -0
- package/dist/src/project-target.js +58 -0
- package/dist/src/skill-discovery.js +86 -6
- package/dist/src/skill-install.js +20 -8
- package/dist/src/tui/app-actions.js +18 -7
- package/dist/src/tui/app.js +20 -5
- package/dist/src/tui/batch.js +2 -1
- package/dist/src/tui/bulk-actions.js +21 -8
- package/dist/src/tui/catalog-installs.js +3 -2
- package/dist/src/tui/catalog-panel.js +3 -2
- package/dist/src/tui/catalog-state.js +47 -21
- package/dist/src/tui/catalog-tree.js +47 -10
- package/dist/src/tui/catalog-view.js +11 -5
- package/dist/src/tui/installations-view.js +19 -6
- package/dist/src/tui/installations.js +1 -1
- package/dist/src/tui/manage-panel.js +1 -1
- package/dist/src/tui/package-actions.js +2 -2
- package/dist/src/tui/package-rows.js +4 -4
- package/dist/src/tui/profile-view.js +68 -31
- package/dist/src/tui/sources-recipes.js +44 -8
- package/dist/src/tui/sources-view.js +12 -5
- package/dist/src/tui/updates-keys.js +30 -11
- package/dist/src/tui/updates-model.js +15 -3
- package/dist/src/tui/updates-panel.js +7 -1
- package/dist/src/tui/updates-tree.js +51 -0
- package/dist/src/tui/updates-view.js +8 -4
- package/dist/src/tui/updates.js +4 -3
- package/dist/src/tui/view-retention.js +17 -0
- package/dist/src/update-flow.js +3 -1
- package/dist/src/update-groups.js +29 -0
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ import { useKeys } from "./keys.js";
|
|
|
15
15
|
import { previewViewport } from "./preview-window.js";
|
|
16
16
|
import { answerYesNo, moveCursor, movePreviewCursor, toggleChoice } from "./mode-keys.js";
|
|
17
17
|
import { BorderPanel, PreviewConfirm, PreviewLines, Working } from "./panel-parts.js";
|
|
18
|
-
import { elideMiddle, fitLines, wrapText } from "./text-wrap.js";
|
|
18
|
+
import { clipText, elideMiddle, fitLines, wrapText } from "./text-wrap.js";
|
|
19
19
|
import { theme } from "./theme.js";
|
|
20
20
|
import { useMirrored, useMounted } from "./view-state.js";
|
|
21
21
|
import { computeWindow, useListHeight } from "./window.js";
|
|
@@ -53,14 +53,16 @@ function rowsFromExport(plan) {
|
|
|
53
53
|
})),
|
|
54
54
|
];
|
|
55
55
|
}
|
|
56
|
+
/** A group header, followed by its children while the group is open; an empty group is not shown. */
|
|
57
|
+
function section(expanded, id, label, depth, children) {
|
|
58
|
+
if (!children.length)
|
|
59
|
+
return [];
|
|
60
|
+
const header = { kind: "group", id, label, depth };
|
|
61
|
+
return expanded.has(id) ? [header, ...children] : [header];
|
|
62
|
+
}
|
|
56
63
|
function exportDisplayRows(rows, expanded) {
|
|
57
64
|
const choices = (block) => rows.filter(row => row.block === block);
|
|
58
|
-
const group = (id, label, depth, children) =>
|
|
59
|
-
if (!children.length)
|
|
60
|
-
return [];
|
|
61
|
-
const header = { kind: "group", id, label, depth };
|
|
62
|
-
return expanded.has(id) ? [header, ...children] : [header];
|
|
63
|
-
};
|
|
65
|
+
const group = (id, label, depth, children) => section(expanded, id, label, depth, children);
|
|
64
66
|
const sourceChildren = [
|
|
65
67
|
...group("group:sources", `Source choices (${choices("sources").length})`, 1, choices("sources")),
|
|
66
68
|
...group("group:skills", `Skills (${choices("skills").length})`, 1, choices("skills")),
|
|
@@ -73,6 +75,30 @@ function exportDisplayRows(rows, expanded) {
|
|
|
73
75
|
...group("group:excluded", `Export notes (${choices("excluded").length})`, 0, choices("excluded")),
|
|
74
76
|
];
|
|
75
77
|
}
|
|
78
|
+
const IMPORT_ADD = "import:add";
|
|
79
|
+
const IMPORT_APPS = "import:apps";
|
|
80
|
+
const importRepoId = (repo) => `import:add:${repo}`;
|
|
81
|
+
/** Additions by Source and App recipes, then what is already there, then what conflicts. */
|
|
82
|
+
function importDisplayRows(rows, expanded) {
|
|
83
|
+
const added = rows.filter(row => row.status === "add");
|
|
84
|
+
const recipes = added.filter(row => row.block === "apps");
|
|
85
|
+
const repos = [...new Set(added.filter(row => row.block !== "apps").map(row => row.source ?? ""))];
|
|
86
|
+
const sources = repos.flatMap(repo => {
|
|
87
|
+
const children = added.filter(row => row.block !== "apps" && (row.source ?? "") === repo);
|
|
88
|
+
return section(expanded, importRepoId(repo), `${repo} (${children.length})`, 1, children);
|
|
89
|
+
});
|
|
90
|
+
const unchanged = rows.filter(row => row.status === "same"), conflicts = rows.filter(row => row.status === "conflict");
|
|
91
|
+
return [
|
|
92
|
+
...section(expanded, IMPORT_ADD, `To import (${added.length})`, 0, [...sources, ...section(expanded, IMPORT_APPS, `App recipes (${recipes.length})`, 1, recipes)]),
|
|
93
|
+
...section(expanded, "import:same", `Unchanged (${unchanged.length})`, 0, unchanged),
|
|
94
|
+
...section(expanded, "import:conflict", `Conflicts (${conflicts.length}) — not selectable`, 0, conflicts),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/** Additions and conflicts are open to read; the unchanged items are what the person already has. */
|
|
98
|
+
function importOpenGroups(rows) {
|
|
99
|
+
const repos = rows.filter(row => row.status === "add" && row.block !== "apps").map(row => importRepoId(row.source ?? ""));
|
|
100
|
+
return new Set([IMPORT_ADD, IMPORT_APPS, "import:conflict", ...repos]);
|
|
101
|
+
}
|
|
76
102
|
function physicalListWindow(rows, rowHeights, selected, height) {
|
|
77
103
|
if (!rows.length)
|
|
78
104
|
return { start: 0, end: 0, indicator: undefined, stickyGroup: false };
|
|
@@ -144,7 +170,7 @@ function planEntries(plan) {
|
|
|
144
170
|
function rowsFromPlan(plan) {
|
|
145
171
|
return planEntries(plan).map((item, index) => ({
|
|
146
172
|
id: `${item.block}:${index}`, block: item.block, index, ordinal: index + 1,
|
|
147
|
-
label:
|
|
173
|
+
label: item.summary, status: item.status, ...(item.group === undefined ? {} : { source: item.group }),
|
|
148
174
|
selectable: item.status === "add",
|
|
149
175
|
}));
|
|
150
176
|
}
|
|
@@ -173,6 +199,12 @@ function selectedImportProfile(profile, entries) {
|
|
|
173
199
|
return parseProfile({ ...profile, format: replicas.length ? PROFILE_FORMAT_V2 : PROFILE_FORMAT,
|
|
174
200
|
sources, skills, apps, packages, replicas });
|
|
175
201
|
}
|
|
202
|
+
/** The rows a list stage shows: the grouped export or import tree, otherwise the flat rows. */
|
|
203
|
+
function listRows(mode, rows, expanded) {
|
|
204
|
+
if (mode === "export-list")
|
|
205
|
+
return exportDisplayRows(rows, expanded);
|
|
206
|
+
return mode === "import-list" ? importDisplayRows(rows, expanded) : rows;
|
|
207
|
+
}
|
|
176
208
|
/** Collect choices, display shared-core previews and confirm portable Profile transfers. */
|
|
177
209
|
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight, existsCheck = pathExists }) {
|
|
178
210
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -208,14 +240,17 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
208
240
|
const diagnostics = useRef([]);
|
|
209
241
|
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
210
242
|
const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
|
|
211
|
-
const hintCursorRow = (mode
|
|
243
|
+
const hintCursorRow = listRows(mode, rows, expandedGroupsRef.current)[cursorRef.current];
|
|
212
244
|
const enterHint = hintCursorRow === undefined ? undefined
|
|
213
245
|
: "kind" in hintCursorRow ? "Enter open"
|
|
214
246
|
: hintCursorRow.block === "excluded" ? "Enter note" : undefined;
|
|
215
247
|
const groupHint = hintCursorRow !== undefined && "kind" in hintCursorRow ? " · ←/→ group" : "";
|
|
248
|
+
// With nothing to choose, a header has no item row to move to, so Enter previews instead of opening it.
|
|
249
|
+
const nothingToImport = !rows.some(row => row.selectable);
|
|
250
|
+
const importEnter = hintCursorRow !== undefined && "kind" in hintCursorRow && !nothingToImport ? "Enter open" : "Enter preview";
|
|
216
251
|
const listingHint = mode === "export-list"
|
|
217
252
|
? `j/k move${groupHint}${enterHint === undefined ? "" : ` · ${enterHint}`} · space toggle · a all · e export · Esc back to menu`
|
|
218
|
-
:
|
|
253
|
+
: `j/k move${groupHint} · space toggle · a all · ${importEnter} · Esc back to menu`;
|
|
219
254
|
const hintText = `${selected.length} selected · ${listingHint}`;
|
|
220
255
|
const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
|
|
221
256
|
const hintRows = Math.max(Math.ceil(stringWidth(hintText) / panelWidth), 1);
|
|
@@ -250,11 +285,9 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
250
285
|
const loadRows = (next, nextMode) => {
|
|
251
286
|
setRows(next);
|
|
252
287
|
select(next.filter(row => row.selectable));
|
|
253
|
-
const open = new Set();
|
|
288
|
+
const open = nextMode === "import-list" ? importOpenGroups(next) : new Set();
|
|
254
289
|
setExpandedGroups(open);
|
|
255
|
-
const firstChoice = nextMode
|
|
256
|
-
? exportDisplayRows(next, open).findIndex(row => !("kind" in row) && row.selectable)
|
|
257
|
-
: 0;
|
|
290
|
+
const firstChoice = listRows(nextMode, next, open).findIndex(row => !("kind" in row) && row.selectable);
|
|
258
291
|
cursorTo(Math.max(firstChoice, 0));
|
|
259
292
|
setMode(nextMode);
|
|
260
293
|
};
|
|
@@ -483,9 +516,9 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
483
516
|
return;
|
|
484
517
|
}
|
|
485
518
|
// export-list and import-list
|
|
486
|
-
const
|
|
487
|
-
const currentRow =
|
|
488
|
-
const next = moveCursor(cursorRef.current,
|
|
519
|
+
const shownRows = listRows(current, rows, expandedGroupsRef.current);
|
|
520
|
+
const currentRow = shownRows[cursorRef.current];
|
|
521
|
+
const next = moveCursor(cursorRef.current, shownRows.length, input, key);
|
|
489
522
|
if (next !== undefined) {
|
|
490
523
|
cursorTo(Math.max(0, next));
|
|
491
524
|
return;
|
|
@@ -503,6 +536,10 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
503
536
|
beginExport();
|
|
504
537
|
return;
|
|
505
538
|
}
|
|
539
|
+
if (key.return && current === "import-list" && nothingToImport) {
|
|
540
|
+
previewImport();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
506
543
|
if (currentRow && "kind" in currentRow && currentRow.kind === "group") {
|
|
507
544
|
if (key.return || key.rightArrow || key.leftArrow) {
|
|
508
545
|
const open = new Set(expandedGroupsRef.current);
|
|
@@ -531,13 +568,13 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
531
568
|
}
|
|
532
569
|
});
|
|
533
570
|
const listing = mode === "export-list" || mode === "import-list";
|
|
534
|
-
const displayRows = mode
|
|
571
|
+
const displayRows = listRows(mode, rows, expandedGroups);
|
|
535
572
|
const rowHeights = displayRows.map(row => {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return wrapText(label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
573
|
+
// A group header is one row, so the sticky header the window repeats costs exactly the row it reserves.
|
|
574
|
+
if ("kind" in row)
|
|
575
|
+
return 1;
|
|
576
|
+
const prefixWidth = row.block === "excluded" ? 2 : stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
|
|
577
|
+
return wrapText(row.label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
541
578
|
});
|
|
542
579
|
const listWindow = physicalListWindow(displayRows, rowHeights, cursor, height);
|
|
543
580
|
const window = listing ? listWindow : computeWindow(lines.length, cursor, height);
|
|
@@ -554,10 +591,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
554
591
|
const open = expandedGroups.has(row.id);
|
|
555
592
|
const prefix = `${marker}${" ".repeat(row.depth * 2)}${open ? "▾" : "▸"} `;
|
|
556
593
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
remainingRows -= label.split("\n").length;
|
|
560
|
-
return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, label] }, row.id);
|
|
594
|
+
remainingRows -= 1;
|
|
595
|
+
return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, clipText(row.label, width)] }, row.id);
|
|
561
596
|
}
|
|
562
597
|
if (row.block === "excluded") {
|
|
563
598
|
const width = Math.max(panelWidth - stringWidth(marker), 1);
|
|
@@ -566,13 +601,15 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
566
601
|
remainingRows -= label.split("\n").length;
|
|
567
602
|
return _jsxs(Text, { color: theme.value, children: [marker, label] }, row.id);
|
|
568
603
|
}
|
|
569
|
-
|
|
570
|
-
const
|
|
604
|
+
// A row the import never changes has no checkbox: unchanged items rest, conflicts warn.
|
|
605
|
+
const box = row.selectable ? `[${selected.some(choice => choice.id === row.id) ? "x" : " "}]` : " ";
|
|
606
|
+
const prefix = `${marker}${box} ${row.ordinal ?? row.index + 1} `;
|
|
571
607
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
572
|
-
const label = fitLines(wrapText(
|
|
608
|
+
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
573
609
|
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
574
610
|
remainingRows -= label.split("\n").length;
|
|
575
|
-
|
|
611
|
+
const color = row.status === "conflict" ? theme.warning : row.status === "same" ? theme.muted : theme.value;
|
|
612
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: prefix }), _jsx(Text, { color: color, children: label })] }, row.id);
|
|
576
613
|
});
|
|
577
614
|
const nameOutcome = resolveExportFileName(text);
|
|
578
615
|
const liveDestination = nameOutcome.kind === "ok"
|
|
@@ -15,6 +15,25 @@ import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
|
15
15
|
import { previewViewport } from "./preview-window.js";
|
|
16
16
|
import { theme } from "./theme.js";
|
|
17
17
|
import { busy } from "./working-model.js";
|
|
18
|
+
/**
|
|
19
|
+
* One group per App name. Invalid recipes, and recipes whose name clashes among those that apply here
|
|
20
|
+
* (they carry an error), stay single rows so nothing is guessed.
|
|
21
|
+
*/
|
|
22
|
+
export function groupRecipeRows(rows) {
|
|
23
|
+
const groups = new Map();
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const key = row.entry.recipe !== undefined && row.entry.error === undefined ? `name:${row.entry.recipe.name}` : `file:${row.entry.file}`;
|
|
26
|
+
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
27
|
+
}
|
|
28
|
+
return [...groups.values()].map(found => {
|
|
29
|
+
const members = [...found.filter(row => row.entry.applicable), ...found.filter(row => !row.entry.applicable)];
|
|
30
|
+
return { primary: members[0], members };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** `windows · approved · <file> (used here)`, one line per recipe of a grouped App. */
|
|
34
|
+
function recipeMemberLine(row) {
|
|
35
|
+
return `${row.entry.recipe?.platform ?? "all platforms"} · ${row.status} · ${row.entry.file}${row.entry.applicable ? " (used here)" : ""}`;
|
|
36
|
+
}
|
|
18
37
|
function recipeApprovalChrome(entry) {
|
|
19
38
|
return {
|
|
20
39
|
title: "Recipe approval · does not install",
|
|
@@ -61,7 +80,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
61
80
|
if (mounted.current)
|
|
62
81
|
setMode({ kind: "browse" });
|
|
63
82
|
};
|
|
64
|
-
const preview = async (entry, status, removeAfterApproval = false) => {
|
|
83
|
+
const preview = async (entry, status, removeAfterApproval = false, members = []) => {
|
|
65
84
|
setError(undefined);
|
|
66
85
|
setBusy("Preparing App recipe preview…");
|
|
67
86
|
try {
|
|
@@ -70,7 +89,10 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
70
89
|
setMode({
|
|
71
90
|
kind: "approve",
|
|
72
91
|
entry, removeAfterApproval, cursor: 0,
|
|
73
|
-
lines:
|
|
92
|
+
lines: [
|
|
93
|
+
...(members.length > 1 ? [`Recipes named ${appName(entry)}:`, ...members.map(row => ` ${recipeMemberLine(row)}`), ""] : []),
|
|
94
|
+
...appRecipePreviewLines(entry, status, steps),
|
|
95
|
+
],
|
|
74
96
|
});
|
|
75
97
|
}
|
|
76
98
|
}
|
|
@@ -198,6 +220,14 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
198
220
|
return true;
|
|
199
221
|
}
|
|
200
222
|
const cancel = () => setMode({ kind: "browse" });
|
|
223
|
+
if (current.kind === "pick") {
|
|
224
|
+
const chosen = /^[1-9]$/u.test(input) ? current.members[Number(input) - 1] : undefined;
|
|
225
|
+
if (chosen)
|
|
226
|
+
void startRemoval(chosen.entry);
|
|
227
|
+
else if (input === "n" || key.escape)
|
|
228
|
+
cancel();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
201
231
|
if (current.kind === "approve") {
|
|
202
232
|
const viewport = previewViewport(current.lines, current.cursor, terminalRows, terminalColumns, recipeApprovalChrome(current.entry));
|
|
203
233
|
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
@@ -265,17 +295,23 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
265
295
|
}, cancel);
|
|
266
296
|
return true;
|
|
267
297
|
}
|
|
268
|
-
const
|
|
269
|
-
if (!
|
|
298
|
+
const group = groupRecipeRows(rows).find(candidate => candidate.primary.entry.file === highlightedFile());
|
|
299
|
+
if (!group)
|
|
270
300
|
return false;
|
|
301
|
+
const row = group.primary;
|
|
271
302
|
if (input === "n")
|
|
272
303
|
startAdd();
|
|
273
304
|
else if (input === "r")
|
|
274
305
|
void reload();
|
|
275
306
|
else if (key.return)
|
|
276
|
-
void preview(row.entry, row.status);
|
|
277
|
-
|
|
278
|
-
|
|
307
|
+
void preview(row.entry, row.status, false, group.members);
|
|
308
|
+
// A grouped App names several files; the chosen one is the only file the removal flow may touch.
|
|
309
|
+
else if (input === "d") {
|
|
310
|
+
if (group.members.length > 1)
|
|
311
|
+
setMode({ kind: "pick", members: group.members });
|
|
312
|
+
else
|
|
313
|
+
void startRemoval(row.entry);
|
|
314
|
+
}
|
|
279
315
|
else
|
|
280
316
|
return false;
|
|
281
317
|
return true;
|
|
@@ -312,7 +348,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
312
348
|
const chrome = recipeApprovalChrome(mode.entry);
|
|
313
349
|
const viewport = previewViewport(mode.lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
314
350
|
return _jsx(PreviewConfirm, { title: chrome.title, lines: mode.lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
315
|
-
})() : 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" ? (() => {
|
|
351
|
+
})() : null, mode.kind === "pick" ? (_jsxs(BorderPanel, { title: "Remove which recipe?", children: [mode.members.map((row, index) => _jsxs(Text, { children: [index + 1, " ", recipeMemberLine(row)] }, row.entry.file)), _jsx(Text, { color: theme.legend, children: "Press a number to review that one file for removal \u00B7 n or Esc cancels" })] })) : 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" ? (() => {
|
|
316
352
|
const chrome = uninstallChrome(mode);
|
|
317
353
|
const lines = appStepLines(mode.plan);
|
|
318
354
|
const viewport = previewViewport(lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
4
5
|
import { initialMode, modeReducer } from "./sources-mode.js";
|
|
5
6
|
import { rowStyle, theme } from "./theme.js";
|
|
6
7
|
import { computeWindow, pageStep, useListHeight } from "./window.js";
|
|
7
8
|
import { useKeys } from "./keys.js";
|
|
8
|
-
import { useSourcesRecipes } from "./sources-recipes.js";
|
|
9
|
+
import { groupRecipeRows, useSourcesRecipes } from "./sources-recipes.js";
|
|
9
10
|
import { BorderPanel, DecisionPrompt, FieldLine, PreviewLines, Working } from "./panel-parts.js";
|
|
10
11
|
import { previewViewport } from "./preview-window.js";
|
|
11
12
|
import { movePreviewCursor } from "./mode-keys.js";
|
|
@@ -123,14 +124,14 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
123
124
|
highlightedFile: () => {
|
|
124
125
|
const rows = rowsRef.current;
|
|
125
126
|
const row = rows[resolveIndex(rows, selectionRef.current)];
|
|
126
|
-
return row?.kind === "app" ? row.
|
|
127
|
+
return row?.kind === "app" ? row.group.primary.entry.file : undefined;
|
|
127
128
|
},
|
|
128
129
|
terminalRows: terminalRows || 24,
|
|
129
130
|
terminalColumns: columns || 80,
|
|
130
131
|
});
|
|
131
132
|
const rows = [
|
|
132
133
|
...sources.map((source) => ({ id: source.id, kind: "skills", source })),
|
|
133
|
-
...recipes.rows.map((
|
|
134
|
+
...groupRecipeRows(recipes.rows).map((group) => ({ id: `recipe:${group.primary.entry.file}`, kind: "app", group })),
|
|
134
135
|
];
|
|
135
136
|
rowsRef.current = rows;
|
|
136
137
|
const selected = resolveIndex(rows, selection);
|
|
@@ -334,13 +335,19 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
334
335
|
return _jsx(Box, { flexDirection: "column", children: recipes.panel });
|
|
335
336
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: [" ", "ID KIND LOCATION / STATUS"] }), builtin ? _jsxs(Text, { children: [" ", _jsx(Text, { color: theme.value, children: builtin.id }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: "skills" }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.warning, children: "included" })] }) : null, rows.length === 0 && recipes.loadingSince === undefined ? _jsx(Text, { children: "No sources or App recipes \u00B7 n to add" }) : null, rows.slice(window.start, window.end).map((row, offset) => {
|
|
336
337
|
const index = window.start + offset;
|
|
337
|
-
const id = row.kind === "skills" ? row.source.id : row.
|
|
338
|
+
const id = row.kind === "skills" ? row.source.id : row.group.primary.entry.recipe?.name ?? row.group.primary.entry.file.split(/[\\/]/u).pop();
|
|
338
339
|
return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", row.kind === "skills" ? (marked.has(row.id) ? "[x] " : "[ ] ") : " ", _jsx(Text, { color: theme.value, children: id }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.kind }), _jsx(Text, { color: theme.legend, children: " " }), row.kind === "skills"
|
|
339
340
|
? _jsx(Text, { color: theme.value, children: row.source.url })
|
|
340
|
-
:
|
|
341
|
+
: _jsx(RecipeStatus, { group: row.group })] }, row.id));
|
|
341
342
|
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.loadingSince !== undefined && mode.kind !== "busy" && !recipes.capturing
|
|
342
343
|
? _jsx(Working, { label: "Loading App recipes\u2026", since: recipes.loadingSince }) : null, recipes.panel, _jsx(Prompt, { mode: mode }), message ? (_jsx(BorderPanel, { title: message.kind === "error" ? "Source action · failed" : "Source action · complete", children: _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) })) : null] }));
|
|
343
344
|
}
|
|
345
|
+
/** Status of the recipe that applies here, led by its platform when it names one; other platforms follow, muted. */
|
|
346
|
+
function RecipeStatus({ group }) {
|
|
347
|
+
const { primary, members } = group;
|
|
348
|
+
const platform = primary.entry.recipe?.platform;
|
|
349
|
+
return (_jsxs(_Fragment, { children: [platform === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: platform }), _jsx(Text, { color: theme.legend, children: " \u00B7 " })] }), primary.entry.error === undefined || primary.entry.recipe === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: path.basename(primary.entry.file) }), _jsx(Text, { color: theme.legend, children: " \u00B7 " })] }), _jsx(Text, { color: primary.status === "approved" ? theme.success : theme.warning, children: primary.status === "approved" ? "✓ approved" : primary.status }), members.slice(1).map(other => (_jsxs(Text, { color: theme.muted, children: [" ", other.entry.recipe?.platform ?? "all platforms", " (other platform)"] }, other.entry.file)))] }));
|
|
350
|
+
}
|
|
344
351
|
function Prompt({ mode, terminalRows = 24, terminalColumns = 80 }) {
|
|
345
352
|
if (mode.kind === "add-kind")
|
|
346
353
|
return _jsx(Text, { children: "Add: 1 Git Source (skills + Packages) \u00B7 2 App recipe (Esc cancel)" });
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { collapseOrParent, expandNode, toggleNode } from "./tree.js";
|
|
2
|
+
import { toggleGroup } from "./updates-tree.js";
|
|
1
3
|
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
2
4
|
import { busy } from "./working-model.js";
|
|
3
5
|
import { pageStep } from "./window.js";
|
|
@@ -18,14 +20,14 @@ function moveCursorKey(context, snapshot, input, key) {
|
|
|
18
20
|
return false;
|
|
19
21
|
return true;
|
|
20
22
|
}
|
|
21
|
-
/** Space toggles the highlighted update, `a` selects them all. */
|
|
23
|
+
/** Space toggles the highlighted update or every update of the highlighted group, `a` selects them all. */
|
|
22
24
|
function selectKey(context, snapshot, input) {
|
|
23
25
|
if (input === " ") {
|
|
24
26
|
const entry = snapshot.entries[snapshot.at];
|
|
25
|
-
if (entry?.kind
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if (entry?.kind === "group")
|
|
28
|
+
context.setChecked(toggleGroup(context.checked(), entry.leafKeys));
|
|
29
|
+
else if (entry?.kind === "update")
|
|
30
|
+
context.setChecked(toggleGroup(context.checked(), [entry.row.key]));
|
|
29
31
|
}
|
|
30
32
|
else if (input === "a")
|
|
31
33
|
context.setChecked(snapshot.updatable.map((row) => row.key));
|
|
@@ -33,21 +35,38 @@ function selectKey(context, snapshot, input) {
|
|
|
33
35
|
return false;
|
|
34
36
|
return true;
|
|
35
37
|
}
|
|
36
|
-
/**
|
|
38
|
+
/** Moves to the group above the highlighted line, or stays when there is none. */
|
|
39
|
+
function selectParent(context, snapshot, parentId) {
|
|
40
|
+
const parent = snapshot.entries.findIndex((entry) => entry.kind !== "summary" && entry.kind !== "unknown" && entry.line.node.id === parentId);
|
|
41
|
+
if (parent >= 0)
|
|
42
|
+
context.setCursor(parent);
|
|
43
|
+
}
|
|
44
|
+
/** Right/left expand and collapse groups and the "cannot be checked" rows; Enter does the same on a line that is not an update. */
|
|
37
45
|
function foldKey(context, snapshot, key) {
|
|
38
46
|
const entry = snapshot.entries[snapshot.at];
|
|
39
47
|
if (key.rightArrow) {
|
|
40
|
-
if (entry?.kind === "
|
|
48
|
+
if (entry?.kind === "group")
|
|
49
|
+
context.setExpanded(expandNode(context.expanded(), entry.line));
|
|
50
|
+
else if (entry?.kind === "summary")
|
|
41
51
|
context.setShowUnknown(true);
|
|
42
52
|
}
|
|
43
53
|
else if (key.leftArrow) {
|
|
44
|
-
if (entry?.kind === "
|
|
45
|
-
context.
|
|
46
|
-
|
|
54
|
+
if (entry?.kind === "group" || entry?.kind === "update") {
|
|
55
|
+
const next = collapseOrParent(context.expanded(), [entry.line], 0);
|
|
56
|
+
context.setExpanded(next.expanded);
|
|
57
|
+
if (next.selectedId !== entry.line.node.id)
|
|
58
|
+
selectParent(context, snapshot, entry.line.parentId);
|
|
59
|
+
}
|
|
60
|
+
else if (entry?.kind === "summary" || entry?.kind === "unknown") {
|
|
61
|
+
if (entry.kind === "unknown")
|
|
62
|
+
context.setCursor(snapshot.entries.findIndex((candidate) => candidate.kind === "summary"));
|
|
47
63
|
context.setShowUnknown(false);
|
|
64
|
+
}
|
|
48
65
|
}
|
|
49
66
|
else if (key.return) {
|
|
50
|
-
if (entry?.kind === "
|
|
67
|
+
if (entry?.kind === "group")
|
|
68
|
+
context.setExpanded(toggleNode(context.expanded(), entry.line));
|
|
69
|
+
else if (entry?.kind === "summary")
|
|
51
70
|
context.setShowUnknown(!context.unknownShown());
|
|
52
71
|
else if (entry?.kind !== "unknown" && snapshot.data)
|
|
53
72
|
context.startPreview();
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { buildUpdateTree } from "./updates-tree.js";
|
|
2
|
+
import { flattenVisible } from "./tree.js";
|
|
1
3
|
export const REFRESHING = "Refreshing sources…";
|
|
2
4
|
export const CHECKING = "Checking for updates…";
|
|
3
5
|
export const PHASE_LABEL = { refreshing: REFRESHING, checking: CHECKING };
|
|
6
|
+
/** A stable React key: tree lines use their node id, the others their own identity. */
|
|
7
|
+
export function entryId(entry) {
|
|
8
|
+
switch (entry.kind) {
|
|
9
|
+
case "summary": return "summary";
|
|
10
|
+
case "unknown": return `unknown:${entry.row.key}`;
|
|
11
|
+
default: return entry.line.node.id;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
/** Names the reasons worth knowing before expanding the collapsed "cannot be checked" row. */
|
|
5
15
|
export function describeUnknownReasons(rows) {
|
|
6
16
|
const newer = rows.filter((row) => /^installed by a newer agent depot/i.test(row.reason)).length;
|
|
@@ -14,14 +24,16 @@ export function describeUnknownReasons(rows) {
|
|
|
14
24
|
return parts.length === 0 ? "" : ` (${parts.join(", ")})`;
|
|
15
25
|
}
|
|
16
26
|
const matchesScope = (row, filter) => filter === "all" || row.scope === filter;
|
|
17
|
-
/** The rows the list shows for a scope filter and fold
|
|
18
|
-
export function viewOf(data, scope, showUnknown) {
|
|
27
|
+
/** The rows the list shows for a scope filter and fold states, and the cursor positions they make up. */
|
|
28
|
+
export function viewOf(data, scope, showUnknown, expanded) {
|
|
19
29
|
const shown = (data?.rows ?? []).filter((row) => matchesScope(row, scope));
|
|
20
30
|
const updatable = shown.filter((row) => row.status === "update available");
|
|
21
31
|
const unknown = shown.filter((row) => row.status === "cannot assess");
|
|
22
32
|
const upToDate = shown.filter((row) => row.status === "up to date").length;
|
|
23
33
|
const entries = [
|
|
24
|
-
...updatable.map((
|
|
34
|
+
...flattenVisible(buildUpdateTree(updatable), expanded).map((line) => line.node.data.kind === "group"
|
|
35
|
+
? { kind: "group", line, label: line.node.data.label, leafKeys: line.node.data.leafKeys }
|
|
36
|
+
: { kind: "update", line, row: line.node.data.row }),
|
|
25
37
|
...(unknown.length > 0 ? [{ kind: "summary" }] : []),
|
|
26
38
|
...(unknown.length > 0 && showUnknown ? unknown.map((row) => ({ kind: "unknown", row })) : []),
|
|
27
39
|
];
|
|
@@ -5,6 +5,7 @@ import { BorderPanel, FieldLine, PreviewConfirm } from "./panel-parts.js";
|
|
|
5
5
|
import { previewViewport } from "./preview-window.js";
|
|
6
6
|
import { describeRefresh, describeUnknownReasons } from "./updates-model.js";
|
|
7
7
|
import { formatAgo, previewLineText } from "./updates.js";
|
|
8
|
+
import { groupCheckState, packageLeafLabel } from "./updates-tree.js";
|
|
8
9
|
import { updateModePreviewChrome, updateModePreviewLines, updateModeQuestion } from "./updates-preview.js";
|
|
9
10
|
function updateResultField(line, defaultOk) {
|
|
10
11
|
if (line.startsWith("Update summary: ")) {
|
|
@@ -26,19 +27,24 @@ export function CheckStatus({ data, now }) {
|
|
|
26
27
|
const errors = data.results.filter((result) => result.error !== undefined);
|
|
27
28
|
return (_jsxs(BorderPanel, { title: "Update check", children: [_jsxs(Text, { color: theme.field, children: ["checked ", _jsx(Text, { color: theme.value, children: formatAgo(now - data.checkedAt) }), _jsx(Text, { color: theme.legend, children: " \u00B7 " }), _jsx(Text, { color: theme.value, children: describeRefresh(data) })] }), data.refresh.failed.map((failure) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: "[WARN] " }), _jsx(FieldLine, { label: "Refresh failed for", value: `${failure.source}: ${failure.reason}` })] }, failure.source))), data.appError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Apps):", value: data.appError })] }) : null, data.rows.filter(row => row.kind === "package" && row.check.status === "current" && row.check.reason !== undefined).map(row => (_jsxs(Text, { color: theme.warning, children: [row.path, ": ", row.reason] }, row.key))), data.packageError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Packages):", value: data.packageError })] }) : null, errors.map((result) => _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: `Error (${result.scope}):`, value: result.error ?? "" })] }, result.scope))] }));
|
|
28
29
|
}
|
|
30
|
+
const BOX = { all: "[x]", some: "[-]", none: "[ ]" };
|
|
29
31
|
/** One line of the list: an update, the fold of unassessable items, or one of those when unfolded. */
|
|
30
32
|
export function EntryLine({ entry, selected, checked, view, showUnknown }) {
|
|
31
33
|
const cursor = selected ? "> " : " ";
|
|
32
34
|
if (entry.kind === "summary") {
|
|
33
35
|
return (_jsxs(Text, { ...rowStyle(selected), color: theme.inactive, children: [cursor, view.unknown.length, " cannot be checked ", showUnknown ? "▾" : "▸", describeUnknownReasons(view.unknown)] }));
|
|
34
36
|
}
|
|
37
|
+
if (entry.kind === "group") {
|
|
38
|
+
const state = groupCheckState(entry.leafKeys, checked);
|
|
39
|
+
return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ".repeat(entry.line.depth), BOX[state], " ", _jsxs(Text, { color: theme.field, children: [entry.line.expanded ? "▾" : "▸", " ", entry.label] })] }));
|
|
40
|
+
}
|
|
35
41
|
const { row } = entry;
|
|
36
42
|
if (entry.kind === "unknown") {
|
|
37
43
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ", _jsx(Text, { color: theme.field, children: row.path }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.scope })] }), _jsxs(Text, { color: theme.muted, children: [" ", row.kind === "package" && /local modifications|local changes/i.test(row.reason)
|
|
38
44
|
? `Update comparison unavailable: ${row.reason}`
|
|
39
45
|
: row.reason] })] }));
|
|
40
46
|
}
|
|
41
|
-
return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, checked ? "
|
|
47
|
+
return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ".repeat(entry.line.depth), BOX[checked.includes(row.key) ? "all" : "none"], " ", _jsx(Text, { color: theme.field, children: packageLeafLabel(row) }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.scope }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.policy }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.value, children: row.installed }), _jsx(Text, { color: theme.legend, children: " -> " }), _jsx(Text, { color: theme.value, children: row.available })] }));
|
|
42
48
|
}
|
|
43
49
|
/** Under the list: the window indicator, what is up to date or selected, and the last result. */
|
|
44
50
|
export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCount, message }) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { bundleRoot } from "../package-model.js";
|
|
2
|
+
import { groupUpdates } from "../update-groups.js";
|
|
3
|
+
import { sourceLabel } from "./installations.js";
|
|
4
|
+
function grouped(row) {
|
|
5
|
+
if (row.kind === "app")
|
|
6
|
+
return { kind: "app", row };
|
|
7
|
+
const source = row.kind === "skill" ? row.item.selection.source : row.check.installed.selection.source;
|
|
8
|
+
const label = sourceLabel(source);
|
|
9
|
+
return { kind: row.kind, source: { id: label, label }, row };
|
|
10
|
+
}
|
|
11
|
+
// Node ids stay stable across reloads, and keys and Source labels hold paths and URLs, so they are encoded.
|
|
12
|
+
const leaf = (row) => ({ id: `update:${encodeURIComponent(row.key)}`, data: { kind: "update", row } });
|
|
13
|
+
function group(id, name, leaves, children = leaves) {
|
|
14
|
+
const leafKeys = leaves.flatMap((node) => (node.data.kind === "update" ? [node.data.row.key] : node.data.leafKeys));
|
|
15
|
+
return { id, data: { kind: "group", label: `${name} (${leafKeys.length})`, leafKeys }, children };
|
|
16
|
+
}
|
|
17
|
+
/** Source groups with Packages and Skills below them, then Apps; built from the updatable rows only. */
|
|
18
|
+
export function buildUpdateTree(rows) {
|
|
19
|
+
const { sources, apps } = groupUpdates(rows.map(grouped));
|
|
20
|
+
const nodes = sources.map(({ source, subgroups }) => {
|
|
21
|
+
const id = `source:${encodeURIComponent(source.id)}`;
|
|
22
|
+
const children = subgroups.map(({ kind, items }) => group(`${id}:${kind}`, kind === "packages" ? "Packages" : "Skills", items.map((item) => leaf(item.row))));
|
|
23
|
+
return group(id, source.label, children);
|
|
24
|
+
});
|
|
25
|
+
if (apps.length > 0)
|
|
26
|
+
nodes.push(group("apps", "Apps", apps.map((item) => leaf(item.row))));
|
|
27
|
+
return nodes;
|
|
28
|
+
}
|
|
29
|
+
/** Whether all, some or none of a group's rows are selected. */
|
|
30
|
+
export function groupCheckState(leafKeys, checked) {
|
|
31
|
+
const count = leafKeys.filter((key) => checked.includes(key)).length;
|
|
32
|
+
if (count === 0)
|
|
33
|
+
return "none";
|
|
34
|
+
return count === leafKeys.length ? "all" : "some";
|
|
35
|
+
}
|
|
36
|
+
/** Space on a group: selects all its rows unless they all are, then unselects them; other rows are untouched. */
|
|
37
|
+
export function toggleGroup(checked, leafKeys) {
|
|
38
|
+
if (groupCheckState(leafKeys, checked) === "all")
|
|
39
|
+
return checked.filter((key) => !leafKeys.includes(key));
|
|
40
|
+
return [...checked, ...leafKeys.filter((key) => !checked.includes(key))];
|
|
41
|
+
}
|
|
42
|
+
/** A Package row inside its Source group: the bundle name, plus its path only when it is not the Source root. */
|
|
43
|
+
export function packageLeafLabel(row) {
|
|
44
|
+
if (row.kind !== "package")
|
|
45
|
+
return row.path;
|
|
46
|
+
const selection = row.check.installed.selection;
|
|
47
|
+
const root = bundleRoot(selection);
|
|
48
|
+
// A Pi bundle has no plugin name, so its directory names it; at the Source root only the host is left.
|
|
49
|
+
const name = selection.host === "claude" ? selection.pluginName : root.split("/").pop() || "pi package";
|
|
50
|
+
return `Package: ${name}${root === "" ? "" : ` ${root}`}`;
|
|
51
|
+
}
|
|
@@ -5,10 +5,11 @@ import { theme } from "./theme.js";
|
|
|
5
5
|
import { previewViewport } from "./preview-window.js";
|
|
6
6
|
import { computeWindow, useListHeight } from "./window.js";
|
|
7
7
|
import { BROWSE } from "./updates-mode.js";
|
|
8
|
+
import { defaultExpanded } from "./tree.js";
|
|
8
9
|
import { useMirrored } from "./view-state.js";
|
|
9
10
|
import { useUpdatesFlow } from "./updates-flow.js";
|
|
10
11
|
import { handleUpdatesKey } from "./updates-keys.js";
|
|
11
|
-
import { viewOf } from "./updates-model.js";
|
|
12
|
+
import { entryId, viewOf } from "./updates-model.js";
|
|
12
13
|
import { CheckStatus, EntryLine, ModePanel, UpdatesFooter } from "./updates-panel.js";
|
|
13
14
|
import { previewLineText } from "./updates.js";
|
|
14
15
|
import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
|
|
@@ -26,6 +27,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
26
27
|
const [scope, scopeRef, setScope] = useMirrored("all");
|
|
27
28
|
const [cursor, cursorRef, setCursor] = useMirrored(0);
|
|
28
29
|
const [showUnknown, showUnknownRef, setShowUnknown] = useMirrored(false);
|
|
30
|
+
const [expanded, expandedRef, setExpanded] = useMirrored(defaultExpanded());
|
|
29
31
|
const [checked, checkedRef, setChecked] = useMirrored([]);
|
|
30
32
|
const [mode, setModeState] = useState(BROWSE);
|
|
31
33
|
// Mirrors the mode synchronously so later keystrokes and the shell never see stale state.
|
|
@@ -52,7 +54,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
52
54
|
const dataRef = useRef(undefined);
|
|
53
55
|
/** The list, the cursor and the fold as of the latest keystroke. */
|
|
54
56
|
const latest = () => {
|
|
55
|
-
const view = viewOf(dataRef.current, scopeRef.current, showUnknownRef.current);
|
|
57
|
+
const view = viewOf(dataRef.current, scopeRef.current, showUnknownRef.current, expandedRef.current);
|
|
56
58
|
return { ...view, data: dataRef.current, at: Math.min(cursorRef.current, Math.max(view.entries.length - 1, 0)) };
|
|
57
59
|
};
|
|
58
60
|
const changeScope = (next) => {
|
|
@@ -77,7 +79,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
77
79
|
}, [load]);
|
|
78
80
|
const data = state.status === "ready" ? state.data : undefined;
|
|
79
81
|
dataRef.current = data;
|
|
80
|
-
const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown);
|
|
82
|
+
const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown, expanded);
|
|
81
83
|
const index = Math.min(cursor, Math.max(entries.length - 1, 0));
|
|
82
84
|
const previewGeometry = (current) => {
|
|
83
85
|
const cursor = "cursor" in current ? current.cursor : 0;
|
|
@@ -95,6 +97,8 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
95
97
|
checked: () => checkedRef.current,
|
|
96
98
|
setChecked,
|
|
97
99
|
setCursor,
|
|
100
|
+
expanded: () => expandedRef.current,
|
|
101
|
+
setExpanded,
|
|
98
102
|
unknownShown: () => showUnknownRef.current,
|
|
99
103
|
setShowUnknown,
|
|
100
104
|
changeScope,
|
|
@@ -115,5 +119,5 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
115
119
|
const showingBrowse = mode.kind === "browse";
|
|
116
120
|
// One indicator for the whole operation: a check's own phase label, else the busy mode's.
|
|
117
121
|
const working = state.status === "loading" ? state : mode.kind === "busy" ? mode : undefined;
|
|
118
|
-
return (_jsxs(Box, { flexDirection: "column", children: [showingBrowse ? _jsxs(Text, { color: theme.title, bold: true, children: ["Updates (scope: ", scope, ") t all \u00B7 p project \u00B7 g user-global \u00B7 u update"] }) : null, working ? _jsx(BorderPanel, { title: state.status === "loading" ? "Update check" : "Updates", children: _jsx(Working, { label: working.label, since: working.startedAt, now: clock }) }) : null, showingBrowse && data ? _jsx(CheckStatus, { data: data, now: clock() }) : null, showingBrowse && data && !hasErrors && shown.length === 0 ? _jsx(Text, { children: "No installations" }) : null, showingBrowse ? entries.slice(window.start, window.end).map((entry, offset) => (_jsx(EntryLine, { entry: entry, selected: window.start + offset === index, checked:
|
|
122
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showingBrowse ? _jsxs(Text, { color: theme.title, bold: true, children: ["Updates (scope: ", scope, ") t all \u00B7 p project \u00B7 g user-global \u00B7 u update"] }) : null, working ? _jsx(BorderPanel, { title: state.status === "loading" ? "Update check" : "Updates", children: _jsx(Working, { label: working.label, since: working.startedAt, now: clock }) }) : null, showingBrowse && data ? _jsx(CheckStatus, { data: data, now: clock() }) : null, showingBrowse && data && !hasErrors && shown.length === 0 ? _jsx(Text, { children: "No installations" }) : null, showingBrowse ? entries.slice(window.start, window.end).map((entry, offset) => (_jsx(EntryLine, { entry: entry, selected: window.start + offset === index, checked: checked, view: view, showUnknown: showUnknown }, entryId(entry)))) : null, showingBrowse ? _jsx(UpdatesFooter, { indicator: window.indicator, upToDate: upToDate, updatableCount: updatable.length, checkedCount: checked.length, message: message }) : null, _jsx(ModePanel, { mode: mode })] }));
|
|
119
123
|
}
|