@danypops/pi-packed 0.17.0 → 0.19.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 -3
- package/extension/src/discover.ts +8 -0
- package/extension/src/keymap.ts +89 -0
- package/extension/src/menu-theme.ts +10 -1
- package/extension/src/resource-config.ts +9 -2
- package/extension/src/tui.ts +63 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,16 +10,16 @@ The extension connects to Packed's authenticated user daemon and starts the pack
|
|
|
10
10
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
|
-
- `/packed` -- one floating overlay, four tabs (Packages/Find/Config/Settings), always shown in a persistent tab bar at the top
|
|
13
|
+
- `/packed` -- one floating overlay, four tabs (Packages/Find/Config/Settings), always shown in a persistent tab bar at the top: the focused tab is inverted (a background highlight, no explicit foreground color), unfocused tabs show only a foreground color, and each label's own first letter is highlighted as its mnemonic on both. `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
14
|
- **Packages** (the default tab) -- 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):
|
|
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
18
|
- `c` -- jump to the Config tab, scoped to the selected package.
|
|
19
19
|
- `f` -- jump to the Find tab. `s` -- jump to the Settings tab.
|
|
20
|
-
- `Enter` opens a smaller floating action menu with all of the above. `r` refreshes, `/` filters, `
|
|
20
|
+
- `Enter` opens a smaller floating action menu with all of the above. `r` refreshes, `/` filters, `v` cycles view modes.
|
|
21
21
|
- **Find** -- search the npm registry for new Pi packages and install one.
|
|
22
|
-
- **Config** -- enable or disable individual extensions, skills, prompt templates, and themes declared by installed packages, per package, at global or project scope (`
|
|
22
|
+
- **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
23
|
- **Settings** -- change whether package mutations (install/update/remove/toggle) require confirmation.
|
|
24
24
|
- `/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.
|
|
25
25
|
- `/profile [name]` -- switch to a named Pi profile (provider, model, tools, instructions, theme), reading `~/.pi/agent/profiles.json` and a trusted project's own `.pi/profiles.json`. `pi --profile <name>` activates one at startup; `Ctrl+Shift+U` cycles through them.
|
|
@@ -88,6 +88,14 @@ export class FindTab implements Component {
|
|
|
88
88
|
return this.busy;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** Unlike capturesEscape, this is ALWAYS true, busy or not: every
|
|
92
|
+
* printable character (a letter that would otherwise be a global
|
|
93
|
+
* tab-jump mnemonic included) is query text here, always -- there's no
|
|
94
|
+
* separate "browse mode" to fall back to the way Packages/Config have. */
|
|
95
|
+
capturesMnemonics(): boolean {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
invalidate(): void {}
|
|
92
100
|
|
|
93
101
|
render(width: number): string[] {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keymap.ts — the /packed panel's own real keybindings, modeled as a
|
|
3
|
+
* Malevich MnemonicContext tree so findMnemonicConflicts can verify no
|
|
4
|
+
* two actions genuinely reachable at once share a key. This is a
|
|
5
|
+
* hand-maintained mirror of tui.ts/resource-config.ts/discover.ts/
|
|
6
|
+
* security-tui.ts's actual switch statements, not derived from them --
|
|
7
|
+
* keep it in sync when a keybinding changes; keymap.test.ts's own
|
|
8
|
+
* conflict-free assertion is the thing that actually catches drift into
|
|
9
|
+
* a genuine collision, not this file's own accuracy.
|
|
10
|
+
*
|
|
11
|
+
* Tree shape mirrors the real runtime dispatch in tui.ts's
|
|
12
|
+
* renderUnifiedPanel: Packages is a direct child of "global" (Tab/Shift-
|
|
13
|
+
* Tab/Left/Right/Escape only -- Packages' own f/c/s bindings already do
|
|
14
|
+
* the same jump the generic mnemonic dispatch would, so that dispatch is
|
|
15
|
+
* skipped entirely while Packages is active, and modeled here as a
|
|
16
|
+
* SEPARATE branch("mnemonic-jump") that only Find/Config/Settings fall
|
|
17
|
+
* under -- matching the real `if (activeKey !== "packages")` guard.
|
|
18
|
+
*/
|
|
19
|
+
import type { MnemonicContext } from "malevich-tui-components";
|
|
20
|
+
|
|
21
|
+
export const PANEL_MNEMONIC_TREE: MnemonicContext = {
|
|
22
|
+
name: "global",
|
|
23
|
+
bindings: [
|
|
24
|
+
{ key: "\x1b", description: "back to Packages, or close from Packages" },
|
|
25
|
+
{ key: "\t", description: "next tab" },
|
|
26
|
+
{ key: "\x1b[Z", description: "previous tab" },
|
|
27
|
+
{ key: "\x1b[C", description: "next tab" },
|
|
28
|
+
{ key: "\x1b[D", description: "previous tab" },
|
|
29
|
+
],
|
|
30
|
+
children: [
|
|
31
|
+
{
|
|
32
|
+
name: "packages",
|
|
33
|
+
bindings: [
|
|
34
|
+
{ key: "\x1b[A", description: "move up" },
|
|
35
|
+
{ key: "\x1b[B", description: "move down" },
|
|
36
|
+
{ key: "v", description: "cycle view mode" },
|
|
37
|
+
{ key: "/", description: "start filter" },
|
|
38
|
+
{ key: "r", description: "refresh" },
|
|
39
|
+
{ key: "U", description: "update every outdated package" },
|
|
40
|
+
{ key: "u", description: "update selected package" },
|
|
41
|
+
{ key: "x", description: "remove selected package" },
|
|
42
|
+
{ key: "d", description: "disable/enable selected package's extensions" },
|
|
43
|
+
{ key: "c", description: "jump to Config, scoped to selected package" },
|
|
44
|
+
{ key: "f", description: "jump to Find" },
|
|
45
|
+
{ key: "s", description: "jump to Settings" },
|
|
46
|
+
{ key: "\r", description: "open the row action menu" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "mnemonic-jump (Find/Config/Settings only -- Packages' own f/c/s already cover this)",
|
|
51
|
+
bindings: [
|
|
52
|
+
{ key: "p", description: "jump to Packages" },
|
|
53
|
+
{ key: "f", description: "jump to Find" },
|
|
54
|
+
{ key: "c", description: "jump to Config" },
|
|
55
|
+
{ key: "s", description: "jump to Settings" },
|
|
56
|
+
],
|
|
57
|
+
children: [
|
|
58
|
+
{
|
|
59
|
+
name: "find",
|
|
60
|
+
bindings: [
|
|
61
|
+
{ key: "\r", description: "search, or install the highlighted result" },
|
|
62
|
+
{ key: "\x1b[A", description: "move up in results" },
|
|
63
|
+
{ key: "\x1b[B", description: "move down in results" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "config",
|
|
68
|
+
bindings: [
|
|
69
|
+
{ key: "\x1b[A", description: "move up" },
|
|
70
|
+
{ key: "\x1b[B", description: "move down" },
|
|
71
|
+
{ key: "v", description: "switch global/project scope" },
|
|
72
|
+
{ key: "/", description: "start filter" },
|
|
73
|
+
{ key: "r", description: "refresh" },
|
|
74
|
+
{ key: " ", description: "toggle selected resource" },
|
|
75
|
+
{ key: "\r", description: "toggle selected resource" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "settings",
|
|
80
|
+
bindings: [
|
|
81
|
+
{ key: "\x1b[A", description: "move up" },
|
|
82
|
+
{ key: "\x1b[B", description: "move down" },
|
|
83
|
+
{ key: "\r", description: "change selected option" },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
@@ -31,7 +31,16 @@ export function dialogTheme(theme: Theme): DialogTheme {
|
|
|
31
31
|
* overlay, replacing what used to be four separately-opened screens. */
|
|
32
32
|
export function tabBarTheme(theme: Theme): TabBarTheme {
|
|
33
33
|
return {
|
|
34
|
+
// Unfocused: foreground color, no background. Focused: inverted --
|
|
35
|
+
// a background color and no explicit foreground, so it reads as a
|
|
36
|
+
// real highlighted/selected tab rather than just a differently
|
|
37
|
+
// colored label.
|
|
34
38
|
tab: (s) => theme.fg("muted", s),
|
|
35
|
-
activeTab: (s) => theme.bold(theme.
|
|
39
|
+
activeTab: (s) => theme.bold(theme.bg("selectedBg", s)),
|
|
40
|
+
// Every tab's first letter is a jump mnemonic (p/f/c/s) -- a distinct
|
|
41
|
+
// warning-colored bold, so it reads as "press this letter" on both the
|
|
42
|
+
// active and inactive tabs alike, not just whichever style tab/activeTab
|
|
43
|
+
// already applies to the whole label.
|
|
44
|
+
mnemonic: (s) => theme.bold(theme.fg("warning", s)),
|
|
36
45
|
};
|
|
37
46
|
}
|
|
@@ -153,6 +153,13 @@ export class ConfigTab implements Component {
|
|
|
153
153
|
return this.searchActive || this.busy;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/** Same condition as capturesEscape -- an active filter or an in-flight
|
|
157
|
+
* toggle means every printable key (including a letter that would
|
|
158
|
+
* otherwise be a global tab-jump mnemonic) belongs to this tab right now. */
|
|
159
|
+
capturesMnemonics(): boolean {
|
|
160
|
+
return this.searchActive || this.busy;
|
|
161
|
+
}
|
|
162
|
+
|
|
156
163
|
invalidate(): void {}
|
|
157
164
|
|
|
158
165
|
render(width: number): string[] {
|
|
@@ -160,7 +167,7 @@ export class ConfigTab implements Component {
|
|
|
160
167
|
const scopeLabel = theme.bold(this.scope === "project" ? "Project Resources" : "Global Resources");
|
|
161
168
|
const hint = this.searchActive
|
|
162
169
|
? rawKeyHint("esc", "clear")
|
|
163
|
-
: rawKeyHint("space", "toggle") + theme.fg("muted", "
|
|
170
|
+
: rawKeyHint("space", "toggle") + theme.fg("muted", " · ") + rawKeyHint("/", "filter") + theme.fg("muted", " · ") + rawKeyHint("v", "scope") + theme.fg("muted", " · ") + rawKeyHint("r", "refresh");
|
|
164
171
|
const spacing = Math.max(1, width - visibleWidth(scopeLabel) - visibleWidth(hint));
|
|
165
172
|
const line1 = truncateToWidth(`${scopeLabel}${" ".repeat(spacing)}${hint}`, width, "");
|
|
166
173
|
const line2 = truncateToWidth(theme.fg("muted", `${this.items.length} resource(s) \u00b7 extensions need a reload after a change`), width, "");
|
|
@@ -199,7 +206,7 @@ export class ConfigTab implements Component {
|
|
|
199
206
|
switch (raw) {
|
|
200
207
|
case "\x1b[A": this.selectedIndex = (this.selectedIndex - 1 + this.filtered.length) % Math.max(this.filtered.length, 1); break;
|
|
201
208
|
case "\x1b[B": this.selectedIndex = (this.selectedIndex + 1) % Math.max(this.filtered.length, 1); break;
|
|
202
|
-
case "
|
|
209
|
+
case "v": // Tab is now reserved globally for sweeping between menus (TabbedContainer)
|
|
203
210
|
this.scope = this.scope === "global" ? "project" : "global";
|
|
204
211
|
this.rebuildItems();
|
|
205
212
|
this.applyFilter();
|
package/extension/src/tui.ts
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
*/
|
|
47
47
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
48
48
|
import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
49
|
-
import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
49
|
+
import { Container, Input, matchesKey, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
50
50
|
import { Dialog, Envelope, Menu, Spinner, Table, TabbedContainer, type Component, type MenuItem, type TextMeasure } from "malevich-tui-components";
|
|
51
51
|
import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
|
|
52
52
|
import type { Row, ViewMode } from "./model.js";
|
|
@@ -429,6 +429,16 @@ export class PackagesTab implements Component {
|
|
|
429
429
|
return this.searchActive || this.updatingRowName !== undefined;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/** Same condition as capturesEscape -- an active filter or an in-flight
|
|
433
|
+
* mutation means every printable key (including a letter that would
|
|
434
|
+
* otherwise be a global tab-jump mnemonic) belongs to this tab right now.
|
|
435
|
+
* Packages itself never needs this check at the host level (see
|
|
436
|
+
* renderUnifiedPanel's own dispatcher), but implementing it keeps this
|
|
437
|
+
* tab's own contract honest and consistent with the other three. */
|
|
438
|
+
capturesMnemonics(): boolean {
|
|
439
|
+
return this.searchActive || this.updatingRowName !== undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
432
442
|
invalidate(): void {
|
|
433
443
|
this.table.invalidate();
|
|
434
444
|
}
|
|
@@ -462,7 +472,7 @@ export class PackagesTab implements Component {
|
|
|
462
472
|
const line1 = truncateToWidth(hint, width, "");
|
|
463
473
|
const dot = "·";
|
|
464
474
|
const line2 = truncateToWidth(
|
|
465
|
-
theme.fg("muted", `${this.statusLine()} ${dot} view: ${this.mode} ${dot} / filter ${dot}
|
|
475
|
+
theme.fg("muted", `${this.statusLine()} ${dot} view: ${this.mode} ${dot} / filter ${dot} v view ${dot} ${this.rows.length} installed`),
|
|
466
476
|
width,
|
|
467
477
|
"",
|
|
468
478
|
);
|
|
@@ -528,7 +538,7 @@ export class PackagesTab implements Component {
|
|
|
528
538
|
case "\x1b[B":
|
|
529
539
|
this.selectedIndex = (this.selectedIndex + 1) % Math.max(this.filtered.length, 1);
|
|
530
540
|
break;
|
|
531
|
-
case "
|
|
541
|
+
case "v": // Tab is now reserved globally for sweeping between menus (TabbedContainer)
|
|
532
542
|
this.mode = nextMode(this.mode);
|
|
533
543
|
this.applyFilter();
|
|
534
544
|
break;
|
|
@@ -655,9 +665,19 @@ interface TabScope {
|
|
|
655
665
|
* Packages, or close from Packages" handling. Defaults to false. */
|
|
656
666
|
capturesEscape?(): boolean;
|
|
657
667
|
/** True while this tab wants Left/Right for itself right now (Find's
|
|
658
|
-
* always-on query cursor) instead of the host's own tab-cycling
|
|
659
|
-
*
|
|
668
|
+
* always-on query cursor) instead of the host's own tab-cycling via
|
|
669
|
+
* TabbedContainer. Tab/Shift-Tab have no such exception -- nothing needs
|
|
670
|
+
* a literal Tab character for itself, so they always sweep between
|
|
671
|
+
* menus. Defaults to false. */
|
|
660
672
|
capturesHorizontalArrows?(): boolean;
|
|
673
|
+
/** True while this tab wants every printable character for itself (an
|
|
674
|
+
* active filter, an in-flight mutation, or Find's permanent query box)
|
|
675
|
+
* instead of the host's own mnemonic letter-jump (p/f/c/s). Defaults to
|
|
676
|
+
* false. Never consulted while Packages itself is active -- Packages'
|
|
677
|
+
* own f/c/s bindings already do the same jump (c additionally scopes
|
|
678
|
+
* Config to the selected row), so the generic host-level jump only
|
|
679
|
+
* matters for reaching a tab from one of the OTHER three. */
|
|
680
|
+
capturesMnemonics?(): boolean;
|
|
661
681
|
}
|
|
662
682
|
|
|
663
683
|
export async function showPackedPanel(
|
|
@@ -759,6 +779,13 @@ function renderUnifiedPanel(
|
|
|
759
779
|
],
|
|
760
780
|
theme: tabBarTheme(theme),
|
|
761
781
|
initialKey: initialTab,
|
|
782
|
+
// Malevich's own default matcher only recognizes legacy CSI sequences;
|
|
783
|
+
// pi-tui's real matchesKey also covers the Kitty keyboard protocol and
|
|
784
|
+
// xterm's modifyOtherKeys encodings for the same keys. Malevich's
|
|
785
|
+
// KeyMatcher type takes a bare string (it doesn't share pi-tui's KeyId
|
|
786
|
+
// union), so this only ever forwards the small fixed set of key names
|
|
787
|
+
// TabbedContainer itself actually calls with (left/right/tab/shift+tab).
|
|
788
|
+
matchesKey: (data, keyId) => matchesKey(data, keyId as Parameters<typeof matchesKey>[1]),
|
|
762
789
|
});
|
|
763
790
|
|
|
764
791
|
// measure must be explicit: Envelope's own default is ASCII-only (raw
|
|
@@ -799,11 +826,41 @@ function renderUnifiedPanel(
|
|
|
799
826
|
tui.requestRender();
|
|
800
827
|
return;
|
|
801
828
|
}
|
|
802
|
-
|
|
829
|
+
// Tab/Shift-Tab always sweep between menus -- nothing needs a literal
|
|
830
|
+
// Tab character for itself (Packages'/Config's own former Tab bindings
|
|
831
|
+
// moved to v once Tab was claimed globally; see the mnemonics.test.ts
|
|
832
|
+
// conflict check for why that reassignment was necessary). Real
|
|
833
|
+
// matchesKey(), not a hardcoded "\x1b[Z" literal, because Shift-Tab has
|
|
834
|
+
// no single universal encoding: legacy terminals send CSI Z, but the
|
|
835
|
+
// Kitty keyboard protocol and xterm's modifyOtherKeys mode both send a
|
|
836
|
+
// different sequence for the same keypress -- a literal check silently
|
|
837
|
+
// misses whichever ones it doesn't happen to be pinned to.
|
|
838
|
+
if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
|
|
839
|
+
tabbedContainer.handleInput(data);
|
|
840
|
+
tui.requestRender();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if ((matchesKey(data, "right") || matchesKey(data, "left")) && !(activeTab.capturesHorizontalArrows?.() ?? false)) {
|
|
803
844
|
tabbedContainer.handleInput(data); // owns Left/Right cycling itself
|
|
804
845
|
tui.requestRender();
|
|
805
846
|
return;
|
|
806
847
|
}
|
|
848
|
+
// The first letter of each tab's label is a jump mnemonic
|
|
849
|
+
// (Packages/Find/Config/Settings -> p/f/c/s), highlighted in the tab
|
|
850
|
+
// bar itself -- but only reachable FROM one of the other three tabs.
|
|
851
|
+
// Packages' own f/c/s bindings already do the same jump (c
|
|
852
|
+
// additionally scopes Config to the selected row); letting the
|
|
853
|
+
// generic version fire there too would just be redundant, not wrong,
|
|
854
|
+
// but skipping it keeps this one dispatcher the single source of
|
|
855
|
+
// truth for "which code path actually owns key X" per active tab.
|
|
856
|
+
if (activeKey !== "packages" && data.length === 1) {
|
|
857
|
+
const target = tabbedContainer.resolveMnemonic(data);
|
|
858
|
+
if (target && target !== activeKey && !(activeTab.capturesMnemonics?.() ?? false)) {
|
|
859
|
+
tabbedContainer.setActive(target as PackedTabKey);
|
|
860
|
+
tui.requestRender();
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
807
864
|
activeTab.handleInput?.(data);
|
|
808
865
|
tui.requestRender();
|
|
809
866
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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.14.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@earendil-works/pi-coding-agent": "*",
|