@danypops/pi-packed 0.19.5 → 0.19.6
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/extension/src/discover.ts +10 -3
- package/extension/src/index.ts +6 -5
- package/extension/src/packed.ts +28 -6
- package/extension/src/profile.ts +123 -43
- package/extension/src/resource-config.ts +55 -17
- package/extension/src/security-tui.ts +9 -2
- package/extension/src/setup-command.ts +10 -2
- package/extension/src/tool-output.ts +50 -18
- package/extension/src/tools.ts +46 -31
- package/extension/src/tui.ts +240 -183
- package/package.json +3 -3
package/extension/src/tui.ts
CHANGED
|
@@ -47,17 +47,27 @@
|
|
|
47
47
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
48
48
|
import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
49
49
|
import { Container, Input, matchesKey, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
50
|
-
import {
|
|
51
|
-
|
|
50
|
+
import {
|
|
51
|
+
type Component,
|
|
52
|
+
Dialog,
|
|
53
|
+
Envelope,
|
|
54
|
+
Menu,
|
|
55
|
+
type MenuItem,
|
|
56
|
+
Spinner,
|
|
57
|
+
TabbedContainer,
|
|
58
|
+
Table,
|
|
59
|
+
type TextMeasure,
|
|
60
|
+
} from "malevich-tui-components";
|
|
61
|
+
import { createFindTab } from "./discover.js";
|
|
62
|
+
import { dialogTheme, menuTheme, tabBarTheme } from "./menu-theme.js";
|
|
52
63
|
import type { Row, ViewMode } from "./model.js";
|
|
64
|
+
import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
|
|
53
65
|
import type { Natives, PackageResources } from "./packed.js";
|
|
54
|
-
import { approvePackageOperation } from "./tools.js";
|
|
55
|
-
import { createSettingsTab, SettingsTab } from "./security-tui.js";
|
|
56
|
-
import { createConfigTab, ConfigTab, applyResourceToggle } from "./resource-config.js";
|
|
57
|
-
import { createFindTab, FindTab } from "./discover.js";
|
|
58
|
-
import { dialogTheme, menuTheme, tabBarTheme } from "./menu-theme.js";
|
|
59
66
|
import { confirmReload } from "./reload.js";
|
|
67
|
+
import { applyResourceToggle, ConfigTab } from "./resource-config.js";
|
|
68
|
+
import { SettingsTab } from "./security-tui.js";
|
|
60
69
|
import type { TabHost } from "./tab-host.js";
|
|
70
|
+
import { approvePackageOperation } from "./tools.js";
|
|
61
71
|
|
|
62
72
|
export type PackedTabKey = "packages" | "find" | "config" | "settings";
|
|
63
73
|
|
|
@@ -75,7 +85,10 @@ export type PackageChoiceOutcome = "changed" | "unchanged" | "cancelled" | "defe
|
|
|
75
85
|
* it inline (a row's own status cell) isn't limited to the scrollback
|
|
76
86
|
* toast every outcome already gets via ctx.ui.notify. "changed" never
|
|
77
87
|
* calls this: ctx.reload() already replaces the session by then. */
|
|
78
|
-
export interface PackageChoiceSettled {
|
|
88
|
+
export interface PackageChoiceSettled {
|
|
89
|
+
ok: boolean;
|
|
90
|
+
message: string;
|
|
91
|
+
}
|
|
79
92
|
|
|
80
93
|
export async function applyPackageChoice(
|
|
81
94
|
choice: string | undefined,
|
|
@@ -102,7 +115,8 @@ export async function applyPackageChoice(
|
|
|
102
115
|
onSettled?.({ ok: true, message: reason });
|
|
103
116
|
return "unchanged";
|
|
104
117
|
}
|
|
105
|
-
const transition =
|
|
118
|
+
const transition =
|
|
119
|
+
outcome.previousVersion && outcome.currentVersion ? ` (${outcome.previousVersion} → ${outcome.currentVersion})` : "";
|
|
106
120
|
if (!(await confirmReload(ctx))) {
|
|
107
121
|
ctx.ui.notify(`Updated ${row.name}${transition}; reload pending -- run /reload when ready.`, "warning");
|
|
108
122
|
onSettled?.({ ok: true, message: `updated${transition}; reload pending` });
|
|
@@ -144,15 +158,27 @@ export async function applyPackageChoice(
|
|
|
144
158
|
return "cancelled";
|
|
145
159
|
}
|
|
146
160
|
|
|
147
|
-
interface UpdateAllResult {
|
|
161
|
+
interface UpdateAllResult {
|
|
162
|
+
changed: number;
|
|
163
|
+
failedNames: string[];
|
|
164
|
+
}
|
|
148
165
|
|
|
149
166
|
/** Present only on phase "done" -- the real captured stdout+stderr from
|
|
150
167
|
* ExecInstaller (ok: true) or the thrown error's message (ok: false). This
|
|
151
168
|
* is the actual execution output, not a synthetic status string, so a host
|
|
152
169
|
* can show a genuine success/failure sign instead of guessing from
|
|
153
170
|
* reloadRequired alone. */
|
|
154
|
-
interface UpdateProgressResult {
|
|
155
|
-
|
|
171
|
+
interface UpdateProgressResult {
|
|
172
|
+
ok: boolean;
|
|
173
|
+
output: string;
|
|
174
|
+
}
|
|
175
|
+
interface UpdateProgressEvent {
|
|
176
|
+
row: Row;
|
|
177
|
+
index: number;
|
|
178
|
+
total: number;
|
|
179
|
+
phase: "start" | "done";
|
|
180
|
+
result?: UpdateProgressResult;
|
|
181
|
+
}
|
|
156
182
|
|
|
157
183
|
/** The core sequential-update loop, with no UI of its own -- reports each
|
|
158
184
|
* step via onProgress so any host surface (a floating overlay, or the
|
|
@@ -220,7 +246,10 @@ async function approveAndRunUpdateAll(
|
|
|
220
246
|
const { changed, failedNames } = await runBatch(outdated, approval.approved);
|
|
221
247
|
for (const failure of failedNames) ctx.ui.notify(`update failed: ${failure}`, "error");
|
|
222
248
|
if (changed === 0) {
|
|
223
|
-
ctx.ui.notify(
|
|
249
|
+
ctx.ui.notify(
|
|
250
|
+
failedNames.length > 0 ? `No packages updated; ${failedNames.length} failed.` : "All packages already up to date.",
|
|
251
|
+
failedNames.length > 0 ? "warning" : "info",
|
|
252
|
+
);
|
|
224
253
|
return failedNames.length > 0 ? "cancelled" : "unchanged";
|
|
225
254
|
}
|
|
226
255
|
const failedSuffix = failedNames.length > 0 ? `, ${failedNames.length} failed` : "";
|
|
@@ -261,7 +290,10 @@ async function runUpdatesWithProgress(
|
|
|
261
290
|
container.addChild(border());
|
|
262
291
|
container.addChild({ invalidate() {}, render: (_width: number) => [theme.bold("Updating packages")] });
|
|
263
292
|
container.addChild(new Spacer(1));
|
|
264
|
-
container.addChild({
|
|
293
|
+
container.addChild({
|
|
294
|
+
invalidate() {},
|
|
295
|
+
render: (width: number) => [truncateToWidth(`${theme.fg("accent", spinner.glyph())} ${currentLabel}`, width, "")],
|
|
296
|
+
});
|
|
265
297
|
container.addChild(new Spacer(1));
|
|
266
298
|
container.addChild({
|
|
267
299
|
invalidate() {},
|
|
@@ -280,7 +312,14 @@ async function runUpdatesWithProgress(
|
|
|
280
312
|
settledLines.push(`${glyph} ${event.row.name}${tail ? theme.fg("dim", ` -- ${tail}`) : ""}`);
|
|
281
313
|
}
|
|
282
314
|
tui.requestRender();
|
|
283
|
-
})
|
|
315
|
+
})
|
|
316
|
+
.finally(() => spinner.stop())
|
|
317
|
+
.then(done)
|
|
318
|
+
// performUpdateAll itself never rejects (every per-row failure is caught and
|
|
319
|
+
// reported via onProgress); this only guards against a genuinely unexpected
|
|
320
|
+
// throw (e.g. a bug in the onProgress callback above) leaving the overlay
|
|
321
|
+
// open forever with done() never called.
|
|
322
|
+
.catch((error: unknown) => done({ changed: 0, failedNames: [error instanceof Error ? error.message : String(error)] }));
|
|
284
323
|
|
|
285
324
|
return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput() {} };
|
|
286
325
|
},
|
|
@@ -342,10 +381,7 @@ export async function applyDisableExtensions(row: Row, natives: Natives, ctx: Ex
|
|
|
342
381
|
|
|
343
382
|
async function loadRows(natives: Natives): Promise<{ rows: Row[]; error?: string }> {
|
|
344
383
|
try {
|
|
345
|
-
const [installed, updates] = await Promise.all([
|
|
346
|
-
natives.installed(),
|
|
347
|
-
natives.updates().catch(() => []),
|
|
348
|
-
]);
|
|
384
|
+
const [installed, updates] = await Promise.all([natives.installed(), natives.updates().catch(() => [])]);
|
|
349
385
|
return { rows: mergeRows(installed, updates) };
|
|
350
386
|
} catch (e) {
|
|
351
387
|
return { rows: [], error: e instanceof Error ? e.message : String(e) };
|
|
@@ -371,7 +407,10 @@ async function showActionMenu(ctx: ExtensionCommandContext, row: Row): Promise<"
|
|
|
371
407
|
);
|
|
372
408
|
}
|
|
373
409
|
|
|
374
|
-
interface PackagesTabTheme {
|
|
410
|
+
interface PackagesTabTheme {
|
|
411
|
+
fg(color: string, s: string): string;
|
|
412
|
+
bold(s: string): string;
|
|
413
|
+
}
|
|
375
414
|
|
|
376
415
|
/** Packages -- the panel's default/"home" tab. A real Component (not the
|
|
377
416
|
* panel's own top-level ctx.ui.custom owner anymore); the shared TabHost
|
|
@@ -399,7 +438,7 @@ export class PackagesTab implements Component {
|
|
|
399
438
|
constructor(
|
|
400
439
|
private readonly natives: Natives,
|
|
401
440
|
private readonly host: TabHost,
|
|
402
|
-
|
|
441
|
+
readonly theme: PackagesTabTheme,
|
|
403
442
|
measure: TextMeasure,
|
|
404
443
|
initialRows: Row[],
|
|
405
444
|
/** c on a row, or the Enter action menu's "Configure resources" --
|
|
@@ -472,7 +511,10 @@ export class PackagesTab implements Component {
|
|
|
472
511
|
const line1 = truncateToWidth(hint, width, "");
|
|
473
512
|
const dot = "·";
|
|
474
513
|
const line2 = truncateToWidth(
|
|
475
|
-
theme.fg(
|
|
514
|
+
theme.fg(
|
|
515
|
+
"muted",
|
|
516
|
+
`${this.statusLine()} ${dot} view: ${this.mode} ${dot} / filter ${dot} v view ${dot} ${this.rows.length} installed`,
|
|
517
|
+
),
|
|
476
518
|
width,
|
|
477
519
|
"",
|
|
478
520
|
);
|
|
@@ -503,7 +545,9 @@ export class PackagesTab implements Component {
|
|
|
503
545
|
? theme.fg("accent", `${this.spinner.glyph()} updating…`)
|
|
504
546
|
: rowSettled
|
|
505
547
|
? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
|
|
506
|
-
: row.hasUpdate
|
|
548
|
+
: row.hasUpdate
|
|
549
|
+
? theme.fg("warning", `↑${row.latest}`)
|
|
550
|
+
: "";
|
|
507
551
|
return { name: `${cursor}${name}`, version: theme.fg("dim", row.version), status };
|
|
508
552
|
}),
|
|
509
553
|
);
|
|
@@ -612,9 +656,16 @@ export class PackagesTab implements Component {
|
|
|
612
656
|
// settled map U's batch path already renders -- not just a scrollback
|
|
613
657
|
// toast.
|
|
614
658
|
const onSettled = (result: PackageChoiceSettled) => this.settled.set(action.row.name, { ok: result.ok, tail: result.message });
|
|
615
|
-
const outcome =
|
|
616
|
-
|
|
617
|
-
|
|
659
|
+
const outcome =
|
|
660
|
+
action.type === "disable"
|
|
661
|
+
? await applyDisableExtensions(action.row, this.natives, this.host.inlineCtx)
|
|
662
|
+
: await applyPackageChoice(
|
|
663
|
+
action.type === "update" ? `Update to ${action.row.latest}` : "Remove",
|
|
664
|
+
action.row,
|
|
665
|
+
this.natives,
|
|
666
|
+
this.host.inlineCtx,
|
|
667
|
+
onSettled,
|
|
668
|
+
);
|
|
618
669
|
this.updatingRowName = undefined;
|
|
619
670
|
if (outcome === "changed") {
|
|
620
671
|
this.host.onSessionReplaced(); // ctx.reload() already replaced the session
|
|
@@ -706,172 +757,178 @@ function renderUnifiedPanel(
|
|
|
706
757
|
initialTab: PackedTabKey,
|
|
707
758
|
initialConfigFilter: string | undefined,
|
|
708
759
|
): Promise<void> {
|
|
709
|
-
return ctx.ui.custom<void>(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
760
|
+
return ctx.ui.custom<void>(
|
|
761
|
+
(tui, theme, _kb, done) => {
|
|
762
|
+
// A y/n decision rendered as this SAME overlay's own content (via a real
|
|
763
|
+
// Malevich Dialog, dispatched by literal key press) instead of
|
|
764
|
+
// ctx.ui.confirm's separate native dialog -- shared by every tab via
|
|
765
|
+
// host.inlineCtx below, exactly like the panel's own approval/reload
|
|
766
|
+
// dialogs always have been.
|
|
767
|
+
let pendingDialog: Dialog | undefined;
|
|
768
|
+
|
|
769
|
+
function confirmInline(title: string, message: string): Promise<boolean> {
|
|
770
|
+
return new Promise((resolve) => {
|
|
771
|
+
const settle = (value: boolean) => {
|
|
772
|
+
pendingDialog = undefined;
|
|
773
|
+
tui.requestRender();
|
|
774
|
+
resolve(value);
|
|
775
|
+
};
|
|
776
|
+
pendingDialog = new Dialog({
|
|
777
|
+
title,
|
|
778
|
+
body: message,
|
|
779
|
+
actions: [
|
|
780
|
+
{ label: "Yes", key: "y", action: () => settle(true) },
|
|
781
|
+
{ label: "No", key: "n", action: () => settle(false) },
|
|
782
|
+
],
|
|
783
|
+
theme: dialogTheme(theme),
|
|
784
|
+
framed: false, // this overlay's own Envelope already draws a border; a second rule would double up on it
|
|
785
|
+
});
|
|
721
786
|
tui.requestRender();
|
|
722
|
-
resolve(value);
|
|
723
|
-
};
|
|
724
|
-
pendingDialog = new Dialog({
|
|
725
|
-
title,
|
|
726
|
-
body: message,
|
|
727
|
-
actions: [
|
|
728
|
-
{ label: "Yes", key: "y", action: () => settle(true) },
|
|
729
|
-
{ label: "No", key: "n", action: () => settle(false) },
|
|
730
|
-
],
|
|
731
|
-
theme: dialogTheme(theme),
|
|
732
|
-
framed: false, // this overlay's own Envelope already draws a border; a second rule would double up on it
|
|
733
787
|
});
|
|
734
|
-
|
|
735
|
-
});
|
|
736
|
-
}
|
|
788
|
+
}
|
|
737
789
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
790
|
+
const inlineCtx: ExtensionCommandContext = { ...ctx, hasUI: true, ui: { ...ctx.ui, confirm: confirmInline } };
|
|
791
|
+
const host: TabHost = {
|
|
792
|
+
ctx,
|
|
793
|
+
inlineCtx,
|
|
794
|
+
requestRender: () => tui.requestRender(),
|
|
795
|
+
onSessionReplaced: () => done(undefined),
|
|
796
|
+
};
|
|
745
797
|
|
|
746
|
-
|
|
798
|
+
const measure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
747
799
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
800
|
+
const packagesTab = new PackagesTab(natives, host, theme, measure, initialRows, (target, configFilter) => {
|
|
801
|
+
if (target === "config" && configFilter !== undefined) configTab.setFilter(configFilter);
|
|
802
|
+
tabbedContainer.setActive(target);
|
|
803
|
+
});
|
|
752
804
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
805
|
+
const findTab = createFindTab(natives, host, theme);
|
|
806
|
+
|
|
807
|
+
// Config/Settings both need theme (only available once this factory
|
|
808
|
+
// runs) so they're constructed here rather than before the overlay
|
|
809
|
+
// opens; both load their own data asynchronously in the background and
|
|
810
|
+
// render their own "Loading…" state until it resolves -- no
|
|
811
|
+
// placeholder-swap machinery needed.
|
|
812
|
+
const configTab = new ConfigTab(natives, host, theme, initialTab === "config" ? initialConfigFilter : undefined);
|
|
813
|
+
void configTab.load().then(() => tui.requestRender());
|
|
814
|
+
|
|
815
|
+
const settingsTab = new SettingsTab(natives, host, theme);
|
|
816
|
+
void settingsTab.load().then(() => tui.requestRender());
|
|
817
|
+
|
|
818
|
+
const tabByKey: Record<PackedTabKey, Component & TabScope> = {
|
|
819
|
+
packages: packagesTab,
|
|
820
|
+
find: findTab,
|
|
821
|
+
config: configTab,
|
|
822
|
+
settings: settingsTab,
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const tabbedContainer = new TabbedContainer({
|
|
826
|
+
tabs: [
|
|
827
|
+
{ key: "packages", label: "Packages", content: tabByKey.packages },
|
|
828
|
+
{ key: "find", label: "Find", content: tabByKey.find },
|
|
829
|
+
{ key: "config", label: "Config", content: tabByKey.config },
|
|
830
|
+
{ key: "settings", label: "Settings", content: tabByKey.settings },
|
|
831
|
+
],
|
|
832
|
+
theme: tabBarTheme(theme),
|
|
833
|
+
initialKey: initialTab,
|
|
834
|
+
// Malevich's own default matcher only recognizes legacy CSI sequences;
|
|
835
|
+
// pi-tui's real matchesKey also covers the Kitty keyboard protocol and
|
|
836
|
+
// xterm's modifyOtherKeys encodings for the same keys. Malevich's
|
|
837
|
+
// KeyMatcher type takes a bare string (it doesn't share pi-tui's KeyId
|
|
838
|
+
// union), so this only ever forwards the small fixed set of key names
|
|
839
|
+
// TabbedContainer itself actually calls with (left/right/tab/shift+tab).
|
|
840
|
+
matchesKey: (data, keyId) => matchesKey(data, keyId as Parameters<typeof matchesKey>[1]),
|
|
841
|
+
});
|
|
790
842
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
843
|
+
// measure must be explicit: Envelope's own default is ASCII-only (raw
|
|
844
|
+
// .length, blind to ANSI escape codes) and every tab's own content is
|
|
845
|
+
// styled through theme.fg/theme.bold -- without this, Envelope pads
|
|
846
|
+
// each line against its own escape-code-inflated "length" instead of
|
|
847
|
+
// its real visible width, so the right border lands at a different
|
|
848
|
+
// column on every line depending on how much styling it carries.
|
|
849
|
+
const envelope = new Envelope({
|
|
850
|
+
title: "packed",
|
|
851
|
+
borderStyle: "rounded",
|
|
852
|
+
style: (s) => theme.fg("border", s),
|
|
853
|
+
titleStyle: (s) => theme.bold(theme.fg("accent", s)),
|
|
854
|
+
measure,
|
|
855
|
+
});
|
|
804
856
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
tui.requestRender();
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
const activeKey = tabbedContainer.getActiveKey() as PackedTabKey;
|
|
818
|
-
const activeTab = tabByKey[activeKey];
|
|
819
|
-
// Each host-level key is checked against that tab's own capture flag
|
|
820
|
-
// independently -- conflating them would, e.g., let Find's always-on
|
|
821
|
-
// capturesHorizontalArrows() also swallow Escape into the query box
|
|
822
|
-
// instead of navigating back.
|
|
823
|
-
if (data === "\x1b" && !(activeTab.capturesEscape?.() ?? false)) {
|
|
824
|
-
if (activeKey !== "packages") tabbedContainer.setActive("packages");
|
|
825
|
-
else { done(undefined); return; }
|
|
826
|
-
tui.requestRender();
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
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)) {
|
|
844
|
-
tabbedContainer.handleInput(data); // owns Left/Right cycling itself
|
|
845
|
-
tui.requestRender();
|
|
846
|
-
return;
|
|
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);
|
|
857
|
+
return {
|
|
858
|
+
render(width: number): string[] {
|
|
859
|
+
envelope.setContent(pendingDialog ?? tabbedContainer);
|
|
860
|
+
return envelope.render(width);
|
|
861
|
+
},
|
|
862
|
+
invalidate: () => envelope.invalidate(),
|
|
863
|
+
handleInput(data: string) {
|
|
864
|
+
if (pendingDialog) {
|
|
865
|
+
pendingDialog.handleInput(data);
|
|
860
866
|
tui.requestRender();
|
|
861
867
|
return;
|
|
862
868
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
869
|
+
const activeKey = tabbedContainer.getActiveKey() as PackedTabKey;
|
|
870
|
+
const activeTab = tabByKey[activeKey];
|
|
871
|
+
// Each host-level key is checked against that tab's own capture flag
|
|
872
|
+
// independently -- conflating them would, e.g., let Find's always-on
|
|
873
|
+
// capturesHorizontalArrows() also swallow Escape into the query box
|
|
874
|
+
// instead of navigating back.
|
|
875
|
+
if (data === "\x1b" && !(activeTab.capturesEscape?.() ?? false)) {
|
|
876
|
+
if (activeKey !== "packages") tabbedContainer.setActive("packages");
|
|
877
|
+
else {
|
|
878
|
+
done(undefined);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
tui.requestRender();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
// Tab/Shift-Tab always sweep between menus -- nothing needs a literal
|
|
885
|
+
// Tab character for itself (Packages'/Config's own former Tab bindings
|
|
886
|
+
// moved to v once Tab was claimed globally; see the mnemonics.test.ts
|
|
887
|
+
// conflict check for why that reassignment was necessary). Real
|
|
888
|
+
// matchesKey(), not a hardcoded "\x1b[Z" literal, because Shift-Tab has
|
|
889
|
+
// no single universal encoding: legacy terminals send CSI Z, but the
|
|
890
|
+
// Kitty keyboard protocol and xterm's modifyOtherKeys mode both send a
|
|
891
|
+
// different sequence for the same keypress -- a literal check silently
|
|
892
|
+
// misses whichever ones it doesn't happen to be pinned to.
|
|
893
|
+
if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
|
|
894
|
+
tabbedContainer.handleInput(data);
|
|
895
|
+
tui.requestRender();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if ((matchesKey(data, "right") || matchesKey(data, "left")) && !(activeTab.capturesHorizontalArrows?.() ?? false)) {
|
|
899
|
+
tabbedContainer.handleInput(data); // owns Left/Right cycling itself
|
|
900
|
+
tui.requestRender();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
// The first letter of each tab's label is a jump mnemonic
|
|
904
|
+
// (Packages/Find/Config/Settings -> p/f/c/s), highlighted in the tab
|
|
905
|
+
// bar itself -- but only reachable FROM one of the other three tabs.
|
|
906
|
+
// Packages' own f/c/s bindings already do the same jump (c
|
|
907
|
+
// additionally scopes Config to the selected row); letting the
|
|
908
|
+
// generic version fire there too would just be redundant, not wrong,
|
|
909
|
+
// but skipping it keeps this one dispatcher the single source of
|
|
910
|
+
// truth for "which code path actually owns key X" per active tab.
|
|
911
|
+
if (activeKey !== "packages" && data.length === 1) {
|
|
912
|
+
const target = tabbedContainer.resolveMnemonic(data);
|
|
913
|
+
if (target && target !== activeKey && !(activeTab.capturesMnemonics?.() ?? false)) {
|
|
914
|
+
tabbedContainer.setActive(target as PackedTabKey);
|
|
915
|
+
tui.requestRender();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
activeTab.handleInput?.(data);
|
|
920
|
+
tui.requestRender();
|
|
921
|
+
},
|
|
922
|
+
};
|
|
923
|
+
// Pinned to the very top (anchor:"top-center"+offsetY:1 -- a fixed row
|
|
924
|
+
// count regardless of terminal size), not row:"40%". The percentage
|
|
925
|
+
// positioning was tried instead but reverted: pi-tui's row-percentage
|
|
926
|
+
// formula interpolates over the range of positions that still fit the
|
|
927
|
+
// CURRENT content height, so the panel visibly jittered up and down
|
|
928
|
+
// every time a tab with a different content height (Packages' table vs.
|
|
929
|
+
// Config's shorter list) became active. A fixed top offset keeps the
|
|
930
|
+
// header pinned in place and only the footer moves as content grows.
|
|
931
|
+
},
|
|
932
|
+
{ overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "top-center", offsetY: 1 } },
|
|
933
|
+
);
|
|
877
934
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.6",
|
|
4
4
|
"description": "Pi package tools, commands, profiles, and TUI for the Packed daemon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
"typecheck": "bunx tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@danypops/packed": "^0.
|
|
15
|
+
"@danypops/packed": "^0.5.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.16.1"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@earendil-works/pi-coding-agent": "*",
|