@danypops/pi-packed 0.20.2 → 0.21.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 +3 -2
- package/extension/src/index.ts +1 -1
- package/extension/src/menu-theme.ts +10 -1
- package/extension/src/package-inspector.ts +93 -0
- package/extension/src/tab-host.ts +2 -0
- package/extension/src/tabs/discover.ts +33 -19
- package/extension/src/terminal-text.ts +6 -0
- package/extension/src/tui.ts +62 -53
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,14 +11,15 @@ The extension connects to Packed's authenticated user daemon and starts the pack
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
13
|
- `/packed` -- one floating overlay, four tabs (Packages/Find/Config/Settings), always shown in a persistent tab bar at the top: the focused tab reverses the theme's text color for high contrast in both dark and light modes, unfocused tabs show only a foreground color with their own first letter highlighted as a jump mnemonic (the focused tab needs no mnemonic -- there's nothing to jump to when you're already there). `Tab`/`Shift-Tab` (or `←`/`→`) sweep between them -- recognized under legacy CSI, xterm's modifyOtherKeys, and the Kitty keyboard protocol alike, not just one hardcoded encoding; from any tab other than Packages, `p`/`f`/`c`/`s` also jump straight to the matching one (never stolen while that tab's own filter/query box is capturing text). `Esc` returns to Packages from anywhere else, or closes the panel from Packages itself. Every approval and reload confirmation, on any tab, renders inline on this same panel (`y`/`n` keys), never a separate popup or native dialog. A standing test (`keymap.test.ts`, using Malevich's tree-style `findMnemonicConflicts`) verifies no two actions genuinely reachable at once share a key.
|
|
14
|
-
- **Packages** (the default tab) -- every installed Pi package
|
|
14
|
+
- **Packages** (the default tab) -- every installed Pi package is a bordered Card with full-card selection highlighting and update availability. Mnemonics follow lazy.nvim's own convention (uppercase acts on every row, lowercase on the one under the cursor):
|
|
15
15
|
- `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`.
|
|
16
16
|
- `x` -- remove the selected package.
|
|
17
17
|
- `d` -- disable (or re-enable) the selected package's own extensions.
|
|
18
|
+
- `i` -- inspect the selected package's registry metadata and README inside the same overlay; `Esc`/`q` returns.
|
|
18
19
|
- `c` -- jump to the Config tab, scoped to the selected package.
|
|
19
20
|
- `f` -- jump to the Find tab. `s` -- jump to the Settings tab.
|
|
20
21
|
- `Enter` opens a smaller floating action menu with all of the above. `r` refreshes, `/` filters, `v` cycles view modes.
|
|
21
|
-
- **Find** -- search the npm registry for new Pi packages and
|
|
22
|
+
- **Find** -- search the npm registry for new Pi packages. Results render as Cards; after a search, `i` inspects the selected result, `Enter` installs it, and `/` returns to query editing.
|
|
22
23
|
- **Config** -- enable or disable individual extensions, skills, prompt templates, and themes declared by installed packages, per package, at global or project scope (`v` switches global/project). `/packed config` opens the panel landing directly on this tab.
|
|
23
24
|
- **Settings** -- change whether package mutations (install/update/remove/toggle) require confirmation.
|
|
24
25
|
- `/packed setup plan [path] [--prune]` / `/packed setup apply [path] [--prune]` -- preview or apply a reproducible-environment manifest (`pi-setup.json`) against installed packages and profiles.
|
package/extension/src/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
26
26
|
|
|
27
27
|
pi.registerCommand("packed", {
|
|
28
28
|
description:
|
|
29
|
-
"Browse and manage installed Pi packages -- u/U update, x remove, d disable, c config, f find, s settings -- or run setup plan/apply or config directly",
|
|
29
|
+
"Browse and manage installed Pi packages -- i inspect, u/U update, x remove, d disable, c config, f find, s settings -- or run setup plan/apply or config directly",
|
|
30
30
|
handler: async (args, ctx) => {
|
|
31
31
|
if (await handleSetupCommand(args, ctx, natives)) return;
|
|
32
32
|
if (await handleResourceConfigCommand(args, ctx, natives, showPackedPanel)) return;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { DialogTheme, MenuTheme, TabBarTheme } from "malevich-tui-components";
|
|
2
|
+
import type { CardTheme, DialogTheme, MenuTheme, TabBarTheme } 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. */
|
|
6
|
+
export function cardTheme(theme: Theme): CardTheme {
|
|
7
|
+
return {
|
|
8
|
+
border: (s) => theme.fg("border", s),
|
|
9
|
+
selectedBorder: (s) => theme.fg("accent", s),
|
|
10
|
+
content: (s) => s,
|
|
11
|
+
selectedContent: (s) => (typeof theme.inverse === "function" ? theme.inverse(theme.fg("text", s)) : theme.fg("accent", s)),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export function menuTheme(theme: Theme): MenuTheme {
|
|
7
16
|
return {
|
|
8
17
|
border: (s) => theme.fg("border", s),
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { matchesKey, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
+
import { buildDetailLines, type Component, type TextMeasure } from "malevich-tui-components";
|
|
4
|
+
import type { Natives, PackageInfo } from "./packed.js";
|
|
5
|
+
import { sanitizeTerminalText } from "./terminal-text.js";
|
|
6
|
+
|
|
7
|
+
const README_CHAR_LIMIT = 50_000;
|
|
8
|
+
const DETAIL_LINE_LIMIT = 2_000;
|
|
9
|
+
const VIEWPORT_LINES = 18;
|
|
10
|
+
|
|
11
|
+
export class PackageInspector implements Component {
|
|
12
|
+
private info: PackageInfo | undefined;
|
|
13
|
+
private error: string | undefined;
|
|
14
|
+
private loading = true;
|
|
15
|
+
private scrollOffset = 0;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly packageName: string,
|
|
19
|
+
private readonly natives: Natives,
|
|
20
|
+
private readonly theme: Theme,
|
|
21
|
+
private readonly measure: TextMeasure,
|
|
22
|
+
private readonly onClose: () => void,
|
|
23
|
+
private readonly requestRender: () => void,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async load(): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
this.info = await this.natives.info(this.packageName);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
31
|
+
} finally {
|
|
32
|
+
this.loading = false;
|
|
33
|
+
this.requestRender();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
invalidate(): void {}
|
|
38
|
+
|
|
39
|
+
handleInput(data: string): void {
|
|
40
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
41
|
+
this.onClose();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (matchesKey(data, "down") || data === "j") this.scrollOffset += 1;
|
|
45
|
+
else if (matchesKey(data, "up") || data === "k") this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
46
|
+
else if (data === "g") this.scrollOffset = 0;
|
|
47
|
+
else if (data === "G") this.scrollOffset = Number.MAX_SAFE_INTEGER;
|
|
48
|
+
this.requestRender();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(width: number): string[] {
|
|
52
|
+
const heading = this.theme.bold(this.theme.fg("accent", `Inspect ${this.packageName}`));
|
|
53
|
+
const help = this.theme.fg("muted", "esc/q close · ↑↓/j/k scroll · g/G top/bottom");
|
|
54
|
+
if (this.loading) return [heading, help, "", this.theme.fg("muted", "Loading registry metadata…")];
|
|
55
|
+
if (this.error) return [heading, help, "", this.theme.fg("error", this.error)];
|
|
56
|
+
if (!this.info) return [heading, help, "", this.theme.fg("muted", "No package metadata")];
|
|
57
|
+
|
|
58
|
+
const info = this.info;
|
|
59
|
+
const rawReadme = sanitizeTerminalText(info.readme?.trim() || "No README published.");
|
|
60
|
+
const readmeTruncated = rawReadme.length > README_CHAR_LIMIT;
|
|
61
|
+
const readme = rawReadme.slice(0, README_CHAR_LIMIT);
|
|
62
|
+
const detailMeasure: TextMeasure = { ...this.measure, wrapTextWithAnsi };
|
|
63
|
+
const details = buildDetailLines(Math.max(1, width), {
|
|
64
|
+
fields: [
|
|
65
|
+
{ label: "Version", value: sanitizeTerminalText(info.version) },
|
|
66
|
+
...(info.license ? [{ label: "License", value: sanitizeTerminalText(info.license) }] : []),
|
|
67
|
+
...(info.repository ? [{ label: "Repository", value: sanitizeTerminalText(info.repository) }] : []),
|
|
68
|
+
],
|
|
69
|
+
sections: [
|
|
70
|
+
...(info.description ? [{ heading: "Description", body: sanitizeTerminalText(info.description) }] : []),
|
|
71
|
+
{ heading: "README (registry metadata)", body: readme },
|
|
72
|
+
],
|
|
73
|
+
theme: {
|
|
74
|
+
field: (s) => this.theme.fg("dim", s),
|
|
75
|
+
heading: (s) => this.theme.bold(this.theme.fg("accent", s)),
|
|
76
|
+
byline: (s) => this.theme.fg("muted", s),
|
|
77
|
+
body: (s) => s,
|
|
78
|
+
},
|
|
79
|
+
measure: detailMeasure,
|
|
80
|
+
}).slice(0, DETAIL_LINE_LIMIT);
|
|
81
|
+
if (readmeTruncated || details.length === DETAIL_LINE_LIMIT) details.push(this.theme.fg("warning", "Output truncated."));
|
|
82
|
+
const maxOffset = Math.max(0, details.length - VIEWPORT_LINES);
|
|
83
|
+
this.scrollOffset = Math.min(this.scrollOffset, maxOffset);
|
|
84
|
+
const visible = details.slice(this.scrollOffset, this.scrollOffset + VIEWPORT_LINES);
|
|
85
|
+
return [
|
|
86
|
+
heading,
|
|
87
|
+
help,
|
|
88
|
+
this.theme.fg("dim", `${this.scrollOffset + 1}-${this.scrollOffset + visible.length}/${details.length}`),
|
|
89
|
+
"",
|
|
90
|
+
...visible,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -21,6 +21,8 @@ export interface TabHost {
|
|
|
21
21
|
* approve/reload call inside a tab's own mutation flow must go through
|
|
22
22
|
* this, not ctx directly. */
|
|
23
23
|
inlineCtx: ExtensionCommandContext;
|
|
24
|
+
/** Opens registry metadata and README for one package inside this overlay. */
|
|
25
|
+
inspectPackage(name: string): void;
|
|
24
26
|
/** Re-renders the whole overlay. Call after any state change a tab
|
|
25
27
|
* makes outside of a handleInput call the host already re-renders for. */
|
|
26
28
|
requestRender(): void;
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
* Install (behind a quick inline confirm, then the standard approval
|
|
10
10
|
* every mutation surface requires) does.
|
|
11
11
|
*/
|
|
12
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { Input, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
|
-
import type
|
|
15
|
+
import { Card, type Component } from "malevich-tui-components";
|
|
16
|
+
import { cardTheme } from "../menu-theme.js";
|
|
16
17
|
import type { Natives, PackageSummary } from "../packed.js";
|
|
17
18
|
import { InstallServiceError } from "../packed.js";
|
|
18
19
|
import type { TabHost } from "../tab-host.js";
|
|
20
|
+
import { sanitizeTerminalText } from "../terminal-text.js";
|
|
19
21
|
import { approvePackageOperation } from "../tools.js";
|
|
20
22
|
import { shouldSearch } from "./discover-model.js";
|
|
21
23
|
|
|
@@ -52,11 +54,6 @@ export async function applyInstall(result: PackageSummary, natives: Natives, ctx
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
interface Theme {
|
|
56
|
-
fg(color: string, s: string): string;
|
|
57
|
-
bold(s: string): string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
57
|
/** /packed's Find tab -- a real Component. Its query box always captures
|
|
61
58
|
* free text (isCapturingInput() is unconditionally true), unlike
|
|
62
59
|
* Packages/Config's toggleable search: there's no "browse mode" here to
|
|
@@ -69,13 +66,13 @@ export class FindTab implements Component {
|
|
|
69
66
|
private searching = false;
|
|
70
67
|
private error: string | undefined;
|
|
71
68
|
private busy = false;
|
|
72
|
-
private
|
|
69
|
+
private queryActive = true;
|
|
70
|
+
private readonly maxVisible = 4;
|
|
73
71
|
|
|
74
72
|
constructor(
|
|
75
73
|
private readonly natives: Natives,
|
|
76
74
|
private readonly host: TabHost,
|
|
77
|
-
|
|
78
|
-
private readonly theme: Theme,
|
|
75
|
+
private readonly _theme: Theme,
|
|
79
76
|
) {}
|
|
80
77
|
|
|
81
78
|
/** Find's query box is always in text-edit mode (no separate toggle like
|
|
@@ -103,8 +100,10 @@ export class FindTab implements Component {
|
|
|
103
100
|
invalidate(): void {}
|
|
104
101
|
|
|
105
102
|
render(width: number): string[] {
|
|
106
|
-
const
|
|
107
|
-
const hint =
|
|
103
|
+
const theme = this._theme;
|
|
104
|
+
const hint = this.queryActive
|
|
105
|
+
? rawKeyHint("enter", "search")
|
|
106
|
+
: `${rawKeyHint("enter", "install")}${theme.fg("muted", " · ")}${rawKeyHint("i", "inspect")}${theme.fg("muted", " · ")}${rawKeyHint("/", "query")}`;
|
|
108
107
|
const status = this.searching ? "searching…" : this.error ? theme.fg("error", this.error) : `${this.results.length} result(s)`;
|
|
109
108
|
const spacing = Math.max(1, width - visibleWidth(hint) - visibleWidth(status));
|
|
110
109
|
const line1 = truncateToWidth(`${hint}${" ".repeat(spacing)}`, width, "") + status;
|
|
@@ -118,11 +117,15 @@ export class FindTab implements Component {
|
|
|
118
117
|
for (let i = start; i < end; i++) {
|
|
119
118
|
const result = this.results[i]!;
|
|
120
119
|
const selected = i === this.selectedIndex;
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
const card = new Card({
|
|
121
|
+
title: theme.bold(`${result.name}@${result.version}`),
|
|
122
|
+
content: [sanitizeTerminalText(result.description ?? "No description")],
|
|
123
|
+
selected,
|
|
124
|
+
theme: cardTheme(theme),
|
|
125
|
+
measure: { visibleWidth, truncateToWidth },
|
|
126
|
+
});
|
|
127
|
+
lines.push(...card.render(width));
|
|
128
|
+
if (i < end - 1) lines.push("");
|
|
126
129
|
}
|
|
127
130
|
return lines;
|
|
128
131
|
}
|
|
@@ -137,11 +140,21 @@ export class FindTab implements Component {
|
|
|
137
140
|
if (this.results.length > 0) this.selectedIndex = (this.selectedIndex + 1) % this.results.length;
|
|
138
141
|
break;
|
|
139
142
|
case "\r":
|
|
140
|
-
if (shouldSearch(this.queryInput.getValue(), this.lastSearchedQuery, this.results.length > 0))
|
|
143
|
+
if (this.queryActive || shouldSearch(this.queryInput.getValue(), this.lastSearchedQuery, this.results.length > 0))
|
|
144
|
+
void this.runSearch();
|
|
141
145
|
else void this.installSelected();
|
|
142
146
|
return;
|
|
147
|
+
case "/":
|
|
148
|
+
this.queryActive = true;
|
|
149
|
+
break;
|
|
150
|
+
case "i": {
|
|
151
|
+
const result = this.results[this.selectedIndex];
|
|
152
|
+
if (!this.queryActive && result) this.host.inspectPackage(result.name);
|
|
153
|
+
else this.queryInput.handleInput(data);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
143
156
|
default:
|
|
144
|
-
this.queryInput.handleInput(data);
|
|
157
|
+
if (this.queryActive) this.queryInput.handleInput(data);
|
|
145
158
|
break;
|
|
146
159
|
}
|
|
147
160
|
this.host.requestRender();
|
|
@@ -158,6 +171,7 @@ export class FindTab implements Component {
|
|
|
158
171
|
this.results = response.results;
|
|
159
172
|
this.lastSearchedQuery = this.queryInput.getValue();
|
|
160
173
|
this.selectedIndex = 0;
|
|
174
|
+
this.queryActive = false;
|
|
161
175
|
} catch (e) {
|
|
162
176
|
this.error = e instanceof Error ? e.message : String(e);
|
|
163
177
|
this.results = [];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const UNSAFE_TERMINAL_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
|
|
2
|
+
|
|
3
|
+
/** Removes terminal control bytes from external registry text while preserving tabs and line breaks. */
|
|
4
|
+
export function sanitizeTerminalText(text: string): string {
|
|
5
|
+
return text.replace(UNSAFE_TERMINAL_CONTROL, "");
|
|
6
|
+
}
|
package/extension/src/tui.ts
CHANGED
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
* settled map U's batch path uses, not only as a scrollback toast. All
|
|
45
45
|
* data flows through the packed CLI (thin seam).
|
|
46
46
|
*/
|
|
47
|
-
import type
|
|
48
|
-
import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
47
|
+
import { DynamicBorder, type ExtensionCommandContext, rawKeyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
49
48
|
import { Container, Input, matchesKey, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
50
49
|
import {
|
|
50
|
+
Card,
|
|
51
51
|
type Component,
|
|
52
52
|
Dialog,
|
|
53
53
|
Envelope,
|
|
@@ -55,13 +55,12 @@ import {
|
|
|
55
55
|
type MenuItem,
|
|
56
56
|
Spinner,
|
|
57
57
|
TabbedContainer,
|
|
58
|
-
Table,
|
|
59
58
|
type TextMeasure,
|
|
60
59
|
} from "malevich-tui-components";
|
|
61
60
|
import { confirmReload } from "./approval/reload.js";
|
|
62
|
-
import { dialogTheme, menuTheme, panelFrameStyle, tabBarTheme } from "./menu-theme.js";
|
|
63
|
-
import type
|
|
64
|
-
import {
|
|
61
|
+
import { cardTheme, dialogTheme, menuTheme, panelFrameStyle, tabBarTheme } from "./menu-theme.js";
|
|
62
|
+
import { filterRows, mergeRows, nextMode, type Row, type ViewMode, visibleRows } from "./model.js";
|
|
63
|
+
import { PackageInspector } from "./package-inspector.js";
|
|
65
64
|
import type { Natives, PackageResources } from "./packed.js";
|
|
66
65
|
import type { TabHost } from "./tab-host.js";
|
|
67
66
|
import { createFindTab } from "./tabs/discover.js";
|
|
@@ -407,11 +406,6 @@ async function showActionMenu(ctx: ExtensionCommandContext, row: Row): Promise<"
|
|
|
407
406
|
);
|
|
408
407
|
}
|
|
409
408
|
|
|
410
|
-
interface PackagesTabTheme {
|
|
411
|
-
fg(color: string, s: string): string;
|
|
412
|
-
bold(s: string): string;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
409
|
/** Packages -- the panel's default/"home" tab. A real Component (not the
|
|
416
410
|
* panel's own top-level ctx.ui.custom owner anymore); the shared TabHost
|
|
417
411
|
* gives it inline approval/reload dialogs and a way to signal the overlay
|
|
@@ -432,14 +426,13 @@ export class PackagesTab implements Component {
|
|
|
432
426
|
private updatingRowName: string | undefined;
|
|
433
427
|
private readonly spinner = new Spinner();
|
|
434
428
|
private readonly settled = new Map<string, { ok: boolean; tail: string | undefined }>();
|
|
435
|
-
private readonly
|
|
436
|
-
private readonly maxVisible = 20;
|
|
429
|
+
private readonly maxVisible = 5;
|
|
437
430
|
|
|
438
431
|
constructor(
|
|
439
432
|
private readonly natives: Natives,
|
|
440
433
|
private readonly host: TabHost,
|
|
441
|
-
readonly theme:
|
|
442
|
-
measure: TextMeasure,
|
|
434
|
+
readonly theme: Theme,
|
|
435
|
+
private readonly measure: TextMeasure,
|
|
443
436
|
initialRows: Row[],
|
|
444
437
|
/** c on a row, or the Enter action menu's "Configure resources" --
|
|
445
438
|
* switches the shared TabbedContainer to Config, seeded with that
|
|
@@ -448,16 +441,6 @@ export class PackagesTab implements Component {
|
|
|
448
441
|
) {
|
|
449
442
|
this.rows = initialRows;
|
|
450
443
|
this.filtered = visibleRows(this.rows, this.mode);
|
|
451
|
-
this.table = new Table({
|
|
452
|
-
columns: [
|
|
453
|
-
{ header: "Package", key: "name" },
|
|
454
|
-
{ header: "Version", key: "version" },
|
|
455
|
-
{ header: "", key: "status" },
|
|
456
|
-
],
|
|
457
|
-
rows: [],
|
|
458
|
-
measure,
|
|
459
|
-
headerStyle: (s) => theme.fg("muted", s),
|
|
460
|
-
});
|
|
461
444
|
}
|
|
462
445
|
|
|
463
446
|
/** An active filter or an in-flight mutation means Escape/Left-Right
|
|
@@ -478,9 +461,7 @@ export class PackagesTab implements Component {
|
|
|
478
461
|
return this.searchActive || this.updatingRowName !== undefined;
|
|
479
462
|
}
|
|
480
463
|
|
|
481
|
-
invalidate(): void {
|
|
482
|
-
this.table.invalidate();
|
|
483
|
-
}
|
|
464
|
+
invalidate(): void {}
|
|
484
465
|
|
|
485
466
|
private applyFilter(): void {
|
|
486
467
|
this.filtered = filterRows(visibleRows(this.rows, this.mode), this.searchInput.getValue());
|
|
@@ -505,6 +486,8 @@ export class PackagesTab implements Component {
|
|
|
505
486
|
theme.fg("muted", " · ") +
|
|
506
487
|
rawKeyHint("d", "disable") +
|
|
507
488
|
theme.fg("muted", " · ") +
|
|
489
|
+
rawKeyHint("i", "inspect") +
|
|
490
|
+
theme.fg("muted", " · ") +
|
|
508
491
|
rawKeyHint("c", "config") +
|
|
509
492
|
theme.fg("muted", " · ") +
|
|
510
493
|
rawKeyHint("r", "refresh");
|
|
@@ -530,28 +513,29 @@ export class PackagesTab implements Component {
|
|
|
530
513
|
// selectedIndex stays this tab's own job.
|
|
531
514
|
const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filtered.length - this.maxVisible));
|
|
532
515
|
const end = Math.min(start + this.maxVisible, this.filtered.length);
|
|
533
|
-
this.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
})
|
|
553
|
-
|
|
554
|
-
|
|
516
|
+
for (const [offset, row] of this.filtered.slice(start, end).entries()) {
|
|
517
|
+
const selected = start + offset === this.selectedIndex;
|
|
518
|
+
// A settled row shows its real outcome even for the instant before
|
|
519
|
+
// the next row's "start" event moves updatingRowName off it.
|
|
520
|
+
const rowSettled = this.settled.get(row.name);
|
|
521
|
+
const isUpdating = !rowSettled && this.updatingRowName === row.name;
|
|
522
|
+
const status = isUpdating
|
|
523
|
+
? theme.fg("accent", `${this.spinner.glyph()} updating…`)
|
|
524
|
+
: rowSettled
|
|
525
|
+
? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
|
|
526
|
+
: row.hasUpdate
|
|
527
|
+
? theme.fg("warning", `↑${row.latest}`)
|
|
528
|
+
: theme.fg("muted", "installed");
|
|
529
|
+
const card = new Card({
|
|
530
|
+
title: theme.bold(row.name),
|
|
531
|
+
content: [`${theme.fg("dim", row.version)} · ${status}`],
|
|
532
|
+
selected,
|
|
533
|
+
theme: cardTheme(theme),
|
|
534
|
+
measure: this.measure,
|
|
535
|
+
});
|
|
536
|
+
lines.push(...card.render(width));
|
|
537
|
+
if (start + offset < end - 1) lines.push("");
|
|
538
|
+
}
|
|
555
539
|
const hasScroll = start > 0 || end < this.filtered.length;
|
|
556
540
|
lines.push(theme.fg("dim", ` ${hasScroll ? `${this.selectedIndex + 1}/${this.filtered.length} ` : ""}${this.mode}`));
|
|
557
541
|
return lines;
|
|
@@ -616,6 +600,11 @@ export class PackagesTab implements Component {
|
|
|
616
600
|
if (row) void this.runRowActionInline({ type: "disable", row });
|
|
617
601
|
return;
|
|
618
602
|
}
|
|
603
|
+
case "i": {
|
|
604
|
+
const row = this.filtered[this.selectedIndex];
|
|
605
|
+
if (row) this.host.inspectPackage(row.name);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
619
608
|
case "c": {
|
|
620
609
|
const row = this.filtered[this.selectedIndex];
|
|
621
610
|
if (row) this.switchTab("config", row.name);
|
|
@@ -765,6 +754,7 @@ function renderUnifiedPanel(
|
|
|
765
754
|
// host.inlineCtx below, exactly like the panel's own approval/reload
|
|
766
755
|
// dialogs always have been.
|
|
767
756
|
let pendingDialog: Dialog | undefined;
|
|
757
|
+
let packageInspector: PackageInspector | undefined;
|
|
768
758
|
|
|
769
759
|
function confirmInline(title: string, message: string): Promise<boolean> {
|
|
770
760
|
return new Promise((resolve) => {
|
|
@@ -788,15 +778,29 @@ function renderUnifiedPanel(
|
|
|
788
778
|
}
|
|
789
779
|
|
|
790
780
|
const inlineCtx: ExtensionCommandContext = { ...ctx, hasUI: true, ui: { ...ctx.ui, confirm: confirmInline } };
|
|
781
|
+
const measure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
791
782
|
const host: TabHost = {
|
|
792
783
|
ctx,
|
|
793
784
|
inlineCtx,
|
|
785
|
+
inspectPackage: (name) => {
|
|
786
|
+
packageInspector = new PackageInspector(
|
|
787
|
+
name,
|
|
788
|
+
natives,
|
|
789
|
+
theme,
|
|
790
|
+
measure,
|
|
791
|
+
() => {
|
|
792
|
+
packageInspector = undefined;
|
|
793
|
+
tui.requestRender();
|
|
794
|
+
},
|
|
795
|
+
() => tui.requestRender(),
|
|
796
|
+
);
|
|
797
|
+
void packageInspector.load();
|
|
798
|
+
tui.requestRender();
|
|
799
|
+
},
|
|
794
800
|
requestRender: () => tui.requestRender(),
|
|
795
801
|
onSessionReplaced: () => done(undefined),
|
|
796
802
|
};
|
|
797
803
|
|
|
798
|
-
const measure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
799
|
-
|
|
800
804
|
const packagesTab = new PackagesTab(natives, host, theme, measure, initialRows, (target, configFilter) => {
|
|
801
805
|
if (target === "config" && configFilter !== undefined) configTab.setFilter(configFilter);
|
|
802
806
|
tabbedContainer.setActive(target);
|
|
@@ -831,6 +835,7 @@ function renderUnifiedPanel(
|
|
|
831
835
|
],
|
|
832
836
|
theme: tabBarTheme(theme),
|
|
833
837
|
initialKey: initialTab,
|
|
838
|
+
measure,
|
|
834
839
|
// Malevich's own default matcher only recognizes legacy CSI sequences;
|
|
835
840
|
// pi-tui's real matchesKey also covers the Kitty keyboard protocol and
|
|
836
841
|
// xterm's modifyOtherKeys encodings for the same keys. Malevich's
|
|
@@ -856,7 +861,7 @@ function renderUnifiedPanel(
|
|
|
856
861
|
|
|
857
862
|
return {
|
|
858
863
|
render(width: number): string[] {
|
|
859
|
-
envelope.setContent(pendingDialog ?? tabbedContainer);
|
|
864
|
+
envelope.setContent(pendingDialog ?? packageInspector ?? tabbedContainer);
|
|
860
865
|
return envelope.render(width);
|
|
861
866
|
},
|
|
862
867
|
invalidate: () => envelope.invalidate(),
|
|
@@ -866,6 +871,10 @@ function renderUnifiedPanel(
|
|
|
866
871
|
tui.requestRender();
|
|
867
872
|
return;
|
|
868
873
|
}
|
|
874
|
+
if (packageInspector) {
|
|
875
|
+
packageInspector.handleInput(data);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
869
878
|
const activeKey = tabbedContainer.getActiveKey() as PackedTabKey;
|
|
870
879
|
const activeTab = tabByKey[activeKey];
|
|
871
880
|
// Each host-level key is checked against that tab's own capture flag
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@danypops/vehicle-core": "^0.12.3",
|
|
35
35
|
"@danypops/vehicle-server": "^0.17.1",
|
|
36
36
|
"jiti": "2.6.1",
|
|
37
|
-
"malevich-tui-components": "^0.
|
|
37
|
+
"malevich-tui-components": "^0.21.1",
|
|
38
38
|
"publint": "0.3.22",
|
|
39
39
|
"semver": "^7.8.5"
|
|
40
40
|
},
|