@xynogen/pix-toolbox 0.1.14 → 0.1.16
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 +2 -1
- package/src/toolbox.ts +8 -6
- package/src/toolbox.test.ts +0 -319
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-toolbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Pi tool — gated tool toggle UI (/toolbox)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src",
|
|
12
|
+
"!src/**/*.test.*",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
package/src/toolbox.ts
CHANGED
|
@@ -345,8 +345,8 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
345
345
|
const accent = "accent";
|
|
346
346
|
const mute = (s: string) => theme.fg("muted", s);
|
|
347
347
|
const guide = (key: string, action: string) =>
|
|
348
|
-
theme.fg("text", key) + theme.fg("
|
|
349
|
-
const guideSep = theme.fg("
|
|
348
|
+
theme.fg("text", key) + theme.fg("muted", ` ${action}`);
|
|
349
|
+
const guideSep = theme.fg("muted", " · ");
|
|
350
350
|
|
|
351
351
|
type RowState = "active" | "gated";
|
|
352
352
|
const stateOf = (name: string): RowState => (ops.isActive(name) ? "active" : "gated");
|
|
@@ -354,7 +354,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
354
354
|
const labelFor = (r: ToolRow): string => {
|
|
355
355
|
const active = stateOf(r.name) === "active";
|
|
356
356
|
const marker = active ? " " : theme.fg("warning", "#");
|
|
357
|
-
const name = active ? theme.fg("success", r.name) : theme.fg("
|
|
357
|
+
const name = active ? theme.fg("success", r.name) : theme.fg("muted", r.name);
|
|
358
358
|
const kind = mute(`[${r.mcp ? "MCP" : "tool"}]`);
|
|
359
359
|
return `${marker} ${name} ${kind}`;
|
|
360
360
|
};
|
|
@@ -386,7 +386,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
386
386
|
selectedPrefix: (t: string) => theme.fg(accent, t),
|
|
387
387
|
selectedText: (t: string) => theme.fg(accent, t),
|
|
388
388
|
description: (t: string) => t,
|
|
389
|
-
scrollInfo: (t: string) => theme.fg("
|
|
389
|
+
scrollInfo: (t: string) => theme.fg("muted", t),
|
|
390
390
|
noMatch: (t: string) => theme.fg("warning", t),
|
|
391
391
|
},
|
|
392
392
|
{
|
|
@@ -395,6 +395,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
395
395
|
},
|
|
396
396
|
);
|
|
397
397
|
|
|
398
|
+
// SAFETY: SelectList exposes these stable fields internally for label refreshes.
|
|
398
399
|
const internal = list as unknown as {
|
|
399
400
|
items: SelectItem[];
|
|
400
401
|
filteredItems: SelectItem[];
|
|
@@ -420,7 +421,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
420
421
|
const sel = list.getSelectedItem();
|
|
421
422
|
if (!sel) return;
|
|
422
423
|
const msg = toggleTool(action, sel.value, rows, ops);
|
|
423
|
-
statusText = theme.fg("
|
|
424
|
+
statusText = theme.fg("dim", msg);
|
|
424
425
|
refreshLabels();
|
|
425
426
|
};
|
|
426
427
|
|
|
@@ -473,7 +474,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
473
474
|
minHeight: MIN_MODAL_HEIGHT,
|
|
474
475
|
header: [
|
|
475
476
|
theme.fg(accent, theme.bold("🧰 Toolbox")),
|
|
476
|
-
theme.fg("
|
|
477
|
+
theme.fg("dim", "Search:"),
|
|
477
478
|
...search.render(inner),
|
|
478
479
|
"",
|
|
479
480
|
],
|
|
@@ -565,6 +566,7 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
565
566
|
}
|
|
566
567
|
|
|
567
568
|
if (typeof ctx.ui.custom === "function") {
|
|
569
|
+
// SAFETY: The runtime command context satisfies showPicker's narrowed UI contract.
|
|
568
570
|
await showPicker(ctx as unknown as Parameters<typeof showPicker>[0]);
|
|
569
571
|
} else {
|
|
570
572
|
ctx.ui.notify(renderList(getRows(), ops.isActive), "info");
|
package/src/toolbox.test.ts
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import registerToolbox, {
|
|
6
|
-
buildRows,
|
|
7
|
-
parseTargets,
|
|
8
|
-
renderList,
|
|
9
|
-
type ToggleOps,
|
|
10
|
-
type ToolRow,
|
|
11
|
-
toggleTool,
|
|
12
|
-
} from "./toolbox.ts";
|
|
13
|
-
|
|
14
|
-
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
const toolInfo = (name: string, source = "builtin") =>
|
|
17
|
-
({
|
|
18
|
-
name,
|
|
19
|
-
description: `${name} does things.`,
|
|
20
|
-
parameters: {},
|
|
21
|
-
sourceInfo: { source, path: "", scope: "user", origin: "package" },
|
|
22
|
-
}) as never;
|
|
23
|
-
|
|
24
|
-
const mcpToolInfo = (name: string) => toolInfo(name, "mcp:context7");
|
|
25
|
-
|
|
26
|
-
// ─── buildRows ──────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
describe("buildRows", () => {
|
|
29
|
-
test("excludes core tools (bash, edit, read, write)", () => {
|
|
30
|
-
const rows = buildRows([
|
|
31
|
-
toolInfo("bash"),
|
|
32
|
-
toolInfo("edit"),
|
|
33
|
-
toolInfo("read"),
|
|
34
|
-
toolInfo("write"),
|
|
35
|
-
toolInfo("grep"),
|
|
36
|
-
toolInfo("ls"),
|
|
37
|
-
]);
|
|
38
|
-
expect(rows.map((r) => r.name)).toEqual(["grep", "ls"]);
|
|
39
|
-
expect(rows.every((r) => !r.mcp)).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("flags MCP tools", () => {
|
|
43
|
-
const rows = buildRows([mcpToolInfo("ctx_search")]);
|
|
44
|
-
expect((rows[0] as ToolRow).mcp).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("empty input returns empty", () => {
|
|
48
|
-
expect(buildRows([])).toEqual([]);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// ─── parseTargets ───────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
describe("parseTargets", () => {
|
|
55
|
-
test("splits on commas, spaces, newlines", () => {
|
|
56
|
-
expect(parseTargets("ls, find grep\nfetch")).toEqual(["ls", "find", "grep", "fetch"]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("dedupes", () => {
|
|
60
|
-
expect(parseTargets("ls, ls")).toEqual(["ls"]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("empty yields empty", () => {
|
|
64
|
-
expect(parseTargets("")).toEqual([]);
|
|
65
|
-
expect(parseTargets(" , ")).toEqual([]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ─── renderList ─────────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
describe("renderList", () => {
|
|
72
|
-
const rows: ToolRow[] = [
|
|
73
|
-
{ name: "read", description: "Read files.", mcp: false },
|
|
74
|
-
{ name: "grep", description: "Search files.", mcp: false },
|
|
75
|
-
{ name: "ctx", description: "MCP search.", mcp: true },
|
|
76
|
-
];
|
|
77
|
-
const isActive = (n: string) => n === "read";
|
|
78
|
-
|
|
79
|
-
test("shows status for each tool", () => {
|
|
80
|
-
const out = renderList(rows, isActive);
|
|
81
|
-
expect(out).toContain("✓ active read");
|
|
82
|
-
expect(out).toContain("# gated grep");
|
|
83
|
-
expect(out).toContain("# gated ctx");
|
|
84
|
-
expect(out).toContain("[MCP]");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("filters by query", () => {
|
|
88
|
-
const out = renderList(rows, isActive, "grep");
|
|
89
|
-
expect(out).toContain("grep");
|
|
90
|
-
expect(out).not.toContain("read");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("no match message", () => {
|
|
94
|
-
const out = renderList(rows, isActive, "zzz");
|
|
95
|
-
expect(out).toContain('No tools matched "zzz"');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("empty rows", () => {
|
|
99
|
-
expect(renderList([], isActive)).toContain("No tools registered");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// ─── toggleTool ─────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
describe("toggleTool", () => {
|
|
106
|
-
const rows: ToolRow[] = [
|
|
107
|
-
{ name: "read", description: "Read.", mcp: false },
|
|
108
|
-
{ name: "grep", description: "Search.", mcp: false },
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
test("enable calls onActivate", () => {
|
|
112
|
-
let called = "";
|
|
113
|
-
const ops: ToggleOps = {
|
|
114
|
-
onActivate: (n) => {
|
|
115
|
-
called = n;
|
|
116
|
-
return true;
|
|
117
|
-
},
|
|
118
|
-
onDeactivate: () => false,
|
|
119
|
-
isActive: () => false,
|
|
120
|
-
};
|
|
121
|
-
const msg = toggleTool("enable", "grep", rows, ops);
|
|
122
|
-
expect(called).toBe("grep");
|
|
123
|
-
expect(msg).toContain("Enabled grep");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("disable calls onDeactivate", () => {
|
|
127
|
-
let called = "";
|
|
128
|
-
const ops: ToggleOps = {
|
|
129
|
-
onActivate: () => false,
|
|
130
|
-
onDeactivate: (n) => {
|
|
131
|
-
called = n;
|
|
132
|
-
return true;
|
|
133
|
-
},
|
|
134
|
-
isActive: () => true,
|
|
135
|
-
};
|
|
136
|
-
const msg = toggleTool("disable", "read", rows, ops);
|
|
137
|
-
expect(called).toBe("read");
|
|
138
|
-
expect(msg).toContain("Disabled read");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("unknown tool returns error", () => {
|
|
142
|
-
const ops: ToggleOps = {
|
|
143
|
-
onActivate: () => false,
|
|
144
|
-
onDeactivate: () => false,
|
|
145
|
-
isActive: () => false,
|
|
146
|
-
};
|
|
147
|
-
expect(toggleTool("enable", "nope", rows, ops)).toContain("Unknown");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("already active returns already message", () => {
|
|
151
|
-
const ops: ToggleOps = {
|
|
152
|
-
onActivate: () => false,
|
|
153
|
-
onDeactivate: () => false,
|
|
154
|
-
isActive: () => true,
|
|
155
|
-
};
|
|
156
|
-
expect(toggleTool("enable", "read", rows, ops)).toContain("already");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("already gated returns already message", () => {
|
|
160
|
-
const ops: ToggleOps = {
|
|
161
|
-
onActivate: () => false,
|
|
162
|
-
onDeactivate: () => false,
|
|
163
|
-
isActive: () => false,
|
|
164
|
-
};
|
|
165
|
-
expect(toggleTool("disable", "read", rows, ops)).toContain("already");
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// ─── Integration: /toolbox command ──────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
// Isolate from real ~/.pi/agent/toolbox.json on disk
|
|
172
|
-
let tmpAgentDir: string;
|
|
173
|
-
beforeAll(() => {
|
|
174
|
-
tmpAgentDir = mkdtempSync(join(tmpdir(), "toolbox-test-"));
|
|
175
|
-
process.env.PI_CODING_AGENT_DIR = tmpAgentDir;
|
|
176
|
-
});
|
|
177
|
-
afterAll(() => {
|
|
178
|
-
delete process.env.PI_CODING_AGENT_DIR;
|
|
179
|
-
try {
|
|
180
|
-
rmSync(tmpAgentDir, { recursive: true });
|
|
181
|
-
} catch {
|
|
182
|
-
// temp dir may already be gone — safe to ignore
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
function makeHost(toolNames: string[]) {
|
|
187
|
-
const handlers: Record<string, Array<(p: unknown) => unknown>> = {};
|
|
188
|
-
let active: string[] = [...toolNames];
|
|
189
|
-
const commands: Array<{
|
|
190
|
-
name: string;
|
|
191
|
-
handler: (...args: unknown[]) => unknown;
|
|
192
|
-
}> = [];
|
|
193
|
-
const pi = {
|
|
194
|
-
on(ev: string, fn: (p: unknown) => unknown) {
|
|
195
|
-
if (!handlers[ev]) handlers[ev] = [];
|
|
196
|
-
handlers[ev].push(fn);
|
|
197
|
-
},
|
|
198
|
-
emit(ev: string, payload: unknown, ctx?: unknown) {
|
|
199
|
-
return Promise.all(
|
|
200
|
-
(handlers[ev] ?? []).map((f) => (f as (...args: unknown[]) => unknown)(payload, ctx)),
|
|
201
|
-
);
|
|
202
|
-
},
|
|
203
|
-
getAllTools() {
|
|
204
|
-
return toolNames.map((name) => ({
|
|
205
|
-
name,
|
|
206
|
-
description: `${name} does things.`,
|
|
207
|
-
parameters: {},
|
|
208
|
-
sourceInfo: { source: "builtin" },
|
|
209
|
-
}));
|
|
210
|
-
},
|
|
211
|
-
getActiveTools() {
|
|
212
|
-
return active;
|
|
213
|
-
},
|
|
214
|
-
setActiveTools(names: string[]) {
|
|
215
|
-
active = [...names];
|
|
216
|
-
},
|
|
217
|
-
getCommands() {
|
|
218
|
-
return [];
|
|
219
|
-
},
|
|
220
|
-
appendEntry() {},
|
|
221
|
-
registerTool() {},
|
|
222
|
-
registerCommand(name: string, def: { handler: (...args: unknown[]) => unknown }) {
|
|
223
|
-
commands.push({ name, handler: def.handler });
|
|
224
|
-
},
|
|
225
|
-
} as never;
|
|
226
|
-
const emit = (ev: string, payload: unknown, ctx?: unknown) =>
|
|
227
|
-
Promise.all(
|
|
228
|
-
(handlers[ev] ?? []).map((f) => (f as (...args: unknown[]) => unknown)(payload, ctx)),
|
|
229
|
-
);
|
|
230
|
-
return {
|
|
231
|
-
pi,
|
|
232
|
-
emit,
|
|
233
|
-
getActive: () => active,
|
|
234
|
-
command: (name: string) => commands.find((c) => c.name === name),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function makeCtx() {
|
|
239
|
-
const notes: Array<{ text: string; level?: string }> = [];
|
|
240
|
-
const ctx = {
|
|
241
|
-
ui: {
|
|
242
|
-
notify(text: string, level?: string) {
|
|
243
|
-
notes.push({ text, level });
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
} as never;
|
|
247
|
-
return { ctx, notes };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
describe("/toolbox command", () => {
|
|
251
|
-
type Note = { text: string; level?: string };
|
|
252
|
-
const ALL = ["read", "write", "bash", "grep", "find"];
|
|
253
|
-
|
|
254
|
-
async function boot() {
|
|
255
|
-
const host = makeHost(ALL);
|
|
256
|
-
registerToolbox(host.pi);
|
|
257
|
-
// session_start triggers init
|
|
258
|
-
await host.emit("session_start", {}, {});
|
|
259
|
-
return host;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
test("registers a /toolbox command", async () => {
|
|
263
|
-
const host = await boot();
|
|
264
|
-
expect(host.command("toolbox")).toBeDefined();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test("bare /toolbox falls back to listing when no custom UI", async () => {
|
|
268
|
-
const host = await boot();
|
|
269
|
-
const { ctx, notes } = makeCtx();
|
|
270
|
-
await host.command("toolbox")?.handler("", ctx);
|
|
271
|
-
expect(notes.length).toBe(1);
|
|
272
|
-
// only non-core tools shown, all start active
|
|
273
|
-
const n0 = notes[0] as Note;
|
|
274
|
-
expect(n0.text).toContain("✓ active grep");
|
|
275
|
-
expect(n0.text).toContain("✓ active find");
|
|
276
|
-
// core tools excluded from toolbox
|
|
277
|
-
expect(n0.text).not.toContain(" read");
|
|
278
|
-
expect(n0.text).not.toContain(" bash");
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("/toolbox list shows non-core tools with status", async () => {
|
|
282
|
-
const host = await boot();
|
|
283
|
-
const { ctx, notes } = makeCtx();
|
|
284
|
-
await host.command("toolbox")?.handler("list", ctx);
|
|
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");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("/toolbox list <query> filters", async () => {
|
|
292
|
-
const host = await boot();
|
|
293
|
-
const { ctx, notes } = makeCtx();
|
|
294
|
-
await host.command("toolbox")?.handler("list fin", ctx);
|
|
295
|
-
const fn0 = notes[0] as Note;
|
|
296
|
-
expect(fn0.text).toContain("find");
|
|
297
|
-
expect(fn0.text).not.toContain("✓ active read");
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("opens interactive picker when ctx.ui.custom exists", async () => {
|
|
301
|
-
const host = await boot();
|
|
302
|
-
let customCalled = 0;
|
|
303
|
-
const notes: Array<{ text: string; level?: string }> = [];
|
|
304
|
-
const ctx = {
|
|
305
|
-
ui: {
|
|
306
|
-
notify(text: string, level?: string) {
|
|
307
|
-
notes.push({ text, level });
|
|
308
|
-
},
|
|
309
|
-
async custom() {
|
|
310
|
-
customCalled++;
|
|
311
|
-
return null;
|
|
312
|
-
},
|
|
313
|
-
},
|
|
314
|
-
} as never;
|
|
315
|
-
await host.command("toolbox")?.handler("", ctx);
|
|
316
|
-
expect(customCalled).toBe(1);
|
|
317
|
-
expect(notes.length).toBe(0);
|
|
318
|
-
});
|
|
319
|
-
});
|