@iseoane/agent-depot 0.5.2 → 0.6.1
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 +47 -0
- package/README.es.md +137 -34
- package/README.md +120 -29
- package/dist/src/app-flow.js +56 -24
- package/dist/src/profile-export.js +72 -9
- package/dist/src/profile-import.js +3 -2
- package/dist/src/profile.js +71 -7
- package/dist/src/tui/app-actions.js +82 -33
- package/dist/src/tui/app.js +19 -14
- package/dist/src/tui/catalog-browse.js +6 -1
- package/dist/src/tui/catalog-panel.js +11 -11
- package/dist/src/tui/catalog-state.js +28 -0
- package/dist/src/tui/catalog-tree.js +22 -5
- package/dist/src/tui/catalog-view.js +59 -16
- package/dist/src/tui/file-picker-model.js +87 -0
- package/dist/src/tui/file-picker.js +142 -0
- package/dist/src/tui/header-layout.js +33 -0
- package/dist/src/tui/installations-panel.js +3 -3
- package/dist/src/tui/installations-rows.js +20 -5
- package/dist/src/tui/installations-tree.js +4 -7
- package/dist/src/tui/installations-view.js +15 -6
- package/dist/src/tui/installations.js +9 -7
- package/dist/src/tui/manage-panel.js +7 -6
- package/dist/src/tui/mode-keys.js +9 -0
- package/dist/src/tui/package-rows.js +17 -5
- package/dist/src/tui/panel-parts.js +119 -6
- package/dist/src/tui/preview-window.js +51 -0
- package/dist/src/tui/profile-view.js +394 -136
- package/dist/src/tui/sources-mode.js +3 -1
- package/dist/src/tui/sources-recipes.js +50 -19
- package/dist/src/tui/sources-view.js +50 -14
- package/dist/src/tui/text-wrap.js +53 -0
- package/dist/src/tui/theme.js +14 -7
- package/dist/src/tui/updates-flow.js +2 -2
- package/dist/src/tui/updates-keys.js +34 -5
- package/dist/src/tui/updates-model.js +2 -0
- package/dist/src/tui/updates-panel.js +40 -15
- package/dist/src/tui/updates-preview.js +65 -0
- package/dist/src/tui/updates-view.js +17 -2
- package/dist/src/tui/updates.js +14 -5
- package/dist/src/tui/window.js +8 -6
- package/package.json +6 -6
- package/skills/doctor-md-agents/README.md +3 -1
- package/skills/doctor-md-skill/README.md +2 -2
package/dist/src/tui/app.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useApp, useWindowSize } from "ink";
|
|
3
3
|
import { useCallback, useRef, useState } from "react";
|
|
4
4
|
import { AGENT_DEPOT_PACKAGE_VERSION } from "../project-manifest.js";
|
|
5
5
|
import { CatalogView } from "./catalog-view.js";
|
|
6
6
|
import { InstallationsView } from "./installations-view.js";
|
|
7
|
+
import { headerTabRows, TABS } from "./header-layout.js";
|
|
7
8
|
import { useKeys } from "./keys.js";
|
|
8
9
|
import { ProfileView } from "./profile-view.js";
|
|
9
10
|
import { SourcesView, sourceRowHints } from "./sources-view.js";
|
|
10
|
-
import {
|
|
11
|
+
import { theme } from "./theme.js";
|
|
11
12
|
import { UpdatesView } from "./updates-view.js";
|
|
12
|
-
const TABS = [
|
|
13
|
-
["sources", "1 Sources"],
|
|
14
|
-
["catalog", "2 Catalog"],
|
|
15
|
-
["installations", "3 Installations"],
|
|
16
|
-
["updates", "4 Updates"],
|
|
17
|
-
["profile", "5 Import/Export"],
|
|
18
|
-
];
|
|
19
13
|
/** Sources derives its own hints from the selected row, so it is excluded here. */
|
|
20
14
|
function hintsOf(view, catalogFocusedOnSource) {
|
|
21
15
|
switch (view) {
|
|
@@ -26,9 +20,13 @@ function hintsOf(view, catalogFocusedOnSource) {
|
|
|
26
20
|
case "profile":
|
|
27
21
|
return "e export · i import · q quit";
|
|
28
22
|
case "updates":
|
|
29
|
-
return "j/k move · space mark · a all · Enter preview · r check again · q quit";
|
|
23
|
+
return "j/k move · space mark · a all · Enter preview · u update · r check again · q quit";
|
|
30
24
|
}
|
|
31
25
|
}
|
|
26
|
+
export function Header({ view, columns }) {
|
|
27
|
+
const innerWidth = Math.max(columns - 4, 1);
|
|
28
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.accent, paddingX: 1, flexDirection: "column", width: columns, children: [_jsxs(Box, { width: innerWidth, justifyContent: "space-between", children: [_jsx(Text, { color: theme.accent, bold: true, children: "AGENT DEPOT" }), AGENT_DEPOT_PACKAGE_VERSION === undefined ? null : _jsxs(Text, { color: theme.value, children: ["v", AGENT_DEPOT_PACKAGE_VERSION] })] }), _jsx(Box, { flexDirection: "column", children: headerTabRows(innerWidth).map((row, rowIndex) => (_jsx(Text, { wrap: "truncate-end", children: row.map(([name, label], tabIndex) => (_jsxs(Text, { color: view === name ? theme.accent : theme.inactive, children: [tabIndex === 0 ? "" : " ", view === name ? "▶ " : "", label] }, name))) }, rowIndex))) })] }));
|
|
29
|
+
}
|
|
32
30
|
export function App({ operations, environment, onExit }) {
|
|
33
31
|
const { exit } = useApp();
|
|
34
32
|
const { columns, rows } = useWindowSize();
|
|
@@ -43,10 +41,15 @@ export function App({ operations, environment, onExit }) {
|
|
|
43
41
|
const [catalogSourceId, setCatalogSourceId] = useState();
|
|
44
42
|
const [sourcesHints, setSourcesHints] = useState(sourceRowHints(undefined));
|
|
45
43
|
const showView = (next) => setView(next);
|
|
46
|
-
useKeys((input) => {
|
|
44
|
+
useKeys((input, key) => {
|
|
47
45
|
if (capturingRef.current)
|
|
48
46
|
return;
|
|
49
|
-
if (
|
|
47
|
+
if (key.tab) {
|
|
48
|
+
const current = TABS.findIndex(([name]) => name === view);
|
|
49
|
+
const direction = key.shift ? -1 : 1;
|
|
50
|
+
showView(TABS[(current + direction + TABS.length) % TABS.length][0]);
|
|
51
|
+
}
|
|
52
|
+
else if (input === "q") {
|
|
50
53
|
onExit?.();
|
|
51
54
|
exit();
|
|
52
55
|
}
|
|
@@ -61,8 +64,10 @@ export function App({ operations, environment, onExit }) {
|
|
|
61
64
|
else if (input === "5")
|
|
62
65
|
showView("profile");
|
|
63
66
|
});
|
|
64
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [
|
|
67
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { view: view, columns: columns }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: view === "sources" ? (_jsx(SourcesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, onHintsChange: setSourcesHints, onOpenCatalog: (source) => {
|
|
65
68
|
setCatalogSourceId(source.id);
|
|
66
69
|
setView("catalog");
|
|
67
|
-
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange })) }), _jsx(Text, { color: theme.muted, children: capturing
|
|
70
|
+
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange })) }), _jsx(Text, { color: theme.muted, children: capturing
|
|
71
|
+
? columns < 64 ? "Enter submit · Esc cancel" : "Enter submit · Esc cancel · y/n confirm · space toggle"
|
|
72
|
+
: view === "sources" ? sourcesHints : hintsOf(view, catalogSourceId !== undefined) })] }));
|
|
68
73
|
}
|
|
@@ -47,6 +47,11 @@ function markRepository(context, row, visible) {
|
|
|
47
47
|
/** `i`: installs the highlighted bundle, or the marked Skills that are listed, or the highlighted one. */
|
|
48
48
|
function chooseHosts(context, row) {
|
|
49
49
|
const data = row.node.data;
|
|
50
|
+
if (data.kind === "app") {
|
|
51
|
+
context.setMessage(undefined);
|
|
52
|
+
context.startApp(data.row.entry);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
50
55
|
// A bundle row is not markable, so it acts on itself and never picks up a Skill mark.
|
|
51
56
|
if (data.kind === "bundle") {
|
|
52
57
|
context.setMessage(undefined);
|
|
@@ -87,6 +92,6 @@ export function handleCatalogBrowseKey(context, input, key) {
|
|
|
87
92
|
markRow(context, row);
|
|
88
93
|
else if (input === "a")
|
|
89
94
|
markRepository(context, row, snapshot.visible);
|
|
90
|
-
else if (input === "i" && context.ready
|
|
95
|
+
else if (input === "i" && row && (context.ready || row.node.data.kind === "app"))
|
|
91
96
|
chooseHosts(context, row);
|
|
92
97
|
}
|
|
@@ -6,12 +6,12 @@ import { skillKey } from "./catalog-tree.js";
|
|
|
6
6
|
import { hasUnmanagedCopy } from "./catalog-installs.js";
|
|
7
7
|
import { describeRow, truncateDescription, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
8
8
|
import { isPackageMode, PackagePanel } from "./package-actions.js";
|
|
9
|
-
import { HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
9
|
+
import { BorderPanel, DecisionPrompt, HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
10
10
|
import { rowStyle } from "./theme.js";
|
|
11
11
|
import { theme } from "./theme.js";
|
|
12
12
|
/** Scope and counts, the filter line, and why the list may be empty. */
|
|
13
|
-
export function CatalogHeader({ scope, listed, installable, total, filter }) {
|
|
14
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Scope: ", scope, " ", listed, " of ", installable] }), filter.editing || filter.query !== "" ? (_jsxs(Text, { children: ["Filter: ", filter.query, filter.editing ? "_" : ""] })) : null, total === 0 ? _jsx(Text, { children: "No skills" }) : null, total > 0 && installable === 0 ? _jsx(Text, { children: "All skills are installed" }) : null, installable > 0 && listed === 0 ? _jsx(Text, { children: "No matching skills" }) : null] }));
|
|
13
|
+
export function CatalogHeader({ scope, listed, installable, total, appCount, filter }) {
|
|
14
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.muted, children: ["Scope: ", scope, " ", listed, " of ", installable, " Skills \u00B7 ", appCount, " Apps"] }), filter.editing || filter.query !== "" ? (_jsxs(Text, { children: ["Filter: ", filter.query, filter.editing ? "_" : ""] })) : null, total === 0 ? _jsx(Text, { children: "No skills" }) : null, total > 0 && installable === 0 ? _jsx(Text, { children: "All skills are installed" }) : null, installable > 0 && listed === 0 ? _jsx(Text, { children: "No matching skills" }) : null] }));
|
|
15
15
|
}
|
|
16
16
|
/** One line of the Catalog tree: cursor, indent, mark box, label and the ownership markers. */
|
|
17
17
|
export function CatalogRowLine({ row, selected, marked, installed }) {
|
|
@@ -20,7 +20,7 @@ export function CatalogRowLine({ row, selected, marked, installed }) {
|
|
|
20
20
|
const ownedBy = data.kind === "skill" ? data.ownedBy : undefined;
|
|
21
21
|
return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group : undefined, wrap: "truncate-end", children: [selected ? "> " : " ", " ".repeat(row.depth), data.kind === "skill" ? marked ? "[x] " : "[ ] " : "", data.kind === "skill" && ownedBy !== undefined
|
|
22
22
|
? _jsxs(_Fragment, { children: [data.skill.name, _jsxs(Text, { color: theme.marker, children: [" ", "[", packageOwnerBadge(ownedBy), "]"] }), " ", truncateDescription(data.skill.description)] })
|
|
23
|
-
: describeRow(row), data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
|
|
23
|
+
: data.kind === "app" ? `App ${data.row.entry.recipe?.name ?? data.row.entry.file}` : describeRow(row), data.kind === "app" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: data.row.inspection.status === "not installed" ? theme.warning : theme.value, children: data.row.inspection.status })] })) : null, data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
|
|
24
24
|
? _jsxs(Text, { color: theme.marker, children: [" ", UNMANAGED_MARKER] })
|
|
25
25
|
: null] }));
|
|
26
26
|
}
|
|
@@ -37,23 +37,23 @@ export function ActionPanel({ mode }) {
|
|
|
37
37
|
case "browse":
|
|
38
38
|
return null;
|
|
39
39
|
case "host":
|
|
40
|
-
return (_jsxs(
|
|
40
|
+
return (_jsxs(BorderPanel, { title: `Skill selection · ${mode.skills.length} selected`, children: [_jsxs(Text, { children: ["Install ", describeSkills(mode.skills), ". Host (space/1-4 toggle \u00B7 j/k move \u00B7 Enter continue \u00B7 Esc cancel)"] }), _jsx(HostChecklist, { choices: HOST_CHOICES, cursor: mode.cursor, selected: mode.selected })] }));
|
|
41
41
|
case "scope":
|
|
42
|
-
return
|
|
42
|
+
return _jsx(BorderPanel, { title: "Skill installation scope", children: _jsxs(Text, { children: [SCOPE_CHOICES.map((scope, i) => `${i + 1} ${scope}`).join(" "), " (Esc cancel)"] }) });
|
|
43
43
|
case "version":
|
|
44
|
-
return _jsx(Text, { children: "
|
|
44
|
+
return _jsx(BorderPanel, { title: "Skill version policy", children: _jsx(Text, { children: "1 latest 2 fixed (Esc cancel)" }) });
|
|
45
45
|
case "fixed":
|
|
46
|
-
return
|
|
46
|
+
return _jsx(BorderPanel, { title: "Fixed Skill version", children: _jsxs(Text, { children: [mode.value, "_ (Enter confirm, Esc cancel)"] }) });
|
|
47
47
|
case "confirm-install": {
|
|
48
48
|
const ready = mode.items.filter((item) => item.prepared !== undefined);
|
|
49
49
|
const single = mode.items.length === 1;
|
|
50
|
-
return (_jsxs(
|
|
50
|
+
return (_jsxs(BorderPanel, { title: `Skill install preview · ${ready.length} ready`, children: [mode.items.map(({ skill, prepared, error }) => prepared === undefined ? (_jsxs(Text, { color: theme.error, children: ["Cannot install ", skill.name, ": ", error] }, skillKey(skill))) : (_jsxs(Box, { flexDirection: "column", children: [single ? null : _jsx(Text, { color: theme.group, children: skill.name }), _jsx(PreviewLines, { lines: prepared.preview })] }, skillKey(skill)))), ready.some((item) => item.prepared?.runsExternalCommand) ? _jsx(Text, { color: theme.warning, children: "This install runs an external command (see above)." }) : null, _jsxs(DecisionPrompt, { children: ["Install ", describeSkills(ready.map((item) => item.skill)), "?"] })] }));
|
|
51
51
|
}
|
|
52
52
|
case "confirm-exposure": {
|
|
53
53
|
const names = mode.items.filter((item) => item.prepared?.additionalHostExposure).map((item) => item.skill.name);
|
|
54
|
-
return (_jsxs(Text, { color: theme.warning, children: [names.length === 1 ? `An identical ${names[0]} already exists` : `Identical copies of ${names.join(", ")} already exist`, "; adding the missing Host location needs separate confirmation (
|
|
54
|
+
return (_jsx(BorderPanel, { title: `Skill Host exposure · ${names.length} confirmation`, children: _jsxs(Text, { color: theme.warning, children: [names.length === 1 ? `An identical ${names[0]} already exists` : `Identical copies of ${names.join(", ")} already exist`, "; adding the missing Host location needs separate confirmation. ", _jsx(DecisionPrompt, { children: "Confirm?" })] }) }));
|
|
55
55
|
}
|
|
56
56
|
case "busy":
|
|
57
|
-
return _jsx(Text, { children: mode.label });
|
|
57
|
+
return _jsx(BorderPanel, { title: "Skill install", children: _jsx(Text, { children: mode.label }) });
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -87,6 +87,34 @@ export function useCatalogSkills(operations, sourceId, all, env, reloadKey = 0)
|
|
|
87
87
|
}, [operations, sourceId, all, env, reloadKey]);
|
|
88
88
|
return state;
|
|
89
89
|
}
|
|
90
|
+
export function useCatalogApps(apps) {
|
|
91
|
+
const [state, setState] = useState({ status: "loading" });
|
|
92
|
+
const generation = useRef(0);
|
|
93
|
+
const reload = useCallback(async () => {
|
|
94
|
+
const current = ++generation.current;
|
|
95
|
+
setState({ status: "loading" });
|
|
96
|
+
try {
|
|
97
|
+
const rows = await Promise.all((await apps.load()).filter(entry => entry.applicable).map(async (entry) => ({
|
|
98
|
+
entry,
|
|
99
|
+
inspection: await apps.inspect(entry),
|
|
100
|
+
})));
|
|
101
|
+
if (generation.current === current)
|
|
102
|
+
setState({
|
|
103
|
+
status: "ready",
|
|
104
|
+
rows: rows.filter(row => row.inspection.status === "not installed"),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (generation.current === current)
|
|
109
|
+
setState({ status: "ready", rows: [], error: error instanceof Error ? error.message : String(error) });
|
|
110
|
+
}
|
|
111
|
+
}, [apps]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
void reload();
|
|
114
|
+
return () => { generation.current += 1; };
|
|
115
|
+
}, [reload]);
|
|
116
|
+
return { state, reload };
|
|
117
|
+
}
|
|
90
118
|
/** The Skills already installed, loaded on mount and on demand. */
|
|
91
119
|
export function useInstalledSkills(operations, env, mounted) {
|
|
92
120
|
const [installed, setInstalled] = useState(NO_INSTALLED);
|
|
@@ -23,7 +23,7 @@ export function sourceLabel(source) {
|
|
|
23
23
|
return url.hostname === "github.com" ? repository : `${url.host}/${repository}`;
|
|
24
24
|
}
|
|
25
25
|
/** Skills and their Source's Packages, in first-seen order; a Source with neither never appears. */
|
|
26
|
-
export function buildTree(skills, sources = [], bundles = [], activeDescriptors = []) {
|
|
26
|
+
export function buildTree(skills, sources = [], bundles = [], activeDescriptors = [], apps = []) {
|
|
27
27
|
const labels = new Map(sources.map((source) => [source.id, sourceLabel(source)]));
|
|
28
28
|
const active = new Set(activeDescriptors);
|
|
29
29
|
const bySource = new Map();
|
|
@@ -32,7 +32,7 @@ export function buildTree(skills, sources = [], bundles = [], activeDescriptors
|
|
|
32
32
|
const bundlesBySource = new Map();
|
|
33
33
|
for (const bundle of bundles)
|
|
34
34
|
bundlesBySource.set(bundle.sourceId, [...(bundlesBySource.get(bundle.sourceId) ?? []), bundle]);
|
|
35
|
-
|
|
35
|
+
const sourcesTree = [...new Set([...bySource.keys(), ...bundlesBySource.keys()])].map((sourceId) => {
|
|
36
36
|
const members = bySource.get(sourceId) ?? [];
|
|
37
37
|
const sourceBundles = bundlesBySource.get(sourceId) ?? [];
|
|
38
38
|
const owned = packageOwnedSkillMatcher(sourceBundles.map((entry) => entry.descriptor).filter((descriptor) => active.has(descriptor)));
|
|
@@ -52,13 +52,28 @@ export function buildTree(skills, sources = [], bundles = [], activeDescriptors
|
|
|
52
52
|
],
|
|
53
53
|
};
|
|
54
54
|
});
|
|
55
|
+
return apps.length === 0 ? sourcesTree : [...sourcesTree, {
|
|
56
|
+
id: "apps:user-global",
|
|
57
|
+
data: { kind: "group", label: `Apps (user-global) (${apps.length})` },
|
|
58
|
+
children: apps.map((row) => ({ id: `app:${row.entry.file}`, data: { kind: "app", row } })),
|
|
59
|
+
}];
|
|
55
60
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
61
|
+
function allExpandableIds(roots) {
|
|
62
|
+
const ids = new Set();
|
|
63
|
+
const visit = (nodes) => {
|
|
64
|
+
for (const node of nodes) {
|
|
65
|
+
if ((node.children?.length ?? 0) === 0)
|
|
66
|
+
continue;
|
|
67
|
+
ids.add(node.id);
|
|
68
|
+
visit(node.children);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
visit(roots);
|
|
72
|
+
return ids;
|
|
58
73
|
}
|
|
59
74
|
/** Filtering opens matching Sources; otherwise all groups start collapsed. */
|
|
60
75
|
export function openByDefault(nodes, query) {
|
|
61
|
-
return query === "" ? defaultExpanded() :
|
|
76
|
+
return query === "" ? defaultExpanded() : allExpandableIds(nodes);
|
|
62
77
|
}
|
|
63
78
|
export function describeRow(row) {
|
|
64
79
|
const { data } = row.node;
|
|
@@ -69,6 +84,8 @@ export function describeRow(row) {
|
|
|
69
84
|
if (data.kind === "bundle") {
|
|
70
85
|
return `Package ${data.entry.descriptor.name} (${data.entry.descriptor.host}) ${packageVersionLabel(data.entry.descriptor.version)}`;
|
|
71
86
|
}
|
|
87
|
+
if (data.kind === "app")
|
|
88
|
+
return `App ${data.row.entry.recipe?.name ?? data.row.entry.file} ${data.row.inspection.status}`;
|
|
72
89
|
return `${data.skill.name} ${truncateDescription(data.skill.description)}`;
|
|
73
90
|
}
|
|
74
91
|
/** The Skills under the tree's Sources, in display order. */
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createAppOperations } from "../app-flow.js";
|
|
4
5
|
import { BROWSE } from "./catalog-actions.js";
|
|
5
6
|
import { handleCatalogBrowseKey } from "./catalog-browse.js";
|
|
6
7
|
import { filterSkills } from "./catalog-filter.js";
|
|
7
8
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
8
9
|
import { isInstalled } from "./catalog-installs.js";
|
|
10
|
+
import { AppPanel, handleAppKey, startAppAction } from "./app-actions.js";
|
|
9
11
|
import { ActionPanel, CatalogHeader, CatalogRowLine, CatalogSelectionHelp } from "./catalog-panel.js";
|
|
10
12
|
import { packageOwnedSkillNotice } from "./catalog-ownership.js";
|
|
11
13
|
import { packageOperationsOf } from "./environment.js";
|
|
12
14
|
import { handlePackageKey, isPackageMode, startPackageAction } from "./package-actions.js";
|
|
13
15
|
import { ListFooter } from "./panel-parts.js";
|
|
14
|
-
import { useCatalogFilter, useCatalogSkills, useInstalledSkills } from "./catalog-state.js";
|
|
16
|
+
import { useCatalogApps, useCatalogFilter, useCatalogSkills, useInstalledSkills } from "./catalog-state.js";
|
|
15
17
|
import { buildTree, leavesOf, openByDefault, skillKey } from "./catalog-tree.js";
|
|
16
18
|
import { theme } from "./theme.js";
|
|
17
19
|
import { useTreeNavigation } from "./tree-navigation.js";
|
|
@@ -38,7 +40,7 @@ function wrappedLineCount(text, columns) {
|
|
|
38
40
|
return lines;
|
|
39
41
|
}
|
|
40
42
|
export function CatalogView({ operations, sourceId, onCapturingChange, environment, listHeight }) {
|
|
41
|
-
const { columns } = useWindowSize();
|
|
43
|
+
const { columns, rows: terminalRows } = useWindowSize();
|
|
42
44
|
const env = environment ?? NO_ENVIRONMENT;
|
|
43
45
|
const [all, setAll] = useState(sourceId === undefined);
|
|
44
46
|
const mounted = useMounted();
|
|
@@ -52,7 +54,17 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
52
54
|
const filter = useCatalogFilter(actionRef, onCapturingChange);
|
|
53
55
|
const state = useCatalogSkills(operations, sourceId, all, env, reloadKey);
|
|
54
56
|
const packages = useMemo(() => packageOperationsOf(env), [env]);
|
|
57
|
+
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
58
|
+
const appCatalogState = useCatalogApps(apps);
|
|
59
|
+
const appCatalog = appCatalogState.state;
|
|
55
60
|
const { installed, installedReady, reloadInstalled } = useInstalledSkills(operations, env, mounted);
|
|
61
|
+
const [appMode, setAppMode] = useState();
|
|
62
|
+
const appModeRef = useRef(undefined);
|
|
63
|
+
const setAppAction = (mode) => {
|
|
64
|
+
appModeRef.current = mode;
|
|
65
|
+
onCapturingChange?.(mode !== undefined || filter.filterRef.current.editing || actionRef.current.kind !== "browse");
|
|
66
|
+
setAppMode(mode);
|
|
67
|
+
};
|
|
56
68
|
const setAction = (next) => {
|
|
57
69
|
actionRef.current = next;
|
|
58
70
|
onCapturingChange?.(filter.filterRef.current.editing || next.kind !== "browse");
|
|
@@ -61,7 +73,13 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
61
73
|
const skills = state.status === "ready" ? state.skills : [];
|
|
62
74
|
const installable = useMemo(() => skills.filter((skill) => !isInstalled(skill, installed)), [state, installed]);
|
|
63
75
|
const visible = useMemo(() => filterSkills(installable, filter.filter.query), [installable, filter.filter.query]);
|
|
64
|
-
const
|
|
76
|
+
const visibleApps = useMemo(() => {
|
|
77
|
+
if (appCatalog.status !== "ready")
|
|
78
|
+
return [];
|
|
79
|
+
const query = filter.filter.query.trim().toLowerCase();
|
|
80
|
+
return query === "" ? appCatalog.rows : appCatalog.rows.filter(row => (row.entry.recipe?.name ?? row.entry.file).toLowerCase().includes(query));
|
|
81
|
+
}, [appCatalog, filter.filter.query]);
|
|
82
|
+
const roots = useMemo(() => buildTree(visible, state.status === "ready" ? state.sources : [], state.status === "ready" ? state.bundles : [], state.status === "ready" ? state.activeBundles : [], visibleApps), [visible, state, visibleApps]);
|
|
65
83
|
const navigation = useTreeNavigation(roots, (nodes) => openByDefault(nodes, filter.queryRef.current));
|
|
66
84
|
// Discovering again starts the list over.
|
|
67
85
|
const { restart } = navigation;
|
|
@@ -86,6 +104,23 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
86
104
|
setAction(BROWSE);
|
|
87
105
|
},
|
|
88
106
|
};
|
|
107
|
+
const appManage = {
|
|
108
|
+
setMode: setAppAction,
|
|
109
|
+
browse: () => setAppAction(undefined),
|
|
110
|
+
setMessage,
|
|
111
|
+
isMounted: () => mounted.current,
|
|
112
|
+
unmark: ids => marks.setMarked(new Set([...marks.markedRef.current].filter(id => !ids.includes(id)))),
|
|
113
|
+
finish: async (result) => {
|
|
114
|
+
await reloadInstalled();
|
|
115
|
+
if (!mounted.current)
|
|
116
|
+
return;
|
|
117
|
+
await appCatalogState.reload();
|
|
118
|
+
if (!mounted.current)
|
|
119
|
+
return;
|
|
120
|
+
setMessage(result);
|
|
121
|
+
setAppAction(undefined);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
89
124
|
const packageHost = {
|
|
90
125
|
setMode: setAction,
|
|
91
126
|
browse: () => setAction(BROWSE),
|
|
@@ -107,29 +142,37 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
107
142
|
marks,
|
|
108
143
|
filter,
|
|
109
144
|
canWiden: sourceId !== undefined,
|
|
110
|
-
ready: state.status === "ready",
|
|
145
|
+
ready: state.status === "ready" && appCatalog.status === "ready",
|
|
111
146
|
toggleAll: () => setAll((value) => !value),
|
|
112
147
|
startPackage: (target) => void startPackageAction(packageContext, target, "i"),
|
|
148
|
+
startApp: (entry) => void startAppAction({ apps, manage: appManage, terminalRows: terminalRows || 24, terminalColumns: columns || 80 }, [entry], "i"),
|
|
113
149
|
setMessage,
|
|
114
150
|
setAction,
|
|
115
151
|
};
|
|
116
152
|
useKeys((input, key) => {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
else
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
const runningAppMode = appModeRef.current;
|
|
154
|
+
if (runningAppMode !== undefined)
|
|
155
|
+
handleAppKey({ apps, manage: appManage, terminalRows: terminalRows || 24, terminalColumns: columns || 80 }, runningAppMode, input, key);
|
|
156
|
+
else {
|
|
157
|
+
const mode = actionRef.current;
|
|
158
|
+
if (isPackageMode(mode))
|
|
159
|
+
handlePackageKey(packageContext, mode, input, key);
|
|
160
|
+
else if (mode.kind !== "browse")
|
|
161
|
+
handleCatalogActionKey(flow, mode, input, key);
|
|
162
|
+
else if (filter.filterRef.current.editing)
|
|
163
|
+
filter.handleEditKey(input, key, navigation.restart);
|
|
164
|
+
else
|
|
165
|
+
handleCatalogBrowseKey(browse, input, key);
|
|
166
|
+
}
|
|
126
167
|
});
|
|
127
168
|
const scope = all || sourceId === undefined ? "all sources" : sourceId;
|
|
128
|
-
if (state.status === "loading" || (state.status === "ready" && !installedReady))
|
|
129
|
-
return _jsx(Text, { children: "Loading
|
|
169
|
+
if (state.status === "loading" || appCatalog.status === "loading" || (state.status === "ready" && !installedReady))
|
|
170
|
+
return _jsx(Text, { children: "Loading catalog..." });
|
|
130
171
|
if (state.status === "error")
|
|
131
172
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
132
173
|
const window = computeWindow(rows.length, index, height);
|
|
133
174
|
const selectedCount = leavesOf(roots).filter((skill) => marks.marked.has(skillKey(skill))).length;
|
|
134
|
-
|
|
175
|
+
if (appMode !== undefined)
|
|
176
|
+
return _jsx(AppPanel, { mode: appMode });
|
|
177
|
+
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 })] }));
|
|
135
178
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { lstat, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/** The portable Profile artifact extension; import shows only these files until `f`. */
|
|
4
|
+
export const PROFILE_EXTENSION = ".json";
|
|
5
|
+
/** The pre-filled export file name; `Ctrl+U` clears it and a missing extension gains `.json`. */
|
|
6
|
+
export const DEFAULT_PROFILE_FILE_NAME = "agent-depot-profile.json";
|
|
7
|
+
/** The directory an export or import browser opens in. Tests inject `startDirectory`. */
|
|
8
|
+
export function pickerStartDirectory(environment) {
|
|
9
|
+
return path.resolve(environment.startDirectory ?? environment.projectRoot ?? process.cwd());
|
|
10
|
+
}
|
|
11
|
+
export function isProfileFileName(name) {
|
|
12
|
+
return path.extname(name).toLowerCase() === PROFILE_EXTENSION;
|
|
13
|
+
}
|
|
14
|
+
/** Directories first, then files, each in code-point order, so the list is stable across platforms. */
|
|
15
|
+
export function sortDirectoryEntries(entries) {
|
|
16
|
+
return [...entries].sort((left, right) => {
|
|
17
|
+
if (left.kind !== right.kind)
|
|
18
|
+
return left.kind === "directory" ? -1 : 1;
|
|
19
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Read one directory into the picker's listing shape; symlinked directories are followed. */
|
|
23
|
+
export async function readDirectoryListing(directory) {
|
|
24
|
+
const dirents = await readdir(directory, { withFileTypes: true });
|
|
25
|
+
const entries = [];
|
|
26
|
+
for (const dirent of dirents) {
|
|
27
|
+
let isDirectory = dirent.isDirectory();
|
|
28
|
+
if (!isDirectory && dirent.isSymbolicLink()) {
|
|
29
|
+
try {
|
|
30
|
+
isDirectory = (await stat(path.join(directory, dirent.name))).isDirectory();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
isDirectory = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
entries.push({ name: dirent.name, kind: isDirectory ? "directory" : "file" });
|
|
37
|
+
}
|
|
38
|
+
const parent = path.dirname(directory);
|
|
39
|
+
return { directory, parent: parent === directory ? undefined : parent, entries: sortDirectoryEntries(entries) };
|
|
40
|
+
}
|
|
41
|
+
/** The rows a listing shows: folders for both purposes, files only for `file`, filtered by `f`. */
|
|
42
|
+
export function pickerEntries(listing, purpose, showAllFiles) {
|
|
43
|
+
return listing.entries.filter(entry => {
|
|
44
|
+
if (entry.kind === "directory")
|
|
45
|
+
return true;
|
|
46
|
+
if (purpose === "directory")
|
|
47
|
+
return false;
|
|
48
|
+
return showAllFiles || isProfileFileName(entry.name);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate a typed file name and give it a JSON extension when it has none.
|
|
53
|
+
* Separators and the dot directories are refused so the name stays inside the chosen folder.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveExportFileName(input) {
|
|
56
|
+
if (input === "")
|
|
57
|
+
return { kind: "invalid", message: "Enter a file name." };
|
|
58
|
+
if (input === "." || input === "..")
|
|
59
|
+
return { kind: "invalid", message: "A file name cannot be . or .." };
|
|
60
|
+
if (input.includes("/") || input.includes("\\"))
|
|
61
|
+
return { kind: "invalid", message: "A file name cannot contain / or \\." };
|
|
62
|
+
if (path.extname(input) === "")
|
|
63
|
+
return { kind: "ok", fileName: `${input}${PROFILE_EXTENSION}`, appendedExtension: true };
|
|
64
|
+
return { kind: "ok", fileName: input, appendedExtension: false };
|
|
65
|
+
}
|
|
66
|
+
/** The full destination path, shown live in the name editor and in the confirmation. */
|
|
67
|
+
export function exportDestination(directory, fileName) {
|
|
68
|
+
return path.join(directory, fileName);
|
|
69
|
+
}
|
|
70
|
+
/** A note when the name keeps an extension other than `.json`; `undefined` when nothing needs explaining. */
|
|
71
|
+
export function fileNameNotice(fileName) {
|
|
72
|
+
const extension = path.extname(fileName);
|
|
73
|
+
if (extension === "" || extension.toLowerCase() === PROFILE_EXTENSION)
|
|
74
|
+
return undefined;
|
|
75
|
+
return `Keeping the ${extension} extension; import lists ${PROFILE_EXTENSION} files by default; press f to show all files.`;
|
|
76
|
+
}
|
|
77
|
+
export async function pathExists(target) {
|
|
78
|
+
try {
|
|
79
|
+
await lstat(target);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
84
|
+
return false;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Text, useWindowSize } from "ink";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { pickerEntries, readDirectoryListing, PROFILE_EXTENSION } from "./file-picker-model.js";
|
|
6
|
+
import { useKeys } from "./keys.js";
|
|
7
|
+
import { moveCursor } from "./mode-keys.js";
|
|
8
|
+
import { BorderPanel } from "./panel-parts.js";
|
|
9
|
+
import { clipText, wrapText } from "./text-wrap.js";
|
|
10
|
+
import { theme } from "./theme.js";
|
|
11
|
+
import { useMirrored, useMounted } from "./view-state.js";
|
|
12
|
+
import { computeWindow, useListHeight } from "./window.js";
|
|
13
|
+
/**
|
|
14
|
+
* A picker callback changes the enclosing view's stage. Defer it so the view's own key
|
|
15
|
+
* handler, which runs in the same event, still sees the picker stage and ignores the key.
|
|
16
|
+
*/
|
|
17
|
+
function settle(action) {
|
|
18
|
+
queueMicrotask(action);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A keyboard-only directory and file browser. It owns its keys while mounted, so the
|
|
22
|
+
* enclosing view must ignore its own bindings in the picker's stages. Each read carries a
|
|
23
|
+
* request generation, so a slow listing never overwrites a newer directory or an unmount.
|
|
24
|
+
*/
|
|
25
|
+
export function FilePicker({ purpose, startDirectory, notice, onCancel, onChoose, listHeight, readDirectory = readDirectoryListing }) {
|
|
26
|
+
const [state, setState] = useState({ status: "loading", directory: startDirectory });
|
|
27
|
+
const [cursor, cursorRef, cursorTo] = useMirrored(0);
|
|
28
|
+
const [showAllFiles, showAllFilesRef, setShowAllFiles] = useMirrored(false);
|
|
29
|
+
const [showFullPath, setShowFullPath] = useState(false);
|
|
30
|
+
const requestId = useRef(0);
|
|
31
|
+
const mounted = useMounted();
|
|
32
|
+
const { columns } = useWindowSize();
|
|
33
|
+
const panelWidth = Math.max((columns || 80) - 4, 1);
|
|
34
|
+
const navigate = useCallback((directory) => {
|
|
35
|
+
const id = ++requestId.current;
|
|
36
|
+
cursorTo(0);
|
|
37
|
+
setState({ status: "loading", directory });
|
|
38
|
+
void readDirectory(directory).then(listing => {
|
|
39
|
+
if (!mounted.current || requestId.current !== id)
|
|
40
|
+
return;
|
|
41
|
+
setState({ status: "ready", directory: listing.directory, parent: listing.parent, entries: listing.entries });
|
|
42
|
+
}, (error) => {
|
|
43
|
+
if (!mounted.current || requestId.current !== id)
|
|
44
|
+
return;
|
|
45
|
+
setState({ status: "error", directory, message: error instanceof Error ? error.message : String(error) });
|
|
46
|
+
});
|
|
47
|
+
}, [cursorTo, mounted, readDirectory]);
|
|
48
|
+
useEffect(() => { navigate(startDirectory); }, [navigate, startDirectory]);
|
|
49
|
+
const entries = state.status === "ready" ? pickerEntries(state, purpose, showAllFiles) : [];
|
|
50
|
+
const rows = useMemo(() => {
|
|
51
|
+
const parent = state.status === "ready" && state.parent !== undefined ? [{ kind: "parent" }] : [];
|
|
52
|
+
const listing = entries.map(entry => ({ kind: "entry", entry }));
|
|
53
|
+
const useFolder = state.status === "ready" && purpose === "directory" ? [{ kind: "use-folder" }] : [];
|
|
54
|
+
return [...useFolder, ...parent, ...listing];
|
|
55
|
+
}, [entries, purpose, state]);
|
|
56
|
+
const choose = (row) => {
|
|
57
|
+
if (!row || state.status !== "ready")
|
|
58
|
+
return;
|
|
59
|
+
if (row.kind === "use-folder") {
|
|
60
|
+
settle(() => onChoose({ kind: "directory", directory: state.directory }));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (row.kind === "parent") {
|
|
64
|
+
if (state.parent !== undefined)
|
|
65
|
+
navigate(state.parent);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const target = path.join(state.directory, row.entry.name);
|
|
69
|
+
if (row.entry.kind === "directory")
|
|
70
|
+
navigate(target);
|
|
71
|
+
else
|
|
72
|
+
settle(() => onChoose({ kind: "file", file: target, directory: state.directory }));
|
|
73
|
+
};
|
|
74
|
+
useKeys((input, key) => {
|
|
75
|
+
if (key.escape) {
|
|
76
|
+
settle(onCancel);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (showFullPath) {
|
|
80
|
+
if (input === "p")
|
|
81
|
+
setShowFullPath(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (state.status !== "ready") {
|
|
85
|
+
const fallbackParent = path.dirname(state.directory);
|
|
86
|
+
if ((key.backspace || key.leftArrow) && fallbackParent !== state.directory)
|
|
87
|
+
navigate(fallbackParent);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (key.backspace || key.leftArrow) {
|
|
91
|
+
if (state.parent !== undefined)
|
|
92
|
+
navigate(state.parent);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (input === "f" && purpose === "file") {
|
|
96
|
+
setShowAllFiles(!showAllFilesRef.current);
|
|
97
|
+
cursorTo(0);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (input === "p") {
|
|
101
|
+
setShowFullPath(true);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const next = moveCursor(cursorRef.current, rows.length, input, key);
|
|
105
|
+
if (next !== undefined) {
|
|
106
|
+
cursorTo(Math.max(next, 0));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key.return)
|
|
110
|
+
choose(rows[Math.min(cursorRef.current, rows.length - 1)]);
|
|
111
|
+
});
|
|
112
|
+
const directory = state.directory;
|
|
113
|
+
const wrappedDirectory = wrapText(`Directory: ${directory}`, panelWidth);
|
|
114
|
+
const directoryLines = showFullPath || wrappedDirectory.length <= 3
|
|
115
|
+
? wrappedDirectory
|
|
116
|
+
: [wrappedDirectory[0], ...wrapText(`… ${wrappedDirectory.length - 2} lines · press p for full path`, panelWidth), wrappedDirectory.at(-1)];
|
|
117
|
+
const ordinaryFooterLines = (purpose === "directory"
|
|
118
|
+
? ["j/k move · Enter open/use · ←", "parent · p full path · Esc back"]
|
|
119
|
+
: ["j/k move · Enter open · ← parent", `f ${showAllFiles ? "json only" : "all files"} · p full path · Esc back`])
|
|
120
|
+
.flatMap(line => wrapText(line, panelWidth));
|
|
121
|
+
const noticeLines = notice === undefined ? [] : wrapText(notice, panelWidth);
|
|
122
|
+
const filterNoteLines = !showFullPath && purpose === "file" && state.status === "ready" && !showAllFiles
|
|
123
|
+
? wrapText(`Showing ${PROFILE_EXTENSION} only · f shows all files`, panelWidth)
|
|
124
|
+
: [];
|
|
125
|
+
const statusLineCount = !showFullPath && (state.status === "loading" || state.status === "error" || (state.status === "ready" && rows.length === 0)) ? 1 : 0;
|
|
126
|
+
const visibleNoticeLines = showFullPath ? [] : noticeLines;
|
|
127
|
+
const footerLines = showFullPath ? wrapText("p hide path · Esc back", panelWidth) : ordinaryFooterLines;
|
|
128
|
+
const reservesWindowIndicator = !showFullPath && rows.length > 0 && listHeight === undefined ? 1 : 0;
|
|
129
|
+
const reserved = 3 + directoryLines.length + statusLineCount + filterNoteLines.length + visibleNoticeLines.length + footerLines.length + reservesWindowIndicator;
|
|
130
|
+
const available = useListHeight(listHeight, reserved, 1);
|
|
131
|
+
const height = rows.length > available && listHeight !== undefined ? Math.max(available - 1, 1) : available;
|
|
132
|
+
const window = computeWindow(rows.length, cursor, height);
|
|
133
|
+
const listedRows = showFullPath ? [] : rows.slice(window.start, window.end);
|
|
134
|
+
return (_jsxs(BorderPanel, { title: purpose === "directory" ? "Choose export folder" : "Choose import file", children: [directoryLines.map((line, index) => _jsx(Text, { color: theme.value, children: line }, index)), !showFullPath && state.status === "loading" ? _jsx(Text, { color: theme.muted, children: "Loading\u2026" }) : null, !showFullPath && state.status === "error" ? _jsxs(Text, { color: theme.error, children: ["Error: ", clipText(state.message, panelWidth)] }) : null, !showFullPath && state.status === "ready" && rows.length === 0 ? _jsx(Text, { color: theme.muted, children: "Empty folder" }) : null, filterNoteLines.map((line, index) => _jsx(Text, { color: theme.muted, children: line }, `filter-${index}`)), listedRows.map((row, position) => {
|
|
135
|
+
const index = window.start + position;
|
|
136
|
+
const marker = index === cursor ? "> " : " ";
|
|
137
|
+
const label = row.kind === "use-folder" ? `Use this folder: ${directory}`
|
|
138
|
+
: row.kind === "parent" ? ".. (parent folder)"
|
|
139
|
+
: row.entry.kind === "directory" ? `${row.entry.name}/` : row.entry.name;
|
|
140
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: marker }), _jsx(Text, { color: theme.value, children: clipText(label, Math.max(panelWidth - 2, 1)) })] }, index);
|
|
141
|
+
}), visibleNoticeLines.map((line, index) => _jsx(Text, { color: theme.warning, children: line }, `notice-${index}`)), footerLines.map((line, index) => _jsx(Text, { color: theme.legend, children: line }, `footer-${index}`)), !showFullPath && window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null] }));
|
|
142
|
+
}
|