@iseoane/agent-depot 0.6.9 → 0.6.10
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 +35 -0
- package/README.md +24 -2
- package/dist/src/app-flow.js +3 -3
- package/dist/src/app-recipe-removal.js +3 -1
- package/dist/src/atomic-file.js +24 -1
- package/dist/src/cli.js +47 -13
- package/dist/src/path-safety.js +4 -1
- package/dist/src/profile-export.js +1 -1
- package/dist/src/profile-import.js +94 -18
- package/dist/src/project-installation.js +2 -1
- package/dist/src/project-manifest.js +7 -0
- package/dist/src/project-target.js +58 -0
- package/dist/src/skill-discovery.js +86 -6
- package/dist/src/skill-install.js +20 -8
- package/dist/src/tui/app-actions.js +18 -7
- package/dist/src/tui/app.js +19 -4
- package/dist/src/tui/batch.js +2 -1
- package/dist/src/tui/bulk-actions.js +21 -8
- package/dist/src/tui/catalog-installs.js +3 -2
- package/dist/src/tui/catalog-panel.js +3 -2
- package/dist/src/tui/catalog-state.js +47 -21
- package/dist/src/tui/catalog-tree.js +47 -10
- package/dist/src/tui/catalog-view.js +11 -5
- package/dist/src/tui/installations-view.js +19 -6
- package/dist/src/tui/manage-panel.js +1 -1
- package/dist/src/tui/profile-view.js +68 -31
- package/dist/src/tui/sources-recipes.js +44 -8
- package/dist/src/tui/sources-view.js +12 -5
- package/dist/src/tui/updates.js +1 -0
- package/dist/src/tui/view-retention.js +17 -0
- package/dist/src/update-flow.js +3 -1
- package/package.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { githubTreeLocation } from "../git-source.js";
|
|
2
|
-
import {
|
|
2
|
+
import { selectionKey } from "../package-model.js";
|
|
3
3
|
import { packageOwnedSkillMatcher } from "../package-owned-skills.js";
|
|
4
4
|
import { packageVersionLabel } from "./package-rows.js";
|
|
5
5
|
import { defaultExpanded } from "./tree.js";
|
|
6
6
|
const DESCRIPTION_LIMIT = 60;
|
|
7
7
|
export const UNMANAGED_MARKER = "on disk, unmanaged · adopt it from Installations";
|
|
8
8
|
export const skillKey = (skill) => `skill:${skill.sourceId}:${skill.path}`;
|
|
9
|
-
|
|
9
|
+
/** Keyed on the full selection, so two plugins of one marketplace root stay distinct nodes. */
|
|
10
|
+
export const bundleKey = (entry) => `bundle:${entry.sourceId}:${selectionKey(entry.selection)}`;
|
|
10
11
|
export function truncateDescription(text) {
|
|
11
12
|
return text.length > DESCRIPTION_LIMIT ? `${text.slice(0, DESCRIPTION_LIMIT - 3)}...` : text;
|
|
12
13
|
}
|
|
@@ -30,11 +31,20 @@ export function buildTree(skills, sources = [], bundles = [], activeDescriptors
|
|
|
30
31
|
for (const skill of skills)
|
|
31
32
|
bySource.set(skill.sourceId, [...(bySource.get(skill.sourceId) ?? []), skill]);
|
|
32
33
|
const bundlesBySource = new Map();
|
|
33
|
-
|
|
34
|
+
const seenBundles = new Set();
|
|
35
|
+
for (const bundle of bundles) {
|
|
36
|
+
// Descriptors with identical coordinates are one Package; a duplicate node id would corrupt the rows.
|
|
37
|
+
if (seenBundles.has(bundleKey(bundle)))
|
|
38
|
+
continue;
|
|
39
|
+
seenBundles.add(bundleKey(bundle));
|
|
34
40
|
bundlesBySource.set(bundle.sourceId, [...(bundlesBySource.get(bundle.sourceId) ?? []), bundle]);
|
|
41
|
+
}
|
|
35
42
|
const sourcesTree = [...new Set([...bySource.keys(), ...bundlesBySource.keys()])].map((sourceId) => {
|
|
36
43
|
const members = bySource.get(sourceId) ?? [];
|
|
37
44
|
const sourceBundles = bundlesBySource.get(sourceId) ?? [];
|
|
45
|
+
const variants = new Map();
|
|
46
|
+
for (const skill of members)
|
|
47
|
+
variants.set(skill.name, [...(variants.get(skill.name) ?? []), skill]);
|
|
38
48
|
const owned = packageOwnedSkillMatcher(sourceBundles.map((entry) => entry.descriptor).filter((descriptor) => active.has(descriptor)));
|
|
39
49
|
return {
|
|
40
50
|
id: `source:${sourceId}`,
|
|
@@ -45,10 +55,21 @@ export function buildTree(skills, sources = [], bundles = [], activeDescriptors
|
|
|
45
55
|
data: { kind: "group", label: `Packages (${sourceBundles.length})` },
|
|
46
56
|
children: sourceBundles.map((entry) => ({ id: bundleKey(entry), data: { kind: "bundle", entry } })),
|
|
47
57
|
}]),
|
|
48
|
-
...members.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
...(members.length === 0 ? [] : [{
|
|
59
|
+
id: `skills:${sourceId}`,
|
|
60
|
+
data: { kind: "group", label: `Skills (${members.length})` },
|
|
61
|
+
children: [...variants.values()].map((group) => {
|
|
62
|
+
const variant = group.length > 1;
|
|
63
|
+
const nodes = group.map((skill) => {
|
|
64
|
+
const ownedBy = owned(skill.path);
|
|
65
|
+
return { id: skillKey(skill), data: { kind: "skill", skill, ...(ownedBy === undefined ? {} : { ownedBy }), ...(variant ? { variant } : {}) } };
|
|
66
|
+
});
|
|
67
|
+
// Same-named Skills of one Source differ in content, so each stays its own installable row under one group.
|
|
68
|
+
return variant
|
|
69
|
+
? { id: `variants:${sourceId}:${group[0].name}`, data: { kind: "group", label: `${group[0].name} (${group.length} variants)` }, children: nodes }
|
|
70
|
+
: nodes[0];
|
|
71
|
+
}),
|
|
72
|
+
}]),
|
|
52
73
|
],
|
|
53
74
|
};
|
|
54
75
|
});
|
|
@@ -86,13 +107,29 @@ export function describeRow(row) {
|
|
|
86
107
|
}
|
|
87
108
|
if (data.kind === "app")
|
|
88
109
|
return `App ${data.row.entry.recipe?.name ?? data.row.entry.file} ${data.row.inspection.status}`;
|
|
89
|
-
return `${data.skill.name} ${truncateDescription(data.skill.description)}`;
|
|
110
|
+
return `${data.skill.name} ${truncateDescription(data.skill.description)}${data.variant === true ? ` (${data.skill.path})` : ""}`;
|
|
111
|
+
}
|
|
112
|
+
/** Skill nodes inside a Source's Skills group, directly or within one of its variant groups. */
|
|
113
|
+
function skillNodesOf(roots) {
|
|
114
|
+
const found = [];
|
|
115
|
+
const visit = (node) => {
|
|
116
|
+
if (node.data.kind === "skill")
|
|
117
|
+
found.push(node);
|
|
118
|
+
else if (node.id.startsWith("variants:"))
|
|
119
|
+
node.children?.forEach(visit);
|
|
120
|
+
};
|
|
121
|
+
for (const root of roots) {
|
|
122
|
+
for (const group of root.children ?? [])
|
|
123
|
+
if (group.id.startsWith("skills:"))
|
|
124
|
+
group.children?.forEach(visit);
|
|
125
|
+
}
|
|
126
|
+
return found;
|
|
90
127
|
}
|
|
91
128
|
/** The Skills under the tree's Sources, in display order. */
|
|
92
129
|
export function leavesOf(roots) {
|
|
93
|
-
return roots
|
|
130
|
+
return skillNodesOf(roots).map((node) => node.data.skill);
|
|
94
131
|
}
|
|
95
132
|
/** Skills that can still be installed as loose Skills, excluding active Package-owned rows. */
|
|
96
133
|
export function looseLeavesOf(roots) {
|
|
97
|
-
return roots.
|
|
134
|
+
return skillNodesOf(roots).filter((node) => node.data.ownedBy === undefined).map((node) => node.data.skill);
|
|
98
135
|
}
|
|
@@ -17,6 +17,7 @@ import { useCatalogApps, useCatalogFilter, useCatalogSkills, useInstalledSkills
|
|
|
17
17
|
import { buildTree, leavesOf, openByDefault, skillKey } from "./catalog-tree.js";
|
|
18
18
|
import { theme } from "./theme.js";
|
|
19
19
|
import { useTreeNavigation } from "./tree-navigation.js";
|
|
20
|
+
import { invalidateRetention } from "./view-retention.js";
|
|
20
21
|
import { useLoadingSince, useMarks, useMounted } from "./view-state.js";
|
|
21
22
|
import { computeWindow, useListHeight } from "./window.js";
|
|
22
23
|
import { useKeys } from "./keys.js";
|
|
@@ -39,7 +40,7 @@ function wrappedLineCount(text, columns) {
|
|
|
39
40
|
}
|
|
40
41
|
return lines;
|
|
41
42
|
}
|
|
42
|
-
export function CatalogView({ operations, sourceId, onCapturingChange, environment, listHeight }) {
|
|
43
|
+
export function CatalogView({ operations, sourceId, onCapturingChange, environment, listHeight, retention }) {
|
|
43
44
|
const { columns, rows: terminalRows } = useWindowSize();
|
|
44
45
|
const env = environment ?? NO_ENVIRONMENT;
|
|
45
46
|
const [all, setAll] = useState(sourceId === undefined);
|
|
@@ -52,12 +53,12 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
52
53
|
// Mirrors the action mode synchronously, like the filter, so later keystrokes and the shell never see stale state.
|
|
53
54
|
const actionRef = useRef(BROWSE);
|
|
54
55
|
const filter = useCatalogFilter(actionRef, onCapturingChange);
|
|
55
|
-
const state = useCatalogSkills(operations, sourceId, all, env, reloadKey);
|
|
56
|
+
const state = useCatalogSkills(operations, sourceId, all, env, reloadKey, retention);
|
|
56
57
|
const packages = useMemo(() => packageOperationsOf(env), [env]);
|
|
57
58
|
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
58
|
-
const appCatalogState = useCatalogApps(apps);
|
|
59
|
+
const appCatalogState = useCatalogApps(apps, retention);
|
|
59
60
|
const appCatalog = appCatalogState.state;
|
|
60
|
-
const { installed, installedReady, reloadInstalled } = useInstalledSkills(operations, env, mounted);
|
|
61
|
+
const { installed, installedReady, installedStale, reloadInstalled } = useInstalledSkills(operations, env, mounted, retention);
|
|
61
62
|
const [appMode, setAppMode] = useState();
|
|
62
63
|
const appModeRef = useRef(undefined);
|
|
63
64
|
const setAppAction = (mode) => {
|
|
@@ -104,6 +105,8 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
104
105
|
}
|
|
105
106
|
}, [loading]);
|
|
106
107
|
const loadingSince = useLoadingSince(loading, runningSince ?? resumedSince);
|
|
108
|
+
const refreshing = !loading && ((state.status === "ready" && state.stale === true) || (appCatalog.status === "ready" && appCatalog.stale === true) || installedStale);
|
|
109
|
+
const refreshingSince = useLoadingSince(refreshing);
|
|
107
110
|
const flow = {
|
|
108
111
|
operations,
|
|
109
112
|
env,
|
|
@@ -112,6 +115,7 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
112
115
|
isMounted: () => mounted.current,
|
|
113
116
|
keepMarked: marks.setMarked,
|
|
114
117
|
finish: async (result) => {
|
|
118
|
+
invalidateRetention(retention);
|
|
115
119
|
await reloadInstalled();
|
|
116
120
|
if (!mounted.current)
|
|
117
121
|
return;
|
|
@@ -126,6 +130,7 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
126
130
|
isMounted: () => mounted.current,
|
|
127
131
|
unmark: ids => marks.setMarked(new Set([...marks.markedRef.current].filter(id => !ids.includes(id)))),
|
|
128
132
|
finish: async (result) => {
|
|
133
|
+
invalidateRetention(retention);
|
|
129
134
|
await reloadInstalled();
|
|
130
135
|
if (!mounted.current)
|
|
131
136
|
return;
|
|
@@ -142,6 +147,7 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
142
147
|
setMessage,
|
|
143
148
|
isMounted: () => mounted.current,
|
|
144
149
|
finish: async (result) => {
|
|
150
|
+
invalidateRetention(retention);
|
|
145
151
|
await reloadInstalled();
|
|
146
152
|
if (!mounted.current)
|
|
147
153
|
return;
|
|
@@ -194,5 +200,5 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
194
200
|
// A paged declaration owns the view: rows of the tree above it would not be counted in its height.
|
|
195
201
|
if (isPackageMode(action))
|
|
196
202
|
return _jsx(PackagePanel, { mode: action });
|
|
197
|
-
return (_jsxs(Box, { flexDirection: "column", children: [state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), appCatalog.status === "ready" && appCatalog.error ? _jsxs(Text, { color: theme.error, children: ["Apps unavailable: ", appCatalog.error] }) : null, _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, appCount: visibleApps.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(CatalogSelectionHelp, { row: rows[index], height: helpLines }), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), appMode ? _jsx(AppPanel, { mode: appMode }) : _jsx(ActionPanel, { mode: action })] }));
|
|
203
|
+
return (_jsxs(Box, { flexDirection: "column", children: [refreshing ? _jsx(Working, { label: "Refreshing catalog (last data shown)\u2026", since: refreshingSince }) : null, state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), appCatalog.status === "ready" && appCatalog.error ? _jsxs(Text, { color: theme.error, children: ["Apps unavailable: ", appCatalog.error] }) : null, _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, appCount: visibleApps.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(CatalogSelectionHelp, { row: rows[index], height: helpLines }), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), appMode ? _jsx(AppPanel, { mode: appMode }) : _jsx(ActionPanel, { mode: action })] }));
|
|
198
204
|
}
|
|
@@ -22,15 +22,18 @@ import { busy } from "./working-model.js";
|
|
|
22
22
|
import { useLoadingSince, useMarks, useMounted } from "./view-state.js";
|
|
23
23
|
import { computeWindow, useListHeight } from "./window.js";
|
|
24
24
|
import { useKeys } from "./keys.js";
|
|
25
|
+
import { invalidateRetention } from "./view-retention.js";
|
|
25
26
|
const NO_ENVIRONMENT = {};
|
|
26
27
|
const LEGEND = "adopted: tracked in place · modified: changed on disk";
|
|
27
|
-
export function InstallationsView({ operations, environment, onCapturingChange, listHeight }) {
|
|
28
|
+
export function InstallationsView({ operations, environment, onCapturingChange, listHeight, retention }) {
|
|
28
29
|
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
29
30
|
const height = useListHeight(listHeight, 7);
|
|
30
31
|
const env = environment ?? NO_ENVIRONMENT;
|
|
31
32
|
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
32
33
|
const packages = useMemo(() => packageOperationsOf(env), [env]);
|
|
33
|
-
const [state, setState] = useState({ status: "loading" });
|
|
34
|
+
const [state, setState] = useState(() => retention?.installations ? { status: "ready", data: retention.installations } : { status: "loading" });
|
|
35
|
+
// The data shown comes from an earlier visit until the first reload of this one ends.
|
|
36
|
+
const [stale, setStale] = useState(retention?.installations !== undefined);
|
|
34
37
|
const [mode, setModeState] = useState(BROWSE);
|
|
35
38
|
// Mirrors the mode synchronously so later keystrokes and the shell never see stale state.
|
|
36
39
|
const modeRef = useRef(BROWSE);
|
|
@@ -50,22 +53,30 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
50
53
|
const loadGeneration = useRef(0);
|
|
51
54
|
const reload = useCallback(async () => {
|
|
52
55
|
const generation = ++loadGeneration.current;
|
|
56
|
+
const retained = retention?.generation;
|
|
53
57
|
if (reloads.current++ === 0 && mounted.current)
|
|
54
58
|
setRefreshingSince(Date.now());
|
|
55
59
|
try {
|
|
56
60
|
const data = await loadInstallations(operations, env, apps, packages);
|
|
57
|
-
|
|
61
|
+
// A result that began before an invalidation may predate the change, so it is not kept.
|
|
62
|
+
if (retention && retention.generation === retained)
|
|
63
|
+
retention.installations = data;
|
|
64
|
+
if (mounted.current && generation === loadGeneration.current) {
|
|
58
65
|
setState({ status: "ready", data });
|
|
66
|
+
setStale(false);
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
catch (error) {
|
|
61
|
-
if (mounted.current && generation === loadGeneration.current)
|
|
70
|
+
if (mounted.current && generation === loadGeneration.current) {
|
|
62
71
|
setState({ status: "error", message: errorText(error) });
|
|
72
|
+
setStale(false);
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
finally {
|
|
65
76
|
if (--reloads.current === 0 && mounted.current)
|
|
66
77
|
setRefreshingSince(undefined);
|
|
67
78
|
}
|
|
68
|
-
}, [operations, env, apps, packages, mounted]);
|
|
79
|
+
}, [operations, env, apps, packages, mounted, retention]);
|
|
69
80
|
useEffect(() => {
|
|
70
81
|
void reload();
|
|
71
82
|
}, [reload]);
|
|
@@ -129,6 +140,8 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
129
140
|
const current = modeRef.current;
|
|
130
141
|
if ("startedAt" in current && mounted.current)
|
|
131
142
|
setMode(busy("Refreshing installations…", current));
|
|
143
|
+
// The action changed what is installed, so what was kept no longer describes it.
|
|
144
|
+
invalidateRetention(retention);
|
|
132
145
|
await reload();
|
|
133
146
|
};
|
|
134
147
|
const manage = {
|
|
@@ -197,6 +210,6 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
197
210
|
const markedApps = markedLeaves.filter(node => node.data.kind === "app").length;
|
|
198
211
|
// The legend explains the markers, so it shows only while a rendered row carries one.
|
|
199
212
|
const legendNeeded = listed.some((row) => row.node.data.kind === "installation" && markersOf(row.node.data.row) !== "");
|
|
200
|
-
return (_jsxs(Box, { flexDirection: "column", children: [refreshingSince !== undefined && mode.kind === "browse" ? _jsx(Working, { label: "Refreshing installations
|
|
213
|
+
return (_jsxs(Box, { flexDirection: "column", children: [refreshingSince !== undefined && mode.kind === "browse" ? _jsx(Working, { label: stale ? "Refreshing installations (last data shown)…" : "Refreshing installations…", since: refreshingSince }) : null, ready.global === undefined ? _jsx(Text, { children: "User-global installations are not supported by the configured operations" }) : null, ready.projectError === undefined ? null : _jsxs(Text, { color: theme.error, children: ["Error: ", ready.projectError] }), listed.map((row, offset) => (_jsx(TreeRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id) }, row.node.id))), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), markedApps > 0 ? _jsxs(Text, { color: theme.muted, children: [markedApps, " Apps marked"] }) : null, legendNeeded ? _jsx(Text, { color: theme.muted, children: LEGEND }) : null, checkingSince !== undefined && refreshingSince === undefined && mode.kind === "browse"
|
|
201
214
|
? _jsx(Working, { label: "Checking App status\u2026", since: checkingSince }) : null, _jsx(ModePanel, { mode: mode })] }));
|
|
202
215
|
}
|
|
@@ -66,6 +66,6 @@ export function ManagePanel({ mode }) {
|
|
|
66
66
|
case "confirm-bulk-exposure":
|
|
67
67
|
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.plan.items.map((item) => item.label).join(", "), " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). ", _jsx(DecisionPrompt, { children: "Confirm?" })] }));
|
|
68
68
|
case "busy":
|
|
69
|
-
return _jsx(Working, { label: mode.label, since: mode.startedAt });
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Working, { label: mode.label, since: mode.startedAt }), (mode.done ?? []).length > 0 ? _jsxs(Text, { color: theme.muted, children: [mode.done.length, " finished"] }) : null] }));
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -15,7 +15,7 @@ import { useKeys } from "./keys.js";
|
|
|
15
15
|
import { previewViewport } from "./preview-window.js";
|
|
16
16
|
import { answerYesNo, moveCursor, movePreviewCursor, toggleChoice } from "./mode-keys.js";
|
|
17
17
|
import { BorderPanel, PreviewConfirm, PreviewLines, Working } from "./panel-parts.js";
|
|
18
|
-
import { elideMiddle, fitLines, wrapText } from "./text-wrap.js";
|
|
18
|
+
import { clipText, elideMiddle, fitLines, wrapText } from "./text-wrap.js";
|
|
19
19
|
import { theme } from "./theme.js";
|
|
20
20
|
import { useMirrored, useMounted } from "./view-state.js";
|
|
21
21
|
import { computeWindow, useListHeight } from "./window.js";
|
|
@@ -53,14 +53,16 @@ function rowsFromExport(plan) {
|
|
|
53
53
|
})),
|
|
54
54
|
];
|
|
55
55
|
}
|
|
56
|
+
/** A group header, followed by its children while the group is open; an empty group is not shown. */
|
|
57
|
+
function section(expanded, id, label, depth, children) {
|
|
58
|
+
if (!children.length)
|
|
59
|
+
return [];
|
|
60
|
+
const header = { kind: "group", id, label, depth };
|
|
61
|
+
return expanded.has(id) ? [header, ...children] : [header];
|
|
62
|
+
}
|
|
56
63
|
function exportDisplayRows(rows, expanded) {
|
|
57
64
|
const choices = (block) => rows.filter(row => row.block === block);
|
|
58
|
-
const group = (id, label, depth, children) =>
|
|
59
|
-
if (!children.length)
|
|
60
|
-
return [];
|
|
61
|
-
const header = { kind: "group", id, label, depth };
|
|
62
|
-
return expanded.has(id) ? [header, ...children] : [header];
|
|
63
|
-
};
|
|
65
|
+
const group = (id, label, depth, children) => section(expanded, id, label, depth, children);
|
|
64
66
|
const sourceChildren = [
|
|
65
67
|
...group("group:sources", `Source choices (${choices("sources").length})`, 1, choices("sources")),
|
|
66
68
|
...group("group:skills", `Skills (${choices("skills").length})`, 1, choices("skills")),
|
|
@@ -73,6 +75,30 @@ function exportDisplayRows(rows, expanded) {
|
|
|
73
75
|
...group("group:excluded", `Export notes (${choices("excluded").length})`, 0, choices("excluded")),
|
|
74
76
|
];
|
|
75
77
|
}
|
|
78
|
+
const IMPORT_ADD = "import:add";
|
|
79
|
+
const IMPORT_APPS = "import:apps";
|
|
80
|
+
const importRepoId = (repo) => `import:add:${repo}`;
|
|
81
|
+
/** Additions by Source and App recipes, then what is already there, then what conflicts. */
|
|
82
|
+
function importDisplayRows(rows, expanded) {
|
|
83
|
+
const added = rows.filter(row => row.status === "add");
|
|
84
|
+
const recipes = added.filter(row => row.block === "apps");
|
|
85
|
+
const repos = [...new Set(added.filter(row => row.block !== "apps").map(row => row.source ?? ""))];
|
|
86
|
+
const sources = repos.flatMap(repo => {
|
|
87
|
+
const children = added.filter(row => row.block !== "apps" && (row.source ?? "") === repo);
|
|
88
|
+
return section(expanded, importRepoId(repo), `${repo} (${children.length})`, 1, children);
|
|
89
|
+
});
|
|
90
|
+
const unchanged = rows.filter(row => row.status === "same"), conflicts = rows.filter(row => row.status === "conflict");
|
|
91
|
+
return [
|
|
92
|
+
...section(expanded, IMPORT_ADD, `To import (${added.length})`, 0, [...sources, ...section(expanded, IMPORT_APPS, `App recipes (${recipes.length})`, 1, recipes)]),
|
|
93
|
+
...section(expanded, "import:same", `Unchanged (${unchanged.length})`, 0, unchanged),
|
|
94
|
+
...section(expanded, "import:conflict", `Conflicts (${conflicts.length}) — not selectable`, 0, conflicts),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/** Additions and conflicts are open to read; the unchanged items are what the person already has. */
|
|
98
|
+
function importOpenGroups(rows) {
|
|
99
|
+
const repos = rows.filter(row => row.status === "add" && row.block !== "apps").map(row => importRepoId(row.source ?? ""));
|
|
100
|
+
return new Set([IMPORT_ADD, IMPORT_APPS, "import:conflict", ...repos]);
|
|
101
|
+
}
|
|
76
102
|
function physicalListWindow(rows, rowHeights, selected, height) {
|
|
77
103
|
if (!rows.length)
|
|
78
104
|
return { start: 0, end: 0, indicator: undefined, stickyGroup: false };
|
|
@@ -144,7 +170,7 @@ function planEntries(plan) {
|
|
|
144
170
|
function rowsFromPlan(plan) {
|
|
145
171
|
return planEntries(plan).map((item, index) => ({
|
|
146
172
|
id: `${item.block}:${index}`, block: item.block, index, ordinal: index + 1,
|
|
147
|
-
label:
|
|
173
|
+
label: item.summary, status: item.status, ...(item.group === undefined ? {} : { source: item.group }),
|
|
148
174
|
selectable: item.status === "add",
|
|
149
175
|
}));
|
|
150
176
|
}
|
|
@@ -173,6 +199,12 @@ function selectedImportProfile(profile, entries) {
|
|
|
173
199
|
return parseProfile({ ...profile, format: replicas.length ? PROFILE_FORMAT_V2 : PROFILE_FORMAT,
|
|
174
200
|
sources, skills, apps, packages, replicas });
|
|
175
201
|
}
|
|
202
|
+
/** The rows a list stage shows: the grouped export or import tree, otherwise the flat rows. */
|
|
203
|
+
function listRows(mode, rows, expanded) {
|
|
204
|
+
if (mode === "export-list")
|
|
205
|
+
return exportDisplayRows(rows, expanded);
|
|
206
|
+
return mode === "import-list" ? importDisplayRows(rows, expanded) : rows;
|
|
207
|
+
}
|
|
176
208
|
/** Collect choices, display shared-core previews and confirm portable Profile transfers. */
|
|
177
209
|
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight, existsCheck = pathExists }) {
|
|
178
210
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -208,14 +240,17 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
208
240
|
const diagnostics = useRef([]);
|
|
209
241
|
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
210
242
|
const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
|
|
211
|
-
const hintCursorRow = (mode
|
|
243
|
+
const hintCursorRow = listRows(mode, rows, expandedGroupsRef.current)[cursorRef.current];
|
|
212
244
|
const enterHint = hintCursorRow === undefined ? undefined
|
|
213
245
|
: "kind" in hintCursorRow ? "Enter open"
|
|
214
246
|
: hintCursorRow.block === "excluded" ? "Enter note" : undefined;
|
|
215
247
|
const groupHint = hintCursorRow !== undefined && "kind" in hintCursorRow ? " · ←/→ group" : "";
|
|
248
|
+
// With nothing to choose, a header has no item row to move to, so Enter previews instead of opening it.
|
|
249
|
+
const nothingToImport = !rows.some(row => row.selectable);
|
|
250
|
+
const importEnter = hintCursorRow !== undefined && "kind" in hintCursorRow && !nothingToImport ? "Enter open" : "Enter preview";
|
|
216
251
|
const listingHint = mode === "export-list"
|
|
217
252
|
? `j/k move${groupHint}${enterHint === undefined ? "" : ` · ${enterHint}`} · space toggle · a all · e export · Esc back to menu`
|
|
218
|
-
:
|
|
253
|
+
: `j/k move${groupHint} · space toggle · a all · ${importEnter} · Esc back to menu`;
|
|
219
254
|
const hintText = `${selected.length} selected · ${listingHint}`;
|
|
220
255
|
const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
|
|
221
256
|
const hintRows = Math.max(Math.ceil(stringWidth(hintText) / panelWidth), 1);
|
|
@@ -250,11 +285,9 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
250
285
|
const loadRows = (next, nextMode) => {
|
|
251
286
|
setRows(next);
|
|
252
287
|
select(next.filter(row => row.selectable));
|
|
253
|
-
const open = new Set();
|
|
288
|
+
const open = nextMode === "import-list" ? importOpenGroups(next) : new Set();
|
|
254
289
|
setExpandedGroups(open);
|
|
255
|
-
const firstChoice = nextMode
|
|
256
|
-
? exportDisplayRows(next, open).findIndex(row => !("kind" in row) && row.selectable)
|
|
257
|
-
: 0;
|
|
290
|
+
const firstChoice = listRows(nextMode, next, open).findIndex(row => !("kind" in row) && row.selectable);
|
|
258
291
|
cursorTo(Math.max(firstChoice, 0));
|
|
259
292
|
setMode(nextMode);
|
|
260
293
|
};
|
|
@@ -483,9 +516,9 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
483
516
|
return;
|
|
484
517
|
}
|
|
485
518
|
// export-list and import-list
|
|
486
|
-
const
|
|
487
|
-
const currentRow =
|
|
488
|
-
const next = moveCursor(cursorRef.current,
|
|
519
|
+
const shownRows = listRows(current, rows, expandedGroupsRef.current);
|
|
520
|
+
const currentRow = shownRows[cursorRef.current];
|
|
521
|
+
const next = moveCursor(cursorRef.current, shownRows.length, input, key);
|
|
489
522
|
if (next !== undefined) {
|
|
490
523
|
cursorTo(Math.max(0, next));
|
|
491
524
|
return;
|
|
@@ -503,6 +536,10 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
503
536
|
beginExport();
|
|
504
537
|
return;
|
|
505
538
|
}
|
|
539
|
+
if (key.return && current === "import-list" && nothingToImport) {
|
|
540
|
+
previewImport();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
506
543
|
if (currentRow && "kind" in currentRow && currentRow.kind === "group") {
|
|
507
544
|
if (key.return || key.rightArrow || key.leftArrow) {
|
|
508
545
|
const open = new Set(expandedGroupsRef.current);
|
|
@@ -531,13 +568,13 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
531
568
|
}
|
|
532
569
|
});
|
|
533
570
|
const listing = mode === "export-list" || mode === "import-list";
|
|
534
|
-
const displayRows = mode
|
|
571
|
+
const displayRows = listRows(mode, rows, expandedGroups);
|
|
535
572
|
const rowHeights = displayRows.map(row => {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return wrapText(label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
573
|
+
// A group header is one row, so the sticky header the window repeats costs exactly the row it reserves.
|
|
574
|
+
if ("kind" in row)
|
|
575
|
+
return 1;
|
|
576
|
+
const prefixWidth = row.block === "excluded" ? 2 : stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
|
|
577
|
+
return wrapText(row.label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
541
578
|
});
|
|
542
579
|
const listWindow = physicalListWindow(displayRows, rowHeights, cursor, height);
|
|
543
580
|
const window = listing ? listWindow : computeWindow(lines.length, cursor, height);
|
|
@@ -554,10 +591,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
554
591
|
const open = expandedGroups.has(row.id);
|
|
555
592
|
const prefix = `${marker}${" ".repeat(row.depth * 2)}${open ? "▾" : "▸"} `;
|
|
556
593
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
remainingRows -= label.split("\n").length;
|
|
560
|
-
return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, label] }, row.id);
|
|
594
|
+
remainingRows -= 1;
|
|
595
|
+
return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, clipText(row.label, width)] }, row.id);
|
|
561
596
|
}
|
|
562
597
|
if (row.block === "excluded") {
|
|
563
598
|
const width = Math.max(panelWidth - stringWidth(marker), 1);
|
|
@@ -566,13 +601,15 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
566
601
|
remainingRows -= label.split("\n").length;
|
|
567
602
|
return _jsxs(Text, { color: theme.value, children: [marker, label] }, row.id);
|
|
568
603
|
}
|
|
569
|
-
|
|
570
|
-
const
|
|
604
|
+
// A row the import never changes has no checkbox: unchanged items rest, conflicts warn.
|
|
605
|
+
const box = row.selectable ? `[${selected.some(choice => choice.id === row.id) ? "x" : " "}]` : " ";
|
|
606
|
+
const prefix = `${marker}${box} ${row.ordinal ?? row.index + 1} `;
|
|
571
607
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
572
|
-
const label = fitLines(wrapText(
|
|
608
|
+
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
573
609
|
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
574
610
|
remainingRows -= label.split("\n").length;
|
|
575
|
-
|
|
611
|
+
const color = row.status === "conflict" ? theme.warning : row.status === "same" ? theme.muted : theme.value;
|
|
612
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: prefix }), _jsx(Text, { color: color, children: label })] }, row.id);
|
|
576
613
|
});
|
|
577
614
|
const nameOutcome = resolveExportFileName(text);
|
|
578
615
|
const liveDestination = nameOutcome.kind === "ok"
|
|
@@ -15,6 +15,25 @@ import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
|
15
15
|
import { previewViewport } from "./preview-window.js";
|
|
16
16
|
import { theme } from "./theme.js";
|
|
17
17
|
import { busy } from "./working-model.js";
|
|
18
|
+
/**
|
|
19
|
+
* One group per App name. Invalid recipes, and recipes whose name clashes among those that apply here
|
|
20
|
+
* (they carry an error), stay single rows so nothing is guessed.
|
|
21
|
+
*/
|
|
22
|
+
export function groupRecipeRows(rows) {
|
|
23
|
+
const groups = new Map();
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const key = row.entry.recipe !== undefined && row.entry.error === undefined ? `name:${row.entry.recipe.name}` : `file:${row.entry.file}`;
|
|
26
|
+
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
27
|
+
}
|
|
28
|
+
return [...groups.values()].map(found => {
|
|
29
|
+
const members = [...found.filter(row => row.entry.applicable), ...found.filter(row => !row.entry.applicable)];
|
|
30
|
+
return { primary: members[0], members };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** `windows · approved · <file> (used here)`, one line per recipe of a grouped App. */
|
|
34
|
+
function recipeMemberLine(row) {
|
|
35
|
+
return `${row.entry.recipe?.platform ?? "all platforms"} · ${row.status} · ${row.entry.file}${row.entry.applicable ? " (used here)" : ""}`;
|
|
36
|
+
}
|
|
18
37
|
function recipeApprovalChrome(entry) {
|
|
19
38
|
return {
|
|
20
39
|
title: "Recipe approval · does not install",
|
|
@@ -61,7 +80,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
61
80
|
if (mounted.current)
|
|
62
81
|
setMode({ kind: "browse" });
|
|
63
82
|
};
|
|
64
|
-
const preview = async (entry, status, removeAfterApproval = false) => {
|
|
83
|
+
const preview = async (entry, status, removeAfterApproval = false, members = []) => {
|
|
65
84
|
setError(undefined);
|
|
66
85
|
setBusy("Preparing App recipe preview…");
|
|
67
86
|
try {
|
|
@@ -70,7 +89,10 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
70
89
|
setMode({
|
|
71
90
|
kind: "approve",
|
|
72
91
|
entry, removeAfterApproval, cursor: 0,
|
|
73
|
-
lines:
|
|
92
|
+
lines: [
|
|
93
|
+
...(members.length > 1 ? [`Recipes named ${appName(entry)}:`, ...members.map(row => ` ${recipeMemberLine(row)}`), ""] : []),
|
|
94
|
+
...appRecipePreviewLines(entry, status, steps),
|
|
95
|
+
],
|
|
74
96
|
});
|
|
75
97
|
}
|
|
76
98
|
}
|
|
@@ -198,6 +220,14 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
198
220
|
return true;
|
|
199
221
|
}
|
|
200
222
|
const cancel = () => setMode({ kind: "browse" });
|
|
223
|
+
if (current.kind === "pick") {
|
|
224
|
+
const chosen = /^[1-9]$/u.test(input) ? current.members[Number(input) - 1] : undefined;
|
|
225
|
+
if (chosen)
|
|
226
|
+
void startRemoval(chosen.entry);
|
|
227
|
+
else if (input === "n" || key.escape)
|
|
228
|
+
cancel();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
201
231
|
if (current.kind === "approve") {
|
|
202
232
|
const viewport = previewViewport(current.lines, current.cursor, terminalRows, terminalColumns, recipeApprovalChrome(current.entry));
|
|
203
233
|
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
@@ -265,17 +295,23 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
265
295
|
}, cancel);
|
|
266
296
|
return true;
|
|
267
297
|
}
|
|
268
|
-
const
|
|
269
|
-
if (!
|
|
298
|
+
const group = groupRecipeRows(rows).find(candidate => candidate.primary.entry.file === highlightedFile());
|
|
299
|
+
if (!group)
|
|
270
300
|
return false;
|
|
301
|
+
const row = group.primary;
|
|
271
302
|
if (input === "n")
|
|
272
303
|
startAdd();
|
|
273
304
|
else if (input === "r")
|
|
274
305
|
void reload();
|
|
275
306
|
else if (key.return)
|
|
276
|
-
void preview(row.entry, row.status);
|
|
277
|
-
|
|
278
|
-
|
|
307
|
+
void preview(row.entry, row.status, false, group.members);
|
|
308
|
+
// A grouped App names several files; the chosen one is the only file the removal flow may touch.
|
|
309
|
+
else if (input === "d") {
|
|
310
|
+
if (group.members.length > 1)
|
|
311
|
+
setMode({ kind: "pick", members: group.members });
|
|
312
|
+
else
|
|
313
|
+
void startRemoval(row.entry);
|
|
314
|
+
}
|
|
279
315
|
else
|
|
280
316
|
return false;
|
|
281
317
|
return true;
|
|
@@ -312,7 +348,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
312
348
|
const chrome = recipeApprovalChrome(mode.entry);
|
|
313
349
|
const viewport = previewViewport(mode.lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
314
350
|
return _jsx(PreviewConfirm, { title: chrome.title, lines: mode.lines, cursor: mode.cursor, question: viewport.reachedEnd ? chrome.questions[0] : chrome.questions[1], chrome: chrome });
|
|
315
|
-
})() : null, mode.kind === "uninstall-question" ? (_jsxs(BorderPanel, { title: "Uninstall before recipe removal", children: [_jsxs(Text, { children: ["App ", appName(mode.removal.entry), " is installed or tracked", mode.removal.installedVersion ? ` (${mode.removal.installedVersion})` : "", "."] }), _jsxs(Text, { children: [_jsx(DecisionPrompt, { children: "Uninstall it before removing the recipe?" }), _jsx(Text, { color: theme.legend, children: "(n cancels removal)" })] })] })) : null, mode.kind === "uninstall-confirm" || mode.kind === "uninstall-manual" ? (() => {
|
|
351
|
+
})() : null, mode.kind === "pick" ? (_jsxs(BorderPanel, { title: "Remove which recipe?", children: [mode.members.map((row, index) => _jsxs(Text, { children: [index + 1, " ", recipeMemberLine(row)] }, row.entry.file)), _jsx(Text, { color: theme.legend, children: "Press a number to review that one file for removal \u00B7 n or Esc cancels" })] })) : null, mode.kind === "uninstall-question" ? (_jsxs(BorderPanel, { title: "Uninstall before recipe removal", children: [_jsxs(Text, { children: ["App ", appName(mode.removal.entry), " is installed or tracked", mode.removal.installedVersion ? ` (${mode.removal.installedVersion})` : "", "."] }), _jsxs(Text, { children: [_jsx(DecisionPrompt, { children: "Uninstall it before removing the recipe?" }), _jsx(Text, { color: theme.legend, children: "(n cancels removal)" })] })] })) : null, mode.kind === "uninstall-confirm" || mode.kind === "uninstall-manual" ? (() => {
|
|
316
352
|
const chrome = uninstallChrome(mode);
|
|
317
353
|
const lines = appStepLines(mode.plan);
|
|
318
354
|
const viewport = previewViewport(lines, mode.cursor, terminalRows, terminalColumns, chrome);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
4
5
|
import { initialMode, modeReducer } from "./sources-mode.js";
|
|
5
6
|
import { rowStyle, theme } from "./theme.js";
|
|
6
7
|
import { computeWindow, pageStep, useListHeight } from "./window.js";
|
|
7
8
|
import { useKeys } from "./keys.js";
|
|
8
|
-
import { useSourcesRecipes } from "./sources-recipes.js";
|
|
9
|
+
import { groupRecipeRows, useSourcesRecipes } from "./sources-recipes.js";
|
|
9
10
|
import { BorderPanel, DecisionPrompt, FieldLine, PreviewLines, Working } from "./panel-parts.js";
|
|
10
11
|
import { previewViewport } from "./preview-window.js";
|
|
11
12
|
import { movePreviewCursor } from "./mode-keys.js";
|
|
@@ -123,14 +124,14 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
123
124
|
highlightedFile: () => {
|
|
124
125
|
const rows = rowsRef.current;
|
|
125
126
|
const row = rows[resolveIndex(rows, selectionRef.current)];
|
|
126
|
-
return row?.kind === "app" ? row.
|
|
127
|
+
return row?.kind === "app" ? row.group.primary.entry.file : undefined;
|
|
127
128
|
},
|
|
128
129
|
terminalRows: terminalRows || 24,
|
|
129
130
|
terminalColumns: columns || 80,
|
|
130
131
|
});
|
|
131
132
|
const rows = [
|
|
132
133
|
...sources.map((source) => ({ id: source.id, kind: "skills", source })),
|
|
133
|
-
...recipes.rows.map((
|
|
134
|
+
...groupRecipeRows(recipes.rows).map((group) => ({ id: `recipe:${group.primary.entry.file}`, kind: "app", group })),
|
|
134
135
|
];
|
|
135
136
|
rowsRef.current = rows;
|
|
136
137
|
const selected = resolveIndex(rows, selection);
|
|
@@ -334,13 +335,19 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
334
335
|
return _jsx(Box, { flexDirection: "column", children: recipes.panel });
|
|
335
336
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: [" ", "ID KIND LOCATION / STATUS"] }), builtin ? _jsxs(Text, { children: [" ", _jsx(Text, { color: theme.value, children: builtin.id }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: "skills" }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.warning, children: "included" })] }) : null, rows.length === 0 && recipes.loadingSince === undefined ? _jsx(Text, { children: "No sources or App recipes \u00B7 n to add" }) : null, rows.slice(window.start, window.end).map((row, offset) => {
|
|
336
337
|
const index = window.start + offset;
|
|
337
|
-
const id = row.kind === "skills" ? row.source.id : row.
|
|
338
|
+
const id = row.kind === "skills" ? row.source.id : row.group.primary.entry.recipe?.name ?? row.group.primary.entry.file.split(/[\\/]/u).pop();
|
|
338
339
|
return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", row.kind === "skills" ? (marked.has(row.id) ? "[x] " : "[ ] ") : " ", _jsx(Text, { color: theme.value, children: id }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.kind }), _jsx(Text, { color: theme.legend, children: " " }), row.kind === "skills"
|
|
339
340
|
? _jsx(Text, { color: theme.value, children: row.source.url })
|
|
340
|
-
:
|
|
341
|
+
: _jsx(RecipeStatus, { group: row.group })] }, row.id));
|
|
341
342
|
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.loadingSince !== undefined && mode.kind !== "busy" && !recipes.capturing
|
|
342
343
|
? _jsx(Working, { label: "Loading App recipes\u2026", since: recipes.loadingSince }) : null, recipes.panel, _jsx(Prompt, { mode: mode }), message ? (_jsx(BorderPanel, { title: message.kind === "error" ? "Source action · failed" : "Source action · complete", children: _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) })) : null] }));
|
|
343
344
|
}
|
|
345
|
+
/** Status of the recipe that applies here, led by its platform when it names one; other platforms follow, muted. */
|
|
346
|
+
function RecipeStatus({ group }) {
|
|
347
|
+
const { primary, members } = group;
|
|
348
|
+
const platform = primary.entry.recipe?.platform;
|
|
349
|
+
return (_jsxs(_Fragment, { children: [platform === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: platform }), _jsx(Text, { color: theme.legend, children: " \u00B7 " })] }), primary.entry.error === undefined || primary.entry.recipe === undefined ? null : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: path.basename(primary.entry.file) }), _jsx(Text, { color: theme.legend, children: " \u00B7 " })] }), _jsx(Text, { color: primary.status === "approved" ? theme.success : theme.warning, children: primary.status === "approved" ? "✓ approved" : primary.status }), members.slice(1).map(other => (_jsxs(Text, { color: theme.muted, children: [" ", other.entry.recipe?.platform ?? "all platforms", " (other platform)"] }, other.entry.file)))] }));
|
|
350
|
+
}
|
|
344
351
|
function Prompt({ mode, terminalRows = 24, terminalColumns = 80 }) {
|
|
345
352
|
if (mode.kind === "add-kind")
|
|
346
353
|
return _jsx(Text, { children: "Add: 1 Git Source (skills + Packages) \u00B7 2 App recipe (Esc cancel)" });
|
package/dist/src/tui/updates.js
CHANGED
|
@@ -184,6 +184,7 @@ export async function prepareUpdates(data, selected) {
|
|
|
184
184
|
continue;
|
|
185
185
|
const { context } = loaded;
|
|
186
186
|
const outcome = await previewUpdateCandidates(items, context);
|
|
187
|
+
lines.push(...loaded.projectTarget.map(previewText));
|
|
187
188
|
lines.push(...outcome.plans.flatMap((plan) => describeUpdatePreview(plan, scope, context.projectRoot, "prompt").map(previewText)));
|
|
188
189
|
failures.push(...outcome.failures);
|
|
189
190
|
groups.push({
|