@nklisch/pi-enhanced 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/node_modules/@nklisch/pi-astral-pocket/LICENSE +3 -0
- package/node_modules/@nklisch/pi-astral-pocket/README.md +82 -0
- package/node_modules/@nklisch/pi-astral-pocket/package.json +52 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/activation.ts +44 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/config.ts +78 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/distiller.ts +162 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/guidance.ts +59 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/index.ts +127 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/sessions.ts +225 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/store.ts +227 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/tools.ts +112 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.darwin-arm64.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.darwin-x64.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-arm64-gnu.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-conveniences/extensions/agents-context.ts +4 -6
- package/node_modules/@nklisch/pi-conveniences/extensions/context-window-footer.ts +38 -22
- package/node_modules/@nklisch/pi-conveniences/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/README.md +12 -6
- package/node_modules/@nklisch/pi-plugins/dist/catalog.js +11 -0
- package/node_modules/@nklisch/pi-plugins/dist/catalog.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/host.js +85 -29
- package/node_modules/@nklisch/pi-plugins/dist/host.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-layout.d.ts +4 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-layout.js +24 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-layout.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +6 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +130 -47
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/plugin-metadata.d.ts +9 -0
- package/node_modules/@nklisch/pi-plugins/dist/plugin-metadata.js +37 -0
- package/node_modules/@nklisch/pi-plugins/dist/plugin-metadata.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/types.d.ts +4 -1
- package/node_modules/@nklisch/pi-plugins/dist/types.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/package.json +1 -1
- package/package.json +4 -1
|
@@ -46,6 +46,8 @@ export declare class PluginManager implements Component, Focusable {
|
|
|
46
46
|
private checking;
|
|
47
47
|
private updatesChecked;
|
|
48
48
|
private checkedMarketplaces;
|
|
49
|
+
private checkTotal;
|
|
50
|
+
private checkErrors;
|
|
49
51
|
private checkingMarketplaces;
|
|
50
52
|
private checkController;
|
|
51
53
|
private checkPromise;
|
|
@@ -62,6 +64,10 @@ export declare class PluginManager implements Component, Focusable {
|
|
|
62
64
|
private batchItemActive;
|
|
63
65
|
private destroyed;
|
|
64
66
|
private completed;
|
|
67
|
+
private bodyScroll;
|
|
68
|
+
private bodyFocusLine;
|
|
69
|
+
private bodyHeight;
|
|
70
|
+
private followFocus;
|
|
65
71
|
constructor(options: PluginManagerOptions);
|
|
66
72
|
get focused(): boolean;
|
|
67
73
|
set focused(value: boolean);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { DEFAULT_REFRESH_TIMEOUT_MS } from "../host.js";
|
|
3
|
+
import { installedPluginVersion } from "../plugin-metadata.js";
|
|
4
|
+
import { framePluginManager, padManagerLine } from "./plugin-manager-layout.js";
|
|
3
5
|
import { filterPluginRows, pluginIdentity, pluginManagerKeyAction, pluginManagerMarketplaceRows, projectInstalledPluginDetail, prunePluginSelection, restorePluginCursor, } from "./plugin-manager-model.js";
|
|
4
6
|
const TAB_ORDER = ["installed", "discover", "marketplaces", "issues"];
|
|
5
7
|
const BATCH_ACTIONS = ["install", "update", "enable", "disable", "remove"];
|
|
@@ -27,10 +29,6 @@ function wrapText(text, width) {
|
|
|
27
29
|
function catalogEntry(catalog, name) {
|
|
28
30
|
return catalog?.plugins.find((item) => item.name === name);
|
|
29
31
|
}
|
|
30
|
-
function receiptVersion(plugin) {
|
|
31
|
-
const value = plugin?.receipt?.version;
|
|
32
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
33
|
-
}
|
|
34
32
|
function identityOf(plugin) {
|
|
35
33
|
return pluginIdentity(plugin.name, plugin.marketplace);
|
|
36
34
|
}
|
|
@@ -76,6 +74,8 @@ export class PluginManager {
|
|
|
76
74
|
checking = false;
|
|
77
75
|
updatesChecked = false;
|
|
78
76
|
checkedMarketplaces = 0;
|
|
77
|
+
checkTotal = 0;
|
|
78
|
+
checkErrors = new Map();
|
|
79
79
|
checkingMarketplaces = new Set();
|
|
80
80
|
checkController;
|
|
81
81
|
checkPromise;
|
|
@@ -92,6 +92,10 @@ export class PluginManager {
|
|
|
92
92
|
batchItemActive = false;
|
|
93
93
|
destroyed = false;
|
|
94
94
|
completed = false;
|
|
95
|
+
bodyScroll = 0;
|
|
96
|
+
bodyFocusLine = 0;
|
|
97
|
+
bodyHeight = 20;
|
|
98
|
+
followFocus = true;
|
|
95
99
|
constructor(options) {
|
|
96
100
|
this.host = options.host;
|
|
97
101
|
this.tui = options.tui;
|
|
@@ -115,35 +119,61 @@ export class PluginManager {
|
|
|
115
119
|
this.dialogInput.focused = value && this.dialog?.kind === "input";
|
|
116
120
|
}
|
|
117
121
|
render(width) {
|
|
118
|
-
const renderWidth = Math.max(1, width);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
const renderWidth = Math.max(1, width < 4 ? width : width - 4);
|
|
123
|
+
// Reserve shell, tabs, help/status and Pi's own footer. Keep the selected
|
|
124
|
+
// row reachable rather than letting long catalogs push it off screen.
|
|
125
|
+
const terminalRows = this.tui.terminal?.rows ?? 40;
|
|
126
|
+
this.bodyHeight = Math.max(1, terminalRows - 9);
|
|
127
|
+
this.bodyFocusLine = 0;
|
|
128
|
+
let body;
|
|
122
129
|
if (this.dialog !== undefined)
|
|
123
|
-
|
|
130
|
+
body = this.renderDialog(renderWidth);
|
|
124
131
|
else if (this.view === "detail")
|
|
125
|
-
|
|
132
|
+
body = this.renderDetail(renderWidth);
|
|
126
133
|
else if (this.view === "confirm")
|
|
127
|
-
|
|
134
|
+
body = this.renderConfirmation(renderWidth);
|
|
128
135
|
else if (this.view === "batch")
|
|
129
|
-
|
|
136
|
+
body = this.renderBatch(renderWidth);
|
|
130
137
|
else if (this.tab === "installed")
|
|
131
|
-
|
|
138
|
+
body = this.renderInstalled(renderWidth);
|
|
132
139
|
else if (this.tab === "discover")
|
|
133
|
-
|
|
140
|
+
body = this.renderDiscover(renderWidth);
|
|
134
141
|
else if (this.tab === "marketplaces")
|
|
135
|
-
|
|
142
|
+
body = this.renderMarketplaces(renderWidth);
|
|
136
143
|
else
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
body = this.renderIssues(renderWidth);
|
|
145
|
+
if (this.searchFocused)
|
|
146
|
+
this.bodyFocusLine = 1;
|
|
147
|
+
if (this.followFocus) {
|
|
148
|
+
if (this.bodyFocusLine < this.bodyScroll)
|
|
149
|
+
this.bodyScroll = this.bodyFocusLine;
|
|
150
|
+
if (this.bodyFocusLine >= this.bodyScroll + this.bodyHeight)
|
|
151
|
+
this.bodyScroll = this.bodyFocusLine - this.bodyHeight + 1;
|
|
152
|
+
}
|
|
153
|
+
this.bodyScroll = Math.max(0, Math.min(this.bodyScroll, body.length - this.bodyHeight));
|
|
154
|
+
const hiddenBelow = Math.max(0, body.length - this.bodyScroll - this.bodyHeight);
|
|
155
|
+
const scrollHint = body.length > this.bodyHeight ? `↑ ${this.bodyScroll} above · ↓ ${hiddenBelow} below · PgUp/PgDn scroll` : "";
|
|
156
|
+
const lines = [
|
|
157
|
+
this.renderTabs(renderWidth),
|
|
158
|
+
this.theme.fg("borderMuted", "─".repeat(renderWidth)),
|
|
159
|
+
...body.slice(this.bodyScroll, this.bodyScroll + this.bodyHeight),
|
|
160
|
+
this.theme.fg("muted", scrollHint),
|
|
161
|
+
this.renderFooter(renderWidth),
|
|
162
|
+
];
|
|
140
163
|
if (this.toast !== undefined)
|
|
141
|
-
lines.push(this.theme.fg("accent",
|
|
142
|
-
return lines
|
|
164
|
+
lines.push(this.theme.fg("accent", this.toast));
|
|
165
|
+
return framePluginManager(lines, width, this.theme);
|
|
143
166
|
}
|
|
144
167
|
handleInput(data) {
|
|
145
168
|
if (this.destroyed)
|
|
146
169
|
return;
|
|
170
|
+
if (!this.searchFocused && this.dialog?.kind !== "input" && (matchesKey(data, Key.pageUp) || matchesKey(data, Key.pageDown))) {
|
|
171
|
+
this.bodyScroll += (matchesKey(data, Key.pageUp) ? -1 : 1) * this.bodyHeight;
|
|
172
|
+
this.followFocus = false;
|
|
173
|
+
this.requestRender();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.followFocus = true;
|
|
147
177
|
if (this.dialog !== undefined) {
|
|
148
178
|
this.handleDialogInput(data);
|
|
149
179
|
return;
|
|
@@ -203,6 +233,7 @@ export class PluginManager {
|
|
|
203
233
|
}
|
|
204
234
|
invalidate() {
|
|
205
235
|
this.searchInput.invalidate();
|
|
236
|
+
this.dialogInput.invalidate();
|
|
206
237
|
}
|
|
207
238
|
dispose() {
|
|
208
239
|
this.destroyed = true;
|
|
@@ -233,8 +264,10 @@ export class PluginManager {
|
|
|
233
264
|
return tab === this.tab ? this.theme.bg("selectedBg", this.theme.fg("text", text)) : this.theme.fg("muted", text);
|
|
234
265
|
}).join(" ");
|
|
235
266
|
const status = this.statusText(width);
|
|
267
|
+
if (visibleWidth(rendered) > width)
|
|
268
|
+
return this.theme.fg("accent", truncateToWidth(`‹ ${labels[this.tab]} › ${TAB_ORDER.indexOf(this.tab) + 1}/4`, width, "…"));
|
|
236
269
|
if (width < 80)
|
|
237
|
-
return
|
|
270
|
+
return rendered;
|
|
238
271
|
const gap = Math.max(1, width - visibleWidth(rendered) - visibleWidth(status));
|
|
239
272
|
return truncateToWidth(rendered + " ".repeat(gap) + status, width, "");
|
|
240
273
|
}
|
|
@@ -242,11 +275,13 @@ export class PluginManager {
|
|
|
242
275
|
if (width < 80)
|
|
243
276
|
return "";
|
|
244
277
|
if (this.checking)
|
|
245
|
-
return this.theme.fg("accent", `◌ Checking ${this.checkedMarketplaces}/${this.
|
|
278
|
+
return this.theme.fg("accent", `◌ Checking ${this.checkedMarketplaces}/${this.checkTotal} marketplaces · manager remains available`);
|
|
246
279
|
if (this.reloadNeeded)
|
|
247
280
|
return this.theme.fg("warning", "● Reload needed");
|
|
281
|
+
if (this.checkErrors.size > 0)
|
|
282
|
+
return this.theme.fg("warning", `Local data · ${this.checkErrors.size} check${this.checkErrors.size === 1 ? "" : "s"} failed`);
|
|
248
283
|
if (this.updatesChecked)
|
|
249
|
-
return this.theme.fg("muted", "Local data ·
|
|
284
|
+
return this.theme.fg("muted", "Local data · checked this session");
|
|
250
285
|
return this.theme.fg("muted", "Local data · updates not checked");
|
|
251
286
|
}
|
|
252
287
|
renderInstalled(width) {
|
|
@@ -273,6 +308,8 @@ export class PluginManager {
|
|
|
273
308
|
const lines = [this.heading("Manage marketplaces")];
|
|
274
309
|
const rows = pluginManagerMarketplaceRows(this.marketplaces, this.checkOnOpen);
|
|
275
310
|
for (const [index, row] of rows.entries()) {
|
|
311
|
+
if (this.marketplaceCursor === index)
|
|
312
|
+
this.bodyFocusLine = lines.length;
|
|
276
313
|
if (row.kind === "marketplace") {
|
|
277
314
|
const marketplace = row.marketplace;
|
|
278
315
|
const status = this.marketplaceStatus(marketplace.name);
|
|
@@ -303,6 +340,8 @@ export class PluginManager {
|
|
|
303
340
|
const issue = this.issues[index];
|
|
304
341
|
const selected = index === this.cursor;
|
|
305
342
|
const color = issue.severity === "error" ? "error" : "warning";
|
|
343
|
+
if (selected)
|
|
344
|
+
this.bodyFocusLine = lines.length;
|
|
306
345
|
lines.push(this.renderCursorLine(selected, this.theme.fg(color, `${issue.severity === "error" ? "×" : "△"} ${issue.title}`)));
|
|
307
346
|
lines.push(this.indent(this.theme.fg("muted", issue.message), width));
|
|
308
347
|
}
|
|
@@ -321,8 +360,8 @@ export class PluginManager {
|
|
|
321
360
|
this.theme.fg("accent", `${this.tab === "discover" ? "Discover" : "Installed"} / Plugin details`),
|
|
322
361
|
this.heading(`${row.name} @ ${row.marketplace}`),
|
|
323
362
|
this.fact("Version", row.availableVersion === undefined
|
|
324
|
-
?
|
|
325
|
-
: `${
|
|
363
|
+
? installedPluginVersion(installed) ?? row.version ?? "unavailable"
|
|
364
|
+
: `${installedPluginVersion(installed) ?? "version unavailable"} → ${row.availableVersion}`),
|
|
326
365
|
this.fact("Status", installed === undefined ? "not installed" : this.statusLabel(installed, entry)),
|
|
327
366
|
this.fact("Source", entry?.source.kind === "local"
|
|
328
367
|
? entry.source.path
|
|
@@ -348,7 +387,7 @@ export class PluginManager {
|
|
|
348
387
|
lines.push("");
|
|
349
388
|
const marker = installed.autoUpdate ? this.theme.fg("success", "[on]") : this.theme.fg("muted", "[off]");
|
|
350
389
|
lines.push(`${marker} ${this.theme.bold("Update automatically on Pi startup")}`);
|
|
351
|
-
lines.push(this.indent(this.theme.fg("muted", "Enabling grants standing authorization to replace executable plugin content when its declared
|
|
390
|
+
lines.push(this.indent(this.theme.fg("muted", "Enabling grants standing authorization to replace executable plugin content when its declared release version changes."), width));
|
|
352
391
|
lines.push("");
|
|
353
392
|
}
|
|
354
393
|
lines.push(this.theme.fg("accent", "Actions"));
|
|
@@ -356,6 +395,8 @@ export class PluginManager {
|
|
|
356
395
|
for (let index = 0; index < actions.length; index++) {
|
|
357
396
|
const action = actions[index];
|
|
358
397
|
const text = action.label;
|
|
398
|
+
if (index === this.detailActionCursor)
|
|
399
|
+
this.bodyFocusLine = lines.length;
|
|
359
400
|
lines.push(this.renderCursorLine(index === this.detailActionCursor, action.destructive ? this.theme.fg("error", text) : index === 0 ? this.theme.fg("accent", text) : this.theme.fg("text", text)));
|
|
360
401
|
}
|
|
361
402
|
return lines;
|
|
@@ -368,6 +409,7 @@ export class PluginManager {
|
|
|
368
409
|
for (const line of wrapText(dialog.message, Math.max(8, width - 4)))
|
|
369
410
|
lines.push(this.indent(line, width));
|
|
370
411
|
lines.push("");
|
|
412
|
+
this.bodyFocusLine = lines.length + this.dialogCursor;
|
|
371
413
|
lines.push(this.renderCursorLine(this.dialogCursor === 0, dialog.destructive
|
|
372
414
|
? this.theme.fg("error", dialog.actionLabel)
|
|
373
415
|
: this.theme.fg("accent", dialog.actionLabel)));
|
|
@@ -376,13 +418,13 @@ export class PluginManager {
|
|
|
376
418
|
else {
|
|
377
419
|
lines.push("");
|
|
378
420
|
this.dialogInput.focused = this._focused;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
421
|
+
if (this.dialogInput.getValue().length === 0)
|
|
422
|
+
lines.push(this.indent(this.theme.fg("muted", dialog.placeholder), width));
|
|
423
|
+
this.bodyFocusLine = lines.length;
|
|
424
|
+
// Render the actual input even when empty so Pi can locate its IME cursor.
|
|
425
|
+
const input = this.dialogInput.render(Math.max(1, width - 4))[0] ?? "";
|
|
426
|
+
lines.push(this.indent(this.theme.fg("border", "[") + input + this.theme.fg("border", "]"), width));
|
|
383
427
|
}
|
|
384
|
-
lines.push("");
|
|
385
|
-
lines.push(this.theme.fg("dim", dialog.kind === "input" ? "enter submit · esc cancel" : "↑↓ or y/n · enter run · esc cancel"));
|
|
386
428
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
387
429
|
}
|
|
388
430
|
handleDialogInput(data) {
|
|
@@ -474,8 +516,6 @@ export class PluginManager {
|
|
|
474
516
|
this.heading(`${actionVerb(batch.action)} ${rows.length} plugin${rows.length === 1 ? "" : "s"}?`),
|
|
475
517
|
this.theme.fg("text", `This batch affects ${rows.length} selected plugin${rows.length === 1 ? "" : "s"}.`),
|
|
476
518
|
];
|
|
477
|
-
for (const row of rows)
|
|
478
|
-
lines.push(this.indent(`• ${row.name} @ ${row.marketplace} · ${row.version ?? "unversioned"}`, width));
|
|
479
519
|
lines.push("");
|
|
480
520
|
if (executable) {
|
|
481
521
|
lines.push(this.theme.bg("toolPendingBg", this.theme.fg("warning", " Executable content")));
|
|
@@ -490,8 +530,13 @@ export class PluginManager {
|
|
|
490
530
|
lines.push(this.indent(this.theme.fg("muted", "Each item runs sequentially and Pi reloads once when the manager closes."), width));
|
|
491
531
|
}
|
|
492
532
|
lines.push("");
|
|
533
|
+
this.bodyFocusLine = lines.length + this.detailActionCursor;
|
|
493
534
|
lines.push(this.renderCursorLine(this.detailActionCursor === 0, this.theme.fg("accent", `${actionVerb(batch.action)} ${rows.length} plugin${rows.length === 1 ? "" : "s"}`)));
|
|
494
535
|
lines.push(this.renderCursorLine(this.detailActionCursor === 1, this.theme.fg("text", "Back to selection")));
|
|
536
|
+
// Keep the effect and confirmation controls ahead of potentially long lists.
|
|
537
|
+
lines.push("", this.heading("Selected plugins"));
|
|
538
|
+
for (const row of rows)
|
|
539
|
+
lines.push(this.indent(`• ${row.name} @ ${row.marketplace} · ${row.version ?? "version unavailable"}`, width));
|
|
495
540
|
return lines;
|
|
496
541
|
}
|
|
497
542
|
renderBatch(width) {
|
|
@@ -511,6 +556,8 @@ export class PluginManager {
|
|
|
511
556
|
const result = this.batchResults[index];
|
|
512
557
|
const prefix = result === undefined ? this.theme.fg("dim", "○") : result.ok ? this.theme.fg("success", "✓") : this.theme.fg("error", "×");
|
|
513
558
|
const note = result === undefined ? (this.batchRunning && index === this.batchResults.length ? "working…" : "waiting") : result.ok ? "complete" : result.error ?? "failed";
|
|
559
|
+
if (this.batchRunning && index === this.batchResults.length)
|
|
560
|
+
this.bodyFocusLine = lines.length;
|
|
514
561
|
lines.push(this.indent(`${prefix} ${identity.plugin} @ ${identity.marketplace} ${this.theme.fg("muted", note)}`, width));
|
|
515
562
|
}
|
|
516
563
|
if (!this.batchRunning) {
|
|
@@ -519,6 +566,7 @@ export class PluginManager {
|
|
|
519
566
|
if (succeeded > 0)
|
|
520
567
|
lines.push(this.theme.fg("warning", "Reload will run once when this manager closes."));
|
|
521
568
|
lines.push("");
|
|
569
|
+
this.bodyFocusLine = lines.length + this.detailActionCursor;
|
|
522
570
|
lines.push(this.renderCursorLine(this.detailActionCursor === 0, this.theme.fg("accent", "View installed plugins")));
|
|
523
571
|
lines.push(this.renderCursorLine(this.detailActionCursor === 1, this.theme.fg("text", "Close manager")));
|
|
524
572
|
}
|
|
@@ -533,12 +581,14 @@ export class PluginManager {
|
|
|
533
581
|
for (const [index, row] of rows.entries()) {
|
|
534
582
|
const status = this.rowStatus(row);
|
|
535
583
|
const selected = this.selected.has(row.id);
|
|
536
|
-
const main = `${selected ? "◉" : "○"} ${row.name} · ${row.marketplace} · ${row.version ?? "
|
|
584
|
+
const main = `${selected ? "◉" : "○"} ${row.name} · ${row.marketplace} · ${row.version ?? "version unavailable"}`;
|
|
537
585
|
const suffix = row.installed && this.tab === "discover" ? ` · ${this.theme.fg("success", "installed")}` : status;
|
|
538
586
|
const title = this.theme.fg("accent", main);
|
|
539
587
|
const cursor = index === this.cursor ? this.theme.fg("accent", "›") : " ";
|
|
540
588
|
const content = `${cursor} ${title}${suffix.length > 0 ? ` ${suffix}` : ""}`;
|
|
541
|
-
|
|
589
|
+
if (index === this.cursor)
|
|
590
|
+
this.bodyFocusLine = lines.length + 2;
|
|
591
|
+
lines.push(this.theme.bg(index === this.cursor ? "selectedBg" : "toolPendingBg", padManagerLine(content, width)));
|
|
542
592
|
lines.push(this.indent(this.theme.fg("muted", row.description ?? "No description declared."), width));
|
|
543
593
|
}
|
|
544
594
|
return lines;
|
|
@@ -554,7 +604,11 @@ export class PluginManager {
|
|
|
554
604
|
}
|
|
555
605
|
renderFooter(width) {
|
|
556
606
|
let footer;
|
|
557
|
-
if (this.
|
|
607
|
+
if (this.dialog !== undefined)
|
|
608
|
+
footer = this.dialog.kind === "input" ? "enter submit · esc cancel" : "↑↓ or y/n · enter run · esc cancel";
|
|
609
|
+
else if (this.searchFocused)
|
|
610
|
+
footer = "Type to search · Enter or Esc return to list";
|
|
611
|
+
else if (this.view === "detail" || this.view === "confirm")
|
|
558
612
|
footer = "↑↓ navigate · Enter run · Esc back";
|
|
559
613
|
else if (this.view === "batch")
|
|
560
614
|
footer = this.batchRunning ? "Esc stop after current · manager stays open" : "Enter view installed · Esc close";
|
|
@@ -562,7 +616,7 @@ export class PluginManager {
|
|
|
562
616
|
footer = "Esc cancel checks · navigation remains available";
|
|
563
617
|
else
|
|
564
618
|
footer = "Ctrl+←/→ tabs · Ctrl+F search · ↑↓ navigate · Space select · a all · Enter details · r check · Esc close";
|
|
565
|
-
if (width < 80) {
|
|
619
|
+
if (width < 80 && this.dialog === undefined && !this.searchFocused) {
|
|
566
620
|
const compact = this.checking ? "◌ checking · Esc cancel" : this.reloadNeeded ? "● reload needed" : "";
|
|
567
621
|
footer = compact.length > 0 ? compact : footer;
|
|
568
622
|
}
|
|
@@ -575,7 +629,7 @@ export class PluginManager {
|
|
|
575
629
|
return `${this.theme.fg("muted", label.padEnd(10))} ${value}`;
|
|
576
630
|
}
|
|
577
631
|
indent(text, width) {
|
|
578
|
-
return truncateToWidth(` ${text}`, width, "");
|
|
632
|
+
return truncateToWidth(` ${text}`, width, "…");
|
|
579
633
|
}
|
|
580
634
|
renderCursorLine(cursor, text) {
|
|
581
635
|
return `${cursor ? this.theme.fg("accent", "›") : " "} ${text}`;
|
|
@@ -784,7 +838,7 @@ export class PluginManager {
|
|
|
784
838
|
}
|
|
785
839
|
if (action === "toggle-auto" && installed !== undefined) {
|
|
786
840
|
if (!installed.autoUpdate) {
|
|
787
|
-
const approved = await this.promptConfirm("Enable automatic plugin updates", "This grants standing authorization to replace executable hooks and MCP servers when Pi startup sees a declared
|
|
841
|
+
const approved = await this.promptConfirm("Enable automatic plugin updates", "This grants standing authorization to replace executable hooks and MCP servers when Pi startup sees a declared release version change. Enable it?", { actionLabel: "Enable" });
|
|
788
842
|
if (!approved)
|
|
789
843
|
return;
|
|
790
844
|
}
|
|
@@ -843,6 +897,8 @@ export class PluginManager {
|
|
|
843
897
|
if (explicit === undefined)
|
|
844
898
|
this.selected = new Set(identities.map(identityToString));
|
|
845
899
|
this.batch = Object.freeze({ action, identities: Object.freeze(identities.map((identity) => Object.freeze({ ...identity }))) });
|
|
900
|
+
this.bodyScroll = 0;
|
|
901
|
+
this.followFocus = false;
|
|
846
902
|
this.detailActionCursor = 0;
|
|
847
903
|
this.view = "confirm";
|
|
848
904
|
this.requestRender();
|
|
@@ -923,6 +979,8 @@ export class PluginManager {
|
|
|
923
979
|
});
|
|
924
980
|
}
|
|
925
981
|
openDetail(id) {
|
|
982
|
+
this.bodyScroll = 0;
|
|
983
|
+
this.followFocus = false;
|
|
926
984
|
this.detailId = id;
|
|
927
985
|
this.detailActionCursor = 0;
|
|
928
986
|
this.runtime = undefined;
|
|
@@ -937,6 +995,7 @@ export class PluginManager {
|
|
|
937
995
|
this.done({ reloadNeeded: this.reloadNeeded });
|
|
938
996
|
}
|
|
939
997
|
setTab(tab) {
|
|
998
|
+
this.bodyScroll = 0;
|
|
940
999
|
this.tab = tab;
|
|
941
1000
|
this.view = "list";
|
|
942
1001
|
this.detailId = undefined;
|
|
@@ -1050,13 +1109,13 @@ export class PluginManager {
|
|
|
1050
1109
|
installedRows() {
|
|
1051
1110
|
return this.installed.map((plugin) => {
|
|
1052
1111
|
const entry = catalogEntry(this.catalogs.get(plugin.marketplace), plugin.name);
|
|
1053
|
-
const installedVersion =
|
|
1112
|
+
const installedVersion = installedPluginVersion(plugin);
|
|
1054
1113
|
const availableVersion = entry?.version !== undefined && entry.version !== installedVersion ? entry.version : undefined;
|
|
1055
1114
|
return {
|
|
1056
1115
|
id: identityOf(plugin),
|
|
1057
1116
|
name: plugin.name,
|
|
1058
1117
|
marketplace: plugin.marketplace,
|
|
1059
|
-
description: typeof
|
|
1118
|
+
description: plugin.description ?? (typeof plugin.receipt?.description === "string" ? plugin.receipt.description : entry?.description),
|
|
1060
1119
|
version: installedVersion,
|
|
1061
1120
|
availableVersion,
|
|
1062
1121
|
installed: true,
|
|
@@ -1080,7 +1139,7 @@ export class PluginManager {
|
|
|
1080
1139
|
marketplace: marketplace.name,
|
|
1081
1140
|
description: entry.description,
|
|
1082
1141
|
version: entry.version,
|
|
1083
|
-
availableVersion: installed !== undefined && entry.version !==
|
|
1142
|
+
availableVersion: installed !== undefined && entry.version !== installedPluginVersion(installed) ? entry.version : undefined,
|
|
1084
1143
|
installed: installed !== undefined,
|
|
1085
1144
|
enabled: installed?.enabled,
|
|
1086
1145
|
autoUpdate: installed?.autoUpdate,
|
|
@@ -1102,7 +1161,7 @@ export class PluginManager {
|
|
|
1102
1161
|
statusLabel(installed, entry) {
|
|
1103
1162
|
if (!installed.enabled)
|
|
1104
1163
|
return "disabled";
|
|
1105
|
-
if (entry?.version !== undefined && entry.version !==
|
|
1164
|
+
if (entry?.version !== undefined && entry.version !== installedPluginVersion(installed))
|
|
1106
1165
|
return "update available";
|
|
1107
1166
|
return "enabled";
|
|
1108
1167
|
}
|
|
@@ -1123,6 +1182,8 @@ export class PluginManager {
|
|
|
1123
1182
|
marketplaceStatus(name) {
|
|
1124
1183
|
if (this.checkingMarketplaces.has(name))
|
|
1125
1184
|
return this.theme.fg("accent", "checking…");
|
|
1185
|
+
if (this.checkErrors.has(name))
|
|
1186
|
+
return this.theme.fg("warning", "check failed · cached catalog");
|
|
1126
1187
|
const catalog = this.catalogs.get(name);
|
|
1127
1188
|
return catalog === undefined ? this.theme.fg("warning", "catalog unavailable") : this.theme.fg("muted", "local checkout ready");
|
|
1128
1189
|
}
|
|
@@ -1166,6 +1227,8 @@ export class PluginManager {
|
|
|
1166
1227
|
this.checkController = controller;
|
|
1167
1228
|
this.checking = true;
|
|
1168
1229
|
this.checkedMarketplaces = 0;
|
|
1230
|
+
this.checkTotal = new Set(names).size;
|
|
1231
|
+
this.updatesChecked = false;
|
|
1169
1232
|
this.checkingMarketplaces = new Set(names);
|
|
1170
1233
|
this.localEpoch++;
|
|
1171
1234
|
this.requestRender();
|
|
@@ -1173,6 +1236,11 @@ export class PluginManager {
|
|
|
1173
1236
|
if (this.destroyed || run !== this.checkRun)
|
|
1174
1237
|
return;
|
|
1175
1238
|
this.checkedMarketplaces++;
|
|
1239
|
+
this.checkingMarketplaces.delete(result.marketplace);
|
|
1240
|
+
if (result.ok)
|
|
1241
|
+
this.checkErrors.delete(result.marketplace);
|
|
1242
|
+
else
|
|
1243
|
+
this.checkErrors.set(result.marketplace, result.error ?? "marketplace check failed");
|
|
1176
1244
|
if (result.ok && result.catalog !== undefined) {
|
|
1177
1245
|
const cursorId = this.currentCursorId();
|
|
1178
1246
|
this.catalogs.set(result.marketplace, result.catalog);
|
|
@@ -1197,8 +1265,11 @@ export class PluginManager {
|
|
|
1197
1265
|
return results;
|
|
1198
1266
|
}
|
|
1199
1267
|
catch (error) {
|
|
1200
|
-
if (!this.destroyed && run === this.checkRun)
|
|
1268
|
+
if (!this.destroyed && run === this.checkRun) {
|
|
1269
|
+
for (const name of this.checkingMarketplaces)
|
|
1270
|
+
this.checkErrors.set(name, errorMessage(error));
|
|
1201
1271
|
this.showToast(`Marketplace check failed: ${errorMessage(error)}`, "warning");
|
|
1272
|
+
}
|
|
1202
1273
|
return [];
|
|
1203
1274
|
}
|
|
1204
1275
|
finally {
|
|
@@ -1242,6 +1313,10 @@ export class PluginManager {
|
|
|
1242
1313
|
if (!marketplaceNames.has(name))
|
|
1243
1314
|
this.catalogs.delete(name);
|
|
1244
1315
|
}
|
|
1316
|
+
for (const name of this.checkErrors.keys()) {
|
|
1317
|
+
if (!marketplaceNames.has(name))
|
|
1318
|
+
this.checkErrors.delete(name);
|
|
1319
|
+
}
|
|
1245
1320
|
this.restoreCursor(cursorId);
|
|
1246
1321
|
this.requestRender();
|
|
1247
1322
|
this.maybeStartOpenCheck();
|
|
@@ -1306,7 +1381,12 @@ export class PluginManager {
|
|
|
1306
1381
|
});
|
|
1307
1382
|
}
|
|
1308
1383
|
}
|
|
1384
|
+
// Runtime diagnostics also aggregate each plugin's diagnostics. Display
|
|
1385
|
+
// each once with its plugin identity, rather than duplicating the warning.
|
|
1386
|
+
const pluginDiagnostics = new Set(runtime.plugins.flatMap((plugin) => plugin.diagnostics));
|
|
1309
1387
|
for (const diagnostic of runtime.diagnostics) {
|
|
1388
|
+
if (pluginDiagnostics.has(diagnostic))
|
|
1389
|
+
continue;
|
|
1310
1390
|
issues.push({ id: `runtime:${diagnostic.scope}`, title: diagnostic.scope, message: diagnostic.message, severity: "error" });
|
|
1311
1391
|
}
|
|
1312
1392
|
for (const plugin of this.installed) {
|
|
@@ -1314,10 +1394,13 @@ export class PluginManager {
|
|
|
1314
1394
|
if (entry === undefined && this.catalogs.has(plugin.marketplace)) {
|
|
1315
1395
|
issues.push({ id: `${identityOf(plugin)}:catalog`, title: identityOf(plugin), message: "Installed bundle is not declared in the current marketplace catalog; the installed copy remains available.", severity: "error", pluginId: identityOf(plugin) });
|
|
1316
1396
|
}
|
|
1317
|
-
else if (plugin.autoUpdate && entry
|
|
1318
|
-
issues.push({ id: `${identityOf(plugin)}:version`, title: identityOf(plugin), message: "
|
|
1397
|
+
else if (plugin.autoUpdate && entry !== undefined && entry.version === undefined && installedPluginVersion(plugin) === undefined) {
|
|
1398
|
+
issues.push({ id: `${identityOf(plugin)}:version`, title: identityOf(plugin), message: "No release version is available locally. Marked checks also inspect source manifests; /plugins update-marked forces replacement when no version is declared.", severity: "warning", pluginId: identityOf(plugin) });
|
|
1319
1399
|
}
|
|
1320
1400
|
}
|
|
1401
|
+
for (const [marketplace, message] of this.checkErrors) {
|
|
1402
|
+
issues.push({ id: `${marketplace}:check`, title: marketplace, message, severity: "warning" });
|
|
1403
|
+
}
|
|
1321
1404
|
for (const [marketplace, catalog] of this.catalogs) {
|
|
1322
1405
|
for (const diagnostic of catalog.diagnostics ?? [])
|
|
1323
1406
|
issues.push({ id: `${marketplace}:${diagnostic.scope}`, title: marketplace, message: diagnostic.message, severity: "warning" });
|