@xynogen/pix-toolbox 0.1.6 → 0.1.8
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 +18 -26
- package/src/toolbox.ts +15 -56
package/package.json
CHANGED
package/src/toolbox.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ describe("buildRows", () => {
|
|
|
41
41
|
|
|
42
42
|
test("flags MCP tools", () => {
|
|
43
43
|
const rows = buildRows([mcpToolInfo("ctx_search")]);
|
|
44
|
-
expect(rows[0].mcp).toBe(true);
|
|
44
|
+
expect((rows[0] as ToolRow).mcp).toBe(true);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test("empty input returns empty", () => {
|
|
@@ -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,
|
|
@@ -260,6 +248,7 @@ function makeCtx() {
|
|
|
260
248
|
}
|
|
261
249
|
|
|
262
250
|
describe("/toolbox command", () => {
|
|
251
|
+
type Note = { text: string; level?: string };
|
|
263
252
|
const ALL = ["read", "write", "bash", "grep", "find"];
|
|
264
253
|
|
|
265
254
|
async function boot() {
|
|
@@ -281,28 +270,31 @@ describe("/toolbox command", () => {
|
|
|
281
270
|
await host.command("toolbox")?.handler("", ctx);
|
|
282
271
|
expect(notes.length).toBe(1);
|
|
283
272
|
// only non-core tools shown, all start active
|
|
284
|
-
|
|
285
|
-
expect(
|
|
273
|
+
const n0 = notes[0] as Note;
|
|
274
|
+
expect(n0.text).toContain("✓ active grep");
|
|
275
|
+
expect(n0.text).toContain("✓ active find");
|
|
286
276
|
// core tools excluded from toolbox
|
|
287
|
-
expect(
|
|
288
|
-
expect(
|
|
277
|
+
expect(n0.text).not.toContain(" read");
|
|
278
|
+
expect(n0.text).not.toContain(" bash");
|
|
289
279
|
});
|
|
290
280
|
|
|
291
281
|
test("/toolbox list shows non-core tools with status", async () => {
|
|
292
282
|
const host = await boot();
|
|
293
283
|
const { ctx, notes } = makeCtx();
|
|
294
284
|
await host.command("toolbox")?.handler("list", ctx);
|
|
295
|
-
|
|
296
|
-
expect(
|
|
297
|
-
expect(
|
|
285
|
+
const ln0 = notes[0] as Note;
|
|
286
|
+
expect(ln0.text).toContain("grep");
|
|
287
|
+
expect(ln0.text).toContain("find");
|
|
288
|
+
expect(ln0.text).not.toContain(" bash");
|
|
298
289
|
});
|
|
299
290
|
|
|
300
291
|
test("/toolbox list <query> filters", async () => {
|
|
301
292
|
const host = await boot();
|
|
302
293
|
const { ctx, notes } = makeCtx();
|
|
303
294
|
await host.command("toolbox")?.handler("list fin", ctx);
|
|
304
|
-
|
|
305
|
-
expect(
|
|
295
|
+
const fn0 = notes[0] as Note;
|
|
296
|
+
expect(fn0.text).toContain("find");
|
|
297
|
+
expect(fn0.text).not.toContain("✓ active read");
|
|
306
298
|
});
|
|
307
299
|
|
|
308
300
|
test("opens interactive picker when ctx.ui.custom exists", async () => {
|
package/src/toolbox.ts
CHANGED
|
@@ -39,12 +39,7 @@ import { frameLines, modalWidth } from "@xynogen/pix-pretty/modal-frame";
|
|
|
39
39
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
40
40
|
|
|
41
41
|
/** 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
|
-
]);
|
|
42
|
+
export const CORE_TOOLS: ReadonlySet<string> = new Set(["bash", "edit", "read", "write"]);
|
|
48
43
|
|
|
49
44
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
50
45
|
|
|
@@ -134,14 +129,10 @@ export function toggleTool(
|
|
|
134
129
|
|
|
135
130
|
if (action === "enable") {
|
|
136
131
|
const did = ops.onActivate(name);
|
|
137
|
-
return did
|
|
138
|
-
? `Enabled ${name} — now prompt-visible.`
|
|
139
|
-
: `${name} is already active.`;
|
|
132
|
+
return did ? `Enabled ${name} — now prompt-visible.` : `${name} is already active.`;
|
|
140
133
|
}
|
|
141
134
|
const did = ops.onDeactivate(name);
|
|
142
|
-
return did
|
|
143
|
-
? `Disabled ${name} — hidden from prompt.`
|
|
144
|
-
: `${name} is already gated.`;
|
|
135
|
+
return did ? `Disabled ${name} — hidden from prompt.` : `${name} is already gated.`;
|
|
145
136
|
}
|
|
146
137
|
|
|
147
138
|
// ─── Persistence ───────────────────────────────────────────────────────────
|
|
@@ -173,11 +164,7 @@ function createState(pi: ExtensionAPI) {
|
|
|
173
164
|
try {
|
|
174
165
|
const sp = getStatePath();
|
|
175
166
|
mkdirSync(dirname(sp), { recursive: true });
|
|
176
|
-
writeFileSync(
|
|
177
|
-
sp,
|
|
178
|
-
JSON.stringify({ enabledTools: [...enabledTools] }, null, 2),
|
|
179
|
-
"utf-8",
|
|
180
|
-
);
|
|
167
|
+
writeFileSync(sp, JSON.stringify({ enabledTools: [...enabledTools] }, null, 2), "utf-8");
|
|
181
168
|
} catch (err) {
|
|
182
169
|
console.warn("toolbox: file persist failed:", err);
|
|
183
170
|
}
|
|
@@ -203,9 +190,7 @@ function createState(pi: ExtensionAPI) {
|
|
|
203
190
|
const fileSaved = loadFromFile();
|
|
204
191
|
if (fileSaved) {
|
|
205
192
|
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
|
-
);
|
|
193
|
+
enabledTools = new Set(fileSaved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)));
|
|
209
194
|
for (const ct of CORE_TOOLS) enabledTools.add(ct);
|
|
210
195
|
initialized = true;
|
|
211
196
|
apply();
|
|
@@ -233,9 +218,7 @@ function createState(pi: ExtensionAPI) {
|
|
|
233
218
|
|
|
234
219
|
if (saved) {
|
|
235
220
|
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
|
-
);
|
|
221
|
+
enabledTools = new Set(saved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)));
|
|
239
222
|
for (const ct of CORE_TOOLS) enabledTools.add(ct);
|
|
240
223
|
} else {
|
|
241
224
|
const names = (pi.getAllTools() ?? []).map((t) => t.name);
|
|
@@ -351,34 +334,24 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
351
334
|
};
|
|
352
335
|
}): Promise<void> {
|
|
353
336
|
await ctx.ui.custom<null>(
|
|
354
|
-
(
|
|
355
|
-
tui: TUI,
|
|
356
|
-
theme: Theme,
|
|
357
|
-
_kb: KeybindingsManager,
|
|
358
|
-
done: (r: null) => void,
|
|
359
|
-
) => {
|
|
337
|
+
(tui: TUI, theme: Theme, _kb: KeybindingsManager, done: (r: null) => void) => {
|
|
360
338
|
const accent = "accent";
|
|
361
339
|
const mute = (s: string) => theme.fg("muted", s);
|
|
362
340
|
|
|
363
341
|
type RowState = "active" | "gated";
|
|
364
|
-
const stateOf = (name: string): RowState =>
|
|
365
|
-
ops.isActive(name) ? "active" : "gated";
|
|
342
|
+
const stateOf = (name: string): RowState => (ops.isActive(name) ? "active" : "gated");
|
|
366
343
|
|
|
367
344
|
const labelFor = (r: ToolRow): string => {
|
|
368
345
|
const active = stateOf(r.name) === "active";
|
|
369
346
|
const marker = active ? " " : theme.fg("warning", "#");
|
|
370
|
-
const name = active
|
|
371
|
-
? theme.fg("success", r.name)
|
|
372
|
-
: theme.fg("dim", r.name);
|
|
347
|
+
const name = active ? theme.fg("success", r.name) : theme.fg("dim", r.name);
|
|
373
348
|
const kind = mute(`[${r.mcp ? "MCP" : "tool"}]`);
|
|
374
349
|
return `${marker} ${name} ${kind}`;
|
|
375
350
|
};
|
|
376
351
|
|
|
377
352
|
const descFor = (r: ToolRow): string => {
|
|
378
353
|
const active = stateOf(r.name) === "active";
|
|
379
|
-
const tag = active
|
|
380
|
-
? theme.fg("success", "active")
|
|
381
|
-
: theme.fg("warning", "gated");
|
|
354
|
+
const tag = active ? theme.fg("success", "active") : theme.fg("warning", "gated");
|
|
382
355
|
return `${tag} ${mute("·")} ${r.description || "(no description)"}`;
|
|
383
356
|
};
|
|
384
357
|
|
|
@@ -394,10 +367,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
394
367
|
};
|
|
395
368
|
|
|
396
369
|
const allItems = rows.map(toItem);
|
|
397
|
-
const widest = allItems.reduce(
|
|
398
|
-
(w, it) => Math.max(w, visibleWidth(it.label)),
|
|
399
|
-
0,
|
|
400
|
-
);
|
|
370
|
+
const widest = allItems.reduce((w, it) => Math.max(w, visibleWidth(it.label)), 0);
|
|
401
371
|
|
|
402
372
|
const list = new SelectList(
|
|
403
373
|
allItems,
|
|
@@ -480,10 +450,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
480
450
|
];
|
|
481
451
|
if (statusText) lines.push(statusText);
|
|
482
452
|
lines.push(
|
|
483
|
-
theme.fg(
|
|
484
|
-
"dim",
|
|
485
|
-
"↑↓ navigate · e enable · d disable · space toggle · esc close",
|
|
486
|
-
),
|
|
453
|
+
theme.fg("dim", "↑↓ navigate · e enable · d disable · space toggle · esc close"),
|
|
487
454
|
);
|
|
488
455
|
return frameLines({
|
|
489
456
|
width: mw,
|
|
@@ -499,16 +466,10 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
499
466
|
handleInput(data: string) {
|
|
500
467
|
if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) {
|
|
501
468
|
list.handleInput?.(data);
|
|
502
|
-
} else if (
|
|
503
|
-
matchesKey(data, Key.enter) ||
|
|
504
|
-
matchesKey(data, Key.escape)
|
|
505
|
-
) {
|
|
469
|
+
} else if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape)) {
|
|
506
470
|
done(null);
|
|
507
471
|
return;
|
|
508
|
-
} else if (
|
|
509
|
-
matchesKey(data, Key.space) ||
|
|
510
|
-
matchesKey(data, Key.tab)
|
|
511
|
-
) {
|
|
472
|
+
} else if (matchesKey(data, Key.space) || matchesKey(data, Key.tab)) {
|
|
512
473
|
flipSelected();
|
|
513
474
|
} else {
|
|
514
475
|
const printable = decodeKittyPrintable(data);
|
|
@@ -553,9 +514,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
553
514
|
return;
|
|
554
515
|
}
|
|
555
516
|
const rows = getRows();
|
|
556
|
-
const msg = targets
|
|
557
|
-
.map((t) => toggleTool(verb, t, rows, ops))
|
|
558
|
-
.join("\n");
|
|
517
|
+
const msg = targets.map((t) => toggleTool(verb, t, rows, ops)).join("\n");
|
|
559
518
|
ctx.ui.notify(msg, "info");
|
|
560
519
|
return;
|
|
561
520
|
}
|