@iseoane/agent-depot 0.6.0 → 0.6.2
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 +37 -0
- package/README.es.md +95 -14
- package/README.md +84 -12
- package/dist/src/package-host-state.js +45 -1
- package/dist/src/tui/file-picker-model.js +87 -0
- package/dist/src/tui/file-picker.js +142 -0
- package/dist/src/tui/panel-parts.js +86 -5
- package/dist/src/tui/preview-window.js +8 -5
- package/dist/src/tui/profile-view.js +213 -174
- package/dist/src/tui/text-wrap.js +53 -0
- package/dist/src/tui/window.js +3 -3
- package/package.json +3 -4
- package/skills/doctor-md-agents/README.md +3 -1
- package/skills/doctor-md-skill/README.md +2 -2
|
@@ -2,17 +2,21 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { Box, Text, useWindowSize } from "ink";
|
|
4
4
|
import stringWidth from "string-width";
|
|
5
|
-
import {
|
|
5
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { createAppOperations } from "../app-flow.js";
|
|
7
7
|
import { buildProfileExport, describeProfileExportExclusion, describeProfilePackage, describeProfileReplica, describeProfileSkill, profileExportPreview, SKILL_EXPORT_EXPLANATION, writeProfileExport, } from "../profile-export.js";
|
|
8
8
|
import { applyProfileImport, buildProfileImport, } from "../profile-import.js";
|
|
9
9
|
import { PROFILE_FORMAT, PROFILE_FORMAT_V2, parseProfile } from "../profile.js";
|
|
10
10
|
import { selectionKey } from "../package-model.js";
|
|
11
11
|
import { packageOperationsOf } from "./environment.js";
|
|
12
|
+
import { DEFAULT_PROFILE_FILE_NAME, exportDestination, fileNameNotice, pathExists, pickerStartDirectory, resolveExportFileName, } from "./file-picker-model.js";
|
|
13
|
+
import { FilePicker } from "./file-picker.js";
|
|
12
14
|
import { useKeys } from "./keys.js";
|
|
13
15
|
import { answerYesNo, moveCursor, movePreviewCursor, toggleChoice } from "./mode-keys.js";
|
|
14
16
|
import { BorderPanel, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
17
|
+
import { fitLines, wrapText } from "./text-wrap.js";
|
|
15
18
|
import { theme } from "./theme.js";
|
|
19
|
+
import { useMirrored, useMounted } from "./view-state.js";
|
|
16
20
|
import { computeWindow, useListHeight } from "./window.js";
|
|
17
21
|
function rowsFromExport(plan) {
|
|
18
22
|
return [
|
|
@@ -67,41 +71,6 @@ function exportDisplayRows(rows, expanded) {
|
|
|
67
71
|
...group("group:excluded", `Export notes (${choices("excluded").length})`, 0, choices("excluded")),
|
|
68
72
|
];
|
|
69
73
|
}
|
|
70
|
-
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
71
|
-
function wrapListText(value, width) {
|
|
72
|
-
const lines = [];
|
|
73
|
-
let line = "";
|
|
74
|
-
let lineWidth = 0;
|
|
75
|
-
for (const { segment } of graphemes.segment(value)) {
|
|
76
|
-
const segmentWidth = stringWidth(segment);
|
|
77
|
-
if (line && lineWidth + segmentWidth > width) {
|
|
78
|
-
lines.push(line);
|
|
79
|
-
line = "";
|
|
80
|
-
lineWidth = 0;
|
|
81
|
-
}
|
|
82
|
-
line += segment;
|
|
83
|
-
lineWidth += segmentWidth;
|
|
84
|
-
}
|
|
85
|
-
return [...lines, line];
|
|
86
|
-
}
|
|
87
|
-
function fitListLines(lines, maxRows, width) {
|
|
88
|
-
if (maxRows <= 0)
|
|
89
|
-
return [];
|
|
90
|
-
if (lines.length <= maxRows)
|
|
91
|
-
return lines;
|
|
92
|
-
const visible = lines.slice(0, maxRows);
|
|
93
|
-
let last = "";
|
|
94
|
-
let used = 0;
|
|
95
|
-
for (const { segment } of graphemes.segment(visible[maxRows - 1])) {
|
|
96
|
-
const segmentWidth = stringWidth(segment);
|
|
97
|
-
if (used + segmentWidth > width - 1)
|
|
98
|
-
break;
|
|
99
|
-
last += segment;
|
|
100
|
-
used += segmentWidth;
|
|
101
|
-
}
|
|
102
|
-
visible[maxRows - 1] = `${last}…`;
|
|
103
|
-
return visible;
|
|
104
|
-
}
|
|
105
74
|
function physicalListWindow(rows, rowHeights, selected, height) {
|
|
106
75
|
if (!rows.length)
|
|
107
76
|
return { start: 0, end: 0, indicator: undefined, stickyGroup: false };
|
|
@@ -171,68 +140,49 @@ function selectedImportProfile(profile, entries) {
|
|
|
171
140
|
return parseProfile({ ...profile, format: replicas.length ? PROFILE_FORMAT_V2 : PROFILE_FORMAT,
|
|
172
141
|
sources, skills, apps, packages, replicas });
|
|
173
142
|
}
|
|
174
|
-
/** A plan whose items are the chosen subset, split back into the block the writer consumes. */
|
|
175
|
-
function chosenPlan(plan, selected) {
|
|
176
|
-
const entries = selectedPlanEntries(plan, selected);
|
|
177
|
-
return {
|
|
178
|
-
...plan,
|
|
179
|
-
items: entries.filter((item) => item.block !== "packages"),
|
|
180
|
-
packages: entries.filter((item) => item.block === "packages"),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
143
|
/** Collect choices, display shared-core previews and confirm portable Profile transfers. */
|
|
184
|
-
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight }) {
|
|
144
|
+
export function ProfileView({ operations, environment = {}, onCapturingChange, listHeight, existsCheck = pathExists }) {
|
|
185
145
|
const apps = useMemo(() => createAppOperations({
|
|
186
146
|
homeDirectory: environment.homeDirectory, ...environment.appEnvironment,
|
|
187
147
|
}), [environment]);
|
|
188
148
|
const packages = useMemo(() => packageOperationsOf(environment), [environment]);
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
mounted.current = true;
|
|
194
|
-
return () => { mounted.current = false; };
|
|
195
|
-
}, []);
|
|
196
|
-
const setMode = (next) => {
|
|
197
|
-
modeRef.current = next;
|
|
149
|
+
const startDirectory = useMemo(() => pickerStartDirectory(environment), [environment]);
|
|
150
|
+
const mounted = useMounted();
|
|
151
|
+
const [mode, modeRef, setModeState] = useMirrored("browse");
|
|
152
|
+
const setMode = useCallback((next) => {
|
|
198
153
|
onCapturingChange?.(next !== "browse" && next !== "results");
|
|
199
154
|
setModeState(next);
|
|
200
|
-
};
|
|
155
|
+
}, [onCapturingChange, setModeState]);
|
|
201
156
|
const [rows, setRows] = useState([]);
|
|
202
|
-
const [expandedGroups,
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const [
|
|
209
|
-
const
|
|
210
|
-
const select = (next) => {
|
|
211
|
-
selectedRef.current = next;
|
|
212
|
-
setSelectedState(next);
|
|
213
|
-
};
|
|
214
|
-
const [cursor, setCursorState] = useState(0);
|
|
215
|
-
const cursorRef = useRef(0);
|
|
216
|
-
const cursorTo = (next) => {
|
|
217
|
-
cursorRef.current = next;
|
|
218
|
-
setCursorState(next);
|
|
219
|
-
};
|
|
220
|
-
const [text, setText] = useState("");
|
|
221
|
-
const typed = useRef("");
|
|
157
|
+
const [expandedGroups, expandedGroupsRef, setExpandedGroups] = useMirrored(new Set());
|
|
158
|
+
const [selected, selectedRef, select] = useMirrored([]);
|
|
159
|
+
const [cursor, cursorRef, cursorTo] = useMirrored(0);
|
|
160
|
+
const [text, typed, setText] = useMirrored("");
|
|
161
|
+
const [nameError, setNameError] = useState(undefined);
|
|
162
|
+
const [importNotice, setImportNotice] = useState(undefined);
|
|
163
|
+
const [exportDirectory, setExportDirectory] = useState(undefined);
|
|
164
|
+
const [importDirectory, setImportDirectory] = useState(undefined);
|
|
222
165
|
const [lines, setLines] = useState([]);
|
|
223
166
|
const detailReturnId = useRef(undefined);
|
|
167
|
+
const confirmRequest = useRef(undefined);
|
|
168
|
+
const nameRequest = useRef(0);
|
|
169
|
+
const importRequest = useRef(0);
|
|
224
170
|
const [profile, setProfile] = useState();
|
|
225
171
|
const [plan, setPlan] = useState();
|
|
226
172
|
const diagnostics = useRef([]);
|
|
227
|
-
const action = useRef("export");
|
|
228
173
|
const { columns: terminalColumns } = useWindowSize();
|
|
229
174
|
const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
|
|
175
|
+
const hintCursorRow = (mode === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows)[cursorRef.current];
|
|
176
|
+
const enterHint = hintCursorRow === undefined ? undefined
|
|
177
|
+
: "kind" in hintCursorRow ? "Enter open"
|
|
178
|
+
: hintCursorRow.block === "excluded" ? "Enter note" : undefined;
|
|
179
|
+
const groupHint = hintCursorRow !== undefined && "kind" in hintCursorRow ? " · ←/→ group" : "";
|
|
230
180
|
const listingHint = mode === "export-list"
|
|
231
|
-
?
|
|
232
|
-
: "j/k move · space toggle · a all · Enter preview · Esc";
|
|
233
|
-
const
|
|
181
|
+
? `j/k move${groupHint}${enterHint === undefined ? "" : ` · ${enterHint}`} · space toggle · a all · e export · Esc back to menu`
|
|
182
|
+
: "j/k move · space toggle · a all · Enter preview · Esc back to menu";
|
|
183
|
+
const hintText = `${selected.length} selected · ${listingHint}`;
|
|
234
184
|
const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
|
|
235
|
-
const hintRows = Math.max(Math.ceil(stringWidth(
|
|
185
|
+
const hintRows = Math.max(Math.ceil(stringWidth(hintText) / panelWidth), 1);
|
|
236
186
|
const height = useListHeight(listHeight, 4 + titleRows + hintRows);
|
|
237
187
|
const showLines = (next, nextMode) => {
|
|
238
188
|
setLines(next);
|
|
@@ -250,11 +200,6 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
250
200
|
}
|
|
251
201
|
}
|
|
252
202
|
};
|
|
253
|
-
const prompt = (next) => {
|
|
254
|
-
typed.current = "";
|
|
255
|
-
setText("");
|
|
256
|
-
setMode(next);
|
|
257
|
-
};
|
|
258
203
|
const loadRows = (next, nextMode) => {
|
|
259
204
|
setRows(next);
|
|
260
205
|
select(next.filter(row => row.selectable));
|
|
@@ -286,13 +231,122 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
286
231
|
}),
|
|
287
232
|
};
|
|
288
233
|
};
|
|
234
|
+
const beginExport = () => {
|
|
235
|
+
if (!hasChosenResource(chosenProfile())) {
|
|
236
|
+
showLines(["Nothing selected; nothing written."], "results");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
setExportDirectory(undefined);
|
|
240
|
+
setNameError(undefined);
|
|
241
|
+
setMode("export-folder");
|
|
242
|
+
};
|
|
243
|
+
const abandonNameCheck = () => { nameRequest.current += 1; };
|
|
244
|
+
const chooseExportDirectory = (directory) => {
|
|
245
|
+
abandonNameCheck();
|
|
246
|
+
setExportDirectory(directory);
|
|
247
|
+
setNameError(undefined);
|
|
248
|
+
if (typed.current === "")
|
|
249
|
+
setText(DEFAULT_PROFILE_FILE_NAME);
|
|
250
|
+
setMode("export-name");
|
|
251
|
+
};
|
|
252
|
+
const submitExportName = () => {
|
|
253
|
+
const outcome = resolveExportFileName(typed.current);
|
|
254
|
+
if (outcome.kind === "invalid") {
|
|
255
|
+
setNameError(outcome.message);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const directory = exportDirectory ?? startDirectory;
|
|
259
|
+
const fileName = outcome.fileName;
|
|
260
|
+
const finalPath = exportDestination(directory, fileName);
|
|
261
|
+
const id = ++nameRequest.current;
|
|
262
|
+
void (async () => {
|
|
263
|
+
let exists;
|
|
264
|
+
try {
|
|
265
|
+
exists = await existsCheck(finalPath);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
|
|
269
|
+
return;
|
|
270
|
+
setNameError(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const current = resolveExportFileName(typed.current);
|
|
274
|
+
if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
|
|
275
|
+
return;
|
|
276
|
+
if (current.kind !== "ok" || current.fileName !== fileName)
|
|
277
|
+
return;
|
|
278
|
+
if (exists) {
|
|
279
|
+
setNameError(`${finalPath} already exists; choose another name`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
confirmRequest.current = { kind: "export", file: finalPath };
|
|
283
|
+
setNameError(undefined);
|
|
284
|
+
showLines([`Destination: ${finalPath}`, ...profileExportPreview(chosenProfile(), diagnostics.current)], "confirm");
|
|
285
|
+
})();
|
|
286
|
+
};
|
|
287
|
+
const confirmApply = () => {
|
|
288
|
+
const request = confirmRequest.current;
|
|
289
|
+
if (request === undefined)
|
|
290
|
+
return;
|
|
291
|
+
void run(async () => {
|
|
292
|
+
let result;
|
|
293
|
+
if (request.kind === "export") {
|
|
294
|
+
await writeProfileExport(request.file, chosenProfile());
|
|
295
|
+
result = [`Exported profile to ${request.file}`];
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const outcomes = await applyProfileImport(request.plan, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, packageOperations: packages, ...environment.installationOptions });
|
|
299
|
+
result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
|
|
300
|
+
}
|
|
301
|
+
if (mounted.current) {
|
|
302
|
+
showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
const previewImport = () => {
|
|
307
|
+
void run(async () => {
|
|
308
|
+
// Reuse the CLI's core preview for the exact selected subset, including argv and Source notes.
|
|
309
|
+
const entries = selectedPlanEntries(plan, selectedRef.current);
|
|
310
|
+
const subset = selectedImportProfile(profile, entries);
|
|
311
|
+
const imported = await buildProfileImport(subset, operations, apps, {}, packages);
|
|
312
|
+
if (!mounted.current)
|
|
313
|
+
return;
|
|
314
|
+
confirmRequest.current = { kind: "import", plan: imported };
|
|
315
|
+
showLines(imported.preview, "confirm");
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
const chooseImportFile = (selection) => {
|
|
319
|
+
if (selection.kind !== "file")
|
|
320
|
+
return;
|
|
321
|
+
setImportDirectory(selection.directory);
|
|
322
|
+
const id = ++importRequest.current;
|
|
323
|
+
setMode("busy");
|
|
324
|
+
void (async () => {
|
|
325
|
+
try {
|
|
326
|
+
const importedProfile = parseProfile(JSON.parse(await readFile(selection.file, "utf8")));
|
|
327
|
+
const imported = await buildProfileImport(importedProfile, operations, apps, {}, packages);
|
|
328
|
+
if (!mounted.current || importRequest.current !== id)
|
|
329
|
+
return;
|
|
330
|
+
setProfile(importedProfile);
|
|
331
|
+
setPlan(imported);
|
|
332
|
+
setImportNotice(undefined);
|
|
333
|
+
loadRows(rowsFromPlan(imported), "import-list");
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (!mounted.current || importRequest.current !== id)
|
|
337
|
+
return;
|
|
338
|
+
setImportNotice(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
339
|
+
setMode("import-browse");
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
342
|
+
};
|
|
289
343
|
useKeys((input, key) => {
|
|
290
344
|
const current = modeRef.current;
|
|
291
|
-
|
|
345
|
+
// The FilePicker owns its keys while it is mounted, and async work ignores every key.
|
|
346
|
+
if (current === "export-folder" || current === "import-browse" || current === "busy")
|
|
292
347
|
return;
|
|
293
348
|
if (current === "browse" || current === "results") {
|
|
294
349
|
if (input === "e") {
|
|
295
|
-
action.current = "export";
|
|
296
350
|
void run(async () => {
|
|
297
351
|
const exported = await buildProfileExport(operations, apps, {
|
|
298
352
|
homeDirectory: environment.homeDirectory, packageOperations: packages,
|
|
@@ -305,8 +359,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
305
359
|
});
|
|
306
360
|
}
|
|
307
361
|
else if (input === "i") {
|
|
308
|
-
|
|
309
|
-
|
|
362
|
+
setImportNotice(undefined);
|
|
363
|
+
setMode("import-browse");
|
|
310
364
|
}
|
|
311
365
|
else if (current === "results") {
|
|
312
366
|
const next = moveCursor(cursorRef.current, lines.length, input, key);
|
|
@@ -315,35 +369,32 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
315
369
|
}
|
|
316
370
|
return;
|
|
317
371
|
}
|
|
318
|
-
if (current === "export-
|
|
372
|
+
if (current === "export-name") {
|
|
319
373
|
if (key.escape) {
|
|
320
|
-
|
|
374
|
+
abandonNameCheck();
|
|
375
|
+
setMode("export-folder");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (key.ctrl && input === "u") {
|
|
379
|
+
abandonNameCheck();
|
|
380
|
+
setText("");
|
|
381
|
+
setNameError(undefined);
|
|
321
382
|
return;
|
|
322
383
|
}
|
|
323
384
|
if (key.return) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (current === "export-path") {
|
|
327
|
-
showLines(profileExportPreview(chosenProfile(), diagnostics.current), "confirm");
|
|
328
|
-
}
|
|
329
|
-
else
|
|
330
|
-
void run(async () => {
|
|
331
|
-
const importedProfile = parseProfile(JSON.parse(await readFile(typed.current, "utf8")));
|
|
332
|
-
const imported = await buildProfileImport(importedProfile, operations, apps, {}, packages);
|
|
333
|
-
if (!mounted.current)
|
|
334
|
-
return;
|
|
335
|
-
setProfile(importedProfile);
|
|
336
|
-
setPlan(imported);
|
|
337
|
-
loadRows(rowsFromPlan(imported), "import-list");
|
|
338
|
-
});
|
|
385
|
+
submitExportName();
|
|
386
|
+
return;
|
|
339
387
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
setText(typed.current);
|
|
388
|
+
if (key.backspace || key.delete) {
|
|
389
|
+
abandonNameCheck();
|
|
390
|
+
setText(typed.current.slice(0, -1));
|
|
391
|
+
setNameError(undefined);
|
|
392
|
+
return;
|
|
343
393
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
setText(typed.current);
|
|
394
|
+
if (input && !key.ctrl && !key.meta) {
|
|
395
|
+
abandonNameCheck();
|
|
396
|
+
setText(typed.current + input);
|
|
397
|
+
setNameError(undefined);
|
|
347
398
|
}
|
|
348
399
|
return;
|
|
349
400
|
}
|
|
@@ -362,23 +413,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
362
413
|
return;
|
|
363
414
|
}
|
|
364
415
|
if (current === "confirm") {
|
|
365
|
-
answerYesNo(input, key, () =>
|
|
366
|
-
void run(async () => {
|
|
367
|
-
let result;
|
|
368
|
-
if (action.current === "export") {
|
|
369
|
-
await writeProfileExport(typed.current, chosenProfile());
|
|
370
|
-
result = [`Exported profile to ${typed.current}`];
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
const confirmed = chosenPlan(plan, selectedRef.current);
|
|
374
|
-
const outcomes = await applyProfileImport(confirmed, operations, apps, true, () => undefined, { homeDirectory: environment.homeDirectory, packageOperations: packages, ...environment.installationOptions });
|
|
375
|
-
result = outcomes.map(outcome => `${outcome.status}: ${outcome.item.label}${outcome.detail ? `; ${outcome.detail}` : ""}`);
|
|
376
|
-
}
|
|
377
|
-
if (mounted.current) {
|
|
378
|
-
showLines(result.length ? result : ["Nothing selected; nothing written."], "results");
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
}, () => setMode("browse"));
|
|
416
|
+
answerYesNo(input, key, confirmApply, () => setMode(confirmRequest.current?.kind === "import" ? "import-list" : "export-name"));
|
|
382
417
|
if (input === "y" || input === "n" || key.escape)
|
|
383
418
|
return;
|
|
384
419
|
const next = movePreviewCursor(cursorRef.current, lines.length, input, key, height);
|
|
@@ -390,56 +425,52 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
390
425
|
setMode("browse");
|
|
391
426
|
return;
|
|
392
427
|
}
|
|
428
|
+
// export-list and import-list
|
|
393
429
|
const visibleRows = current === "export-list" ? exportDisplayRows(rows, expandedGroupsRef.current) : rows;
|
|
394
430
|
const currentRow = visibleRows[cursorRef.current];
|
|
395
431
|
const next = moveCursor(cursorRef.current, visibleRows.length, input, key);
|
|
396
|
-
if (next !== undefined)
|
|
432
|
+
if (next !== undefined) {
|
|
397
433
|
cursorTo(Math.max(0, next));
|
|
398
|
-
|
|
399
|
-
(key.return || key.rightArrow || key.leftArrow)) {
|
|
400
|
-
if (key.return && !hasChosenResource(chosenProfile())) {
|
|
401
|
-
showLines(["Nothing selected; nothing written."], "results");
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const open = new Set(expandedGroupsRef.current);
|
|
405
|
-
if (key.return) {
|
|
406
|
-
if (open.has(currentRow.id))
|
|
407
|
-
open.delete(currentRow.id);
|
|
408
|
-
else
|
|
409
|
-
open.add(currentRow.id);
|
|
410
|
-
}
|
|
411
|
-
else if (key.rightArrow)
|
|
412
|
-
open.add(currentRow.id);
|
|
413
|
-
else
|
|
414
|
-
open.delete(currentRow.id);
|
|
415
|
-
setExpandedGroups(open);
|
|
434
|
+
return;
|
|
416
435
|
}
|
|
417
|
-
|
|
418
|
-
|
|
436
|
+
if (input === " ") {
|
|
437
|
+
if (currentRow && !("kind" in currentRow) && currentRow.selectable)
|
|
438
|
+
select(toggleChoice(rows, selectedRef.current, currentRow));
|
|
439
|
+
return;
|
|
419
440
|
}
|
|
420
|
-
|
|
441
|
+
if (input === "a") {
|
|
421
442
|
select(selectedRef.current.length ? [] : rows.filter(row => row.selectable));
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (current === "export-list" && input === "e") {
|
|
446
|
+
beginExport();
|
|
447
|
+
return;
|
|
425
448
|
}
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
449
|
+
if (currentRow && "kind" in currentRow && currentRow.kind === "group") {
|
|
450
|
+
if (key.return || key.rightArrow || key.leftArrow) {
|
|
451
|
+
const open = new Set(expandedGroupsRef.current);
|
|
452
|
+
if (key.return) {
|
|
453
|
+
if (open.has(currentRow.id))
|
|
454
|
+
open.delete(currentRow.id);
|
|
455
|
+
else
|
|
456
|
+
open.add(currentRow.id);
|
|
430
457
|
}
|
|
458
|
+
else if (key.rightArrow)
|
|
459
|
+
open.add(currentRow.id);
|
|
431
460
|
else
|
|
432
|
-
|
|
461
|
+
open.delete(currentRow.id);
|
|
462
|
+
setExpandedGroups(open);
|
|
433
463
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (key.return && currentRow && !("kind" in currentRow) && currentRow.block === "excluded") {
|
|
467
|
+
detailReturnId.current = currentRow.id;
|
|
468
|
+
showLines(wrapText(currentRow.label, Math.max((terminalColumns || 80) - 8, 1)), "detail");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (key.return && current === "import-list") {
|
|
472
|
+
previewImport();
|
|
473
|
+
return;
|
|
443
474
|
}
|
|
444
475
|
});
|
|
445
476
|
const listing = mode === "export-list" || mode === "import-list";
|
|
@@ -449,7 +480,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
449
480
|
: stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
|
|
450
481
|
const label = !("kind" in row) && row.block !== "excluded" && !row.selectable
|
|
451
482
|
? `${row.label} (not selectable)` : row.label;
|
|
452
|
-
return
|
|
483
|
+
return wrapText(label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
453
484
|
});
|
|
454
485
|
const listWindow = physicalListWindow(displayRows, rowHeights, cursor, height);
|
|
455
486
|
const window = listing ? listWindow : computeWindow(lines.length, cursor, height);
|
|
@@ -466,14 +497,14 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
466
497
|
const open = expandedGroups.has(row.id);
|
|
467
498
|
const prefix = `${marker}${" ".repeat(row.depth * 2)}${open ? "▾" : "▸"} `;
|
|
468
499
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
469
|
-
const label =
|
|
500
|
+
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
470
501
|
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
471
502
|
remainingRows -= label.split("\n").length;
|
|
472
503
|
return _jsxs(Text, { color: theme.group, bold: true, children: [prefix, label] }, row.id);
|
|
473
504
|
}
|
|
474
505
|
if (row.block === "excluded") {
|
|
475
506
|
const width = Math.max(panelWidth - stringWidth(marker), 1);
|
|
476
|
-
const label =
|
|
507
|
+
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
477
508
|
.join(`\n${" ".repeat(stringWidth(marker))}`);
|
|
478
509
|
remainingRows -= label.split("\n").length;
|
|
479
510
|
return _jsxs(Text, { color: theme.value, children: [marker, label] }, row.id);
|
|
@@ -481,10 +512,18 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
481
512
|
const prefix = `${marker}[${selected.some(choice => choice.id === row.id) ? "x" : " "}] ${row.ordinal ?? row.index + 1} `;
|
|
482
513
|
const value = `${row.label}${row.selectable ? "" : " (not selectable)"}`;
|
|
483
514
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
484
|
-
const label =
|
|
515
|
+
const label = fitLines(wrapText(value, width), remainingRows, width)
|
|
485
516
|
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
486
517
|
remainingRows -= label.split("\n").length;
|
|
487
518
|
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: prefix }), _jsx(Text, { color: theme.value, children: label })] }, row.id);
|
|
488
519
|
});
|
|
489
|
-
|
|
520
|
+
const nameOutcome = resolveExportFileName(text);
|
|
521
|
+
const liveDestination = nameOutcome.kind === "ok"
|
|
522
|
+
? exportDestination(exportDirectory ?? startDirectory, nameOutcome.fileName) : undefined;
|
|
523
|
+
const nameNote = nameOutcome.kind === "ok" ? fileNameNotice(nameOutcome.fileName) : undefined;
|
|
524
|
+
return _jsxs(Box, { flexDirection: "column", children: [mode === "export-folder" ? _jsx(FilePicker, { purpose: "directory", listHeight: listHeight, startDirectory: exportDirectory ?? startDirectory, onCancel: () => setMode("export-list"), onChoose: selection => { if (selection.kind === "directory")
|
|
525
|
+
chooseExportDirectory(selection.directory); } }) : null, mode === "import-browse" ? _jsx(FilePicker, { purpose: "file", listHeight: listHeight, startDirectory: importDirectory ?? startDirectory, notice: importNotice, onCancel: () => setMode("browse"), onChoose: chooseImportFile }) : null, listing ? _jsxs(BorderPanel, { title: mode === "export-list" ? "Export Profile" : "Import Profile", children: [listContent, _jsx(Text, { color: theme.legend, children: hintText }), window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null] }) : null, mode === "browse" ? _jsx(BorderPanel, { title: "Portable Profile", children: _jsx(Text, { color: theme.legend, children: "e export \u00B7 i import" }) }) : null, mode === "busy" ? _jsx(Text, { children: "Loading Profile\u2026" }) : null, mode === "export-name" ? _jsxs(BorderPanel, { title: "Export file name", children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Folder: " }), _jsx(Text, { color: theme.value, children: exportDirectory ?? startDirectory })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Name: " }), _jsxs(Text, { color: theme.value, children: [text, "_"] })] }), liveDestination === undefined ? null : _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: "Destination: " }), _jsx(Text, { color: theme.value, children: liveDestination })] }), nameNote === undefined ? null : _jsx(Text, { color: theme.warning, children: nameNote }), nameOutcome.kind === "ok" && nameOutcome.appendedExtension
|
|
526
|
+
? _jsxs(Text, { color: theme.muted, children: ["A name without an extension is saved as ", nameOutcome.fileName, "."] }) : null, nameError === undefined ? null : _jsx(Text, { color: theme.error, children: nameError }), _jsx(Text, { color: theme.legend, children: "Type to edit \u00B7 Ctrl+U clear \u00B7 Enter continue \u00B7 Esc back to folder" })] }) : null, mode === "confirm" ? _jsx(PreviewConfirm, { lines: lines, cursor: cursor, question: confirmRequest.current?.kind === "export"
|
|
527
|
+
? "Save/Export Profile? (y/n) · arrows/PgUp/PgDn page · j/k line"
|
|
528
|
+
: "Apply Profile? (y/n) · arrows/PgUp/PgDn page · j/k line" }) : null, mode === "detail" ? _jsx(PreviewConfirm, { title: "Export note", lines: lines, cursor: cursor, question: "Esc close \u00B7 arrows/PgUp/PgDn page \u00B7 j/k line" }) : null, mode === "results" ? _jsxs(_Fragment, { children: [_jsx(PreviewLines, { lines: lines.slice(window.start, window.end) }), _jsx(Text, { children: "j/k scroll" })] }) : null, window.indicator && mode === "results" ? _jsx(Text, { children: window.indicator }) : null] });
|
|
490
529
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
3
|
+
/** Split text on grapheme boundaries so wide and combining characters wrap at `width` columns. */
|
|
4
|
+
export function wrapText(value, width) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
let line = "";
|
|
7
|
+
let lineWidth = 0;
|
|
8
|
+
for (const { segment } of graphemes.segment(value)) {
|
|
9
|
+
const segmentWidth = stringWidth(segment);
|
|
10
|
+
if (line && lineWidth + segmentWidth > width) {
|
|
11
|
+
lines.push(line);
|
|
12
|
+
line = "";
|
|
13
|
+
lineWidth = 0;
|
|
14
|
+
}
|
|
15
|
+
line += segment;
|
|
16
|
+
lineWidth += segmentWidth;
|
|
17
|
+
}
|
|
18
|
+
return [...lines, line];
|
|
19
|
+
}
|
|
20
|
+
/** `lines` capped to `maxRows`, with the last visible row shortened to `width` plus an ellipsis. */
|
|
21
|
+
export function fitLines(lines, maxRows, width) {
|
|
22
|
+
if (maxRows <= 0)
|
|
23
|
+
return [];
|
|
24
|
+
if (lines.length <= maxRows)
|
|
25
|
+
return lines;
|
|
26
|
+
const visible = lines.slice(0, maxRows);
|
|
27
|
+
let last = "";
|
|
28
|
+
let used = 0;
|
|
29
|
+
for (const { segment } of graphemes.segment(visible[maxRows - 1])) {
|
|
30
|
+
const segmentWidth = stringWidth(segment);
|
|
31
|
+
if (used + segmentWidth > width - 1)
|
|
32
|
+
break;
|
|
33
|
+
last += segment;
|
|
34
|
+
used += segmentWidth;
|
|
35
|
+
}
|
|
36
|
+
visible[maxRows - 1] = `${last}…`;
|
|
37
|
+
return visible;
|
|
38
|
+
}
|
|
39
|
+
/** One line shortened to `width` columns plus an ellipsis, for single-row labels. */
|
|
40
|
+
export function clipText(value, width) {
|
|
41
|
+
if (stringWidth(value) <= width)
|
|
42
|
+
return value;
|
|
43
|
+
let last = "";
|
|
44
|
+
let used = 0;
|
|
45
|
+
for (const { segment } of graphemes.segment(value)) {
|
|
46
|
+
const segmentWidth = stringWidth(segment);
|
|
47
|
+
if (used + segmentWidth > Math.max(width - 1, 0))
|
|
48
|
+
break;
|
|
49
|
+
last += segment;
|
|
50
|
+
used += segmentWidth;
|
|
51
|
+
}
|
|
52
|
+
return `${last}…`;
|
|
53
|
+
}
|
package/dist/src/tui/window.js
CHANGED
|
@@ -4,7 +4,7 @@ import { headerTabRowCount } from "./header-layout.js";
|
|
|
4
4
|
export const FALLBACK_TERMINAL_ROWS = 24;
|
|
5
5
|
/** Header borders, title, one tab row and footer; extra tab rows are added per terminal width. */
|
|
6
6
|
export const APP_CHROME_ROWS = 5;
|
|
7
|
-
const
|
|
7
|
+
const DEFAULT_MINIMUM_ROWS = 3;
|
|
8
8
|
/** The slice of a list that fits in `height` rows while keeping `selected` visible (roughly centred). */
|
|
9
9
|
export function computeWindow(total, selected, height) {
|
|
10
10
|
const size = Math.max(Math.floor(height), 1);
|
|
@@ -23,10 +23,10 @@ export function pageStep(height) {
|
|
|
23
23
|
* Rows available to a list: the terminal height minus the shell chrome and the
|
|
24
24
|
* view's own `reserved` lines. `override` fixes the height for tests.
|
|
25
25
|
*/
|
|
26
|
-
export function useListHeight(override, reserved) {
|
|
26
|
+
export function useListHeight(override, reserved, minimumRows = DEFAULT_MINIMUM_ROWS) {
|
|
27
27
|
const { rows, columns } = useWindowSize();
|
|
28
28
|
if (override !== undefined)
|
|
29
29
|
return Math.max(override, 1);
|
|
30
30
|
const chromeRows = APP_CHROME_ROWS + headerTabRowCount(Math.max(columns - 4, 1)) - 1;
|
|
31
|
-
return Math.max((rows || FALLBACK_TERMINAL_ROWS) - chromeRows - reserved,
|
|
31
|
+
return Math.max((rows || FALLBACK_TERMINAL_ROWS) - chromeRows - reserved, minimumRows);
|
|
32
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iseoane/agent-depot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "A cross-platform catalog for reusable coding-agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "iseoane",
|
|
@@ -28,8 +28,6 @@
|
|
|
28
28
|
"!dist/src/**/*.map",
|
|
29
29
|
"resources/pi",
|
|
30
30
|
"skills",
|
|
31
|
-
"!skills/**/test_*.py",
|
|
32
|
-
"!skills/**/testdata",
|
|
33
31
|
"README.md",
|
|
34
32
|
"README.es.md",
|
|
35
33
|
"LICENSE",
|
|
@@ -84,7 +82,8 @@
|
|
|
84
82
|
"audit:all": "pnpm audit:dead-code && pnpm audit:dupes && pnpm audit:health",
|
|
85
83
|
"test:e2e": "pnpm build && node --import ./dist/tests/isolated-env.js --test --test-timeout=120000 \"dist/tests/e2e/*.e2e.test.js\"",
|
|
86
84
|
"prepublishOnly": "pnpm build && pnpm test",
|
|
87
|
-
"test:pack": "pnpm build && node scripts/pack-smoke.mjs"
|
|
85
|
+
"test:pack": "pnpm build && node scripts/pack-smoke.mjs",
|
|
86
|
+
"test:skills": "node scripts/test-skills.mjs"
|
|
88
87
|
},
|
|
89
88
|
"dependencies": {
|
|
90
89
|
"ink": "^7.1.1",
|
|
@@ -44,8 +44,10 @@ Cada cita del juez se verifica mecánicamente contra la transcripción por subca
|
|
|
44
44
|
|
|
45
45
|
## Tests
|
|
46
46
|
|
|
47
|
+
Desde la raíz del repositorio Agent Depot, ejecuta:
|
|
48
|
+
|
|
47
49
|
```bash
|
|
48
|
-
|
|
50
|
+
pnpm test:skills
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
Sin dependencias: stdlib de Python 3.9+.
|