@xynogen/pix-core 0.2.4 → 0.3.0
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 +11 -17
- package/skills/ask-user/SKILL.md +0 -48
- package/src/commands/agent-sop/agent-sop.ts +0 -58
- package/src/commands/clear/clear.ts +0 -32
- package/src/commands/diff/diff.ts +0 -32
- package/src/commands/models/models.test.ts +0 -95
- package/src/commands/models/models.ts +0 -367
- package/src/commands/models/patch-builtin.test.ts +0 -66
- package/src/commands/models/patch-builtin.ts +0 -120
- package/src/commands/tools.test.ts +0 -15
- package/src/commands/update/update.test.ts +0 -112
- package/src/commands/update/update.ts +0 -271
- package/src/index.ts +0 -45
- package/src/lib/data.ts +0 -33
- package/src/nudge/capability.test.ts +0 -258
- package/src/nudge/capability.ts +0 -189
- package/src/nudge/index.ts +0 -17
- package/src/nudge/tools.test.ts +0 -157
- package/src/nudge/tools.ts +0 -212
- package/src/tool/ask/ask.test.ts +0 -243
- package/src/tool/ask/components.ts +0 -55
- package/src/tool/ask/helpers.ts +0 -77
- package/src/tool/ask/index.ts +0 -130
- package/src/tool/ask/questionnaire.ts +0 -693
- package/src/tool/ask/rpc.ts +0 -84
- package/src/tool/ask/schema.ts +0 -69
- package/src/tool/ask/single-select-layout.test.ts +0 -124
- package/src/tool/ask/single-select-layout.ts +0 -237
- package/src/tool/ask/types.ts +0 -17
- package/src/tool/todo/todo.test.ts +0 -646
- package/src/tool/todo/todo.ts +0 -218
- package/src/tool/toolbox/toolbox.test.ts +0 -314
- package/src/tool/toolbox/toolbox.ts +0 -570
- package/src/ui/diagnostics.ts +0 -145
- package/src/ui/footer.ts +0 -513
- package/src/ui/welcome.test.ts +0 -124
- package/src/ui/welcome.ts +0 -369
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* toolbox.ts — /toolbox command for user-driven tool gating
|
|
3
|
-
*
|
|
4
|
-
* Registers a `/toolbox` slash command that opens a TUI picker listing every
|
|
5
|
-
* registered tool (built-in and MCP). The user can toggle tools on/off —
|
|
6
|
-
* this controls which tools are described in the system prompt via
|
|
7
|
-
* pi.setActiveTools(). All tools remain callable via function definitions
|
|
8
|
-
* regardless of prompt visibility.
|
|
9
|
-
*
|
|
10
|
-
* Also supports headless usage:
|
|
11
|
-
* /toolbox enable <names> — enable tool(s) by name
|
|
12
|
-
* /toolbox disable <names> — disable tool(s) by name
|
|
13
|
-
* /toolbox list [query] — text search (no picker)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { dirname, join } from "node:path";
|
|
18
|
-
import type {
|
|
19
|
-
ExtensionAPI,
|
|
20
|
-
ExtensionContext,
|
|
21
|
-
Theme,
|
|
22
|
-
ToolInfo,
|
|
23
|
-
} from "@earendil-works/pi-coding-agent";
|
|
24
|
-
import { DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import {
|
|
26
|
-
Container,
|
|
27
|
-
decodeKittyPrintable,
|
|
28
|
-
fuzzyFilter,
|
|
29
|
-
Input,
|
|
30
|
-
Key,
|
|
31
|
-
type KeybindingsManager,
|
|
32
|
-
matchesKey,
|
|
33
|
-
type SelectItem,
|
|
34
|
-
SelectList,
|
|
35
|
-
Text,
|
|
36
|
-
type TUI,
|
|
37
|
-
visibleWidth,
|
|
38
|
-
} from "@earendil-works/pi-tui";
|
|
39
|
-
|
|
40
|
-
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
/** Tools that can never be disabled — always prompt-visible. */
|
|
43
|
-
export const CORE_TOOLS: ReadonlySet<string> = new Set([
|
|
44
|
-
"bash",
|
|
45
|
-
"edit",
|
|
46
|
-
"read",
|
|
47
|
-
"write",
|
|
48
|
-
]);
|
|
49
|
-
|
|
50
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
export interface ToolRow {
|
|
53
|
-
name: string;
|
|
54
|
-
description: string;
|
|
55
|
-
mcp: boolean;
|
|
56
|
-
source?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Callbacks for toggleTool / renderList — test seam. */
|
|
60
|
-
export interface ToggleOps {
|
|
61
|
-
isActive: (name: string) => boolean;
|
|
62
|
-
onActivate: (name: string) => boolean;
|
|
63
|
-
onDeactivate: (name: string) => boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
function isMcpTool(info: ToolInfo): boolean {
|
|
69
|
-
return /mcp/i.test(info.sourceInfo?.source ?? "");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function buildRows(tools: ToolInfo[]): ToolRow[] {
|
|
73
|
-
return tools
|
|
74
|
-
.filter((t) => !CORE_TOOLS.has(t.name))
|
|
75
|
-
.map((t) => ({
|
|
76
|
-
name: t.name,
|
|
77
|
-
description: firstSentence(t.description ?? ""),
|
|
78
|
-
mcp: isMcpTool(t),
|
|
79
|
-
source: t.sourceInfo?.source,
|
|
80
|
-
}))
|
|
81
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const firstSentence = (desc: string): string => {
|
|
85
|
-
const clean = (desc ?? "").replace(/\s+/g, " ").trim();
|
|
86
|
-
const m = clean.match(/^.*?[.!?](?=\s|$)/);
|
|
87
|
-
return (m ? m[0] : clean).slice(0, 120);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export function parseTargets(raw: string): string[] {
|
|
91
|
-
const seen = new Set<string>();
|
|
92
|
-
const out: string[] = [];
|
|
93
|
-
for (const t of raw.split(/[\s,]+/)) {
|
|
94
|
-
const name = t.trim();
|
|
95
|
-
if (!name || seen.has(name)) continue;
|
|
96
|
-
seen.add(name);
|
|
97
|
-
out.push(name);
|
|
98
|
-
}
|
|
99
|
-
return out;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function renderList(
|
|
103
|
-
rows: ToolRow[],
|
|
104
|
-
isActive: (name: string) => boolean,
|
|
105
|
-
query?: string,
|
|
106
|
-
): string {
|
|
107
|
-
const filtered = query
|
|
108
|
-
? rows.filter(
|
|
109
|
-
(r) =>
|
|
110
|
-
r.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
111
|
-
r.description.toLowerCase().includes(query.toLowerCase()),
|
|
112
|
-
)
|
|
113
|
-
: rows;
|
|
114
|
-
|
|
115
|
-
if (!filtered.length) {
|
|
116
|
-
return query ? `No tools matched "${query}".` : "No tools registered.";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const lines = filtered.map((r) => {
|
|
120
|
-
const status = isActive(r.name) ? "✓ active" : "# gated";
|
|
121
|
-
const kind = r.mcp ? "MCP" : "tool";
|
|
122
|
-
return `${status} ${r.name} [${kind}] ${r.description}`;
|
|
123
|
-
});
|
|
124
|
-
return lines.join("\n");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function toggleTool(
|
|
128
|
-
action: "enable" | "disable",
|
|
129
|
-
name: string,
|
|
130
|
-
rows: ToolRow[],
|
|
131
|
-
ops: ToggleOps,
|
|
132
|
-
): string {
|
|
133
|
-
const row = rows.find((r) => r.name === name);
|
|
134
|
-
if (!row) return `Unknown tool "${name}".`;
|
|
135
|
-
|
|
136
|
-
if (action === "enable") {
|
|
137
|
-
const did = ops.onActivate(name);
|
|
138
|
-
return did
|
|
139
|
-
? `Enabled ${name} — now prompt-visible.`
|
|
140
|
-
: `${name} is already active.`;
|
|
141
|
-
}
|
|
142
|
-
const did = ops.onDeactivate(name);
|
|
143
|
-
return did
|
|
144
|
-
? `Disabled ${name} — hidden from prompt.`
|
|
145
|
-
: `${name} is already gated.`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─── Persistence ───────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
interface ToolboxState {
|
|
151
|
-
enabledTools: string[];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getStatePath(): string {
|
|
155
|
-
return join(getAgentDir(), "toolbox.json");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ─── State ──────────────────────────────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
function createState(pi: ExtensionAPI) {
|
|
161
|
-
let enabledTools = new Set<string>();
|
|
162
|
-
let initialized = false;
|
|
163
|
-
|
|
164
|
-
function persist(): void {
|
|
165
|
-
// Write to session so state survives branch navigation within a session
|
|
166
|
-
try {
|
|
167
|
-
pi.appendEntry<ToolboxState>("toolbox-config", {
|
|
168
|
-
enabledTools: [...enabledTools],
|
|
169
|
-
});
|
|
170
|
-
} catch (err) {
|
|
171
|
-
console.warn("toolbox: persist failed:", err);
|
|
172
|
-
}
|
|
173
|
-
// Write to disk so state survives across completely new sessions
|
|
174
|
-
try {
|
|
175
|
-
const sp = getStatePath();
|
|
176
|
-
mkdirSync(dirname(sp), { recursive: true });
|
|
177
|
-
writeFileSync(
|
|
178
|
-
sp,
|
|
179
|
-
JSON.stringify({ enabledTools: [...enabledTools] }, null, 2),
|
|
180
|
-
"utf-8",
|
|
181
|
-
);
|
|
182
|
-
} catch (err) {
|
|
183
|
-
console.warn("toolbox: file persist failed:", err);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Load previously persisted enabled tool names from disk. */
|
|
188
|
-
function loadFromFile(): string[] | undefined {
|
|
189
|
-
try {
|
|
190
|
-
const sp = getStatePath();
|
|
191
|
-
if (!existsSync(sp)) return undefined;
|
|
192
|
-
const raw = JSON.parse(readFileSync(sp, "utf-8")) as ToolboxState;
|
|
193
|
-
if (Array.isArray(raw?.enabledTools)) return raw.enabledTools;
|
|
194
|
-
} catch {
|
|
195
|
-
// File doesn't exist, is corrupt, or we're in a test env without getAgentDir
|
|
196
|
-
}
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function restoreFromBranch(ctx: ExtensionContext): void {
|
|
201
|
-
// Prefer file-based persistence (survives across sessions).
|
|
202
|
-
// Fall back to session entries (survives branch navigation within a session).
|
|
203
|
-
// Fall back to full enable (first run).
|
|
204
|
-
const fileSaved = loadFromFile();
|
|
205
|
-
if (fileSaved) {
|
|
206
|
-
const validNames = new Set((pi.getAllTools() ?? []).map((t) => t.name));
|
|
207
|
-
enabledTools = new Set(
|
|
208
|
-
fileSaved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)),
|
|
209
|
-
);
|
|
210
|
-
for (const ct of CORE_TOOLS) enabledTools.add(ct);
|
|
211
|
-
initialized = true;
|
|
212
|
-
apply();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Fall back to plain init if sessionManager is unavailable (e.g. tests / headless)
|
|
217
|
-
if (!ctx?.sessionManager) {
|
|
218
|
-
ensureInit();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// getEntries() returns ALL entries in the session file — unlike getBranch()
|
|
223
|
-
// which only walks ancestors. Custom entries appended via appendCustomEntry
|
|
224
|
-
// are children of the leaf, not ancestors.
|
|
225
|
-
const allEntries = ctx.sessionManager.getEntries();
|
|
226
|
-
let saved: string[] | undefined;
|
|
227
|
-
|
|
228
|
-
for (const entry of allEntries) {
|
|
229
|
-
if (entry.type === "custom" && entry.customType === "toolbox-config") {
|
|
230
|
-
const data = entry.data as ToolboxState | undefined;
|
|
231
|
-
if (data?.enabledTools) saved = data.enabledTools;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (saved) {
|
|
236
|
-
const validNames = new Set((pi.getAllTools() ?? []).map((t) => t.name));
|
|
237
|
-
enabledTools = new Set(
|
|
238
|
-
saved.filter((n) => validNames.has(n) || CORE_TOOLS.has(n)),
|
|
239
|
-
);
|
|
240
|
-
for (const ct of CORE_TOOLS) enabledTools.add(ct);
|
|
241
|
-
} else {
|
|
242
|
-
const names = (pi.getAllTools() ?? []).map((t) => t.name);
|
|
243
|
-
enabledTools = new Set(names);
|
|
244
|
-
}
|
|
245
|
-
initialized = true;
|
|
246
|
-
apply();
|
|
247
|
-
// Persist to disk on first init so future sessions pick it up
|
|
248
|
-
persist();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function ensureInit(): void {
|
|
252
|
-
if (initialized) return;
|
|
253
|
-
let names: string[] = [];
|
|
254
|
-
try {
|
|
255
|
-
names = (pi.getAllTools() ?? []).map((t) => t.name);
|
|
256
|
-
} catch (err) {
|
|
257
|
-
console.warn("toolbox: getAllTools failed:", err);
|
|
258
|
-
}
|
|
259
|
-
if (!names.length) return;
|
|
260
|
-
enabledTools = new Set(names);
|
|
261
|
-
initialized = true;
|
|
262
|
-
apply();
|
|
263
|
-
persist();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function apply(): void {
|
|
267
|
-
if (!initialized) return;
|
|
268
|
-
try {
|
|
269
|
-
pi.setActiveTools([...enabledTools]);
|
|
270
|
-
} catch (err) {
|
|
271
|
-
console.warn("toolbox: setActiveTools failed:", err);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function isActive(name: string): boolean {
|
|
276
|
-
return enabledTools.has(name);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function onActivate(name: string): boolean {
|
|
280
|
-
if (!initialized) return false;
|
|
281
|
-
if (enabledTools.has(name)) return false;
|
|
282
|
-
enabledTools.add(name);
|
|
283
|
-
apply();
|
|
284
|
-
persist();
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function onDeactivate(name: string): boolean {
|
|
289
|
-
if (!initialized) return false;
|
|
290
|
-
if (CORE_TOOLS.has(name)) return false;
|
|
291
|
-
const did = enabledTools.delete(name);
|
|
292
|
-
if (did) {
|
|
293
|
-
apply();
|
|
294
|
-
persist();
|
|
295
|
-
}
|
|
296
|
-
return did;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
ensureInit,
|
|
301
|
-
restoreFromBranch,
|
|
302
|
-
isActive,
|
|
303
|
-
onActivate,
|
|
304
|
-
onDeactivate,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ─── Registration ───────────────────────────────────────────────────────────
|
|
309
|
-
|
|
310
|
-
export default function registerToolbox(pi: ExtensionAPI): void {
|
|
311
|
-
const state = createState(pi);
|
|
312
|
-
|
|
313
|
-
// Defer init until tools are registered — session_start fires after all extensions load.
|
|
314
|
-
// Try to restore persisted state; fall back to full init if no config found.
|
|
315
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
316
|
-
state.restoreFromBranch(ctx);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Re-restore when navigating branch history
|
|
320
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
321
|
-
state.restoreFromBranch(ctx);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
function getRows(): ToolRow[] {
|
|
325
|
-
try {
|
|
326
|
-
return buildRows(pi.getAllTools() ?? []);
|
|
327
|
-
} catch {
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const ops: ToggleOps = {
|
|
333
|
-
isActive: state.isActive,
|
|
334
|
-
onActivate: state.onActivate,
|
|
335
|
-
onDeactivate: state.onDeactivate,
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
async function showPicker(ctx: {
|
|
339
|
-
ui: {
|
|
340
|
-
custom: <T>(f: unknown) => Promise<T>;
|
|
341
|
-
notify: (m: string, t?: "info" | "warning" | "error") => void;
|
|
342
|
-
};
|
|
343
|
-
}): Promise<void> {
|
|
344
|
-
await ctx.ui.custom<null>(
|
|
345
|
-
(
|
|
346
|
-
tui: TUI,
|
|
347
|
-
theme: Theme,
|
|
348
|
-
_kb: KeybindingsManager,
|
|
349
|
-
done: (r: null) => void,
|
|
350
|
-
) => {
|
|
351
|
-
const accent = "accent";
|
|
352
|
-
const mute = (s: string) => theme.fg("muted", s);
|
|
353
|
-
const container = new Container();
|
|
354
|
-
|
|
355
|
-
type RowState = "active" | "gated";
|
|
356
|
-
const stateOf = (name: string): RowState =>
|
|
357
|
-
ops.isActive(name) ? "active" : "gated";
|
|
358
|
-
|
|
359
|
-
const labelFor = (r: ToolRow): string => {
|
|
360
|
-
const active = stateOf(r.name) === "active";
|
|
361
|
-
const marker = active ? " " : theme.fg("warning", "#");
|
|
362
|
-
const name = active
|
|
363
|
-
? theme.fg("success", r.name)
|
|
364
|
-
: theme.fg("dim", r.name);
|
|
365
|
-
const kind = mute(`[${r.mcp ? "MCP" : "tool"}]`);
|
|
366
|
-
return `${marker} ${name} ${kind}`;
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
const descFor = (r: ToolRow): string => {
|
|
370
|
-
const active = stateOf(r.name) === "active";
|
|
371
|
-
const tag = active
|
|
372
|
-
? theme.fg("success", "active")
|
|
373
|
-
: theme.fg("warning", "gated");
|
|
374
|
-
return `${tag} ${mute("·")} ${r.description || "(no description)"}`;
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const rows = getRows();
|
|
378
|
-
const byValue = new Map<string, ToolRow>();
|
|
379
|
-
const toItem = (r: ToolRow): SelectItem => {
|
|
380
|
-
byValue.set(r.name, r);
|
|
381
|
-
return {
|
|
382
|
-
value: r.name,
|
|
383
|
-
label: labelFor(r),
|
|
384
|
-
description: descFor(r),
|
|
385
|
-
};
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
const allItems = rows.map(toItem);
|
|
389
|
-
const widest = allItems.reduce(
|
|
390
|
-
(w, it) => Math.max(w, visibleWidth(it.label)),
|
|
391
|
-
0,
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
const list = new SelectList(
|
|
395
|
-
allItems,
|
|
396
|
-
Math.min(allItems.length, 14),
|
|
397
|
-
{
|
|
398
|
-
selectedPrefix: (t: string) => theme.fg(accent, t),
|
|
399
|
-
selectedText: (t: string) => theme.fg(accent, t),
|
|
400
|
-
description: (t: string) => t,
|
|
401
|
-
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
402
|
-
noMatch: (t: string) => theme.fg("warning", t),
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
minPrimaryColumnWidth: widest + 2,
|
|
406
|
-
maxPrimaryColumnWidth: widest + 2,
|
|
407
|
-
},
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
const internal = list as unknown as {
|
|
411
|
-
items: SelectItem[];
|
|
412
|
-
filteredItems: SelectItem[];
|
|
413
|
-
selectedIndex: number;
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const search = new Input();
|
|
417
|
-
const statusLine = new Text("");
|
|
418
|
-
|
|
419
|
-
const refreshLabels = () => {
|
|
420
|
-
for (const it of internal.items) {
|
|
421
|
-
const r = byValue.get(it.value);
|
|
422
|
-
if (!r) continue;
|
|
423
|
-
it.label = labelFor(r);
|
|
424
|
-
it.description = descFor(r);
|
|
425
|
-
}
|
|
426
|
-
list.invalidate();
|
|
427
|
-
container.invalidate();
|
|
428
|
-
tui.requestRender?.();
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
const doToggle = (action: "enable" | "disable") => {
|
|
432
|
-
const sel = list.getSelectedItem();
|
|
433
|
-
if (!sel) return;
|
|
434
|
-
const msg = toggleTool(action, sel.value, rows, ops);
|
|
435
|
-
statusLine.setText(theme.fg("muted", msg));
|
|
436
|
-
refreshLabels();
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const flipSelected = () => {
|
|
440
|
-
const sel = list.getSelectedItem();
|
|
441
|
-
if (!sel) return;
|
|
442
|
-
if (stateOf(sel.value) === "active") doToggle("disable");
|
|
443
|
-
else doToggle("enable");
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const applyFilter = (q: string) => {
|
|
447
|
-
const query = q.trim();
|
|
448
|
-
internal.filteredItems =
|
|
449
|
-
query.length === 0
|
|
450
|
-
? internal.items
|
|
451
|
-
: fuzzyFilter(
|
|
452
|
-
internal.items,
|
|
453
|
-
query,
|
|
454
|
-
(it: SelectItem) => `${it.value} ${it.description ?? ""}`,
|
|
455
|
-
);
|
|
456
|
-
internal.selectedIndex = 0;
|
|
457
|
-
list.invalidate();
|
|
458
|
-
container.invalidate();
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
list.onSelect = () => done(null);
|
|
462
|
-
list.onCancel = () => done(null);
|
|
463
|
-
search.onEscape = () => done(null);
|
|
464
|
-
|
|
465
|
-
container.addChild(
|
|
466
|
-
new DynamicBorder((s: string) => theme.fg(accent, s)),
|
|
467
|
-
);
|
|
468
|
-
container.addChild(
|
|
469
|
-
new Text(theme.fg(accent, theme.bold("🧰 Toolbox"))),
|
|
470
|
-
);
|
|
471
|
-
container.addChild(new Text(theme.fg("muted", "Search:")));
|
|
472
|
-
container.addChild(search);
|
|
473
|
-
container.addChild(list);
|
|
474
|
-
container.addChild(statusLine);
|
|
475
|
-
container.addChild(
|
|
476
|
-
new Text(
|
|
477
|
-
theme.fg(
|
|
478
|
-
"dim",
|
|
479
|
-
"↑↓ navigate · e enable · d disable · space toggle · esc close",
|
|
480
|
-
),
|
|
481
|
-
),
|
|
482
|
-
);
|
|
483
|
-
container.addChild(
|
|
484
|
-
new DynamicBorder((s: string) => theme.fg(accent, s)),
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
return {
|
|
488
|
-
render(w: number) {
|
|
489
|
-
return container.render(w);
|
|
490
|
-
},
|
|
491
|
-
invalidate() {
|
|
492
|
-
container.invalidate();
|
|
493
|
-
},
|
|
494
|
-
handleInput(data: string) {
|
|
495
|
-
if (matchesKey(data, Key.up) || matchesKey(data, Key.down)) {
|
|
496
|
-
list.handleInput?.(data);
|
|
497
|
-
} else if (
|
|
498
|
-
matchesKey(data, Key.enter) ||
|
|
499
|
-
matchesKey(data, Key.escape)
|
|
500
|
-
) {
|
|
501
|
-
done(null);
|
|
502
|
-
return;
|
|
503
|
-
} else if (
|
|
504
|
-
matchesKey(data, Key.space) ||
|
|
505
|
-
matchesKey(data, Key.tab)
|
|
506
|
-
) {
|
|
507
|
-
flipSelected();
|
|
508
|
-
} else {
|
|
509
|
-
const printable = decodeKittyPrintable(data);
|
|
510
|
-
if (printable !== undefined) {
|
|
511
|
-
if (printable === "e") {
|
|
512
|
-
doToggle("enable");
|
|
513
|
-
} else if (printable === "d") {
|
|
514
|
-
doToggle("disable");
|
|
515
|
-
} else {
|
|
516
|
-
search.handleInput?.(data);
|
|
517
|
-
applyFilter(search.getValue?.() ?? "");
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
search.handleInput?.(data);
|
|
521
|
-
applyFilter(search.getValue?.() ?? "");
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
container.invalidate();
|
|
525
|
-
tui.requestRender?.();
|
|
526
|
-
},
|
|
527
|
-
};
|
|
528
|
-
},
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
pi.registerCommand("toolbox", {
|
|
533
|
-
description:
|
|
534
|
-
"Toggle tools on/off. ↑↓ navigate, e/d enable/disable, space toggle. " +
|
|
535
|
-
"Headless: /toolbox enable|disable <names>, /toolbox list [query]",
|
|
536
|
-
handler: async (args, ctx) => {
|
|
537
|
-
const raw = (args ?? "").trim();
|
|
538
|
-
const verb = raw.split(/\s+/, 1)[0]?.toLowerCase();
|
|
539
|
-
|
|
540
|
-
if (verb === "enable" || verb === "disable") {
|
|
541
|
-
const targets = parseTargets(raw.slice(verb.length).trim());
|
|
542
|
-
if (!targets.length) {
|
|
543
|
-
ctx.ui.notify(
|
|
544
|
-
`/toolbox ${verb} needs a tool name, e.g. /toolbox ${verb} grep`,
|
|
545
|
-
"warning",
|
|
546
|
-
);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
const rows = getRows();
|
|
550
|
-
const msg = targets
|
|
551
|
-
.map((t) => toggleTool(verb, t, rows, ops))
|
|
552
|
-
.join("\n");
|
|
553
|
-
ctx.ui.notify(msg, "info");
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (verb === "list") {
|
|
558
|
-
const query = raw.slice(verb.length).trim() || undefined;
|
|
559
|
-
ctx.ui.notify(renderList(getRows(), ops.isActive, query), "info");
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (typeof ctx.ui.custom === "function") {
|
|
564
|
-
await showPicker(ctx as unknown as Parameters<typeof showPicker>[0]);
|
|
565
|
-
} else {
|
|
566
|
-
ctx.ui.notify(renderList(getRows(), ops.isActive), "info");
|
|
567
|
-
}
|
|
568
|
-
},
|
|
569
|
-
});
|
|
570
|
-
}
|
package/src/ui/diagnostics.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* diagnostics.ts — Lightweight diagnostic reporter (pi-lens replacement)
|
|
3
|
-
*
|
|
4
|
-
* Renders LSP diagnostics from recently touched files with a collapsed view:
|
|
5
|
-
* - Header shows total counts across all files (●4E !1W)
|
|
6
|
-
* - Body shows top 3 diagnostics only
|
|
7
|
-
* - "+N more" line if diagnostics exceed 3
|
|
8
|
-
*
|
|
9
|
-
* Registers widget with id "pi-lens" to override external pi-lens widget.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
|
-
|
|
15
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
interface Diagnostic {
|
|
18
|
-
severity: "error" | "warning" | "information" | "hint";
|
|
19
|
-
message: string;
|
|
20
|
-
line?: number;
|
|
21
|
-
col?: number;
|
|
22
|
-
source?: string;
|
|
23
|
-
code?: string | number;
|
|
24
|
-
uri?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface FileRecord {
|
|
28
|
-
filePath: string;
|
|
29
|
-
diagnostics: Diagnostic[];
|
|
30
|
-
touchedAt: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─── Module state ─────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
const files = new Map<string, FileRecord>();
|
|
36
|
-
let requestRenderFn: (() => void) | null = null;
|
|
37
|
-
|
|
38
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
export function clearDiagnosticState(): void {
|
|
41
|
-
files.clear();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function requestRender(): void {
|
|
45
|
-
requestRenderFn?.();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ─── Diagnostic collection ────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Track that a file was touched. In this simplified version, we don't query
|
|
52
|
-
* LSP diagnostics directly (that requires a full LSP client). Instead, we
|
|
53
|
-
* register the file and show a placeholder/summary. Future enhancement: hook
|
|
54
|
-
* into pi-lens's diagnostic events or build LSP integration.
|
|
55
|
-
*/
|
|
56
|
-
function recordFileTouched(filePath: string): void {
|
|
57
|
-
const rec: FileRecord = {
|
|
58
|
-
filePath,
|
|
59
|
-
diagnostics: [], // Empty for now - we'd populate from LSP in full version
|
|
60
|
-
touchedAt: Date.now(),
|
|
61
|
-
};
|
|
62
|
-
files.set(filePath, rec);
|
|
63
|
-
requestRender();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
function renderWidget(width: number, theme: Theme): string[] {
|
|
69
|
-
const w = Math.max(1, width || 80);
|
|
70
|
-
|
|
71
|
-
const cyan = (s: string) => theme.fg("accent", s);
|
|
72
|
-
const dim = (s: string) => theme.fg("muted", s);
|
|
73
|
-
const green = (s: string) => theme.fg("success", s);
|
|
74
|
-
|
|
75
|
-
const lines: string[] = [];
|
|
76
|
-
|
|
77
|
-
// Show a compact summary. This widget overrides pi-lens's verbose output.
|
|
78
|
-
// For detailed diagnostics, users can run /lens-booboo or /lsp-diagnostics.
|
|
79
|
-
const filesCount = files.size;
|
|
80
|
-
|
|
81
|
-
if (filesCount === 0) {
|
|
82
|
-
// No files touched yet this session
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const recentFiles = [...files.values()]
|
|
87
|
-
.sort((a, b) => b.touchedAt - a.touchedAt)
|
|
88
|
-
.slice(0, 3)
|
|
89
|
-
.map((f) => f.filePath.split("/").pop() ?? f.filePath);
|
|
90
|
-
|
|
91
|
-
const filesList = recentFiles.join(", ");
|
|
92
|
-
const summary =
|
|
93
|
-
filesCount <= 3
|
|
94
|
-
? `${green("✓")} ${filesList}`
|
|
95
|
-
: `${green("✓")} ${filesList} +${filesCount - 3} more`;
|
|
96
|
-
|
|
97
|
-
const header = ` ${cyan("pix-lens")} ${summary} ${dim("(/lens-booboo for details)")}`;
|
|
98
|
-
lines.push(fitLine(header, w));
|
|
99
|
-
|
|
100
|
-
return lines;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function fitLine(s: string, maxWidth: number, ellipsis = "…"): string {
|
|
104
|
-
return truncateToWidth(s, Math.max(0, maxWidth), ellipsis);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ─── Extension ────────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
export default function (pi: ExtensionAPI) {
|
|
110
|
-
pi.on("session_start", (_event, ctx) => {
|
|
111
|
-
clearDiagnosticState();
|
|
112
|
-
|
|
113
|
-
// Register widget
|
|
114
|
-
if (!ctx.ui.setWidget) return;
|
|
115
|
-
ctx.ui.setWidget(
|
|
116
|
-
"pi-lens",
|
|
117
|
-
(tui, theme: Theme) => {
|
|
118
|
-
requestRenderFn = () => tui.requestRender();
|
|
119
|
-
return {
|
|
120
|
-
render: (width: number) => renderWidget(width, theme),
|
|
121
|
-
dispose() {
|
|
122
|
-
requestRenderFn = null;
|
|
123
|
-
},
|
|
124
|
-
invalidate() {},
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
|
-
{ placement: "belowEditor" },
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Track files after write/edit
|
|
132
|
-
pi.on("tool_result", async (event, _ctx) => {
|
|
133
|
-
if (event.toolName === "write" || event.toolName === "edit") {
|
|
134
|
-
const filePath = (event.input as { path?: string })?.path;
|
|
135
|
-
if (typeof filePath === "string") {
|
|
136
|
-
recordFileTouched(filePath);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
pi.on("session_shutdown", () => {
|
|
142
|
-
clearDiagnosticState();
|
|
143
|
-
requestRenderFn = null;
|
|
144
|
-
});
|
|
145
|
-
}
|