@iseoane/agent-depot 0.6.13 → 0.6.15
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 +61 -0
- package/README.md +3 -1
- package/dist/src/app-flow.js +8 -1
- package/dist/src/atomic-file.js +29 -3
- package/dist/src/cli.js +2 -0
- package/dist/src/flow-progress.js +9 -0
- package/dist/src/git-source.js +257 -15
- package/dist/src/preview-model.js +37 -0
- package/dist/src/process-runner.js +1 -0
- package/dist/src/profile-cli.js +12 -4
- package/dist/src/profile-export.js +23 -3
- package/dist/src/skill-update.js +1 -0
- package/dist/src/source-refresh-session.js +139 -0
- package/dist/src/sources.js +4 -4
- package/dist/src/tui/adoption-actions.js +21 -2
- package/dist/src/tui/app-actions.js +57 -47
- package/dist/src/tui/app.js +22 -4
- package/dist/src/tui/bulk-actions.js +45 -11
- package/dist/src/tui/catalog-actions.js +12 -0
- package/dist/src/tui/catalog-flow.js +31 -11
- package/dist/src/tui/catalog-panel.js +15 -13
- package/dist/src/tui/catalog-tree.js +7 -2
- package/dist/src/tui/catalog-view.js +12 -6
- package/dist/src/tui/file-picker.js +2 -1
- package/dist/src/tui/installations-browse.js +1 -1
- package/dist/src/tui/installations-panel.js +12 -4
- package/dist/src/tui/installations-rows.js +2 -2
- package/dist/src/tui/installations-tree.js +1 -1
- package/dist/src/tui/installations-view.js +16 -7
- package/dist/src/tui/keys.js +35 -4
- package/dist/src/tui/manage-actions.js +55 -27
- package/dist/src/tui/manage-panel.js +15 -29
- package/dist/src/tui/mode-keys.js +11 -1
- package/dist/src/tui/package-actions.js +25 -18
- package/dist/src/tui/package-rows.js +9 -4
- package/dist/src/tui/panel-parts.js +54 -37
- package/dist/src/tui/preview-render.js +93 -0
- package/dist/src/tui/preview-window.js +185 -20
- package/dist/src/tui/profile-view.js +54 -44
- package/dist/src/tui/render.js +13 -1
- package/dist/src/tui/safe-text.js +25 -0
- package/dist/src/tui/sources-mode.js +2 -2
- package/dist/src/tui/sources-recipes.js +53 -32
- package/dist/src/tui/sources-view.js +85 -39
- package/dist/src/tui/text-wrap.js +47 -3
- package/dist/src/tui/unmanaged-actions.js +17 -5
- package/dist/src/tui/updates-flow.js +11 -5
- package/dist/src/tui/updates-keys.js +2 -2
- package/dist/src/tui/updates-model.js +5 -2
- package/dist/src/tui/updates-panel.js +10 -9
- package/dist/src/tui/updates-preview.js +27 -44
- package/dist/src/tui/updates-view.js +15 -11
- package/dist/src/tui/updates.js +117 -33
- package/dist/src/update-flow.js +31 -15
- package/package.json +1 -1
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box,
|
|
2
|
+
import { Box, useWindowSize } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
5
|
+
import { useCallback, useEffect, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
5
6
|
import { initialMode, modeReducer } from "./sources-mode.js";
|
|
6
7
|
import { rowStyle, theme } from "./theme.js";
|
|
7
8
|
import { computeWindow, pageStep, useListHeight } from "./window.js";
|
|
8
|
-
import { useKeys } from "./keys.js";
|
|
9
|
+
import { isYes, useKeys } from "./keys.js";
|
|
9
10
|
import { groupRecipeRows, useSourcesRecipes } from "./sources-recipes.js";
|
|
10
|
-
import { BorderPanel,
|
|
11
|
-
import {
|
|
11
|
+
import { BorderPanel, PagedPreview, Working } from "./panel-parts.js";
|
|
12
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
12
13
|
import { movePreviewCursor } from "./mode-keys.js";
|
|
14
|
+
import { animationEnabled, formatElapsed } from "./working-model.js";
|
|
13
15
|
/** Footer keys for the selected row; App seeds the footer before the view reports a selection. */
|
|
14
16
|
export function sourceRowHints(row) {
|
|
15
17
|
if (row === undefined)
|
|
@@ -29,7 +31,7 @@ async function refreshEach(operations, targets) {
|
|
|
29
31
|
let failed = false;
|
|
30
32
|
for (const source of targets) {
|
|
31
33
|
try {
|
|
32
|
-
lines.push(`Refreshed Git Source: ${(await operations.refreshSource(source.id)).id}`);
|
|
34
|
+
lines.push(`Refreshed Git Source: ${(await operations.refreshSource(source.id, { force: true })).id}`);
|
|
33
35
|
}
|
|
34
36
|
catch (error) {
|
|
35
37
|
failed = true;
|
|
@@ -41,19 +43,67 @@ async function refreshEach(operations, targets) {
|
|
|
41
43
|
function errorText(error) {
|
|
42
44
|
return error instanceof Error ? error.message : String(error);
|
|
43
45
|
}
|
|
46
|
+
const NO_REFRESHES = new Map();
|
|
47
|
+
const NO_SUBSCRIPTION = () => () => undefined;
|
|
48
|
+
/** Re-renders on every session change and, when animation is on, every second so elapsed times move. */
|
|
49
|
+
function useRefreshStates(session) {
|
|
50
|
+
const states = useSyncExternalStore(session?.subscribe ?? NO_SUBSCRIPTION, session?.snapshot ?? (() => NO_REFRESHES));
|
|
51
|
+
const [, setTick] = useState(0);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (session === undefined || !animationEnabled())
|
|
54
|
+
return undefined;
|
|
55
|
+
const timer = setInterval(() => setTick((value) => value + 1), 1000);
|
|
56
|
+
// A redraw timer alone must never keep the process alive.
|
|
57
|
+
timer.unref();
|
|
58
|
+
return () => clearInterval(timer);
|
|
59
|
+
}, [session]);
|
|
60
|
+
return states;
|
|
61
|
+
}
|
|
62
|
+
/** The mark is text first; color only supports it. */
|
|
63
|
+
function RefreshMark({ state, now }) {
|
|
64
|
+
switch (state?.status) {
|
|
65
|
+
case undefined:
|
|
66
|
+
case "not-refreshed":
|
|
67
|
+
return _jsx(Text, { color: theme.muted, children: "\u00B7 not refreshed" });
|
|
68
|
+
case "refreshing":
|
|
69
|
+
return _jsxs(Text, { color: theme.warning, children: ["\u2026 refreshing ", formatElapsed(now - (state.startedAt ?? now))] });
|
|
70
|
+
case "refreshed":
|
|
71
|
+
return _jsx(Text, { color: theme.success, children: "\u2713 refreshed" });
|
|
72
|
+
case "failed":
|
|
73
|
+
return _jsx(Text, { color: theme.error, children: "\u2717 refresh failed" });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
44
76
|
function refreshPreviewLines(sources) {
|
|
45
77
|
return sources.map(source => `Source: refresh Git Source ${source.id} from ${source.url}`);
|
|
46
78
|
}
|
|
47
|
-
function
|
|
48
|
-
return
|
|
79
|
+
function refreshChrome(count) {
|
|
80
|
+
return {
|
|
49
81
|
title: "Refresh Git Sources",
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
82
|
+
action: `Refresh ${count} ${count === 1 ? "Source" : "Sources"}`,
|
|
83
|
+
consequence: "Refresh these Sources? y/n",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function removeSourcePreviewLines(source) {
|
|
87
|
+
return [`Source: remove Git Source ${source.id} from ${source.url}`];
|
|
53
88
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
89
|
+
const REMOVE_SOURCE_CHROME = {
|
|
90
|
+
title: "Remove Git Source",
|
|
91
|
+
action: "Remove Git Source",
|
|
92
|
+
consequence: "Remove this Source? y/n",
|
|
93
|
+
};
|
|
94
|
+
/** The lines and chrome of the open confirmation preview, shared by key handling and rendering. */
|
|
95
|
+
function confirmPreview(mode) {
|
|
96
|
+
return mode.action === "refresh"
|
|
97
|
+
? { lines: refreshPreviewLines(mode.sources), chrome: refreshChrome(mode.sources.length) }
|
|
98
|
+
: { lines: removeSourcePreviewLines(mode.source), chrome: REMOVE_SOURCE_CHROME };
|
|
99
|
+
}
|
|
100
|
+
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment, onHintsChange, refreshSession }) {
|
|
101
|
+
const refreshStates = useRefreshStates(refreshSession);
|
|
102
|
+
const { rows: terminalRows } = useWindowSize();
|
|
103
|
+
const previewGeometry = usePreviewGeometry();
|
|
104
|
+
// With a refresh session, one more row is kept for the selected Source's refresh error; below 24 rows
|
|
105
|
+
// the list is already at its minimum, so that line is left out rather than pushing the footer away.
|
|
106
|
+
const height = useListHeight(listHeight, refreshSession === undefined ? 9 : 10);
|
|
57
107
|
const [state, setState] = useState(() => ({ status: "loading", startedAt: Date.now() }));
|
|
58
108
|
// The highlight follows the source id; the index is only the fallback once that source is gone.
|
|
59
109
|
// Selection and marks are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
@@ -126,8 +176,6 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
126
176
|
const row = rows[resolveIndex(rows, selectionRef.current)];
|
|
127
177
|
return row?.kind === "app" ? row.group.primary.entry.file : undefined;
|
|
128
178
|
},
|
|
129
|
-
terminalRows: terminalRows || 24,
|
|
130
|
-
terminalColumns: columns || 80,
|
|
131
179
|
});
|
|
132
180
|
const rows = [
|
|
133
181
|
...sources.map((source) => ({ id: source.id, kind: "skills", source })),
|
|
@@ -136,8 +184,8 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
136
184
|
rowsRef.current = rows;
|
|
137
185
|
const selected = resolveIndex(rows, selection);
|
|
138
186
|
const selectedRow = rows[selected];
|
|
139
|
-
const hints = mode.kind === "confirm"
|
|
140
|
-
?
|
|
187
|
+
const hints = mode.kind === "confirm"
|
|
188
|
+
? `↑↓/Pg page · y ${mode.action} · n cancel`
|
|
141
189
|
: sourceRowHints(selectedRow);
|
|
142
190
|
useEffect(() => { onHintsChange?.(hints); }, [hints, onHintsChange]);
|
|
143
191
|
const select = (index) => {
|
|
@@ -240,17 +288,17 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
240
288
|
return;
|
|
241
289
|
}
|
|
242
290
|
if (mode.kind === "confirm") {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
if (input === "y" && !viewport.reachedEnd)
|
|
251
|
-
return;
|
|
291
|
+
const { lines, chrome } = confirmPreview(mode);
|
|
292
|
+
const viewport = previewGeometry(lines, mode.cursor, chrome);
|
|
293
|
+
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
294
|
+
if (cursor !== undefined) {
|
|
295
|
+
dispatch({ type: "preview-cursor", cursor });
|
|
296
|
+
return;
|
|
252
297
|
}
|
|
253
|
-
|
|
298
|
+
const yes = isYes(input, key);
|
|
299
|
+
if (yes && !viewport.reachedEnd)
|
|
300
|
+
return;
|
|
301
|
+
if (yes) {
|
|
254
302
|
if (mode.action === "refresh") {
|
|
255
303
|
const { sources: targets } = mode;
|
|
256
304
|
void run(targets.length === 1 ? "Refreshing Git Source…" : "Refreshing Git Sources…", async () => {
|
|
@@ -329,17 +377,19 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
329
377
|
if (state.status === "error")
|
|
330
378
|
return _jsx(BorderPanel, { title: "Source error", children: _jsx(Text, { color: theme.error, children: state.message }) });
|
|
331
379
|
const window = computeWindow(rows.length, selected, height);
|
|
380
|
+
const now = refreshSession?.now() ?? 0;
|
|
381
|
+
const selectedRefresh = selectedRow?.kind === "skills" ? refreshStates.get(selectedRow.source.id) : undefined;
|
|
332
382
|
if (mode.kind === "confirm")
|
|
333
|
-
return _jsx(Prompt, { mode: mode,
|
|
383
|
+
return _jsx(Prompt, { mode: mode, previewGeometry: previewGeometry });
|
|
334
384
|
if (recipes.capturing)
|
|
335
385
|
return _jsx(Box, { flexDirection: "column", children: recipes.panel });
|
|
336
386
|
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) => {
|
|
337
387
|
const index = window.start + offset;
|
|
338
388
|
const id = row.kind === "skills" ? row.source.id : row.group.primary.entry.recipe?.name ?? row.group.primary.entry.file.split(/[\\/]/u).pop();
|
|
339
389
|
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"
|
|
340
|
-
? _jsx(Text, { color: theme.value, children: row.source.url })
|
|
390
|
+
? _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: row.source.url }), refreshSession === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(RefreshMark, { state: refreshStates.get(row.source.id), now: now })] })] })
|
|
341
391
|
: _jsx(RecipeStatus, { group: row.group })] }, row.id));
|
|
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
|
|
392
|
+
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedRefresh?.status === "failed" && (terminalRows || 24) >= 24 ? _jsxs(Text, { color: theme.error, wrap: "truncate-end", children: ["Refresh failed: ", selectedRefresh.error] }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.loadingSince !== undefined && mode.kind !== "busy" && !recipes.capturing
|
|
343
393
|
? _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] }));
|
|
344
394
|
}
|
|
345
395
|
/** Status of the recipe that applies here, led by its platform when it names one; other platforms follow, muted. */
|
|
@@ -348,20 +398,16 @@ function RecipeStatus({ group }) {
|
|
|
348
398
|
const platform = primary.entry.recipe?.platform;
|
|
349
399
|
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
400
|
}
|
|
351
|
-
function Prompt({ mode,
|
|
401
|
+
function Prompt({ mode, previewGeometry }) {
|
|
352
402
|
if (mode.kind === "add-kind")
|
|
353
403
|
return _jsx(Text, { children: "Add: 1 Git Source (skills + Packages) \u00B7 2 App recipe (Esc cancel)" });
|
|
354
404
|
if (mode.kind === "input")
|
|
355
405
|
return _jsxs(Text, { children: ["URL: ", mode.value] });
|
|
356
406
|
if (mode.kind === "busy")
|
|
357
407
|
return _jsx(BorderPanel, { title: "Sources", children: _jsx(Working, { label: mode.label, since: mode.startedAt }) });
|
|
358
|
-
if (mode.kind === "confirm") {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
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
|
|
362
|
-
? _jsx(DecisionPrompt, { children: "Refresh these Sources?" })
|
|
363
|
-
: _jsx(Text, { color: theme.legend, children: "Scroll to end; n cancels." })] }));
|
|
364
|
-
})() : (_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?" })] }));
|
|
408
|
+
if (mode.kind === "confirm" && previewGeometry !== undefined) {
|
|
409
|
+
const { lines, chrome } = confirmPreview(mode);
|
|
410
|
+
return _jsx(PagedPreview, { lines: lines, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
365
411
|
}
|
|
366
412
|
return null;
|
|
367
413
|
}
|
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
import stringWidth from "string-width";
|
|
2
2
|
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
3
|
+
// Control, format and line-separator characters the TUI prints as escapes; the zero-width joiner stays for emoji sequences.
|
|
4
|
+
export const UNSAFE_PATTERN = "(?!\\u200D)[\\p{Cc}\\p{Cf}\\u2028\\u2029]";
|
|
5
|
+
const UNSAFE_CHARACTER = new RegExp(UNSAFE_PATTERN, "u");
|
|
6
|
+
/** Text shaped like an escape the TUI prints (`x1b`, `u202e`, `u{1f600}`), ignoring case. */
|
|
7
|
+
export const ESCAPE_TEXT = "x[0-9a-f]{2}|u[0-9a-f]{4}|u\\{[0-9a-f]{1,6}\\}";
|
|
8
|
+
const ESCAPE_UNIT = new RegExp(`(\\\\+)(${ESCAPE_TEXT})`, "giu");
|
|
9
|
+
/** Whether `escape` (`x1b`, `u202e`, `u{1f600}`) names a character the TUI would print as an escape. */
|
|
10
|
+
export function namesEscapedCharacter(escape) {
|
|
11
|
+
const code = Number.parseInt(escape.replace(/^[xu]\{?|\}$/giu, ""), 16);
|
|
12
|
+
return code <= 0x10ffff && UNSAFE_CHARACTER.test(String.fromCodePoint(code));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The units text is cut at: graphemes, except around an escape written as text that names a character the TUI escapes. Cut inside,
|
|
16
|
+
* its doubled backslash would leave a `\x1b` that reads as a real ESC. The backslash run splits into pairs from the left, so a cut
|
|
17
|
+
* between pairs keeps its parity, and the last one or two backslashes stay with the escape text: a unit is at most 12 columns. A unit
|
|
18
|
+
* wider than `maxWidth` falls back to graphemes. `index` is the UTF-16 offset of the unit.
|
|
19
|
+
*/
|
|
20
|
+
export function textUnits(value, maxWidth = Infinity) {
|
|
21
|
+
const units = [];
|
|
22
|
+
let from = 0;
|
|
23
|
+
const addGraphemes = (text, offset) => {
|
|
24
|
+
for (const { segment, index } of graphemes.segment(text))
|
|
25
|
+
units.push({ segment, index: offset + index });
|
|
26
|
+
};
|
|
27
|
+
for (const match of value.matchAll(ESCAPE_UNIT)) {
|
|
28
|
+
const [whole, backslashes = "", escape = ""] = match;
|
|
29
|
+
if (!namesEscapedCharacter(escape))
|
|
30
|
+
continue;
|
|
31
|
+
const tail = backslashes.length % 2 === 1 ? 1 : 2;
|
|
32
|
+
const pairs = backslashes.length - tail;
|
|
33
|
+
const unit = backslashes.slice(pairs) + escape;
|
|
34
|
+
addGraphemes(value.slice(from, match.index), from);
|
|
35
|
+
if (stringWidth(unit) > maxWidth)
|
|
36
|
+
addGraphemes(whole, match.index);
|
|
37
|
+
else {
|
|
38
|
+
for (let offset = 0; offset < pairs; offset += 2)
|
|
39
|
+
units.push({ segment: "\\\\", index: match.index + offset });
|
|
40
|
+
units.push({ segment: unit, index: match.index + pairs });
|
|
41
|
+
}
|
|
42
|
+
from = match.index + whole.length;
|
|
43
|
+
}
|
|
44
|
+
addGraphemes(value.slice(from), from);
|
|
45
|
+
return units;
|
|
46
|
+
}
|
|
3
47
|
/** Split text on grapheme boundaries so wide and combining characters wrap at `width` columns. */
|
|
4
48
|
export function wrapText(value, width) {
|
|
5
49
|
const lines = [];
|
|
6
50
|
let line = "";
|
|
7
51
|
let lineWidth = 0;
|
|
8
|
-
for (const { segment } of
|
|
52
|
+
for (const { segment } of textUnits(value, width)) {
|
|
9
53
|
const segmentWidth = stringWidth(segment);
|
|
10
54
|
if (line && lineWidth + segmentWidth > width) {
|
|
11
55
|
lines.push(line);
|
|
@@ -26,7 +70,7 @@ export function fitLines(lines, maxRows, width) {
|
|
|
26
70
|
const visible = lines.slice(0, maxRows);
|
|
27
71
|
let last = "";
|
|
28
72
|
let used = 0;
|
|
29
|
-
for (const { segment } of
|
|
73
|
+
for (const { segment } of textUnits(visible[maxRows - 1])) {
|
|
30
74
|
const segmentWidth = stringWidth(segment);
|
|
31
75
|
if (used + segmentWidth > width - 1)
|
|
32
76
|
break;
|
|
@@ -55,7 +99,7 @@ export function clipText(value, width) {
|
|
|
55
99
|
return value;
|
|
56
100
|
let last = "";
|
|
57
101
|
let used = 0;
|
|
58
|
-
for (const { segment } of
|
|
102
|
+
for (const { segment } of textUnits(value)) {
|
|
59
103
|
const segmentWidth = stringWidth(segment);
|
|
60
104
|
if (used + segmentWidth > Math.max(width - 1, 0))
|
|
61
105
|
break;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { quoteText } from "../preview-model.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { describeUnmanagedRemoval, describeUnmanagedSymlinkRemoval, removeInspectedUnmanagedSkill, removeInspectedUnmanagedSymlink, } from "../unmanaged-removal.js";
|
|
4
5
|
import { inspectUserGlobalSkillRemoval, inspectUserGlobalSymlinkRemoval, } from "../user-global-skill-inventory.js";
|
|
5
|
-
import {
|
|
6
|
+
import { answerPaged, moveCursor } from "./mode-keys.js";
|
|
6
7
|
import { homeOf } from "./installations.js";
|
|
7
8
|
import { busy } from "./working-model.js";
|
|
8
9
|
export const UNMANAGED_KINDS = ["unmanaged-scope", "unmanaged-pick", "confirm-unmanaged"];
|
|
@@ -52,7 +53,7 @@ export async function prepareUnmanagedRemoval(operations, environment, group, lo
|
|
|
52
53
|
if (planned.has(link.path))
|
|
53
54
|
continue;
|
|
54
55
|
planned.set(link.path, link);
|
|
55
|
-
preview.push(`also removes link ${
|
|
56
|
+
preview.push(`also removes link ${quoteText(link.path)} → ${quoteText(chosen.path)} (it would dangle)`);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
for (const location of linksFirst([...planned.values()])) {
|
|
@@ -105,7 +106,7 @@ async function startPreview(context, group, locations) {
|
|
|
105
106
|
const prepared = await prepareUnmanagedRemoval(context.operations, context.env, group, locations);
|
|
106
107
|
if (!context.isMounted())
|
|
107
108
|
return;
|
|
108
|
-
context.setMode({ kind: "confirm-unmanaged", group, prepared });
|
|
109
|
+
context.setMode({ kind: "confirm-unmanaged", group, prepared, cursor: 0 });
|
|
109
110
|
}
|
|
110
111
|
catch (error) {
|
|
111
112
|
if (!context.isMounted())
|
|
@@ -125,6 +126,11 @@ function cancel(context) {
|
|
|
125
126
|
context.setMessage({ kind: "ok", text: "Uninstall cancelled" });
|
|
126
127
|
context.browse();
|
|
127
128
|
}
|
|
129
|
+
/** What the confirmation shows, so the key handler and the panel page the same rows. */
|
|
130
|
+
export function unmanagedConfirmPreview(mode) {
|
|
131
|
+
const { name } = mode.group;
|
|
132
|
+
return { lines: mode.prepared.preview, chrome: { title: "Remove unmanaged skill", action: `Remove ${name}`, consequence: `Remove ${name}? y/n` } };
|
|
133
|
+
}
|
|
128
134
|
async function execute(context, group, prepared) {
|
|
129
135
|
context.setMode(busy(`Removing ${group.name}…`));
|
|
130
136
|
await context.finish(await runUnmanagedRemoval(prepared, context.operations, context.env));
|
|
@@ -170,9 +176,15 @@ export function handleUnmanagedKey(context, mode, input, key) {
|
|
|
170
176
|
}
|
|
171
177
|
return true;
|
|
172
178
|
}
|
|
173
|
-
case "confirm-unmanaged":
|
|
174
|
-
|
|
179
|
+
case "confirm-unmanaged": {
|
|
180
|
+
const { lines, chrome } = unmanagedConfirmPreview(current);
|
|
181
|
+
answerPaged(context.previewGeometry(lines, current.cursor, chrome), current.cursor, input, key, {
|
|
182
|
+
moveTo: (cursor) => context.setMode({ ...current, cursor }),
|
|
183
|
+
yes: () => void execute(context, current.group, current.prepared),
|
|
184
|
+
no: () => cancel(context),
|
|
185
|
+
});
|
|
175
186
|
return true;
|
|
187
|
+
}
|
|
176
188
|
default:
|
|
177
189
|
return false;
|
|
178
190
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { formatProgressLabel } from "../flow-progress.js";
|
|
2
3
|
import { errorText } from "./batch.js";
|
|
3
4
|
import { BROWSE } from "./updates-mode.js";
|
|
4
5
|
import { CHECKING, emptyData, PHASE_LABEL, REFRESHING } from "./updates-model.js";
|
|
@@ -12,7 +13,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
12
13
|
useEffect(() => () => {
|
|
13
14
|
pendingAnswer.current?.(false);
|
|
14
15
|
}, []);
|
|
15
|
-
const load = useCallback(async (refresh) => {
|
|
16
|
+
const load = useCallback(async (refresh, force = false) => {
|
|
16
17
|
const token = ++loadToken.current;
|
|
17
18
|
// The check after an applied update continues that update's clock; a check from browsing starts one.
|
|
18
19
|
const running = busy(refresh ? REFRESHING : CHECKING, modeRef.current, clock);
|
|
@@ -26,7 +27,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
26
27
|
setMode(running);
|
|
27
28
|
let next;
|
|
28
29
|
try {
|
|
29
|
-
next = { status: "ready", data: await loadUpdateRows(operations, env, { refresh, onPhase: phase, now: clock }) };
|
|
30
|
+
next = { status: "ready", data: await loadUpdateRows(operations, env, { refresh, force, onPhase: phase, now: clock }) };
|
|
30
31
|
}
|
|
31
32
|
catch (error) {
|
|
32
33
|
// The scopes are read separately, so this is unexpected; show it as the check's result.
|
|
@@ -62,9 +63,14 @@ export function useUpdatesFlow(inputs) {
|
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
65
|
const apply = async (prepared, confirmedPaths) => {
|
|
65
|
-
|
|
66
|
+
const running = busy("Applying updates…", undefined, clock);
|
|
67
|
+
setMode(running);
|
|
66
68
|
let result;
|
|
67
69
|
try {
|
|
70
|
+
const progress = (event) => {
|
|
71
|
+
if (mounted.current)
|
|
72
|
+
setMode(busy(formatProgressLabel(event), running, clock));
|
|
73
|
+
};
|
|
68
74
|
const outcome = await runUpdates(prepared, confirmedPaths, (plan, reason) => new Promise(resolve => {
|
|
69
75
|
if (!mounted.current) {
|
|
70
76
|
resolve(false);
|
|
@@ -75,8 +81,8 @@ export function useUpdatesFlow(inputs) {
|
|
|
75
81
|
resolve(done);
|
|
76
82
|
};
|
|
77
83
|
pendingAnswer.current = answer;
|
|
78
|
-
setMode({ kind: "manual", plan, reason, answer, cursor: 0 });
|
|
79
|
-
}));
|
|
84
|
+
setMode({ kind: "manual", plan, reason, answer, cursor: 0, startedAt: running.startedAt });
|
|
85
|
+
}), progress);
|
|
80
86
|
result = { kind: outcome.failed === 0 ? "ok" : "error", lines: outcome.lines };
|
|
81
87
|
}
|
|
82
88
|
catch (error) {
|
|
@@ -120,10 +120,10 @@ export function handleUpdatesKey(context, mode, input, key) {
|
|
|
120
120
|
answerYesNo(input, key, () => {
|
|
121
121
|
if (!context.previewAtEnd(mode))
|
|
122
122
|
return;
|
|
123
|
-
context.setMode(busy("Checking App version…",
|
|
123
|
+
context.setMode(busy("Checking App version…", mode, context.now));
|
|
124
124
|
mode.answer(true);
|
|
125
125
|
}, () => {
|
|
126
|
-
context.setMode(busy("Continuing updates…",
|
|
126
|
+
context.setMode(busy("Continuing updates…", mode, context.now));
|
|
127
127
|
mode.answer(false);
|
|
128
128
|
});
|
|
129
129
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildUpdateTree } from "./updates-tree.js";
|
|
2
2
|
import { flattenVisible } from "./tree.js";
|
|
3
|
+
import { formatAgo } from "./updates.js";
|
|
3
4
|
export const REFRESHING = "Refreshing sources…";
|
|
4
5
|
export const CHECKING = "Checking for updates…";
|
|
5
6
|
export const PHASE_LABEL = { refreshing: REFRESHING, checking: CHECKING };
|
|
@@ -40,7 +41,7 @@ export function viewOf(data, scope, showUnknown, expanded) {
|
|
|
40
41
|
return { shown, updatable, unknown, upToDate, entries };
|
|
41
42
|
}
|
|
42
43
|
export function describeRefresh(data) {
|
|
43
|
-
const { attempted, refreshed, failed, unreadable } = data.refresh;
|
|
44
|
+
const { attempted, refreshed, failed, unreadable, reusedAt } = data.refresh;
|
|
44
45
|
if (!attempted)
|
|
45
46
|
return "sources not fetched again";
|
|
46
47
|
if (unreadable.length > 0) {
|
|
@@ -49,7 +50,9 @@ export function describeRefresh(data) {
|
|
|
49
50
|
if (failed.length > 0) {
|
|
50
51
|
return `${failed.length} of ${failed.length + refreshed.length} sources could not be refreshed; their current data was used`;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
if (refreshed.length === 0)
|
|
54
|
+
return "no Git sources to refresh";
|
|
55
|
+
return reusedAt === undefined ? "sources refreshed" : `sources refreshed ${formatAgo(data.checkedAt - reusedAt)}`;
|
|
53
56
|
}
|
|
54
57
|
/** Data for a check that failed as a whole, so the view can show the reason. */
|
|
55
58
|
export function emptyData(reason, checkedAt) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import { rowStyle, theme } from "./theme.js";
|
|
4
|
-
import { BorderPanel, FieldLine,
|
|
5
|
-
import {
|
|
5
|
+
import { BorderPanel, FieldLine, PagedPreview } from "./panel-parts.js";
|
|
6
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
6
7
|
import { describeRefresh, describeUnknownReasons } from "./updates-model.js";
|
|
7
|
-
import { formatAgo
|
|
8
|
+
import { formatAgo } from "./updates.js";
|
|
9
|
+
import { lineText } from "./preview-render.js";
|
|
8
10
|
import { groupCheckState, packageLeafLabel } from "./updates-tree.js";
|
|
9
|
-
import { updateModePreviewChrome, updateModePreviewLines
|
|
11
|
+
import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
|
|
10
12
|
function updateResultField(line, defaultOk) {
|
|
11
13
|
if (line.startsWith("Update summary: ")) {
|
|
12
14
|
const successful = /^Update summary: \d+ updated, 0 failed$/.test(line);
|
|
@@ -54,11 +56,10 @@ export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCoun
|
|
|
54
56
|
}) })) : null] }));
|
|
55
57
|
}
|
|
56
58
|
function CapturedUpdatePreview({ mode }) {
|
|
57
|
-
const
|
|
58
|
-
const lines =
|
|
59
|
+
const styled = updateModePreviewLines(mode);
|
|
60
|
+
const lines = styled.map(lineText);
|
|
59
61
|
const chrome = updateModePreviewChrome(mode);
|
|
60
|
-
|
|
61
|
-
return (_jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: updateModeQuestion(mode, viewport.reachedEnd), chrome: chrome }));
|
|
62
|
+
return _jsx(PagedPreview, { lines: lines, styled: styled, viewport: usePreviewGeometry()(lines, mode.cursor, chrome) });
|
|
62
63
|
}
|
|
63
64
|
export function ModePanel({ mode }) {
|
|
64
65
|
switch (mode.kind) {
|
|
@@ -1,65 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { quoteText } from "../preview-model.js";
|
|
2
|
+
import { appPlanLines } from "./app-actions.js";
|
|
3
|
+
import { fieldLine, renderPreviewDocument, textLine } from "./preview-render.js";
|
|
2
4
|
export function updateModePreviewLines(mode) {
|
|
3
5
|
switch (mode.kind) {
|
|
4
6
|
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
|
-
];
|
|
7
|
+
return renderPreviewDocument(mode.prepared.document);
|
|
11
8
|
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
|
-
];
|
|
9
|
+
return [...appPlanLines(mode.plan), ...(mode.reason === undefined ? [] : [textLine(mode.reason)])];
|
|
19
10
|
case "confirm-paths":
|
|
20
11
|
return [
|
|
21
|
-
|
|
22
|
-
...mode.prepared.nonCanonicalPaths.map(({ scope, path }) => ({
|
|
23
|
-
kind: "field",
|
|
24
|
-
label: "Path:",
|
|
25
|
-
value: `[${scope}] ${JSON.stringify(path)}`,
|
|
26
|
-
})),
|
|
12
|
+
textLine("Outside .agents/skills and .claude/skills:"),
|
|
13
|
+
...mode.prepared.nonCanonicalPaths.map(({ scope, path }) => fieldLine("Path:", `[${scope}] ${quoteText(path)}`)),
|
|
27
14
|
];
|
|
28
15
|
default:
|
|
29
16
|
return [];
|
|
30
17
|
}
|
|
31
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* What else confirming does besides applying: replaced local changes and external commands. Kept to
|
|
21
|
+
* a bounded length so a narrow footer still fits the question after it; the warnings name every Skill.
|
|
22
|
+
*/
|
|
23
|
+
function previewEffects(prepared) {
|
|
24
|
+
const replaced = prepared.groups.flatMap((group) => group.applicable.filter((item) => group.overwriteShown.has(item.id)));
|
|
25
|
+
const commands = prepared.document.sections.flatMap(({ items }) => items.flatMap(({ rows }) => rows.filter((row) => row.kind === "command"))).length;
|
|
26
|
+
return [
|
|
27
|
+
...(replaced.length === 1 ? [`replaces ${replaced[0].selection.path}'s local changes`] : []),
|
|
28
|
+
...(replaced.length > 1 ? [`replaces local changes in ${replaced.length} Skills`] : []),
|
|
29
|
+
...(commands === 0 ? [] : [`runs ${commands} ${commands === 1 ? "command" : "commands"}`]),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
32
|
export function updateModePreviewChrome(mode) {
|
|
33
33
|
switch (mode.kind) {
|
|
34
34
|
case "preview": {
|
|
35
35
|
const count = mode.prepared.applicable.length;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
footerRows: 2,
|
|
41
|
-
};
|
|
36
|
+
if (count === 0)
|
|
37
|
+
return { title: "Update batch preview", consequence: "Nothing can be applied.", cancel: "[Enter/Esc] back" };
|
|
38
|
+
const action = mode.prepared.document.action;
|
|
39
|
+
return { title: "Update batch preview", action, consequence: [...previewEffects(mode.prepared), `${action}? y/n`].join(" · ") };
|
|
42
40
|
}
|
|
43
41
|
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
|
-
};
|
|
42
|
+
return { title: "Manual App update", action: "Check now", consequence: "Manual step done, check now? y/n" };
|
|
49
43
|
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
|
-
};
|
|
44
|
+
return { title: "Confirm replacing non-canonical locations", action: "Replace these paths", consequence: "Replace exactly these paths? y/n" };
|
|
55
45
|
default:
|
|
56
|
-
return { title: "Updates"
|
|
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.";
|
|
46
|
+
return { title: "Updates" };
|
|
62
47
|
}
|
|
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
48
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
5
|
import { theme } from "./theme.js";
|
|
5
|
-
import {
|
|
6
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
6
7
|
import { computeWindow, useListHeight } from "./window.js";
|
|
7
8
|
import { BROWSE } from "./updates-mode.js";
|
|
8
9
|
import { defaultExpanded } from "./tree.js";
|
|
@@ -11,16 +12,21 @@ import { useUpdatesFlow } from "./updates-flow.js";
|
|
|
11
12
|
import { handleUpdatesKey } from "./updates-keys.js";
|
|
12
13
|
import { entryId, viewOf } from "./updates-model.js";
|
|
13
14
|
import { CheckStatus, EntryLine, ModePanel, UpdatesFooter } from "./updates-panel.js";
|
|
14
|
-
import {
|
|
15
|
+
import { lineText } from "./preview-render.js";
|
|
15
16
|
import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
|
|
16
17
|
import { useKeys } from "./keys.js";
|
|
17
18
|
import { BorderPanel, Working } from "./panel-parts.js";
|
|
19
|
+
/** Whether the working panel is drawn. A preview owns the screen: its height was computed without the panel, so the two never share it. */
|
|
20
|
+
export function showsWorkingPanel(state, mode) {
|
|
21
|
+
if (mode.kind === "preview" || mode.kind === "manual" || mode.kind === "confirm-paths")
|
|
22
|
+
return false;
|
|
23
|
+
return state.status === "loading" || mode.kind === "busy";
|
|
24
|
+
}
|
|
18
25
|
const NO_ENVIRONMENT = {};
|
|
19
26
|
/** How often the age of the check is redrawn. */
|
|
20
27
|
const CLOCK_TICK_MS = 10_000;
|
|
21
28
|
export function UpdatesView({ operations, environment, onCapturingChange, listHeight, now }) {
|
|
22
29
|
const height = useListHeight(listHeight, 9);
|
|
23
|
-
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
24
30
|
const env = environment ?? NO_ENVIRONMENT;
|
|
25
31
|
const clock = now ?? Date.now;
|
|
26
32
|
// Scope, cursor, fold and selection are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
@@ -73,7 +79,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
73
79
|
const { state, load, startPreview, apply } = useUpdatesFlow({
|
|
74
80
|
operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, modeRef, setMessage,
|
|
75
81
|
});
|
|
76
|
-
// Entering the view refreshes the Git sources first, like `r
|
|
82
|
+
// Entering the view refreshes the Git sources first, like `r`, but may reuse a recent refresh (ADR 0004).
|
|
77
83
|
useEffect(() => {
|
|
78
84
|
void load(true);
|
|
79
85
|
}, [load]);
|
|
@@ -81,10 +87,8 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
81
87
|
dataRef.current = data;
|
|
82
88
|
const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown, expanded);
|
|
83
89
|
const index = Math.min(cursor, Math.max(entries.length - 1, 0));
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
return previewViewport(updateModePreviewLines(current).map(previewLineText), cursor, terminalRows || 24, terminalColumns || 80, updateModePreviewChrome(current));
|
|
87
|
-
};
|
|
90
|
+
const geometry = usePreviewGeometry();
|
|
91
|
+
const previewGeometry = (current) => geometry(updateModePreviewLines(current).map(lineText), "cursor" in current ? current.cursor : 0, updateModePreviewChrome(current));
|
|
88
92
|
const keyContext = {
|
|
89
93
|
height,
|
|
90
94
|
now: clock,
|
|
@@ -105,7 +109,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
105
109
|
recheck: () => {
|
|
106
110
|
setMessage(undefined);
|
|
107
111
|
setShowUnknown(false);
|
|
108
|
-
void load(true);
|
|
112
|
+
void load(true, true);
|
|
109
113
|
},
|
|
110
114
|
startPreview: () => void startPreview(),
|
|
111
115
|
setMode,
|
|
@@ -118,6 +122,6 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
118
122
|
const hasErrors = data?.appError !== undefined || (data?.results ?? []).some((result) => result.error !== undefined);
|
|
119
123
|
const showingBrowse = mode.kind === "browse";
|
|
120
124
|
// One indicator for the whole operation: a check's own phase label, else the busy mode's.
|
|
121
|
-
const working = state.status === "loading" ? state : mode.kind === "busy" ? mode : undefined;
|
|
125
|
+
const working = !showsWorkingPanel(state, mode) ? undefined : state.status === "loading" ? state : mode.kind === "busy" ? mode : undefined;
|
|
122
126
|
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 })] }));
|
|
123
127
|
}
|