pi-extmgr 0.1.27 → 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 +21 -10
- package/package.json +21 -16
- package/src/commands/auto-update.ts +5 -5
- package/src/commands/cache.ts +1 -1
- package/src/commands/history.ts +5 -34
- package/src/commands/install.ts +2 -2
- package/src/commands/registry.ts +7 -7
- package/src/commands/types.ts +1 -1
- package/src/constants.ts +0 -8
- package/src/extensions/discovery.ts +125 -42
- package/src/index.ts +15 -15
- package/src/packages/catalog.ts +9 -8
- package/src/packages/discovery.ts +56 -19
- package/src/packages/extensions.ts +65 -103
- package/src/packages/install.ts +104 -74
- package/src/packages/management.ts +78 -65
- package/src/types/index.ts +20 -11
- package/src/ui/async-task.ts +101 -65
- package/src/ui/footer.ts +47 -31
- package/src/ui/help.ts +17 -13
- package/src/ui/package-config.ts +36 -48
- package/src/ui/remote.ts +714 -119
- package/src/ui/theme.ts +2 -2
- package/src/ui/unified.ts +964 -371
- package/src/utils/auto-update.ts +44 -39
- package/src/utils/cache.ts +208 -37
- package/src/utils/command.ts +1 -1
- package/src/utils/duration.ts +132 -0
- package/src/utils/format.ts +4 -33
- package/src/utils/fs.ts +8 -4
- package/src/utils/history.ts +47 -9
- package/src/utils/mode.ts +2 -2
- package/src/utils/notify.ts +1 -15
- package/src/utils/npm-exec.ts +1 -1
- package/src/utils/package-source.ts +35 -7
- package/src/utils/path-identity.ts +7 -0
- package/src/utils/relative-path-selection.ts +100 -0
- package/src/utils/settings.ts +11 -61
- package/src/utils/status.ts +12 -10
- package/src/utils/ui-helpers.ts +2 -2
- package/src/utils/retry.ts +0 -49
package/src/ui/unified.ts
CHANGED
|
@@ -2,19 +2,28 @@
|
|
|
2
2
|
* Unified extension manager UI
|
|
3
3
|
* Displays local extensions and installed packages in one view
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { relative } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
DynamicBorder,
|
|
9
|
+
type ExtensionAPI,
|
|
10
|
+
type ExtensionCommandContext,
|
|
11
|
+
type Theme,
|
|
12
|
+
} from "@mariozechner/pi-coding-agent";
|
|
8
13
|
import {
|
|
9
14
|
Container,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
matchesKey,
|
|
15
|
+
type Focusable,
|
|
16
|
+
fuzzyMatch,
|
|
17
|
+
getKeybindings,
|
|
18
|
+
Input,
|
|
15
19
|
Key,
|
|
20
|
+
matchesKey,
|
|
21
|
+
Spacer,
|
|
22
|
+
Text,
|
|
23
|
+
truncateToWidth,
|
|
24
|
+
wrapTextWithAnsi,
|
|
16
25
|
} from "@mariozechner/pi-tui";
|
|
17
|
-
import
|
|
26
|
+
import { UI } from "../constants.js";
|
|
18
27
|
import {
|
|
19
28
|
discoverExtensions,
|
|
20
29
|
removeLocalExtension,
|
|
@@ -22,34 +31,34 @@ import {
|
|
|
22
31
|
} from "../extensions/discovery.js";
|
|
23
32
|
import { getInstalledPackages } from "../packages/discovery.js";
|
|
24
33
|
import {
|
|
25
|
-
updatePackageWithOutcome,
|
|
26
34
|
removePackageWithOutcome,
|
|
27
|
-
updatePackagesWithOutcome,
|
|
28
35
|
showInstalledPackagesList,
|
|
36
|
+
updatePackagesWithOutcome,
|
|
37
|
+
updatePackageWithOutcome,
|
|
29
38
|
} from "../packages/management.js";
|
|
30
|
-
import { showRemote } from "./remote.js";
|
|
31
|
-
import { showHelp } from "./help.js";
|
|
32
|
-
import { runTaskWithLoader } from "./async-task.js";
|
|
33
|
-
import { formatEntry as formatExtEntry, dynamicTruncate, formatBytes } from "../utils/format.js";
|
|
34
39
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} from "
|
|
41
|
-
import { buildFooterState, buildFooterShortcuts, getPendingToggleChangeCount } from "./footer.js";
|
|
42
|
-
import { logExtensionDelete, logExtensionToggle } from "../utils/history.js";
|
|
40
|
+
type InstalledPackage,
|
|
41
|
+
type LocalUnifiedItem,
|
|
42
|
+
type State,
|
|
43
|
+
type UnifiedAction,
|
|
44
|
+
type UnifiedItem,
|
|
45
|
+
} from "../types/index.js";
|
|
43
46
|
import { getKnownUpdates, promptAutoUpdateWizard } from "../utils/auto-update.js";
|
|
44
|
-
import { updateExtmgrStatus } from "../utils/status.js";
|
|
45
47
|
import { parseChoiceByLabel } from "../utils/command.js";
|
|
48
|
+
import { formatBytes, formatEntry as formatExtEntry } from "../utils/format.js";
|
|
49
|
+
import { logExtensionDelete, logExtensionToggle } from "../utils/history.js";
|
|
50
|
+
import { hasCustomUI, runCustomUI } from "../utils/mode.js";
|
|
46
51
|
import { notify } from "../utils/notify.js";
|
|
47
|
-
import {
|
|
52
|
+
import { normalizePathIdentity } from "../utils/path-identity.js";
|
|
48
53
|
import { getPackageSourceKind, normalizePackageIdentity } from "../utils/package-source.js";
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
54
|
+
import { updateExtmgrStatus } from "../utils/status.js";
|
|
55
|
+
import { confirmReload, formatListOutput } from "../utils/ui-helpers.js";
|
|
56
|
+
import { runTaskWithLoader } from "./async-task.js";
|
|
57
|
+
import { buildFooterShortcuts, buildFooterState, getPendingToggleChangeCount } from "./footer.js";
|
|
58
|
+
import { showHelp } from "./help.js";
|
|
52
59
|
import { configurePackageExtensions } from "./package-config.js";
|
|
60
|
+
import { showRemote } from "./remote.js";
|
|
61
|
+
import { getChangeMarker, getPackageIcon, getScopeIcon, getStatusIcon } from "./theme.js";
|
|
53
62
|
|
|
54
63
|
async function showInteractiveFallback(
|
|
55
64
|
ctx: ExtensionCommandContext,
|
|
@@ -146,202 +155,108 @@ async function showInteractiveOnce(
|
|
|
146
155
|
// Staged changes tracking for local extensions.
|
|
147
156
|
const staged = new Map<string, State>();
|
|
148
157
|
const byId = new Map(items.map((item) => [item.id, item]));
|
|
158
|
+
let managerState: UnifiedManagerViewState | undefined;
|
|
149
159
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"The unified extensions manager",
|
|
153
|
-
() =>
|
|
154
|
-
ctx.ui.custom<UnifiedAction>((tui, theme, _keybindings, done) => {
|
|
155
|
-
const container = new Container();
|
|
156
|
-
|
|
157
|
-
const titleText = new Text("", 2, 0);
|
|
158
|
-
const subtitleText = new Text("", 2, 0);
|
|
159
|
-
const quickText = new Text("", 2, 0);
|
|
160
|
-
const footerState = buildFooterState(items);
|
|
161
|
-
const footerText = new Text("", 2, 0);
|
|
162
|
-
|
|
163
|
-
// Header
|
|
164
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
165
|
-
container.addChild(titleText);
|
|
166
|
-
container.addChild(subtitleText);
|
|
167
|
-
container.addChild(quickText);
|
|
168
|
-
container.addChild(new Spacer(1));
|
|
169
|
-
|
|
170
|
-
// Build settings items
|
|
171
|
-
const settingsItems = buildSettingsItems(items, staged, theme);
|
|
172
|
-
const syncThemedContent = (): void => {
|
|
173
|
-
titleText.setText(theme.fg("accent", theme.bold("Extensions Manager")));
|
|
174
|
-
subtitleText.setText(
|
|
175
|
-
theme.fg(
|
|
176
|
-
"muted",
|
|
177
|
-
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle local • Enter/A actions • c configure pkg extensions • u update pkg • x remove selected`
|
|
178
|
-
)
|
|
179
|
-
);
|
|
180
|
-
quickText.setText(
|
|
181
|
-
theme.fg(
|
|
182
|
-
"dim",
|
|
183
|
-
"Quick: i Install | f Search | U Update all | t Auto-update | p Palette"
|
|
184
|
-
)
|
|
185
|
-
);
|
|
186
|
-
footerText.setText(theme.fg("dim", buildFooterShortcuts(footerState)));
|
|
187
|
-
|
|
188
|
-
for (const settingsItem of settingsItems) {
|
|
189
|
-
const item = byId.get(settingsItem.id);
|
|
190
|
-
if (!item) continue;
|
|
191
|
-
|
|
192
|
-
if (item.type === "local") {
|
|
193
|
-
const currentState = staged.get(item.id) ?? item.state!;
|
|
194
|
-
const changed = staged.has(item.id) && currentState !== item.originalState;
|
|
195
|
-
settingsItem.label = formatUnifiedItemLabel(item, currentState, theme, changed);
|
|
196
|
-
} else {
|
|
197
|
-
settingsItem.label = formatUnifiedItemLabel(item, "enabled", theme, false);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
syncThemedContent();
|
|
202
|
-
|
|
203
|
-
const settingsList = new SettingsList(
|
|
204
|
-
settingsItems,
|
|
205
|
-
Math.min(items.length + 2, UI.maxListHeight),
|
|
206
|
-
getSettingsListTheme(),
|
|
207
|
-
(id: string, newValue: string) => {
|
|
208
|
-
const item = byId.get(id);
|
|
209
|
-
if (!item || item.type !== "local") return;
|
|
210
|
-
|
|
211
|
-
const state = newValue as State;
|
|
212
|
-
staged.set(id, state);
|
|
213
|
-
|
|
214
|
-
const settingsItem = settingsItems.find((x) => x.id === id);
|
|
215
|
-
if (settingsItem) {
|
|
216
|
-
const changed = state !== item.originalState;
|
|
217
|
-
settingsItem.label = formatUnifiedItemLabel(item, state, theme, changed);
|
|
218
|
-
}
|
|
219
|
-
tui.requestRender();
|
|
220
|
-
},
|
|
221
|
-
() => done({ type: "cancel" })
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
container.addChild(settingsList);
|
|
225
|
-
container.addChild(new Spacer(1));
|
|
226
|
-
|
|
227
|
-
// Footer with keyboard shortcuts
|
|
228
|
-
container.addChild(footerText);
|
|
229
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
render(width: number) {
|
|
233
|
-
return container.render(width);
|
|
234
|
-
},
|
|
235
|
-
invalidate() {
|
|
236
|
-
container.invalidate();
|
|
237
|
-
syncThemedContent();
|
|
238
|
-
},
|
|
239
|
-
handleInput(data: string) {
|
|
240
|
-
const selIdx = getSettingsListSelectedIndex(settingsList) ?? 0;
|
|
241
|
-
const selectedId = settingsItems[selIdx]?.id ?? settingsItems[0]?.id;
|
|
242
|
-
const selectedItem = selectedId ? byId.get(selectedId) : undefined;
|
|
243
|
-
|
|
244
|
-
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
245
|
-
done({ type: "apply" });
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Enter on a package opens its action menu (fewer clicks)
|
|
250
|
-
if (
|
|
251
|
-
(data === "\r" || data === "\n") &&
|
|
252
|
-
selectedId &&
|
|
253
|
-
selectedItem?.type === "package"
|
|
254
|
-
) {
|
|
255
|
-
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (data === "a" || data === "A") {
|
|
260
|
-
if (selectedId) {
|
|
261
|
-
done({ type: "action", itemId: selectedId, action: "menu" });
|
|
262
|
-
}
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Quick actions (global)
|
|
267
|
-
if (data === "i") {
|
|
268
|
-
done({ type: "quick", action: "install" });
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
if (data === "f") {
|
|
272
|
-
done({ type: "quick", action: "search" });
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (data === "U") {
|
|
276
|
-
done({ type: "quick", action: "update-all" });
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (data === "t" || data === "T") {
|
|
280
|
-
done({ type: "quick", action: "auto-update" });
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Fast actions on selected row
|
|
285
|
-
if (selectedId && selectedItem?.type === "package") {
|
|
286
|
-
if (data === "u") {
|
|
287
|
-
done({ type: "action", itemId: selectedId, action: "update" });
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (data === "x" || data === "X") {
|
|
291
|
-
done({ type: "action", itemId: selectedId, action: "remove" });
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (data === "v" || data === "V") {
|
|
295
|
-
done({ type: "action", itemId: selectedId, action: "details" });
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (data === "c" || data === "C") {
|
|
299
|
-
done({ type: "action", itemId: selectedId, action: "configure" });
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
160
|
+
while (true) {
|
|
161
|
+
let nextManagerState = managerState;
|
|
303
162
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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();
|
|
308
240
|
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
if (data === "?" || data === "h" || data === "H") {
|
|
316
|
-
done({ type: "help" });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (data === "m" || data === "M" || data === "p" || data === "P") {
|
|
320
|
-
done({ type: "menu" });
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
settingsList.handleInput?.(data);
|
|
324
|
-
tui.requestRender();
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}),
|
|
328
|
-
"Showing read-only local and installed package lists instead."
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
if (!result) {
|
|
332
|
-
await showInteractiveFallback(ctx, pi);
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}),
|
|
244
|
+
"Showing read-only local and installed package lists instead."
|
|
245
|
+
);
|
|
335
246
|
|
|
336
|
-
|
|
337
|
-
|
|
247
|
+
if (!result) {
|
|
248
|
+
await showInteractiveFallback(ctx, pi);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
338
251
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
252
|
+
const outcome = await handleUnifiedAction(result, items, staged, byId, ctx, pi);
|
|
253
|
+
if (outcome === "resume") {
|
|
254
|
+
managerState = nextManagerState;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
343
257
|
|
|
344
|
-
|
|
258
|
+
return outcome;
|
|
259
|
+
}
|
|
345
260
|
}
|
|
346
261
|
|
|
347
262
|
export function buildUnifiedItems(
|
|
@@ -354,7 +269,8 @@ export function buildUnifiedItems(
|
|
|
354
269
|
|
|
355
270
|
// Add local extensions
|
|
356
271
|
for (const entry of localEntries) {
|
|
357
|
-
|
|
272
|
+
const currentPath = entry.state === "disabled" ? entry.disabledPath : entry.activePath;
|
|
273
|
+
localPaths.add(normalizePathIdentity(currentPath));
|
|
358
274
|
items.push({
|
|
359
275
|
type: "local",
|
|
360
276
|
id: entry.id,
|
|
@@ -369,10 +285,8 @@ export function buildUnifiedItems(
|
|
|
369
285
|
}
|
|
370
286
|
|
|
371
287
|
for (const pkg of installedPackages) {
|
|
372
|
-
const pkgSourceNormalized =
|
|
373
|
-
const pkgResolvedNormalized = pkg.resolvedPath
|
|
374
|
-
? normalizePathForDuplicateCheck(pkg.resolvedPath)
|
|
375
|
-
: "";
|
|
288
|
+
const pkgSourceNormalized = normalizePathIdentity(pkg.source);
|
|
289
|
+
const pkgResolvedNormalized = pkg.resolvedPath ? normalizePathIdentity(pkg.resolvedPath) : "";
|
|
376
290
|
|
|
377
291
|
let isDuplicate = false;
|
|
378
292
|
for (const localPath of localPaths) {
|
|
@@ -380,16 +294,7 @@ export function buildUnifiedItems(
|
|
|
380
294
|
isDuplicate = true;
|
|
381
295
|
break;
|
|
382
296
|
}
|
|
383
|
-
if (
|
|
384
|
-
pkgResolvedNormalized &&
|
|
385
|
-
(localPath.startsWith(`${pkgResolvedNormalized}/`) ||
|
|
386
|
-
pkgResolvedNormalized.startsWith(localPath))
|
|
387
|
-
) {
|
|
388
|
-
isDuplicate = true;
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
const localDir = localPath.split("/").slice(0, -1).join("/");
|
|
392
|
-
if (pkgResolvedNormalized && pkgResolvedNormalized === localDir) {
|
|
297
|
+
if (pkgResolvedNormalized && localPath.startsWith(`${pkgResolvedNormalized}/`)) {
|
|
393
298
|
isDuplicate = true;
|
|
394
299
|
break;
|
|
395
300
|
}
|
|
@@ -400,9 +305,9 @@ export function buildUnifiedItems(
|
|
|
400
305
|
type: "package",
|
|
401
306
|
id: `pkg:${pkg.source}`,
|
|
402
307
|
displayName: pkg.name,
|
|
403
|
-
summary: pkg.description || `${pkg.source} (${pkg.scope})`,
|
|
404
308
|
scope: pkg.scope,
|
|
405
309
|
source: pkg.source,
|
|
310
|
+
resolvedPath: pkg.resolvedPath,
|
|
406
311
|
version: pkg.version,
|
|
407
312
|
description: pkg.description,
|
|
408
313
|
size: pkg.size,
|
|
@@ -425,48 +330,90 @@ export function buildUnifiedItems(
|
|
|
425
330
|
return items;
|
|
426
331
|
}
|
|
427
332
|
|
|
428
|
-
function
|
|
333
|
+
function buildManagerSummary(
|
|
429
334
|
items: UnifiedItem[],
|
|
430
335
|
staged: Map<string, State>,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
+
}
|
|
444
367
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
};
|
|
451
|
-
}
|
|
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;
|
|
452
400
|
}
|
|
453
401
|
|
|
454
402
|
function formatUnifiedItemLabel(
|
|
455
403
|
item: UnifiedItem,
|
|
456
|
-
state: State,
|
|
404
|
+
state: State | undefined,
|
|
457
405
|
theme: Theme,
|
|
458
406
|
changed = false
|
|
459
407
|
): string {
|
|
460
408
|
if (item.type === "local") {
|
|
461
|
-
const statusIcon = getStatusIcon(theme, state
|
|
409
|
+
const statusIcon = getStatusIcon(theme, state ?? item.state);
|
|
462
410
|
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
463
411
|
const changeMarker = getChangeMarker(theme, changed);
|
|
464
412
|
const name = theme.bold(item.displayName);
|
|
465
|
-
|
|
466
|
-
return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
413
|
+
return `${statusIcon} [${scopeIcon}] ${name}${changeMarker}`;
|
|
467
414
|
}
|
|
468
415
|
|
|
469
|
-
const sourceKind = getPackageSourceKind(item.source
|
|
416
|
+
const sourceKind = getPackageSourceKind(item.source);
|
|
470
417
|
const pkgIcon = getPackageIcon(
|
|
471
418
|
theme,
|
|
472
419
|
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
@@ -474,34 +421,635 @@ function formatUnifiedItemLabel(
|
|
|
474
421
|
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
475
422
|
const name = theme.bold(item.displayName);
|
|
476
423
|
const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
|
|
424
|
+
const size = item.size !== undefined ? theme.fg("dim", ` • ${formatBytes(item.size)}`) : "";
|
|
477
425
|
const updateBadge = item.updateAvailable ? ` ${theme.fg("warning", "[update]")}` : "";
|
|
478
426
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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 "~";
|
|
492
472
|
}
|
|
493
473
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
infoParts.push(formatSize(theme, item.size));
|
|
474
|
+
if (normalizedPath.startsWith(`${normalizedHome}/`)) {
|
|
475
|
+
return `~/${normalizedPath.slice(normalizedHome.length + 1)}`;
|
|
497
476
|
}
|
|
498
477
|
|
|
499
|
-
const
|
|
500
|
-
|
|
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;
|
|
501
489
|
}
|
|
502
490
|
|
|
503
|
-
function
|
|
504
|
-
return
|
|
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);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
private setFilter(filter: UnifiedFilter): void {
|
|
1003
|
+
this.filter = filter;
|
|
1004
|
+
this.refreshVisibleItems();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
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
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function getToggleItemsForApply(items: UnifiedItem[]): LocalUnifiedItem[] {
|
|
1052
|
+
return items.filter((item): item is LocalUnifiedItem => item.type === "local");
|
|
505
1053
|
}
|
|
506
1054
|
|
|
507
1055
|
async function applyToggleChangesFromManager(
|
|
@@ -510,7 +1058,7 @@ async function applyToggleChangesFromManager(
|
|
|
510
1058
|
ctx: ExtensionCommandContext,
|
|
511
1059
|
pi: ExtensionAPI,
|
|
512
1060
|
options?: { promptReload?: boolean }
|
|
513
|
-
): Promise<{ changed: number; reloaded: boolean }> {
|
|
1061
|
+
): Promise<{ changed: number; reloaded: boolean; hasErrors: boolean }> {
|
|
514
1062
|
const toggleItems = getToggleItemsForApply(items);
|
|
515
1063
|
const apply = await applyStagedChanges(toggleItems, staged, pi);
|
|
516
1064
|
|
|
@@ -529,24 +1077,14 @@ async function applyToggleChangesFromManager(
|
|
|
529
1077
|
const shouldPromptReload = options?.promptReload ?? true;
|
|
530
1078
|
|
|
531
1079
|
if (shouldPromptReload) {
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
"Local extensions changed. Reload pi now?"
|
|
535
|
-
);
|
|
536
|
-
|
|
537
|
-
if (shouldReload) {
|
|
538
|
-
await ctx.reload();
|
|
539
|
-
return { changed: apply.changed, reloaded: true };
|
|
540
|
-
}
|
|
541
|
-
} else {
|
|
542
|
-
ctx.ui.notify(
|
|
543
|
-
"Changes saved. Reload pi later to fully apply extension state updates.",
|
|
544
|
-
"info"
|
|
545
|
-
);
|
|
1080
|
+
const reloaded = await confirmReload(ctx, "Local extensions changed.");
|
|
1081
|
+
return { changed: apply.changed, reloaded, hasErrors: apply.errors.length > 0 };
|
|
546
1082
|
}
|
|
1083
|
+
|
|
1084
|
+
ctx.ui.notify("Changes saved. Reload pi later to fully apply extension state updates.", "info");
|
|
547
1085
|
}
|
|
548
1086
|
|
|
549
|
-
return { changed: apply.changed, reloaded: false };
|
|
1087
|
+
return { changed: apply.changed, reloaded: false, hasErrors: apply.errors.length > 0 };
|
|
550
1088
|
}
|
|
551
1089
|
|
|
552
1090
|
async function resolvePendingChangesBeforeLeave(
|
|
@@ -556,7 +1094,7 @@ async function resolvePendingChangesBeforeLeave(
|
|
|
556
1094
|
ctx: ExtensionCommandContext,
|
|
557
1095
|
pi: ExtensionAPI,
|
|
558
1096
|
destinationLabel: string
|
|
559
|
-
): Promise<"continue" | "stay"
|
|
1097
|
+
): Promise<"continue" | "stay"> {
|
|
560
1098
|
const pendingCount = getPendingToggleChangeCount(staged, byId);
|
|
561
1099
|
if (pendingCount === 0) return "continue";
|
|
562
1100
|
|
|
@@ -571,13 +1109,14 @@ async function resolvePendingChangesBeforeLeave(
|
|
|
571
1109
|
}
|
|
572
1110
|
|
|
573
1111
|
if (choice === "Discard changes") {
|
|
1112
|
+
staged.clear();
|
|
574
1113
|
return "continue";
|
|
575
1114
|
}
|
|
576
1115
|
|
|
577
|
-
const
|
|
1116
|
+
const apply = await applyToggleChangesFromManager(items, staged, ctx, pi, {
|
|
578
1117
|
promptReload: false,
|
|
579
1118
|
});
|
|
580
|
-
return
|
|
1119
|
+
return apply.changed === 0 && apply.hasErrors ? "stay" : "continue";
|
|
581
1120
|
}
|
|
582
1121
|
|
|
583
1122
|
const PALETTE_OPTIONS = {
|
|
@@ -603,6 +1142,12 @@ const QUICK_DESTINATION_LABELS: Record<QuickDestination, string> = {
|
|
|
603
1142
|
help: "Help",
|
|
604
1143
|
};
|
|
605
1144
|
|
|
1145
|
+
const LOCAL_ACTION_OPTIONS = {
|
|
1146
|
+
details: "View details",
|
|
1147
|
+
remove: "Remove local extension",
|
|
1148
|
+
back: "Back to manager",
|
|
1149
|
+
} as const;
|
|
1150
|
+
|
|
606
1151
|
const PACKAGE_ACTION_OPTIONS = {
|
|
607
1152
|
configure: "Configure extensions",
|
|
608
1153
|
update: "Update package",
|
|
@@ -611,10 +1156,28 @@ const PACKAGE_ACTION_OPTIONS = {
|
|
|
611
1156
|
back: "Back to manager",
|
|
612
1157
|
} as const;
|
|
613
1158
|
|
|
1159
|
+
type LocalActionKey = keyof typeof LOCAL_ACTION_OPTIONS;
|
|
614
1160
|
type PackageActionKey = keyof typeof PACKAGE_ACTION_OPTIONS;
|
|
615
1161
|
|
|
1162
|
+
type LocalActionSelection = Exclude<LocalActionKey, "back"> | "cancel";
|
|
616
1163
|
type PackageActionSelection = Exclude<PackageActionKey, "back"> | "cancel";
|
|
617
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
|
+
|
|
618
1181
|
async function promptPackageActionSelection(
|
|
619
1182
|
pkg: InstalledPackage,
|
|
620
1183
|
ctx: ExtensionCommandContext
|
|
@@ -631,6 +1194,27 @@ async function promptPackageActionSelection(
|
|
|
631
1194
|
return selection;
|
|
632
1195
|
}
|
|
633
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
|
+
|
|
634
1218
|
async function navigateWithPendingGuard(
|
|
635
1219
|
destination: QuickDestination,
|
|
636
1220
|
items: UnifiedItem[],
|
|
@@ -638,7 +1222,7 @@ async function navigateWithPendingGuard(
|
|
|
638
1222
|
byId: Map<string, UnifiedItem>,
|
|
639
1223
|
ctx: ExtensionCommandContext,
|
|
640
1224
|
pi: ExtensionAPI
|
|
641
|
-
): Promise<"
|
|
1225
|
+
): Promise<"reload" | "resume" | "stay" | "exit"> {
|
|
642
1226
|
const pending = await resolvePendingChangesBeforeLeave(
|
|
643
1227
|
items,
|
|
644
1228
|
staged,
|
|
@@ -648,21 +1232,20 @@ async function navigateWithPendingGuard(
|
|
|
648
1232
|
QUICK_DESTINATION_LABELS[destination]
|
|
649
1233
|
);
|
|
650
1234
|
if (pending === "stay") return "stay";
|
|
651
|
-
if (pending === "exit") return "exit";
|
|
652
1235
|
|
|
653
1236
|
switch (destination) {
|
|
654
1237
|
case "install":
|
|
655
1238
|
await showRemote("install", ctx, pi);
|
|
656
|
-
return "
|
|
1239
|
+
return "reload";
|
|
657
1240
|
case "search":
|
|
658
1241
|
await showRemote("search", ctx, pi);
|
|
659
|
-
return "
|
|
1242
|
+
return "reload";
|
|
660
1243
|
case "browse":
|
|
661
1244
|
await showRemote("", ctx, pi);
|
|
662
|
-
return "
|
|
1245
|
+
return "reload";
|
|
663
1246
|
case "update-all": {
|
|
664
1247
|
const outcome = await updatePackagesWithOutcome(ctx, pi);
|
|
665
|
-
return outcome.reloaded ? "exit" : "
|
|
1248
|
+
return outcome.reloaded ? "exit" : "reload";
|
|
666
1249
|
}
|
|
667
1250
|
case "auto-update":
|
|
668
1251
|
await promptAutoUpdateWizard(pi, ctx, (packages) => {
|
|
@@ -672,10 +1255,10 @@ async function navigateWithPendingGuard(
|
|
|
672
1255
|
);
|
|
673
1256
|
});
|
|
674
1257
|
void updateExtmgrStatus(ctx, pi);
|
|
675
|
-
return "
|
|
1258
|
+
return "resume";
|
|
676
1259
|
case "help":
|
|
677
1260
|
showHelp(ctx);
|
|
678
|
-
return "
|
|
1261
|
+
return "resume";
|
|
679
1262
|
}
|
|
680
1263
|
}
|
|
681
1264
|
|
|
@@ -686,7 +1269,7 @@ async function handleUnifiedAction(
|
|
|
686
1269
|
byId: Map<string, UnifiedItem>,
|
|
687
1270
|
ctx: ExtensionCommandContext,
|
|
688
1271
|
pi: ExtensionAPI
|
|
689
|
-
): Promise<boolean> {
|
|
1272
|
+
): Promise<boolean | "resume"> {
|
|
690
1273
|
if (result.type === "cancel") {
|
|
691
1274
|
const pendingCount = getPendingToggleChangeCount(staged, byId);
|
|
692
1275
|
if (pendingCount > 0) {
|
|
@@ -697,12 +1280,13 @@ async function handleUnifiedAction(
|
|
|
697
1280
|
]);
|
|
698
1281
|
|
|
699
1282
|
if (!choice || choice === "Stay in manager") {
|
|
700
|
-
return
|
|
1283
|
+
return "resume";
|
|
701
1284
|
}
|
|
702
1285
|
|
|
703
1286
|
if (choice === "Save and exit") {
|
|
704
1287
|
const apply = await applyToggleChangesFromManager(items, staged, ctx, pi);
|
|
705
1288
|
if (apply.reloaded) return true;
|
|
1289
|
+
if (apply.changed === 0 && apply.hasErrors) return "resume";
|
|
706
1290
|
}
|
|
707
1291
|
}
|
|
708
1292
|
|
|
@@ -711,8 +1295,7 @@ async function handleUnifiedAction(
|
|
|
711
1295
|
|
|
712
1296
|
if (result.type === "remote") {
|
|
713
1297
|
const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Remote");
|
|
714
|
-
if (pending === "stay") return
|
|
715
|
-
if (pending === "exit") return true;
|
|
1298
|
+
if (pending === "stay") return "resume";
|
|
716
1299
|
|
|
717
1300
|
await showRemote("", ctx, pi);
|
|
718
1301
|
return false;
|
|
@@ -720,11 +1303,10 @@ async function handleUnifiedAction(
|
|
|
720
1303
|
|
|
721
1304
|
if (result.type === "help") {
|
|
722
1305
|
const pending = await resolvePendingChangesBeforeLeave(items, staged, byId, ctx, pi, "Help");
|
|
723
|
-
if (pending === "stay") return
|
|
724
|
-
if (pending === "exit") return true;
|
|
1306
|
+
if (pending === "stay") return "resume";
|
|
725
1307
|
|
|
726
1308
|
showHelp(ctx);
|
|
727
|
-
return
|
|
1309
|
+
return "resume";
|
|
728
1310
|
}
|
|
729
1311
|
|
|
730
1312
|
if (result.type === "menu") {
|
|
@@ -744,10 +1326,11 @@ async function handleUnifiedAction(
|
|
|
744
1326
|
|
|
745
1327
|
const destination = choice ? destinationByAction[choice] : undefined;
|
|
746
1328
|
if (!destination) {
|
|
747
|
-
return
|
|
1329
|
+
return "resume";
|
|
748
1330
|
}
|
|
749
1331
|
|
|
750
1332
|
const outcome = await navigateWithPendingGuard(destination, items, staged, byId, ctx, pi);
|
|
1333
|
+
if (outcome === "stay" || outcome === "resume") return "resume";
|
|
751
1334
|
return outcome === "exit";
|
|
752
1335
|
}
|
|
753
1336
|
|
|
@@ -761,6 +1344,7 @@ async function handleUnifiedAction(
|
|
|
761
1344
|
|
|
762
1345
|
const destination = quickDestinationMap[result.action];
|
|
763
1346
|
const outcome = await navigateWithPendingGuard(destination, items, staged, byId, ctx, pi);
|
|
1347
|
+
if (outcome === "stay" || outcome === "resume") return "resume";
|
|
764
1348
|
return outcome === "exit";
|
|
765
1349
|
}
|
|
766
1350
|
|
|
@@ -768,35 +1352,49 @@ async function handleUnifiedAction(
|
|
|
768
1352
|
const item = byId.get(result.itemId);
|
|
769
1353
|
if (!item) return false;
|
|
770
1354
|
|
|
771
|
-
const pendingDestination = item.type === "local" ? "remove extension" : "package actions";
|
|
772
|
-
const pending = await resolvePendingChangesBeforeLeave(
|
|
773
|
-
items,
|
|
774
|
-
staged,
|
|
775
|
-
byId,
|
|
776
|
-
ctx,
|
|
777
|
-
pi,
|
|
778
|
-
pendingDestination
|
|
779
|
-
);
|
|
780
|
-
if (pending === "stay") return false;
|
|
781
|
-
if (pending === "exit") return true;
|
|
782
|
-
|
|
783
1355
|
if (item.type === "local") {
|
|
784
|
-
|
|
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";
|
|
785
1383
|
|
|
786
1384
|
const confirmed = await ctx.ui.confirm(
|
|
787
1385
|
"Delete Local Extension",
|
|
788
1386
|
`Delete ${item.displayName} from disk?\n\nThis cannot be undone.`
|
|
789
1387
|
);
|
|
790
|
-
if (!confirmed) return
|
|
1388
|
+
if (!confirmed) return "resume";
|
|
791
1389
|
|
|
792
1390
|
const removal = await removeLocalExtension(
|
|
793
|
-
{ activePath: item.activePath
|
|
1391
|
+
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
794
1392
|
ctx.cwd
|
|
795
1393
|
);
|
|
796
1394
|
if (!removal.ok) {
|
|
797
1395
|
logExtensionDelete(pi, item.id, false, removal.error);
|
|
798
1396
|
ctx.ui.notify(`Failed to remove extension: ${removal.error}`, "error");
|
|
799
|
-
return
|
|
1397
|
+
return "resume";
|
|
800
1398
|
}
|
|
801
1399
|
|
|
802
1400
|
logExtensionDelete(pi, item.id, true);
|
|
@@ -805,19 +1403,15 @@ async function handleUnifiedAction(
|
|
|
805
1403
|
"info"
|
|
806
1404
|
);
|
|
807
1405
|
|
|
808
|
-
|
|
809
|
-
if (reloaded) {
|
|
810
|
-
return true;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
return false;
|
|
1406
|
+
return await confirmReload(ctx, "Extension removed.");
|
|
814
1407
|
}
|
|
815
1408
|
|
|
816
1409
|
const pkg: InstalledPackage = {
|
|
817
|
-
source: item.source
|
|
1410
|
+
source: item.source,
|
|
818
1411
|
name: item.displayName,
|
|
819
1412
|
...(item.version ? { version: item.version } : {}),
|
|
820
1413
|
scope: item.scope,
|
|
1414
|
+
...(item.resolvedPath ? { resolvedPath: item.resolvedPath } : {}),
|
|
821
1415
|
...(item.description ? { description: item.description } : {}),
|
|
822
1416
|
...(item.size !== undefined ? { size: item.size } : {}),
|
|
823
1417
|
};
|
|
@@ -828,9 +1422,30 @@ async function handleUnifiedAction(
|
|
|
828
1422
|
: result.action;
|
|
829
1423
|
|
|
830
1424
|
if (selection === "cancel") {
|
|
831
|
-
return
|
|
1425
|
+
return "resume";
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (selection === "details") {
|
|
1429
|
+
showUnifiedItemDetails(item, ctx);
|
|
1430
|
+
return "resume";
|
|
832
1431
|
}
|
|
833
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
|
+
|
|
834
1449
|
switch (selection) {
|
|
835
1450
|
case "configure": {
|
|
836
1451
|
const outcome = await configurePackageExtensions(pkg, ctx, pi);
|
|
@@ -844,23 +1459,15 @@ async function handleUnifiedAction(
|
|
|
844
1459
|
const outcome = await removePackageWithOutcome(pkg.source, ctx, pi);
|
|
845
1460
|
return outcome.reloaded;
|
|
846
1461
|
}
|
|
847
|
-
case "details": {
|
|
848
|
-
const sizeStr = pkg.size !== undefined ? `\nSize: ${formatBytes(pkg.size)}` : "";
|
|
849
|
-
ctx.ui.notify(
|
|
850
|
-
`Name: ${pkg.name}\nVersion: ${pkg.version || "unknown"}\nSource: ${pkg.source}\nScope: ${pkg.scope}${sizeStr}${pkg.description ? `\nDescription: ${pkg.description}` : ""}`,
|
|
851
|
-
"info"
|
|
852
|
-
);
|
|
853
|
-
return false;
|
|
854
|
-
}
|
|
855
1462
|
}
|
|
856
1463
|
}
|
|
857
1464
|
|
|
858
1465
|
const apply = await applyToggleChangesFromManager(items, staged, ctx, pi);
|
|
859
|
-
return apply.reloaded;
|
|
1466
|
+
return apply.reloaded ? true : "resume";
|
|
860
1467
|
}
|
|
861
1468
|
|
|
862
1469
|
async function applyStagedChanges(
|
|
863
|
-
items:
|
|
1470
|
+
items: LocalUnifiedItem[],
|
|
864
1471
|
staged: Map<string, State>,
|
|
865
1472
|
pi: ExtensionAPI
|
|
866
1473
|
) {
|
|
@@ -868,13 +1475,10 @@ async function applyStagedChanges(
|
|
|
868
1475
|
const errors: string[] = [];
|
|
869
1476
|
|
|
870
1477
|
for (const item of items) {
|
|
871
|
-
if (item.type !== "local" || !item.originalState || !item.activePath || !item.disabledPath) {
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
1478
|
const target = staged.get(item.id) ?? item.originalState;
|
|
876
1479
|
if (target === item.originalState) continue;
|
|
877
1480
|
|
|
1481
|
+
const fromState = item.originalState;
|
|
878
1482
|
const result = await setExtensionState(
|
|
879
1483
|
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
880
1484
|
target
|
|
@@ -882,10 +1486,13 @@ async function applyStagedChanges(
|
|
|
882
1486
|
|
|
883
1487
|
if (result.ok) {
|
|
884
1488
|
changed++;
|
|
885
|
-
|
|
1489
|
+
item.state = target;
|
|
1490
|
+
item.originalState = target;
|
|
1491
|
+
staged.delete(item.id);
|
|
1492
|
+
logExtensionToggle(pi, item.id, fromState, target, true);
|
|
886
1493
|
} else {
|
|
887
1494
|
errors.push(`${item.id}: ${result.error}`);
|
|
888
|
-
logExtensionToggle(pi, item.id,
|
|
1495
|
+
logExtensionToggle(pi, item.id, fromState, target, false, result.error);
|
|
889
1496
|
}
|
|
890
1497
|
}
|
|
891
1498
|
|
|
@@ -913,23 +1520,9 @@ export async function showInstalledPackagesLegacy(
|
|
|
913
1520
|
export async function showListOnly(ctx: ExtensionCommandContext): Promise<void> {
|
|
914
1521
|
const entries = await discoverExtensions(ctx.cwd);
|
|
915
1522
|
if (entries.length === 0) {
|
|
916
|
-
|
|
917
|
-
if (ctx.hasUI) {
|
|
918
|
-
ctx.ui.notify(msg, "info");
|
|
919
|
-
} else {
|
|
920
|
-
console.log(msg);
|
|
921
|
-
}
|
|
1523
|
+
notify(ctx, "No extensions found in ~/.pi/agent/extensions or .pi/extensions", "info");
|
|
922
1524
|
return;
|
|
923
1525
|
}
|
|
924
1526
|
|
|
925
|
-
|
|
926
|
-
const output = lines.join("\n");
|
|
927
|
-
const titledOutput = `Local extensions:\n${output}`;
|
|
928
|
-
|
|
929
|
-
if (ctx.hasUI) {
|
|
930
|
-
ctx.ui.notify(titledOutput, "info");
|
|
931
|
-
} else {
|
|
932
|
-
console.log("Local extensions:");
|
|
933
|
-
console.log(output);
|
|
934
|
-
}
|
|
1527
|
+
formatListOutput(ctx, "Local extensions", entries.map(formatExtEntry));
|
|
935
1528
|
}
|