@iseoane/agent-depot 0.6.4 → 0.6.6
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 +94 -0
- package/README.es.md +14 -10
- package/README.md +22 -8
- package/dist/src/app-cli.js +10 -5
- package/dist/src/app-flow.js +2 -2
- package/dist/src/atomic-file.js +9 -0
- package/dist/src/cli.js +98 -67
- package/dist/src/executable-resolution.js +9 -3
- package/dist/src/git-source.js +2 -4
- package/dist/src/package-cli.js +9 -4
- package/dist/src/package-flow.js +2 -1
- package/dist/src/package-host-state.js +44 -6
- package/dist/src/package-state.js +2 -5
- package/dist/src/process-runner.js +234 -16
- package/dist/src/profile-cli.js +8 -6
- package/dist/src/progress.js +157 -0
- package/dist/src/self-update.js +6 -1
- package/dist/src/source-state.js +2 -5
- package/dist/src/tui/adoption-actions.js +4 -3
- package/dist/src/tui/app-actions.js +16 -9
- package/dist/src/tui/bulk-actions.js +7 -6
- package/dist/src/tui/catalog-flow.js +3 -2
- package/dist/src/tui/catalog-panel.js +2 -2
- package/dist/src/tui/catalog-view.js +26 -6
- package/dist/src/tui/file-picker.js +52 -20
- package/dist/src/tui/installations-view.js +44 -11
- package/dist/src/tui/manage-actions.js +7 -6
- package/dist/src/tui/manage-panel.js +2 -2
- package/dist/src/tui/package-actions.js +63 -31
- package/dist/src/tui/panel-parts.js +20 -0
- package/dist/src/tui/profile-view.js +84 -15
- package/dist/src/tui/sources-mode.js +2 -1
- package/dist/src/tui/sources-recipes.js +22 -10
- package/dist/src/tui/sources-view.js +13 -12
- package/dist/src/tui/text-wrap.js +13 -0
- package/dist/src/tui/unmanaged-actions.js +3 -2
- package/dist/src/tui/updates-flow.js +10 -7
- package/dist/src/tui/updates-keys.js +3 -2
- package/dist/src/tui/updates-model.js +2 -2
- package/dist/src/tui/updates-panel.js +3 -2
- package/dist/src/tui/updates-view.js +6 -3
- package/dist/src/tui/view-state.js +12 -0
- package/dist/src/tui/working-model.js +33 -0
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import { runInstall } from "./catalog-installs.js";
|
|
|
3
3
|
import { errorText } from "./batch.js";
|
|
4
4
|
import { defaultAdoptionHosts, findAdoptionCandidates, prepareAdoption } from "./installations.js";
|
|
5
5
|
import { answerYesNo, applyChecklistOutcome, hostChecklistKey, moveCursor, versionInputKey } from "./mode-keys.js";
|
|
6
|
+
import { busy } from "./working-model.js";
|
|
6
7
|
function cancel(context) {
|
|
7
8
|
context.setMessage({ kind: "ok", text: "Adoption cancelled" });
|
|
8
9
|
context.browse();
|
|
@@ -22,7 +23,7 @@ export async function startAdoption(context, group) {
|
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
24
25
|
context.setMessage(undefined);
|
|
25
|
-
context.setMode(
|
|
26
|
+
context.setMode(busy(`Looking for a Source Skill named ${entry.name}…`));
|
|
26
27
|
try {
|
|
27
28
|
const candidates = await findAdoptionCandidates(context.operations, entry);
|
|
28
29
|
if (!context.isMounted())
|
|
@@ -43,7 +44,7 @@ export async function startAdoption(context, group) {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
async function prepare(context, mode, version) {
|
|
46
|
-
context.setMode(
|
|
47
|
+
context.setMode(busy(`Refreshing source and checking that ${mode.entry.name} matches…`));
|
|
47
48
|
try {
|
|
48
49
|
const adoption = await prepareAdoption(context.operations, context.env, mode.entry, mode.skill, { hosts: mode.hosts, version });
|
|
49
50
|
if (!context.isMounted())
|
|
@@ -55,7 +56,7 @@ async function prepare(context, mode, version) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
async function confirm(context, mode, exposure) {
|
|
58
|
-
context.setMode(
|
|
59
|
+
context.setMode(busy(`Adopting ${mode.entry.name}…`));
|
|
59
60
|
let result;
|
|
60
61
|
try {
|
|
61
62
|
result = { kind: "ok", text: (await runInstall(mode.adoption.prepared, context.operations, exposure)).join(" ") };
|
|
@@ -3,8 +3,9 @@ import { Text, useWindowSize } from "ink";
|
|
|
3
3
|
import { PROJECT_HOSTS } from "../project-manifest.js";
|
|
4
4
|
import { errorText, runBatch } from "./batch.js";
|
|
5
5
|
import { hostChecklistKey, movePreviewCursor } from "./mode-keys.js";
|
|
6
|
-
import { BorderPanel, HostChecklist, PreviewConfirm } from "./panel-parts.js";
|
|
6
|
+
import { BorderPanel, HostChecklist, PreviewConfirm, Working } from "./panel-parts.js";
|
|
7
7
|
import { previewViewport } from "./preview-window.js";
|
|
8
|
+
import { operationStart } from "./working-model.js";
|
|
8
9
|
export const APP_KINDS = ["app-busy", "app-approve", "app-hosts", "app-confirm", "app-manual"];
|
|
9
10
|
/** Narrows the view's mode without asserting a partial mode is an App mode. */
|
|
10
11
|
export function isAppMode(mode) {
|
|
@@ -59,9 +60,14 @@ function appPreviewLines(mode) {
|
|
|
59
60
|
...appStepLines(mode.plan),
|
|
60
61
|
];
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
/** The busy mode of an App step; `previous` is the busy mode this step continues, so the clock keeps running. */
|
|
64
|
+
function appBusy(label, previous) {
|
|
65
|
+
return { kind: "app-busy", label, startedAt: operationStart(previous) };
|
|
66
|
+
}
|
|
67
|
+
async function prepareNext(context, requests, results = [], failures = 0, previous) {
|
|
63
68
|
const { manage, apps } = context;
|
|
64
|
-
|
|
69
|
+
const running = appBusy("Preparing App step…", previous);
|
|
70
|
+
manage.setMode(running);
|
|
65
71
|
// Independent planning failures never stop the remaining marked Apps.
|
|
66
72
|
const [request, ...remaining] = requests;
|
|
67
73
|
if (!request) {
|
|
@@ -79,7 +85,7 @@ async function prepareNext(context, requests, results = [], failures = 0) {
|
|
|
79
85
|
}
|
|
80
86
|
catch (error) {
|
|
81
87
|
if (manage.isMounted()) {
|
|
82
|
-
await prepareNext(context, remaining, [...results, `${appName(request.entry)}: failed: ${errorText(error)}`], failures + 1);
|
|
88
|
+
await prepareNext(context, remaining, [...results, `${appName(request.entry)}: failed: ${errorText(error)}`], failures + 1, running);
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
}
|
|
@@ -92,7 +98,7 @@ export async function startAppAction(context, entries, input) {
|
|
|
92
98
|
const entry = entries[0];
|
|
93
99
|
if (!entry)
|
|
94
100
|
throw new Error("No App selected");
|
|
95
|
-
manage.setMode(
|
|
101
|
+
manage.setMode(appBusy("Preparing recipe approval…"));
|
|
96
102
|
const [steps, approval] = await Promise.all([apps.preview(entry), apps.approvalStatus(entry)]);
|
|
97
103
|
if (manage.isMounted()) {
|
|
98
104
|
manage.setMode({ kind: "app-approve", entry, lines: appRecipePreviewLines(entry, approval.status, steps), cursor: 0 });
|
|
@@ -118,7 +124,8 @@ export async function startAppAction(context, entries, input) {
|
|
|
118
124
|
}
|
|
119
125
|
async function execute(context, mode) {
|
|
120
126
|
const { manage, apps } = context;
|
|
121
|
-
|
|
127
|
+
const running = appBusy("Running App step…");
|
|
128
|
+
manage.setMode(running);
|
|
122
129
|
let manualRequired = false;
|
|
123
130
|
const [outcome] = await runBatch([mode.plan], async (plan) => {
|
|
124
131
|
const result = mode.kind === "app-manual"
|
|
@@ -136,7 +143,7 @@ async function execute(context, mode) {
|
|
|
136
143
|
return `${appName(plan.entry)}: ${result.status}${result.installedVersion ? ` ${result.installedVersion}` : ""}`;
|
|
137
144
|
}, (plan, error) => `${appName(plan.entry)}: failed: ${errorText(error)}`);
|
|
138
145
|
if (manage.isMounted() && outcome && !manualRequired) {
|
|
139
|
-
await prepareNext(context, mode.remaining, [...mode.results, outcome.line], mode.failures + (outcome.ok ? 0 : 1));
|
|
146
|
+
await prepareNext(context, mode.remaining, [...mode.results, outcome.line], mode.failures + (outcome.ok ? 0 : 1), running);
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
/** Applies checklist and y/n keys, continuing independent batch steps after cancellation or failure. */
|
|
@@ -187,7 +194,7 @@ export function handleAppKey(context, mode, input, key) {
|
|
|
187
194
|
void execute(context, mode);
|
|
188
195
|
return;
|
|
189
196
|
}
|
|
190
|
-
manage.setMode(
|
|
197
|
+
manage.setMode(appBusy("Approving recipe…"));
|
|
191
198
|
void apps.approve(mode.entry).then(() => manage.finish({ kind: "ok", text: `Approved ${appName(mode.entry)}` }), error => manage.finish({ kind: "error", text: errorText(error) }));
|
|
192
199
|
}
|
|
193
200
|
}
|
|
@@ -195,7 +202,7 @@ export function handleAppKey(context, mode, input, key) {
|
|
|
195
202
|
export function AppPanel({ mode }) {
|
|
196
203
|
const { rows, columns } = useWindowSize();
|
|
197
204
|
if (mode.kind === "app-busy")
|
|
198
|
-
return _jsx(BorderPanel, { title: "App lifecycle", children: _jsx(
|
|
205
|
+
return _jsx(BorderPanel, { title: "App lifecycle", children: _jsx(Working, { label: mode.label, since: mode.startedAt }) });
|
|
199
206
|
if (mode.kind === "app-hosts") {
|
|
200
207
|
return (_jsxs(BorderPanel, { title: "App Host steps", children: [_jsx(Text, { children: "Checked = setup. Unchecked = teardown. Wiring is not tracked." }), _jsxs(Text, { children: ["Space/1-", mode.choices.length, " toggle \u00B7 j/k move \u00B7 Enter continue \u00B7 Esc cancel"] }), _jsx(HostChecklist, { choices: mode.choices, cursor: mode.cursor, selected: mode.selected })] }));
|
|
201
208
|
}
|
|
@@ -5,6 +5,7 @@ import { missingHosts, prepareHostAddition, prepareUninstall, runHostAddition, r
|
|
|
5
5
|
import { answerYesNo, applyChecklistOutcome, hostChecklistKey } from "./mode-keys.js";
|
|
6
6
|
import { skillOf, sourceIdOf } from "./installations.js";
|
|
7
7
|
import { prepareUnmanagedRemoval, runUnmanagedRemoval } from "./unmanaged-actions.js";
|
|
8
|
+
import { busy } from "./working-model.js";
|
|
8
9
|
export const BULK_KINDS = ["bulk-hosts", "confirm-bulk-uninstall", "confirm-bulk-hosts", "confirm-bulk-exposure"];
|
|
9
10
|
const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
10
11
|
function skip(target, label, reason) {
|
|
@@ -161,7 +162,7 @@ function cancel(context, text) {
|
|
|
161
162
|
}
|
|
162
163
|
/** Runs a prepare step in the busy mode; a plan with items goes to its confirmation, otherwise the reasons are reported. */
|
|
163
164
|
async function preparePlan(context, label, prepare, confirmation) {
|
|
164
|
-
context.setMode(
|
|
165
|
+
context.setMode(busy(label));
|
|
165
166
|
try {
|
|
166
167
|
const plan = await prepare();
|
|
167
168
|
if (!context.isMounted())
|
|
@@ -182,7 +183,7 @@ async function preparePlan(context, label, prepare, confirmation) {
|
|
|
182
183
|
}
|
|
183
184
|
/** `u` with marks: prepares every marked leaf, then asks once. */
|
|
184
185
|
export function startBulkUninstall(context, targets) {
|
|
185
|
-
return preparePlan(context, `Preparing uninstall of ${count(targets.length, "item")}
|
|
186
|
+
return preparePlan(context, `Preparing uninstall of ${count(targets.length, "item")}…`, () => prepareBulkUninstall(context, targets), (plan) => ({ kind: "confirm-bulk-uninstall", plan }));
|
|
186
187
|
}
|
|
187
188
|
/** `h` with marks: asks the hosts once, for the union of what the eligible items lack. */
|
|
188
189
|
export function startBulkHostAddition(context, targets) {
|
|
@@ -196,10 +197,10 @@ export function startBulkHostAddition(context, targets) {
|
|
|
196
197
|
context.setMode({ kind: "bulk-hosts", targets: ready, skipped, choices, cursor: 0, selected: [] });
|
|
197
198
|
}
|
|
198
199
|
function prepareHosts(context, mode) {
|
|
199
|
-
return preparePlan(context, `Preparing host change of ${count(mode.targets.length, "item")}
|
|
200
|
+
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 }));
|
|
200
201
|
}
|
|
201
202
|
async function apply(context, label, action) {
|
|
202
|
-
context.setMode(
|
|
203
|
+
context.setMode(busy(label));
|
|
203
204
|
let result;
|
|
204
205
|
try {
|
|
205
206
|
result = await action();
|
|
@@ -226,7 +227,7 @@ export function handleBulkKey(context, mode, input, key) {
|
|
|
226
227
|
}
|
|
227
228
|
case "confirm-bulk-uninstall":
|
|
228
229
|
answerYesNo(input, key, () => {
|
|
229
|
-
void apply(context, `Removing ${count(current.plan.items.length, "item")}
|
|
230
|
+
void apply(context, `Removing ${count(current.plan.items.length, "item")}…`, () => runBulkUninstall(current.plan, operations, env));
|
|
230
231
|
}, () => cancel(context, "Uninstall cancelled"));
|
|
231
232
|
return true;
|
|
232
233
|
case "confirm-bulk-hosts":
|
|
@@ -234,7 +235,7 @@ export function handleBulkKey(context, mode, input, key) {
|
|
|
234
235
|
return true;
|
|
235
236
|
case "confirm-bulk-exposure":
|
|
236
237
|
answerYesNo(input, key, () => {
|
|
237
|
-
void apply(context, `Changing hosts of ${count(current.plan.items.length, "item")}
|
|
238
|
+
void apply(context, `Changing hosts of ${count(current.plan.items.length, "item")}…`, () => runBulkHostAddition(current.plan, operations, env));
|
|
238
239
|
}, () => cancel(context, "Add hosts cancelled"));
|
|
239
240
|
return true;
|
|
240
241
|
default:
|
|
@@ -3,12 +3,13 @@ import { prepareInstall, runInstall } from "./catalog-installs.js";
|
|
|
3
3
|
import { skillKey } from "./catalog-tree.js";
|
|
4
4
|
import { errorText, runBatch } from "./batch.js";
|
|
5
5
|
import { answerYesNo, applyChecklistOutcome, hostChecklistKey, versionInputKey } from "./mode-keys.js";
|
|
6
|
+
import { busy } from "./working-model.js";
|
|
6
7
|
function cancel(context) {
|
|
7
8
|
context.setMessage({ kind: "ok", text: "Install cancelled" });
|
|
8
9
|
context.setAction(BROWSE);
|
|
9
10
|
}
|
|
10
11
|
async function startInstall(context, skills, choice) {
|
|
11
|
-
context.setAction(
|
|
12
|
+
context.setAction(busy(`Refreshing source and preparing install of ${describeSkills(skills)}…`));
|
|
12
13
|
const items = [];
|
|
13
14
|
for (const skill of skills) {
|
|
14
15
|
try {
|
|
@@ -30,7 +31,7 @@ async function startInstall(context, skills, choice) {
|
|
|
30
31
|
}
|
|
31
32
|
/** Applies the prepared items one by one; a failure is reported for that item and the rest still run. */
|
|
32
33
|
async function confirmInstall(context, items, exposure) {
|
|
33
|
-
context.setAction(
|
|
34
|
+
context.setAction(busy(`Installing ${describeSkills(items.map((item) => item.skill))}…`));
|
|
34
35
|
const outcomes = await runBatch(items, async ({ prepared, error }) => {
|
|
35
36
|
if (prepared === undefined)
|
|
36
37
|
throw new Error(error);
|
|
@@ -6,7 +6,7 @@ import { skillKey } from "./catalog-tree.js";
|
|
|
6
6
|
import { hasUnmanagedCopy } from "./catalog-installs.js";
|
|
7
7
|
import { describeRow, truncateDescription, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
8
8
|
import { isPackageMode, PackagePanel } from "./package-actions.js";
|
|
9
|
-
import { BorderPanel, DecisionPrompt, HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
9
|
+
import { BorderPanel, DecisionPrompt, HostChecklist, PreviewLines, Working } from "./panel-parts.js";
|
|
10
10
|
import { rowStyle } from "./theme.js";
|
|
11
11
|
import { theme } from "./theme.js";
|
|
12
12
|
/** Scope and counts, the filter line, and why the list may be empty. */
|
|
@@ -54,6 +54,6 @@ export function ActionPanel({ mode }) {
|
|
|
54
54
|
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?" })] }) }));
|
|
55
55
|
}
|
|
56
56
|
case "busy":
|
|
57
|
-
return _jsx(BorderPanel, { title: "Skill install", children: _jsx(
|
|
57
|
+
return _jsx(BorderPanel, { title: "Skill install", children: _jsx(Working, { label: mode.label, since: mode.startedAt }) });
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -11,13 +11,13 @@ import { AppPanel, handleAppKey, startAppAction } from "./app-actions.js";
|
|
|
11
11
|
import { ActionPanel, CatalogHeader, CatalogRowLine, CatalogSelectionHelp } from "./catalog-panel.js";
|
|
12
12
|
import { packageOwnedSkillNotice } from "./catalog-ownership.js";
|
|
13
13
|
import { packageOperationsOf } from "./environment.js";
|
|
14
|
-
import { handlePackageKey, isPackageMode, startPackageAction } from "./package-actions.js";
|
|
15
|
-
import { ListFooter } from "./panel-parts.js";
|
|
14
|
+
import { handlePackageKey, isPackageMode, PackagePanel, startPackageAction } from "./package-actions.js";
|
|
15
|
+
import { ListFooter, Working } from "./panel-parts.js";
|
|
16
16
|
import { useCatalogApps, useCatalogFilter, useCatalogSkills, useInstalledSkills } from "./catalog-state.js";
|
|
17
17
|
import { buildTree, leavesOf, openByDefault, skillKey } from "./catalog-tree.js";
|
|
18
18
|
import { theme } from "./theme.js";
|
|
19
19
|
import { useTreeNavigation } from "./tree-navigation.js";
|
|
20
|
-
import { useMarks, useMounted } from "./view-state.js";
|
|
20
|
+
import { useLoadingSince, useMarks, useMounted } from "./view-state.js";
|
|
21
21
|
import { computeWindow, useListHeight } from "./window.js";
|
|
22
22
|
import { useKeys } from "./keys.js";
|
|
23
23
|
const NO_ENVIRONMENT = {};
|
|
@@ -89,6 +89,21 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
89
89
|
? Math.max(1, ...state.activeBundles.map((owner) => wrappedLineCount(packageOwnedSkillNotice(owner), Math.max(columns, 1))))
|
|
90
90
|
: 1;
|
|
91
91
|
const height = useListHeight(listHeight, 6 + helpLines);
|
|
92
|
+
// A reload that ends an action continues that action's clock.
|
|
93
|
+
const runningSince = appMode?.kind === "app-busy" ? appMode.startedAt : action.kind === "busy" ? action.startedAt : undefined;
|
|
94
|
+
const loading = state.status === "loading" || appCatalog.status === "loading" || (state.status === "ready" && !installedReady);
|
|
95
|
+
// A Package action reloads after its mode has ended, so its clock is kept until that reload is done.
|
|
96
|
+
const [resumedSince, setResumedSince] = useState();
|
|
97
|
+
const wasLoading = useRef(false);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (loading)
|
|
100
|
+
wasLoading.current = true;
|
|
101
|
+
else if (wasLoading.current) {
|
|
102
|
+
wasLoading.current = false;
|
|
103
|
+
setResumedSince(undefined);
|
|
104
|
+
}
|
|
105
|
+
}, [loading]);
|
|
106
|
+
const loadingSince = useLoadingSince(loading, runningSince ?? resumedSince);
|
|
92
107
|
const flow = {
|
|
93
108
|
operations,
|
|
94
109
|
env,
|
|
@@ -130,12 +145,14 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
130
145
|
await reloadInstalled();
|
|
131
146
|
if (!mounted.current)
|
|
132
147
|
return;
|
|
148
|
+
const running = actionRef.current;
|
|
149
|
+
setResumedSince(running.kind === "busy" ? running.startedAt : undefined);
|
|
133
150
|
setReloadKey((value) => value + 1);
|
|
134
151
|
setMessage(result);
|
|
135
152
|
setAction(BROWSE);
|
|
136
153
|
},
|
|
137
154
|
};
|
|
138
|
-
const packageContext = { packages, manage: packageHost };
|
|
155
|
+
const packageContext = { packages, manage: packageHost, terminalRows: terminalRows || 24, terminalColumns: columns || 80 };
|
|
139
156
|
const browse = {
|
|
140
157
|
navigation,
|
|
141
158
|
height,
|
|
@@ -166,13 +183,16 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
166
183
|
}
|
|
167
184
|
});
|
|
168
185
|
const scope = all || sourceId === undefined ? "all sources" : sourceId;
|
|
169
|
-
if (
|
|
170
|
-
return _jsx(
|
|
186
|
+
if (loading)
|
|
187
|
+
return _jsx(Working, { label: "Loading catalog\u2026", since: loadingSince });
|
|
171
188
|
if (state.status === "error")
|
|
172
189
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
173
190
|
const window = computeWindow(rows.length, index, height);
|
|
174
191
|
const selectedCount = leavesOf(roots).filter((skill) => marks.marked.has(skillKey(skill))).length;
|
|
175
192
|
if (appMode !== undefined)
|
|
176
193
|
return _jsx(AppPanel, { mode: appMode });
|
|
194
|
+
// A paged declaration owns the view: rows of the tree above it would not be counted in its height.
|
|
195
|
+
if (isPackageMode(action))
|
|
196
|
+
return _jsx(PackagePanel, { mode: action });
|
|
177
197
|
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 })] }));
|
|
178
198
|
}
|
|
@@ -4,9 +4,9 @@ import { Text, useWindowSize } from "ink";
|
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { pickerEntries, readDirectoryListing, PROFILE_EXTENSION } from "./file-picker-model.js";
|
|
6
6
|
import { useKeys } from "./keys.js";
|
|
7
|
-
import { moveCursor } from "./mode-keys.js";
|
|
8
|
-
import { BorderPanel } from "./panel-parts.js";
|
|
9
|
-
import { clipText, wrapText } from "./text-wrap.js";
|
|
7
|
+
import { moveCursor, movePreviewCursor } from "./mode-keys.js";
|
|
8
|
+
import { BorderPanel, Working } from "./panel-parts.js";
|
|
9
|
+
import { clipText, fitLines, wrapText } from "./text-wrap.js";
|
|
10
10
|
import { theme } from "./theme.js";
|
|
11
11
|
import { useMirrored, useMounted } from "./view-state.js";
|
|
12
12
|
import { computeWindow, useListHeight } from "./window.js";
|
|
@@ -23,18 +23,22 @@ function settle(action) {
|
|
|
23
23
|
* request generation, so a slow listing never overwrites a newer directory or an unmount.
|
|
24
24
|
*/
|
|
25
25
|
export function FilePicker({ purpose, startDirectory, notice, onCancel, onChoose, listHeight, readDirectory = readDirectoryListing }) {
|
|
26
|
-
const [state, setState] = useState({ status: "loading", directory: startDirectory });
|
|
26
|
+
const [state, setState] = useState(() => ({ status: "loading", directory: startDirectory, startedAt: Date.now() }));
|
|
27
27
|
const [cursor, cursorRef, cursorTo] = useMirrored(0);
|
|
28
28
|
const [showAllFiles, showAllFilesRef, setShowAllFiles] = useMirrored(false);
|
|
29
|
-
const [showFullPath, setShowFullPath] =
|
|
29
|
+
const [showFullPath, showFullPathRef, setShowFullPath] = useMirrored(false);
|
|
30
|
+
const [pathCursor, pathCursorRef, pathCursorTo] = useMirrored(0);
|
|
31
|
+
// The page size of the full path view, which the key handler needs before the render computes it.
|
|
32
|
+
const pathPageRef = useRef(1);
|
|
30
33
|
const requestId = useRef(0);
|
|
34
|
+
const pathLineCount = useRef(1);
|
|
31
35
|
const mounted = useMounted();
|
|
32
36
|
const { columns } = useWindowSize();
|
|
33
37
|
const panelWidth = Math.max((columns || 80) - 4, 1);
|
|
34
38
|
const navigate = useCallback((directory) => {
|
|
35
39
|
const id = ++requestId.current;
|
|
36
40
|
cursorTo(0);
|
|
37
|
-
setState({ status: "loading", directory });
|
|
41
|
+
setState({ status: "loading", directory, startedAt: Date.now() });
|
|
38
42
|
void readDirectory(directory).then(listing => {
|
|
39
43
|
if (!mounted.current || requestId.current !== id)
|
|
40
44
|
return;
|
|
@@ -76,9 +80,14 @@ export function FilePicker({ purpose, startDirectory, notice, onCancel, onChoose
|
|
|
76
80
|
settle(onCancel);
|
|
77
81
|
return;
|
|
78
82
|
}
|
|
79
|
-
if (
|
|
83
|
+
if (showFullPathRef.current) {
|
|
80
84
|
if (input === "p")
|
|
81
85
|
setShowFullPath(false);
|
|
86
|
+
else {
|
|
87
|
+
const next = movePreviewCursor(pathCursorRef.current, pathLineCount.current, input, key, pathPageRef.current);
|
|
88
|
+
if (next !== undefined)
|
|
89
|
+
pathCursorTo(next);
|
|
90
|
+
}
|
|
82
91
|
return;
|
|
83
92
|
}
|
|
84
93
|
if (state.status !== "ready") {
|
|
@@ -98,6 +107,7 @@ export function FilePicker({ purpose, startDirectory, notice, onCancel, onChoose
|
|
|
98
107
|
return;
|
|
99
108
|
}
|
|
100
109
|
if (input === "p") {
|
|
110
|
+
pathCursorTo(0);
|
|
101
111
|
setShowFullPath(true);
|
|
102
112
|
return;
|
|
103
113
|
}
|
|
@@ -111,32 +121,54 @@ export function FilePicker({ purpose, startDirectory, notice, onCancel, onChoose
|
|
|
111
121
|
});
|
|
112
122
|
const directory = state.directory;
|
|
113
123
|
const wrappedDirectory = wrapText(`Directory: ${directory}`, panelWidth);
|
|
114
|
-
const
|
|
115
|
-
? wrappedDirectory
|
|
116
|
-
: [wrappedDirectory[0], ...wrapText(`… ${wrappedDirectory.length - 2} lines · press p for full path`, panelWidth), wrappedDirectory.at(-1)];
|
|
117
|
-
const ordinaryFooterLines = (purpose === "directory"
|
|
124
|
+
const ordinaryFooter = purpose === "directory"
|
|
118
125
|
? ["j/k move · Enter open/use · ←", "parent · p full path · Esc back"]
|
|
119
|
-
: ["j/k move · Enter open · ← parent", `f ${showAllFiles ? "json only" : "all files"} · p full path · Esc back`]
|
|
120
|
-
.flatMap(line => wrapText(line, panelWidth));
|
|
126
|
+
: ["j/k move · Enter open · ← parent", `f ${showAllFiles ? "json only" : "all files"} · p full path · Esc back`];
|
|
121
127
|
const noticeLines = notice === undefined ? [] : wrapText(notice, panelWidth);
|
|
122
|
-
const filterNoteLines = !showFullPath && purpose === "file" && state.status === "ready" && !showAllFiles
|
|
123
|
-
? wrapText(`Showing ${PROFILE_EXTENSION} only · f shows all files`, panelWidth)
|
|
124
|
-
: [];
|
|
125
128
|
const statusLineCount = !showFullPath && (state.status === "loading" || state.status === "error" || (state.status === "ready" && rows.length === 0)) ? 1 : 0;
|
|
126
|
-
const
|
|
127
|
-
|
|
129
|
+
const listRowsNeeded = rows.length > 0 ? 1 : 0;
|
|
130
|
+
// The roomy layout collapses a long path to three rows and wraps the key hints; the compact one keeps a
|
|
131
|
+
// folder list on screen when that roomy chrome alone would fill a short terminal.
|
|
132
|
+
const roomyDirectoryLines = wrappedDirectory.length <= 3
|
|
133
|
+
? wrappedDirectory
|
|
134
|
+
: [wrappedDirectory[0], ...wrapText(`… ${wrappedDirectory.length - 2} lines · press p for full path`, panelWidth), wrappedDirectory.at(-1)];
|
|
135
|
+
const roomyFooterLines = ordinaryFooter.flatMap(line => wrapText(line, panelWidth));
|
|
136
|
+
const filterNote = purpose === "file" && state.status === "ready" && !showAllFiles
|
|
137
|
+
? `Showing ${PROFILE_EXTENSION} only · f shows all files`
|
|
138
|
+
: undefined;
|
|
139
|
+
const roomyFilterNoteLines = filterNote === undefined ? [] : wrapText(filterNote, panelWidth);
|
|
140
|
+
// The compact legend keeps the keys a user would not guess (the filter, the full path, going up).
|
|
141
|
+
const compactFooter = purpose === "directory" ? "← parent · p full path · Esc back"
|
|
142
|
+
: `f ${showAllFiles ? "json only" : "all files"} · p full path · Esc back`;
|
|
143
|
+
const roomyReserved = 3 + roomyDirectoryLines.length + statusLineCount + roomyFilterNoteLines.length + noticeLines.length
|
|
144
|
+
+ roomyFooterLines.length + (rows.length > 0 && listHeight === undefined ? 1 : 0);
|
|
145
|
+
const roomyHeight = useListHeight(listHeight, roomyReserved, 0);
|
|
146
|
+
const compact = !showFullPath && listHeight === undefined && roomyHeight < listRowsNeeded;
|
|
147
|
+
const directoryLines = compact ? [clipText(`Directory: ${directory}`, panelWidth)] : roomyDirectoryLines;
|
|
148
|
+
const footerLines = showFullPath ? wrapText("j/k scroll · p hide path · Esc back", panelWidth)
|
|
149
|
+
: compact ? [clipText(compactFooter, panelWidth)] : roomyFooterLines;
|
|
150
|
+
const filterNoteLines = showFullPath ? [] : compact ? (filterNote === undefined ? [] : [clipText(filterNote, panelWidth)]) : roomyFilterNoteLines;
|
|
151
|
+
const visibleNoticeLines = showFullPath ? [] : compact ? fitLines(noticeLines, 2, panelWidth) : noticeLines;
|
|
128
152
|
const reservesWindowIndicator = !showFullPath && rows.length > 0 && listHeight === undefined ? 1 : 0;
|
|
129
153
|
const reserved = 3 + directoryLines.length + statusLineCount + filterNoteLines.length + visibleNoticeLines.length + footerLines.length + reservesWindowIndicator;
|
|
130
154
|
const available = useListHeight(listHeight, reserved, 1);
|
|
131
155
|
const height = rows.length > available && listHeight !== undefined ? Math.max(available - 1, 1) : available;
|
|
132
156
|
const window = computeWindow(rows.length, cursor, height);
|
|
133
157
|
const listedRows = showFullPath ? [] : rows.slice(window.start, window.end);
|
|
134
|
-
|
|
158
|
+
// The full path view pages the wrapped path, so a path longer than the terminal is still readable.
|
|
159
|
+
const pathAvailable = useListHeight(listHeight, 3 + footerLines.length, 1);
|
|
160
|
+
const pathHeight = wrappedDirectory.length > pathAvailable ? Math.max(pathAvailable - 1, 1) : pathAvailable;
|
|
161
|
+
const pathWindow = computeWindow(wrappedDirectory.length, pathCursor, pathHeight);
|
|
162
|
+
pathLineCount.current = wrappedDirectory.length;
|
|
163
|
+
pathPageRef.current = pathHeight;
|
|
164
|
+
const shownDirectoryLines = showFullPath ? wrappedDirectory.slice(pathWindow.start, pathWindow.end) : directoryLines;
|
|
165
|
+
return (_jsxs(BorderPanel, { title: purpose === "directory" ? "Choose export folder" : "Choose import file", children: [shownDirectoryLines.map((line, index) => _jsx(Text, { color: theme.value, children: line }, index)), !showFullPath && state.status === "loading" ? _jsx(Working, { label: "Loading folder\u2026", since: state.startedAt }) : null, !showFullPath && state.status === "error" ? _jsxs(Text, { color: theme.error, children: ["Error: ", clipText(state.message, panelWidth)] }) : null, !showFullPath && state.status === "ready" && rows.length === 0 ? _jsx(Text, { color: theme.muted, children: "Empty folder" }) : null, filterNoteLines.map((line, index) => _jsx(Text, { color: theme.muted, children: line }, `filter-${index}`)), listedRows.map((row, position) => {
|
|
135
166
|
const index = window.start + position;
|
|
136
167
|
const marker = index === cursor ? "> " : " ";
|
|
137
168
|
const label = row.kind === "use-folder" ? `Use this folder: ${directory}`
|
|
138
169
|
: row.kind === "parent" ? ".. (parent folder)"
|
|
139
170
|
: row.entry.kind === "directory" ? `${row.entry.name}/` : row.entry.name;
|
|
140
171
|
return _jsxs(Text, { children: [_jsx(Text, { color: theme.field, children: marker }), _jsx(Text, { color: theme.value, children: clipText(label, Math.max(panelWidth - 2, 1)) })] }, index);
|
|
141
|
-
}), visibleNoticeLines.map((line, index) => _jsx(Text, { color: theme.warning, children: line }, `notice-${index}`)), footerLines.map((line, index) => _jsx(Text, { color: theme.legend, children: line }, `footer-${index}`)),
|
|
172
|
+
}), visibleNoticeLines.map((line, index) => _jsx(Text, { color: theme.warning, children: line }, `notice-${index}`)), footerLines.map((line, index) => _jsx(Text, { color: theme.legend, children: line }, `footer-${index}`)), showFullPath ? (pathWindow.indicator ? _jsx(Text, { color: theme.muted, children: pathWindow.indicator }) : null)
|
|
173
|
+
: window.indicator ? _jsx(Text, { color: theme.muted, children: window.indicator }) : null] }));
|
|
142
174
|
}
|
|
@@ -3,7 +3,7 @@ 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
5
|
import { AppPanel, handleAppKey, isAppMode, startAppAction } from "./app-actions.js";
|
|
6
|
-
import { handlePackageKey, isPackageMode } from "./package-actions.js";
|
|
6
|
+
import { handlePackageKey, isPackageMode, PackagePanel } from "./package-actions.js";
|
|
7
7
|
import { packageOperationsOf } from "./environment.js";
|
|
8
8
|
import { loadInstallations } from "./installations.js";
|
|
9
9
|
import { BROWSE } from "./installations-mode.js";
|
|
@@ -14,11 +14,12 @@ import { handleManageKey } from "./manage-actions.js";
|
|
|
14
14
|
import { ModePanel } from "./installations-panel.js";
|
|
15
15
|
import { TreeRowLine } from "./installations-rows.js";
|
|
16
16
|
import { buildTree, leavesOf, markersOf } from "./installations-tree.js";
|
|
17
|
-
import { ListFooter } from "./panel-parts.js";
|
|
17
|
+
import { ListFooter, Working } from "./panel-parts.js";
|
|
18
18
|
import { useTreeNavigation } from "./tree-navigation.js";
|
|
19
19
|
import { defaultExpanded } from "./tree.js";
|
|
20
20
|
import { theme } from "./theme.js";
|
|
21
|
-
import {
|
|
21
|
+
import { busy } from "./working-model.js";
|
|
22
|
+
import { useLoadingSince, useMarks, useMounted } from "./view-state.js";
|
|
22
23
|
import { computeWindow, useListHeight } from "./window.js";
|
|
23
24
|
import { useKeys } from "./keys.js";
|
|
24
25
|
const NO_ENVIRONMENT = {};
|
|
@@ -41,9 +42,16 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
41
42
|
onCapturingChange?.(next.kind !== "browse");
|
|
42
43
|
setModeState(next);
|
|
43
44
|
};
|
|
45
|
+
// Reloads and App checks can overlap, so counts decide when each indicator ends.
|
|
46
|
+
const [refreshingSince, setRefreshingSince] = useState();
|
|
47
|
+
const reloads = useRef(0);
|
|
48
|
+
const [checkingSince, setCheckingSince] = useState();
|
|
49
|
+
const checks = useRef(0);
|
|
44
50
|
const loadGeneration = useRef(0);
|
|
45
51
|
const reload = useCallback(async () => {
|
|
46
52
|
const generation = ++loadGeneration.current;
|
|
53
|
+
if (reloads.current++ === 0 && mounted.current)
|
|
54
|
+
setRefreshingSince(Date.now());
|
|
47
55
|
try {
|
|
48
56
|
const data = await loadInstallations(operations, env, apps, packages);
|
|
49
57
|
if (mounted.current && generation === loadGeneration.current)
|
|
@@ -53,6 +61,10 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
53
61
|
if (mounted.current && generation === loadGeneration.current)
|
|
54
62
|
setState({ status: "error", message: errorText(error) });
|
|
55
63
|
}
|
|
64
|
+
finally {
|
|
65
|
+
if (--reloads.current === 0 && mounted.current)
|
|
66
|
+
setRefreshingSince(undefined);
|
|
67
|
+
}
|
|
56
68
|
}, [operations, env, apps, packages, mounted]);
|
|
57
69
|
useEffect(() => {
|
|
58
70
|
void reload();
|
|
@@ -63,15 +75,20 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
63
75
|
const { rows, index } = navigation;
|
|
64
76
|
const checked = useRef(new Set());
|
|
65
77
|
const highlighted = rows[index]?.node.data;
|
|
78
|
+
// The entry is kept when the tree is rebuilt with a check's result, and replaced only by a reload,
|
|
79
|
+
// so the effect below does not re-run for the node objects `buildTree` makes anew.
|
|
80
|
+
const highlightedEntry = highlighted?.kind === "app" ? highlighted.row.entry : undefined;
|
|
66
81
|
useEffect(() => {
|
|
67
|
-
if (
|
|
82
|
+
if (highlightedEntry === undefined)
|
|
68
83
|
return;
|
|
69
|
-
const
|
|
84
|
+
const entry = highlightedEntry;
|
|
70
85
|
const identity = `${entry.file}:${entry.hash}`;
|
|
71
86
|
if (checked.current.has(identity))
|
|
72
87
|
return;
|
|
73
88
|
checked.current.add(identity);
|
|
74
89
|
const generation = loadGeneration.current;
|
|
90
|
+
if (checks.current++ === 0)
|
|
91
|
+
setCheckingSince(Date.now());
|
|
75
92
|
void apps.approvalStatus(entry).then(approval => {
|
|
76
93
|
if (approval.status !== "approved")
|
|
77
94
|
return undefined;
|
|
@@ -79,6 +96,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
79
96
|
}).then(inspection => {
|
|
80
97
|
if (!inspection || !mounted.current || generation !== loadGeneration.current)
|
|
81
98
|
return;
|
|
99
|
+
// An unknown result is asked again only when focus returns to the App, never by the rebuilt tree.
|
|
82
100
|
if (inspection.status === "unknown")
|
|
83
101
|
checked.current.delete(identity);
|
|
84
102
|
setState(previous => previous.status !== "ready" ? previous : {
|
|
@@ -94,14 +112,25 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
94
112
|
return;
|
|
95
113
|
checked.current.delete(identity);
|
|
96
114
|
setMessage({ kind: "error", text: errorText(error) });
|
|
115
|
+
}).finally(() => {
|
|
116
|
+
if (--checks.current === 0 && mounted.current)
|
|
117
|
+
setCheckingSince(undefined);
|
|
97
118
|
});
|
|
98
|
-
}, [
|
|
119
|
+
}, [highlightedEntry, apps, mounted]);
|
|
99
120
|
const installed = useMemo(() => ({
|
|
100
121
|
global: (data?.global ?? []).map((row) => row.selection),
|
|
101
122
|
project: (data?.project ?? []).map((row) => row.selection),
|
|
102
123
|
sources: data?.sources ?? [],
|
|
103
124
|
unmanagedNames: new Set(),
|
|
104
125
|
}), [data]);
|
|
126
|
+
const loadingSince = useLoadingSince(state.status === "loading");
|
|
127
|
+
/** Reloads after an action: the busy panel, which keeps the action's clock, says what is now running. */
|
|
128
|
+
const refresh = async () => {
|
|
129
|
+
const current = modeRef.current;
|
|
130
|
+
if ("startedAt" in current && mounted.current)
|
|
131
|
+
setMode(busy("Refreshing installations…", current));
|
|
132
|
+
await reload();
|
|
133
|
+
};
|
|
105
134
|
const manage = {
|
|
106
135
|
operations,
|
|
107
136
|
env,
|
|
@@ -113,7 +142,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
113
142
|
unmark: (ids) => marks.setMarked(new Set([...marks.markedRef.current].filter((id) => !ids.includes(id)))),
|
|
114
143
|
finish: async (result) => {
|
|
115
144
|
checked.current.clear();
|
|
116
|
-
await
|
|
145
|
+
await refresh();
|
|
117
146
|
if (!mounted.current)
|
|
118
147
|
return;
|
|
119
148
|
setMessage(result);
|
|
@@ -127,7 +156,7 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
127
156
|
browse: manage.browse,
|
|
128
157
|
setMessage,
|
|
129
158
|
isMounted: manage.isMounted,
|
|
130
|
-
reload,
|
|
159
|
+
reload: refresh,
|
|
131
160
|
};
|
|
132
161
|
const browse = {
|
|
133
162
|
navigation,
|
|
@@ -145,18 +174,21 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
145
174
|
if (current.kind === "browse")
|
|
146
175
|
handleBrowseKey(browse, input, key);
|
|
147
176
|
else if (isPackageMode(current))
|
|
148
|
-
handlePackageKey({ packages, manage }, current, input, key);
|
|
177
|
+
handlePackageKey({ packages, manage, terminalRows: terminalRows || 24, terminalColumns: terminalColumns || 80 }, current, input, key);
|
|
149
178
|
else if (isAppMode(current))
|
|
150
179
|
handleAppKey({ apps, manage, terminalRows: terminalRows || 24, terminalColumns: terminalColumns || 80 }, current, input, key);
|
|
151
180
|
else if (!handleManageKey(manage, current, input, key))
|
|
152
181
|
handleAdoptionKey(adoption, current, input, key);
|
|
153
182
|
});
|
|
154
183
|
if (state.status === "loading")
|
|
155
|
-
return _jsx(
|
|
184
|
+
return _jsx(Working, { label: "Loading installations\u2026", since: loadingSince });
|
|
156
185
|
if (state.status === "error")
|
|
157
186
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
158
187
|
if (isAppMode(mode))
|
|
159
188
|
return _jsx(AppPanel, { mode: mode });
|
|
189
|
+
// A paged declaration owns the view: rows of the tree above it would not be counted in its height.
|
|
190
|
+
if (isPackageMode(mode))
|
|
191
|
+
return _jsx(PackagePanel, { mode: mode });
|
|
160
192
|
const ready = state.data;
|
|
161
193
|
const window = computeWindow(rows.length, index, height);
|
|
162
194
|
const listed = rows.slice(window.start, window.end);
|
|
@@ -165,5 +197,6 @@ export function InstallationsView({ operations, environment, onCapturingChange,
|
|
|
165
197
|
const markedApps = markedLeaves.filter(node => node.data.kind === "app").length;
|
|
166
198
|
// The legend explains the markers, so it shows only while a rendered row carries one.
|
|
167
199
|
const legendNeeded = listed.some((row) => row.node.data.kind === "installation" && markersOf(row.node.data.row) !== "");
|
|
168
|
-
return (_jsxs(Box, { flexDirection: "column", children: [ready.global === undefined ? _jsx(Text, { children: "User-global installations are not supported by the configured operations" }) : null, ready.projectError === undefined ? null : _jsxs(Text, { color: theme.error, children: ["Error: ", ready.projectError] }), listed.map((row, offset) => (_jsx(TreeRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id) }, row.node.id))), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), markedApps > 0 ? _jsxs(Text, { color: theme.muted, children: [markedApps, " Apps marked"] }) : null, legendNeeded ? _jsx(Text, { color: theme.muted, children: LEGEND }) : null,
|
|
200
|
+
return (_jsxs(Box, { flexDirection: "column", children: [refreshingSince !== undefined && mode.kind === "browse" ? _jsx(Working, { label: "Refreshing installations\u2026", since: refreshingSince }) : null, ready.global === undefined ? _jsx(Text, { children: "User-global installations are not supported by the configured operations" }) : null, ready.projectError === undefined ? null : _jsxs(Text, { color: theme.error, children: ["Error: ", ready.projectError] }), listed.map((row, offset) => (_jsx(TreeRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id) }, row.node.id))), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), markedApps > 0 ? _jsxs(Text, { color: theme.muted, children: [markedApps, " Apps marked"] }) : null, legendNeeded ? _jsx(Text, { color: theme.muted, children: LEGEND }) : null, checkingSince !== undefined && refreshingSince === undefined && mode.kind === "browse"
|
|
201
|
+
? _jsx(Working, { label: "Checking App status\u2026", since: checkingSince }) : null, _jsx(ModePanel, { mode: mode })] }));
|
|
169
202
|
}
|