@iseoane/agent-depot 0.5.2 → 0.6.0
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 +19 -0
- package/README.es.md +51 -29
- package/README.md +44 -25
- package/dist/src/app-flow.js +56 -24
- package/dist/src/profile-export.js +72 -9
- package/dist/src/profile-import.js +3 -2
- package/dist/src/profile.js +71 -7
- package/dist/src/tui/app-actions.js +82 -33
- package/dist/src/tui/app.js +19 -14
- package/dist/src/tui/catalog-browse.js +6 -1
- package/dist/src/tui/catalog-panel.js +11 -11
- package/dist/src/tui/catalog-state.js +28 -0
- package/dist/src/tui/catalog-tree.js +22 -5
- package/dist/src/tui/catalog-view.js +59 -16
- package/dist/src/tui/header-layout.js +33 -0
- package/dist/src/tui/installations-panel.js +3 -3
- package/dist/src/tui/installations-rows.js +20 -5
- package/dist/src/tui/installations-tree.js +4 -7
- package/dist/src/tui/installations-view.js +15 -6
- package/dist/src/tui/installations.js +9 -7
- package/dist/src/tui/manage-panel.js +7 -6
- package/dist/src/tui/mode-keys.js +9 -0
- package/dist/src/tui/package-rows.js +17 -5
- package/dist/src/tui/panel-parts.js +38 -6
- package/dist/src/tui/preview-window.js +48 -0
- package/dist/src/tui/profile-view.js +261 -42
- package/dist/src/tui/sources-mode.js +3 -1
- package/dist/src/tui/sources-recipes.js +50 -19
- package/dist/src/tui/sources-view.js +50 -14
- package/dist/src/tui/theme.js +14 -7
- package/dist/src/tui/updates-flow.js +2 -2
- package/dist/src/tui/updates-keys.js +34 -5
- package/dist/src/tui/updates-model.js +2 -0
- package/dist/src/tui/updates-panel.js +40 -15
- package/dist/src/tui/updates-preview.js +65 -0
- package/dist/src/tui/updates-view.js +17 -2
- package/dist/src/tui/updates.js +14 -5
- package/dist/src/tui/window.js +6 -4
- package/package.json +4 -3
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createAppOperations } from "../app-flow.js";
|
|
4
5
|
import { BROWSE } from "./catalog-actions.js";
|
|
5
6
|
import { handleCatalogBrowseKey } from "./catalog-browse.js";
|
|
6
7
|
import { filterSkills } from "./catalog-filter.js";
|
|
7
8
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
8
9
|
import { isInstalled } from "./catalog-installs.js";
|
|
10
|
+
import { AppPanel, handleAppKey, startAppAction } from "./app-actions.js";
|
|
9
11
|
import { ActionPanel, CatalogHeader, CatalogRowLine, CatalogSelectionHelp } from "./catalog-panel.js";
|
|
10
12
|
import { packageOwnedSkillNotice } from "./catalog-ownership.js";
|
|
11
13
|
import { packageOperationsOf } from "./environment.js";
|
|
12
14
|
import { handlePackageKey, isPackageMode, startPackageAction } from "./package-actions.js";
|
|
13
15
|
import { ListFooter } from "./panel-parts.js";
|
|
14
|
-
import { useCatalogFilter, useCatalogSkills, useInstalledSkills } from "./catalog-state.js";
|
|
16
|
+
import { useCatalogApps, useCatalogFilter, useCatalogSkills, useInstalledSkills } from "./catalog-state.js";
|
|
15
17
|
import { buildTree, leavesOf, openByDefault, skillKey } from "./catalog-tree.js";
|
|
16
18
|
import { theme } from "./theme.js";
|
|
17
19
|
import { useTreeNavigation } from "./tree-navigation.js";
|
|
@@ -38,7 +40,7 @@ function wrappedLineCount(text, columns) {
|
|
|
38
40
|
return lines;
|
|
39
41
|
}
|
|
40
42
|
export function CatalogView({ operations, sourceId, onCapturingChange, environment, listHeight }) {
|
|
41
|
-
const { columns } = useWindowSize();
|
|
43
|
+
const { columns, rows: terminalRows } = useWindowSize();
|
|
42
44
|
const env = environment ?? NO_ENVIRONMENT;
|
|
43
45
|
const [all, setAll] = useState(sourceId === undefined);
|
|
44
46
|
const mounted = useMounted();
|
|
@@ -52,7 +54,17 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
52
54
|
const filter = useCatalogFilter(actionRef, onCapturingChange);
|
|
53
55
|
const state = useCatalogSkills(operations, sourceId, all, env, reloadKey);
|
|
54
56
|
const packages = useMemo(() => packageOperationsOf(env), [env]);
|
|
57
|
+
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
58
|
+
const appCatalogState = useCatalogApps(apps);
|
|
59
|
+
const appCatalog = appCatalogState.state;
|
|
55
60
|
const { installed, installedReady, reloadInstalled } = useInstalledSkills(operations, env, mounted);
|
|
61
|
+
const [appMode, setAppMode] = useState();
|
|
62
|
+
const appModeRef = useRef(undefined);
|
|
63
|
+
const setAppAction = (mode) => {
|
|
64
|
+
appModeRef.current = mode;
|
|
65
|
+
onCapturingChange?.(mode !== undefined || filter.filterRef.current.editing || actionRef.current.kind !== "browse");
|
|
66
|
+
setAppMode(mode);
|
|
67
|
+
};
|
|
56
68
|
const setAction = (next) => {
|
|
57
69
|
actionRef.current = next;
|
|
58
70
|
onCapturingChange?.(filter.filterRef.current.editing || next.kind !== "browse");
|
|
@@ -61,7 +73,13 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
61
73
|
const skills = state.status === "ready" ? state.skills : [];
|
|
62
74
|
const installable = useMemo(() => skills.filter((skill) => !isInstalled(skill, installed)), [state, installed]);
|
|
63
75
|
const visible = useMemo(() => filterSkills(installable, filter.filter.query), [installable, filter.filter.query]);
|
|
64
|
-
const
|
|
76
|
+
const visibleApps = useMemo(() => {
|
|
77
|
+
if (appCatalog.status !== "ready")
|
|
78
|
+
return [];
|
|
79
|
+
const query = filter.filter.query.trim().toLowerCase();
|
|
80
|
+
return query === "" ? appCatalog.rows : appCatalog.rows.filter(row => (row.entry.recipe?.name ?? row.entry.file).toLowerCase().includes(query));
|
|
81
|
+
}, [appCatalog, filter.filter.query]);
|
|
82
|
+
const roots = useMemo(() => buildTree(visible, state.status === "ready" ? state.sources : [], state.status === "ready" ? state.bundles : [], state.status === "ready" ? state.activeBundles : [], visibleApps), [visible, state, visibleApps]);
|
|
65
83
|
const navigation = useTreeNavigation(roots, (nodes) => openByDefault(nodes, filter.queryRef.current));
|
|
66
84
|
// Discovering again starts the list over.
|
|
67
85
|
const { restart } = navigation;
|
|
@@ -86,6 +104,23 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
86
104
|
setAction(BROWSE);
|
|
87
105
|
},
|
|
88
106
|
};
|
|
107
|
+
const appManage = {
|
|
108
|
+
setMode: setAppAction,
|
|
109
|
+
browse: () => setAppAction(undefined),
|
|
110
|
+
setMessage,
|
|
111
|
+
isMounted: () => mounted.current,
|
|
112
|
+
unmark: ids => marks.setMarked(new Set([...marks.markedRef.current].filter(id => !ids.includes(id)))),
|
|
113
|
+
finish: async (result) => {
|
|
114
|
+
await reloadInstalled();
|
|
115
|
+
if (!mounted.current)
|
|
116
|
+
return;
|
|
117
|
+
await appCatalogState.reload();
|
|
118
|
+
if (!mounted.current)
|
|
119
|
+
return;
|
|
120
|
+
setMessage(result);
|
|
121
|
+
setAppAction(undefined);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
89
124
|
const packageHost = {
|
|
90
125
|
setMode: setAction,
|
|
91
126
|
browse: () => setAction(BROWSE),
|
|
@@ -107,29 +142,37 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
107
142
|
marks,
|
|
108
143
|
filter,
|
|
109
144
|
canWiden: sourceId !== undefined,
|
|
110
|
-
ready: state.status === "ready",
|
|
145
|
+
ready: state.status === "ready" && appCatalog.status === "ready",
|
|
111
146
|
toggleAll: () => setAll((value) => !value),
|
|
112
147
|
startPackage: (target) => void startPackageAction(packageContext, target, "i"),
|
|
148
|
+
startApp: (entry) => void startAppAction({ apps, manage: appManage, terminalRows: terminalRows || 24, terminalColumns: columns || 80 }, [entry], "i"),
|
|
113
149
|
setMessage,
|
|
114
150
|
setAction,
|
|
115
151
|
};
|
|
116
152
|
useKeys((input, key) => {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
else
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
const runningAppMode = appModeRef.current;
|
|
154
|
+
if (runningAppMode !== undefined)
|
|
155
|
+
handleAppKey({ apps, manage: appManage, terminalRows: terminalRows || 24, terminalColumns: columns || 80 }, runningAppMode, input, key);
|
|
156
|
+
else {
|
|
157
|
+
const mode = actionRef.current;
|
|
158
|
+
if (isPackageMode(mode))
|
|
159
|
+
handlePackageKey(packageContext, mode, input, key);
|
|
160
|
+
else if (mode.kind !== "browse")
|
|
161
|
+
handleCatalogActionKey(flow, mode, input, key);
|
|
162
|
+
else if (filter.filterRef.current.editing)
|
|
163
|
+
filter.handleEditKey(input, key, navigation.restart);
|
|
164
|
+
else
|
|
165
|
+
handleCatalogBrowseKey(browse, input, key);
|
|
166
|
+
}
|
|
126
167
|
});
|
|
127
168
|
const scope = all || sourceId === undefined ? "all sources" : sourceId;
|
|
128
|
-
if (state.status === "loading" || (state.status === "ready" && !installedReady))
|
|
129
|
-
return _jsx(Text, { children: "Loading
|
|
169
|
+
if (state.status === "loading" || appCatalog.status === "loading" || (state.status === "ready" && !installedReady))
|
|
170
|
+
return _jsx(Text, { children: "Loading catalog..." });
|
|
130
171
|
if (state.status === "error")
|
|
131
172
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
132
173
|
const window = computeWindow(rows.length, index, height);
|
|
133
174
|
const selectedCount = leavesOf(roots).filter((skill) => marks.marked.has(skillKey(skill))).length;
|
|
134
|
-
|
|
175
|
+
if (appMode !== undefined)
|
|
176
|
+
return _jsx(AppPanel, { mode: appMode });
|
|
177
|
+
return (_jsxs(Box, { flexDirection: "column", children: [state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), appCatalog.status === "ready" && appCatalog.error ? _jsxs(Text, { color: theme.error, children: ["Apps unavailable: ", appCatalog.error] }) : null, _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, appCount: visibleApps.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(CatalogSelectionHelp, { row: rows[index], height: helpLines }), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), appMode ? _jsx(AppPanel, { mode: appMode }) : _jsx(ActionPanel, { mode: action })] }));
|
|
135
178
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const TABS = [
|
|
2
|
+
["sources", "[1] Sources"],
|
|
3
|
+
["catalog", "[2] Catalog"],
|
|
4
|
+
["installations", "[3] Installations"],
|
|
5
|
+
["updates", "[4] Updates"],
|
|
6
|
+
["profile", "[5] Profile"],
|
|
7
|
+
];
|
|
8
|
+
export function headerTabRows(width) {
|
|
9
|
+
const rows = [];
|
|
10
|
+
let row = [];
|
|
11
|
+
let rowWidth = 0;
|
|
12
|
+
for (const tab of TABS) {
|
|
13
|
+
const separatorWidth = row.length === 0 ? 0 : 2;
|
|
14
|
+
// Each rendered row has at most one active marker, regardless of tab count.
|
|
15
|
+
const nextWidth = rowWidth + separatorWidth + tab[1].length;
|
|
16
|
+
if (row.length > 0 && nextWidth + 2 > width) {
|
|
17
|
+
rows.push(row);
|
|
18
|
+
row = [];
|
|
19
|
+
rowWidth = 0;
|
|
20
|
+
}
|
|
21
|
+
rowWidth += (row.length === 0 ? 0 : 2) + tab[1].length;
|
|
22
|
+
row.push(tab);
|
|
23
|
+
}
|
|
24
|
+
if (row.length > 0)
|
|
25
|
+
rows.push(row);
|
|
26
|
+
return rows;
|
|
27
|
+
}
|
|
28
|
+
export function headerTabRowCount(width) {
|
|
29
|
+
return headerTabRows(width).length;
|
|
30
|
+
}
|
|
31
|
+
export function formatHeaderTabs(view, width) {
|
|
32
|
+
return headerTabRows(width).map(row => row.map(([name, label]) => `${view === name ? "▶ " : ""}${label}`).join(" ")).join("\n");
|
|
33
|
+
}
|
|
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { HOST_CHOICES } from "./catalog-actions.js";
|
|
4
4
|
import { ManagePanel } from "./manage-panel.js";
|
|
5
|
-
import { HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
5
|
+
import { DecisionPrompt, HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
6
6
|
import { theme } from "./theme.js";
|
|
7
7
|
/** The prompt of the adoption and manage modes; browsing shows nothing. */
|
|
8
8
|
export function ModePanel({ mode }) {
|
|
@@ -18,9 +18,9 @@ export function ModePanel({ mode }) {
|
|
|
18
18
|
case "fixed":
|
|
19
19
|
return _jsxs(Text, { children: ["Fixed version: ", mode.value, "_ (Enter confirm, Esc cancel)"] });
|
|
20
20
|
case "confirm":
|
|
21
|
-
return mode.adoption.verdict.adoptable ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.adoption.prepared.preview }), mode.adoption.prepared.runsExternalCommand ? _jsx(Text, { color: theme.warning, children: "This install runs an external command (see above)." }) : null, _jsxs(
|
|
21
|
+
return mode.adoption.verdict.adoptable ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(PreviewLines, { lines: mode.adoption.prepared.preview }), mode.adoption.prepared.runsExternalCommand ? _jsx(Text, { color: theme.warning, children: "This install runs an external command (see above)." }) : null, _jsxs(DecisionPrompt, { children: ["Adopt ", mode.entry.name, "?"] })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.error, children: mode.adoption.verdict.reason }), _jsx(Text, { children: "Enter/Esc to go back" })] }));
|
|
22
22
|
case "confirm-exposure":
|
|
23
|
-
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). Confirm?
|
|
23
|
+
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
24
|
default:
|
|
25
25
|
return _jsx(ManagePanel, { mode: mode });
|
|
26
26
|
}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Text } from "ink";
|
|
3
|
-
import {
|
|
4
|
-
import { describePackageRow } from "./package-rows.js";
|
|
3
|
+
import { describeLinks, describeNode, markersOf } from "./installations-tree.js";
|
|
5
4
|
import { rowStyle, theme } from "./theme.js";
|
|
5
|
+
function AppDetails({ row }) {
|
|
6
|
+
const version = row.inspection.installedVersion;
|
|
7
|
+
const statusColor = row.inspection.status === "invalid" ? theme.error : theme.warning;
|
|
8
|
+
const invalid = row.inspection.status === "invalid" ? ` · invalid recipe: ${row.inspection.reason ?? "unknown error"}` : "";
|
|
9
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: version === undefined ? statusColor : theme.version, children: version ?? row.inspection.status }), row.inspection.evidence === "recorded" ? _jsx(Text, { color: theme.value, children: " (recorded)" }) : null, invalid === "" ? null : _jsx(Text, { color: theme.error, children: invalid })] }));
|
|
10
|
+
}
|
|
11
|
+
function PackageDetails({ row }) {
|
|
12
|
+
const version = row.installedVersion;
|
|
13
|
+
const installedPrefix = version === undefined ? "" : row.installed.startsWith("installed at ") ? "installed at " : "installed ";
|
|
14
|
+
const installedSuffix = version === undefined ? "" : row.installed.slice(installedPrefix.length + version.length);
|
|
15
|
+
const status = row.status.kind === "unknown" && row.status.reason === "local changes"
|
|
16
|
+
? "update not checked · local changes"
|
|
17
|
+
: row.status.kind === "unknown" ? "cannot assess" : row.status.kind === "current" ? "up to date" : "update available";
|
|
18
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsxs(Text, { color: theme.value, children: [row.selection.host, " "] }), version === undefined ? _jsx(Text, { color: theme.warning, children: row.installed }) : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: installedPrefix }), _jsx(Text, { color: theme.version, children: version }), _jsx(Text, { color: theme.value, children: installedSuffix })] }), _jsx(Text, { color: theme.value, children: " available " }), _jsx(Text, { color: row.available === "unknown" ? theme.warning : theme.version, children: row.available }), _jsxs(Text, { color: theme.warning, children: [" ", status] }), row.drift === undefined ? null : _jsxs(Text, { color: theme.warning, children: [" drift: ", row.drift] })] }));
|
|
19
|
+
}
|
|
6
20
|
/** One line of the Installations tree: cursor, indent, mark box, label and the leaf's details. */
|
|
7
21
|
export function TreeRowLine({ row, selected, marked }) {
|
|
8
22
|
const item = row.node.data;
|
|
9
|
-
const
|
|
23
|
+
const markable = item.kind !== "group" && item.kind !== "package";
|
|
24
|
+
const leaf = !row.expandable && markable;
|
|
10
25
|
const prefix = `${selected ? "> " : " "}${" ".repeat(row.depth)}${leaf ? (marked ? "[x] " : "[ ] ") : ""}`;
|
|
11
|
-
return (_jsxs(Text, { ...rowStyle(selected), color: item.kind === "group" ? theme.group : undefined, children: [prefix, describeNode(row), item.kind === "installation" ? (_jsxs(Text, { color: theme.
|
|
26
|
+
return (_jsxs(Text, { ...rowStyle(selected), color: item.kind === "group" ? theme.group : undefined, children: [prefix, item.kind === "app" ? item.row.entry.recipe?.name ?? item.row.name ?? item.row.entry.file : describeNode(row), item.kind === "installation" ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: item.row.installed === "unknown" ? theme.warning : theme.version, children: item.row.installed }), _jsxs(Text, { color: theme.field, children: [" (", item.row.policy, ") [", item.row.selection.hosts.join(", "), "]"] }), markersOf(item.row) === "" ? null : _jsxs(Text, { color: theme.marker, children: [" ", markersOf(item.row)] })] })) : null, item.kind === "app" ? _jsx(AppDetails, { row: item.row }) : null, item.kind === "package" ? _jsx(PackageDetails, { row: item.row }) : null, item.kind === "unmanaged" && describeLinks(item.group) !== "" ? (_jsxs(Text, { color: theme.success, children: [" ", describeLinks(item.group)] })) : null] }));
|
|
12
27
|
}
|
|
@@ -49,10 +49,6 @@ export function buildTree(data) {
|
|
|
49
49
|
});
|
|
50
50
|
return roots;
|
|
51
51
|
}
|
|
52
|
-
/** Version, policy and hosts of a leaf; `adopted` / `modified` only when true (the legend explains them). */
|
|
53
|
-
export function describeDetails({ selection, policy, installed }) {
|
|
54
|
-
return `${installed} (${policy}) [${selection.hosts.join(", ")}]`;
|
|
55
|
-
}
|
|
56
52
|
export function markersOf({ adopted, modified }) {
|
|
57
53
|
return [adopted ? "adopted" : "", modified ? "modified" : ""].filter((flag) => flag !== "").join(" ");
|
|
58
54
|
}
|
|
@@ -78,11 +74,12 @@ export function describeNode(row) {
|
|
|
78
74
|
return data.row.name;
|
|
79
75
|
if (data.kind === "app") {
|
|
80
76
|
const { entry, inspection } = data.row;
|
|
81
|
-
const name = entry.recipe?.name ?? entry.file;
|
|
82
|
-
if (inspection.status === "invalid")
|
|
77
|
+
const name = entry.recipe?.name ?? data.row.name ?? entry.file;
|
|
78
|
+
if (inspection.status === "invalid" && inspection.evidence !== "recorded")
|
|
83
79
|
return `${name} invalid recipe: ${inspection.reason}`;
|
|
84
80
|
const version = inspection.installedVersion ?? inspection.status;
|
|
85
|
-
|
|
81
|
+
const invalid = inspection.status === "invalid" ? ` · invalid recipe: ${inspection.reason}` : "";
|
|
82
|
+
return `${name} ${version}${inspection.evidence === "recorded" ? " (recorded)" : ""}${invalid}`;
|
|
86
83
|
}
|
|
87
84
|
return `${data.group.name} [${data.group.hosts.join(", ")}]`;
|
|
88
85
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createAppOperations } from "../app-flow.js";
|
|
5
|
-
import { handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
|
|
5
|
+
import { AppPanel, handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
|
|
6
6
|
import { handlePackageKey, isPackageMode } from "./package-actions.js";
|
|
7
7
|
import { packageOperationsOf } from "./environment.js";
|
|
8
8
|
import { loadInstallations } from "./installations.js";
|
|
@@ -24,6 +24,7 @@ import { useKeys } from "./keys.js";
|
|
|
24
24
|
const NO_ENVIRONMENT = {};
|
|
25
25
|
const LEGEND = "adopted: tracked in place · modified: changed on disk";
|
|
26
26
|
export function InstallationsView({ operations, environment, onCapturingChange, listHeight }) {
|
|
27
|
+
const { rows: terminalRows, columns: terminalColumns } = useWindowSize();
|
|
27
28
|
const height = useListHeight(listHeight, 7);
|
|
28
29
|
const env = environment ?? NO_ENVIRONMENT;
|
|
29
30
|
const apps = useMemo(() => createAppOperations({ homeDirectory: env.homeDirectory, ...env.appEnvironment }), [env]);
|
|
@@ -71,9 +72,15 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
71
72
|
return;
|
|
72
73
|
checked.current.add(identity);
|
|
73
74
|
const generation = loadGeneration.current;
|
|
74
|
-
void apps.
|
|
75
|
-
if (
|
|
75
|
+
void apps.approvalStatus(entry).then(approval => {
|
|
76
|
+
if (approval.status !== "approved")
|
|
77
|
+
return undefined;
|
|
78
|
+
return apps.inspect(entry);
|
|
79
|
+
}).then(inspection => {
|
|
80
|
+
if (!inspection || !mounted.current || generation !== loadGeneration.current)
|
|
76
81
|
return;
|
|
82
|
+
if (inspection.status === "unknown")
|
|
83
|
+
checked.current.delete(identity);
|
|
77
84
|
setState(previous => previous.status !== "ready" ? previous : {
|
|
78
85
|
...previous,
|
|
79
86
|
data: {
|
|
@@ -131,7 +138,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
131
138
|
packages,
|
|
132
139
|
startAdoption: (group) => void startAdoption(adoption, group),
|
|
133
140
|
setMessage,
|
|
134
|
-
startApps: (entries, input) => void startAppAction({ apps, manage }, entries, input),
|
|
141
|
+
startApps: (entries, input) => void startAppAction({ apps, manage, terminalRows: terminalRows || 24, terminalColumns: terminalColumns || 80 }, entries, input),
|
|
135
142
|
};
|
|
136
143
|
useKeys((input, key) => {
|
|
137
144
|
const current = modeRef.current;
|
|
@@ -140,7 +147,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
140
147
|
else if (isPackageMode(current))
|
|
141
148
|
handlePackageKey({ packages, manage }, current, input, key);
|
|
142
149
|
else if (isAppMode(current))
|
|
143
|
-
handleAppKey({ apps, manage }, current, input, key);
|
|
150
|
+
handleAppKey({ apps, manage, terminalRows: terminalRows || 24, terminalColumns: terminalColumns || 80 }, current, input, key);
|
|
144
151
|
else if (!handleManageKey(manage, current, input, key))
|
|
145
152
|
handleAdoptionKey(adoption, current, input, key);
|
|
146
153
|
});
|
|
@@ -148,6 +155,8 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
148
155
|
return _jsx(Text, { children: "Loading installations..." });
|
|
149
156
|
if (state.status === "error")
|
|
150
157
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
158
|
+
if (isAppMode(mode))
|
|
159
|
+
return _jsx(AppPanel, { mode: mode });
|
|
151
160
|
const ready = state.data;
|
|
152
161
|
const window = computeWindow(rows.length, index, height);
|
|
153
162
|
const listed = rows.slice(window.start, window.end);
|
|
@@ -79,13 +79,15 @@ export async function loadInstallations(operations, environment, appOperations =
|
|
|
79
79
|
let appsError;
|
|
80
80
|
try {
|
|
81
81
|
const tracked = await appOperations.trackedApps();
|
|
82
|
-
apps = await Promise.all((await appOperations.load()).filter(entry => entry.applicable).map(async (entry) =>
|
|
83
|
-
entry
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
apps = (await Promise.all((await appOperations.load()).filter(entry => entry.applicable).map(async (entry) => {
|
|
83
|
+
const record = tracked.find(item => item.recipeFile === entry.canonicalFile
|
|
84
|
+
&& (item.name === entry.recipe?.name || entry.recipe === undefined));
|
|
85
|
+
const inspection = record && (entry.error || !entry.recipe)
|
|
86
|
+
? { status: "invalid", installedVersion: record.installedVersion, evidence: "recorded", reason: entry.error }
|
|
87
|
+
: await appOperations.inspect(entry, record === undefined ? undefined : { installedVersion: record.installedVersion });
|
|
88
|
+
return { entry, inspection, ...(record === undefined ? {} : { name: record.name }) };
|
|
89
|
+
}))).filter(row => row.inspection.status === "installed"
|
|
90
|
+
|| (row.inspection.status === "invalid" && row.inspection.evidence === "recorded"));
|
|
89
91
|
}
|
|
90
92
|
catch (error) {
|
|
91
93
|
appsError = errorText(error);
|
|
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { AppPanel } from "./app-actions.js";
|
|
4
4
|
import { PackagePanel } from "./package-actions.js";
|
|
5
|
-
import { ChecklistLines, HostChecklist, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
5
|
+
import { ChecklistLines, DecisionPrompt, HostChecklist, PreviewConfirm, PreviewLines } from "./panel-parts.js";
|
|
6
6
|
import { theme } from "./theme.js";
|
|
7
7
|
const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
|
|
8
8
|
function SkippedLines({ skipped }) {
|
|
@@ -10,7 +10,7 @@ function SkippedLines({ skipped }) {
|
|
|
10
10
|
}
|
|
11
11
|
/** One block per planned item, the skipped ones, then the y/n question. */
|
|
12
12
|
function PlanPreview({ entries, skipped, question }) {
|
|
13
|
-
return (_jsxs(Box, { flexDirection: "column", children: [entries.map((entry) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.group, children: entry.label }), _jsx(PreviewLines, { lines: entry.lines, warnFirst: entry.warnFirst })] }, entry.id))), _jsx(SkippedLines, { skipped: skipped }), _jsx(
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", children: [entries.map((entry) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.group, children: entry.label }), _jsx(PreviewLines, { lines: entry.lines, warnFirst: entry.warnFirst })] }, entry.id))), _jsx(SkippedLines, { skipped: skipped }), _jsx(DecisionPrompt, { children: question })] }));
|
|
14
14
|
}
|
|
15
15
|
function UninstallPreview({ plan }) {
|
|
16
16
|
const entries = plan.items.map((item) => ({
|
|
@@ -19,15 +19,16 @@ function UninstallPreview({ plan }) {
|
|
|
19
19
|
lines: item.kind === "managed" ? item.prepared.plan.preview : item.prepared.preview,
|
|
20
20
|
warnFirst: item.kind === "unmanaged",
|
|
21
21
|
}));
|
|
22
|
-
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Uninstall ${plural(plan.items.length)}
|
|
22
|
+
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Uninstall ${plural(plan.items.length)}?` });
|
|
23
23
|
}
|
|
24
24
|
function HostsPreview({ plan }) {
|
|
25
25
|
const entries = plan.items.map((item) => ({ id: item.id, label: item.label, lines: item.prepared.preview, warnFirst: false }));
|
|
26
|
-
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Add hosts to ${plural(plan.items.length)}
|
|
26
|
+
return _jsx(PlanPreview, { entries: entries, skipped: plan.skipped, question: `Add hosts to ${plural(plan.items.length)}?` });
|
|
27
27
|
}
|
|
28
28
|
/** Renders the prompt of a manage mode: host lists, previews and confirmations. */
|
|
29
29
|
export function ManagePanel({ mode }) {
|
|
30
30
|
switch (mode.kind) {
|
|
31
|
+
case "app-busy":
|
|
31
32
|
case "app-approve":
|
|
32
33
|
case "app-hosts":
|
|
33
34
|
case "app-confirm":
|
|
@@ -45,7 +46,7 @@ export function ManagePanel({ mode }) {
|
|
|
45
46
|
case "confirm-host-add":
|
|
46
47
|
return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Add hosts to ${mode.skill.name}? y/n` });
|
|
47
48
|
case "confirm-host-exposure":
|
|
48
|
-
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.skill.name, " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). Confirm?
|
|
49
|
+
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.skill.name, " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). ", _jsx(DecisionPrompt, { children: "Confirm?" })] }));
|
|
49
50
|
case "confirm-host-remove":
|
|
50
51
|
return _jsx(PreviewConfirm, { lines: mode.prepared.preview, question: `Remove hosts from ${mode.skill.name}? y/n` });
|
|
51
52
|
case "confirm-uninstall":
|
|
@@ -63,7 +64,7 @@ export function ManagePanel({ mode }) {
|
|
|
63
64
|
case "confirm-bulk-hosts":
|
|
64
65
|
return _jsx(HostsPreview, { plan: mode.plan });
|
|
65
66
|
case "confirm-bulk-exposure":
|
|
66
|
-
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.plan.items.map((item) => item.label).join(", "), " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). Confirm?
|
|
67
|
+
return (_jsxs(Text, { color: theme.warning, children: ["Adding hosts exposes the installed ", mode.plan.items.map((item) => item.label).join(", "), " to more hosts and needs separate confirmation (CLI: --confirm-additional-host). ", _jsx(DecisionPrompt, { children: "Confirm?" })] }));
|
|
67
68
|
case "busy":
|
|
68
69
|
return _jsx(Text, { children: mode.label });
|
|
69
70
|
}
|
|
@@ -7,6 +7,15 @@ export function moveCursor(cursor, length, input, key) {
|
|
|
7
7
|
return Math.max(cursor - 1, 0);
|
|
8
8
|
return undefined;
|
|
9
9
|
}
|
|
10
|
+
/** Read-only preview navigation: arrows and page keys move a viewport; j/k move one line. */
|
|
11
|
+
export function movePreviewCursor(cursor, length, input, key, pageSize) {
|
|
12
|
+
const step = key.pageDown || key.downArrow ? pageSize : key.pageUp || key.upArrow ? -pageSize : 0;
|
|
13
|
+
if (step !== 0)
|
|
14
|
+
return Math.min(Math.max(cursor + step, 0), Math.max(length - 1, 0));
|
|
15
|
+
if (input === "j" || input === "k")
|
|
16
|
+
return moveCursor(cursor, length, input, key);
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
10
19
|
/** `selected` with `choice` flipped, always in the order of `choices`. */
|
|
11
20
|
export function toggleChoice(choices, selected, choice) {
|
|
12
21
|
return choices.filter((candidate) => candidate === choice ? !selected.includes(candidate) : selected.includes(candidate));
|
|
@@ -51,15 +51,27 @@ export function packageRow(record, inspection) {
|
|
|
51
51
|
selection: record.selection,
|
|
52
52
|
name: inspection.descriptor?.name ?? describeCoordinates(record.selection),
|
|
53
53
|
installed: installedEvidenceLabel(inspection.installed),
|
|
54
|
+
...(inspection.installed.kind === "installed" && inspection.installed.version !== undefined
|
|
55
|
+
? { installedVersion: inspection.installed.version }
|
|
56
|
+
: inspection.installed.kind === "installed" && inspection.installed.commit !== undefined
|
|
57
|
+
? { installedVersion: inspection.installed.commit.slice(0, 7) }
|
|
58
|
+
: {}),
|
|
54
59
|
available: packageVersionLabel(inspection.available),
|
|
55
|
-
status:
|
|
60
|
+
status: packageRowStatus(inspection),
|
|
56
61
|
...(inspection.drift === undefined ? {} : { drift: inspection.drift }),
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
function packageRowStatus(inspection) {
|
|
65
|
+
const status = classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor);
|
|
66
|
+
if (status === "unknown") {
|
|
67
|
+
return {
|
|
68
|
+
kind: "unknown",
|
|
69
|
+
reason: inspection.installed.kind === "installed" && /local modifications|local changes/iu.test(inspection.installed.reason ?? "")
|
|
70
|
+
? "local changes"
|
|
71
|
+
: "unknown",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { kind: status };
|
|
63
75
|
}
|
|
64
76
|
/** One component of the inventory; the executable effect and a host-only owner are called out. */
|
|
65
77
|
export function describePackageComponent(component) {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Box, Text } from "ink";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { theme } from "./theme.js";
|
|
4
|
+
import { previewViewport } from "./preview-window.js";
|
|
5
|
+
/** A titled, color-independent boundary for a focused interaction or result. */
|
|
6
|
+
export function BorderPanel({ title, children }) {
|
|
7
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: theme.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.title, bold: true, children: title }), children] }));
|
|
8
|
+
}
|
|
9
|
+
export function FieldLine({ label, value }) {
|
|
10
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: label }), _jsxs(Text, { color: theme.value, children: [" ", value] })] });
|
|
11
|
+
}
|
|
12
|
+
export function DecisionPrompt({ children }) {
|
|
13
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: children }), " ", _jsx(Text, { color: theme.success, bold: true, children: "y" }), _jsx(Text, { color: theme.legend, children: "/" }), _jsx(Text, { color: theme.error, children: "n" })] }));
|
|
14
|
+
}
|
|
4
15
|
/** The `> [x] 1 name` rows of a checklist; the caller renders the header. */
|
|
5
16
|
export function ChecklistLines({ choices, cursor, isSelected, keyOf, label }) {
|
|
6
17
|
return (_jsx(_Fragment, { children: choices.map((choice, position) => (_jsxs(Text, { children: [position === cursor ? "> " : " ", "[", isSelected(choice) ? "x" : " ", "] ", position + 1, " ", label(choice)] }, keyOf(choice)))) }));
|
|
@@ -10,14 +21,35 @@ export function HostChecklist({ choices, cursor, selected }) {
|
|
|
10
21
|
return _jsx(ChecklistLines, { choices: choices, cursor: cursor, isSelected: (host) => selected.includes(host), keyOf: (host) => host, label: (host) => host });
|
|
11
22
|
}
|
|
12
23
|
/** Preview lines; `warnFirst` colors the first one as a warning. */
|
|
24
|
+
const PREVIEW_FIELD_LABELS = [
|
|
25
|
+
"File:", "Name:", "Platform:", "Status:", "Homepage:", "Owned Skills:",
|
|
26
|
+
"App:", "Environment:", "Version:", "argv:", "Executable:", "Manual:",
|
|
27
|
+
"Declaration:", "Version pattern:", "Working directory:", "Previous version:",
|
|
28
|
+
"Latest version:", "Package:", "Host:", "Source:", "Scope:", "Coordinates:", "Origin:",
|
|
29
|
+
"Available:", "Approval:", "Path:", "Blast radius:", "Warning:", " target:", " proposed changes:",
|
|
30
|
+
" external command:", " external cwd:", " external target:",
|
|
31
|
+
];
|
|
32
|
+
function previewField(line) {
|
|
33
|
+
const label = PREVIEW_FIELD_LABELS.find(candidate => line.startsWith(candidate));
|
|
34
|
+
return label === undefined ? undefined : { label, value: line.slice(label.length) };
|
|
35
|
+
}
|
|
13
36
|
export function PreviewLines({ lines, warnFirst = false }) {
|
|
14
|
-
return (_jsx(_Fragment, { children: lines.map((line, position) =>
|
|
37
|
+
return (_jsx(_Fragment, { children: lines.map((line, position) => {
|
|
38
|
+
const field = previewField(line);
|
|
39
|
+
return field ? (_jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: field.label }), _jsx(Text, { color: theme.value, children: field.value })] }, position)) : _jsx(Text, { color: warnFirst && position === 0 ? theme.warning : theme.value, children: line }, position);
|
|
40
|
+
}) }));
|
|
15
41
|
}
|
|
16
42
|
/** Under a list: the window indicator, how many items are marked, and the last result. */
|
|
17
43
|
export function ListFooter({ indicator, selectedCount, message }) {
|
|
18
|
-
return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, selectedCount > 0 ? _jsxs(Text, { color: theme.muted, children: [selectedCount, " selected"] }) : null, message ? _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) : null] }));
|
|
44
|
+
return (_jsxs(_Fragment, { children: [indicator ? _jsx(Text, { color: theme.muted, children: indicator }) : null, selectedCount > 0 ? _jsxs(Text, { color: theme.muted, children: [selectedCount, " selected"] }) : null, message ? (_jsx(BorderPanel, { title: message.kind === "error" ? "Result · failed" : "Result · complete", children: _jsx(Text, { color: message.kind === "error" ? theme.error : theme.success, children: message.text }) })) : null] }));
|
|
19
45
|
}
|
|
20
46
|
/** A preview followed by a y/n question. */
|
|
21
|
-
export function PreviewConfirm({ lines, question, warnFirst }) {
|
|
22
|
-
|
|
47
|
+
export function PreviewConfirm({ title = "Confirmation", lines, question, warnFirst, cursor, chrome }) {
|
|
48
|
+
const { rows, columns } = useWindowSize();
|
|
49
|
+
const viewport = cursor === undefined ? undefined : previewViewport(lines, cursor, rows || 24, columns || 80, chrome ?? { title, questions: [question], footerRows: 2 });
|
|
50
|
+
const shown = viewport?.rows ?? lines;
|
|
51
|
+
return (_jsxs(BorderPanel, { title: title, children: [_jsx(PreviewLines, { lines: shown, warnFirst: warnFirst && (viewport?.start ?? 0) === 0 }), viewport && viewport.total > viewport.height ? _jsxs(Text, { color: theme.muted, children: [viewport.start + 1, "\u2013", viewport.end, " of ", viewport.total] }) : null, viewport ? _jsxs(Text, { color: theme.legend, children: ["Review ", viewport.cursor + 1, " of ", viewport.total, " \u00B7 arrows/Pg page"] }) : null, question.includes("y/n") ? (() => {
|
|
52
|
+
const marker = question.indexOf("y/n");
|
|
53
|
+
return _jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: question.slice(0, marker) }), _jsx(Text, { color: theme.success, bold: true, children: "y" }), _jsx(Text, { color: theme.legend, children: "/" }), _jsx(Text, { color: theme.error, children: "n" }), _jsx(Text, { color: theme.legend, children: question.slice(marker + 3) })] });
|
|
54
|
+
})() : _jsx(Text, { color: theme.legend, children: question })] }));
|
|
23
55
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
import { headerTabRowCount } from "./header-layout.js";
|
|
3
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
4
|
+
function wrapLine(line, width) {
|
|
5
|
+
const segments = [...graphemes.segment(line)];
|
|
6
|
+
if (segments.length === 0)
|
|
7
|
+
return [""];
|
|
8
|
+
const wrapped = [];
|
|
9
|
+
let row = "";
|
|
10
|
+
let rowWidth = 0;
|
|
11
|
+
for (const { segment } of segments) {
|
|
12
|
+
const segmentWidth = stringWidth(segment);
|
|
13
|
+
if (row !== "" && rowWidth + segmentWidth > width) {
|
|
14
|
+
wrapped.push(row);
|
|
15
|
+
row = "";
|
|
16
|
+
rowWidth = 0;
|
|
17
|
+
}
|
|
18
|
+
row += segment;
|
|
19
|
+
rowWidth += segmentWidth;
|
|
20
|
+
}
|
|
21
|
+
wrapped.push(row);
|
|
22
|
+
return wrapped;
|
|
23
|
+
}
|
|
24
|
+
export function previewViewport(lines, cursor, terminalRows, terminalColumns, chrome = { title: "Confirmation", questions: ["Scroll to end before y; n cancels.", "Confirm? y/n"] }) {
|
|
25
|
+
const contentWidth = Math.max(terminalColumns - 8, 1);
|
|
26
|
+
const wrapped = lines.flatMap((line, sourceLine) => wrapLine(line, contentWidth).map((row) => ({ row, sourceLine })));
|
|
27
|
+
const previewRows = wrapped.map(({ row }) => row);
|
|
28
|
+
const headerRows = headerTabRowCount(contentWidth) + 3;
|
|
29
|
+
const titleRows = wrapLine(chrome.title, contentWidth).length;
|
|
30
|
+
const questionRows = Math.max(...chrome.questions.map(question => wrapLine(question, contentWidth).length));
|
|
31
|
+
const panelChromeRows = 2 + titleRows + questionRows + (chrome.footerRows ?? 1);
|
|
32
|
+
const reservedRows = chrome.reservedRows !== undefined && chrome.reservedRows > 0 ? Math.ceil(chrome.reservedRows) : 0;
|
|
33
|
+
const availableRows = Math.max(terminalRows - headerRows - 1 - panelChromeRows - reservedRows, 1);
|
|
34
|
+
const height = previewRows.length > availableRows ? Math.max(availableRows - 1, 1) : availableRows;
|
|
35
|
+
const selected = Math.min(Math.max(cursor, 0), Math.max(previewRows.length - 1, 0));
|
|
36
|
+
const start = Math.min(Math.max(selected - Math.floor(height / 2), 0), Math.max(previewRows.length - height, 0));
|
|
37
|
+
const end = Math.min(start + height, previewRows.length);
|
|
38
|
+
return {
|
|
39
|
+
rows: previewRows.slice(start, end),
|
|
40
|
+
sourceLines: wrapped.slice(start, end).map(({ sourceLine }) => sourceLine),
|
|
41
|
+
start,
|
|
42
|
+
end,
|
|
43
|
+
total: previewRows.length,
|
|
44
|
+
cursor: selected,
|
|
45
|
+
height,
|
|
46
|
+
reachedEnd: end === previewRows.length,
|
|
47
|
+
};
|
|
48
|
+
}
|