@iseoane/agent-depot 0.6.4 → 0.6.6
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 +2 -4
- 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/self-update.js +6 -1
- 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
|
@@ -10,10 +10,11 @@ import { hasUnmanagedCopy, isInstalled, loadInstalledSkills } from "./catalog-in
|
|
|
10
10
|
import { appName, appRecipePreviewLines, appStepLines } from "./app-actions.js";
|
|
11
11
|
import { errorText } from "./batch.js";
|
|
12
12
|
import { useMounted } from "./view-state.js";
|
|
13
|
-
import { BorderPanel, DecisionPrompt, PreviewConfirm } from "./panel-parts.js";
|
|
13
|
+
import { BorderPanel, DecisionPrompt, PreviewConfirm, Working } from "./panel-parts.js";
|
|
14
14
|
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
15
15
|
import { previewViewport } from "./preview-window.js";
|
|
16
16
|
import { theme } from "./theme.js";
|
|
17
|
+
import { busy } from "./working-model.js";
|
|
17
18
|
function recipeApprovalChrome(entry) {
|
|
18
19
|
return {
|
|
19
20
|
title: "Recipe approval · does not install",
|
|
@@ -43,6 +44,9 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
43
44
|
const [error, setError] = useState();
|
|
44
45
|
const [notice, setNotice] = useState();
|
|
45
46
|
const [loadError, setLoadError] = useState();
|
|
47
|
+
// When the oldest running reload began; reloads can overlap, so a count decides when loading ends.
|
|
48
|
+
const [loadingSince, setLoadingSince] = useState(() => Date.now());
|
|
49
|
+
const reloading = useRef(0);
|
|
46
50
|
const [mode, setModeState] = useState({ kind: "browse" });
|
|
47
51
|
const modeRef = useRef(mode);
|
|
48
52
|
const setMode = (next) => {
|
|
@@ -50,6 +54,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
50
54
|
onCapturingChange?.(next.kind !== "browse");
|
|
51
55
|
setModeState(next);
|
|
52
56
|
};
|
|
57
|
+
/** Shows the busy panel; an operation already busy keeps its start time. */
|
|
58
|
+
const setBusy = (label) => setMode(busy(label, modeRef.current));
|
|
53
59
|
const finish = async () => {
|
|
54
60
|
await reload();
|
|
55
61
|
if (mounted.current)
|
|
@@ -57,7 +63,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
57
63
|
};
|
|
58
64
|
const preview = async (entry, status, removeAfterApproval = false) => {
|
|
59
65
|
setError(undefined);
|
|
60
|
-
|
|
66
|
+
setBusy("Preparing App recipe preview…");
|
|
61
67
|
try {
|
|
62
68
|
const steps = await apps.preview(entry);
|
|
63
69
|
if (mounted.current) {
|
|
@@ -95,7 +101,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
95
101
|
},
|
|
96
102
|
};
|
|
97
103
|
const startAI = async () => {
|
|
98
|
-
|
|
104
|
+
setBusy("Checking the authoring Skill…");
|
|
99
105
|
try {
|
|
100
106
|
if (!operations.discoverSkills)
|
|
101
107
|
throw new Error("Discovery is not supported by the configured operations");
|
|
@@ -124,7 +130,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
124
130
|
};
|
|
125
131
|
const startRemoval = async (entry) => {
|
|
126
132
|
setError(undefined);
|
|
127
|
-
|
|
133
|
+
setBusy("Checking App recipe…");
|
|
128
134
|
try {
|
|
129
135
|
const approval = await apps.approvalStatus(entry);
|
|
130
136
|
if (approval.status === "needs approval") {
|
|
@@ -143,7 +149,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
143
149
|
}
|
|
144
150
|
};
|
|
145
151
|
const uninstall = async (current) => {
|
|
146
|
-
|
|
152
|
+
setBusy(current.kind === "uninstall-manual" ? "Checking App uninstall…" : "Uninstalling App…");
|
|
147
153
|
try {
|
|
148
154
|
const result = current.kind === "uninstall-manual"
|
|
149
155
|
? await apps.completeManual(current.plan, true)
|
|
@@ -200,7 +206,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
200
206
|
else if (input === "n" || key.escape)
|
|
201
207
|
cancel();
|
|
202
208
|
else if (input === "y" && viewport.reachedEnd) {
|
|
203
|
-
|
|
209
|
+
setBusy("Approving App recipe…");
|
|
204
210
|
void apps.approve(current.entry).then(async () => {
|
|
205
211
|
await reload();
|
|
206
212
|
if (!mounted.current)
|
|
@@ -220,7 +226,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
220
226
|
}
|
|
221
227
|
if (current.kind === "uninstall-question") {
|
|
222
228
|
answerYesNo(input, key, () => {
|
|
223
|
-
|
|
229
|
+
setBusy("Preparing App uninstall…");
|
|
224
230
|
void apps.planLifecycle(current.removal.entry, "uninstall").then(plan => {
|
|
225
231
|
if (mounted.current)
|
|
226
232
|
setMode({ kind: !plan.argv && plan.manual ? "uninstall-manual" : "uninstall-confirm", removal: current.removal, plan, cursor: 0 });
|
|
@@ -245,7 +251,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
245
251
|
}
|
|
246
252
|
if (current.kind === "remove") {
|
|
247
253
|
answerYesNo(input, key, () => {
|
|
248
|
-
|
|
254
|
+
setBusy("Removing App recipe…");
|
|
249
255
|
void removeAppRecipe(apps, current.removal, true).then(async () => {
|
|
250
256
|
if (mounted.current)
|
|
251
257
|
setNotice(`Removed App recipe: ${current.removal.file}`);
|
|
@@ -276,6 +282,8 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
276
282
|
};
|
|
277
283
|
/** Reloads parsing and approval receipts, never version/latest commands. */
|
|
278
284
|
const reload = useCallback(async () => {
|
|
285
|
+
if (reloading.current++ === 0 && mounted.current)
|
|
286
|
+
setLoadingSince(Date.now());
|
|
279
287
|
try {
|
|
280
288
|
const entries = await apps.load();
|
|
281
289
|
const rows = await Promise.all(entries.map(async (entry) => {
|
|
@@ -294,9 +302,13 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
294
302
|
if (mounted.current)
|
|
295
303
|
setLoadError(errorText(error));
|
|
296
304
|
}
|
|
305
|
+
finally {
|
|
306
|
+
if (--reloading.current === 0 && mounted.current)
|
|
307
|
+
setLoadingSince(undefined);
|
|
308
|
+
}
|
|
297
309
|
}, [apps, mounted]);
|
|
298
310
|
useEffect(() => { void reload(); }, [reload]);
|
|
299
|
-
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, notice ? _jsx(Text, { color: theme.success, children: notice }) : null, mode.kind === "guide" ? (_jsxs(BorderPanel, { title: "Add an App recipe", children: [_jsx(Text, { children: "1 With the AI skill \u00B7 install if missing, then copy the prompt" }), _jsx(Text, { children: "2 Manual \u00B7 write a recipe using the schema" }), _jsx(Text, { children: "n or Esc closes" })] })) : null, mode.kind === "manual" ? (_jsxs(BorderPanel, { title: "Write an App recipe", children: [_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory] }), _jsx(Text, { children: "Schema: agent-depot app schema \u00B7 n or Esc closes" })] })) : null, mode.kind === "prompt" ? (_jsxs(BorderPanel, { title: "App recipe authoring prompt", children: [_jsx(Text, { children: "Use the agent-depot-apprecipe skill to create the recipe for <app>" }), _jsxs(Text, { children: ["Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(BorderPanel, { title: "App recipe workflow", children: _jsx(
|
|
311
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, notice ? _jsx(Text, { color: theme.success, children: notice }) : null, mode.kind === "guide" ? (_jsxs(BorderPanel, { title: "Add an App recipe", children: [_jsx(Text, { children: "1 With the AI skill \u00B7 install if missing, then copy the prompt" }), _jsx(Text, { children: "2 Manual \u00B7 write a recipe using the schema" }), _jsx(Text, { children: "n or Esc closes" })] })) : null, mode.kind === "manual" ? (_jsxs(BorderPanel, { title: "Write an App recipe", children: [_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory] }), _jsx(Text, { children: "Schema: agent-depot app schema \u00B7 n or Esc closes" })] })) : null, mode.kind === "prompt" ? (_jsxs(BorderPanel, { title: "App recipe authoring prompt", children: [_jsx(Text, { children: "Use the agent-depot-apprecipe skill to create the recipe for <app>" }), _jsxs(Text, { children: ["Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(BorderPanel, { title: "App recipe workflow", children: _jsx(Working, { label: mode.label, since: mode.startedAt }) }) : null, mode.kind === "approve" ? (() => {
|
|
300
312
|
const chrome = recipeApprovalChrome(mode.entry);
|
|
301
313
|
const viewport = previewViewport(mode.lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
302
314
|
return _jsx(PreviewConfirm, { title: chrome.title, lines: mode.lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
@@ -306,5 +318,5 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
306
318
|
const viewport = previewViewport(lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
307
319
|
return _jsx(PreviewConfirm, { title: chrome.title, lines: lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
308
320
|
})() : null, mode.kind === "remove" ? (_jsx(PreviewConfirm, { title: "Recipe removal", lines: [`Delete recipe: ${mode.removal.file}`, "Only this recipe file or symlink will be removed; App data and Host configuration are not deleted."], question: `Remove recipe for ${appName(mode.removal.entry)}? y/n` })) : null] }));
|
|
309
|
-
return { rows, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory, capturing: mode.kind !== "browse" };
|
|
321
|
+
return { rows, loadingSince, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory, capturing: mode.kind !== "browse" };
|
|
310
322
|
}
|
|
@@ -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.6",
|
|
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",
|