@iseoane/agent-depot 0.6.3 → 0.6.5
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 +94 -0
- package/README.es.md +14 -10
- package/README.md +22 -8
- package/dist/src/app-cli.js +10 -5
- package/dist/src/app-flow.js +2 -2
- package/dist/src/atomic-file.js +9 -0
- package/dist/src/cli.js +98 -67
- package/dist/src/executable-resolution.js +9 -3
- package/dist/src/git-source.js +3 -5
- package/dist/src/package-cli.js +9 -4
- package/dist/src/package-flow.js +2 -1
- package/dist/src/package-host-state.js +44 -6
- package/dist/src/package-state.js +2 -5
- package/dist/src/process-runner.js +234 -16
- package/dist/src/profile-cli.js +8 -6
- package/dist/src/progress.js +157 -0
- package/dist/src/source-state.js +2 -5
- package/dist/src/tui/adoption-actions.js +4 -3
- package/dist/src/tui/app-actions.js +16 -9
- package/dist/src/tui/bulk-actions.js +7 -6
- package/dist/src/tui/catalog-flow.js +3 -2
- package/dist/src/tui/catalog-panel.js +2 -2
- package/dist/src/tui/catalog-view.js +26 -6
- package/dist/src/tui/file-picker.js +52 -20
- package/dist/src/tui/installations-view.js +44 -11
- package/dist/src/tui/manage-actions.js +7 -6
- package/dist/src/tui/manage-panel.js +2 -2
- package/dist/src/tui/package-actions.js +63 -31
- package/dist/src/tui/panel-parts.js +20 -0
- package/dist/src/tui/profile-view.js +84 -15
- package/dist/src/tui/sources-mode.js +2 -1
- package/dist/src/tui/sources-recipes.js +22 -10
- package/dist/src/tui/sources-view.js +13 -12
- package/dist/src/tui/text-wrap.js +13 -0
- package/dist/src/tui/unmanaged-actions.js +3 -2
- package/dist/src/tui/updates-flow.js +10 -7
- package/dist/src/tui/updates-keys.js +3 -2
- package/dist/src/tui/updates-model.js +2 -2
- package/dist/src/tui/updates-panel.js +3 -2
- package/dist/src/tui/updates-view.js +6 -3
- package/dist/src/tui/view-state.js +12 -0
- package/dist/src/tui/working-model.js +33 -0
- package/package.json +2 -2
|
@@ -6,7 +6,7 @@ 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";
|
|
9
|
+
import { BorderPanel, DecisionPrompt, FieldLine, PreviewLines, Working } from "./panel-parts.js";
|
|
10
10
|
import { previewViewport } from "./preview-window.js";
|
|
11
11
|
import { movePreviewCursor } from "./mode-keys.js";
|
|
12
12
|
/** Footer keys for the selected row; App seeds the footer before the view reports a selection. */
|
|
@@ -53,7 +53,7 @@ function refreshViewport(mode, rows, columns) {
|
|
|
53
53
|
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment, onHintsChange }) {
|
|
54
54
|
const { columns, rows: terminalRows } = useWindowSize();
|
|
55
55
|
const height = useListHeight(listHeight, 9);
|
|
56
|
-
const [state, setState] = useState({ status: "loading" });
|
|
56
|
+
const [state, setState] = useState(() => ({ status: "loading", startedAt: Date.now() }));
|
|
57
57
|
// The highlight follows the source id; the index is only the fallback once that source is gone.
|
|
58
58
|
// Selection and marks are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
59
59
|
const [selection, setSelectionState] = useState({ index: 0 });
|
|
@@ -147,8 +147,8 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
147
147
|
setSelection({ id: listed[bounded]?.id, index: bounded });
|
|
148
148
|
};
|
|
149
149
|
/** Runs a mutating action in the busy mode, reports its outcome and reloads the list. */
|
|
150
|
-
const run = async (action) => {
|
|
151
|
-
dispatch({ type: "busy" });
|
|
150
|
+
const run = async (label, action) => {
|
|
151
|
+
dispatch({ type: "busy", label, startedAt: Date.now() });
|
|
152
152
|
setMessage(undefined);
|
|
153
153
|
let outcome;
|
|
154
154
|
try {
|
|
@@ -180,7 +180,7 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
180
180
|
stop("Removing sources is not supported by the configured operations");
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
|
-
dispatch({ type: "busy" });
|
|
183
|
+
dispatch({ type: "busy", label: "Checking dependent Skills…", startedAt: Date.now() });
|
|
184
184
|
setMessage(undefined);
|
|
185
185
|
try {
|
|
186
186
|
const installations = await operations.listUserGlobalInstallations();
|
|
@@ -226,7 +226,7 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
226
226
|
const url = typed.current.trim();
|
|
227
227
|
if (url === "")
|
|
228
228
|
return;
|
|
229
|
-
void run(async () => `Added Git Source: ${(await operations.addGitSource(url)).id}`);
|
|
229
|
+
void run("Adding Git Source…", async () => `Added Git Source: ${(await operations.addGitSource(url)).id}`);
|
|
230
230
|
}
|
|
231
231
|
else if (key.backspace || key.delete) {
|
|
232
232
|
typed.current = typed.current.slice(0, -1);
|
|
@@ -252,7 +252,7 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
252
252
|
if (input === "y") {
|
|
253
253
|
if (mode.action === "refresh") {
|
|
254
254
|
const { sources: targets } = mode;
|
|
255
|
-
void run(async () => {
|
|
255
|
+
void run(targets.length === 1 ? "Refreshing Git Source…" : "Refreshing Git Sources…", async () => {
|
|
256
256
|
const result = await refreshEach(operations, targets);
|
|
257
257
|
setMarked(new Set());
|
|
258
258
|
return result;
|
|
@@ -260,7 +260,7 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
260
260
|
}
|
|
261
261
|
else {
|
|
262
262
|
const { source } = mode;
|
|
263
|
-
void run(async () => {
|
|
263
|
+
void run("Removing Git Source…", async () => {
|
|
264
264
|
await operations.removeGitSource?.(source.id);
|
|
265
265
|
return `Removed Git Source: ${source.id}`;
|
|
266
266
|
});
|
|
@@ -324,7 +324,7 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
324
324
|
// Sources removed since they were marked no longer count.
|
|
325
325
|
const selectedCount = sources.filter((source) => marked.has(source.id)).length;
|
|
326
326
|
if (state.status === "loading")
|
|
327
|
-
return _jsx(BorderPanel, { title: "Sources", children: _jsx(
|
|
327
|
+
return _jsx(BorderPanel, { title: "Sources", children: _jsx(Working, { label: "Loading sources\u2026", since: state.startedAt }) });
|
|
328
328
|
if (state.status === "error")
|
|
329
329
|
return _jsx(BorderPanel, { title: "Source error", children: _jsx(Text, { color: theme.error, children: state.message }) });
|
|
330
330
|
const window = computeWindow(rows.length, selected, height);
|
|
@@ -332,13 +332,14 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
332
332
|
return _jsx(Prompt, { mode: mode, terminalRows: terminalRows || 24, terminalColumns: columns || 80 });
|
|
333
333
|
if (recipes.capturing)
|
|
334
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) => {
|
|
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 && 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
336
|
const index = window.start + offset;
|
|
337
337
|
const id = row.kind === "skills" ? row.source.id : row.recipe.entry.recipe?.name ?? row.recipe.entry.file.split(/[\\/]/u).pop();
|
|
338
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
339
|
? _jsx(Text, { color: theme.value, children: row.source.url })
|
|
340
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.
|
|
341
|
+
}), 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
|
+
? _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] }));
|
|
342
343
|
}
|
|
343
344
|
function Prompt({ mode, terminalRows = 24, terminalColumns = 80 }) {
|
|
344
345
|
if (mode.kind === "add-kind")
|
|
@@ -346,7 +347,7 @@ function Prompt({ mode, terminalRows = 24, terminalColumns = 80 }) {
|
|
|
346
347
|
if (mode.kind === "input")
|
|
347
348
|
return _jsxs(Text, { children: ["URL: ", mode.value] });
|
|
348
349
|
if (mode.kind === "busy")
|
|
349
|
-
return _jsx(BorderPanel, { title: "Sources", children: _jsx(
|
|
350
|
+
return _jsx(BorderPanel, { title: "Sources", children: _jsx(Working, { label: mode.label, since: mode.startedAt }) });
|
|
350
351
|
if (mode.kind === "confirm") {
|
|
351
352
|
return mode.action === "refresh" ? (() => {
|
|
352
353
|
const viewport = refreshViewport(mode, terminalRows, terminalColumns);
|
|
@@ -36,6 +36,19 @@ export function fitLines(lines, maxRows, width) {
|
|
|
36
36
|
visible[maxRows - 1] = `${last}…`;
|
|
37
37
|
return visible;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* `lines` capped to `maxRows` by dropping the middle: the first and last rows stay (the start and end of
|
|
41
|
+
* a path) with a row counting what was left out. With two rows or fewer it falls back to `fitLines`.
|
|
42
|
+
*/
|
|
43
|
+
export function elideMiddle(lines, maxRows, width) {
|
|
44
|
+
if (lines.length <= maxRows)
|
|
45
|
+
return lines;
|
|
46
|
+
if (maxRows <= 2)
|
|
47
|
+
return fitLines(lines, maxRows, width);
|
|
48
|
+
const tail = lines.slice(lines.length - (maxRows - 2));
|
|
49
|
+
const hidden = lines.length - 1 - tail.length;
|
|
50
|
+
return [lines[0], `… ${hidden} more ${hidden === 1 ? "row" : "rows"}`, ...tail];
|
|
51
|
+
}
|
|
39
52
|
/** One line shortened to `width` columns plus an ellipsis, for single-row labels. */
|
|
40
53
|
export function clipText(value, width) {
|
|
41
54
|
if (stringWidth(value) <= width)
|
|
@@ -4,6 +4,7 @@ import { describeUnmanagedRemoval, describeUnmanagedSymlinkRemoval, removeInspec
|
|
|
4
4
|
import { inspectUserGlobalSkillRemoval, inspectUserGlobalSymlinkRemoval, } from "../user-global-skill-inventory.js";
|
|
5
5
|
import { answerYesNo, moveCursor } from "./mode-keys.js";
|
|
6
6
|
import { homeOf } from "./installations.js";
|
|
7
|
+
import { busy } from "./working-model.js";
|
|
7
8
|
export const UNMANAGED_KINDS = ["unmanaged-scope", "unmanaged-pick", "confirm-unmanaged"];
|
|
8
9
|
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
9
10
|
/** Links first, so a Claude link never outlives the canonical directory it points to. */
|
|
@@ -99,7 +100,7 @@ export async function runUnmanagedRemoval(prepared, operations, environment) {
|
|
|
99
100
|
return { kind: failed ? "error" : "ok", text: lines.join("\n") };
|
|
100
101
|
}
|
|
101
102
|
async function startPreview(context, group, locations) {
|
|
102
|
-
context.setMode(
|
|
103
|
+
context.setMode(busy(`Preparing removal of ${group.name}…`));
|
|
103
104
|
try {
|
|
104
105
|
const prepared = await prepareUnmanagedRemoval(context.operations, context.env, group, locations);
|
|
105
106
|
if (!context.isMounted())
|
|
@@ -125,7 +126,7 @@ function cancel(context) {
|
|
|
125
126
|
context.browse();
|
|
126
127
|
}
|
|
127
128
|
async function execute(context, group, prepared) {
|
|
128
|
-
context.setMode(
|
|
129
|
+
context.setMode(busy(`Removing ${group.name}…`));
|
|
129
130
|
await context.finish(await runUnmanagedRemoval(prepared, context.operations, context.env));
|
|
130
131
|
}
|
|
131
132
|
/** Handles a key in an unmanaged removal mode; returns false for any other mode. */
|
|
@@ -2,10 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { errorText } from "./batch.js";
|
|
3
3
|
import { BROWSE } from "./updates-mode.js";
|
|
4
4
|
import { CHECKING, emptyData, PHASE_LABEL, REFRESHING } from "./updates-model.js";
|
|
5
|
+
import { busy } from "./working-model.js";
|
|
5
6
|
import { loadUpdateRows, prepareUpdates, runUpdates } from "./updates.js";
|
|
6
7
|
export function useUpdatesFlow(inputs) {
|
|
7
|
-
const { operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, setMessage } = inputs;
|
|
8
|
-
const [state, setState] = useState({ status: "loading", label: REFRESHING });
|
|
8
|
+
const { operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, modeRef, setMessage } = inputs;
|
|
9
|
+
const [state, setState] = useState(() => ({ status: "loading", label: REFRESHING, startedAt: clock() }));
|
|
9
10
|
const loadToken = useRef(0);
|
|
10
11
|
const pendingAnswer = useRef(undefined);
|
|
11
12
|
useEffect(() => () => {
|
|
@@ -13,14 +14,16 @@ export function useUpdatesFlow(inputs) {
|
|
|
13
14
|
}, []);
|
|
14
15
|
const load = useCallback(async (refresh) => {
|
|
15
16
|
const token = ++loadToken.current;
|
|
17
|
+
// The check after an applied update continues that update's clock; a check from browsing starts one.
|
|
18
|
+
const running = busy(refresh ? REFRESHING : CHECKING, modeRef.current, clock);
|
|
16
19
|
const phase = (next) => {
|
|
17
20
|
if (!mounted.current || token !== loadToken.current)
|
|
18
21
|
return;
|
|
19
|
-
setState({ status: "loading", label: PHASE_LABEL[next] });
|
|
22
|
+
setState({ status: "loading", label: PHASE_LABEL[next], startedAt: running.startedAt });
|
|
20
23
|
};
|
|
21
24
|
setChecked([]);
|
|
22
25
|
setCursor(0);
|
|
23
|
-
setMode(
|
|
26
|
+
setMode(running);
|
|
24
27
|
let next;
|
|
25
28
|
try {
|
|
26
29
|
next = { status: "ready", data: await loadUpdateRows(operations, env, { refresh, onPhase: phase, now: clock }) };
|
|
@@ -34,7 +37,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
34
37
|
return;
|
|
35
38
|
setState(next);
|
|
36
39
|
setMode(BROWSE);
|
|
37
|
-
}, [operations, env, mounted, setMode, setChecked, setCursor, clock]);
|
|
40
|
+
}, [operations, env, mounted, setMode, modeRef, setChecked, setCursor, clock]);
|
|
38
41
|
const startPreview = async () => {
|
|
39
42
|
const { data: current, updatable: listed } = latest();
|
|
40
43
|
if (!current)
|
|
@@ -45,7 +48,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
47
50
|
setMessage(undefined);
|
|
48
|
-
setMode(
|
|
51
|
+
setMode(busy("Preparing preview…", undefined, clock));
|
|
49
52
|
try {
|
|
50
53
|
const prepared = await prepareUpdates(current, selected);
|
|
51
54
|
if (mounted.current)
|
|
@@ -59,7 +62,7 @@ export function useUpdatesFlow(inputs) {
|
|
|
59
62
|
}
|
|
60
63
|
};
|
|
61
64
|
const apply = async (prepared, confirmedPaths) => {
|
|
62
|
-
setMode(
|
|
65
|
+
setMode(busy("Applying updates…", undefined, clock));
|
|
63
66
|
let result;
|
|
64
67
|
try {
|
|
65
68
|
const outcome = await runUpdates(prepared, confirmedPaths, (plan, reason) => new Promise(resolve => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
2
|
+
import { busy } from "./working-model.js";
|
|
2
3
|
import { pageStep } from "./window.js";
|
|
3
4
|
const SCOPE_KEYS = new Map([["t", "all"], ["p", "project"], ["g", "user-global"]]);
|
|
4
5
|
/** Moves the cursor with the arrows, j/k and paging; returns whether the key was one of them. */
|
|
@@ -100,10 +101,10 @@ export function handleUpdatesKey(context, mode, input, key) {
|
|
|
100
101
|
answerYesNo(input, key, () => {
|
|
101
102
|
if (!context.previewAtEnd(mode))
|
|
102
103
|
return;
|
|
103
|
-
context.setMode(
|
|
104
|
+
context.setMode(busy("Checking App version…", undefined, context.now));
|
|
104
105
|
mode.answer(true);
|
|
105
106
|
}, () => {
|
|
106
|
-
context.setMode(
|
|
107
|
+
context.setMode(busy("Continuing updates…", undefined, context.now));
|
|
107
108
|
mode.answer(false);
|
|
108
109
|
});
|
|
109
110
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const REFRESHING = "Refreshing sources
|
|
2
|
-
export const CHECKING = "Checking for updates
|
|
1
|
+
export const REFRESHING = "Refreshing sources…";
|
|
2
|
+
export const CHECKING = "Checking for updates…";
|
|
3
3
|
export const PHASE_LABEL = { refreshing: REFRESHING, checking: CHECKING };
|
|
4
4
|
/** Names the reasons worth knowing before expanding the collapsed "cannot be checked" row. */
|
|
5
5
|
export function describeUnknownReasons(rows) {
|
|
@@ -3,7 +3,7 @@ import { Box, Text, useWindowSize } from "ink";
|
|
|
3
3
|
import { rowStyle, theme } from "./theme.js";
|
|
4
4
|
import { BorderPanel, FieldLine, PreviewConfirm } from "./panel-parts.js";
|
|
5
5
|
import { previewViewport } from "./preview-window.js";
|
|
6
|
-
import {
|
|
6
|
+
import { describeRefresh, describeUnknownReasons } from "./updates-model.js";
|
|
7
7
|
import { formatAgo, previewLineText } from "./updates.js";
|
|
8
8
|
import { updateModePreviewChrome, updateModePreviewLines, updateModeQuestion } from "./updates-preview.js";
|
|
9
9
|
function updateResultField(line, defaultOk) {
|
|
@@ -61,7 +61,8 @@ export function ModePanel({ mode }) {
|
|
|
61
61
|
case "browse":
|
|
62
62
|
return null;
|
|
63
63
|
case "busy":
|
|
64
|
-
|
|
64
|
+
// The view renders the one indicator of a busy mode, so a check never shows two.
|
|
65
|
+
return null;
|
|
65
66
|
case "preview":
|
|
66
67
|
case "confirm-paths":
|
|
67
68
|
return _jsx(CapturedUpdatePreview, { mode: mode });
|
|
@@ -13,7 +13,7 @@ import { CheckStatus, EntryLine, ModePanel, UpdatesFooter } from "./updates-pane
|
|
|
13
13
|
import { previewLineText } from "./updates.js";
|
|
14
14
|
import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
|
|
15
15
|
import { useKeys } from "./keys.js";
|
|
16
|
-
import { BorderPanel,
|
|
16
|
+
import { BorderPanel, Working } from "./panel-parts.js";
|
|
17
17
|
const NO_ENVIRONMENT = {};
|
|
18
18
|
/** How often the age of the check is redrawn. */
|
|
19
19
|
const CLOCK_TICK_MS = 10_000;
|
|
@@ -69,7 +69,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
69
69
|
setMode(BROWSE);
|
|
70
70
|
};
|
|
71
71
|
const { state, load, startPreview, apply } = useUpdatesFlow({
|
|
72
|
-
operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, setMessage,
|
|
72
|
+
operations, env, clock, mounted, checkedRef, latest, setChecked, setCursor, setMode, modeRef, setMessage,
|
|
73
73
|
});
|
|
74
74
|
// Entering the view refreshes the Git sources first, like `r`.
|
|
75
75
|
useEffect(() => {
|
|
@@ -85,6 +85,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
85
85
|
};
|
|
86
86
|
const keyContext = {
|
|
87
87
|
height,
|
|
88
|
+
now: clock,
|
|
88
89
|
previewMetrics: current => {
|
|
89
90
|
const geometry = previewGeometry(current);
|
|
90
91
|
return { length: geometry.total, pageSize: geometry.height };
|
|
@@ -112,5 +113,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
|
|
|
112
113
|
const view = { shown, updatable, unknown, upToDate, entries };
|
|
113
114
|
const hasErrors = data?.appError !== undefined || (data?.results ?? []).some((result) => result.error !== undefined);
|
|
114
115
|
const showingBrowse = mode.kind === "browse";
|
|
115
|
-
|
|
116
|
+
// One indicator for the whole operation: a check's own phase label, else the busy mode's.
|
|
117
|
+
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 })] }));
|
|
116
119
|
}
|
|
@@ -10,6 +10,18 @@ export function useMounted() {
|
|
|
10
10
|
}, []);
|
|
11
11
|
return mounted;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* When the current stretch of loading began, so a loading line shows how long it has waited even
|
|
15
|
+
* though the view re-renders. `seed` continues an operation that was already running (its busy start).
|
|
16
|
+
*/
|
|
17
|
+
export function useLoadingSince(loading, seed) {
|
|
18
|
+
const [since, setSince] = useState(() => (loading ? seed ?? Date.now() : undefined));
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setSince((previous) => (loading ? previous ?? seed ?? Date.now() : undefined));
|
|
21
|
+
}, [loading, seed]);
|
|
22
|
+
// Until the effect records it, the first loading render derives the same start.
|
|
23
|
+
return since ?? seed ?? Date.now();
|
|
24
|
+
}
|
|
13
25
|
/** Marks by node id. The ref is updated first so keys in one burst see every mark. */
|
|
14
26
|
export function useMarks() {
|
|
15
27
|
const [marked, setMarkedState] = useState(new Set());
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Braille spinner frames; one is shown per tick while an operation runs. */
|
|
2
|
+
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
+
/** Fixed glyph shown instead of the spinner when animation is off. */
|
|
4
|
+
export const STATIC_GLYPH = "·";
|
|
5
|
+
/** How often the spinner advances and the elapsed time is redrawn. */
|
|
6
|
+
export const SPINNER_TICK_MS = 80;
|
|
7
|
+
/** Elapsed time stays hidden until the operation has run this long. */
|
|
8
|
+
const ELAPSED_THRESHOLD_MS = 1000;
|
|
9
|
+
/** `12s` or `1m 05s`; empty under one second. */
|
|
10
|
+
export function formatElapsed(ms) {
|
|
11
|
+
if (!(ms >= ELAPSED_THRESHOLD_MS))
|
|
12
|
+
return "";
|
|
13
|
+
const seconds = Math.floor(ms / 1000);
|
|
14
|
+
if (seconds < 60)
|
|
15
|
+
return `${seconds}s`;
|
|
16
|
+
return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
17
|
+
}
|
|
18
|
+
/** Animation is on unless `AGENT_DEPOT_NO_ANIMATION=1`, which tests set so frames stay static. */
|
|
19
|
+
export function animationEnabled(env = process.env) {
|
|
20
|
+
return env.AGENT_DEPOT_NO_ANIMATION !== "1";
|
|
21
|
+
}
|
|
22
|
+
/** Whether a rendered frame starts with a Working line, so tests can wait for the real content. */
|
|
23
|
+
export function isWorkingFrame(text) {
|
|
24
|
+
return /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏·] \S[^\n]*…/u.test(text.trimStart());
|
|
25
|
+
}
|
|
26
|
+
/** When an operation began: the start time of the busy mode it continues, otherwise now. Only busy modes carry one. */
|
|
27
|
+
export function operationStart(previous, now = Date.now) {
|
|
28
|
+
return previous?.startedAt ?? now();
|
|
29
|
+
}
|
|
30
|
+
/** The busy mode for `label`; it keeps the start time of an operation already busy, so the clock survives label changes. */
|
|
31
|
+
export function busy(label, previous, now = Date.now) {
|
|
32
|
+
return { kind: "busy", label, startedAt: operationStart(previous, now) };
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iseoane/agent-depot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "A cross-platform catalog for reusable coding-agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "iseoane",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"typecheck": "tsc --noEmit",
|
|
75
75
|
"lint": "eslint \"src/**/*.{ts,tsx}\" \"resources/pi/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
|
|
76
76
|
"test": "pnpm build && node --test --import ./dist/tests/isolated-env.js \"dist/tests/*.test.js\"",
|
|
77
|
-
"test:single": "pnpm build && node -e \"const { spawnSync } = require('node:child_process'); const input = process.argv[1] || 'tests/version.test.ts'; const file = input.replace(/^tests[\\\\/]/, 'dist/tests/').replace(/\\\\.
|
|
77
|
+
"test:single": "pnpm build && node -e \"const { spawnSync } = require('node:child_process'); const input = process.argv[1] || 'tests/version.test.ts'; const file = input.replace(/^tests[\\\\/]/, 'dist/tests/').replace(/\\\\.tsx?$/i, '.js'); process.exit(spawnSync(process.execPath, ['--import', './dist/tests/isolated-env.js', '--test', file], { stdio: 'inherit' }).status ?? 1);\"",
|
|
78
78
|
"test:coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && tsc -p tsconfig.coverage.json && node -e \"const { spawnSync } = require('node:child_process'); process.exit(spawnSync(process.execPath, ['--enable-source-maps','--import','./dist/tests/isolated-env.js','--test','--test-reporter=dot','dist/tests/*.test.js'], { stdio: 'inherit', env: { ...process.env, NODE_V8_COVERAGE: 'coverage/v8' } }).status ?? 1);\"",
|
|
79
79
|
"audit:dead-code": "npx --yes fallow@3.30.0 dead-code --fail-on-issues",
|
|
80
80
|
"audit:dupes": "npx --yes fallow@3.30.0 dupes",
|