pi-extmgr 0.1.28 → 0.2.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/README.md +18 -8
- package/package.json +10 -2
- package/src/commands/auto-update.ts +1 -1
- package/src/commands/history.ts +2 -31
- package/src/constants.ts +0 -8
- package/src/extensions/discovery.ts +121 -39
- package/src/packages/discovery.ts +34 -0
- package/src/packages/extensions.ts +55 -98
- package/src/packages/install.ts +85 -56
- package/src/packages/management.ts +25 -38
- package/src/types/index.ts +4 -2
- package/src/ui/footer.ts +49 -29
- package/src/ui/help.ts +15 -11
- package/src/ui/remote.ts +704 -112
- package/src/ui/unified.ts +922 -311
- package/src/utils/auto-update.ts +34 -29
- package/src/utils/cache.ts +205 -34
- package/src/utils/duration.ts +132 -0
- package/src/utils/format.ts +0 -30
- package/src/utils/fs.ts +8 -4
- package/src/utils/history.ts +43 -7
- package/src/utils/mode.ts +1 -1
- package/src/utils/notify.ts +0 -14
- package/src/utils/package-source.ts +2 -5
- package/src/utils/path-identity.ts +7 -0
- package/src/utils/relative-path-selection.ts +100 -0
- package/src/utils/settings.ts +4 -63
- package/src/utils/retry.ts +0 -49
package/src/ui/unified.ts
CHANGED
|
@@ -2,21 +2,26 @@
|
|
|
2
2
|
* Unified extension manager UI
|
|
3
3
|
* Displays local extensions and installed packages in one view
|
|
4
4
|
*/
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { relative } from "node:path";
|
|
5
7
|
import {
|
|
6
8
|
DynamicBorder,
|
|
7
9
|
type ExtensionAPI,
|
|
8
10
|
type ExtensionCommandContext,
|
|
9
|
-
getSettingsListTheme,
|
|
10
11
|
type Theme,
|
|
11
12
|
} from "@mariozechner/pi-coding-agent";
|
|
12
13
|
import {
|
|
13
14
|
Container,
|
|
15
|
+
type Focusable,
|
|
16
|
+
fuzzyMatch,
|
|
17
|
+
getKeybindings,
|
|
18
|
+
Input,
|
|
14
19
|
Key,
|
|
15
20
|
matchesKey,
|
|
16
|
-
type SettingItem,
|
|
17
|
-
SettingsList,
|
|
18
21
|
Spacer,
|
|
19
22
|
Text,
|
|
23
|
+
truncateToWidth,
|
|
24
|
+
wrapTextWithAnsi,
|
|
20
25
|
} from "@mariozechner/pi-tui";
|
|
21
26
|
import { UI } from "../constants.js";
|
|
22
27
|
import {
|
|
@@ -40,26 +45,20 @@ import {
|
|
|
40
45
|
} from "../types/index.js";
|
|
41
46
|
import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
|
|
42
47
|
import { parseChoiceByLabel } from "../utils/command.js";
|
|
43
|
-
import {
|
|
48
|
+
import { formatBytes, formatEntry as formatExtEntry } from "../utils/format.js";
|
|
44
49
|
import { logExtensionDelete, logExtensionToggle } from "../utils/history.js";
|
|
45
50
|
import { hasCustomUI, runCustomUI } from "../utils/mode.js";
|
|
46
51
|
import { notify } from "../utils/notify.js";
|
|
52
|
+
import { normalizePathIdentity } from "../utils/path-identity.js";
|
|
47
53
|
import { getPackageSourceKind, normalizePackageIdentity } from "../utils/package-source.js";
|
|
48
|
-
import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
|
|
49
54
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
50
|
-
import { confirmReload } from "../utils/ui-helpers.js";
|
|
55
|
+
import { confirmReload, formatListOutput } from "../utils/ui-helpers.js";
|
|
51
56
|
import { runTaskWithLoader } from "./async-task.js";
|
|
52
57
|
import { buildFooterShortcuts, buildFooterState, getPendingToggleChangeCount } from "./footer.js";
|
|
53
58
|
import { showHelp } from "./help.js";
|
|
54
59
|
import { configurePackageExtensions } from "./package-config.js";
|
|
55
60
|
import { showRemote } from "./remote.js";
|
|
56
|
-
import {
|
|
57
|
-
formatSize,
|
|
58
|
-
getChangeMarker,
|
|
59
|
-
getPackageIcon,
|
|
60
|
-
getScopeIcon,
|
|
61
|
-
getStatusIcon,
|
|
62
|
-
} from "./theme.js";
|
|
61
|
+
import { getChangeMarker, getPackageIcon, getScopeIcon, getStatusIcon } from "./theme.js";
|
|
63
62
|
|
|
64
63
|
async function showInteractiveFallback(
|
|
65
64
|
ctx: ExtensionCommandContext,
|
|
@@ -156,206 +155,108 @@ async function showInteractiveOnce(
|
|
|
156
155
|
// Staged changes tracking for local extensions.
|
|
157
156
|
const staged = new Map<string, State>();
|
|
158
157
|
const byId = new Map(items.map((item) => [item.id, item]));
|
|
158
|
+
let managerState: UnifiedManagerViewState | undefined;
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"The unified extensions manager",
|
|
163
|
-
() =>
|
|
164
|
-
ctx.ui.custom<UnifiedAction>((tui, theme, _keybindings, done) => {
|
|
165
|
-
const container = new Container();
|
|
166
|
-
|
|
167
|
-
const titleText = new Text("", 2, 0);
|
|
168
|
-
const subtitleText = new Text("", 2, 0);
|
|
169
|
-
const quickText = new Text("", 2, 0);
|
|
170
|
-
const footerState = buildFooterState(items);
|
|
171
|
-
const footerText = new Text("", 2, 0);
|
|
172
|
-
|
|
173
|
-
// Header
|
|
174
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
175
|
-
container.addChild(titleText);
|
|
176
|
-
container.addChild(subtitleText);
|
|
177
|
-
container.addChild(quickText);
|
|
178
|
-
container.addChild(new Spacer(1));
|
|
179
|
-
|
|
180
|
-
// Build settings items
|
|
181
|
-
const settingsItems = buildSettingsItems(items, staged, theme);
|
|
182
|
-
const syncThemedContent = (): void => {
|
|
183
|
-
titleText.setText(theme.fg("accent", theme.bold("Extensions Manager")));
|
|
184
|
-
subtitleText.setText(
|
|
185
|
-
theme.fg(
|
|
186
|
-
"muted",
|
|
187
|
-
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle local • Enter/A actions • c configure pkg extensions • u update pkg • x remove selected`
|
|
188
|
-
)
|
|
189
|
-
);
|
|
190
|
-
quickText.setText(
|
|
191
|
-
theme.fg(
|
|
192
|
-
"dim",
|
|
193
|
-
"Quick: i Install | f Search | U Update all | t Auto-update | p Palette"
|
|
194
|
-
)
|
|
195
|
-
);
|
|
196
|
-
footerText.setText(theme.fg("dim", buildFooterShortcuts(footerState)));
|
|
197
|
-
|
|
198
|
-
for (const settingsItem of settingsItems) {
|
|
199
|
-
const item = byId.get(settingsItem.id);
|
|
200
|
-
if (!item) continue;
|
|
201
|
-
|
|
202
|
-
if (item.type === "local") {
|
|
203
|
-
const currentState = staged.get(item.id) ?? item.state;
|
|
204
|
-
const changed = currentState !== item.originalState;
|
|
205
|
-
settingsItem.label = formatUnifiedItemLabel(item, currentState, theme, changed);
|
|
206
|
-
} else {
|
|
207
|
-
settingsItem.label = formatUnifiedItemLabel(item, "enabled", theme, false);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
syncThemedContent();
|
|
212
|
-
|
|
213
|
-
const settingsList = new SettingsList(
|
|
214
|
-
settingsItems,
|
|
215
|
-
Math.min(items.length + 2, UI.maxListHeight),
|
|
216
|
-
getSettingsListTheme(),
|
|
217
|
-
(id: string, newValue: string) => {
|
|
218
|
-
const item = byId.get(id);
|
|
219
|
-
if (!item || item.type !== "local") return;
|
|
220
|
-
|
|
221
|
-
const state = newValue as State;
|
|
222
|
-
if (state === item.originalState) {
|
|
223
|
-
staged.delete(id);
|
|
224
|
-
} else {
|
|
225
|
-
staged.set(id, state);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const settingsItem = settingsItems.find((x) => x.id === id);
|
|
229
|
-
if (settingsItem) {
|
|
230
|
-
const changed = state !== item.originalState;
|
|
231
|
-
settingsItem.label = formatUnifiedItemLabel(item, state, theme, changed);
|
|
232
|
-
}
|
|
233
|
-
tui.requestRender();
|
|
234
|
-
},
|
|
235
|
-
() => done({ type: "cancel" })
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
container.addChild(settingsList);
|
|
239
|
-
container.addChild(new Spacer(1));
|
|
240
|
-
|
|
241
|
-
// Footer with keyboard shortcuts
|
|
242
|
-
container.addChild(footerText);
|
|
243
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
render(width: number) {
|
|
247
|
-
return container.render(width);
|
|
248
|
-
},
|
|
249
|
-
invalidate() {
|
|
250
|
-
container.invalidate();
|
|
251
|
-
syncThemedContent();
|
|
252
|
-
},
|
|
253
|
-
handleInput(data: string) {
|
|
254
|
-
const selIdx = getSettingsListSelectedIndex(settingsList) ?? 0;
|
|
255
|
-
const selectedId = settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
|
|
256
|
-
const selectedItem = selectedId ? byId.get(selectedId) : undefined;
|
|
257
|
-
|
|
258
|
-
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
259
|
-
done({ type: "apply" });
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Enter on a package opens its action menu (fewer clicks)
|
|
264
|
-
if (
|
|
265
|
-
(data === "\r" || data === "\n") &&
|
|
266
|
-
selectedId &&
|
|
267
|
-
selectedItem?.type === "package"
|
|
268
|
-
) {
|
|
269
|
-
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (data === "a" || data === "A") {
|
|
274
|
-
if (selectedId) {
|
|
275
|
-
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Quick actions (global)
|
|
281
|
-
if (data === "i") {
|
|
282
|
-
done({ type: "quick", action: "install" });
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (data === "f") {
|
|
286
|
-
done({ type: "quick", action: "search" });
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (data === "U") {
|
|
290
|
-
done({ type: "quick", action: "update-all" });
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
if (data === "t" || data === "T") {
|
|
294
|
-
done({ type: "quick", action: "auto-update" });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Fast actions on selected row
|
|
299
|
-
if (selectedId && selectedItem?.type === "package") {
|
|
300
|
-
if (data === "u") {
|
|
301
|
-
done({ type: "action", itemId: selectedId, action: "update" });
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (data === "x" || data === "X") {
|
|
305
|
-
done({ type: "action", itemId: selectedId, action: "remove" });
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (data === "v" || data === "V") {
|
|
309
|
-
done({ type: "action", itemId: selectedId, action: "details" });
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
if (data === "c" || data === "C") {
|
|
313
|
-
done({ type: "action", itemId: selectedId, action: "configure" });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
160
|
+
while (true) {
|
|
161
|
+
let nextManagerState = managerState;
|
|
317
162
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
163
|
+
const result = await runCustomUI(
|
|
164
|
+
ctx,
|
|
165
|
+
"The unified extensions manager",
|
|
166
|
+
() =>
|
|
167
|
+
ctx.ui.custom<UnifiedAction>((tui, theme, _keybindings, done) => {
|
|
168
|
+
const container = new Container();
|
|
169
|
+
|
|
170
|
+
const titleText = new Text("", 2, 0);
|
|
171
|
+
const statsText = new Text("", 2, 0);
|
|
172
|
+
const footerText = new Text("", 2, 0);
|
|
173
|
+
let browser!: UnifiedManagerBrowser;
|
|
174
|
+
const complete = (action: UnifiedAction): void => {
|
|
175
|
+
nextManagerState = browser.getViewState();
|
|
176
|
+
done(action);
|
|
177
|
+
};
|
|
178
|
+
browser = new UnifiedManagerBrowser(
|
|
179
|
+
items,
|
|
180
|
+
staged,
|
|
181
|
+
theme,
|
|
182
|
+
ctx.cwd,
|
|
183
|
+
Math.max(4, Math.min(UI.maxListHeight, tui.terminal.rows - 12)),
|
|
184
|
+
complete,
|
|
185
|
+
managerState
|
|
186
|
+
);
|
|
187
|
+
let lastWidth = tui.terminal.columns;
|
|
188
|
+
|
|
189
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
190
|
+
container.addChild(titleText);
|
|
191
|
+
container.addChild(statsText);
|
|
192
|
+
container.addChild(new Spacer(1));
|
|
193
|
+
container.addChild(browser);
|
|
194
|
+
container.addChild(new Spacer(1));
|
|
195
|
+
container.addChild(footerText);
|
|
196
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
197
|
+
|
|
198
|
+
const syncThemedContent = (width = lastWidth): void => {
|
|
199
|
+
lastWidth = width;
|
|
200
|
+
titleText.setText(theme.fg("accent", theme.bold("Extensions Manager")));
|
|
201
|
+
statsText.setText(
|
|
202
|
+
buildManagerSummary(items, staged, byId, theme, {
|
|
203
|
+
visibleItems: browser.getVisibleItems(),
|
|
204
|
+
filter: browser.getFilter(),
|
|
205
|
+
searchQuery: browser.getSearchQuery(),
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
footerText.setText(
|
|
209
|
+
theme.fg(
|
|
210
|
+
"dim",
|
|
211
|
+
buildFooterShortcuts(buildFooterState(staged, byId, browser.getSelectedItem()))
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
syncThemedContent();
|
|
217
|
+
|
|
218
|
+
let focused = false;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
get focused() {
|
|
222
|
+
return focused;
|
|
223
|
+
},
|
|
224
|
+
set focused(value: boolean) {
|
|
225
|
+
focused = value;
|
|
226
|
+
browser.focused = value;
|
|
227
|
+
},
|
|
228
|
+
render(width: number) {
|
|
229
|
+
syncThemedContent(width);
|
|
230
|
+
return container.render(width);
|
|
231
|
+
},
|
|
232
|
+
invalidate() {
|
|
233
|
+
container.invalidate();
|
|
234
|
+
browser.invalidate();
|
|
235
|
+
syncThemedContent(lastWidth);
|
|
236
|
+
},
|
|
237
|
+
handleInput(data: string) {
|
|
238
|
+
if (browser.handleManagerInput(data)) {
|
|
239
|
+
tui.requestRender();
|
|
322
240
|
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
if (data === "?" || data === "h" || data === "H") {
|
|
330
|
-
done({ type: "help" });
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (data === "m" || data === "M" || data === "p" || data === "P") {
|
|
334
|
-
done({ type: "menu" });
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
settingsList.handleInput?.(data);
|
|
338
|
-
tui.requestRender();
|
|
339
|
-
},
|
|
340
|
-
};
|
|
341
|
-
}),
|
|
342
|
-
"Showing read-only local and installed package lists instead."
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
if (!result) {
|
|
346
|
-
await showInteractiveFallback(ctx, pi);
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}),
|
|
244
|
+
"Showing read-only local and installed package lists instead."
|
|
245
|
+
);
|
|
349
246
|
|
|
350
|
-
|
|
351
|
-
|
|
247
|
+
if (!result) {
|
|
248
|
+
await showInteractiveFallback(ctx, pi);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
352
251
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
252
|
+
const outcome = await handleUnifiedAction(result, items, staged, byId, ctx, pi);
|
|
253
|
+
if (outcome === "resume") {
|
|
254
|
+
managerState = nextManagerState;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
357
257
|
|
|
358
|
-
|
|
258
|
+
return outcome;
|
|
259
|
+
}
|
|
359
260
|
}
|
|
360
261
|
|
|
361
262
|
export function buildUnifiedItems(
|
|
@@ -368,7 +269,8 @@ export function buildUnifiedItems(
|
|
|
368
269
|
|
|
369
270
|
// Add local extensions
|
|
370
271
|
for (const entry of localEntries) {
|
|
371
|
-
|
|
272
|
+
const currentPath = entry.state === "disabled" ? entry.disabledPath : entry.activePath;
|
|
273
|
+
localPaths.add(normalizePathIdentity(currentPath));
|
|
372
274
|
items.push({
|
|
373
275
|
type: "local",
|
|
374
276
|
id: entry.id,
|
|
@@ -383,10 +285,8 @@ export function buildUnifiedItems(
|
|
|
383
285
|
}
|
|
384
286
|
|
|
385
287
|
for (const pkg of installedPackages) {
|
|
386
|
-
const pkgSourceNormalized =
|
|
387
|
-
const pkgResolvedNormalized = pkg.resolvedPath
|
|
388
|
-
? normalizePathForDuplicateCheck(pkg.resolvedPath)
|
|
389
|
-
: "";
|
|
288
|
+
const pkgSourceNormalized = normalizePathIdentity(pkg.source);
|
|
289
|
+
const pkgResolvedNormalized = pkg.resolvedPath ? normalizePathIdentity(pkg.resolvedPath) : "";
|
|
390
290
|
|
|
391
291
|
let isDuplicate = false;
|
|
392
292
|
for (const localPath of localPaths) {
|
|
@@ -407,6 +307,7 @@ export function buildUnifiedItems(
|
|
|
407
307
|
displayName: pkg.name,
|
|
408
308
|
scope: pkg.scope,
|
|
409
309
|
source: pkg.source,
|
|
310
|
+
resolvedPath: pkg.resolvedPath,
|
|
410
311
|
version: pkg.version,
|
|
411
312
|
description: pkg.description,
|
|
412
313
|
size: pkg.size,
|
|
@@ -429,45 +330,87 @@ export function buildUnifiedItems(
|
|
|
429
330
|
return items;
|
|
430
331
|
}
|
|
431
332
|
|
|
432
|
-
function
|
|
333
|
+
function buildManagerSummary(
|
|
433
334
|
items: UnifiedItem[],
|
|
434
335
|
staged: Map<string, State>,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
336
|
+
byId: Map<string, UnifiedItem>,
|
|
337
|
+
theme: Theme,
|
|
338
|
+
options?: {
|
|
339
|
+
visibleItems?: readonly UnifiedItem[];
|
|
340
|
+
filter?: UnifiedFilter;
|
|
341
|
+
searchQuery?: string;
|
|
342
|
+
}
|
|
343
|
+
): string {
|
|
344
|
+
const summaryItems = options?.visibleItems ?? items;
|
|
345
|
+
const filtered =
|
|
346
|
+
Boolean(options?.searchQuery) ||
|
|
347
|
+
options?.filter === "local" ||
|
|
348
|
+
options?.filter === "packages" ||
|
|
349
|
+
options?.filter === "updates" ||
|
|
350
|
+
options?.filter === "disabled";
|
|
351
|
+
const localCount = summaryItems.filter((item) => item.type === "local").length;
|
|
352
|
+
const packageCount = summaryItems.length - localCount;
|
|
353
|
+
const updateCount = summaryItems.filter(
|
|
354
|
+
(item) => item.type === "package" && item.updateAvailable
|
|
355
|
+
).length;
|
|
356
|
+
const pendingCount = getPendingToggleChangeCount(staged, byId);
|
|
357
|
+
const parts = [
|
|
358
|
+
filtered
|
|
359
|
+
? theme.fg("accent", `showing ${summaryItems.length} of ${items.length}`)
|
|
360
|
+
: theme.fg("muted", `${items.length} item${items.length === 1 ? "" : "s"}`),
|
|
361
|
+
theme.fg("muted", `${localCount} local`),
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
if (packageCount > 0) {
|
|
365
|
+
parts.push(theme.fg("muted", `${packageCount} package${packageCount === 1 ? "" : "s"}`));
|
|
366
|
+
}
|
|
448
367
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
};
|
|
455
|
-
}
|
|
368
|
+
if (updateCount > 0) {
|
|
369
|
+
parts.push(theme.fg("warning", `${updateCount} update${updateCount === 1 ? "" : "s"}`));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (pendingCount > 0) {
|
|
373
|
+
parts.push(theme.fg("warning", `${pendingCount} unsaved`));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return parts.join(" • ");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
type UnifiedFilter = "all" | "local" | "packages" | "updates" | "disabled";
|
|
380
|
+
|
|
381
|
+
interface UnifiedManagerViewState {
|
|
382
|
+
filter: UnifiedFilter;
|
|
383
|
+
searchQuery: string;
|
|
384
|
+
selectedItemId?: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const UNIFIED_FILTER_OPTIONS: Array<{ id: UnifiedFilter; key: string; label: string }> = [
|
|
388
|
+
{ id: "all", key: "1", label: "All" },
|
|
389
|
+
{ id: "local", key: "2", label: "Local" },
|
|
390
|
+
{ id: "packages", key: "3", label: "Packages" },
|
|
391
|
+
{ id: "updates", key: "4", label: "Updates" },
|
|
392
|
+
{ id: "disabled", key: "5", label: "Disabled" },
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
function getCurrentUnifiedItemState(
|
|
396
|
+
item: UnifiedItem,
|
|
397
|
+
staged: Map<string, State>
|
|
398
|
+
): State | undefined {
|
|
399
|
+
return item.type === "local" ? (staged.get(item.id) ?? item.state) : undefined;
|
|
456
400
|
}
|
|
457
401
|
|
|
458
402
|
function formatUnifiedItemLabel(
|
|
459
403
|
item: UnifiedItem,
|
|
460
|
-
state: State,
|
|
404
|
+
state: State | undefined,
|
|
461
405
|
theme: Theme,
|
|
462
406
|
changed = false
|
|
463
407
|
): string {
|
|
464
408
|
if (item.type === "local") {
|
|
465
|
-
const statusIcon = getStatusIcon(theme, state);
|
|
409
|
+
const statusIcon = getStatusIcon(theme, state ?? item.state);
|
|
466
410
|
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
467
411
|
const changeMarker = getChangeMarker(theme, changed);
|
|
468
412
|
const name = theme.bold(item.displayName);
|
|
469
|
-
|
|
470
|
-
return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
413
|
+
return `${statusIcon} [${scopeIcon}] ${name}${changeMarker}`;
|
|
471
414
|
}
|
|
472
415
|
|
|
473
416
|
const sourceKind = getPackageSourceKind(item.source);
|
|
@@ -478,30 +421,631 @@ function formatUnifiedItemLabel(
|
|
|
478
421
|
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
479
422
|
const name = theme.bold(item.displayName);
|
|
480
423
|
const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
|
|
424
|
+
const size = item.size !== undefined ? theme.fg("dim", ` • ${formatBytes(item.size)}`) : "";
|
|
481
425
|
const updateBadge = item.updateAvailable ? ` ${theme.fg("warning", "[update]")}` : "";
|
|
482
426
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
427
|
+
return `${pkgIcon} [${scopeIcon}] ${name}${version}${size}${updateBadge}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function getLocalItemCurrentPath(item: LocalUnifiedItem, state?: State): string {
|
|
431
|
+
return (state ?? item.state) === "enabled" ? item.activePath : item.disabledPath;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function formatUnifiedItemDescription(
|
|
435
|
+
item: UnifiedItem,
|
|
436
|
+
state: State | undefined,
|
|
437
|
+
changed: boolean,
|
|
438
|
+
cwd: string
|
|
439
|
+
): string {
|
|
440
|
+
if (item.type === "local") {
|
|
441
|
+
const details = [
|
|
442
|
+
item.summary,
|
|
443
|
+
"local extension",
|
|
444
|
+
item.scope,
|
|
445
|
+
changed ? `staged → ${state ?? item.state}` : (state ?? item.state),
|
|
446
|
+
compactDisplayPath(getLocalItemCurrentPath(item, state), cwd),
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
return details.filter(Boolean).join(" • ");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const sourceKind = getPackageSourceKind(item.source);
|
|
453
|
+
const source = sourceKind === "local" ? compactDisplayPath(item.source, cwd) : item.source;
|
|
454
|
+
const details = [
|
|
455
|
+
item.description || "No description",
|
|
456
|
+
`${sourceKind === "unknown" ? "package" : `${sourceKind} package`}`,
|
|
457
|
+
item.scope,
|
|
458
|
+
source,
|
|
459
|
+
item.updateAvailable ? "update available" : undefined,
|
|
460
|
+
item.size !== undefined ? formatBytes(item.size) : undefined,
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
return details.filter(Boolean).join(" • ");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function compactDisplayPath(filePath: string, cwd: string): string {
|
|
467
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
468
|
+
const normalizedHome = homedir().replace(/\\/g, "/");
|
|
469
|
+
|
|
470
|
+
if (normalizedPath === normalizedHome) {
|
|
471
|
+
return "~";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (normalizedPath.startsWith(`${normalizedHome}/`)) {
|
|
475
|
+
return `~/${normalizedPath.slice(normalizedHome.length + 1)}`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const relativePath = relative(cwd, filePath).replace(/\\/g, "/");
|
|
479
|
+
if (
|
|
480
|
+
relativePath &&
|
|
481
|
+
relativePath !== ".." &&
|
|
482
|
+
!relativePath.startsWith("../") &&
|
|
483
|
+
!isAbsoluteDisplayPath(relativePath)
|
|
484
|
+
) {
|
|
485
|
+
return `./${relativePath}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return normalizedPath;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isAbsoluteDisplayPath(value: string): boolean {
|
|
492
|
+
return /^([a-zA-Z]:\/|\/|\\\\)/.test(value);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function matchesUnifiedFilter(
|
|
496
|
+
item: UnifiedItem,
|
|
497
|
+
filter: UnifiedFilter,
|
|
498
|
+
staged: Map<string, State>
|
|
499
|
+
): boolean {
|
|
500
|
+
switch (filter) {
|
|
501
|
+
case "all":
|
|
502
|
+
return true;
|
|
503
|
+
case "local":
|
|
504
|
+
return item.type === "local";
|
|
505
|
+
case "packages":
|
|
506
|
+
return item.type === "package";
|
|
507
|
+
case "updates":
|
|
508
|
+
return item.type === "package" && Boolean(item.updateAvailable);
|
|
509
|
+
case "disabled":
|
|
510
|
+
return item.type === "local" && getCurrentUnifiedItemState(item, staged) === "disabled";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function getUnifiedItemSearchFields(
|
|
515
|
+
item: UnifiedItem,
|
|
516
|
+
staged: Map<string, State>,
|
|
517
|
+
cwd: string
|
|
518
|
+
): { primary: string[]; secondary: string[] } {
|
|
519
|
+
if (item.type === "local") {
|
|
520
|
+
const state = getCurrentUnifiedItemState(item, staged) ?? item.state;
|
|
521
|
+
return {
|
|
522
|
+
primary: [item.displayName, compactDisplayPath(getLocalItemCurrentPath(item, state), cwd)],
|
|
523
|
+
secondary: [item.summary],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const source =
|
|
528
|
+
getPackageSourceKind(item.source) === "local"
|
|
529
|
+
? compactDisplayPath(item.source, cwd)
|
|
530
|
+
: item.source;
|
|
531
|
+
return {
|
|
532
|
+
primary: [item.displayName, source],
|
|
533
|
+
secondary: [item.version ?? "", item.description ?? ""],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function scoreUnifiedItemSearchMatch(
|
|
538
|
+
item: UnifiedItem,
|
|
539
|
+
query: string,
|
|
540
|
+
staged: Map<string, State>,
|
|
541
|
+
cwd: string
|
|
542
|
+
): number | undefined {
|
|
543
|
+
const tokens = query
|
|
544
|
+
.trim()
|
|
545
|
+
.toLowerCase()
|
|
546
|
+
.split(/\s+/)
|
|
547
|
+
.filter((token) => token.length > 0);
|
|
548
|
+
if (tokens.length === 0) {
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const fields = getUnifiedItemSearchFields(item, staged, cwd);
|
|
553
|
+
const primary = fields.primary
|
|
554
|
+
.map((value) => value.trim().toLowerCase())
|
|
555
|
+
.filter((value) => value.length > 0);
|
|
556
|
+
const secondary = fields.secondary
|
|
557
|
+
.map((value) => value.trim().toLowerCase())
|
|
558
|
+
.filter((value) => value.length > 0);
|
|
559
|
+
|
|
560
|
+
let totalScore = 0;
|
|
561
|
+
|
|
562
|
+
for (const token of tokens) {
|
|
563
|
+
const primarySubstringScore = primary.reduce<number | undefined>((best, field) => {
|
|
564
|
+
const index = field.indexOf(token);
|
|
565
|
+
if (index < 0) {
|
|
566
|
+
return best;
|
|
567
|
+
}
|
|
568
|
+
return best === undefined ? index : Math.min(best, index);
|
|
569
|
+
}, undefined);
|
|
570
|
+
if (primarySubstringScore !== undefined) {
|
|
571
|
+
totalScore += primarySubstringScore;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const secondarySubstringScore = secondary.reduce<number | undefined>((best, field) => {
|
|
576
|
+
const index = field.indexOf(token);
|
|
577
|
+
if (index < 0) {
|
|
578
|
+
return best;
|
|
579
|
+
}
|
|
580
|
+
const score = 100 + index;
|
|
581
|
+
return best === undefined ? score : Math.min(best, score);
|
|
582
|
+
}, undefined);
|
|
583
|
+
if (secondarySubstringScore !== undefined) {
|
|
584
|
+
totalScore += secondarySubstringScore;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const primaryFuzzyScore = primary.reduce<number | undefined>((best, field) => {
|
|
589
|
+
const match = fuzzyMatch(token, field);
|
|
590
|
+
if (!match.matches) {
|
|
591
|
+
return best;
|
|
592
|
+
}
|
|
593
|
+
const score = 200 + match.score;
|
|
594
|
+
return best === undefined ? score : Math.min(best, score);
|
|
595
|
+
}, undefined);
|
|
596
|
+
if (primaryFuzzyScore !== undefined) {
|
|
597
|
+
totalScore += primaryFuzzyScore;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return totalScore;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function searchUnifiedItems(
|
|
608
|
+
items: UnifiedItem[],
|
|
609
|
+
query: string,
|
|
610
|
+
staged: Map<string, State>,
|
|
611
|
+
cwd: string
|
|
612
|
+
): UnifiedItem[] {
|
|
613
|
+
const matches = items
|
|
614
|
+
.map((item, index) => ({
|
|
615
|
+
item,
|
|
616
|
+
index,
|
|
617
|
+
score: scoreUnifiedItemSearchMatch(item, query, staged, cwd),
|
|
618
|
+
}))
|
|
619
|
+
.filter(
|
|
620
|
+
(match): match is { item: UnifiedItem; index: number; score: number } =>
|
|
621
|
+
match.score !== undefined
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
matches.sort((a, b) => a.score - b.score || a.index - b.index);
|
|
625
|
+
return matches.map((match) => match.item);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
class UnifiedManagerBrowser implements Focusable {
|
|
629
|
+
private readonly searchInput = new Input();
|
|
630
|
+
private readonly filteredItems: UnifiedItem[] = [];
|
|
631
|
+
private selectedIndex = 0;
|
|
632
|
+
private filter: UnifiedFilter = "all";
|
|
633
|
+
private searchActive = false;
|
|
634
|
+
private _focused = false;
|
|
635
|
+
|
|
636
|
+
constructor(
|
|
637
|
+
private readonly items: UnifiedItem[],
|
|
638
|
+
private readonly staged: Map<string, State>,
|
|
639
|
+
private readonly theme: Theme,
|
|
640
|
+
private readonly cwd: string,
|
|
641
|
+
private readonly maxVisibleItems: number,
|
|
642
|
+
private readonly onAction: (action: UnifiedAction) => void,
|
|
643
|
+
initialState?: UnifiedManagerViewState
|
|
644
|
+
) {
|
|
645
|
+
if (initialState) {
|
|
646
|
+
this.filter = initialState.filter;
|
|
647
|
+
this.searchInput.setValue(initialState.searchQuery);
|
|
648
|
+
this.refreshVisibleItems(initialState.selectedItemId);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
this.refreshVisibleItems();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
get focused(): boolean {
|
|
656
|
+
return this._focused;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
set focused(value: boolean) {
|
|
660
|
+
this._focused = value;
|
|
661
|
+
this.searchInput.focused = value && this.searchActive;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
getSelectedItem(): UnifiedItem | undefined {
|
|
665
|
+
return this.filteredItems[this.selectedIndex];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
getVisibleItems(): readonly UnifiedItem[] {
|
|
669
|
+
return this.filteredItems;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
getFilter(): UnifiedFilter {
|
|
673
|
+
return this.filter;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
getSearchQuery(): string {
|
|
677
|
+
return this.searchInput.getValue().trim();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
getViewState(): UnifiedManagerViewState {
|
|
681
|
+
const selectedItemId = this.getSelectedItem()?.id;
|
|
682
|
+
return {
|
|
683
|
+
filter: this.filter,
|
|
684
|
+
searchQuery: this.getSearchQuery(),
|
|
685
|
+
...(selectedItemId ? { selectedItemId } : {}),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
invalidate(): void {
|
|
690
|
+
this.searchInput.invalidate();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
handleInput(data: string): void {
|
|
694
|
+
this.handleManagerInput(data);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
handleManagerInput(data: string): boolean {
|
|
698
|
+
const kb = getKeybindings();
|
|
699
|
+
|
|
700
|
+
if (this.searchActive) {
|
|
701
|
+
if (matchesKey(data, Key.enter)) {
|
|
702
|
+
this.searchActive = false;
|
|
703
|
+
this.searchInput.focused = false;
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (matchesKey(data, Key.escape)) {
|
|
708
|
+
this.searchInput.setValue("");
|
|
709
|
+
this.searchActive = false;
|
|
710
|
+
this.searchInput.focused = false;
|
|
711
|
+
this.refreshVisibleItems();
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
this.searchInput.handleInput(data);
|
|
716
|
+
this.refreshVisibleItems();
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (data === "/" || matchesKey(data, Key.ctrl("f"))) {
|
|
721
|
+
this.searchActive = true;
|
|
722
|
+
this.searchInput.focused = this._focused;
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (matchesKey(data, Key.escape) && this.getSearchQuery()) {
|
|
727
|
+
this.searchInput.setValue("");
|
|
728
|
+
this.refreshVisibleItems();
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (matchesKey(data, Key.shift("tab"))) {
|
|
733
|
+
this.cycleFilter(-1);
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (matchesKey(data, Key.tab)) {
|
|
738
|
+
this.cycleFilter(1);
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const directFilter = UNIFIED_FILTER_OPTIONS.find((option) => option.key === data)?.id;
|
|
743
|
+
if (directFilter) {
|
|
744
|
+
this.setFilter(directFilter);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (kb.matches(data, "tui.select.up")) {
|
|
749
|
+
this.moveSelection(-1);
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (kb.matches(data, "tui.select.down")) {
|
|
754
|
+
this.moveSelection(1);
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (kb.matches(data, "tui.select.pageUp")) {
|
|
759
|
+
this.moveSelection(-Math.max(1, this.maxVisibleItems - 1));
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (kb.matches(data, "tui.select.pageDown")) {
|
|
764
|
+
this.moveSelection(Math.max(1, this.maxVisibleItems - 1));
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (matchesKey(data, Key.home)) {
|
|
769
|
+
this.selectedIndex = 0;
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (matchesKey(data, Key.end)) {
|
|
774
|
+
this.selectedIndex = Math.max(0, this.filteredItems.length - 1);
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const selectedItem = this.getSelectedItem();
|
|
779
|
+
const selectedId = selectedItem?.id;
|
|
780
|
+
|
|
781
|
+
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
782
|
+
this.onAction({ type: "apply" });
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if ((matchesKey(data, Key.space) || data === " ") && selectedItem?.type === "local") {
|
|
787
|
+
const currentState =
|
|
788
|
+
getCurrentUnifiedItemState(selectedItem, this.staged) ?? selectedItem.state;
|
|
789
|
+
const nextState: State = currentState === "enabled" ? "disabled" : "enabled";
|
|
790
|
+
if (nextState === selectedItem.originalState) {
|
|
791
|
+
this.staged.delete(selectedItem.id);
|
|
792
|
+
} else {
|
|
793
|
+
this.staged.set(selectedItem.id, nextState);
|
|
794
|
+
}
|
|
795
|
+
this.refreshVisibleItems(selectedItem.id);
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (matchesKey(data, Key.enter) && selectedId) {
|
|
800
|
+
this.onAction({ type: "action", itemId: selectedId, action: "menu" });
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (data === "a" || data === "A") {
|
|
805
|
+
if (selectedId) {
|
|
806
|
+
this.onAction({ type: "action", itemId: selectedId, action: "menu" });
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (data === "i") {
|
|
812
|
+
this.onAction({ type: "quick", action: "install" });
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (data === "f") {
|
|
817
|
+
this.onAction({ type: "quick", action: "search" });
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (data === "U") {
|
|
822
|
+
this.onAction({ type: "quick", action: "update-all" });
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (data === "t" || data === "T") {
|
|
827
|
+
this.onAction({ type: "quick", action: "auto-update" });
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (selectedId && (data === "v" || data === "V")) {
|
|
832
|
+
this.onAction({ type: "action", itemId: selectedId, action: "details" });
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (selectedId && selectedItem?.type === "package") {
|
|
837
|
+
if (data === "u") {
|
|
838
|
+
this.onAction({ type: "action", itemId: selectedId, action: "update" });
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
if (data === "x" || data === "X") {
|
|
842
|
+
this.onAction({ type: "action", itemId: selectedId, action: "remove" });
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
if (data === "c" || data === "C") {
|
|
846
|
+
this.onAction({ type: "action", itemId: selectedId, action: "configure" });
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (selectedId && selectedItem?.type === "local" && (data === "x" || data === "X")) {
|
|
852
|
+
this.onAction({ type: "action", itemId: selectedId, action: "remove" });
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (data === "r" || data === "R") {
|
|
857
|
+
this.onAction({ type: "remote" });
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (data === "?" || data === "h" || data === "H") {
|
|
862
|
+
this.onAction({ type: "help" });
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (data === "m" || data === "M" || data === "p" || data === "P") {
|
|
867
|
+
this.onAction({ type: "menu" });
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (matchesKey(data, Key.escape)) {
|
|
872
|
+
this.onAction({ type: "cancel" });
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
render(width: number): string[] {
|
|
880
|
+
const lines: string[] = [];
|
|
881
|
+
|
|
882
|
+
const searchQuery = this.searchInput.getValue().trim();
|
|
883
|
+
if (this.searchActive) {
|
|
884
|
+
lines.push(...this.searchInput.render(width));
|
|
885
|
+
lines.push("");
|
|
886
|
+
} else if (searchQuery) {
|
|
887
|
+
lines.push(truncateToWidth(this.theme.fg("accent", ` Search: ${searchQuery}`), width, ""));
|
|
888
|
+
lines.push("");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
lines.push(truncateToWidth(this.buildFilterLine(), width, ""));
|
|
892
|
+
lines.push("");
|
|
893
|
+
|
|
894
|
+
if (this.filteredItems.length === 0) {
|
|
895
|
+
lines.push(this.theme.fg("warning", " No matching extensions or packages"));
|
|
896
|
+
return lines;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const { startIndex, endIndex } = this.getVisibleRange();
|
|
900
|
+
const visibleItems = this.filteredItems.slice(startIndex, endIndex);
|
|
901
|
+
const localCount = this.filteredItems.filter((item) => item.type === "local").length;
|
|
902
|
+
const packageCount = this.filteredItems.length - localCount;
|
|
903
|
+
const visibleLocalItems = visibleItems.filter((item) => item.type === "local");
|
|
904
|
+
const visiblePackageItems = visibleItems.filter((item) => item.type === "package");
|
|
905
|
+
|
|
906
|
+
if (visibleLocalItems.length > 0) {
|
|
907
|
+
lines.push(this.theme.fg("accent", ` Local extensions (${localCount})`));
|
|
908
|
+
for (const item of visibleLocalItems) {
|
|
909
|
+
lines.push(this.renderItemLine(item, width));
|
|
910
|
+
}
|
|
911
|
+
if (visiblePackageItems.length > 0) {
|
|
912
|
+
lines.push("");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (visiblePackageItems.length > 0) {
|
|
917
|
+
lines.push(this.theme.fg("accent", ` Installed packages (${packageCount})`));
|
|
918
|
+
for (const item of visiblePackageItems) {
|
|
919
|
+
lines.push(this.renderItemLine(item, width));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (startIndex > 0 || endIndex < this.filteredItems.length) {
|
|
924
|
+
lines.push("");
|
|
925
|
+
lines.push(
|
|
926
|
+
this.theme.fg(
|
|
927
|
+
"dim",
|
|
928
|
+
` Showing ${startIndex + 1}-${endIndex} of ${this.filteredItems.length}`
|
|
929
|
+
)
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const selectedItem = this.getSelectedItem();
|
|
934
|
+
if (selectedItem) {
|
|
935
|
+
lines.push("");
|
|
936
|
+
const selectedState = getCurrentUnifiedItemState(selectedItem, this.staged);
|
|
937
|
+
const detailText = formatUnifiedItemDescription(
|
|
938
|
+
selectedItem,
|
|
939
|
+
selectedState,
|
|
940
|
+
selectedItem.type === "local" && selectedState !== selectedItem.originalState,
|
|
941
|
+
this.cwd
|
|
942
|
+
);
|
|
943
|
+
for (const line of wrapTextWithAnsi(detailText, width - 4)) {
|
|
944
|
+
lines.push(this.theme.fg("dim", ` ${line}`));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return lines;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private buildFilterLine(): string {
|
|
952
|
+
const filters = UNIFIED_FILTER_OPTIONS.map(({ id, key, label }) => {
|
|
953
|
+
const text = `${key}:${label}`;
|
|
954
|
+
return id === this.filter
|
|
955
|
+
? this.theme.fg("accent", `[${text}]`)
|
|
956
|
+
: this.theme.fg("muted", text);
|
|
957
|
+
}).join(" ");
|
|
958
|
+
const searchHint = this.theme.fg(
|
|
959
|
+
this.searchActive || this.searchInput.getValue() ? "accent" : "dim",
|
|
960
|
+
"/ search"
|
|
961
|
+
);
|
|
962
|
+
return ` ${filters} · ${searchHint}`;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private renderItemLine(item: UnifiedItem, width: number): string {
|
|
966
|
+
const state = getCurrentUnifiedItemState(item, this.staged);
|
|
967
|
+
const changed = item.type === "local" && state !== item.originalState;
|
|
968
|
+
const prefix = this.getSelectedItem()?.id === item.id ? this.theme.fg("accent", "→ ") : " ";
|
|
969
|
+
return truncateToWidth(
|
|
970
|
+
prefix + formatUnifiedItemLabel(item, state, this.theme, changed),
|
|
971
|
+
width
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
private refreshVisibleItems(preferredItemId?: string): void {
|
|
976
|
+
const previousSelectedId = preferredItemId ?? this.getSelectedItem()?.id;
|
|
977
|
+
const filteredByMode = this.items.filter((item) =>
|
|
978
|
+
matchesUnifiedFilter(item, this.filter, this.staged)
|
|
979
|
+
);
|
|
980
|
+
const query = this.searchInput.getValue().trim();
|
|
981
|
+
this.filteredItems.length = 0;
|
|
982
|
+
this.filteredItems.push(
|
|
983
|
+
...(query ? searchUnifiedItems(filteredByMode, query, this.staged, this.cwd) : filteredByMode)
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
if (this.filteredItems.length === 0) {
|
|
987
|
+
this.selectedIndex = 0;
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const nextSelectedIndex = previousSelectedId
|
|
992
|
+
? this.filteredItems.findIndex((item) => item.id === previousSelectedId)
|
|
993
|
+
: -1;
|
|
994
|
+
if (nextSelectedIndex >= 0) {
|
|
995
|
+
this.selectedIndex = nextSelectedIndex;
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
this.selectedIndex = Math.min(this.selectedIndex, this.filteredItems.length - 1);
|
|
496
1000
|
}
|
|
497
1001
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1002
|
+
private setFilter(filter: UnifiedFilter): void {
|
|
1003
|
+
this.filter = filter;
|
|
1004
|
+
this.refreshVisibleItems();
|
|
501
1005
|
}
|
|
502
1006
|
|
|
503
|
-
|
|
504
|
-
|
|
1007
|
+
private cycleFilter(direction: -1 | 1): void {
|
|
1008
|
+
const currentIndex = UNIFIED_FILTER_OPTIONS.findIndex((option) => option.id === this.filter);
|
|
1009
|
+
const nextIndex =
|
|
1010
|
+
(currentIndex + direction + UNIFIED_FILTER_OPTIONS.length) % UNIFIED_FILTER_OPTIONS.length;
|
|
1011
|
+
const nextFilter = UNIFIED_FILTER_OPTIONS[nextIndex]?.id;
|
|
1012
|
+
if (nextFilter) {
|
|
1013
|
+
this.setFilter(nextFilter);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private moveSelection(delta: number): void {
|
|
1018
|
+
if (this.filteredItems.length === 0) {
|
|
1019
|
+
this.selectedIndex = 0;
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const nextIndex = this.selectedIndex + delta;
|
|
1024
|
+
if (nextIndex < 0) {
|
|
1025
|
+
this.selectedIndex = 0;
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (nextIndex >= this.filteredItems.length) {
|
|
1030
|
+
this.selectedIndex = this.filteredItems.length - 1;
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
this.selectedIndex = nextIndex;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
private getVisibleRange(): { startIndex: number; endIndex: number } {
|
|
1038
|
+
const maxVisible = Math.max(1, this.maxVisibleItems);
|
|
1039
|
+
const startIndex = Math.max(
|
|
1040
|
+
0,
|
|
1041
|
+
Math.min(
|
|
1042
|
+
this.selectedIndex - Math.floor(maxVisible / 2),
|
|
1043
|
+
Math.max(0, this.filteredItems.length - maxVisible)
|
|
1044
|
+
)
|
|
1045
|
+
);
|
|
1046
|
+
const endIndex = Math.min(startIndex + maxVisible, this.filteredItems.length);
|
|
1047
|
+
return { startIndex, endIndex };
|
|
1048
|
+
}
|
|
505
1049
|
}
|
|
506
1050
|
|
|
507
1051
|
function getToggleItemsForApply(items: UnifiedItem[]): LocalUnifiedItem[] {
|
|
@@ -565,6 +1109,7 @@ async function resolvePendingChangesBeforeLeave(
|
|
|
565
1109
|
}
|
|
566
1110
|
|
|
567
1111
|
if (choice === "Discard changes") {
|
|
1112
|
+
staged.clear();
|
|
568
1113
|
return "continue";
|
|
569
1114
|
}
|
|
570
1115
|
|
|
@@ -597,6 +1142,12 @@ const QUICK_DESTINATION_LABELS: Record<QuickDestination, string> = {
|
|
|
597
1142
|
help: "Help",
|
|
598
1143
|
};
|
|
599
1144
|
|
|
1145
|
+
const LOCAL_ACTION_OPTIONS = {
|
|
1146
|
+
details: "View details",
|
|
1147
|
+
remove: "Remove local extension",
|
|
1148
|
+
back: "Back to manager",
|
|
1149
|
+
} as const;
|
|
1150
|
+
|
|
600
1151
|
const PACKAGE_ACTION_OPTIONS = {
|
|
601
1152
|
configure: "Configure extensions",
|
|
602
1153
|
update: "Update package",
|
|
@@ -605,10 +1156,28 @@ const PACKAGE_ACTION_OPTIONS = {
|
|
|
605
1156
|
back: "Back to manager",
|
|
606
1157
|
} as const;
|
|
607
1158
|
|
|
1159
|
+
type LocalActionKey = keyof typeof LOCAL_ACTION_OPTIONS;
|
|
608
1160
|
type PackageActionKey = keyof typeof PACKAGE_ACTION_OPTIONS;
|
|
609
1161
|
|
|
1162
|
+
type LocalActionSelection = Exclude<LocalActionKey, "back"> | "cancel";
|
|
610
1163
|
type PackageActionSelection = Exclude<PackageActionKey, "back"> | "cancel";
|
|
611
1164
|
|
|
1165
|
+
async function promptLocalActionSelection(
|
|
1166
|
+
item: LocalUnifiedItem,
|
|
1167
|
+
ctx: ExtensionCommandContext
|
|
1168
|
+
): Promise<LocalActionSelection> {
|
|
1169
|
+
const selection = parseChoiceByLabel(
|
|
1170
|
+
LOCAL_ACTION_OPTIONS,
|
|
1171
|
+
await ctx.ui.select(item.displayName, Object.values(LOCAL_ACTION_OPTIONS))
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
if (!selection || selection === "back") {
|
|
1175
|
+
return "cancel";
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return selection;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
612
1181
|
async function promptPackageActionSelection(
|
|
613
1182
|
pkg: InstalledPackage,
|
|
614
1183
|
ctx: ExtensionCommandContext
|
|
@@ -625,6 +1194,27 @@ async function promptPackageActionSelection(
|
|
|
625
1194
|
return selection;
|
|
626
1195
|
}
|
|
627
1196
|
|
|
1197
|
+
function showUnifiedItemDetails(
|
|
1198
|
+
item: UnifiedItem,
|
|
1199
|
+
ctx: ExtensionCommandContext,
|
|
1200
|
+
state?: State
|
|
1201
|
+
): void {
|
|
1202
|
+
if (item.type === "local") {
|
|
1203
|
+
const currentState = state ?? item.state;
|
|
1204
|
+
ctx.ui.notify(
|
|
1205
|
+
`Name: ${item.displayName}\nScope: ${item.scope}\nState: ${currentState}\nPath: ${getLocalItemCurrentPath(item, currentState)}\nSummary: ${item.summary}`,
|
|
1206
|
+
"info"
|
|
1207
|
+
);
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const sizeStr = item.size !== undefined ? `\nSize: ${formatBytes(item.size)}` : "";
|
|
1212
|
+
ctx.ui.notify(
|
|
1213
|
+
`Name: ${item.displayName}\nVersion: ${item.version || "unknown"}\nSource: ${item.source}\nScope: ${item.scope}${sizeStr}${item.description ? `\nDescription: ${item.description}` : ""}`,
|
|
1214
|
+
"info"
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
628
1218
|
async function navigateWithPendingGuard(
|
|
629
1219
|
destination: QuickDestination,
|
|
630
1220
|
items: UnifiedItem[],
|
|
@@ -632,7 +1222,7 @@ async function navigateWithPendingGuard(
|
|
|
632
1222
|
byId: Map<string, UnifiedItem>,
|
|
633
1223
|
ctx: ExtensionCommandContext,
|
|
634
1224
|
pi: ExtensionAPI
|
|
635
|
-
): Promise<"
|
|
1225
|
+
): Promise<"reload" | "resume" | "stay" | "exit"> {
|
|
636
1226
|
const pending = await resolvePendingChangesBeforeLeave(
|
|
637
1227
|
items,
|
|
638
1228
|
staged,
|
|
@@ -646,16 +1236,16 @@ async function navigateWithPendingGuard(
|
|
|
646
1236
|
switch (destination) {
|
|
647
1237
|
case "install":
|
|
648
1238
|
await showRemote("install", ctx, pi);
|
|
649
|
-
return "
|
|
1239
|
+
return "reload";
|
|
650
1240
|
case "search":
|
|
651
1241
|
await showRemote("search", ctx, pi);
|
|
652
|
-
return "
|
|
1242
|
+
return "reload";
|
|
653
1243
|
case "browse":
|
|
654
1244
|
await showRemote("", ctx, pi);
|
|
655
|
-
return "
|
|
1245
|
+
return "reload";
|
|
656
1246
|
case "update-all": {
|
|
657
1247
|
const outcome = await updatePackagesWithOutcome(ctx, pi);
|
|
658
|
-
return outcome.reloaded ? "exit" : "
|
|
1248
|
+
return outcome.reloaded ? "exit" : "reload";
|
|
659
1249
|
}
|
|
660
1250
|
case "auto-update":
|
|
661
1251
|
await promptAutoUpdateWizard(pi, ctx, (packages) => {
|
|
@@ -665,10 +1255,10 @@ async function navigateWithPendingGuard(
|
|
|
665
1255
|
);
|
|
666
1256
|
});
|
|
667
1257
|
void updateExtmgrStatus(ctx, pi);
|
|
668
|
-
return "
|
|
1258
|
+
return "resume";
|
|
669
1259
|
case "help":
|
|
670
1260
|
showHelp(ctx);
|
|
671
|
-
return "
|
|
1261
|
+
return "resume";
|
|
672
1262
|
}
|
|
673
1263
|
}
|
|
674
1264
|
|
|
@@ -679,7 +1269,7 @@ async function handleUnifiedAction(
|
|
|
679
1269
|
byId: Map<string, UnifiedItem>,
|
|
680
1270
|
ctx: ExtensionCommandContext,
|
|
681
1271
|
pi: ExtensionAPI
|
|
682
|
-
): Promise<boolean> {
|
|
1272
|
+
): Promise<boolean | "resume"> {
|
|
683
1273
|
if (result.type === "cancel") {
|
|
684
1274
|
const pendingCount = getPendingToggleChangeCount(staged, byId);
|
|
685
1275
|
if (pendingCount > 0) {
|
|
@@ -690,13 +1280,13 @@ async function handleUnifiedAction(
|
|
|
690
1280
|
]);
|
|
691
1281
|
|
|
692
1282
|
if (!choice || choice === "Stay in manager") {
|
|
693
|
-
return
|
|
1283
|
+
return "resume";
|
|
694
1284
|
}
|
|
695
1285
|
|
|
696
1286
|
if (choice === "Save and exit") {
|
|
697
1287
|
const apply = await applyToggleChangesFromManager(items, staged, ctx, pi);
|
|
698
1288
|
if (apply.reloaded) return true;
|
|
699
|
-
if (apply.changed === 0 && apply.hasErrors) return
|
|
1289
|
+
if (apply.changed === 0 && apply.hasErrors) return "resume";
|
|
700
1290
|
}
|
|
701
1291
|
}
|
|
702
1292
|
|
|
@@ -705,7 +1295,7 @@ async function handleUnifiedAction(
|
|
|
705
1295
|
|
|
706
1296
|
if (result.type === "remote") {
|
|
707
1297
|
const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Remote");
|
|
708
|
-
if (pending === "stay") return
|
|
1298
|
+
if (pending === "stay") return "resume";
|
|
709
1299
|
|
|
710
1300
|
await showRemote("", ctx, pi);
|
|
711
1301
|
return false;
|
|
@@ -713,10 +1303,10 @@ async function handleUnifiedAction(
|
|
|
713
1303
|
|
|
714
1304
|
if (result.type === "help") {
|
|
715
1305
|
const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Help");
|
|
716
|
-
if (pending === "stay") return
|
|
1306
|
+
if (pending === "stay") return "resume";
|
|
717
1307
|
|
|
718
1308
|
showHelp(ctx);
|
|
719
|
-
return
|
|
1309
|
+
return "resume";
|
|
720
1310
|
}
|
|
721
1311
|
|
|
722
1312
|
if (result.type === "menu") {
|
|
@@ -736,10 +1326,11 @@ async function handleUnifiedAction(
|
|
|
736
1326
|
|
|
737
1327
|
const destination = choice ? destinationByAction[choice] : undefined;
|
|
738
1328
|
if (!destination) {
|
|
739
|
-
return
|
|
1329
|
+
return "resume";
|
|
740
1330
|
}
|
|
741
1331
|
|
|
742
1332
|
const outcome = await navigateWithPendingGuard(destination, items, staged, byId, ctx, pi);
|
|
1333
|
+
if (outcome === "stay" || outcome === "resume") return "resume";
|
|
743
1334
|
return outcome === "exit";
|
|
744
1335
|
}
|
|
745
1336
|
|
|
@@ -753,6 +1344,7 @@ async function handleUnifiedAction(
|
|
|
753
1344
|
|
|
754
1345
|
const destination = quickDestinationMap[result.action];
|
|
755
1346
|
const outcome = await navigateWithPendingGuard(destination, items, staged, byId, ctx, pi);
|
|
1347
|
+
if (outcome === "stay" || outcome === "resume") return "resume";
|
|
756
1348
|
return outcome === "exit";
|
|
757
1349
|
}
|
|
758
1350
|
|
|
@@ -760,25 +1352,40 @@ async function handleUnifiedAction(
|
|
|
760
1352
|
const item = byId.get(result.itemId);
|
|
761
1353
|
if (!item) return false;
|
|
762
1354
|
|
|
763
|
-
const pendingDestination = item.type === "local" ? "remove extension" : "package actions";
|
|
764
|
-
const pending = await resolvePendingChangesBeforeLeave(
|
|
765
|
-
items,
|
|
766
|
-
staged,
|
|
767
|
-
byId,
|
|
768
|
-
ctx,
|
|
769
|
-
pi,
|
|
770
|
-
pendingDestination
|
|
771
|
-
);
|
|
772
|
-
if (pending === "stay") return false;
|
|
773
|
-
|
|
774
1355
|
if (item.type === "local") {
|
|
775
|
-
|
|
1356
|
+
const selection =
|
|
1357
|
+
!result.action || result.action === "menu"
|
|
1358
|
+
? await promptLocalActionSelection(item, ctx)
|
|
1359
|
+
: result.action;
|
|
1360
|
+
|
|
1361
|
+
if (selection === "cancel") {
|
|
1362
|
+
return "resume";
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (selection === "details") {
|
|
1366
|
+
showUnifiedItemDetails(item, ctx, staged.get(item.id) ?? item.state);
|
|
1367
|
+
return "resume";
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (selection !== "remove") {
|
|
1371
|
+
return "resume";
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const pending = await resolvePendingChangesBeforeLeave(
|
|
1375
|
+
items,
|
|
1376
|
+
staged,
|
|
1377
|
+
byId,
|
|
1378
|
+
ctx,
|
|
1379
|
+
pi,
|
|
1380
|
+
"remove extension"
|
|
1381
|
+
);
|
|
1382
|
+
if (pending === "stay") return "resume";
|
|
776
1383
|
|
|
777
1384
|
const confirmed = await ctx.ui.confirm(
|
|
778
1385
|
"Delete Local Extension",
|
|
779
1386
|
`Delete ${item.displayName} from disk?\n\nThis cannot be undone.`
|
|
780
1387
|
);
|
|
781
|
-
if (!confirmed) return
|
|
1388
|
+
if (!confirmed) return "resume";
|
|
782
1389
|
|
|
783
1390
|
const removal = await removeLocalExtension(
|
|
784
1391
|
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
@@ -787,7 +1394,7 @@ async function handleUnifiedAction(
|
|
|
787
1394
|
if (!removal.ok) {
|
|
788
1395
|
logExtensionDelete(pi, item.id, false, removal.error);
|
|
789
1396
|
ctx.ui.notify(`Failed to remove extension: ${removal.error}`, "error");
|
|
790
|
-
return
|
|
1397
|
+
return "resume";
|
|
791
1398
|
}
|
|
792
1399
|
|
|
793
1400
|
logExtensionDelete(pi, item.id, true);
|
|
@@ -804,6 +1411,7 @@ async function handleUnifiedAction(
|
|
|
804
1411
|
name: item.displayName,
|
|
805
1412
|
...(item.version ? { version: item.version } : {}),
|
|
806
1413
|
scope: item.scope,
|
|
1414
|
+
...(item.resolvedPath ? { resolvedPath: item.resolvedPath } : {}),
|
|
807
1415
|
...(item.description ? { description: item.description } : {}),
|
|
808
1416
|
...(item.size !== undefined ? { size: item.size } : {}),
|
|
809
1417
|
};
|
|
@@ -814,9 +1422,30 @@ async function handleUnifiedAction(
|
|
|
814
1422
|
: result.action;
|
|
815
1423
|
|
|
816
1424
|
if (selection === "cancel") {
|
|
817
|
-
return
|
|
1425
|
+
return "resume";
|
|
818
1426
|
}
|
|
819
1427
|
|
|
1428
|
+
if (selection === "details") {
|
|
1429
|
+
showUnifiedItemDetails(item, ctx);
|
|
1430
|
+
return "resume";
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const pendingDestinationBySelection = {
|
|
1434
|
+
configure: "configure package extensions",
|
|
1435
|
+
update: "update package",
|
|
1436
|
+
remove: "remove package",
|
|
1437
|
+
} satisfies Record<Exclude<PackageActionSelection, "cancel" | "details">, string>;
|
|
1438
|
+
|
|
1439
|
+
const pending = await resolvePendingChangesBeforeLeave(
|
|
1440
|
+
items,
|
|
1441
|
+
staged,
|
|
1442
|
+
byId,
|
|
1443
|
+
ctx,
|
|
1444
|
+
pi,
|
|
1445
|
+
pendingDestinationBySelection[selection]
|
|
1446
|
+
);
|
|
1447
|
+
if (pending === "stay") return "resume";
|
|
1448
|
+
|
|
820
1449
|
switch (selection) {
|
|
821
1450
|
case "configure": {
|
|
822
1451
|
const outcome = await configurePackageExtensions(pkg, ctx, pi);
|
|
@@ -830,19 +1459,11 @@ async function handleUnifiedAction(
|
|
|
830
1459
|
const outcome = await removePackageWithOutcome(pkg.source, ctx, pi);
|
|
831
1460
|
return outcome.reloaded;
|
|
832
1461
|
}
|
|
833
|
-
case "details": {
|
|
834
|
-
const sizeStr = pkg.size !== undefined ? `\nSize: ${formatBytes(pkg.size)}` : "";
|
|
835
|
-
ctx.ui.notify(
|
|
836
|
-
`Name: ${pkg.name}\nVersion: ${pkg.version || "unknown"}\nSource: ${pkg.source}\nScope: ${pkg.scope}${sizeStr}${pkg.description ? `\nDescription: ${pkg.description}` : ""}`,
|
|
837
|
-
"info"
|
|
838
|
-
);
|
|
839
|
-
return false;
|
|
840
|
-
}
|
|
841
1462
|
}
|
|
842
1463
|
}
|
|
843
1464
|
|
|
844
1465
|
const apply = await applyToggleChangesFromManager(items, staged, ctx, pi);
|
|
845
|
-
return apply.reloaded;
|
|
1466
|
+
return apply.reloaded ? true : "resume";
|
|
846
1467
|
}
|
|
847
1468
|
|
|
848
1469
|
async function applyStagedChanges(
|
|
@@ -857,6 +1478,7 @@ async function applyStagedChanges(
|
|
|
857
1478
|
const target = staged.get(item.id) ?? item.originalState;
|
|
858
1479
|
if (target === item.originalState) continue;
|
|
859
1480
|
|
|
1481
|
+
const fromState = item.originalState;
|
|
860
1482
|
const result = await setExtensionState(
|
|
861
1483
|
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
862
1484
|
target
|
|
@@ -864,10 +1486,13 @@ async function applyStagedChanges(
|
|
|
864
1486
|
|
|
865
1487
|
if (result.ok) {
|
|
866
1488
|
changed++;
|
|
867
|
-
|
|
1489
|
+
item.state = target;
|
|
1490
|
+
item.originalState = target;
|
|
1491
|
+
staged.delete(item.id);
|
|
1492
|
+
logExtensionToggle(pi, item.id, fromState, target, true);
|
|
868
1493
|
} else {
|
|
869
1494
|
errors.push(`${item.id}: ${result.error}`);
|
|
870
|
-
logExtensionToggle(pi, item.id,
|
|
1495
|
+
logExtensionToggle(pi, item.id, fromState, target, false, result.error);
|
|
871
1496
|
}
|
|
872
1497
|
}
|
|
873
1498
|
|
|
@@ -895,23 +1520,9 @@ export async function showInstalledPackagesLegacy(
|
|
|
895
1520
|
export async function showListOnly(ctx: ExtensionCommandContext): Promise<void> {
|
|
896
1521
|
const entries = await discoverExtensions(ctx.cwd);
|
|
897
1522
|
if (entries.length === 0) {
|
|
898
|
-
|
|
899
|
-
if (ctx.hasUI) {
|
|
900
|
-
ctx.ui.notify(msg, "info");
|
|
901
|
-
} else {
|
|
902
|
-
console.log(msg);
|
|
903
|
-
}
|
|
1523
|
+
notify(ctx, "No extensions found in ~/.pi/agent/extensions or .pi/extensions", "info");
|
|
904
1524
|
return;
|
|
905
1525
|
}
|
|
906
1526
|
|
|
907
|
-
|
|
908
|
-
const output = lines.join("\n");
|
|
909
|
-
const titledOutput = `Local extensions:\n${output}`;
|
|
910
|
-
|
|
911
|
-
if (ctx.hasUI) {
|
|
912
|
-
ctx.ui.notify(titledOutput, "info");
|
|
913
|
-
} else {
|
|
914
|
-
console.log("Local extensions:");
|
|
915
|
-
console.log(output);
|
|
916
|
-
}
|
|
1527
|
+
formatListOutput(ctx, "Local extensions", entries.map(formatExtEntry));
|
|
917
1528
|
}
|