@iseoane/agent-depot 0.6.13 → 0.6.14
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 +32 -0
- package/README.md +2 -1
- package/dist/src/flow-progress.js +6 -0
- package/dist/src/git-source.js +18 -6
- package/dist/src/preview-model.js +34 -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 +54 -45
- package/dist/src/tui/app.js +22 -4
- package/dist/src/tui/bulk-actions.js +42 -9
- 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-panel.js +12 -4
- package/dist/src/tui/installations-rows.js +1 -1
- package/dist/src/tui/installations-view.js +16 -7
- package/dist/src/tui/keys.js +27 -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 +2 -1
- package/dist/src/tui/panel-parts.js +45 -36
- package/dist/src/tui/preview-render.js +93 -0
- package/dist/src/tui/preview-window.js +141 -10
- package/dist/src/tui/profile-view.js +49 -40
- package/dist/src/tui/render.js +11 -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 +86 -39
- 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 +8 -10
- package/dist/src/tui/updates.js +117 -33
- package/dist/src/update-flow.js +31 -15
- package/package.json +1 -1
package/dist/src/tui/app.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box,
|
|
3
|
-
import {
|
|
2
|
+
import { Box, useApp, useWindowSize } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
5
|
import { AGENT_DEPOT_PACKAGE_VERSION } from "../project-manifest.js";
|
|
6
|
+
import { withRefreshSession } from "../source-refresh-session.js";
|
|
5
7
|
import { CatalogView } from "./catalog-view.js";
|
|
6
8
|
import { InstallationsView } from "./installations-view.js";
|
|
7
9
|
import { headerTabRows, TABS } from "./header-layout.js";
|
|
@@ -28,7 +30,23 @@ export function Header({ view, columns }) {
|
|
|
28
30
|
const innerWidth = Math.max(columns - 4, 1);
|
|
29
31
|
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.accent, paddingX: 1, flexDirection: "column", width: columns, children: [_jsxs(Box, { width: innerWidth, justifyContent: "space-between", children: [_jsx(Text, { color: theme.accent, bold: true, children: "AGENT DEPOT" }), AGENT_DEPOT_PACKAGE_VERSION === undefined ? null : _jsxs(Text, { color: theme.value, children: ["v", AGENT_DEPOT_PACKAGE_VERSION] })] }), _jsx(Box, { flexDirection: "column", children: headerTabRows(innerWidth).map((row, rowIndex) => (_jsx(Text, { wrap: "truncate-end", children: row.map(([name, label], tabIndex) => (_jsxs(Text, { color: view === name ? theme.accent : theme.inactive, children: [tabIndex === 0 ? "" : " ", view === name ? "▶ " : "", label] }, name))) }, rowIndex))) })] }));
|
|
30
32
|
}
|
|
31
|
-
export function App({ operations, environment, onExit }) {
|
|
33
|
+
export function App({ operations: plainOperations, environment: plainEnvironment, onExit, refreshSession, refreshOnStart }) {
|
|
34
|
+
// Memoized: views reload whenever the operations they receive change identity.
|
|
35
|
+
const operations = useMemo(() => refreshSession === undefined ? plainOperations : withRefreshSession(plainOperations, refreshSession), [plainOperations, refreshSession]);
|
|
36
|
+
// Package previews refresh through the same session, so they reuse and show the same refreshes.
|
|
37
|
+
const environment = useMemo(() => refreshSession === undefined ? plainEnvironment : {
|
|
38
|
+
...plainEnvironment,
|
|
39
|
+
refreshSession,
|
|
40
|
+
...(plainEnvironment?.packages !== undefined ? {} : {
|
|
41
|
+
packagesEnvironment: { ...plainEnvironment?.packagesEnvironment, sourceOperations: operations },
|
|
42
|
+
}),
|
|
43
|
+
}, [plainEnvironment, refreshSession, operations]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!refreshOnStart || refreshSession === undefined)
|
|
46
|
+
return;
|
|
47
|
+
// D3 (I0031): only registered Git Sources; the built-in Source is never refreshed.
|
|
48
|
+
plainOperations.listSources().then((sources) => refreshSession.refreshInBackground(sources.filter((source) => source.kind === "git")), () => undefined);
|
|
49
|
+
}, [plainOperations, refreshSession, refreshOnStart]);
|
|
32
50
|
const { exit } = useApp();
|
|
33
51
|
const { columns, rows } = useWindowSize();
|
|
34
52
|
const [capturing, setCapturing] = useState(false);
|
|
@@ -79,7 +97,7 @@ export function App({ operations, environment, onExit }) {
|
|
|
79
97
|
else if (input === "5")
|
|
80
98
|
showView("profile");
|
|
81
99
|
});
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { view: view, columns: columns }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: view === "sources" ? (_jsx(SourcesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, onHintsChange: setSourcesHints, onOpenCatalog: (source) => {
|
|
100
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { view: view, columns: columns }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: view === "sources" ? (_jsx(SourcesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, onHintsChange: setSourcesHints, refreshSession: refreshSession, onOpenCatalog: (source) => {
|
|
83
101
|
setCatalogSourceId(source.id);
|
|
84
102
|
showView("catalog");
|
|
85
103
|
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, retention: retention })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange, retention: retention })) }), _jsx(Text, { color: theme.muted, children: capturing
|
|
@@ -2,7 +2,9 @@ import { PROJECT_HOSTS } from "../project-manifest.js";
|
|
|
2
2
|
import { assertManagedInstallationRecordsUnchanged } from "../skill-removal.js";
|
|
3
3
|
import { errorText, runBatch } from "./batch.js";
|
|
4
4
|
import { missingHosts, prepareHostAddition, prepareUninstall, runHostAddition, runUninstall, UNINSTALL_REFUSALS, } from "./catalog-installs.js";
|
|
5
|
-
import { answerYesNo, applyChecklistOutcome, hostChecklistKey } from "./mode-keys.js";
|
|
5
|
+
import { answerPaged, answerYesNo, applyChecklistOutcome, hostChecklistKey } from "./mode-keys.js";
|
|
6
|
+
import { span } from "./preview-render.js";
|
|
7
|
+
import { theme } from "./theme.js";
|
|
6
8
|
import { skillOf, sourceIdOf } from "./installations.js";
|
|
7
9
|
import { prepareUnmanagedRemoval, runUnmanagedRemoval } from "./unmanaged-actions.js";
|
|
8
10
|
import { busy } from "./working-model.js";
|
|
@@ -193,7 +195,7 @@ async function preparePlan(context, label, prepare, confirmation) {
|
|
|
193
195
|
}
|
|
194
196
|
/** `u` with marks: prepares every marked leaf, then asks once. */
|
|
195
197
|
export function startBulkUninstall(context, targets) {
|
|
196
|
-
return preparePlan(context, `Preparing uninstall of ${count(targets.length, "item")}…`, () => prepareBulkUninstall(context, targets), (plan) => ({ kind: "confirm-bulk-uninstall", plan }));
|
|
198
|
+
return preparePlan(context, `Preparing uninstall of ${count(targets.length, "item")}…`, () => prepareBulkUninstall(context, targets), (plan) => ({ kind: "confirm-bulk-uninstall", plan, cursor: 0 }));
|
|
197
199
|
}
|
|
198
200
|
/** `h` with marks: asks the hosts once, for the union of what the eligible items lack. */
|
|
199
201
|
export function startBulkHostAddition(context, targets) {
|
|
@@ -207,7 +209,7 @@ export function startBulkHostAddition(context, targets) {
|
|
|
207
209
|
context.setMode({ kind: "bulk-hosts", targets: ready, skipped, choices, cursor: 0, selected: [] });
|
|
208
210
|
}
|
|
209
211
|
function prepareHosts(context, mode) {
|
|
210
|
-
return preparePlan(context, `Preparing host change of ${count(mode.targets.length, "item")}…`, () => prepareBulkHostAddition(context, mode.targets, mode.skipped, mode.selected), (plan) => ({ kind: "confirm-bulk-hosts", plan }));
|
|
212
|
+
return preparePlan(context, `Preparing host change of ${count(mode.targets.length, "item")}…`, () => prepareBulkHostAddition(context, mode.targets, mode.skipped, mode.selected), (plan) => ({ kind: "confirm-bulk-hosts", plan, cursor: 0 }));
|
|
211
213
|
}
|
|
212
214
|
async function apply(context, label, action) {
|
|
213
215
|
const started = busy(label);
|
|
@@ -226,6 +228,30 @@ async function apply(context, label, action) {
|
|
|
226
228
|
context.unmark(result.applied);
|
|
227
229
|
await context.finish(result.message);
|
|
228
230
|
}
|
|
231
|
+
function previewOf(item) {
|
|
232
|
+
if (!("kind" in item))
|
|
233
|
+
return item.prepared.preview;
|
|
234
|
+
return item.kind === "managed" ? item.prepared.plan.preview : item.prepared.preview;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* One block per planned item (its label, then its preview), then the skipped ones, with the footer's action and question.
|
|
238
|
+
* Only the labels are styled; `styled` is sparse so the other lines keep the colours inferred from their text.
|
|
239
|
+
*/
|
|
240
|
+
export function bulkConfirmPreview(mode) {
|
|
241
|
+
const { plan } = mode;
|
|
242
|
+
const items = count(plan.items.length, "item");
|
|
243
|
+
const blocks = plan.items.flatMap((item) => [
|
|
244
|
+
{ text: item.label, label: true },
|
|
245
|
+
...previewOf(item).map((text) => ({ text, label: false })),
|
|
246
|
+
]);
|
|
247
|
+
const skipped = plan.skipped.map(({ label, reason }) => ({ text: `Skipped ${label}: ${reason}`, label: false }));
|
|
248
|
+
const entries = [...blocks, ...skipped];
|
|
249
|
+
const lines = entries.map(({ text }) => text);
|
|
250
|
+
const styled = entries.map(({ text, label }) => label ? [span(text, theme.group)] : undefined);
|
|
251
|
+
return mode.kind === "confirm-bulk-uninstall"
|
|
252
|
+
? { lines, styled, chrome: { title: "Uninstall", action: `Uninstall ${items}`, consequence: `Uninstall ${items}? y/n` } }
|
|
253
|
+
: { lines, styled, chrome: { title: "Add hosts", action: `Add hosts to ${items}`, consequence: `Add hosts to ${items}? y/n` } };
|
|
254
|
+
}
|
|
229
255
|
/** Handles a key in a bulk mode; returns false for any other mode. */
|
|
230
256
|
export function handleBulkKey(context, mode, input, key) {
|
|
231
257
|
const current = mode;
|
|
@@ -241,13 +267,20 @@ export function handleBulkKey(context, mode, input, key) {
|
|
|
241
267
|
return true;
|
|
242
268
|
}
|
|
243
269
|
case "confirm-bulk-uninstall":
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
case "confirm-bulk-hosts": {
|
|
271
|
+
const { lines, chrome } = bulkConfirmPreview(current);
|
|
272
|
+
answerPaged(context.previewGeometry(lines, current.cursor, chrome), current.cursor, input, key, {
|
|
273
|
+
moveTo: (cursor) => context.setMode({ ...current, cursor }),
|
|
274
|
+
yes: () => {
|
|
275
|
+
if (current.kind === "confirm-bulk-hosts")
|
|
276
|
+
context.setMode({ kind: "confirm-bulk-exposure", plan: current.plan });
|
|
277
|
+
else
|
|
278
|
+
void apply(context, `Removing ${count(current.plan.items.length, "item")}…`, (progress) => runBulkUninstall(current.plan, operations, env, progress));
|
|
279
|
+
},
|
|
280
|
+
no: () => cancel(context, current.kind === "confirm-bulk-hosts" ? "Add hosts cancelled" : "Uninstall cancelled"),
|
|
281
|
+
});
|
|
250
282
|
return true;
|
|
283
|
+
}
|
|
251
284
|
case "confirm-bulk-exposure":
|
|
252
285
|
answerYesNo(input, key, () => {
|
|
253
286
|
void apply(context, `Changing hosts of ${count(current.plan.items.length, "item")}…`, (progress) => runBulkHostAddition(current.plan, operations, env, progress));
|
|
@@ -7,3 +7,15 @@ export function describeSkills(skills) {
|
|
|
7
7
|
return skills.length === 1 ? skills[0].name : `${skills.length} skills`;
|
|
8
8
|
}
|
|
9
9
|
export const BROWSE = { kind: "browse" };
|
|
10
|
+
/** What the install confirmation shows, so the key handler and the panel page the same rows. */
|
|
11
|
+
export function installPreview(items) {
|
|
12
|
+
const ready = items.filter((item) => item.prepared !== undefined);
|
|
13
|
+
const single = items.length === 1;
|
|
14
|
+
const name = describeSkills(ready.map((item) => item.skill));
|
|
15
|
+
const lines = [
|
|
16
|
+
...(ready[0]?.prepared?.projectTarget ?? []),
|
|
17
|
+
...items.flatMap(({ skill, prepared, error }) => prepared === undefined ? [`Cannot install ${skill.name}: ${error}`] : [...(single ? [] : [skill.name]), ...prepared.preview]),
|
|
18
|
+
...(ready.some((item) => item.prepared?.runsExternalCommand) ? ["This install runs an external command (see above)."] : []),
|
|
19
|
+
];
|
|
20
|
+
return { lines, chrome: { title: `Skill install preview · ${ready.length} ready`, action: `Install ${name}`, consequence: `Install ${name}? y/n` } };
|
|
21
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatProgressLabel } from "../flow-progress.js";
|
|
2
|
+
import { BROWSE, describeSkills, installPreview, HOST_CHOICES, SCOPE_CHOICES } from "./catalog-actions.js";
|
|
2
3
|
import { prepareInstall, runInstall } from "./catalog-installs.js";
|
|
3
4
|
import { skillKey } from "./catalog-tree.js";
|
|
4
5
|
import { errorText, runBatch } from "./batch.js";
|
|
5
|
-
import { answerYesNo, applyChecklistOutcome, hostChecklistKey, versionInputKey } from "./mode-keys.js";
|
|
6
|
+
import { answerYesNo, applyChecklistOutcome, hostChecklistKey, movePreviewCursor, versionInputKey } from "./mode-keys.js";
|
|
6
7
|
import { busy } from "./working-model.js";
|
|
7
8
|
function cancel(context) {
|
|
8
9
|
context.setMessage({ kind: "ok", text: "Install cancelled" });
|
|
@@ -27,22 +28,46 @@ async function startInstall(context, skills, choice) {
|
|
|
27
28
|
context.setAction(BROWSE);
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
|
-
context.setAction({ kind: "confirm-install", items });
|
|
31
|
+
context.setAction({ kind: "confirm-install", items, cursor: 0 });
|
|
31
32
|
}
|
|
32
33
|
/** Applies the prepared items one by one; a failure is reported for that item and the rest still run. */
|
|
33
34
|
async function confirmInstall(context, items, exposure) {
|
|
34
|
-
|
|
35
|
+
const running = busy(`Installing ${describeSkills(items.map((item) => item.skill))}…`);
|
|
36
|
+
context.setAction(running);
|
|
35
37
|
const outcomes = await runBatch(items, async ({ prepared, error }) => {
|
|
36
38
|
if (prepared === undefined)
|
|
37
39
|
throw new Error(error);
|
|
38
40
|
return (await runInstall(prepared, context.operations, exposure && prepared.additionalHostExposure)).join(" ");
|
|
39
|
-
}, ({ skill }, caught) => `Failed ${skill.name}: ${errorText(caught)}
|
|
41
|
+
}, ({ skill }, caught) => `Failed ${skill.name}: ${errorText(caught)}`, ({ skill }, position, total, finished) => {
|
|
42
|
+
if (!context.isMounted())
|
|
43
|
+
return;
|
|
44
|
+
const label = formatProgressLabel({ kind: "skill", name: skill.name, position, total, verb: "Installing" });
|
|
45
|
+
context.setAction({ ...running, label, done: finished.map((outcome) => outcome.line) });
|
|
46
|
+
});
|
|
40
47
|
const lines = outcomes.map((outcome) => outcome.line);
|
|
41
48
|
const failed = new Set(outcomes.filter((outcome) => !outcome.ok).map((outcome) => skillKey(outcome.item.skill)));
|
|
42
49
|
// Installed Skills leave the list; only the failed ones stay marked for another try.
|
|
43
50
|
context.keepMarked(failed);
|
|
44
51
|
await context.finish({ kind: failed.size > 0 ? "error" : "ok", text: lines.join("\n") });
|
|
45
52
|
}
|
|
53
|
+
/** Pages the preview like the other footer confirmations: `y` answers only once it has all been displayed. */
|
|
54
|
+
function handleInstallKey(context, mode, input, key) {
|
|
55
|
+
const { lines, chrome } = installPreview(mode.items);
|
|
56
|
+
const viewport = context.previewGeometry(lines, mode.cursor, chrome);
|
|
57
|
+
const cursor = movePreviewCursor(mode.cursor, viewport.total, input, key, viewport.height);
|
|
58
|
+
if (cursor !== undefined) {
|
|
59
|
+
context.setAction({ ...mode, cursor });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
answerYesNo(input, key, () => {
|
|
63
|
+
if (!viewport.reachedEnd)
|
|
64
|
+
return;
|
|
65
|
+
if (mode.items.some((item) => item.prepared?.additionalHostExposure))
|
|
66
|
+
context.setAction({ kind: "confirm-exposure", items: mode.items });
|
|
67
|
+
else
|
|
68
|
+
void confirmInstall(context, mode.items, false);
|
|
69
|
+
}, () => cancel(context));
|
|
70
|
+
}
|
|
46
71
|
function handleHostKey(context, mode, input, key) {
|
|
47
72
|
applyChecklistOutcome(hostChecklistKey(mode, HOST_CHOICES, input, key), {
|
|
48
73
|
cancel: () => cancel(context),
|
|
@@ -92,12 +117,7 @@ export function handleCatalogActionKey(context, mode, input, key) {
|
|
|
92
117
|
handleFixedKey(context, mode, input, key);
|
|
93
118
|
return;
|
|
94
119
|
case "confirm-install":
|
|
95
|
-
|
|
96
|
-
if (mode.items.some((item) => item.prepared?.additionalHostExposure))
|
|
97
|
-
context.setAction({ ...mode, kind: "confirm-exposure" });
|
|
98
|
-
else
|
|
99
|
-
void confirmInstall(context, mode.items, false);
|
|
100
|
-
}, () => cancel(context));
|
|
120
|
+
handleInstallKey(context, mode, input, key);
|
|
101
121
|
return;
|
|
102
122
|
case "confirm-exposure":
|
|
103
123
|
answerYesNo(input, key, () => void confirmInstall(context, mode.items, true), () => cancel(context));
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box
|
|
3
|
-
import {
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
4
|
+
import { describeSkills, HOST_CHOICES, installPreview, SCOPE_CHOICES } from "./catalog-actions.js";
|
|
4
5
|
import { packageOwnedSkillNotice, packageOwnerBadge } from "./catalog-ownership.js";
|
|
5
|
-
import { skillKey } from "./catalog-tree.js";
|
|
6
6
|
import { hasUnmanagedCopy } from "./catalog-installs.js";
|
|
7
|
-
import { describeRow, truncateDescription, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
7
|
+
import { describeRow, singleLine, truncateDescription, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
8
8
|
import { isPackageMode, PackagePanel } from "./package-actions.js";
|
|
9
|
-
import { BorderPanel, DecisionPrompt, HostChecklist,
|
|
9
|
+
import { BorderPanel, DecisionPrompt, HostChecklist, PagedPreview, Working } from "./panel-parts.js";
|
|
10
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
10
11
|
import { rowStyle } from "./theme.js";
|
|
11
12
|
import { theme } from "./theme.js";
|
|
12
13
|
/** Scope and counts, the filter line, and why the list may be empty. */
|
|
@@ -19,7 +20,7 @@ export function CatalogRowLine({ row, selected, marked, installed }) {
|
|
|
19
20
|
const isGroup = data.kind === "source" || data.kind === "group";
|
|
20
21
|
const ownedBy = data.kind === "skill" ? data.ownedBy : undefined;
|
|
21
22
|
return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group : undefined, wrap: "truncate-end", children: [selected ? "> " : " ", " ".repeat(row.depth), data.kind === "skill" ? marked ? "[x] " : "[ ] " : "", data.kind === "skill" && ownedBy !== undefined
|
|
22
|
-
? _jsxs(_Fragment, { children: [data.skill.name, _jsxs(Text, { color: theme.marker, children: [" ", "[", packageOwnerBadge(ownedBy), "]"] }), " ", truncateDescription(data.skill.description), data.variant === true ? ` (${data.skill.path})` : ""] })
|
|
23
|
+
? _jsxs(_Fragment, { children: [singleLine(data.skill.name), _jsxs(Text, { color: theme.marker, children: [" ", "[", packageOwnerBadge(ownedBy), "]"] }), " ", truncateDescription(data.skill.description), data.variant === true ? ` (${data.skill.path})` : ""] })
|
|
23
24
|
: data.kind === "app" ? `App ${data.row.entry.recipe?.name ?? data.row.entry.file}` : describeRow(row), data.kind === "app" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: data.row.inspection.status === "not installed" ? theme.warning : theme.value, children: data.row.inspection.status })] })) : null, data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
|
|
24
25
|
? _jsxs(Text, { color: theme.marker, children: [" ", UNMANAGED_MARKER] })
|
|
25
26
|
: null] }));
|
|
@@ -29,6 +30,11 @@ export function CatalogSelectionHelp({ row, height }) {
|
|
|
29
30
|
const owner = row?.node.data.kind === "skill" ? row.node.data.ownedBy : undefined;
|
|
30
31
|
return (_jsx(Box, { flexDirection: "column", minHeight: height, children: owner === undefined ? null : _jsx(Text, { color: theme.marker, children: packageOwnedSkillNotice(owner) }) }));
|
|
31
32
|
}
|
|
33
|
+
/** The install confirmation with the shared footer; the view shows it alone so no tree rows count against its height. */
|
|
34
|
+
export function InstallConfirmPanel({ mode }) {
|
|
35
|
+
const { lines, chrome } = installPreview(mode.items);
|
|
36
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, viewport: usePreviewGeometry()(lines, mode.cursor, chrome) });
|
|
37
|
+
}
|
|
32
38
|
/** The prompt of the install flow; browsing shows nothing. */
|
|
33
39
|
export function ActionPanel({ mode }) {
|
|
34
40
|
if (isPackageMode(mode))
|
|
@@ -44,17 +50,13 @@ export function ActionPanel({ mode }) {
|
|
|
44
50
|
return _jsx(BorderPanel, { title: "Skill version policy", children: _jsx(Text, { children: "1 latest 2 fixed (Esc cancel)" }) });
|
|
45
51
|
case "fixed":
|
|
46
52
|
return _jsx(BorderPanel, { title: "Fixed Skill version", children: _jsxs(Text, { children: [mode.value, "_ (Enter confirm, Esc cancel)"] }) });
|
|
47
|
-
case "confirm-install":
|
|
48
|
-
|
|
49
|
-
const single = mode.items.length === 1;
|
|
50
|
-
const projectTarget = ready[0]?.prepared?.projectTarget ?? [];
|
|
51
|
-
return (_jsxs(BorderPanel, { title: `Skill install preview · ${ready.length} ready`, children: [projectTarget.length === 0 ? null : _jsx(PreviewLines, { lines: projectTarget }), mode.items.map(({ skill, prepared, error }) => prepared === undefined ? (_jsxs(Text, { color: theme.error, children: ["Cannot install ", skill.name, ": ", error] }, skillKey(skill))) : (_jsxs(Box, { flexDirection: "column", children: [single ? null : _jsx(Text, { color: theme.group, children: skill.name }), _jsx(PreviewLines, { lines: prepared.preview })] }, skillKey(skill)))), ready.some((item) => item.prepared?.runsExternalCommand) ? _jsx(Text, { color: theme.warning, children: "This install runs an external command (see above)." }) : null, _jsxs(DecisionPrompt, { children: ["Install ", describeSkills(ready.map((item) => item.skill)), "?"] })] }));
|
|
52
|
-
}
|
|
53
|
+
case "confirm-install":
|
|
54
|
+
return _jsx(InstallConfirmPanel, { mode: mode });
|
|
53
55
|
case "confirm-exposure": {
|
|
54
56
|
const names = mode.items.filter((item) => item.prepared?.additionalHostExposure).map((item) => item.skill.name);
|
|
55
57
|
return (_jsx(BorderPanel, { title: `Skill Host exposure · ${names.length} confirmation`, children: _jsxs(Text, { color: theme.warning, children: [names.length === 1 ? `An identical ${names[0]} already exists` : `Identical copies of ${names.join(", ")} already exist`, "; adding the missing Host location needs separate confirmation. ", _jsx(DecisionPrompt, { children: "Confirm?" })] }) }));
|
|
56
58
|
}
|
|
57
59
|
case "busy":
|
|
58
|
-
return
|
|
60
|
+
return (_jsxs(BorderPanel, { title: "Skill install", children: [_jsx(Working, { label: mode.label, since: mode.startedAt }), (mode.done ?? []).length > 0 ? _jsxs(Text, { color: theme.muted, children: [mode.done.length, " finished"] }) : null] }));
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -8,8 +8,13 @@ export const UNMANAGED_MARKER = "on disk, unmanaged · adopt it from Installatio
|
|
|
8
8
|
export const skillKey = (skill) => `skill:${skill.sourceId}:${skill.path}`;
|
|
9
9
|
/** Keyed on the full selection, so two plugins of one marketplace root stay distinct nodes. */
|
|
10
10
|
export const bundleKey = (entry) => `bundle:${entry.sourceId}:${selectionKey(entry.selection)}`;
|
|
11
|
+
/** A Skill's name or description on one row; a line break from a remote SKILL.md would add rows to the list window. */
|
|
12
|
+
export function singleLine(text) {
|
|
13
|
+
return text.replace(/[\r\n]+/gu, " ");
|
|
14
|
+
}
|
|
11
15
|
export function truncateDescription(text) {
|
|
12
|
-
|
|
16
|
+
const line = singleLine(text);
|
|
17
|
+
return line.length > DESCRIPTION_LIMIT ? `${line.slice(0, DESCRIPTION_LIMIT - 3)}...` : line;
|
|
13
18
|
}
|
|
14
19
|
/** A repository name, with directory and ref for scoped GitHub Sources. */
|
|
15
20
|
export function sourceLabel(source) {
|
|
@@ -107,7 +112,7 @@ export function describeRow(row) {
|
|
|
107
112
|
}
|
|
108
113
|
if (data.kind === "app")
|
|
109
114
|
return `App ${data.row.entry.recipe?.name ?? data.row.entry.file} ${data.row.inspection.status}`;
|
|
110
|
-
return `${data.skill.name} ${truncateDescription(data.skill.description)}${data.variant === true ? ` (${data.skill.path})` : ""}`;
|
|
115
|
+
return `${singleLine(data.skill.name)} ${truncateDescription(data.skill.description)}${data.variant === true ? ` (${data.skill.path})` : ""}`;
|
|
111
116
|
}
|
|
112
117
|
/** Skill nodes inside a Source's Skills group, directly or within one of its variant groups. */
|
|
113
118
|
function skillNodesOf(roots) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box,
|
|
2
|
+
import { Box, useWindowSize } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
3
4
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
5
|
import { createAppOperations } from "../app-flow.js";
|
|
5
6
|
import { BROWSE } from "./catalog-actions.js";
|
|
@@ -8,7 +9,7 @@ import { filterSkills } from "./catalog-filter.js";
|
|
|
8
9
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
9
10
|
import { isInstalled } from "./catalog-installs.js";
|
|
10
11
|
import { AppPanel, handleAppKey, startAppAction } from "./app-actions.js";
|
|
11
|
-
import { ActionPanel, CatalogHeader, CatalogRowLine, CatalogSelectionHelp } from "./catalog-panel.js";
|
|
12
|
+
import { ActionPanel, CatalogHeader, InstallConfirmPanel, CatalogRowLine, CatalogSelectionHelp } from "./catalog-panel.js";
|
|
12
13
|
import { packageOwnedSkillNotice } from "./catalog-ownership.js";
|
|
13
14
|
import { packageOperationsOf } from "./environment.js";
|
|
14
15
|
import { handlePackageKey, isPackageMode, PackagePanel, startPackageAction } from "./package-actions.js";
|
|
@@ -21,6 +22,7 @@ import { invalidateRetention } from "./view-retention.js";
|
|
|
21
22
|
import { useLoadingSince, useMarks, useMounted } from "./view-state.js";
|
|
22
23
|
import { computeWindow, useListHeight } from "./window.js";
|
|
23
24
|
import { useKeys } from "./keys.js";
|
|
25
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
24
26
|
const NO_ENVIRONMENT = {};
|
|
25
27
|
function wrappedLineCount(text, columns) {
|
|
26
28
|
let lines = 1;
|
|
@@ -41,7 +43,8 @@ function wrappedLineCount(text, columns) {
|
|
|
41
43
|
return lines;
|
|
42
44
|
}
|
|
43
45
|
export function CatalogView({ operations, sourceId, onCapturingChange, environment, listHeight, retention }) {
|
|
44
|
-
const { columns
|
|
46
|
+
const { columns } = useWindowSize();
|
|
47
|
+
const previewGeometry = usePreviewGeometry();
|
|
45
48
|
const env = environment ?? NO_ENVIRONMENT;
|
|
46
49
|
const [all, setAll] = useState(sourceId === undefined);
|
|
47
50
|
const mounted = useMounted();
|
|
@@ -110,6 +113,7 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
110
113
|
const flow = {
|
|
111
114
|
operations,
|
|
112
115
|
env,
|
|
116
|
+
previewGeometry,
|
|
113
117
|
setAction,
|
|
114
118
|
setMessage,
|
|
115
119
|
isMounted: () => mounted.current,
|
|
@@ -158,7 +162,7 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
158
162
|
setAction(BROWSE);
|
|
159
163
|
},
|
|
160
164
|
};
|
|
161
|
-
const packageContext = { packages, manage: packageHost,
|
|
165
|
+
const packageContext = { packages, manage: packageHost, previewGeometry };
|
|
162
166
|
const browse = {
|
|
163
167
|
navigation,
|
|
164
168
|
height,
|
|
@@ -168,14 +172,14 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
168
172
|
ready: state.status === "ready" && appCatalog.status === "ready",
|
|
169
173
|
toggleAll: () => setAll((value) => !value),
|
|
170
174
|
startPackage: (target) => void startPackageAction(packageContext, target, "i"),
|
|
171
|
-
startApp: (entry) => void startAppAction({ apps, manage: appManage,
|
|
175
|
+
startApp: (entry) => void startAppAction({ apps, manage: appManage, previewGeometry }, [entry], "i"),
|
|
172
176
|
setMessage,
|
|
173
177
|
setAction,
|
|
174
178
|
};
|
|
175
179
|
useKeys((input, key) => {
|
|
176
180
|
const runningAppMode = appModeRef.current;
|
|
177
181
|
if (runningAppMode !== undefined)
|
|
178
|
-
handleAppKey({ apps, manage: appManage,
|
|
182
|
+
handleAppKey({ apps, manage: appManage, previewGeometry }, runningAppMode, input, key);
|
|
179
183
|
else {
|
|
180
184
|
const mode = actionRef.current;
|
|
181
185
|
if (isPackageMode(mode))
|
|
@@ -200,5 +204,7 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
200
204
|
// A paged declaration owns the view: rows of the tree above it would not be counted in its height.
|
|
201
205
|
if (isPackageMode(action))
|
|
202
206
|
return _jsx(PackagePanel, { mode: action });
|
|
207
|
+
if (action.kind === "confirm-install")
|
|
208
|
+
return _jsx(InstallConfirmPanel, { mode: action });
|
|
203
209
|
return (_jsxs(Box, { flexDirection: "column", children: [refreshing ? _jsx(Working, { label: "Refreshing catalog (last data shown)\u2026", since: refreshingSince }) : null, state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), appCatalog.status === "ready" && appCatalog.error ? _jsxs(Text, { color: theme.error, children: ["Apps unavailable: ", appCatalog.error] }) : null, _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, appCount: visibleApps.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(CatalogSelectionHelp, { row: rows[index], height: helpLines }), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), appMode ? _jsx(AppPanel, { mode: appMode }) : _jsx(ActionPanel, { mode: action })] }));
|
|
204
210
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { useWindowSize } from "ink";
|
|
4
|
+
import { Text } from "./safe-text.js";
|
|
4
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
6
|
import { pickerEntries, readDirectoryListing, PROFILE_EXTENSION } from "./file-picker-model.js";
|
|
6
7
|
import { useKeys } from "./keys.js";
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Box
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from "ink";
|
|
3
|
+
import { Text } from "./safe-text.js";
|
|
4
|
+
import { adoptionPreview } from "./adoption-actions.js";
|
|
3
5
|
import { HOST_CHOICES } from "./catalog-actions.js";
|
|
4
6
|
import { ManagePanel } from "./manage-panel.js";
|
|
5
|
-
import { DecisionPrompt, HostChecklist,
|
|
7
|
+
import { DecisionPrompt, HostChecklist, PagedPreview } from "./panel-parts.js";
|
|
8
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
6
9
|
import { theme } from "./theme.js";
|
|
10
|
+
/** An adoptable preview with the shared footer; the view shows it alone so no tree rows count against its height. */
|
|
11
|
+
export function AdoptionConfirmPanel({ mode }) {
|
|
12
|
+
const { lines, chrome } = adoptionPreview(mode);
|
|
13
|
+
return _jsx(PagedPreview, { chrome: chrome, lines: lines, viewport: usePreviewGeometry()(lines, mode.cursor, chrome) });
|
|
14
|
+
}
|
|
7
15
|
/** The prompt of the adoption and manage modes; browsing shows nothing. */
|
|
8
16
|
export function ModePanel({ mode }) {
|
|
9
17
|
switch (mode.kind) {
|
|
@@ -18,7 +26,7 @@ export function ModePanel({ mode }) {
|
|
|
18
26
|
case "fixed":
|
|
19
27
|
return _jsxs(Text, { children: ["Fixed version: ", mode.value, "_ (Enter confirm, Esc cancel)"] });
|
|
20
28
|
case "confirm":
|
|
21
|
-
return mode.adoption.verdict.adoptable ? (
|
|
29
|
+
return mode.adoption.verdict.adoptable ? (_jsx(AdoptionConfirmPanel, { mode: mode })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.error, children: mode.adoption.verdict.reason }), _jsx(Text, { children: "Enter/Esc to go back" })] }));
|
|
22
30
|
case "confirm-exposure":
|
|
23
31
|
return (_jsxs(Text, { color: theme.warning, children: ["Adopting ", mode.entry.name, " adds the missing Host location and exposes it to more hosts; this needs separate confirmation (CLI: --confirm-additional-host). ", _jsx(DecisionPrompt, { children: "Confirm?" })] }));
|
|
24
32
|
default:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Text } from "
|
|
2
|
+
import { Text } from "./safe-text.js";
|
|
3
3
|
import { describeLinks, describeNode, markersOf } from "./installations-tree.js";
|
|
4
4
|
import { rowStyle, theme } from "./theme.js";
|
|
5
5
|
function AppDetails({ row }) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } 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 { AppPanel, handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
|
|
@@ -10,8 +11,9 @@ import { BROWSE } from "./installations-mode.js";
|
|
|
10
11
|
import { errorText } from "./batch.js";
|
|
11
12
|
import { handleAdoptionKey, startAdoption } from "./adoption-actions.js";
|
|
12
13
|
import { handleBrowseKey } from "./installations-browse.js";
|
|
13
|
-
import { handleManageKey } from "./manage-actions.js";
|
|
14
|
-
import {
|
|
14
|
+
import { handleManageKey, isPagedConfirmMode } from "./manage-actions.js";
|
|
15
|
+
import { PagedConfirmPanel } from "./manage-panel.js";
|
|
16
|
+
import { AdoptionConfirmPanel, ModePanel } from "./installations-panel.js";
|
|
15
17
|
import { TreeRowLine } from "./installations-rows.js";
|
|
16
18
|
import { buildTree, leavesOf, markersOf } from "./installations-tree.js";
|
|
17
19
|
import { ListFooter, Working } from "./panel-parts.js";
|
|
@@ -23,10 +25,11 @@ import { useLoadingSince, useMarks, useMounted } from "./view-state.js";
|
|
|
23
25
|
import { computeWindow, useListHeight } from "./window.js";
|
|
24
26
|
import { useKeys } from "./keys.js";
|
|
25
27
|
import { invalidateRetention } from "./view-retention.js";
|
|
28
|
+
import { usePreviewGeometry } from "./preview-window.js";
|
|
26
29
|
const NO_ENVIRONMENT = {};
|
|
27
30
|
const LEGEND = "adopted: tracked in place · modified: changed on disk";
|
|
28
31
|
export function InstallationsView({ operations, environment, onCapturingChange, listHeight, retention }) {
|
|
29
|
-
const
|
|
32
|
+
const previewGeometry = usePreviewGeometry();
|
|
30
33
|
const height = useListHeight(listHeight, 7);
|
|
31
34
|
const env = environment ?? NO_ENVIRONMENT;
|
|
32
35
|
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
@@ -148,6 +151,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
148
151
|
operations,
|
|
149
152
|
env,
|
|
150
153
|
installed,
|
|
154
|
+
previewGeometry,
|
|
151
155
|
setMode,
|
|
152
156
|
browse: () => setMode(BROWSE),
|
|
153
157
|
setMessage,
|
|
@@ -165,6 +169,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
165
169
|
const adoption = {
|
|
166
170
|
operations,
|
|
167
171
|
env,
|
|
172
|
+
previewGeometry,
|
|
168
173
|
setMode,
|
|
169
174
|
browse: manage.browse,
|
|
170
175
|
setMessage,
|
|
@@ -180,16 +185,16 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
180
185
|
packages,
|
|
181
186
|
startAdoption: (group) => void startAdoption(adoption, group),
|
|
182
187
|
setMessage,
|
|
183
|
-
startApps: (entries, input) => void startAppAction({ apps, manage,
|
|
188
|
+
startApps: (entries, input) => void startAppAction({ apps, manage, previewGeometry }, entries, input),
|
|
184
189
|
};
|
|
185
190
|
useKeys((input, key) => {
|
|
186
191
|
const current = modeRef.current;
|
|
187
192
|
if (current.kind === "browse")
|
|
188
193
|
handleBrowseKey(browse, input, key);
|
|
189
194
|
else if (isPackageMode(current))
|
|
190
|
-
handlePackageKey({ packages, manage,
|
|
195
|
+
handlePackageKey({ packages, manage, previewGeometry }, current, input, key);
|
|
191
196
|
else if (isAppMode(current))
|
|
192
|
-
handleAppKey({ apps, manage,
|
|
197
|
+
handleAppKey({ apps, manage, previewGeometry }, current, input, key);
|
|
193
198
|
else if (!handleManageKey(manage, current, input, key))
|
|
194
199
|
handleAdoptionKey(adoption, current, input, key);
|
|
195
200
|
});
|
|
@@ -202,6 +207,10 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
202
207
|
// A paged declaration owns the view: rows of the tree above it would not be counted in its height.
|
|
203
208
|
if (isPackageMode(mode))
|
|
204
209
|
return _jsx(PackagePanel, { mode: mode });
|
|
210
|
+
if (isPagedConfirmMode(mode))
|
|
211
|
+
return _jsx(PagedConfirmPanel, { mode: mode });
|
|
212
|
+
if (mode.kind === "confirm" && mode.adoption.verdict.adoptable)
|
|
213
|
+
return _jsx(AdoptionConfirmPanel, { mode: mode });
|
|
205
214
|
const ready = state.data;
|
|
206
215
|
const window = computeWindow(rows.length, index, height);
|
|
207
216
|
const listed = rows.slice(window.start, window.end);
|
package/dist/src/tui/keys.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useInput } from "ink";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
/** Plain printable characters only: control characters and escape sequences are never split. */
|
|
3
4
|
const PRINTABLE = /^[^\p{Cc}]+$/u;
|
|
4
5
|
/**
|
|
@@ -10,22 +11,44 @@ const PRINTABLE = /^[^\p{Cc}]+$/u;
|
|
|
10
11
|
*/
|
|
11
12
|
const ESCAPE_REMNANT = /^(?:\[[\d;<:?]*[A-DFHIMOPQRSZm~]|O[A-DFHPQRS])$/;
|
|
12
13
|
const PASTE_MARKERS = new Set(["[200~", "[201~"]);
|
|
14
|
+
/** Keys that did not arrive alone: split off a multi-character block, or read after another key in the same turn. */
|
|
15
|
+
const groupedKeys = new WeakSet();
|
|
16
|
+
/**
|
|
17
|
+
* Whether this key press is a `y` that answers a confirmation. A `y` that arrived together with other
|
|
18
|
+
* keys never does: they may have opened the question or moved the cursor to its end before it was drawn.
|
|
19
|
+
* Text fields read `input` directly, so pasted text still reaches them.
|
|
20
|
+
*/
|
|
21
|
+
export function isYes(input, key) {
|
|
22
|
+
return input === "y" && !groupedKeys.has(key);
|
|
23
|
+
}
|
|
24
|
+
function grouped(key) {
|
|
25
|
+
const copy = { ...key };
|
|
26
|
+
groupedKeys.add(copy);
|
|
27
|
+
return copy;
|
|
28
|
+
}
|
|
13
29
|
/**
|
|
14
30
|
* `useInput` that treats several plain characters delivered in one write (fast
|
|
15
|
-
* typing, a paste) as the separate key presses they are, in order.
|
|
31
|
+
* typing, a paste) as the separate key presses they are, in order. Ink also hands
|
|
32
|
+
* the escape sequences of one read over as separate events within one turn; every key
|
|
33
|
+
* after the first of a turn counts as grouped, as do all characters of a split block.
|
|
16
34
|
*/
|
|
17
35
|
export function useKeys(handler) {
|
|
36
|
+
const eventsThisTurn = useRef(0);
|
|
18
37
|
useInput((input, key) => {
|
|
38
|
+
if (eventsThisTurn.current++ === 0)
|
|
39
|
+
queueMicrotask(() => { eventsThisTurn.current = 0; });
|
|
40
|
+
const later = eventsThisTurn.current > 1;
|
|
19
41
|
if (ESCAPE_REMNANT.test(input)) {
|
|
20
42
|
if (!PASTE_MARKERS.has(input))
|
|
21
|
-
handler(input, key);
|
|
43
|
+
handler(input, later ? grouped(key) : key);
|
|
22
44
|
return;
|
|
23
45
|
}
|
|
24
46
|
if (input.length > 1 && !key.ctrl && !key.meta && !key.escape && PRINTABLE.test(input)) {
|
|
47
|
+
const blockKey = grouped(key);
|
|
25
48
|
for (const character of input)
|
|
26
|
-
handler(character,
|
|
49
|
+
handler(character, blockKey);
|
|
27
50
|
return;
|
|
28
51
|
}
|
|
29
|
-
handler(input, key);
|
|
52
|
+
handler(input, later ? grouped(key) : key);
|
|
30
53
|
});
|
|
31
54
|
}
|