@iseoane/agent-depot 0.3.0 → 0.4.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 +18 -0
- package/README.md +87 -3
- package/dist/src/app-flow.js +12 -5
- package/dist/src/cli.js +5 -0
- package/dist/src/profile-cli.js +88 -0
- package/dist/src/profile-export.js +135 -0
- package/dist/src/profile-import.js +218 -0
- package/dist/src/profile.js +130 -0
- package/dist/src/project-manifest.js +6 -0
- package/dist/src/tui/app-actions.js +3 -3
- package/dist/src/tui/app.js +10 -4
- package/dist/src/tui/profile-view.js +249 -0
- package/dist/src/tui/sources-recipes.js +216 -0
- package/dist/src/tui/sources-view.js +16 -7
- package/package.json +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { createAppOperations } from "../app-flow.js";
|
|
6
|
+
import { buildProfileExport, profileExportPreview, writeProfileExport, } from "../profile-export.js";
|
|
7
|
+
import { applyProfileImport, buildProfileImport, } from "../profile-import.js";
|
|
8
|
+
import { PROFILE_FORMAT } from "../profile.js";
|
|
9
|
+
import { useKeys } from "./keys.js";
|
|
10
|
+
import { answerYesNo, moveCursor, toggleChoice } from "./mode-keys.js";
|
|
11
|
+
import { ChecklistLines, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
12
|
+
import { computeWindow, useListHeight } from "./window.js";
|
|
13
|
+
function rowsFromExport(plan) {
|
|
14
|
+
return [
|
|
15
|
+
...plan.profile.sources.map((value, index) => ({
|
|
16
|
+
block: "sources", index, label: `Sources: ${value.url}`, selectable: true,
|
|
17
|
+
})),
|
|
18
|
+
...plan.profile.skills.map((value, index) => ({
|
|
19
|
+
block: "skills", index, label: `Skills: ${value.path}`, selectable: true,
|
|
20
|
+
})),
|
|
21
|
+
...plan.profile.apps.map((value, index) => ({
|
|
22
|
+
block: "apps", index, label: `Apps: ${value.name} (${value.platform ?? "all platforms"})`, selectable: true,
|
|
23
|
+
})),
|
|
24
|
+
...plan.exclusions.map((item, index) => ({
|
|
25
|
+
block: "excluded", index, label: `Excluded ${item.label}: ${item.reason}`, selectable: false,
|
|
26
|
+
})),
|
|
27
|
+
...plan.warnings.filter(warning => warning.kind === "load").map((warning, index) => ({
|
|
28
|
+
block: "excluded", index: plan.exclusions.length + index, label: warning.message, selectable: false,
|
|
29
|
+
})),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
function rowsFromPlan(plan) {
|
|
33
|
+
return plan.items.map((item, index) => ({
|
|
34
|
+
block: item.block, index,
|
|
35
|
+
label: `${item.status}: ${item.label}${item.difference ? `; ${item.difference}` : ""}`,
|
|
36
|
+
selectable: item.status === "add",
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function selectedPlanItems(plan, selected) {
|
|
40
|
+
return plan.items.filter((item, index) => item.status !== "add" || selected.some(row => row.index === index));
|
|
41
|
+
}
|
|
42
|
+
/** Collect choices, display shared-core previews and confirm portable Profile transfers. */
|
|
43
|
+
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight }) {
|
|
44
|
+
const apps = useMemo(() => createAppOperations({
|
|
45
|
+
homeDirectory: environment.homeDirectory, ...environment.appEnvironment,
|
|
46
|
+
}), [environment]);
|
|
47
|
+
const [mode, setModeState] = useState("browse");
|
|
48
|
+
const modeRef = useRef("browse");
|
|
49
|
+
const mounted = useRef(true);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
mounted.current = true;
|
|
52
|
+
return () => { mounted.current = false; };
|
|
53
|
+
}, []);
|
|
54
|
+
const setMode = (next) => {
|
|
55
|
+
modeRef.current = next;
|
|
56
|
+
onCapturingChange?.(next !== "browse" && next !== "results");
|
|
57
|
+
setModeState(next);
|
|
58
|
+
};
|
|
59
|
+
const [rows, setRows] = useState([]);
|
|
60
|
+
const [selected, setSelectedState] = useState([]);
|
|
61
|
+
const selectedRef = useRef([]);
|
|
62
|
+
const select = (next) => {
|
|
63
|
+
selectedRef.current = next;
|
|
64
|
+
setSelectedState(next);
|
|
65
|
+
};
|
|
66
|
+
const [cursor, setCursorState] = useState(0);
|
|
67
|
+
const cursorRef = useRef(0);
|
|
68
|
+
const cursorTo = (next) => {
|
|
69
|
+
cursorRef.current = next;
|
|
70
|
+
setCursorState(next);
|
|
71
|
+
};
|
|
72
|
+
const [text, setText] = useState("");
|
|
73
|
+
const typed = useRef("");
|
|
74
|
+
const [lines, setLines] = useState([]);
|
|
75
|
+
const [profile, setProfile] = useState();
|
|
76
|
+
const [plan, setPlan] = useState();
|
|
77
|
+
const diagnostics = useRef([]);
|
|
78
|
+
const action = useRef("export");
|
|
79
|
+
const profileVersion = useRef("");
|
|
80
|
+
const height = useListHeight(listHeight, 9);
|
|
81
|
+
const showLines = (next, nextMode) => {
|
|
82
|
+
setLines(next);
|
|
83
|
+
cursorTo(0);
|
|
84
|
+
setMode(nextMode);
|
|
85
|
+
};
|
|
86
|
+
const run = async (work) => {
|
|
87
|
+
setMode("busy");
|
|
88
|
+
try {
|
|
89
|
+
await work();
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (mounted.current) {
|
|
93
|
+
showLines([`Error: ${error instanceof Error ? error.message : String(error)}`], "results");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const prompt = (next) => {
|
|
98
|
+
typed.current = "";
|
|
99
|
+
setText("");
|
|
100
|
+
setMode(next);
|
|
101
|
+
};
|
|
102
|
+
const loadRows = (next, nextMode) => {
|
|
103
|
+
setRows(next);
|
|
104
|
+
select(next.filter(row => row.selectable));
|
|
105
|
+
cursorTo(0);
|
|
106
|
+
setMode(nextMode);
|
|
107
|
+
};
|
|
108
|
+
const chosenProfile = () => {
|
|
109
|
+
const chosen = selectedRef.current;
|
|
110
|
+
const isChosen = (block, index) => chosen.some(row => row.block === block && row.index === index);
|
|
111
|
+
return {
|
|
112
|
+
...profile,
|
|
113
|
+
sources: profile.sources.filter((_, index) => isChosen("sources", index)),
|
|
114
|
+
skills: profile.skills.filter((_, index) => isChosen("skills", index)),
|
|
115
|
+
apps: profile.apps.filter((_, index) => isChosen("apps", index)),
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
useKeys((input, key) => {
|
|
119
|
+
const current = modeRef.current;
|
|
120
|
+
if (current === "busy")
|
|
121
|
+
return;
|
|
122
|
+
if (current === "browse" || current === "results") {
|
|
123
|
+
if (input === "e") {
|
|
124
|
+
action.current = "export";
|
|
125
|
+
void run(async () => {
|
|
126
|
+
const exported = await buildProfileExport(operations, apps, { homeDirectory: environment.homeDirectory });
|
|
127
|
+
if (!mounted.current)
|
|
128
|
+
return;
|
|
129
|
+
setProfile(exported.profile);
|
|
130
|
+
diagnostics.current = [...exported.exclusions, ...exported.warnings];
|
|
131
|
+
loadRows(rowsFromExport(exported), "export-list");
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (input === "i") {
|
|
135
|
+
action.current = "import";
|
|
136
|
+
prompt("import-path");
|
|
137
|
+
}
|
|
138
|
+
else if (current === "results") {
|
|
139
|
+
const next = moveCursor(cursorRef.current, lines.length, input, key);
|
|
140
|
+
if (next !== undefined)
|
|
141
|
+
cursorTo(Math.max(0, next));
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (current === "export-path" || current === "import-path") {
|
|
146
|
+
if (key.escape) {
|
|
147
|
+
setMode("browse");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (key.return) {
|
|
151
|
+
if (!typed.current.trim())
|
|
152
|
+
return;
|
|
153
|
+
if (current === "export-path") {
|
|
154
|
+
showLines(profileExportPreview(chosenProfile(), diagnostics.current), "confirm");
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
void run(async () => {
|
|
158
|
+
const raw = JSON.parse(await readFile(typed.current, "utf8"));
|
|
159
|
+
const imported = await buildProfileImport(raw, operations, apps);
|
|
160
|
+
profileVersion.current = raw.agentDepotVersion;
|
|
161
|
+
if (!mounted.current)
|
|
162
|
+
return;
|
|
163
|
+
setPlan(imported);
|
|
164
|
+
loadRows(rowsFromPlan(imported), "import-list");
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (key.backspace || key.delete) {
|
|
168
|
+
typed.current = typed.current.slice(0, -1);
|
|
169
|
+
setText(typed.current);
|
|
170
|
+
}
|
|
171
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
172
|
+
typed.current += input;
|
|
173
|
+
setText(typed.current);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (current === "confirm") {
|
|
178
|
+
answerYesNo(input, key, () => {
|
|
179
|
+
void run(async () => {
|
|
180
|
+
let result;
|
|
181
|
+
if (action.current === "export") {
|
|
182
|
+
await writeProfileExport(typed.current, chosenProfile());
|
|
183
|
+
result = [`Exported profile to ${typed.current}`];
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const confirmed = { ...plan, items: selectedPlanItems(plan, selectedRef.current) };
|
|
187
|
+
const outcomes = await applyProfileImport(confirmed, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, ...environment.installationOptions });
|
|
188
|
+
result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
|
|
189
|
+
}
|
|
190
|
+
if (mounted.current) {
|
|
191
|
+
showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}, () => setMode("browse"));
|
|
195
|
+
if (input === "y" || input === "n" || key.escape)
|
|
196
|
+
return;
|
|
197
|
+
const next = moveCursor(cursorRef.current, lines.length, input, key);
|
|
198
|
+
if (next !== undefined)
|
|
199
|
+
cursorTo(Math.max(0, next));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (key.escape) {
|
|
203
|
+
setMode("browse");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Export diagnostics follow the checklist, but never own its cursor.
|
|
207
|
+
// Import same/conflict rows still draw a cursor so their differences can be reviewed.
|
|
208
|
+
const checklistLength = rows.filter(row => row.block !== "excluded").length;
|
|
209
|
+
const next = moveCursor(cursorRef.current, checklistLength, input, key);
|
|
210
|
+
if (next !== undefined)
|
|
211
|
+
cursorTo(Math.max(0, next));
|
|
212
|
+
else if (input === " " && rows[cursorRef.current]?.selectable) {
|
|
213
|
+
select(toggleChoice(rows, selectedRef.current, rows[cursorRef.current]));
|
|
214
|
+
}
|
|
215
|
+
else if (input === "a")
|
|
216
|
+
select(selectedRef.current.length ? [] : rows.filter(row => row.selectable));
|
|
217
|
+
else if (key.return) {
|
|
218
|
+
if (current === "export-list") {
|
|
219
|
+
if (!selectedRef.current.length) {
|
|
220
|
+
showLines(["Nothing selected; nothing written."], "results");
|
|
221
|
+
}
|
|
222
|
+
else
|
|
223
|
+
prompt("export-path");
|
|
224
|
+
}
|
|
225
|
+
else
|
|
226
|
+
void run(async () => {
|
|
227
|
+
// Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
|
|
228
|
+
const items = selectedPlanItems(plan, selectedRef.current);
|
|
229
|
+
const preview = await buildProfileImport({
|
|
230
|
+
format: PROFILE_FORMAT, agentDepotVersion: profileVersion.current,
|
|
231
|
+
sources: items.filter(item => item.block === "sources").map(item => item.value),
|
|
232
|
+
skills: items.filter(item => item.block === "skills").map(item => item.value),
|
|
233
|
+
apps: items.filter(item => item.block === "apps").map(item => item.value),
|
|
234
|
+
}, operations, apps);
|
|
235
|
+
if (mounted.current)
|
|
236
|
+
showLines(preview.preview, "confirm");
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const listing = mode === "export-list" || mode === "import-list";
|
|
241
|
+
const window = computeWindow(listing ? rows.length : lines.length, cursor, height);
|
|
242
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Portable user-global Profile \u00B7 e export \u00B7 i import" }), mode === "busy" ? _jsx(Text, { children: "Loading Profile\u2026" }) : null, mode.endsWith("-path") ? _jsxs(Text, { children: ["Profile path: ", text, "_"] }) : null, listing ? ["sources", "skills", "apps", "excluded"].map(block => {
|
|
243
|
+
const choices = rows.slice(window.start, window.end).filter(row => row.block === block);
|
|
244
|
+
if (!choices.length)
|
|
245
|
+
return null;
|
|
246
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: block === "excluded" ? "Excluded" : block[0].toUpperCase() + block.slice(1) }), block === "excluded" ? _jsx(PreviewLines, { lines: choices.map(row => `${row.label} (not selectable)`) }) :
|
|
247
|
+
_jsx(ChecklistLines, { choices: choices, cursor: choices.indexOf(rows[cursor]), isSelected: row => selected.includes(row), keyOf: row => `${row.block}:${row.index}`, label: row => `${row.label}${row.selectable ? "" : " (not selectable)"}` })] }, block);
|
|
248
|
+
}) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines.slice(window.start, window.end), question: "Apply Profile? (y/n) \u00B7 j/k scroll" }) : null, mode === "results" ? _jsxs(_Fragment, { children: [_jsx(PreviewLines, { lines: lines.slice(window.start, window.end) }), _jsx(Text, { children: "j/k scroll" })] }) : null, window.indicator && (listing || mode === "confirm" || mode === "results") ? _jsx(Text, { children: window.indicator }) : null, listing ? _jsx(Text, { children: "j/k move \u00B7 space toggle \u00B7 a all \u00B7 Enter preview \u00B7 Esc cancel" }) : null] });
|
|
249
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createAppOperations } from "../app-flow.js";
|
|
5
|
+
import { BUILT_IN_SOURCE } from "../sources.js";
|
|
6
|
+
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
7
|
+
import { ActionPanel } from "./catalog-panel.js";
|
|
8
|
+
import { hasUnmanagedCopy, isInstalled, loadInstalledSkills } from "./catalog-installs.js";
|
|
9
|
+
import { appName, appStepLines } from "./app-actions.js";
|
|
10
|
+
import { errorText } from "./batch.js";
|
|
11
|
+
import { useMounted } from "./view-state.js";
|
|
12
|
+
import { PreviewConfirm } from "./panel-parts.js";
|
|
13
|
+
import { answerYesNo } from "./mode-keys.js";
|
|
14
|
+
import { rowStyle, theme } from "./theme.js";
|
|
15
|
+
import { computeWindow, pageStep } from "./window.js";
|
|
16
|
+
/**
|
|
17
|
+
* Returns section focus, key dispatch (true when consumed), the recipe panel,
|
|
18
|
+
* and the Git list's share of the common height budget. Sources owns key dispatch.
|
|
19
|
+
*/
|
|
20
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, height, gitCount, }) {
|
|
21
|
+
const homeDirectory = environment?.homeDirectory;
|
|
22
|
+
const appEnvironment = environment?.appEnvironment;
|
|
23
|
+
const apps = useMemo(() => createAppOperations({
|
|
24
|
+
homeDirectory,
|
|
25
|
+
...appEnvironment,
|
|
26
|
+
}), [homeDirectory, appEnvironment]);
|
|
27
|
+
const mounted = useMounted();
|
|
28
|
+
const [rows, setRows] = useState([]);
|
|
29
|
+
const [error, setError] = useState();
|
|
30
|
+
const [loadError, setLoadError] = useState();
|
|
31
|
+
const [focused, setFocused] = useState(false);
|
|
32
|
+
// Ref mirrors keep Tab and subsequent keys in a single burst on the same section.
|
|
33
|
+
const focusRef = useRef(false);
|
|
34
|
+
const [index, setIndex] = useState(0);
|
|
35
|
+
const indexRef = useRef(0);
|
|
36
|
+
const [mode, setModeState] = useState({ kind: "browse" });
|
|
37
|
+
const modeRef = useRef(mode);
|
|
38
|
+
const setMode = (next) => {
|
|
39
|
+
modeRef.current = next;
|
|
40
|
+
onCapturingChange?.(next.kind !== "browse");
|
|
41
|
+
setModeState(next);
|
|
42
|
+
};
|
|
43
|
+
const finish = async () => {
|
|
44
|
+
await reload();
|
|
45
|
+
if (mounted.current)
|
|
46
|
+
setMode({ kind: "browse" });
|
|
47
|
+
};
|
|
48
|
+
const preview = async (entry, status) => {
|
|
49
|
+
setError(undefined);
|
|
50
|
+
setMode({ kind: "busy" });
|
|
51
|
+
try {
|
|
52
|
+
const steps = await apps.preview(entry);
|
|
53
|
+
if (mounted.current) {
|
|
54
|
+
setMode({
|
|
55
|
+
kind: "approve",
|
|
56
|
+
entry,
|
|
57
|
+
lines: [
|
|
58
|
+
`File: ${entry.file}`,
|
|
59
|
+
`Platform: ${entry.recipe?.platform ?? "all"}`,
|
|
60
|
+
`Status: ${status}`,
|
|
61
|
+
...steps.flatMap(step => [step.step, ...appStepLines(step)]),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (mounted.current) {
|
|
68
|
+
setError(errorText(error));
|
|
69
|
+
setMode({ kind: "browse" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const flow = {
|
|
74
|
+
operations,
|
|
75
|
+
env: environment ?? {},
|
|
76
|
+
isMounted: () => mounted.current,
|
|
77
|
+
setAction: action => setMode(action.kind === "browse"
|
|
78
|
+
? { kind: "browse" }
|
|
79
|
+
: { kind: "install", action }),
|
|
80
|
+
setMessage: message => {
|
|
81
|
+
if (mounted.current)
|
|
82
|
+
setError(message?.text);
|
|
83
|
+
},
|
|
84
|
+
keepMarked: () => undefined,
|
|
85
|
+
finish: async (result) => {
|
|
86
|
+
if (mounted.current) {
|
|
87
|
+
setError(result.kind === "error" ? result.text : undefined);
|
|
88
|
+
setMode({ kind: result.kind === "ok" ? "prompt" : "browse" });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const startAI = async () => {
|
|
93
|
+
setMode({ kind: "busy" });
|
|
94
|
+
try {
|
|
95
|
+
if (!operations.discoverSkills)
|
|
96
|
+
throw new Error("Discovery is not supported by the configured operations");
|
|
97
|
+
const [skills, installed] = await Promise.all([
|
|
98
|
+
operations.discoverSkills([BUILT_IN_SOURCE.id]),
|
|
99
|
+
loadInstalledSkills(operations, environment ?? {}),
|
|
100
|
+
]);
|
|
101
|
+
const skill = skills.find(skill => skill.name === "agent-depot-apprecipe");
|
|
102
|
+
if (!skill)
|
|
103
|
+
throw new Error("Built-in agent-depot-apprecipe Skill is unavailable");
|
|
104
|
+
if (!mounted.current)
|
|
105
|
+
return;
|
|
106
|
+
if (isInstalled(skill, installed) || hasUnmanagedCopy(skill, installed)) {
|
|
107
|
+
setMode({ kind: "prompt" });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
flow.setAction({ kind: "host", skills: [skill], cursor: 0, selected: [] });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (mounted.current) {
|
|
115
|
+
setError(errorText(error));
|
|
116
|
+
setMode({ kind: "browse" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/** Handles captured modes and browse keys only in the focused section. */
|
|
121
|
+
const handleKey = (input, key) => {
|
|
122
|
+
const current = modeRef.current;
|
|
123
|
+
if (current.kind === "busy")
|
|
124
|
+
return true;
|
|
125
|
+
if (current.kind === "install") {
|
|
126
|
+
// Host/scope/version are selection prompts; version chooses latest or fixed
|
|
127
|
+
// by number. Only the separate fixed mode accepts free-text version input.
|
|
128
|
+
// n declines these selections; fixed input and in-flight installs are untouched.
|
|
129
|
+
const decline = input === "n" && ["host", "scope", "version"].includes(current.action.kind);
|
|
130
|
+
handleCatalogActionKey(flow, current.action, input, decline ? { ...key, escape: true } : key);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (current.kind === "guide" || current.kind === "manual" || current.kind === "prompt") {
|
|
134
|
+
if (key.escape || input === "n")
|
|
135
|
+
setMode({ kind: "browse" });
|
|
136
|
+
else if (current.kind === "guide" && input === "1")
|
|
137
|
+
void startAI();
|
|
138
|
+
else if (current.kind === "guide" && input === "2")
|
|
139
|
+
setMode({ kind: "manual" });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (current.kind === "approve") {
|
|
143
|
+
answerYesNo(input, key, () => {
|
|
144
|
+
setMode({ kind: "busy" });
|
|
145
|
+
void apps.approve(current.entry).then(finish, error => {
|
|
146
|
+
if (mounted.current) {
|
|
147
|
+
setError(errorText(error));
|
|
148
|
+
setMode({ kind: "browse" });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}, () => setMode({ kind: "browse" }));
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (key.tab) {
|
|
155
|
+
setError(undefined);
|
|
156
|
+
focusRef.current = !focusRef.current;
|
|
157
|
+
setFocused(focusRef.current);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (!focusRef.current)
|
|
161
|
+
return false;
|
|
162
|
+
if (key.downArrow || input === "j" || key.upArrow || input === "k" || key.pageDown || key.pageUp) {
|
|
163
|
+
const step = key.pageDown || key.pageUp ? pageStep(recipeHeight) : 1;
|
|
164
|
+
const down = key.downArrow || input === "j" || key.pageDown;
|
|
165
|
+
indexRef.current = Math.min(Math.max(indexRef.current + (down ? step : -step), 0), Math.max(rows.length - 1, 0));
|
|
166
|
+
setIndex(indexRef.current);
|
|
167
|
+
}
|
|
168
|
+
else if (input === "n") {
|
|
169
|
+
setError(undefined);
|
|
170
|
+
setMode({ kind: "guide" });
|
|
171
|
+
}
|
|
172
|
+
else if (input === "r") {
|
|
173
|
+
void reload();
|
|
174
|
+
}
|
|
175
|
+
else if (key.return) {
|
|
176
|
+
const row = rows[Math.min(indexRef.current, rows.length - 1)];
|
|
177
|
+
if (row)
|
|
178
|
+
void preview(row.entry, row.status);
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
};
|
|
182
|
+
/** Reloads parsing and approval receipts, never version/latest commands. */
|
|
183
|
+
const reload = useCallback(async () => {
|
|
184
|
+
try {
|
|
185
|
+
const entries = await apps.load();
|
|
186
|
+
const rows = await Promise.all(entries.map(async (entry) => {
|
|
187
|
+
const inspection = await apps.approvalStatus(entry);
|
|
188
|
+
const status = inspection.status === "invalid"
|
|
189
|
+
? `invalid recipe: ${inspection.reason}`
|
|
190
|
+
: inspection.status;
|
|
191
|
+
return { entry, status };
|
|
192
|
+
}));
|
|
193
|
+
if (mounted.current) {
|
|
194
|
+
setRows(rows);
|
|
195
|
+
setLoadError(undefined);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (mounted.current)
|
|
200
|
+
setLoadError(errorText(error));
|
|
201
|
+
}
|
|
202
|
+
}, [apps, mounted]);
|
|
203
|
+
useEffect(() => { void reload(); }, [reload]);
|
|
204
|
+
// Give each nonempty section half the rows, then reclaim unused Git capacity.
|
|
205
|
+
const budget = Math.max(height, gitCount > 0 && rows.length > 0 ? 2 : 1);
|
|
206
|
+
const gitShare = Math.min(gitCount, Math.max(1, Math.floor(budget / 2)));
|
|
207
|
+
const recipeHeight = Math.min(rows.length, budget - gitShare);
|
|
208
|
+
const gitHeight = Math.max(1, budget - recipeHeight);
|
|
209
|
+
const at = Math.min(index, rows.length - 1);
|
|
210
|
+
const window = computeWindow(rows.length, at, recipeHeight);
|
|
211
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(focused && rows.length === 0), children: ["App recipes \u00B7 ", apps.directory, " \u00B7 Tab switches section"] }), focused ? _jsx(Text, { color: theme.muted, children: "j/k move \u00B7 Enter preview \u00B7 n add recipe \u00B7 r reload" }) : null, loadError ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error ? _jsx(Text, { color: theme.error, children: error }) : null, rows.slice(window.start, window.end).map(({ entry, status }, offset) => {
|
|
212
|
+
const selected = focused && window.start + offset === at;
|
|
213
|
+
return (_jsxs(Text, { ...rowStyle(selected), children: [selected ? "> " : " ", entry.file.split(/[\\/]/u).pop(), " \u00B7 ", status] }, entry.file));
|
|
214
|
+
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, mode.kind === "guide" ? (_jsxs(Text, { children: ["1 With the AI skill · install if missing, then copy the prompt\n", "2 Manual · write a recipe using the schema\nn or Esc closes"] })) : null, mode.kind === "manual" ? (_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory, "\n", "Schema: agent-depot app schema \u00B7 n or Esc closes"] })) : null, mode.kind === "prompt" ? (_jsxs(Text, { children: ["Use the agent-depot-apprecipe skill to create the recipe for <app>", "\n", "Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(Text, { children: "Working..." }) : null, mode.kind === "approve" ? (_jsx(PreviewConfirm, { lines: mode.lines, question: `Approve ${appName(mode.entry)}? y/n` })) : null] }));
|
|
215
|
+
return { focused, handleKey, gitHeight, panel };
|
|
216
|
+
}
|
|
@@ -5,6 +5,7 @@ import { initialMode, modeReducer } from "./sources-mode.js";
|
|
|
5
5
|
import { rowStyle, theme } from "./theme.js";
|
|
6
6
|
import { computeWindow, pageStep, useListHeight } from "./window.js";
|
|
7
7
|
import { useKeys } from "./keys.js";
|
|
8
|
+
import { useSourcesRecipes } from "./sources-recipes.js";
|
|
8
9
|
/** The highlighted index: by stable id while that source exists, otherwise the bounded fallback index. */
|
|
9
10
|
function resolveIndex(sources, selection) {
|
|
10
11
|
const found = sources.findIndex((source) => source.id === selection.id);
|
|
@@ -28,8 +29,8 @@ async function refreshEach(operations, targets) {
|
|
|
28
29
|
function errorText(error) {
|
|
29
30
|
return error instanceof Error ? error.message : String(error);
|
|
30
31
|
}
|
|
31
|
-
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight }) {
|
|
32
|
-
const height = useListHeight(listHeight,
|
|
32
|
+
export function SourcesView({ operations, onCapturingChange, onOpenCatalog, listHeight, environment }) {
|
|
33
|
+
const height = useListHeight(listHeight, 9);
|
|
33
34
|
const [state, setState] = useState({ status: "loading" });
|
|
34
35
|
// The highlight follows the source id; the index is only the fallback once that source is gone.
|
|
35
36
|
// Selection and marks are mirrored in refs updated first, so keys delivered in one burst act on the latest state.
|
|
@@ -95,6 +96,9 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
95
96
|
const sourcesRef = useRef(sources);
|
|
96
97
|
sourcesRef.current = sources;
|
|
97
98
|
const selected = resolveIndex(sources, selection);
|
|
99
|
+
const recipes = useSourcesRecipes({
|
|
100
|
+
operations, environment, onCapturingChange, height, gitCount: sources.length,
|
|
101
|
+
});
|
|
98
102
|
const select = (index) => {
|
|
99
103
|
const listed = sourcesRef.current;
|
|
100
104
|
const bounded = Math.min(Math.max(index, 0), Math.max(listed.length - 1, 0));
|
|
@@ -201,6 +205,11 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
201
205
|
dispatch({ type: "done" });
|
|
202
206
|
return;
|
|
203
207
|
}
|
|
208
|
+
if (recipes.handleKey(input, key)) {
|
|
209
|
+
if (key.tab)
|
|
210
|
+
setMessage(undefined);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
204
213
|
const listed = sourcesRef.current;
|
|
205
214
|
const at = resolveIndex(listed, selectionRef.current);
|
|
206
215
|
const highlighted = listed[at];
|
|
@@ -211,10 +220,10 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
211
220
|
select(at - 1);
|
|
212
221
|
}
|
|
213
222
|
else if (key.pageDown) {
|
|
214
|
-
select(at + pageStep(
|
|
223
|
+
select(at + pageStep(recipes.gitHeight));
|
|
215
224
|
}
|
|
216
225
|
else if (key.pageUp) {
|
|
217
|
-
select(at - pageStep(
|
|
226
|
+
select(at - pageStep(recipes.gitHeight));
|
|
218
227
|
}
|
|
219
228
|
else if (key.return && highlighted) {
|
|
220
229
|
onOpenCatalog?.(highlighted);
|
|
@@ -255,11 +264,11 @@ export function SourcesView({ operations, onCapturingChange, onOpenCatalog, list
|
|
|
255
264
|
return _jsx(Text, { children: "Loading sources..." });
|
|
256
265
|
if (state.status === "error")
|
|
257
266
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
258
|
-
const window = computeWindow(sources.length, selected,
|
|
267
|
+
const window = computeWindow(sources.length, selected, recipes.gitHeight);
|
|
259
268
|
return (_jsxs(Box, { flexDirection: "column", children: [builtin ? _jsxs(Text, { color: theme.muted, children: [" ", builtin.id, " \u00B7 included"] }) : null, sources.length === 0 ? _jsx(Text, { children: "No Git sources \u00B7 n to add" }) : null, sources.length > 0 ? _jsxs(Text, { color: theme.muted, children: [" ", "ID KIND URL"] }) : null, sources.slice(window.start, window.end).map((source, offset) => {
|
|
260
269
|
const index = window.start + offset;
|
|
261
|
-
return (_jsxs(Text, { ...rowStyle(index === selected), children: [index === selected ? "> " : " ", marked.has(source.id) ? "[x] " : "[ ] ", source.id, " ", source.kind, " ", source.url] }, source.id));
|
|
262
|
-
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, _jsx(Prompt, { mode: mode }), message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
|
|
270
|
+
return (_jsxs(Text, { ...rowStyle(!recipes.focused && index === selected), children: [!recipes.focused && index === selected ? "> " : " ", marked.has(source.id) ? "[x] " : "[ ] ", source.id, " ", source.kind, " ", source.url] }, source.id));
|
|
271
|
+
}), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null, selectedCount > 0 ? _jsxs(Text, { children: [selectedCount, " selected"] }) : null, recipes.panel, _jsx(Prompt, { mode: mode }), message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
|
|
263
272
|
}
|
|
264
273
|
function Prompt({ mode }) {
|
|
265
274
|
if (mode.kind === "input")
|