@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +25 -3
  3. package/dist/src/app-flow.js +3 -3
  4. package/dist/src/app-recipe-removal.js +3 -1
  5. package/dist/src/atomic-file.js +24 -1
  6. package/dist/src/cli.js +49 -15
  7. package/dist/src/package-cli.js +1 -18
  8. package/dist/src/package-flow.js +8 -8
  9. package/dist/src/package-model.js +51 -0
  10. package/dist/src/path-safety.js +4 -1
  11. package/dist/src/profile-export.js +4 -4
  12. package/dist/src/profile-import.js +94 -18
  13. package/dist/src/project-installation.js +2 -1
  14. package/dist/src/project-manifest.js +7 -0
  15. package/dist/src/project-target.js +58 -0
  16. package/dist/src/skill-discovery.js +86 -6
  17. package/dist/src/skill-install.js +20 -8
  18. package/dist/src/tui/app-actions.js +18 -7
  19. package/dist/src/tui/app.js +20 -5
  20. package/dist/src/tui/batch.js +2 -1
  21. package/dist/src/tui/bulk-actions.js +21 -8
  22. package/dist/src/tui/catalog-installs.js +3 -2
  23. package/dist/src/tui/catalog-panel.js +3 -2
  24. package/dist/src/tui/catalog-state.js +47 -21
  25. package/dist/src/tui/catalog-tree.js +47 -10
  26. package/dist/src/tui/catalog-view.js +11 -5
  27. package/dist/src/tui/installations-view.js +19 -6
  28. package/dist/src/tui/installations.js +1 -1
  29. package/dist/src/tui/manage-panel.js +1 -1
  30. package/dist/src/tui/package-actions.js +2 -2
  31. package/dist/src/tui/package-rows.js +4 -4
  32. package/dist/src/tui/profile-view.js +68 -31
  33. package/dist/src/tui/sources-recipes.js +44 -8
  34. package/dist/src/tui/sources-view.js +12 -5
  35. package/dist/src/tui/updates-keys.js +30 -11
  36. package/dist/src/tui/updates-model.js +15 -3
  37. package/dist/src/tui/updates-panel.js +7 -1
  38. package/dist/src/tui/updates-tree.js +51 -0
  39. package/dist/src/tui/updates-view.js +8 -4
  40. package/dist/src/tui/updates.js +4 -3
  41. package/dist/src/tui/view-retention.js +17 -0
  42. package/dist/src/update-flow.js +3 -1
  43. package/dist/src/update-groups.js +29 -0
  44. 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: `${item.status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`,
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 === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows)[cursorRef.current];
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
- : "j/k move · space toggle · a all · Enter preview · Esc back to menu";
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 === "export-list"
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 visibleRows = current === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows;
487
- const currentRow = visibleRows[cursorRef.current];
488
- const next = moveCursor(cursorRef.current, visibleRows.length, input, key);
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 === "export-list" ? exportDisplayRows(rows, expandedGroups) : rows;
571
+ const displayRows = listRows(mode, rows, expandedGroups);
535
572
  const rowHeights = displayRows.map(row => {
536
- const prefixWidth = "kind" in row ? stringWidth(`> ${" ".repeat(row.depth * 2)}▸ `) : row.block === "excluded" ? 2
537
- : stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
538
- const label = !("kind" in row) && row.block !== "excluded" && !row.selectable
539
- ? `${row.label} (not selectable)` : row.label;
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
- const label = fitLines(wrapText(row.label, width), remainingRows, width)
558
- .join(`\n${" ".repeat(stringWidth(prefix))}`);
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
- const prefix = `${marker}[${selected.some(choice => choice.id === row.id) ? "x" : " "}] ${row.ordinal ?? row.index + 1} `;
570
- const value = `${row.label}${row.selectable ? "" : " (not selectable)"}`;
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(value, width), remainingRows, width)
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
- return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: prefix }), _jsx(Text, { color: theme.value, children: label })] }, row.id);
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: appRecipePreviewLines(entry, status, steps),
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 row = rows.find(row => row.entry.file === highlightedFile());
269
- if (!row)
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
- else if (input === "d")
278
- void startRemoval(row.entry);
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.recipe.entry.file : undefined;
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((recipe) => ({ id: `recipe:${recipe.entry.file}`, kind: "app", recipe })),
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.recipe.entry.recipe?.name ?? row.recipe.entry.file.split(/[\\/]/u).pop();
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
- : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: row.recipe.entry.file }), _jsx(Text, { color: theme.legend, children: " \u00B7 " }), _jsx(Text, { color: row.recipe.status === "approved" ? theme.success : theme.warning, children: row.recipe.status })] })] }, row.id));
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 !== "update")
26
- return true;
27
- const checked = context.checked();
28
- context.setChecked(checked.includes(entry.row.key) ? checked.filter((id) => id !== entry.row.key) : [...checked, entry.row.key]);
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
- /** Right/left unfold and fold the "cannot be checked" rows; Enter toggles the fold or previews the selection. */
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 === "summary")
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 === "unknown")
45
- context.setCursor(snapshot.updatable.length);
46
- if (entry?.kind === "summary" || entry?.kind === "unknown")
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 === "summary")
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 state, and the cursor positions they make up. */
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((row) => ({ kind: "update", row })),
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 ? "[x]" : "[ ]", " ", _jsx(Text, { color: theme.field, children: row.path }), _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 })] }));
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: entry.kind === "update" && checked.includes(entry.row.key), view: view, showUnknown: showUnknown }, entry.kind === "summary" ? "summary" : entry.row.key))) : null, showingBrowse ? _jsx(UpdatesFooter, { indicator: window.indicator, upToDate: upToDate, updatableCount: updatable.length, checkedCount: checked.length, message: message }) : null, _jsx(ModePanel, { mode: mode })] }));
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
  }