@iseoane/agent-depot 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.es.md +120 -53
  3. package/README.md +101 -41
  4. package/dist/src/app-flow.js +56 -24
  5. package/dist/src/cli.js +55 -0
  6. package/dist/src/package-cli.js +2 -1
  7. package/dist/src/package-flow.js +1 -1
  8. package/dist/src/package-host-state.js +10 -6
  9. package/dist/src/profile-export.js +72 -9
  10. package/dist/src/profile-import.js +3 -2
  11. package/dist/src/profile.js +71 -7
  12. package/dist/src/self-update.js +141 -0
  13. package/dist/src/tui/app-actions.js +82 -33
  14. package/dist/src/tui/app.js +19 -14
  15. package/dist/src/tui/catalog-browse.js +6 -1
  16. package/dist/src/tui/catalog-panel.js +11 -11
  17. package/dist/src/tui/catalog-state.js +28 -0
  18. package/dist/src/tui/catalog-tree.js +22 -5
  19. package/dist/src/tui/catalog-view.js +59 -16
  20. package/dist/src/tui/header-layout.js +33 -0
  21. package/dist/src/tui/installations-panel.js +3 -3
  22. package/dist/src/tui/installations-rows.js +20 -5
  23. package/dist/src/tui/installations-tree.js +4 -7
  24. package/dist/src/tui/installations-view.js +15 -6
  25. package/dist/src/tui/installations.js +9 -7
  26. package/dist/src/tui/manage-panel.js +7 -6
  27. package/dist/src/tui/mode-keys.js +9 -0
  28. package/dist/src/tui/package-rows.js +21 -7
  29. package/dist/src/tui/panel-parts.js +38 -6
  30. package/dist/src/tui/preview-window.js +48 -0
  31. package/dist/src/tui/profile-view.js +261 -42
  32. package/dist/src/tui/sources-mode.js +3 -1
  33. package/dist/src/tui/sources-recipes.js +50 -19
  34. package/dist/src/tui/sources-view.js +50 -14
  35. package/dist/src/tui/theme.js +14 -7
  36. package/dist/src/tui/updates-flow.js +2 -2
  37. package/dist/src/tui/updates-keys.js +34 -5
  38. package/dist/src/tui/updates-model.js +2 -0
  39. package/dist/src/tui/updates-panel.js +40 -15
  40. package/dist/src/tui/updates-preview.js +65 -0
  41. package/dist/src/tui/updates-view.js +17 -2
  42. package/dist/src/tui/updates.js +14 -5
  43. package/dist/src/tui/window.js +6 -4
  44. package/package.json +35 -14
  45. package/resources/pi/agents-panel/contract.ts +62 -0
  46. package/resources/pi/agents-panel/index.ts +471 -0
  47. package/skills/doctor-md-agents/SKILL.md +2 -1
  48. package/skills/doctor-md-agents/scripts/scan_corrections.py +141 -13
  49. package/skills/doctor-md-skill/references/supported-harnesses.md +2 -0
  50. package/skills/doctor-md-skill/scripts/collect_sessions.py +26 -2
@@ -1,11 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text, useWindowSize } from "ink";
3
3
  import { useCallback, useEffect, useReducer, useRef, useState } from "react";
4
4
  import { initialMode, modeReducer } from "./sources-mode.js";
5
5
  import { rowStyle, theme } from "./theme.js";
6
6
  import { computeWindow, pageStep, useListHeight } from "./window.js";
7
7
  import { useKeys } from "./keys.js";
8
8
  import { useSourcesRecipes } from "./sources-recipes.js";
9
+ import { BorderPanel, DecisionPrompt, FieldLine, PreviewLines } from "./panel-parts.js";
10
+ import { previewViewport } from "./preview-window.js";
11
+ import { movePreviewCursor } from "./mode-keys.js";
9
12
  /** Footer keys for the selected row; App seeds the footer before the view reports a selection. */
10
13
  export function sourceRowHints(row) {
11
14
  if (row === undefined)
@@ -37,7 +40,18 @@ async function refreshEach(operations, targets) {
37
40
  function errorText(error) {
38
41
  return error instanceof Error ? error.message : String(error);
39
42
  }
43
+ function refreshPreviewLines(sources) {
44
+ return sources.map(source => `Source: refresh Git Source ${source.id} from ${source.url}`);
45
+ }
46
+ function refreshViewport(mode, rows, columns) {
47
+ return previewViewport(refreshPreviewLines(mode.sources), mode.cursor, rows, columns, {
48
+ title: "Refresh Git Sources",
49
+ questions: ["Scroll to end; n cancels.", "Refresh these Sources? y/n"],
50
+ footerRows: 2,
51
+ });
52
+ }
40
53
  export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment, onHintsChange }) {
54
+ const { columns, rows: terminalRows } = useWindowSize();
41
55
  const height = useListHeight(listHeight, 9);
42
56
  const [state, setState] = useState({ status: "loading" });
43
57
  // The highlight follows the source id; the index is only the fallback once that source is gone.
@@ -111,6 +125,8 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
111
125
  const row = rows[resolveIndex(rows, selectionRef.current)];
112
126
  return row?.kind === "app" ? row.recipe.entry.file : undefined;
113
127
  },
128
+ terminalRows: terminalRows || 24,
129
+ terminalColumns: columns || 80,
114
130
  });
115
131
  const rows = [
116
132
  ...sources.map((source) => ({ id: source.id, kind: "skills", source })),
@@ -119,7 +135,9 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
119
135
  rowsRef.current = rows;
120
136
  const selected = resolveIndex(rows, selection);
121
137
  const selectedRow = rows[selected];
122
- const hints = sourceRowHints(selectedRow);
138
+ const hints = mode.kind === "confirm" && mode.action === "refresh"
139
+ ? "↑↓/Pg page · y refresh · n cancel"
140
+ : sourceRowHints(selectedRow);
123
141
  useEffect(() => { onHintsChange?.(hints); }, [hints, onHintsChange]);
124
142
  const select = (index) => {
125
143
  setMessage(undefined);
@@ -221,6 +239,16 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
221
239
  return;
222
240
  }
223
241
  if (mode.kind === "confirm") {
242
+ if (mode.action === "refresh") {
243
+ const viewport = refreshViewport(mode, terminalRows || 24, columns || 80);
244
+ const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
245
+ if (cursor !== undefined) {
246
+ dispatch({ type: "preview-cursor", cursor });
247
+ return;
248
+ }
249
+ if (input === "y" && !viewport.reachedEnd)
250
+ return;
251
+ }
224
252
  if (input === "y") {
225
253
  if (mode.action === "refresh") {
226
254
  const { sources: targets } = mode;
@@ -296,28 +324,36 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
296
324
  // Sources removed since they were marked no longer count.
297
325
  const selectedCount = sources.filter((source) => marked.has(source.id)).length;
298
326
  if (state.status === "loading")
299
- return _jsx(Text, { children: "Loading sources..." });
327
+ return _jsx(BorderPanel, { title: "Sources", children: _jsx(FieldLine, { label: "Status", value: "Loading sources..." }) });
300
328
  if (state.status === "error")
301
- return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
329
+ return _jsx(BorderPanel, { title: "Source error", children: _jsx(Text, { color: theme.error, children: state.message }) });
302
330
  const window = computeWindow(rows.length, selected, height);
303
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: [" ", "ID KIND LOCATION / STATUS"] }), builtin ? _jsxs(Text, { color: theme.muted, children: [" ", builtin.id, " skills included"] }) : null, rows.length === 0 ? _jsx(Text, { children: "No sources or App recipes \u00B7 n to add" }) : null, rows.slice(window.start, window.end).map((row, offset) => {
331
+ if (mode.kind === "confirm")
332
+ return _jsx(Prompt, { mode: mode, terminalRows: terminalRows || 24, terminalColumns: columns || 80 });
333
+ if (recipes.capturing)
334
+ return _jsx(Box, { flexDirection: "column", children: recipes.panel });
335
+ 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 ? _jsx(Text, { children: "No sources or App recipes \u00B7 n to add" }) : null, rows.slice(window.start, window.end).map((row, offset) => {
304
336
  const index = window.start + offset;
305
337
  const id = row.kind === "skills" ? row.source.id : row.recipe.entry.recipe?.name ?? row.recipe.entry.file.split(/[\\/]/u).pop();
306
- return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", row.kind === "skills" ? (marked.has(row.id) ? "[x] " : "[ ] ") : " ", id, " ", row.kind, " ", row.kind === "skills" ? row.source.url : `${row.recipe.entry.file} · ${row.recipe.status}`] }, row.id));
307
- }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.panel, _jsx(Prompt, { mode: mode }), message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
338
+ 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
+ ? _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
+ }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : 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] }));
308
342
  }
309
- function Prompt({ mode }) {
343
+ function Prompt({ mode, terminalRows = 24, terminalColumns = 80 }) {
310
344
  if (mode.kind === "add-kind")
311
345
  return _jsx(Text, { children: "Add: 1 Git Source (skills + Packages) \u00B7 2 App recipe (Esc cancel)" });
312
346
  if (mode.kind === "input")
313
347
  return _jsxs(Text, { children: ["URL: ", mode.value] });
314
348
  if (mode.kind === "busy")
315
- return _jsx(Text, { children: "Working..." });
349
+ return _jsx(BorderPanel, { title: "Sources", children: _jsx(FieldLine, { label: "Status", value: "Working..." }) });
316
350
  if (mode.kind === "confirm") {
317
- const lines = mode.action === "refresh"
318
- ? mode.sources.map((source) => `refresh Git Source ${source.id} from ${source.url}`)
319
- : [`remove Git Source ${mode.source.id} from ${mode.source.url}`];
320
- return _jsxs(Text, { children: [lines.join("\n"), "\n", "[y/n]"] });
351
+ return mode.action === "refresh" ? (() => {
352
+ const viewport = refreshViewport(mode, terminalRows, terminalColumns);
353
+ return (_jsxs(BorderPanel, { title: "Refresh Git Sources", children: [_jsx(PreviewLines, { lines: viewport.rows }), viewport.total > viewport.height ? _jsxs(Text, { color: theme.legend, children: [viewport.start + 1, "\u2013", viewport.end, " of ", viewport.total] }) : null, _jsxs(Text, { color: theme.legend, children: ["Review ", viewport.cursor + 1, " of ", viewport.total, " \u00B7 arrows/Pg page"] }), viewport.reachedEnd
354
+ ? _jsx(DecisionPrompt, { children: "Refresh these Sources?" })
355
+ : _jsx(Text, { color: theme.legend, children: "Scroll to end; n cancels." })] }));
356
+ })() : (_jsxs(BorderPanel, { title: "Remove Git Source", children: [_jsx(FieldLine, { label: "Source", value: `remove Git Source ${mode.source.id} from ${mode.source.url}` }), _jsx(DecisionPrompt, { children: "Remove this Source?" })] }));
321
357
  }
322
358
  return null;
323
359
  }
@@ -1,19 +1,26 @@
1
1
  /** Central xeo palette for the TUI; views take every colour from here. */
2
2
  export const theme = {
3
- /** Title and active tab. */
3
+ /** Panel titles and active labels use Xeo accent pink. */
4
+ title: "#ff2e9a",
4
5
  accent: "#ff2e9a",
5
- /** Group nodes and selection text. */
6
+ /** Borders and group nodes. */
7
+ border: "#7aa2f7",
6
8
  group: "#7aa2f7",
7
- /** Markers such as hosts and versions. */
8
- marker: "#449dab",
9
+ /** Field names and markers such as hosts. */
10
+ field: "#7aa2f7",
11
+ marker: "#7aa2f7",
12
+ /** Primary and emphasized values. */
13
+ text: "#a9b1d6",
14
+ value: "#c0caf5",
15
+ version: "#ff9e64",
9
16
  success: "#9ece6a",
10
17
  warning: "#e0af68",
11
18
  error: "#f7768e",
12
- /** Hints and footer. */
19
+ /** Legends, hints and footer. */
20
+ legend: "#565f89",
13
21
  muted: "#414868",
14
- /** Inactive tabs. */
22
+ /** Inactive tabs and selected-row background. */
15
23
  inactive: "#565f89",
16
- /** Background of the selected row. */
17
24
  selection: "#292e42",
18
25
  };
19
26
  /** Columns assumed when the terminal width is unknown. */
@@ -49,7 +49,7 @@ export function useUpdatesFlow(inputs) {
49
49
  try {
50
50
  const prepared = await prepareUpdates(current, selected);
51
51
  if (mounted.current)
52
- setMode({ kind: "preview", prepared });
52
+ setMode({ kind: "preview", prepared, cursor: 0 });
53
53
  }
54
54
  catch (error) {
55
55
  if (!mounted.current)
@@ -72,7 +72,7 @@ export function useUpdatesFlow(inputs) {
72
72
  resolve(done);
73
73
  };
74
74
  pendingAnswer.current = answer;
75
- setMode({ kind: "manual", plan, reason, answer });
75
+ setMode({ kind: "manual", plan, reason, answer, cursor: 0 });
76
76
  }));
77
77
  result = { kind: outcome.failed === 0 ? "ok" : "error", lines: outcome.lines };
78
78
  }
@@ -1,4 +1,4 @@
1
- import { answerYesNo } from "./mode-keys.js";
1
+ import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
2
2
  import { pageStep } from "./window.js";
3
3
  const SCOPE_KEYS = new Map([["t", "all"], ["p", "project"], ["g", "user-global"]]);
4
4
  /** Moves the cursor with the arrows, j/k and paging; returns whether the key was one of them. */
@@ -61,18 +61,28 @@ function handleBrowseKey(context, input, key) {
61
61
  context.changeScope(scope);
62
62
  else if (input === "r")
63
63
  context.recheck();
64
+ else if (input === "u")
65
+ context.startPreview();
64
66
  else
65
67
  foldKey(context, snapshot, key);
66
68
  }
67
69
  function handlePreviewKey(context, mode, input, key) {
70
+ const metrics = context.previewMetrics(mode);
71
+ const cursor = movePreviewCursor(mode.cursor, metrics.length, input, key, metrics.pageSize);
72
+ if (cursor !== undefined) {
73
+ context.setMode({ ...mode, cursor });
74
+ return;
75
+ }
68
76
  if (mode.prepared.applicable.length === 0) {
69
77
  if (key.escape || key.return || input === "n")
70
78
  context.cancel();
71
79
  return;
72
80
  }
73
81
  answerYesNo(input, key, () => {
82
+ if (!context.previewAtEnd(mode))
83
+ return;
74
84
  if (mode.prepared.nonCanonicalPaths.length > 0)
75
- context.setMode({ ...mode, kind: "confirm-paths" });
85
+ context.setMode({ ...mode, kind: "confirm-paths", cursor: 0 });
76
86
  else
77
87
  context.apply(mode.prepared, []);
78
88
  }, context.cancel);
@@ -80,8 +90,16 @@ function handlePreviewKey(context, mode, input, key) {
80
90
  /** Handles a key in the current Updates mode; `busy` ignores keys. */
81
91
  export function handleUpdatesKey(context, mode, input, key) {
82
92
  switch (mode.kind) {
83
- case "manual":
93
+ case "manual": {
94
+ const metrics = context.previewMetrics(mode);
95
+ const cursor = movePreviewCursor(mode.cursor, metrics.length, input, key, metrics.pageSize);
96
+ if (cursor !== undefined) {
97
+ context.setMode({ ...mode, cursor });
98
+ return;
99
+ }
84
100
  answerYesNo(input, key, () => {
101
+ if (!context.previewAtEnd(mode))
102
+ return;
85
103
  context.setMode({ kind: "busy", label: "Checking App version..." });
86
104
  mode.answer(true);
87
105
  }, () => {
@@ -89,15 +107,26 @@ export function handleUpdatesKey(context, mode, input, key) {
89
107
  mode.answer(false);
90
108
  });
91
109
  return;
110
+ }
92
111
  case "browse":
93
112
  handleBrowseKey(context, input, key);
94
113
  return;
95
114
  case "preview":
96
115
  handlePreviewKey(context, mode, input, key);
97
116
  return;
98
- case "confirm-paths":
99
- answerYesNo(input, key, () => context.apply(mode.prepared, mode.prepared.nonCanonicalPaths), context.cancel);
117
+ case "confirm-paths": {
118
+ const metrics = context.previewMetrics(mode);
119
+ const cursor = movePreviewCursor(mode.cursor, metrics.length, input, key, metrics.pageSize);
120
+ if (cursor !== undefined) {
121
+ context.setMode({ ...mode, cursor });
122
+ return;
123
+ }
124
+ answerYesNo(input, key, () => {
125
+ if (context.previewAtEnd(mode))
126
+ context.apply(mode.prepared, mode.prepared.nonCanonicalPaths);
127
+ }, context.cancel);
100
128
  return;
129
+ }
101
130
  default:
102
131
  return;
103
132
  }
@@ -5,9 +5,11 @@ export const PHASE_LABEL = { refreshing: REFRESHING, checking: CHECKING };
5
5
  export function describeUnknownReasons(rows) {
6
6
  const newer = rows.filter((row) => /^installed by a newer agent depot/i.test(row.reason)).length;
7
7
  const pinned = rows.filter((row) => /^pinned to agent depot/i.test(row.reason)).length;
8
+ const localChanges = rows.filter((row) => row.kind === "package" && /local modifications|local changes/i.test(row.reason)).length;
8
9
  const parts = [
9
10
  ...(newer > 0 ? [`${newer} installed by a newer Agent Depot`] : []),
10
11
  ...(pinned > 0 ? [`${pinned} pinned to another Agent Depot`] : []),
12
+ ...(localChanges > 0 ? [`${localChanges} Package update comparison unavailable because the checkout has local changes`] : []),
11
13
  ];
12
14
  return parts.length === 0 ? "" : ` (${parts.join(", ")})`;
13
15
  }
@@ -1,13 +1,30 @@
1
- import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text, useWindowSize } from "ink";
3
3
  import { rowStyle, theme } from "./theme.js";
4
+ import { BorderPanel, FieldLine, PreviewConfirm } from "./panel-parts.js";
5
+ import { previewViewport } from "./preview-window.js";
4
6
  import { CHECKING, describeRefresh, describeUnknownReasons, REFRESHING } from "./updates-model.js";
5
- import { formatAgo } from "./updates.js";
6
- import { PreviewLines } from "./panel-parts.js";
7
+ import { formatAgo, previewLineText } from "./updates.js";
8
+ import { updateModePreviewChrome, updateModePreviewLines, updateModeQuestion } from "./updates-preview.js";
9
+ function updateResultField(line, defaultOk) {
10
+ if (line.startsWith("Update summary: ")) {
11
+ const successful = /^Update summary: \d+ updated, 0 failed$/.test(line);
12
+ return { ok: successful, label: "Update summary:", value: line.slice("Update summary: ".length) };
13
+ }
14
+ if (line.startsWith("Updated "))
15
+ return { ok: true, label: "Updated", value: line.slice("Updated ".length) };
16
+ if (line.startsWith("Failed "))
17
+ return { ok: false, label: "Failed", value: line.slice("Failed ".length) };
18
+ if (line.startsWith("Cancelled "))
19
+ return { ok: false, label: "Cancelled", value: line.slice("Cancelled ".length) };
20
+ if (line === "Update cancelled")
21
+ return { ok: true, label: undefined, value: line };
22
+ return { ok: defaultOk, label: undefined, value: line };
23
+ }
7
24
  /** What the last check found out: its age, how the sources were refreshed, and what failed. */
8
25
  export function CheckStatus({ data, now }) {
9
26
  const errors = data.results.filter((result) => result.error !== undefined);
10
- return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["checked ", formatAgo(now - data.checkedAt), " \u00B7 ", describeRefresh(data)] }), data.refresh.failed.map((failure) => (_jsxs(Text, { color: theme.warning, children: ["refresh failed for ", failure.source, ": ", failure.reason] }, failure.source))), data.appError ? _jsxs(Text, { color: theme.error, children: ["Error (Apps): ", data.appError] }) : null, data.packageError ? _jsxs(Text, { color: theme.error, children: ["Error (Packages): ", data.packageError] }) : null, errors.map((result) => _jsxs(Text, { color: theme.error, children: ["Error (", result.scope, "): ", result.error] }, result.scope))] }));
27
+ 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.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))] }));
11
28
  }
12
29
  /** One line of the list: an update, the fold of unassessable items, or one of those when unfolded. */
13
30
  export function EntryLine({ entry, selected, checked, view, showUnknown }) {
@@ -17,28 +34,36 @@ export function EntryLine({ entry, selected, checked, view, showUnknown }) {
17
34
  }
18
35
  const { row } = entry;
19
36
  if (entry.kind === "unknown") {
20
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ", row.path, " ", row.scope] }), _jsxs(Text, { color: theme.muted, children: [" ", row.reason] })] }));
37
+ 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
+ ? `Update comparison unavailable: ${row.reason}`
39
+ : row.reason] })] }));
21
40
  }
22
- return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, checked ? "[x]" : "[ ]", " ", row.path, " ", row.scope, " ", row.policy, " ", row.installed, " ", "->", " ", row.available] }));
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 })] }));
23
42
  }
24
43
  /** Under the list: the window indicator, what is up to date or selected, and the last result. */
25
44
  export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCount, message }) {
26
- return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, upToDate > 0 ? _jsxs(Text, { color: theme.muted, children: [upToDate, " up to date"] }) : null, updatableCount > 0 ? _jsxs(Text, { color: theme.muted, children: [checkedCount, " selected"] }) : null, message ? (_jsx(Box, { flexDirection: "column", children: message.lines.map((line, position) => _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: line }, position)) })) : null] }));
45
+ return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, upToDate > 0 ? _jsxs(Text, { color: theme.muted, children: [upToDate, " up to date"] }) : null, updatableCount > 0 ? _jsxs(Text, { color: theme.muted, children: [checkedCount, " selected"] }) : null, message ? (_jsx(BorderPanel, { title: "Update result", children: message.lines.map((line, position) => {
46
+ const result = updateResultField(line, message.kind === "ok");
47
+ return _jsxs(Text, { children: [_jsxs(Text, { color: result.ok ? theme.success : theme.error, children: ["[", result.ok ? "OK" : "FAIL", "] "] }), result.label === undefined ? _jsx(Text, { color: theme.value, children: result.value }) : _jsx(FieldLine, { label: result.label, value: result.value })] }, position);
48
+ }) })) : null] }));
49
+ }
50
+ function CapturedUpdatePreview({ mode }) {
51
+ const { rows, columns } = useWindowSize();
52
+ const lines = updateModePreviewLines(mode).map(previewLineText);
53
+ const chrome = updateModePreviewChrome(mode);
54
+ const viewport = previewViewport(lines, mode.cursor, rows || 24, columns || 80, chrome);
55
+ return (_jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: updateModeQuestion(mode, viewport.reachedEnd), chrome: chrome }));
27
56
  }
28
- /** The prompt of the preview and confirmation modes; browsing and the check itself show nothing. */
29
57
  export function ModePanel({ mode }) {
30
58
  switch (mode.kind) {
31
59
  case "manual":
32
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["App: ", mode.plan.entry.recipe.name] }), _jsxs(Text, { children: ["Environment: ", mode.plan.environment] }), _jsxs(Text, { children: ["Working directory: ", mode.plan.cwd] }), _jsxs(Text, { children: ["Manual: ", mode.plan.manual] }), mode.reason ? _jsx(Text, { color: theme.warning, children: mode.reason }) : null, _jsx(Text, { children: "Manual step done, check now? y/n" })] }));
60
+ return _jsx(CapturedUpdatePreview, { mode: mode });
33
61
  case "browse":
34
62
  return null;
35
63
  case "busy":
36
- return mode.label === REFRESHING || mode.label === CHECKING ? null : _jsx(Text, { children: mode.label });
64
+ return mode.label === REFRESHING || mode.label === CHECKING ? null : (_jsx(BorderPanel, { title: "Updates", children: _jsx(FieldLine, { label: "Status", value: mode.label }) }));
37
65
  case "preview":
38
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.prepared.lines }), _jsx(PreviewLines, { lines: mode.prepared.appFailures }), _jsx(PreviewLines, { lines: mode.prepared.packageFailures }), mode.prepared.failures.map(({ item, message }) => (_jsxs(Text, { color: theme.error, children: ["Preview failed for ", JSON.stringify(item.id), ": ", message] }, item.id))), mode.prepared.applicable.length === 0
39
- ? _jsx(Text, { children: "Nothing can be applied. Enter/Esc to go back" })
40
- : _jsxs(Text, { children: ["Apply ", mode.prepared.applicable.length, " ", mode.prepared.applicable.length === 1 ? "update" : "updates", "? y/n"] })] }));
41
66
  case "confirm-paths":
42
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.warning, children: "Confirm replacing non-canonical locations (outside .agents/skills and .claude/skills):" }), mode.prepared.nonCanonicalPaths.map((location, position) => _jsxs(Text, { color: theme.warning, children: [" [", location.scope, "] ", JSON.stringify(location.path)] }, position)), _jsx(Text, { children: "Replace exactly these paths? y/n" })] }));
67
+ return _jsx(CapturedUpdatePreview, { mode: mode });
43
68
  }
44
69
  }
@@ -0,0 +1,65 @@
1
+ import { previewText } from "./updates.js";
2
+ export function updateModePreviewLines(mode) {
3
+ switch (mode.kind) {
4
+ case "preview":
5
+ return [
6
+ ...mode.prepared.lines,
7
+ ...mode.prepared.appFailures.map(previewText),
8
+ ...mode.prepared.packageFailures.map(previewText),
9
+ ...mode.prepared.failures.map(({ item, message }) => previewText(`Preview failed for ${JSON.stringify(item.id)}: ${message}`)),
10
+ ];
11
+ case "manual":
12
+ return [
13
+ { kind: "field", label: "App:", value: mode.plan.entry.recipe?.name ?? mode.plan.entry.file },
14
+ { kind: "field", label: "Environment:", value: mode.plan.environment },
15
+ { kind: "field", label: "Working directory:", value: mode.plan.cwd },
16
+ { kind: "field", label: "Manual:", value: mode.plan.manual ?? "" },
17
+ ...(mode.reason === undefined ? [] : [previewText(mode.reason)]),
18
+ ];
19
+ case "confirm-paths":
20
+ return [
21
+ previewText("Outside .agents/skills and .claude/skills:"),
22
+ ...mode.prepared.nonCanonicalPaths.map(({ scope, path }) => ({
23
+ kind: "field",
24
+ label: "Path:",
25
+ value: `[${scope}] ${JSON.stringify(path)}`,
26
+ })),
27
+ ];
28
+ default:
29
+ return [];
30
+ }
31
+ }
32
+ export function updateModePreviewChrome(mode) {
33
+ switch (mode.kind) {
34
+ case "preview": {
35
+ const count = mode.prepared.applicable.length;
36
+ const question = `Apply ${count} ${count === 1 ? "update" : "updates"}? y/n`;
37
+ return {
38
+ title: "Update batch preview",
39
+ questions: ["Scroll to end before confirming; n cancels.", question],
40
+ footerRows: 2,
41
+ };
42
+ }
43
+ case "manual":
44
+ return {
45
+ title: "Manual App update",
46
+ questions: ["Scroll to end before confirming; n cancels.", "Manual step done, check now? y/n"],
47
+ footerRows: 2,
48
+ };
49
+ case "confirm-paths":
50
+ return {
51
+ title: "Confirm replacing non-canonical locations",
52
+ questions: ["Scroll to end before confirming; n cancels.", "Replace exactly these paths? y/n"],
53
+ footerRows: 2,
54
+ };
55
+ default:
56
+ return { title: "Updates", questions: ["Continue? y/n"] };
57
+ }
58
+ }
59
+ export function updateModeQuestion(mode, reachedEnd) {
60
+ if (mode.kind === "preview" && mode.prepared.applicable.length === 0) {
61
+ return "Nothing can be applied. Enter or Esc to go back.";
62
+ }
63
+ const chrome = updateModePreviewChrome(mode);
64
+ return reachedEnd ? chrome.questions.at(-1) ?? "Continue? y/n" : chrome.questions[0] ?? "Scroll to end before confirming; n cancels.";
65
+ }
@@ -1,7 +1,8 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
2
+ import { Box, Text, useWindowSize } from "ink";
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { theme } from "./theme.js";
5
+ import { previewViewport } from "./preview-window.js";
5
6
  import { computeWindow, useListHeight } from "./window.js";
6
7
  import { BROWSE } from "./updates-mode.js";
7
8
  import { useMirrored } from "./view-state.js";
@@ -9,12 +10,16 @@ import { useUpdatesFlow } from "./updates-flow.js";
9
10
  import { handleUpdatesKey } from "./updates-keys.js";
10
11
  import { viewOf } from "./updates-model.js";
11
12
  import { CheckStatus, EntryLine, ModePanel, UpdatesFooter } from "./updates-panel.js";
13
+ import { previewLineText } from "./updates.js";
14
+ import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
12
15
  import { useKeys } from "./keys.js";
16
+ import { BorderPanel, FieldLine } from "./panel-parts.js";
13
17
  const NO_ENVIRONMENT = {};
14
18
  /** How often the age of the check is redrawn. */
15
19
  const CLOCK_TICK_MS = 10_000;
16
20
  export function UpdatesView({ operations, environment, onCapturingChange, listHeight, now }) {
17
21
  const height = useListHeight(listHeight, 9);
22
+ const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
18
23
  const env = environment ?? NO_ENVIRONMENT;
19
24
  const clock = now ?? Date.now;
20
25
  // Scope, cursor, fold and selection are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
@@ -74,8 +79,17 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
74
79
  dataRef.current = data;
75
80
  const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown);
76
81
  const index = Math.min(cursor, Math.max(entries.length - 1, 0));
82
+ const previewGeometry = (current) => {
83
+ const cursor = "cursor" in current ? current.cursor : 0;
84
+ return previewViewport(updateModePreviewLines(current).map(previewLineText), cursor, terminalRows || 24, terminalColumns || 80, updateModePreviewChrome(current));
85
+ };
77
86
  const keyContext = {
78
87
  height,
88
+ previewMetrics: current => {
89
+ const geometry = previewGeometry(current);
90
+ return { length: geometry.total, pageSize: geometry.height };
91
+ },
92
+ previewAtEnd: current => previewGeometry(current).reachedEnd,
79
93
  latest,
80
94
  checked: () => checkedRef.current,
81
95
  setChecked,
@@ -97,5 +111,6 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
97
111
  const window = computeWindow(entries.length, index, height);
98
112
  const view = { shown, updatable, unknown, upToDate, entries };
99
113
  const hasErrors = data?.appError !== undefined || (data?.results ?? []).some((result) => result.error !== undefined);
100
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.accent, bold: true, children: ["Updates (scope: ", scope, ") t all \u00B7 p project \u00B7 g user-global"] }), state.status === "loading" ? _jsx(Text, { children: state.label }) : null, data ? _jsx(CheckStatus, { data: data, now: clock() }) : null, data && !hasErrors && shown.length === 0 ? _jsx(Text, { children: "No installations" }) : null, 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))), _jsx(UpdatesFooter, { indicator: window.indicator, upToDate: upToDate, updatableCount: updatable.length, checkedCount: checked.length, message: message }), _jsx(ModePanel, { mode: mode })] }));
114
+ const showingBrowse = mode.kind === "browse";
115
+ 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, state.status === "loading" ? _jsx(BorderPanel, { title: "Update check", children: _jsx(FieldLine, { label: "Status", value: state.label }) }) : 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 })] }));
101
116
  }
@@ -111,7 +111,7 @@ export async function loadUpdateRows(operations, environment, options) {
111
111
  catch (error) {
112
112
  appError = errorText(error);
113
113
  }
114
- for (const check of appChecks) {
114
+ for (const check of appChecks.filter(candidate => candidate.inspection.status === "installed")) {
115
115
  rows.push({
116
116
  kind: "app",
117
117
  key: `app:${check.entry.file}`,
@@ -161,6 +161,12 @@ export function formatAgo(elapsedMs) {
161
161
  return `${minutes} min ago`;
162
162
  return `${Math.floor(minutes / 60)} h ago`;
163
163
  }
164
+ export function previewText(text) {
165
+ return { kind: "text", text };
166
+ }
167
+ export function previewLineText(line) {
168
+ return line.kind === "field" ? `${line.label} ${line.value}` : line.text;
169
+ }
164
170
  /** Plan step: previews the selection from the assessed snapshots, scope by scope. Changes nothing. */
165
171
  export async function prepareUpdates(data, selected) {
166
172
  const groups = [];
@@ -176,7 +182,7 @@ export async function prepareUpdates(data, selected) {
176
182
  continue;
177
183
  const { context } = loaded;
178
184
  const outcome = await previewUpdateCandidates(items, context);
179
- lines.push(...outcome.plans.flatMap((plan) => describeUpdatePreview(plan, scope, context.projectRoot, "prompt")));
185
+ lines.push(...outcome.plans.flatMap((plan) => describeUpdatePreview(plan, scope, context.projectRoot, "prompt").map(previewText)));
180
186
  failures.push(...outcome.failures);
181
187
  groups.push({
182
188
  scope,
@@ -192,7 +198,10 @@ export async function prepareUpdates(data, selected) {
192
198
  try {
193
199
  const plan = await data.apps.planLifecycle(row.check.entry, "update", undefined, row.check);
194
200
  appPlans.push(plan);
195
- lines.push(`Environment: ${plan.environment}`, row.path, `Version: ${plan.previousVersion} -> ${plan.latestVersion ?? "unknown"}`, ...(plan.argv ? [`argv: ${JSON.stringify(plan.argv)}`, `Executable: ${plan.executable ?? `${JSON.stringify(plan.argv[0])} not found in PATH`}`] : []), ...(plan.manual ? [`Manual: ${plan.manual}`] : []), `Working directory: ${plan.cwd}`, ...(plan.warning ? [plan.warning] : []));
201
+ lines.push({ kind: "field", label: "Environment:", value: plan.environment }, previewText(row.path), { kind: "field", label: "Version:", value: `${plan.previousVersion} -> ${plan.latestVersion ?? "unknown"}` }, ...(plan.argv ? [
202
+ { kind: "field", label: "argv:", value: JSON.stringify(plan.argv) },
203
+ { kind: "field", label: "Executable:", value: plan.executable ?? `${JSON.stringify(plan.argv[0])} not found in PATH` },
204
+ ] : []), ...(plan.manual ? [{ kind: "field", label: "Manual:", value: plan.manual }] : []), { kind: "field", label: "Working directory:", value: plan.cwd }, ...(plan.warning ? [previewText(plan.warning)] : []));
196
205
  }
197
206
  catch (error) {
198
207
  appFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
@@ -205,9 +214,9 @@ export async function prepareUpdates(data, selected) {
205
214
  const plan = await data.packages.planLifecycle(row.check.installed.selection, "update");
206
215
  const approval = await data.packages.approvalStatus(row.check.installed.selection, "update");
207
216
  packagePlans.push({ plan, approval });
208
- lines.push(...packagePlanLines(plan, approval));
217
+ lines.push(...packagePlanLines(plan, approval).map(previewText));
209
218
  if (approval === "needs approval")
210
- lines.push("This apply approves the declaration above.");
219
+ lines.push(previewText("This apply approves the declaration above."));
211
220
  }
212
221
  catch (error) {
213
222
  packageFailures.push(`Failed ${row.path}: preview failed: ${errorText(error)}`);
@@ -1,8 +1,9 @@
1
1
  import { useWindowSize } from "ink";
2
+ import { headerTabRowCount } from "./header-layout.js";
2
3
  /** Rows assumed when the terminal size is unknown (for example under test renderers). */
3
4
  export const FALLBACK_TERMINAL_ROWS = 24;
4
- /** Lines the shell itself uses: title, tab bar, separator and footer hint. */
5
- export const APP_CHROME_ROWS = 4;
5
+ /** Header borders, title, one tab row and footer; extra tab rows are added per terminal width. */
6
+ export const APP_CHROME_ROWS = 5;
6
7
  const MINIMUM_ROWS = 3;
7
8
  /** The slice of a list that fits in `height` rows while keeping `selected` visible (roughly centred). */
8
9
  export function computeWindow(total, selected, height) {
@@ -23,8 +24,9 @@ export function pageStep(height) {
23
24
  * view's own `reserved` lines. `override` fixes the height for tests.
24
25
  */
25
26
  export function useListHeight(override, reserved) {
26
- const { rows } = useWindowSize();
27
+ const { rows, columns } = useWindowSize();
27
28
  if (override !== undefined)
28
29
  return Math.max(override, 1);
29
- return Math.max((rows || FALLBACK_TERMINAL_ROWS) - APP_CHROME_ROWS - reserved, MINIMUM_ROWS);
30
+ const chromeRows = APP_CHROME_ROWS + headerTabRowCount(Math.max(columns - 4, 1)) - 1;
31
+ return Math.max((rows || FALLBACK_TERMINAL_ROWS) - chromeRows - reserved, MINIMUM_ROWS);
30
32
  }