@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 +1 -1
- package/src/toolbox.test.ts +4 -16
- package/src/toolbox.ts +51 -65
package/package.json
CHANGED
package/src/toolbox.test.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
|
476
|
-
|
|
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
|
-
|
|
459
|
+
const result = frameModal({
|
|
489
460
|
width: mw,
|
|
490
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
}
|