@danypops/pi-packed 0.14.0 → 0.16.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 +1 -1
- package/extension/src/menu-theme.ts +14 -1
- package/extension/src/tui.ts +177 -71
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ The extension connects to Packed's authenticated user daemon and starts the pack
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
13
|
- `/packed` -- a floating overlay panel: every installed Pi package, with update availability. Mnemonics follow lazy.nvim's own convention (uppercase acts on every row, lowercase on the one under the cursor):
|
|
14
|
-
- `u` / `U` -- update the selected package / update every outdated package, one combined confirmation and one reload. `U`
|
|
14
|
+
- `u` / `U` -- update the selected package / update every outdated package, one combined confirmation and one reload. `U` shows a spinner inline next to the package currently updating, settling into a real ✓ or ✗ plus a short tail of that update's own captured output once it finishes -- the list stays visible throughout, nothing swaps to a separate screen. A reload is confirmed separately from the update itself -- decline it to defer: the update already happened, only picking it up in this Pi session is deferred until `/reload`. Every confirm in this flow -- approval and reload alike -- renders inline on this same panel (`y`/`n` keys), never a separate popup.
|
|
15
15
|
- `x` -- remove the selected package.
|
|
16
16
|
- `d` -- disable (or re-enable) the selected package's own extensions.
|
|
17
17
|
- `c` -- jump to resource config for the selected package (skills, prompts, themes).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { MenuTheme } from "malevich-tui-components";
|
|
2
|
+
import type { DialogTheme, MenuTheme } from "malevich-tui-components";
|
|
3
3
|
|
|
4
4
|
/** Maps Pi's own Theme onto Malevich's Menu -- the one place this mapping
|
|
5
5
|
* exists, shared by every ctx.ui.custom overlay menu in this extension. */
|
|
@@ -12,3 +12,16 @@ export function menuTheme(theme: Theme): MenuTheme {
|
|
|
12
12
|
title: (s) => theme.fg("accent", s),
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/** Maps Pi's own Theme onto Malevich's Dialog -- used for an inline y/n
|
|
17
|
+
* decision (e.g. confirmReload) rendered as part of an already-open
|
|
18
|
+
* ctx.ui.custom overlay's own content, instead of ctx.ui.confirm's separate,
|
|
19
|
+
* arrow-select-only native dialog. */
|
|
20
|
+
export function dialogTheme(theme: Theme): DialogTheme {
|
|
21
|
+
return {
|
|
22
|
+
border: (s) => theme.fg("border", s),
|
|
23
|
+
title: (s) => theme.bold(theme.fg("accent", s)),
|
|
24
|
+
body: (s) => s,
|
|
25
|
+
dim: (s) => theme.fg("muted", s),
|
|
26
|
+
};
|
|
27
|
+
}
|
package/extension/src/tui.ts
CHANGED
|
@@ -12,24 +12,33 @@
|
|
|
12
12
|
* own row percentage, not a fixed row count -- scales with the real
|
|
13
13
|
* terminal, unlike the anchor:"top-center"+offsetY this replaced, which
|
|
14
14
|
* was pinned 1 row below the absolute top on any terminal size). Enter
|
|
15
|
-
* opens a second, smaller overlay action menu on top of it. U's batch
|
|
16
|
-
* this same package list --
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
15
|
+
* opens a second, smaller overlay action menu on top of it. U's batch
|
|
16
|
+
* update stays on this same package list -- an indeterminate spinner
|
|
17
|
+
* (Malevich's Spinner, extracted upstream from this panel's original need
|
|
18
|
+
* once no embeddable one existed anywhere) renders inline next to the row
|
|
19
|
+
* currently updating, settling into a real ✓/✗ plus a bounded tail of
|
|
20
|
+
* that row's own actual captured stdout/stderr once it finishes, never a
|
|
21
|
+
* determinate bar (a single subprocess call has no knowable percentage).
|
|
22
|
+
* Rows render through Malevich's Table (real column-aligned
|
|
23
|
+
* Package/Version/status cells, per-row selection styling baked into each
|
|
24
|
+
* cell since Table's own cellStyle is column-wide, not row-wide) inside
|
|
25
|
+
* this panel's own scroll-window slice -- Table deliberately owns no
|
|
26
|
+
* pagination of its own, so the visible-window-around-selectedIndex math
|
|
27
|
+
* stays here. u/x/d and the Enter action menu all run their whole
|
|
28
|
+
* approve+mutate+confirmReload flow inline, without ever closing this
|
|
29
|
+
* overlay first -- every confirm() along the way (mutation approval,
|
|
30
|
+
* confirmReload's separate reload gate) renders as a real Malevich Dialog
|
|
31
|
+
* on this SAME overlay, dispatched by literal y/n keys, instead of
|
|
32
|
+
* ctx.ui.confirm's own separate native dialog (arrow-select only, and a
|
|
33
|
+
* genuinely different overlay stacked on top -- confirmed live as a real
|
|
34
|
+
* user complaint). Declining a reload defers it: the mutation itself
|
|
35
|
+
* already happened, and the panel stays open with refreshed rows instead
|
|
36
|
+
* of ending the session. All data flows through the packed CLI (thin seam).
|
|
28
37
|
*/
|
|
29
38
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
30
39
|
import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
31
40
|
import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
32
|
-
import { Envelope, Menu,
|
|
41
|
+
import { Dialog, Envelope, Menu, Spinner, Table, type MenuItem, type TableColumn, type TextMeasure } from "malevich-tui-components";
|
|
33
42
|
import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
|
|
34
43
|
import type { Row, ViewMode } from "./model.js";
|
|
35
44
|
import type { Natives, PackageResources } from "./packed.js";
|
|
@@ -37,11 +46,11 @@ import { approvePackageOperation } from "./tools.js";
|
|
|
37
46
|
import { showPackedSettings } from "./security-tui.js";
|
|
38
47
|
import { showResourceConfig, applyResourceToggle } from "./resource-config.js";
|
|
39
48
|
import { showDiscoverPanel } from "./discover.js";
|
|
40
|
-
import { menuTheme } from "./menu-theme.js";
|
|
49
|
+
import { dialogTheme, menuTheme } from "./menu-theme.js";
|
|
41
50
|
import { confirmReload } from "./reload.js";
|
|
42
51
|
|
|
43
52
|
interface PanelAction {
|
|
44
|
-
type: "
|
|
53
|
+
type: "config" | "find" | "refresh" | "settings";
|
|
45
54
|
row?: Row;
|
|
46
55
|
}
|
|
47
56
|
|
|
@@ -115,7 +124,13 @@ export async function applyPackageChoice(
|
|
|
115
124
|
|
|
116
125
|
interface UpdateAllResult { changed: number; failedNames: string[]; }
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
/** Present only on phase "done" -- the real captured stdout+stderr from
|
|
128
|
+
* ExecInstaller (ok: true) or the thrown error's message (ok: false). This
|
|
129
|
+
* is the actual execution output, not a synthetic status string, so a host
|
|
130
|
+
* can show a genuine success/failure sign instead of guessing from
|
|
131
|
+
* reloadRequired alone. */
|
|
132
|
+
interface UpdateProgressResult { ok: boolean; output: string; }
|
|
133
|
+
interface UpdateProgressEvent { row: Row; index: number; total: number; phase: "start" | "done"; result?: UpdateProgressResult; }
|
|
119
134
|
|
|
120
135
|
/** The core sequential-update loop, with no UI of its own -- reports each
|
|
121
136
|
* step via onProgress so any host surface (a floating overlay, or the
|
|
@@ -134,14 +149,28 @@ async function performUpdateAll(
|
|
|
134
149
|
try {
|
|
135
150
|
const outcome = await natives.update(`npm:${row.name}`, approved);
|
|
136
151
|
if (outcome.reloadRequired) changed += 1;
|
|
152
|
+
onProgress?.({ row, index: i, total: outdated.length, phase: "done", result: { ok: true, output: outcome.output } });
|
|
137
153
|
} catch (e) {
|
|
138
|
-
|
|
154
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
155
|
+
failedNames.push(`${row.name}: ${message}`);
|
|
156
|
+
onProgress?.({ row, index: i, total: outdated.length, phase: "done", result: { ok: false, output: message } });
|
|
139
157
|
}
|
|
140
|
-
onProgress?.({ row, index: i, total: outdated.length, phase: "done" });
|
|
141
158
|
}
|
|
142
159
|
return { changed, failedNames };
|
|
143
160
|
}
|
|
144
161
|
|
|
162
|
+
const MAX_LOG_TAIL_CHARS = 60;
|
|
163
|
+
|
|
164
|
+
/** The last non-empty line of real captured output, bounded -- never the
|
|
165
|
+
* full stdout/stderr dump inline (a noisy npm install can produce hundreds
|
|
166
|
+
* of lines). undefined when there's nothing worth showing (the common
|
|
167
|
+
* case: `pi update` often produces no output on success at all). */
|
|
168
|
+
function logTail(output: string): string | undefined {
|
|
169
|
+
const line = output.trim().split("\n").filter(Boolean).at(-1);
|
|
170
|
+
if (!line) return undefined;
|
|
171
|
+
return line.length > MAX_LOG_TAIL_CHARS ? `${line.slice(0, MAX_LOG_TAIL_CHARS - 1)}…` : line;
|
|
172
|
+
}
|
|
173
|
+
|
|
145
174
|
/** Approves once for the whole batch, runs it via whatever runBatch does
|
|
146
175
|
* (a floating overlay for applyUpdateAll's own public API, or renderPanel's
|
|
147
176
|
* embedded progress bar), then reports the combined result -- shared so
|
|
@@ -182,11 +211,17 @@ async function approveAndRunUpdateAll(
|
|
|
182
211
|
return "changed";
|
|
183
212
|
}
|
|
184
213
|
|
|
185
|
-
|
|
214
|
+
const MAX_SETTLED_LOG_LINES = 5;
|
|
215
|
+
|
|
216
|
+
/** Floats its own spinner+log overlay over the still-open panel -- kept
|
|
186
217
|
* for applyUpdateAll's own public API (and anything calling it directly,
|
|
187
218
|
* outside the packages panel). renderPanel's own U key does not use this;
|
|
188
|
-
* it renders the same
|
|
189
|
-
* instead of stacking a second one.
|
|
219
|
+
* it renders the same spinner+log inline on its own already-open overlay
|
|
220
|
+
* instead of stacking a second one. An indeterminate spinner (not a
|
|
221
|
+
* determinate bar) because a single subprocess call has no knowable
|
|
222
|
+
* percentage -- only "still running" or "settled". Each settled row
|
|
223
|
+
* appends one bounded log line (real captured output, not a synthetic
|
|
224
|
+
* status) with a genuine success/failure glyph, up to a small scrollback. */
|
|
190
225
|
async function runUpdatesWithProgress(
|
|
191
226
|
outdated: Row[],
|
|
192
227
|
natives: Natives,
|
|
@@ -195,22 +230,35 @@ async function runUpdatesWithProgress(
|
|
|
195
230
|
): Promise<UpdateAllResult> {
|
|
196
231
|
return ctx.ui.custom<UpdateAllResult>(
|
|
197
232
|
(tui, theme, _kb, done) => {
|
|
198
|
-
const
|
|
233
|
+
const spinner = new Spinner();
|
|
234
|
+
const settledLines: string[] = [];
|
|
235
|
+
let currentLabel = `${outdated[0]?.name ?? ""} (1/${outdated.length})`;
|
|
199
236
|
const border = () => new DynamicBorder((s) => theme.fg("border", s));
|
|
200
237
|
const container = new Container();
|
|
201
238
|
container.addChild(new Spacer(1));
|
|
202
239
|
container.addChild(border());
|
|
203
240
|
container.addChild({ invalidate() {}, render: (_width: number) => [theme.bold("Updating packages")] });
|
|
204
241
|
container.addChild(new Spacer(1));
|
|
205
|
-
container.addChild(
|
|
242
|
+
container.addChild({ invalidate() {}, render: (width: number) => [truncateToWidth(`${theme.fg("accent", spinner.glyph())} ${currentLabel}`, width, "")] });
|
|
243
|
+
container.addChild(new Spacer(1));
|
|
244
|
+
container.addChild({
|
|
245
|
+
invalidate() {},
|
|
246
|
+
render: (width: number) => settledLines.slice(-MAX_SETTLED_LOG_LINES).map((line) => truncateToWidth(line, width, "")),
|
|
247
|
+
});
|
|
206
248
|
container.addChild(new Spacer(1));
|
|
207
249
|
container.addChild(border());
|
|
208
250
|
|
|
251
|
+
spinner.start(() => tui.requestRender());
|
|
209
252
|
performUpdateAll(outdated, natives, approved, (event) => {
|
|
210
|
-
|
|
211
|
-
|
|
253
|
+
if (event.phase === "start") {
|
|
254
|
+
currentLabel = `${event.row.name} (${event.index + 1}/${event.total})`;
|
|
255
|
+
} else {
|
|
256
|
+
const glyph = event.result?.ok ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
257
|
+
const tail = event.result ? logTail(event.result.output) : undefined;
|
|
258
|
+
settledLines.push(`${glyph} ${event.row.name}${tail ? theme.fg("dim", ` -- ${tail}`) : ""}`);
|
|
259
|
+
}
|
|
212
260
|
tui.requestRender();
|
|
213
|
-
}).then(done);
|
|
261
|
+
}).finally(() => spinner.stop()).then(done);
|
|
214
262
|
|
|
215
263
|
return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput() {} };
|
|
216
264
|
},
|
|
@@ -322,8 +370,10 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
|
|
|
322
370
|
}
|
|
323
371
|
|
|
324
372
|
// Panel loop: actions resolve the component, run outside it, then reopen.
|
|
325
|
-
//
|
|
326
|
-
//
|
|
373
|
+
// Every mutation (u/x/d, U, and whatever the Enter action menu picks) is
|
|
374
|
+
// handled entirely inside renderPanel itself -- approval and reload
|
|
375
|
+
// confirms render inline on the same already-open overlay -- and never
|
|
376
|
+
// reaches here; only non-mutation navigation resolves this loop.
|
|
327
377
|
for (;;) {
|
|
328
378
|
const action = await renderPanel(ctx, natives, rows);
|
|
329
379
|
if (!action) return; // closed
|
|
@@ -343,24 +393,8 @@ export async function showPackedPanel(ctx: ExtensionCommandContext, natives: Nat
|
|
|
343
393
|
continue; // a successful install already reloaded; a no-op returns here
|
|
344
394
|
}
|
|
345
395
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
continue; // showResourceConfig already handles its own reload prompt
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const row = action.row;
|
|
352
|
-
if (!row) continue;
|
|
353
|
-
|
|
354
|
-
if (action.type === "disable") {
|
|
355
|
-
const outcome = await applyDisableExtensions(row, natives, ctx);
|
|
356
|
-
if (outcome === "changed") return;
|
|
357
|
-
if (outcome === "deferred") await refreshRows();
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const outcome = await applyPackageChoice(action.type === "update" ? `Update to ${row.latest}` : "Remove", row, natives, ctx);
|
|
362
|
-
if (outcome === "changed") return; // ctx.reload() already replaced the session
|
|
363
|
-
if (outcome === "deferred") await refreshRows();
|
|
396
|
+
await showResourceConfig(ctx, natives, action.row?.name);
|
|
397
|
+
// showResourceConfig already handles its own reload prompt
|
|
364
398
|
}
|
|
365
399
|
}
|
|
366
400
|
|
|
@@ -372,11 +406,16 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
372
406
|
let searchActive = false;
|
|
373
407
|
let filtered = visibleRows(rows, mode);
|
|
374
408
|
let selectedIndex = 0;
|
|
375
|
-
// Set only while U's batch update is running
|
|
376
|
-
//
|
|
377
|
-
//
|
|
409
|
+
// Set only while U's batch update is running -- blocks input for the
|
|
410
|
+
// whole batch (the installer owns input until it finishes), same as
|
|
411
|
+
// before. The list stays fully visible throughout. Which specific row
|
|
412
|
+
// currently shows a spinner vs. a settled ✓/✗ is settled's own job
|
|
413
|
+
// (below), not this -- a just-finished row must show its glyph
|
|
414
|
+
// immediately, even for the instant before the next row's "start"
|
|
415
|
+
// event reassigns this to the next name.
|
|
378
416
|
let updatingRowName: string | undefined;
|
|
379
|
-
const
|
|
417
|
+
const spinner = new Spinner();
|
|
418
|
+
const settled = new Map<string, { ok: boolean; tail: string | undefined }>();
|
|
380
419
|
|
|
381
420
|
const maxVisible = 20;
|
|
382
421
|
|
|
@@ -385,22 +424,78 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
385
424
|
selectedIndex = 0;
|
|
386
425
|
}
|
|
387
426
|
|
|
427
|
+
// A y/n decision rendered as this SAME overlay's own content (via a real
|
|
428
|
+
// Malevich Dialog, dispatched by literal key press) instead of
|
|
429
|
+
// ctx.ui.confirm's separate native dialog -- confirmed live as a real
|
|
430
|
+
// user complaint: ctx.ui.confirm opens as its own distinct overlay on
|
|
431
|
+
// top of this one ("a new window"), arrow-select only, no y/n keys.
|
|
432
|
+
let pendingDialog: Dialog | undefined;
|
|
433
|
+
|
|
434
|
+
function confirmInline(title: string, message: string): Promise<boolean> {
|
|
435
|
+
return new Promise((resolve) => {
|
|
436
|
+
const settle = (value: boolean) => {
|
|
437
|
+
pendingDialog = undefined;
|
|
438
|
+
tui.requestRender();
|
|
439
|
+
resolve(value);
|
|
440
|
+
};
|
|
441
|
+
pendingDialog = new Dialog({
|
|
442
|
+
title,
|
|
443
|
+
body: message,
|
|
444
|
+
actions: [
|
|
445
|
+
{ label: "Yes", key: "y", action: () => settle(true) },
|
|
446
|
+
{ label: "No", key: "n", action: () => settle(false) },
|
|
447
|
+
],
|
|
448
|
+
theme: dialogTheme(theme),
|
|
449
|
+
});
|
|
450
|
+
tui.requestRender();
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Every confirm() call inside a mutation flow driven from this open
|
|
455
|
+
// overlay (approvePackageOperation's own approval, confirmReload's
|
|
456
|
+
// separate reload gate) routes through confirmInline instead of the
|
|
457
|
+
// real ctx.ui.confirm -- notify/reload/etc. stay the genuine ctx.
|
|
458
|
+
const inlineCtx: ExtensionCommandContext = { ...ctx, hasUI: true, ui: { ...ctx.ui, confirm: confirmInline } };
|
|
459
|
+
|
|
460
|
+
/** u/x/d and the Enter action menu all funnel through here: approval and
|
|
461
|
+
* reload confirms render inline (via inlineCtx/confirmInline) on this
|
|
462
|
+
* same overlay, never closing it first the way done()-dispatch used to. */
|
|
463
|
+
async function runRowActionInline(action: { type: "update" | "remove" | "disable"; row: Row }): Promise<void> {
|
|
464
|
+
updatingRowName = action.row.name;
|
|
465
|
+
tui.requestRender();
|
|
466
|
+
const outcome = action.type === "disable"
|
|
467
|
+
? await applyDisableExtensions(action.row, natives, inlineCtx)
|
|
468
|
+
: await applyPackageChoice(action.type === "update" ? `Update to ${action.row.latest}` : "Remove", action.row, natives, inlineCtx);
|
|
469
|
+
updatingRowName = undefined;
|
|
470
|
+
if (outcome === "changed") {
|
|
471
|
+
done(undefined); // ctx.reload() already replaced the session
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const reloaded = await loadRows(natives);
|
|
475
|
+
if (reloaded.error) ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
|
|
476
|
+
else rows = reloaded.rows;
|
|
477
|
+
applyFilter();
|
|
478
|
+
tui.requestRender();
|
|
479
|
+
}
|
|
480
|
+
|
|
388
481
|
/** U -- runs the whole approve+update+notify+reload flow without ever
|
|
389
|
-
* closing this panel or replacing the list: each
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* session. */
|
|
482
|
+
* closing this panel or replacing the list: each row's own settled
|
|
483
|
+
* outcome (spinner while in flight, then a real ✓/✗ plus a bounded tail
|
|
484
|
+
* of its actual captured output) appears inline next to that row, via
|
|
485
|
+
* updatingRowName/spinner/settled, which list's own render checks. Rows
|
|
486
|
+
* refresh in place afterward unless a reload already ended the session. */
|
|
394
487
|
async function runUpdateAllInline(): Promise<void> {
|
|
395
488
|
const outdated = rows.filter((row) => row.hasUpdate);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
489
|
+
settled.clear();
|
|
490
|
+
const outcome = await approveAndRunUpdateAll(outdated, natives, inlineCtx, (batch, approved) => {
|
|
491
|
+
spinner.start(() => tui.requestRender());
|
|
399
492
|
return performUpdateAll(batch, natives, approved, (event) => {
|
|
400
493
|
updatingRowName = event.row.name;
|
|
401
|
-
if (event.phase === "done"
|
|
494
|
+
if (event.phase === "done" && event.result) {
|
|
495
|
+
settled.set(event.row.name, { ok: event.result.ok, tail: logTail(event.result.output) });
|
|
496
|
+
}
|
|
402
497
|
tui.requestRender();
|
|
403
|
-
});
|
|
498
|
+
}).finally(() => spinner.stop());
|
|
404
499
|
});
|
|
405
500
|
updatingRowName = undefined;
|
|
406
501
|
if (outcome === "changed") {
|
|
@@ -410,6 +505,7 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
410
505
|
const reloaded = await loadRows(natives);
|
|
411
506
|
if (reloaded.error) ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
|
|
412
507
|
else rows = reloaded.rows;
|
|
508
|
+
settled.clear(); // fresh state for a subsequent batch
|
|
413
509
|
applyFilter();
|
|
414
510
|
tui.requestRender();
|
|
415
511
|
}
|
|
@@ -485,10 +581,16 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
485
581
|
const selected = i === selectedIndex;
|
|
486
582
|
const cursor = selected ? theme.fg("accent", "❯ ") : " ";
|
|
487
583
|
const name = selected ? theme.bold(row.name) : row.name;
|
|
488
|
-
|
|
584
|
+
// A settled row shows its real outcome even for the instant
|
|
585
|
+
// before the next row's "start" event moves updatingRowName
|
|
586
|
+
// off it -- settled always wins over "still spinning".
|
|
587
|
+
const rowSettled = settled.get(row.name);
|
|
588
|
+
const isUpdating = !rowSettled && updatingRowName === row.name;
|
|
489
589
|
const status = isUpdating
|
|
490
|
-
? theme.fg("accent", `${
|
|
491
|
-
:
|
|
590
|
+
? theme.fg("accent", `${spinner.glyph()} updating…`)
|
|
591
|
+
: rowSettled
|
|
592
|
+
? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
|
|
593
|
+
: row.hasUpdate ? theme.fg("warning", `↑${row.latest}`) : "";
|
|
492
594
|
return { name: `${cursor}${name}`, version: theme.fg("dim", row.version), status };
|
|
493
595
|
}),
|
|
494
596
|
);
|
|
@@ -525,11 +627,17 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
525
627
|
return {
|
|
526
628
|
render(width: number): string[] {
|
|
527
629
|
envelope.setTitle(panelTitle());
|
|
630
|
+
envelope.setContent(pendingDialog ?? body);
|
|
528
631
|
return envelope.render(width);
|
|
529
632
|
},
|
|
530
633
|
invalidate: () => envelope.invalidate(),
|
|
531
634
|
handleInput(data: string) {
|
|
532
|
-
if (
|
|
635
|
+
if (pendingDialog) {
|
|
636
|
+
pendingDialog.handleInput(data);
|
|
637
|
+
tui.requestRender();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (updatingRowName) return; // the mutation/installer owns input until it finishes
|
|
533
641
|
|
|
534
642
|
if (searchActive) {
|
|
535
643
|
if (data === "\x1b") {
|
|
@@ -574,17 +682,17 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
574
682
|
return;
|
|
575
683
|
case "u": {
|
|
576
684
|
const row = filtered[selectedIndex];
|
|
577
|
-
if (row?.hasUpdate)
|
|
685
|
+
if (row?.hasUpdate) void runRowActionInline({ type: "update", row });
|
|
578
686
|
return;
|
|
579
687
|
}
|
|
580
688
|
case "x": {
|
|
581
689
|
const row = filtered[selectedIndex];
|
|
582
|
-
if (row)
|
|
690
|
+
if (row) void runRowActionInline({ type: "remove", row });
|
|
583
691
|
return;
|
|
584
692
|
}
|
|
585
693
|
case "d": {
|
|
586
694
|
const row = filtered[selectedIndex];
|
|
587
|
-
if (row)
|
|
695
|
+
if (row) void runRowActionInline({ type: "disable", row });
|
|
588
696
|
return;
|
|
589
697
|
}
|
|
590
698
|
case "c": {
|
|
@@ -597,9 +705,7 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
597
705
|
if (!row) return;
|
|
598
706
|
void (async () => {
|
|
599
707
|
const choice = await showActionMenu(ctx, row);
|
|
600
|
-
if (choice === "update")
|
|
601
|
-
else if (choice === "remove") done({ type: "remove", row });
|
|
602
|
-
else if (choice === "disable") done({ type: "disable", row });
|
|
708
|
+
if (choice === "update" || choice === "remove" || choice === "disable") void runRowActionInline({ type: choice, row });
|
|
603
709
|
else if (choice === "config") done({ type: "config", row });
|
|
604
710
|
})();
|
|
605
711
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Pi package tools, commands, profiles, and TUI for the Packed daemon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@danypops/packed": "^0.2.0",
|
|
16
16
|
"@danypops/vehicle-client": "^0.1.1",
|
|
17
17
|
"@danypops/vehicle-client-pi": "^0.1.5",
|
|
18
|
-
"malevich-tui-components": "^0.
|
|
18
|
+
"malevich-tui-components": "^0.11.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@earendil-works/pi-coding-agent": "*",
|