@xynogen/pix-toolbox 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-toolbox",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Pi tool — gated tool toggle UI (/toolbox)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -53,12 +53,7 @@ describe("buildRows", () => {
53
53
 
54
54
  describe("parseTargets", () => {
55
55
  test("splits on commas, spaces, newlines", () => {
56
- expect(parseTargets("ls, find grep\nfetch")).toEqual([
57
- "ls",
58
- "find",
59
- "grep",
60
- "fetch",
61
- ]);
56
+ expect(parseTargets("ls, find grep\nfetch")).toEqual(["ls", "find", "grep", "fetch"]);
62
57
  });
63
58
 
64
59
  test("dedupes", () => {
@@ -202,9 +197,7 @@ function makeHost(toolNames: string[]) {
202
197
  },
203
198
  emit(ev: string, payload: unknown, ctx?: unknown) {
204
199
  return Promise.all(
205
- (handlers[ev] ?? []).map((f) =>
206
- (f as (...args: unknown[]) => unknown)(payload, ctx),
207
- ),
200
+ (handlers[ev] ?? []).map((f) => (f as (...args: unknown[]) => unknown)(payload, ctx)),
208
201
  );
209
202
  },
210
203
  getAllTools() {
@@ -226,18 +219,13 @@ function makeHost(toolNames: string[]) {
226
219
  },
227
220
  appendEntry() {},
228
221
  registerTool() {},
229
- registerCommand(
230
- name: string,
231
- def: { handler: (...args: unknown[]) => unknown },
232
- ) {
222
+ registerCommand(name: string, def: { handler: (...args: unknown[]) => unknown }) {
233
223
  commands.push({ name, handler: def.handler });
234
224
  },
235
225
  } as never;
236
226
  const emit = (ev: string, payload: unknown, ctx?: unknown) =>
237
227
  Promise.all(
238
- (handlers[ev] ?? []).map((f) =>
239
- (f as (...args: unknown[]) => unknown)(payload, ctx),
240
- ),
228
+ (handlers[ev] ?? []).map((f) => (f as (...args: unknown[]) => unknown)(payload, ctx)),
241
229
  );
242
230
  return {
243
231
  pi,
package/src/toolbox.ts CHANGED
@@ -34,17 +34,18 @@ import {
34
34
  type TUI,
35
35
  visibleWidth,
36
36
  } from "@earendil-works/pi-tui";
37
- import { frameLines, modalWidth } from "@xynogen/pix-pretty/modal-frame";
37
+ import {
38
+ frameModal,
39
+ MIN_MODAL_HEIGHT,
40
+ ModalPager,
41
+ modalWidth,
42
+ terminalModalHeight,
43
+ } from "@xynogen/pix-pretty/modal-frame";
38
44
 
39
45
  // ─── Constants ──────────────────────────────────────────────────────────────
40
46
 
41
47
  /** Tools that can never be disabled — always prompt-visible. */
42
- export const CORE_TOOLS: ReadonlySet<string> = new Set([
43
- "bash",
44
- "edit",
45
- "read",
46
- "write",
47
- ]);
48
+ export const CORE_TOOLS: ReadonlySet<string> = new Set(["bash", "edit", "read", "write"]);
48
49
 
49
50
  // ─── Types ──────────────────────────────────────────────────────────────────
50
51
 
@@ -134,14 +135,10 @@ export function toggleTool(
134
135
 
135
136
  if (action === "enable") {
136
137
  const did = ops.onActivate(name);
137
- return did
138
- ? `Enabled ${name} — now prompt-visible.`
139
- : `${name} is already active.`;
138
+ return did ? `Enabled ${name} — now prompt-visible.` : `${name} is already active.`;
140
139
  }
141
140
  const did = ops.onDeactivate(name);
142
- return did
143
- ? `Disabled ${name} — hidden from prompt.`
144
- : `${name} is already gated.`;
141
+ return did ? `Disabled ${name} — hidden from prompt.` : `${name} is already gated.`;
145
142
  }
146
143
 
147
144
  // ─── Persistence ───────────────────────────────────────────────────────────
@@ -173,11 +170,7 @@ function createState(pi: ExtensionAPI) {
173
170
  try {
174
171
  const sp = getStatePath();
175
172
  mkdirSync(dirname(sp), { recursive: true });
176
- writeFileSync(
177
- sp,
178
- JSON.stringify({ enabledTools: [...enabledTools] }, null, 2),
179
- "utf-8",
180
- );
173
+ writeFileSync(sp, JSON.stringify({ enabledTools: [...enabledTools] }, null, 2), "utf-8");
181
174
  } catch (err) {
182
175
  console.warn("toolbox: file persist failed:", err);
183
176
  }
@@ -203,9 +196,7 @@ function createState(pi: ExtensionAPI) {
203
196
  const fileSaved = loadFromFile();
204
197
  if (fileSaved) {
205
198
  const validNames = new Set((pi.getAllTools() ?? []).map((t) => t.name));
206
- enabledTools = new Set(
207
- fileSaved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)),
208
- );
199
+ enabledTools = new Set(fileSaved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)));
209
200
  for (const ct of CORE_TOOLS) enabledTools.add(ct);
210
201
  initialized = true;
211
202
  apply();
@@ -233,9 +224,7 @@ function createState(pi: ExtensionAPI) {
233
224
 
234
225
  if (saved) {
235
226
  const validNames = new Set((pi.getAllTools() ?? []).map((t) => t.name));
236
- enabledTools = new Set(
237
- saved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)),
238
- );
227
+ enabledTools = new Set(saved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)));
239
228
  for (const ct of CORE_TOOLS) enabledTools.add(ct);
240
229
  } else {
241
230
  const names = (pi.getAllTools() ?? []).map((t) => t.name);
@@ -351,34 +340,24 @@ export default function registerToolbox(pi: ExtensionAPI): void {
351
340
  };
352
341
  }): Promise<void> {
353
342
  await ctx.ui.custom<null>(
354
- (
355
- tui: TUI,
356
- theme: Theme,
357
- _kb: KeybindingsManager,
358
- done: (r: null) => void,
359
- ) => {
343
+ (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (r: null) => void) => {
360
344
  const accent = "accent";
361
345
  const mute = (s: string) => theme.fg("muted", s);
362
346
 
363
347
  type RowState = "active" | "gated";
364
- const stateOf = (name: string): RowState =>
365
- ops.isActive(name) ? "active" : "gated";
348
+ const stateOf = (name: string): RowState => (ops.isActive(name) ? "active" : "gated");
366
349
 
367
350
  const labelFor = (r: ToolRow): string => {
368
351
  const active = stateOf(r.name) === "active";
369
352
  const marker = active ? " " : theme.fg("warning", "#");
370
- const name = active
371
- ? theme.fg("success", r.name)
372
- : theme.fg("dim", r.name);
353
+ const name = active ? theme.fg("success", r.name) : theme.fg("dim", r.name);
373
354
  const kind = mute(`[${r.mcp ? "MCP" : "tool"}]`);
374
355
  return `${marker} ${name} ${kind}`;
375
356
  };
376
357
 
377
358
  const descFor = (r: ToolRow): string => {
378
359
  const active = stateOf(r.name) === "active";
379
- const tag = active
380
- ? theme.fg("success", "active")
381
- : theme.fg("warning", "gated");
360
+ const tag = active ? theme.fg("success", "active") : theme.fg("warning", "gated");
382
361
  return `${tag} ${mute("·")} ${r.description || "(no description)"}`;
383
362
  };
384
363
 
@@ -394,14 +373,11 @@ export default function registerToolbox(pi: ExtensionAPI): void {
394
373
  };
395
374
 
396
375
  const allItems = rows.map(toItem);
397
- const widest = allItems.reduce(
398
- (w, it) => Math.max(w, visibleWidth(it.label)),
399
- 0,
400
- );
376
+ const widest = allItems.reduce((w, it) => Math.max(w, visibleWidth(it.label)), 0);
401
377
 
402
378
  const list = new SelectList(
403
379
  allItems,
404
- Math.min(allItems.length, 14),
380
+ Math.max(1, allItems.length),
405
381
  {
406
382
  selectedPrefix: (t: string) => theme.fg(accent, t),
407
383
  selectedText: (t: string) => theme.fg(accent, t),
@@ -423,6 +399,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
423
399
 
424
400
  const search = new Input();
425
401
  let statusText = "";
402
+ const pager = new ModalPager();
426
403
 
427
404
  const refreshLabels = () => {
428
405
  for (const it of internal.items) {
@@ -472,43 +449,52 @@ export default function registerToolbox(pi: ExtensionAPI): void {
472
449
  render(w: number) {
473
450
  const mw = modalWidth(w);
474
451
  const inner = mw - 4; // CHROME = 2 border + 2 padding
475
- const lines: string[] = [
476
- theme.fg(accent, theme.bold("🧰 Toolbox")),
477
- theme.fg("muted", "Search:"),
478
- ...search.render(inner),
479
- ...list.render(inner),
480
- ];
481
- if (statusText) lines.push(statusText);
482
- lines.push(
452
+ const footer = statusText ? [statusText] : [];
453
+ footer.push(
483
454
  theme.fg(
484
455
  "dim",
485
- "↑↓ navigate · e enable · d disable · space toggle · esc close",
456
+ "↑↓ navigate · ←→/PgUp/PgDn inspect · e enable · d disable · space toggle · esc close",
486
457
  ),
487
458
  );
488
- return frameLines({
459
+ const result = frameModal({
489
460
  width: mw,
490
- lines,
461
+ maxHeight: terminalModalHeight(tui.terminal.rows),
462
+ minHeight: MIN_MODAL_HEIGHT,
463
+ header: [
464
+ theme.fg(accent, theme.bold("🧰 Toolbox")),
465
+ theme.fg("muted", "Search:"),
466
+ ...search.render(inner),
467
+ ],
468
+ body: list.render(inner),
469
+ selectedBodyRange: pager.selectedRange({
470
+ start: internal.selectedIndex,
471
+ end: internal.selectedIndex + 1,
472
+ }),
473
+ footer,
474
+ bodyOffset: pager.bodyOffset,
491
475
  color: (s) => theme.fg(accent, s),
492
476
  bg: (s) => theme.bg("customMessageBg", s),
477
+ fg: (s) => theme.fg("text", s),
493
478
  });
479
+ pager.sync(result);
480
+ return result.lines;
494
481
  },
495
482
  invalidate() {
496
483
  list.invalidate();
497
484
  search.invalidate();
498
485
  },
499
486
  handleInput(data: string) {
487
+ if (pager.handleInput(data, keybindings, true)) {
488
+ tui.requestRender?.();
489
+ return;
490
+ }
500
491
  if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) {
501
492
  list.handleInput?.(data);
502
- } else if (
503
- matchesKey(data, Key.enter) ||
504
- matchesKey(data, Key.escape)
505
- ) {
493
+ pager.followSelection();
494
+ } else if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape)) {
506
495
  done(null);
507
496
  return;
508
- } else if (
509
- matchesKey(data, Key.space) ||
510
- matchesKey(data, Key.tab)
511
- ) {
497
+ } else if (matchesKey(data, Key.space) || matchesKey(data, Key.tab)) {
512
498
  flipSelected();
513
499
  } else {
514
500
  const printable = decodeKittyPrintable(data);
@@ -520,10 +506,12 @@ export default function registerToolbox(pi: ExtensionAPI): void {
520
506
  } else {
521
507
  search.handleInput?.(data);
522
508
  applyFilter(search.getValue?.() ?? "");
509
+ pager.followSelection();
523
510
  }
524
511
  } else {
525
512
  search.handleInput?.(data);
526
513
  applyFilter(search.getValue?.() ?? "");
514
+ pager.followSelection();
527
515
  }
528
516
  }
529
517
  list.invalidate();
@@ -553,9 +541,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
553
541
  return;
554
542
  }
555
543
  const rows = getRows();
556
- const msg = targets
557
- .map((t) => toggleTool(verb, t, rows, ops))
558
- .join("\n");
544
+ const msg = targets.map((t) => toggleTool(verb, t, rows, ops)).join("\n");
559
545
  ctx.ui.notify(msg, "info");
560
546
  return;
561
547
  }