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 +33 -0
- package/README.md +1 -8
- package/index.ts +0 -8
- package/package.json +1 -1
- package/clients/interviewer-templates.ts +0 -90
- package/clients/interviewer.ts +0 -287
- package/clients/safe-spawn-async.ts +0 -220
- package/commands/fix-from-booboo.ts +0 -485
- package/commands/fix-simplified.ts +0 -768
- package/commands/rate.ts +0 -341
- package/commands/refactor.ts +0 -203
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
|
|
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,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>`;
|
package/clients/interviewer.ts
DELETED
|
@@ -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, "&")
|
|
45
|
-
.replace(/</g, "<")
|
|
46
|
-
.replace(/>/g, ">")
|
|
47
|
-
.replace(/"/g, """);
|
|
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
|
-
}
|