pi-lens 3.6.5 → 3.6.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [3.6.3] - 2026-04-03
6
+
7
+ ### Removed (Dead Code Cleanup)
8
+ - **Deleted unused interviewer tool** — Browser-based interview with diff confirmation was never used:
9
+ - Removed `clients/interviewer.ts` (290 lines)
10
+ - Removed `clients/interviewer-templates.ts` (240 lines)
11
+ - Removed initialization from `index.ts`
12
+
13
+ - **Deleted deprecated commands** — All were superseded by `/lens-booboo`:
14
+ - `/lens-booboo-fix` command (fix-from-booboo.ts, 430 lines) — showed warning to use `/lens-booboo`
15
+ - `/lens-fix-simplified` command (fix-simplified.ts, 770 lines) — never registered, unused
16
+ - `/lens-rate` command (rate.ts, 340 lines) — showed warning to use `/lens-booboo`
17
+ - `/lens-booboo-refactor` command (refactor.ts, 207 lines) — depended on removed interviewer tool
18
+
19
+ - **Deleted duplicate safe-spawn module**:
20
+ - Removed `clients/safe-spawn-async.ts` (220 lines) — 100% duplicate of functions in `safe-spawn.ts`
21
+ - All imports already used `safe-spawn.ts`, making `safe-spawn-async.ts` pure dead code
22
+
23
+ ### Test Suite Overhaul
24
+ - **Removed ~85 wasteful/broken test files**:
25
+ - "Is tool available" tests (8 files) — just checked if external CLIs installed
26
+ - Heavy integration tests (2 files) — 5s timeouts, full codebase scans
27
+ - Broken LSP tests (7 files) — import path errors
28
+ - Broken runner tests (7 files) — thin CLI wrappers with wrong imports
29
+ - Trivial utility tests (5 files) — file extension parsing, string sanitization
30
+
31
+ - **Added meaningful integration tests**:
32
+ - `tests/clients/dispatch/dispatcher-flow.test.ts` — Runner registration, execution, delta mode, conditional runners
33
+ - `tests/extension-hooks.test.ts` — pi API: tool/command/flag registration, event handlers
34
+ - `tests/mocks/runner-factory.ts` — Mock runners for testing without real CLI tools
35
+
36
+ - **Results:** 22 tests passing in 1.2s (was 104 tests in ~18s with 48 failures)
37
+
5
38
  ## [3.6.2] - 2026-04-02
6
39
 
7
40
  ### Added
package/README.md CHANGED
@@ -128,7 +128,7 @@ Enable full Language Server Protocol support with `--lens-lsp`:
128
128
  | **Config** | YAML, JSON, Prisma |
129
129
  | **Web** | Vue, Svelte, CSS/SCSS/Sass/Less |
130
130
 
131
- **Auto-installation (8 tools):** TypeScript, Python, Biome, Ruff, and analysis tools (Madge, jscpd, ast-grep, Knip) auto-install on first use to `.pi-lens/tools/`. Other LSP servers are launched via `npx` when available or require manual installation.
131
+ **Auto-installation (8 tools):** TypeScript, Python, Biome, Ruff, and analysis tools (Madge, jscpd, ast-grep, Knip) auto-install on first use to `.pi-lens/tools/`. Other LSP servers require manual installation or are launched via `npx` when available.
132
132
 
133
133
  **Usage:**
134
134
  ```bash
@@ -575,13 +575,6 @@ pattern: "TODO" // Use grep instead
575
575
 
576
576
  See [CHANGELOG.md](CHANGELOG.md) for full history.
577
577
 
578
- ### Latest Highlights
579
-
580
- - **Tree-sitter Query Cache:** Compiled query cache with mtime-based invalidation — 10× faster structural analysis startup
581
- - **LSP Support:** 31 Language Server Protocol clients (4 core auto-installed, others via npx or manual)
582
- - **NAPI Runner:** 100x faster TypeScript/JavaScript structural analysis (~9ms vs ~1200ms) — security rules fire inline
583
- - **Slop Detection:** 33+ TypeScript and 40+ Python patterns for AI-generated code quality issues
584
-
585
578
  ---
586
579
 
587
580
  ## License
package/index.ts CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  } from "./clients/format-service.js";
23
23
  import { GoClient } from "./clients/go-client.js";
24
24
  import { ensureTool } from "./clients/installer/index.js";
25
- import { buildInterviewer } from "./clients/interviewer.js";
26
25
  import { JscpdClient } from "./clients/jscpd-client.js";
27
26
  import { KnipClient } from "./clients/knip-client.js";
28
27
  // RELOAD TEST 6: Cache verification run
@@ -50,7 +49,6 @@ import { TodoScanner } from "./clients/todo-scanner.js";
50
49
  import { TypeCoverageClient } from "./clients/type-coverage-client.js";
51
50
  import { TypeScriptClient } from "./clients/typescript-client.js";
52
51
  import { handleBooboo } from "./commands/booboo.js";
53
- import { initRefactorLoop } from "./commands/refactor.js";
54
52
 
55
53
  /** Parse a diff to extract modified line ranges in the new file.
56
54
  * Handles pi's custom diff format:
@@ -185,9 +183,6 @@ export default function (pi: ExtensionAPI) {
185
183
  const agentBehaviorClient = new AgentBehaviorClient();
186
184
  const cacheManager = new CacheManager();
187
185
 
188
- // --- Initialize auto-loops ---
189
- initRefactorLoop(pi);
190
-
191
186
  // --- Flags ---
192
187
 
193
188
  pi.registerFlag("lens-verbose", {
@@ -403,9 +398,6 @@ export default function (pi: ExtensionAPI) {
403
398
  "yaml",
404
399
  ] as const;
405
400
 
406
- // --- Interviewer tool (browser-based interview with diff confirmation) ---
407
- buildInterviewer(pi, dbg);
408
-
409
401
  pi.registerTool({
410
402
  name: "ast_grep_search",
411
403
  label: "AST Search",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.6.5",
3
+ "version": "3.6.6",
4
4
  "type": "module",
5
5
  "description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
@@ -1,90 +0,0 @@
1
- export const CONFIRMATION_HTML = (
2
- question: string,
3
- plan: string,
4
- diff: string,
5
- esc: (s: string) => string,
6
- mdToHtml: (md: string) => string,
7
- diffHtml: string,
8
- addCount: number,
9
- delCount: number,
10
- ): string => `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
11
- <title>🏗️ Changes Applied</title>
12
- <style>
13
- *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:960px;margin:0 auto;line-height:1.5}
14
- h2{font-size:16px;color:#58a6ff;margin-bottom:14px}
15
- .plan{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 18px;margin-bottom:18px;font-size:13px;line-height:1.6}
16
- .plan h3{color:#f0f6fc;font-size:14px;margin:10px 0 4px}.plan h4{color:#c9d1d9;font-size:13px;margin:8px 0 3px}
17
- .plan li{margin:2px 0 2px 16px;list-style:disc}.plan code{background:#21262d;padding:1px 5px;border-radius:3px;font-size:12px}
18
- .diff-wrap{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:18px;overflow:hidden}
19
- .diff-hdr{padding:7px 14px;font-size:11px;color:#8b949e;border-bottom:1px solid #30363d;font-family:monospace;display:flex;justify-content:space-between}
20
- .diff-stats{display:flex;gap:10px}.stat-add{color:#3fb950}.stat-del{color:#ff7b72}
21
- .diff-pre{padding:12px;font-family:'Fira Code',Consolas,monospace;font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre;margin:0}
22
- .da{color:#3fb950;display:block}.dd{color:#ff7b72;display:block}.dh{color:#79c0ff;display:block}.df{color:#8b949e;display:block}.dc{color:#e6edf3;display:block}
23
- .actions{display:flex;gap:10px;flex-wrap:wrap}
24
- .btn-c{background:#238636;color:#fff;border:1px solid #2ea043;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer}.btn-c:hover{background:#2ea043}
25
- .btn-chat{background:#1a2332;color:#79c0ff;border:1px solid #1f6feb;padding:10px 24px;border-radius:6px;font-size:14px;cursor:pointer}.btn-chat:hover{background:#1f3050}
26
- .chat-area{display:none;margin-top:12px}
27
- textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
28
- textarea:focus{border-color:#58a6ff}
29
- .hint{color:#6e7681;font-size:12px;margin-top:10px}
30
- </style></head><body>
31
- <h2>${esc(question)}</h2>
32
- <div class="plan"><p>${mdToHtml(plan)}</p></div>
33
- ${diff ? `<div class="diff-wrap"><div class="diff-hdr"><span>Changes</span><div class="diff-stats"><span class="stat-add">+${addCount}</span><span class="stat-del">−${delCount}</span></div></div><pre class="diff-pre">${diffHtml}</pre></div>` : ""}
34
- <form method="POST" id="f">
35
- <input type="hidden" name="choice" id="c" value="Looks good">
36
- <div class="actions">
37
- <button class="btn-c" type="submit">✅ Looks good — move to next offender</button>
38
- <button class="btn-chat" type="button" onclick="toggleChat()">💬 Request changes</button>
39
- </div>
40
- <div class="chat-area" id="ca"><textarea name="freeText" placeholder="Describe what you'd like changed..."></textarea>
41
- <div style="margin-top:8px"><button class="btn-c" type="submit" onclick="document.getElementById('c').value='Redo'">Submit</button></div></div>
42
- </form>
43
- <p class="hint">Tab closes after submit · Ctrl+Enter to confirm</p>
44
- <script>
45
- function toggleChat(){const r=document.getElementById('ca');r.style.display=r.style.display==='none'?'block':'none';if(r.style.display==='block')r.querySelector('textarea').focus();}
46
- document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){document.getElementById('f').submit();}});
47
- </script>
48
- </body></html>`;
49
-
50
- export const INTERVIEW_HTML = (
51
- question: string,
52
- optionsHtml: string,
53
- hasFreeText: boolean,
54
- esc: (s: string) => string,
55
- ): string => `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
56
- <title>🏗️ Decision</title>
57
- <style>
58
- *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:880px;margin:0 auto;line-height:1.5}
59
- .question{font-size:15px;font-weight:600;color:#f0f6fc;margin-bottom:12px}
60
- .opts{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
61
- .card{border:1px solid #30363d;border-radius:8px;padding:11px 14px;cursor:pointer;transition:border-color .12s,background .12s;display:flex;align-items:flex-start;gap:10px}
62
- .card:hover,.card.selected{border-color:#58a6ff;background:#0d1f30}.card.rec{border-color:#1f6feb}
63
- .card input{margin-top:3px;accent-color:#58a6ff;flex-shrink:0}.card-body{flex:1}
64
- .card-top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
65
- .num{color:#6e7681;font-size:13px;min-width:18px}.lbl{font-size:13.5px;font-weight:500}
66
- .badge-rec{background:#1f4e2e;color:#3fb950;font-size:10px;padding:1px 7px;border-radius:10px;margin-left:4px;font-weight:600}
67
- .ctx{color:#8b949e;font-size:12px;margin-top:3px;padding-left:22px}
68
- .impact{display:flex;gap:6px;margin-top:5px;padding-left:22px;flex-wrap:wrap}
69
- .ib{font-size:11px;padding:2px 8px;border-radius:10px;font-family:monospace;font-weight:600}
70
- .ib.up{background:#1a3a2a;color:#3fb950;border:1px solid #238636}
71
- .ib.dn{background:#3a1a1a;color:#ff7b72;border:1px solid #f85149}
72
- .ib.proj{background:#1a2a3a;color:#79c0ff;border:1px solid #1f6feb}
73
- .free-area{display:none;margin-top:10px;padding-left:22px}
74
- textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
75
- textarea:focus{border-color:#58a6ff}
76
- .submit-row{display:flex;align-items:center;gap:12px;margin-top:4px}
77
- button{background:#238636;color:#fff;border:1px solid #2ea043;padding:9px 22px;border-radius:6px;font-size:13.5px;font-weight:600;cursor:pointer;transition:background .12s}
78
- button:hover{background:#2ea043}.hint{color:#6e7681;font-size:12px}
79
- </style></head><body>
80
- <div class="question">${esc(question)}</div>
81
- <form method="POST" id="f">
82
- <div class="opts">${optionsHtml}</div>
83
- ${hasFreeText ? '<div class="free-area" id="fa"><textarea name="freeText" placeholder="Describe your preferred approach..."></textarea></div>' : ""}
84
- <div class="submit-row"><button type="submit">Submit</button><span class="hint">Ctrl+Enter</span></div>
85
- </form>
86
- <script>
87
- const cards=document.querySelectorAll('.card');function sel(c){cards.forEach(x=>{x.classList.remove('selected');x.querySelector('input').checked=false});c.classList.add('selected');c.querySelector('input').checked=true;const fa=document.getElementById('fa');if(fa)fa.style.display=c.querySelector('input').value==='__free__'?'block':'none';}
88
- cards.forEach(c=>c.addEventListener('click',()=>sel(c)));const rec=document.querySelector('.card.rec');if(rec)sel(rec);else if(cards.length)sel(cards[0]);
89
- document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')document.getElementById('f').submit();});
90
- </script></body></html>`;
@@ -1,287 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import * as http from "node:http";
3
- import * as net from "node:net";
4
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
- import { Type } from "@sinclair/typebox";
6
- import { CONFIRMATION_HTML, INTERVIEW_HTML } from "./interviewer-templates.js";
7
-
8
- export type InterviewOption = {
9
- value: string;
10
- label: string;
11
- context?: string;
12
- recommended?: boolean;
13
- impact?: {
14
- linesReduced?: number;
15
- miProjection?: string;
16
- cognitiveProjection?: string;
17
- };
18
- };
19
-
20
- export function buildInterviewer(
21
- pi: ExtensionAPI,
22
- _dbg: (msg: string) => void,
23
- ): (
24
- question: string,
25
- options: InterviewOption[],
26
- timeoutSeconds: number,
27
- plan?: string,
28
- diff?: string,
29
- confirmationMode?: boolean,
30
- ) => Promise<string | null> {
31
- let interviewHandler:
32
- | ((
33
- question: string,
34
- options: InterviewOption[],
35
- timeoutSeconds: number,
36
- plan?: string,
37
- diff?: string,
38
- confirmationMode?: boolean,
39
- ) => Promise<string | null>)
40
- | null = null;
41
-
42
- const esc = (s: string) =>
43
- s
44
- .replace(/&/g, "&amp;")
45
- .replace(/</g, "&lt;")
46
- .replace(/>/g, "&gt;")
47
- .replace(/"/g, "&quot;");
48
-
49
- const mdToHtml = (md: string) =>
50
- md
51
- .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
52
- .replace(/`([^`]+)`/g, "<code>$1</code>")
53
- .replace(/^### (.+)/gm, "<h4>$1</h4>")
54
- .replace(/^## (.+)/gm, "<h3>$1</h3>")
55
- .replace(/^# (.+)/gm, "<h2>$1</h2>")
56
- .replace(/^- (.+)/gm, "<li>$1</li>")
57
- .replace(/\n\n/g, "</p><p>");
58
-
59
- const confirmationHTML = (
60
- question: string,
61
- plan: string,
62
- diff: string,
63
- ): string => {
64
- const diffLines = diff.split("\n");
65
- const diffHtml = diffLines
66
- .map((line) => {
67
- if (line.startsWith("+++") || line.startsWith("---"))
68
- return `<span class="df">${esc(line)}</span>`;
69
- if (line.startsWith("@@"))
70
- return `<span class="dh">${esc(line)}</span>`;
71
- if (line.startsWith("+")) return `<span class="da">${esc(line)}</span>`;
72
- if (line.startsWith("-")) return `<span class="dd">${esc(line)}</span>`;
73
- return `<span class="dc">${esc(line)}</span>`;
74
- })
75
- .join("\n");
76
- const addCount = (diff.match(/^\+/gm) || []).length;
77
- const delCount =
78
- (diff.match(/^-/gm) || []).length - (diff.match(/^---/gm) || []).length;
79
-
80
- return CONFIRMATION_HTML(
81
- question,
82
- plan,
83
- diff,
84
- esc,
85
- mdToHtml,
86
- diffHtml,
87
- addCount,
88
- delCount,
89
- );
90
- };
91
-
92
- const interviewHTML = (
93
- question: string,
94
- options: InterviewOption[],
95
- _timeoutSeconds: number,
96
- _plan?: string,
97
- _diff?: string,
98
- _confirmationMode?: boolean,
99
- ): string => {
100
- if (_confirmationMode && _plan && _diff)
101
- return confirmationHTML(question, _plan, _diff);
102
-
103
- const optionsHtml = options
104
- .map((opt, idx) => {
105
- const impactBadge = (val: number, label: string, good: boolean) =>
106
- `<span class="ib ${good ? "up" : "dn"}">${val > 0 ? "+" : ""}${val} ${label}</span>`;
107
- let impactHtml = "";
108
- if (opt.impact) {
109
- const parts: string[] = [];
110
- if (opt.impact.linesReduced !== undefined)
111
- parts.push(impactBadge(opt.impact.linesReduced, "lines", true));
112
- if (opt.impact.miProjection)
113
- parts.push(
114
- `<span class="ib proj">MI ${opt.impact.miProjection}</span>`,
115
- );
116
- if (opt.impact.cognitiveProjection)
117
- parts.push(
118
- `<span class="ib proj">Cognitive ${opt.impact.cognitiveProjection}</span>`,
119
- );
120
- if (parts.length)
121
- impactHtml = `<div class="impact">${parts.join("")}</div>`;
122
- }
123
- return `<label class="card${opt.recommended ? " rec" : ""}"><input type="radio" name="choice" value="${esc(opt.value)}"${opt.recommended ? " checked" : ""}><div class="card-body"><div class="card-top"><span class="num">${idx + 1}.</span><span class="lbl">${esc(opt.label)}</span>${opt.recommended ? '<span class="badge-rec">Recommended</span>' : ""}</div>${impactHtml}${opt.context ? `<div class="ctx">${esc(opt.context)}</div>` : ""}</div></label>`;
124
- })
125
- .join("\n");
126
- const hasFreeText = options.some((o) => o.value === "__free__");
127
-
128
- return INTERVIEW_HTML(question, optionsHtml, hasFreeText, esc);
129
- };
130
-
131
- const openBrowserInterview = (
132
- html: string,
133
- timeoutSeconds: number,
134
- ): Promise<string | null> => {
135
- return new Promise((resolve) => {
136
- const getPort = (cb: (port: number) => void) => {
137
- const s = net.createServer();
138
- s.listen(0, () => {
139
- const p = (s.address() as net.AddressInfo).port;
140
- s.close(() => cb(p));
141
- });
142
- s.on("error", () => cb(-1));
143
- };
144
- getPort((port) => {
145
- if (port < 0) {
146
- resolve(null);
147
- return;
148
- }
149
- const server = http.createServer((req, res) => {
150
- if (req.method === "GET") {
151
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
152
- res.end(html);
153
- } else if (req.method === "POST") {
154
- let body = "";
155
- req.on("data", (c: Buffer) => {
156
- body += c.toString();
157
- });
158
- req.on("end", () => {
159
- const p = new URLSearchParams(body);
160
- const choice = p.get("choice") ?? "";
161
- const freeText = p.get("freeText") ?? "";
162
- const final =
163
- choice === "__free__" || choice === "Redo"
164
- ? freeText.trim()
165
- : choice;
166
- res.writeHead(200, {
167
- "Content-Type": "text/html; charset=utf-8",
168
- });
169
- res.end(
170
- `<!DOCTYPE html><html><head><meta charset='UTF-8'><style>body{font-family:system-ui;background:#0d1117;color:#e6edf3;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center}.fade{transition:opacity 0.5s}</style></head><body><div class="fade"><h2>✅ Response received</h2><p style='color:#8b949e;margin-top:8px'>Closing tab...</p><p id="count" style='color:#58a6ff;margin-top:4px'></p></div><script>let s=3;const el=document.getElementById('count');const tick=()=>{el.textContent=s+'s';if(s<=0){window.close();}else{s--;setTimeout(tick,1000);}};tick();</script></body></html>`,
171
- );
172
- clearTimeout(timer);
173
- server.close();
174
- resolve(final || null);
175
- });
176
- }
177
- });
178
- server.listen(port);
179
- const url = `http://localhost:${port}`;
180
- if (process.platform === "win32")
181
- spawnSync("cmd", ["/c", "start", "", url], { shell: false });
182
- else if (process.platform === "darwin") spawnSync("open", [url]);
183
- else spawnSync("xdg-open", [url]);
184
- const timer = setTimeout(() => {
185
- server.close();
186
- resolve(null);
187
- }, timeoutSeconds * 1000);
188
- });
189
- });
190
- };
191
-
192
- interviewHandler = (
193
- question,
194
- options,
195
- timeoutSeconds,
196
- plan,
197
- diff,
198
- confirmationMode,
199
- ) =>
200
- openBrowserInterview(
201
- interviewHTML(
202
- question,
203
- options,
204
- timeoutSeconds,
205
- plan,
206
- diff,
207
- confirmationMode,
208
- ),
209
- timeoutSeconds,
210
- );
211
-
212
- pi.registerTool({
213
- name: "interviewer",
214
- label: "Interview",
215
- description:
216
- "Present a multiple-choice interview to the user via browser form. Use this when you need the user to make a decision with options. Returns their choice or null on timeout. Supports confirmation mode with plan+diff display.",
217
- parameters: Type.Object({
218
- question: Type.String({
219
- description: "The question to present to the user",
220
- }),
221
- options: Type.Optional(
222
- Type.Array(
223
- Type.Object({
224
- value: Type.String(),
225
- label: Type.String(),
226
- context: Type.Optional(Type.String()),
227
- recommended: Type.Optional(Type.Boolean()),
228
- impact: Type.Optional(
229
- Type.Object({
230
- linesReduced: Type.Optional(Type.Number()),
231
- miProjection: Type.Optional(Type.String()),
232
- cognitiveProjection: Type.Optional(Type.String()),
233
- }),
234
- ),
235
- }),
236
- ),
237
- ),
238
- plan: Type.Optional(
239
- Type.String({
240
- description:
241
- "Refactoring plan (markdown) — shows in confirmation mode",
242
- }),
243
- ),
244
- diff: Type.Optional(
245
- Type.String({
246
- description: "Unified diff text — shows in confirmation mode",
247
- }),
248
- ),
249
- confirmationMode: Type.Optional(
250
- Type.Boolean({ description: "Show plan+diff confirmation screen" }),
251
- ),
252
- timeoutSeconds: Type.Optional(
253
- Type.Number({
254
- description: "Auto-close after this many seconds (default 600)",
255
- }),
256
- ),
257
- }),
258
- async execute(_toolCallId, input, _signal, _onUpdate, _ctx) {
259
- if (!interviewHandler)
260
- return {
261
- content: [
262
- { type: "text" as const, text: "Interview tool not initialized" },
263
- ],
264
- details: null,
265
- };
266
- const result = await interviewHandler(
267
- input.question,
268
- input.options ?? [],
269
- input.timeoutSeconds ?? 600,
270
- input.plan,
271
- input.diff,
272
- input.confirmationMode,
273
- );
274
- return {
275
- content: [
276
- {
277
- type: "text" as const,
278
- text: result ?? "No response (timed out or dismissed)",
279
- },
280
- ],
281
- details: result ?? null,
282
- };
283
- },
284
- });
285
-
286
- return interviewHandler;
287
- }