@danypops/pi-packed 0.19.5 → 0.19.7

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.
@@ -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 { Dialog, Envelope, Menu, Spinner, Table, TabbedContainer, type Component, type MenuItem, type TextMeasure } from "malevich-tui-components";
51
- import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
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 { ok: boolean; message: string; }
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 = outcome.previousVersion && outcome.currentVersion ? ` (${outcome.previousVersion} → ${outcome.currentVersion})` : "";
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 { changed: number; failedNames: string[]; }
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 { ok: boolean; output: string; }
155
- interface UpdateProgressEvent { row: Row; index: number; total: number; phase: "start" | "done"; result?: UpdateProgressResult; }
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(failedNames.length > 0 ? `No packages updated; ${failedNames.length} failed.` : "All packages already up to date.", failedNames.length > 0 ? "warning" : "info");
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({ invalidate() {}, render: (width: number) => [truncateToWidth(`${theme.fg("accent", spinner.glyph())} ${currentLabel}`, width, "")] });
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
- }).finally(() => spinner.stop()).then(done);
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 { fg(color: string, s: string): string; bold(s: string): string; }
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
- private readonly theme: PackagesTabTheme,
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("muted", `${this.statusLine()} ${dot} view: ${this.mode} ${dot} / filter ${dot} v view ${dot} ${this.rows.length} installed`),
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 ? theme.fg("warning", `↑${row.latest}`) : "";
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 = action.type === "disable"
616
- ? await applyDisableExtensions(action.row, this.natives, this.host.inlineCtx)
617
- : await applyPackageChoice(action.type === "update" ? `Update to ${action.row.latest}` : "Remove", action.row, this.natives, this.host.inlineCtx, onSettled);
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>((tui, theme, _kb, done) => {
710
- // A y/n decision rendered as this SAME overlay's own content (via a real
711
- // Malevich Dialog, dispatched by literal key press) instead of
712
- // ctx.ui.confirm's separate native dialog -- shared by every tab via
713
- // host.inlineCtx below, exactly like the panel's own approval/reload
714
- // dialogs always have been.
715
- let pendingDialog: Dialog | undefined;
716
-
717
- function confirmInline(title: string, message: string): Promise<boolean> {
718
- return new Promise((resolve) => {
719
- const settle = (value: boolean) => {
720
- pendingDialog = undefined;
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
- tui.requestRender();
735
- });
736
- }
788
+ }
737
789
 
738
- const inlineCtx: ExtensionCommandContext = { ...ctx, hasUI: true, ui: { ...ctx.ui, confirm: confirmInline } };
739
- const host: TabHost = {
740
- ctx,
741
- inlineCtx,
742
- requestRender: () => tui.requestRender(),
743
- onSessionReplaced: () => done(undefined),
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
- const measure: TextMeasure = { visibleWidth, truncateToWidth };
798
+ const measure: TextMeasure = { visibleWidth, truncateToWidth };
747
799
 
748
- const packagesTab = new PackagesTab(natives, host, theme, measure, initialRows, (target, configFilter) => {
749
- if (target === "config" && configFilter !== undefined) configTab.setFilter(configFilter);
750
- tabbedContainer.setActive(target);
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
- const findTab = createFindTab(natives, host, theme);
754
-
755
- // Config/Settings both need theme (only available once this factory
756
- // runs) so they're constructed here rather than before the overlay
757
- // opens; both load their own data asynchronously in the background and
758
- // render their own "Loading…" state until it resolves -- no
759
- // placeholder-swap machinery needed.
760
- const configTab = new ConfigTab(natives, host, theme, initialTab === "config" ? initialConfigFilter : undefined);
761
- void configTab.load().then(() => tui.requestRender());
762
-
763
- const settingsTab = new SettingsTab(natives, host, theme);
764
- void settingsTab.load().then(() => tui.requestRender());
765
-
766
- const tabByKey: Record<PackedTabKey, Component & TabScope> = {
767
- packages: packagesTab,
768
- find: findTab,
769
- config: configTab,
770
- settings: settingsTab,
771
- };
772
-
773
- const tabbedContainer = new TabbedContainer({
774
- tabs: [
775
- { key: "packages", label: "Packages", content: tabByKey.packages },
776
- { key: "find", label: "Find", content: tabByKey.find },
777
- { key: "config", label: "Config", content: tabByKey.config },
778
- { key: "settings", label: "Settings", content: tabByKey.settings },
779
- ],
780
- theme: tabBarTheme(theme),
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]),
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
- // measure must be explicit: Envelope's own default is ASCII-only (raw
792
- // .length, blind to ANSI escape codes) and every tab's own content is
793
- // styled through theme.fg/theme.bold -- without this, Envelope pads
794
- // each line against its own escape-code-inflated "length" instead of
795
- // its real visible width, so the right border lands at a different
796
- // column on every line depending on how much styling it carries.
797
- const envelope = new Envelope({
798
- title: "packed",
799
- borderStyle: "rounded",
800
- style: (s) => theme.fg("border", s),
801
- titleStyle: (s) => theme.bold(theme.fg("accent", s)),
802
- measure,
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
- return {
806
- render(width: number): string[] {
807
- envelope.setContent(pendingDialog ?? tabbedContainer);
808
- return envelope.render(width);
809
- },
810
- invalidate: () => envelope.invalidate(),
811
- handleInput(data: string) {
812
- if (pendingDialog) {
813
- pendingDialog.handleInput(data);
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
- activeTab.handleInput?.(data);
865
- tui.requestRender();
866
- },
867
- };
868
- // Pinned to the very top (anchor:"top-center"+offsetY:1 -- a fixed row
869
- // count regardless of terminal size), not row:"40%". The percentage
870
- // positioning was tried instead but reverted: pi-tui's row-percentage
871
- // formula interpolates over the range of positions that still fit the
872
- // CURRENT content height, so the panel visibly jittered up and down
873
- // every time a tab with a different content height (Packages' table vs.
874
- // Config's shorter list) became active. A fixed top offset keeps the
875
- // header pinned in place and only the footer moves as content grows.
876
- }, { overlay: true, overlayOptions: { width: "70%", maxHeight: "70%", anchor: "top-center", offsetY: 1 } });
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.5",
3
+ "version": "0.19.7",
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.2.0",
16
- "@danypops/vehicle-client": "^0.1.1",
15
+ "@danypops/packed": "^0.5.0",
16
+ "@danypops/vehicle-client": "^0.2.0",
17
17
  "@danypops/vehicle-client-pi": "^0.1.5",
18
- "malevich-tui-components": "^0.14.1"
18
+ "malevich-tui-components": "^0.16.1"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "@earendil-works/pi-coding-agent": "*",