@iseoane/agent-depot 0.6.13 → 0.6.15
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 +61 -0
- package/README.md +3 -1
- package/dist/src/app-flow.js +8 -1
- package/dist/src/atomic-file.js +29 -3
- package/dist/src/cli.js +2 -0
- package/dist/src/flow-progress.js +9 -0
- package/dist/src/git-source.js +257 -15
- package/dist/src/preview-model.js +37 -0
- package/dist/src/process-runner.js +1 -0
- package/dist/src/profile-cli.js +12 -4
- package/dist/src/profile-export.js +23 -3
- package/dist/src/skill-update.js +1 -0
- package/dist/src/source-refresh-session.js +139 -0
- package/dist/src/sources.js +4 -4
- package/dist/src/tui/adoption-actions.js +21 -2
- package/dist/src/tui/app-actions.js +57 -47
- package/dist/src/tui/app.js +22 -4
- package/dist/src/tui/bulk-actions.js +45 -11
- package/dist/src/tui/catalog-actions.js +12 -0
- package/dist/src/tui/catalog-flow.js +31 -11
- package/dist/src/tui/catalog-panel.js +15 -13
- package/dist/src/tui/catalog-tree.js +7 -2
- package/dist/src/tui/catalog-view.js +12 -6
- package/dist/src/tui/file-picker.js +2 -1
- package/dist/src/tui/installations-browse.js +1 -1
- package/dist/src/tui/installations-panel.js +12 -4
- package/dist/src/tui/installations-rows.js +2 -2
- package/dist/src/tui/installations-tree.js +1 -1
- package/dist/src/tui/installations-view.js +16 -7
- package/dist/src/tui/keys.js +35 -4
- package/dist/src/tui/manage-actions.js +55 -27
- package/dist/src/tui/manage-panel.js +15 -29
- package/dist/src/tui/mode-keys.js +11 -1
- package/dist/src/tui/package-actions.js +25 -18
- package/dist/src/tui/package-rows.js +9 -4
- package/dist/src/tui/panel-parts.js +54 -37
- package/dist/src/tui/preview-render.js +93 -0
- package/dist/src/tui/preview-window.js +185 -20
- package/dist/src/tui/profile-view.js +54 -44
- package/dist/src/tui/render.js +13 -1
- package/dist/src/tui/safe-text.js +25 -0
- package/dist/src/tui/sources-mode.js +2 -2
- package/dist/src/tui/sources-recipes.js +53 -32
- package/dist/src/tui/sources-view.js +85 -39
- package/dist/src/tui/text-wrap.js +47 -3
- package/dist/src/tui/unmanaged-actions.js +17 -5
- package/dist/src/tui/updates-flow.js +11 -5
- package/dist/src/tui/updates-keys.js +2 -2
- package/dist/src/tui/updates-model.js +5 -2
- package/dist/src/tui/updates-panel.js +10 -9
- package/dist/src/tui/updates-preview.js +27 -44
- package/dist/src/tui/updates-view.js +15 -11
- package/dist/src/tui/updates.js +117 -33
- package/dist/src/update-flow.js +31 -15
- package/package.json +1 -1
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Box, useWindowSize } from "ink";
|
|
5
|
+
import { PreEscapedText, Text } from "./safe-text.js";
|
|
4
6
|
import stringWidth from "string-width";
|
|
5
7
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
6
8
|
import { createAppOperations } from "../app-flow.js";
|
|
7
|
-
import { buildProfileExport, describeProfileExportExclusion, describeProfilePackage, describeProfileReplica, describeProfileSkill, profileExportPreview, SKILL_EXPORT_EXPLANATION, writeProfileExport, } from "../profile-export.js";
|
|
9
|
+
import { buildProfileExport, describeProfileExportExclusion, describeProfilePackage, describeProfileReplica, describeProfileSkill, profileExportPreview, requireOverwritableProfileExport, SKILL_EXPORT_EXPLANATION, writeProfileExport, } from "../profile-export.js";
|
|
8
10
|
import { applyProfileImport, buildProfileImport, } from "../profile-import.js";
|
|
9
11
|
import { PROFILE_FORMAT, PROFILE_FORMAT_V2, parseProfile } from "../profile.js";
|
|
10
12
|
import { selectionKey } from "../package-model.js";
|
|
11
13
|
import { packageOperationsOf } from "./environment.js";
|
|
12
14
|
import { DEFAULT_PROFILE_FILE_NAME, exportDestination, fileNameNotice, pathExists, pickerStartDirectory, resolveExportFileName, } from "./file-picker-model.js";
|
|
13
15
|
import { FilePicker } from "./file-picker.js";
|
|
14
|
-
import { useKeys } from "./keys.js";
|
|
15
|
-
import {
|
|
16
|
+
import { isYes, useKeys } from "./keys.js";
|
|
17
|
+
import { escapeControlText, usePreviewGeometry } from "./preview-window.js";
|
|
16
18
|
import { answerYesNo, moveCursor, movePreviewCursor, toggleChoice } from "./mode-keys.js";
|
|
17
|
-
import { BorderPanel,
|
|
18
|
-
import { clipText, elideMiddle, fitLines, wrapText } from "./text-wrap.js";
|
|
19
|
+
import { BorderPanel, PagedPreview, PreviewLines, Working } from "./panel-parts.js";
|
|
20
|
+
import { clipText, elideMiddle, fitLines, textUnits, wrapText } from "./text-wrap.js";
|
|
19
21
|
import { theme } from "./theme.js";
|
|
20
22
|
import { useMirrored, useMounted } from "./view-state.js";
|
|
21
23
|
import { computeWindow, useListHeight } from "./window.js";
|
|
@@ -136,32 +138,36 @@ function physicalListWindow(rows, rowHeights, selected, height) {
|
|
|
136
138
|
/** An error naming a long path keeps its first and last rows, where the path starts and the reason ends. */
|
|
137
139
|
const NAME_ERROR_ROWS = 3;
|
|
138
140
|
const NAME_HINT = "Type to edit · Ctrl+U clear · Enter continue · Esc back to folder";
|
|
141
|
+
const NOTE_CHROME = { title: "Export note", cancel: "[Esc] close" };
|
|
139
142
|
/**
|
|
140
143
|
* A `Label: value` line of the export name stage, wrapped to the panel and kept within `rows` rows. The
|
|
141
144
|
* end of a path (its last folder, the file name) and the end of the typed name (where the cursor is) are
|
|
142
145
|
* what matter, so with fewer than three rows the field keeps its label and its tail behind a `…`; with
|
|
143
146
|
* three or more a path keeps its first row too, around a count of the rows left out.
|
|
144
147
|
*/
|
|
145
|
-
function NameField({ label, value, rows, width, tail = false }) {
|
|
146
|
-
|
|
148
|
+
export function NameField({ label, value, rows, width, tail = false }) {
|
|
149
|
+
// Measured and drawn as escaped, so a hostile path or name takes the rows it is shown on.
|
|
150
|
+
const escapedValue = escapeControlText(value);
|
|
151
|
+
const wrapped = wrapText(`${label}${escapedValue}`, width);
|
|
147
152
|
const cut = wrapped.length > rows;
|
|
148
153
|
const keepsMiddleCount = cut && !tail && rows > 2;
|
|
149
154
|
let shown = wrapped;
|
|
150
155
|
if (keepsMiddleCount)
|
|
151
156
|
shown = elideMiddle(wrapped, rows, width);
|
|
152
157
|
else if (cut) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
// Dropped by units, so the cut cannot leave half of a doubled backslash that would read as a real escape.
|
|
159
|
+
const units = textUnits(escapedValue).map(({ segment }) => segment);
|
|
160
|
+
for (let dropped = 1; dropped <= units.length; dropped += 1) {
|
|
161
|
+
shown = wrapText(`${label}…${units.slice(dropped).join("")}`, width);
|
|
156
162
|
if (shown.length <= rows)
|
|
157
163
|
break;
|
|
158
164
|
}
|
|
159
165
|
}
|
|
160
166
|
return _jsx(_Fragment, { children: shown.map((line, index) => {
|
|
161
167
|
if (index === 0 && line.startsWith(label)) {
|
|
162
|
-
return _jsxs(
|
|
168
|
+
return _jsxs(PreEscapedText, { children: [_jsx(PreEscapedText, { color: theme.field, children: label }), _jsx(PreEscapedText, { color: theme.value, children: line.slice(label.length) })] }, index);
|
|
163
169
|
}
|
|
164
|
-
return _jsx(
|
|
170
|
+
return _jsx(PreEscapedText, { color: keepsMiddleCount && index === 1 ? theme.muted : theme.value, children: line }, index);
|
|
165
171
|
}) });
|
|
166
172
|
}
|
|
167
173
|
function planEntries(plan) {
|
|
@@ -238,7 +244,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
238
244
|
const [profile, setProfile] = useState();
|
|
239
245
|
const [plan, setPlan] = useState();
|
|
240
246
|
const diagnostics = useRef([]);
|
|
241
|
-
const {
|
|
247
|
+
const { columns: terminalColumns } = useWindowSize();
|
|
242
248
|
const panelWidth = Math.max((terminalColumns || 80) - 4, 1);
|
|
243
249
|
const hintCursorRow = listRows(mode, rows, expandedGroupsRef.current)[cursorRef.current];
|
|
244
250
|
const enterHint = hintCursorRow === undefined ? undefined
|
|
@@ -255,13 +261,17 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
255
261
|
const titleRows = Math.max(Math.ceil(stringWidth("Export Profile") / panelWidth), 1);
|
|
256
262
|
const hintRows = Math.max(Math.ceil(stringWidth(hintText) / panelWidth), 1);
|
|
257
263
|
const height = useListHeight(listHeight, 4 + titleRows + hintRows);
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
const previewGeometry = usePreviewGeometry();
|
|
265
|
+
const confirmChrome = () => {
|
|
266
|
+
const request = confirmRequest.current;
|
|
267
|
+
if (request?.kind !== "export")
|
|
268
|
+
return { title: "Confirmation", action: "Apply Profile", consequence: "Apply Profile? (y/n)" };
|
|
269
|
+
if (!request.overwrite)
|
|
270
|
+
return { title: "Confirmation", action: "Save/Export Profile", consequence: "Save/Export Profile? (y/n)" };
|
|
271
|
+
// The full path is on the Destination line; the base name keeps the footer short on narrow terminals.
|
|
272
|
+
const name = path.basename(request.file);
|
|
273
|
+
return { title: "Confirmation", action: `Overwrite ${name}`, consequence: `Overwrite ${name}? (y/n)` };
|
|
274
|
+
};
|
|
265
275
|
const showLines = (next, nextMode) => {
|
|
266
276
|
setLines(next);
|
|
267
277
|
cursorTo(0);
|
|
@@ -348,6 +358,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
348
358
|
let exists;
|
|
349
359
|
try {
|
|
350
360
|
exists = await existsCheck(finalPath);
|
|
361
|
+
if (exists)
|
|
362
|
+
await requireOverwritableProfileExport(finalPath);
|
|
351
363
|
}
|
|
352
364
|
catch (error) {
|
|
353
365
|
if (!mounted.current || nameRequest.current !== id || modeRef.current !== "export-name")
|
|
@@ -363,13 +375,10 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
363
375
|
return;
|
|
364
376
|
if (current.kind !== "ok" || current.fileName !== fileName)
|
|
365
377
|
return;
|
|
366
|
-
|
|
367
|
-
setNameError(`${finalPath} already exists; choose another name`);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
confirmRequest.current = { kind: "export", file: finalPath };
|
|
378
|
+
confirmRequest.current = { kind: "export", file: finalPath, overwrite: exists };
|
|
371
379
|
setNameError(undefined);
|
|
372
|
-
|
|
380
|
+
const destination = exists ? `Destination: ${finalPath} (exists; it will be replaced)` : `Destination: ${finalPath}`;
|
|
381
|
+
showLines([destination, ...profileExportPreview(chosenProfile(), diagnostics.current)], "confirm");
|
|
373
382
|
})();
|
|
374
383
|
};
|
|
375
384
|
const confirmApply = () => {
|
|
@@ -379,7 +388,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
379
388
|
void run(request.kind === "export" ? "Exporting…" : "Applying import…", async () => {
|
|
380
389
|
let result;
|
|
381
390
|
if (request.kind === "export") {
|
|
382
|
-
await writeProfileExport(request.file, chosenProfile());
|
|
391
|
+
await writeProfileExport(request.file, chosenProfile(), { overwrite: request.overwrite });
|
|
383
392
|
result = [`Exported profile to ${request.file}`];
|
|
384
393
|
}
|
|
385
394
|
else {
|
|
@@ -495,20 +504,21 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
495
504
|
setMode("export-list");
|
|
496
505
|
return;
|
|
497
506
|
}
|
|
498
|
-
const
|
|
507
|
+
const viewport = previewGeometry(lines, cursorRef.current, NOTE_CHROME);
|
|
508
|
+
const next = movePreviewCursor(cursorRef.current, viewport.total, input, key, viewport.height);
|
|
499
509
|
if (next !== undefined)
|
|
500
510
|
cursorTo(Math.max(0, next));
|
|
501
511
|
return;
|
|
502
512
|
}
|
|
503
513
|
if (current === "confirm") {
|
|
504
|
-
answerYesNo(input, key, confirmApply, () => setMode(confirmRequest.current?.kind === "import" ? "import-list" : "export-name"));
|
|
505
|
-
if (input === "y" || input === "n" || key.escape)
|
|
506
|
-
return;
|
|
507
514
|
// The preview cursor counts wrapped rows, not source lines, or a long destination would cap the paging.
|
|
508
|
-
const viewport =
|
|
515
|
+
const viewport = previewGeometry(lines, cursorRef.current, confirmChrome());
|
|
509
516
|
const next = movePreviewCursor(cursorRef.current, viewport.total, input, key, viewport.height);
|
|
510
517
|
if (next !== undefined)
|
|
511
518
|
cursorTo(Math.max(0, next));
|
|
519
|
+
else if (!isYes(input, key) || viewport.reachedEnd) {
|
|
520
|
+
answerYesNo(input, key, confirmApply, () => setMode(confirmRequest.current?.kind === "import" ? "import-list" : "export-name"));
|
|
521
|
+
}
|
|
512
522
|
return;
|
|
513
523
|
}
|
|
514
524
|
if (key.escape) {
|
|
@@ -574,7 +584,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
574
584
|
if ("kind" in row)
|
|
575
585
|
return 1;
|
|
576
586
|
const prefixWidth = row.block === "excluded" ? 2 : stringWidth(`> [ ] ${row.ordinal ?? row.index + 1} `);
|
|
577
|
-
return wrapText(row.label, Math.max(panelWidth - prefixWidth, 1)).length;
|
|
587
|
+
return wrapText(escapeControlText(row.label), Math.max(panelWidth - prefixWidth, 1)).length;
|
|
578
588
|
});
|
|
579
589
|
const listWindow = physicalListWindow(displayRows, rowHeights, cursor, height);
|
|
580
590
|
const window = listing ? listWindow : computeWindow(lines.length, cursor, height);
|
|
@@ -592,24 +602,24 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
592
602
|
const prefix = `${marker}${" ".repeat(row.depth * 2)}${open ? "▾" : "▸"} `;
|
|
593
603
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
594
604
|
remainingRows -= 1;
|
|
595
|
-
return _jsxs(
|
|
605
|
+
return _jsxs(PreEscapedText, { color: theme.group, bold: true, children: [prefix, clipText(escapeControlText(row.label), width)] }, row.id);
|
|
596
606
|
}
|
|
597
607
|
if (row.block === "excluded") {
|
|
598
608
|
const width = Math.max(panelWidth - stringWidth(marker), 1);
|
|
599
|
-
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
609
|
+
const label = fitLines(wrapText(escapeControlText(row.label), width), remainingRows, width)
|
|
600
610
|
.join(`\n${" ".repeat(stringWidth(marker))}`);
|
|
601
611
|
remainingRows -= label.split("\n").length;
|
|
602
|
-
return _jsxs(
|
|
612
|
+
return _jsxs(PreEscapedText, { color: theme.value, children: [marker, label] }, row.id);
|
|
603
613
|
}
|
|
604
614
|
// A row the import never changes has no checkbox: unchanged items rest, conflicts warn.
|
|
605
615
|
const box = row.selectable ? `[${selected.some(choice => choice.id === row.id) ? "x" : " "}]` : " ";
|
|
606
616
|
const prefix = `${marker}${box} ${row.ordinal ?? row.index + 1} `;
|
|
607
617
|
const width = Math.max(panelWidth - stringWidth(prefix), 1);
|
|
608
|
-
const label = fitLines(wrapText(row.label, width), remainingRows, width)
|
|
618
|
+
const label = fitLines(wrapText(escapeControlText(row.label), width), remainingRows, width)
|
|
609
619
|
.join(`\n${" ".repeat(stringWidth(prefix))}`);
|
|
610
620
|
remainingRows -= label.split("\n").length;
|
|
611
621
|
const color = row.status === "conflict" ? theme.warning : row.status === "same" ? theme.muted : theme.value;
|
|
612
|
-
return _jsxs(
|
|
622
|
+
return _jsxs(PreEscapedText, { children: [_jsx(PreEscapedText, { color: theme.field, children: prefix }), _jsx(PreEscapedText, { color: color, children: label })] }, row.id);
|
|
613
623
|
});
|
|
614
624
|
const nameOutcome = resolveExportFileName(text);
|
|
615
625
|
const liveDestination = nameOutcome.kind === "ok"
|
|
@@ -617,8 +627,8 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
617
627
|
const nameNote = nameOutcome.kind === "ok" ? fileNameNotice(nameOutcome.fileName) : undefined;
|
|
618
628
|
const extensionNote = nameOutcome.kind === "ok" && nameOutcome.appendedExtension
|
|
619
629
|
? `A name without an extension is saved as ${nameOutcome.fileName}.` : undefined;
|
|
620
|
-
const textRows = (value) => value === undefined ? 0 : wrapText(value, panelWidth).length;
|
|
621
|
-
const nameErrorLines = nameError === undefined ? [] : wrapText(nameError, panelWidth);
|
|
630
|
+
const textRows = (value) => value === undefined ? 0 : wrapText(escapeControlText(value), panelWidth).length;
|
|
631
|
+
const nameErrorLines = nameError === undefined ? [] : wrapText(escapeControlText(nameError), panelWidth);
|
|
622
632
|
// Keys type into the name, so the stage cannot scroll; instead each long field keeps its end (and, given
|
|
623
633
|
// three rows or more, its first row) within a share of the terminal. The confirmation pages the full destination.
|
|
624
634
|
const nameFixedRows = 3 + textRows(nameNote) + textRows(extensionNote) + Math.min(nameErrorLines.length, NAME_ERROR_ROWS)
|
|
@@ -629,7 +639,7 @@ export function ProfileView({ operations, environment = {}, onCapturingChange, l
|
|
|
629
639
|
const destinationRows = Math.floor(fieldRows / 3) + (fieldRows % 3 >= 1 ? 1 : 0);
|
|
630
640
|
const nameRows = fieldRows - folderRows - destinationRows;
|
|
631
641
|
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")
|
|
632
|
-
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" && working ? _jsx(Working, { label: working.label, since: working.startedAt }) : null, mode === "export-name" ? _jsxs(BorderPanel, { title: "Export file name", children: [_jsx(NameField, { label: "Folder: ", value: exportDirectory ?? startDirectory, rows: folderRows, width: panelWidth }), _jsx(NameField, { label: "Name: ", value: `${text}_`, rows: nameRows, width: panelWidth, tail: true }), liveDestination === undefined ? null : _jsx(NameField, { label: "Destination: ", value: liveDestination, rows: destinationRows, width: panelWidth }), nameNote === undefined ? null : _jsx(
|
|
633
|
-
? _jsx(
|
|
634
|
-
: elideMiddle(nameErrorLines, NAME_ERROR_ROWS, panelWidth).map((line, index) => _jsx(
|
|
642
|
+
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" && working ? _jsx(Working, { label: working.label, since: working.startedAt }) : null, mode === "export-name" ? _jsxs(BorderPanel, { title: "Export file name", children: [_jsx(NameField, { label: "Folder: ", value: exportDirectory ?? startDirectory, rows: folderRows, width: panelWidth }), _jsx(NameField, { label: "Name: ", value: `${text}_`, rows: nameRows, width: panelWidth, tail: true }), liveDestination === undefined ? null : _jsx(NameField, { label: "Destination: ", value: liveDestination, rows: destinationRows, width: panelWidth }), nameNote === undefined ? null : _jsx(PreEscapedText, { color: theme.warning, children: escapeControlText(nameNote) }), extensionNote === undefined ? null : _jsx(PreEscapedText, { color: theme.muted, children: escapeControlText(extensionNote) }), nameError === undefined ? null : nameErrorLines.length <= NAME_ERROR_ROWS
|
|
643
|
+
? _jsx(PreEscapedText, { color: theme.error, children: escapeControlText(nameError) })
|
|
644
|
+
: elideMiddle(nameErrorLines, NAME_ERROR_ROWS, panelWidth).map((line, index) => _jsx(PreEscapedText, { color: theme.error, children: line }, index)), nameChecking === undefined ? null : _jsx(Working, { label: "Checking name\u2026", since: nameChecking }), _jsx(Text, { color: theme.legend, children: NAME_HINT })] }) : null, mode === "confirm" ? _jsx(PagedPreview, { lines: lines, viewport: previewGeometry(lines, cursor, confirmChrome()) }) : null, mode === "detail" ? _jsx(PagedPreview, { lines: lines, viewport: previewGeometry(lines, cursor, NOTE_CHROME) }) : 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] });
|
|
635
645
|
}
|
package/dist/src/tui/render.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
|
+
import { createSourceRefreshSession } from "../source-refresh-session.js";
|
|
3
4
|
import { App } from "./app.js";
|
|
4
5
|
/** Renders the TUI and resolves once the user exits it. */
|
|
5
6
|
export async function renderTui(operations, environment) {
|
|
6
|
-
const
|
|
7
|
+
const refreshSession = createSourceRefreshSession(operations);
|
|
8
|
+
const instance = render(_jsx(App, { operations: operations, environment: environment, refreshSession: refreshSession, refreshOnStart: environment?.refreshOnStart ?? true }), { alternateScreen: true });
|
|
7
9
|
await instance.waitUntilExit();
|
|
10
|
+
await finishRunningRefreshes(refreshSession, (message) => process.stderr.write(message));
|
|
11
|
+
}
|
|
12
|
+
/** Drops the queued refreshes and waits for the running ones, telling the user only when it has to wait. */
|
|
13
|
+
export async function finishRunningRefreshes(refreshSession, write) {
|
|
14
|
+
refreshSession.stop();
|
|
15
|
+
// Killing a refresh would leave its Source cache lock behind, so wait for the running ones instead.
|
|
16
|
+
if ([...refreshSession.snapshot().values()].some((state) => state.status === "refreshing")) {
|
|
17
|
+
write("Waiting for the Source refreshes still running to finish…\n");
|
|
18
|
+
await refreshSession.settled();
|
|
19
|
+
}
|
|
8
20
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text as InkText } from "ink";
|
|
3
|
+
import { cloneElement, Fragment, isValidElement } from "react";
|
|
4
|
+
import { escapeControlText } from "./preview-window.js";
|
|
5
|
+
/** Ink's `Text` for text that was already passed through `escapeControlText`; escaping it again would double its backslashes. */
|
|
6
|
+
export const PreEscapedText = InkText;
|
|
7
|
+
// Messages and errors span lines on purpose and Ink lays them out, so a line break is the one control character left alone.
|
|
8
|
+
const LINE_BREAK = /\r?\n/u;
|
|
9
|
+
function escapeNode(node) {
|
|
10
|
+
if (typeof node === "string")
|
|
11
|
+
return node.split(LINE_BREAK).map(escapeControlText).join("\n");
|
|
12
|
+
if (Array.isArray(node))
|
|
13
|
+
return node.map(escapeNode);
|
|
14
|
+
if (isValidElement(node) && node.type === Fragment) {
|
|
15
|
+
return cloneElement(node, undefined, escapeNode(node.props.children));
|
|
16
|
+
}
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The `Text` every view uses. Skill, recipe and App text can come from a remote Source or a user-written recipe; raw, an ESC
|
|
21
|
+
* sequence in it can retitle the window, write the clipboard or redraw the screen. Nested `Text` escapes its own children.
|
|
22
|
+
*/
|
|
23
|
+
export function Text({ children, ...props }) {
|
|
24
|
+
return _jsx(InkText, { ...props, children: escapeNode(children) });
|
|
25
|
+
}
|
|
@@ -11,9 +11,9 @@ export function modeReducer(mode, event) {
|
|
|
11
11
|
case "confirm":
|
|
12
12
|
return event.action === "refresh"
|
|
13
13
|
? { kind: "confirm", action: "refresh", sources: event.sources, cursor: 0 }
|
|
14
|
-
: { kind: "confirm", action: "remove", source: event.source };
|
|
14
|
+
: { kind: "confirm", action: "remove", source: event.source, cursor: 0 };
|
|
15
15
|
case "preview-cursor":
|
|
16
|
-
return mode.kind === "confirm"
|
|
16
|
+
return mode.kind === "confirm" ? { ...mode, cursor: event.cursor } : mode;
|
|
17
17
|
case "busy":
|
|
18
18
|
return busy(event.label, mode, () => event.startedAt);
|
|
19
19
|
case "done":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
5
|
import { createAppOperations } from "../app-flow.js";
|
|
5
6
|
import { planAppRecipeRemoval, removeAppRecipe } from "../app-recipe-removal.js";
|
|
@@ -7,12 +8,14 @@ import { BUILT_IN_SOURCE } from "../sources.js";
|
|
|
7
8
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
8
9
|
import { ActionPanel } from "./catalog-panel.js";
|
|
9
10
|
import { hasUnmanagedCopy, isInstalled, loadInstalledSkills } from "./catalog-installs.js";
|
|
10
|
-
import { appName, appRecipePreviewLines,
|
|
11
|
+
import { appName, appPlanLines, appRecipePreviewLines, recipeApprovalChrome } from "./app-actions.js";
|
|
11
12
|
import { errorText } from "./batch.js";
|
|
12
13
|
import { useMounted } from "./view-state.js";
|
|
13
|
-
import { BorderPanel, DecisionPrompt,
|
|
14
|
+
import { BorderPanel, DecisionPrompt, PagedPreview, Working } from "./panel-parts.js";
|
|
15
|
+
import { isYes } from "./keys.js";
|
|
14
16
|
import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
|
|
15
|
-
import {
|
|
17
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
18
|
+
import { lineText, textLine } from "./preview-render.js";
|
|
16
19
|
import { theme } from "./theme.js";
|
|
17
20
|
import { busy } from "./working-model.js";
|
|
18
21
|
/**
|
|
@@ -34,24 +37,24 @@ export function groupRecipeRows(rows) {
|
|
|
34
37
|
function recipeMemberLine(row) {
|
|
35
38
|
return `${row.entry.recipe?.platform ?? "all platforms"} · ${row.status} · ${row.entry.file}${row.entry.applicable ? " (used here)" : ""}`;
|
|
36
39
|
}
|
|
37
|
-
function recipeApprovalChrome(entry) {
|
|
38
|
-
return {
|
|
39
|
-
title: "Recipe approval · does not install",
|
|
40
|
-
questions: [`Approve ${appName(entry)}? y/n · recipe approval does not install the App.`, "Scroll to end before y; n cancels."],
|
|
41
|
-
footerRows: 2,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
40
|
function uninstallChrome(mode) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const name = appName(mode.removal.entry);
|
|
42
|
+
return mode.kind === "uninstall-manual"
|
|
43
|
+
? { title: "App manual uninstall", action: "Check the uninstall now", consequence: "Complete the manual uninstall, then check now? y/n" }
|
|
44
|
+
: { title: "App uninstall confirmation", action: `Uninstall ${name}`, consequence: `Uninstall ${name}? y/n` };
|
|
45
|
+
}
|
|
46
|
+
function removeChrome(removal) {
|
|
47
|
+
const name = appName(removal.entry);
|
|
48
|
+
return { title: "Recipe removal", action: `Remove the ${name} recipe`, consequence: `Remove recipe for ${name}? y/n` };
|
|
49
|
+
}
|
|
50
|
+
function removeLines(removal) {
|
|
51
|
+
return [`Delete recipe: ${removal.file}`, "Only this recipe file or symlink will be removed; App data and Host configuration are not deleted."];
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
52
54
|
* Recipe loading and captured actions; Sources owns the single list and cursor.
|
|
53
55
|
*/
|
|
54
|
-
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile,
|
|
56
|
+
export function useSourcesRecipes({ operations, environment, onCapturingChange, highlightedFile, }) {
|
|
57
|
+
const previewGeometry = usePreviewGeometry();
|
|
55
58
|
const homeDirectory = environment?.homeDirectory;
|
|
56
59
|
const appEnvironment = environment?.appEnvironment;
|
|
57
60
|
const apps = useMemo(() => createAppOperations({
|
|
@@ -90,7 +93,9 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
90
93
|
kind: "approve",
|
|
91
94
|
entry, removeAfterApproval, cursor: 0,
|
|
92
95
|
lines: [
|
|
93
|
-
...(members.length > 1
|
|
96
|
+
...(members.length > 1
|
|
97
|
+
? [textLine(`Recipes named ${appName(entry)}:`), ...members.map(row => textLine(` ${recipeMemberLine(row)}`)), textLine(" ")]
|
|
98
|
+
: []),
|
|
94
99
|
...appRecipePreviewLines(entry, status, steps),
|
|
95
100
|
],
|
|
96
101
|
});
|
|
@@ -106,6 +111,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
106
111
|
const flow = {
|
|
107
112
|
operations,
|
|
108
113
|
env: environment ?? {},
|
|
114
|
+
previewGeometry,
|
|
109
115
|
isMounted: () => mounted.current,
|
|
110
116
|
setAction: action => setMode(action.kind === "browse"
|
|
111
117
|
? { kind: "browse" }
|
|
@@ -161,7 +167,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
161
167
|
}
|
|
162
168
|
const removal = await planAppRecipeRemoval(apps, entry);
|
|
163
169
|
if (mounted.current)
|
|
164
|
-
setMode(
|
|
170
|
+
setMode(removal.installed ? { kind: "uninstall-question", removal } : { kind: "remove", removal, cursor: 0 });
|
|
165
171
|
}
|
|
166
172
|
catch (error) {
|
|
167
173
|
if (mounted.current) {
|
|
@@ -181,7 +187,7 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
181
187
|
if (result.status === "manual required")
|
|
182
188
|
setMode({ ...current, kind: "uninstall-manual" });
|
|
183
189
|
else if (result.status === "untracked")
|
|
184
|
-
setMode({ kind: "remove", removal: current.removal });
|
|
190
|
+
setMode({ kind: "remove", removal: current.removal, cursor: 0 });
|
|
185
191
|
else {
|
|
186
192
|
setError(`Uninstall failed: ${result.reason ?? result.status}; recipe retained`);
|
|
187
193
|
setMode({ kind: "browse" });
|
|
@@ -229,13 +235,13 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
229
235
|
return true;
|
|
230
236
|
}
|
|
231
237
|
if (current.kind === "approve") {
|
|
232
|
-
const viewport =
|
|
238
|
+
const viewport = previewGeometry(current.lines.map(lineText), current.cursor, recipeApprovalChrome(current.entry));
|
|
233
239
|
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
234
240
|
if (cursor !== undefined)
|
|
235
241
|
setMode({ ...current, cursor });
|
|
236
242
|
else if (input === "n" || key.escape)
|
|
237
243
|
cancel();
|
|
238
|
-
else if (input
|
|
244
|
+
else if (isYes(input, key) && viewport.reachedEnd) {
|
|
239
245
|
setBusy("Approving App recipe…");
|
|
240
246
|
void apps.approve(current.entry).then(async () => {
|
|
241
247
|
await reload();
|
|
@@ -268,18 +274,26 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
268
274
|
return true;
|
|
269
275
|
}
|
|
270
276
|
if (current.kind === "uninstall-confirm" || current.kind === "uninstall-manual") {
|
|
271
|
-
const lines =
|
|
272
|
-
const viewport =
|
|
277
|
+
const lines = appPlanLines(current.plan).map(lineText);
|
|
278
|
+
const viewport = previewGeometry(lines, current.cursor, uninstallChrome(current));
|
|
273
279
|
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
274
280
|
if (cursor !== undefined)
|
|
275
281
|
setMode({ ...current, cursor });
|
|
276
282
|
else if (input === "n" || key.escape)
|
|
277
283
|
cancel();
|
|
278
|
-
else if (input
|
|
284
|
+
else if (isYes(input, key) && viewport.reachedEnd)
|
|
279
285
|
void uninstall(current);
|
|
280
286
|
return true;
|
|
281
287
|
}
|
|
282
288
|
if (current.kind === "remove") {
|
|
289
|
+
const viewport = previewGeometry(removeLines(current.removal), current.cursor, removeChrome(current.removal));
|
|
290
|
+
const cursor = movePreviewCursor(current.cursor, viewport.total, input, key, viewport.height);
|
|
291
|
+
if (cursor !== undefined) {
|
|
292
|
+
setMode({ ...current, cursor });
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (isYes(input, key) && !viewport.reachedEnd)
|
|
296
|
+
return true;
|
|
283
297
|
answerYesNo(input, key, () => {
|
|
284
298
|
setBusy("Removing App recipe…");
|
|
285
299
|
void removeAppRecipe(apps, current.removal, true).then(async () => {
|
|
@@ -344,15 +358,22 @@ export function useSourcesRecipes({ operations, environment, onCapturingChange,
|
|
|
344
358
|
}
|
|
345
359
|
}, [apps, mounted]);
|
|
346
360
|
useEffect(() => { void reload(); }, [reload]);
|
|
347
|
-
|
|
361
|
+
// A paged preview sizes itself to the whole terminal, so no message row may sit above it.
|
|
362
|
+
const pagedPreview = (mode.kind === "install" && mode.action.kind === "confirm-install")
|
|
363
|
+
|| ["approve", "remove", "uninstall-confirm", "uninstall-manual"].includes(mode.kind);
|
|
364
|
+
const panel = (_jsxs(Box, { flexDirection: "column", children: [loadError && !pagedPreview ? _jsxs(Text, { color: theme.error, children: ["Error loading App recipes: ", loadError] }) : null, error && !pagedPreview ? _jsx(Text, { color: theme.error, children: error }) : null, notice && !pagedPreview ? _jsx(Text, { color: theme.success, children: notice }) : null, mode.kind === "guide" ? (_jsxs(BorderPanel, { title: "Add an App recipe", children: [_jsx(Text, { children: "1 With the AI skill \u00B7 install if missing, then copy the prompt" }), _jsx(Text, { children: "2 Manual \u00B7 write a recipe using the schema" }), _jsx(Text, { children: "n or Esc closes" })] })) : null, mode.kind === "manual" ? (_jsxs(BorderPanel, { title: "Write an App recipe", children: [_jsxs(Text, { children: ["Write a JSON recipe in ", apps.directory] }), _jsx(Text, { children: "Schema: agent-depot app schema \u00B7 n or Esc closes" })] })) : null, mode.kind === "prompt" ? (_jsxs(BorderPanel, { title: "App recipe authoring prompt", children: [_jsx(Text, { children: "Use the agent-depot-apprecipe skill to create the recipe for <app>" }), _jsxs(Text, { children: ["Recipes: ", apps.directory, " \u00B7 n or Esc closes"] })] })) : null, mode.kind === "install" ? _jsx(ActionPanel, { mode: mode.action }) : null, mode.kind === "busy" ? _jsx(BorderPanel, { title: "App recipe workflow", children: _jsx(Working, { label: mode.label, since: mode.startedAt }) }) : null, mode.kind === "approve" ? (() => {
|
|
348
365
|
const chrome = recipeApprovalChrome(mode.entry);
|
|
349
|
-
const
|
|
350
|
-
return _jsx(
|
|
366
|
+
const lines = mode.lines.map(lineText);
|
|
367
|
+
return _jsx(PagedPreview, { lines: lines, styled: mode.lines, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
351
368
|
})() : null, mode.kind === "pick" ? (_jsxs(BorderPanel, { title: "Remove which recipe?", children: [mode.members.map((row, index) => _jsxs(Text, { children: [index + 1, " ", recipeMemberLine(row)] }, row.entry.file)), _jsx(Text, { color: theme.legend, children: "Press a number to review that one file for removal \u00B7 n or Esc cancels" })] })) : null, mode.kind === "uninstall-question" ? (_jsxs(BorderPanel, { title: "Uninstall before recipe removal", children: [_jsxs(Text, { children: ["App ", appName(mode.removal.entry), " is installed or tracked", mode.removal.installedVersion ? ` (${mode.removal.installedVersion})` : "", "."] }), _jsxs(Text, { children: [_jsx(DecisionPrompt, { children: "Uninstall it before removing the recipe?" }), _jsx(Text, { color: theme.legend, children: "(n cancels removal)" })] })] })) : null, mode.kind === "uninstall-confirm" || mode.kind === "uninstall-manual" ? (() => {
|
|
352
369
|
const chrome = uninstallChrome(mode);
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
return _jsx(
|
|
356
|
-
})() : null, mode.kind === "remove" ? (
|
|
370
|
+
const styled = appPlanLines(mode.plan);
|
|
371
|
+
const lines = styled.map(lineText);
|
|
372
|
+
return _jsx(PagedPreview, { lines: lines, styled: styled, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
373
|
+
})() : null, mode.kind === "remove" ? (() => {
|
|
374
|
+
const chrome = removeChrome(mode.removal);
|
|
375
|
+
const lines = removeLines(mode.removal);
|
|
376
|
+
return _jsx(PagedPreview, { lines: lines, viewport: previewGeometry(lines, mode.cursor, chrome) });
|
|
377
|
+
})() : null] }));
|
|
357
378
|
return { rows, loadingSince, handleKey, startAdd, reload, clearMessage: () => { setError(undefined); setNotice(undefined); }, panel, directory: apps.directory, capturing: mode.kind !== "browse" };
|
|
358
379
|
}
|