pi-vscode-sr 1.1.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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +302 -0
- package/images/pi-vscode.png +0 -0
- package/package.json +35 -0
- package/src/index.ts +362 -0
- package/tsconfig.json +14 -0
- package/vscode-ext/5856330514355130099_121.jpg +0 -0
- package/vscode-ext/5856330514355130099_121.jpg:Zone.Identifier +0 -0
- package/vscode-ext/dist/extension.js +255 -0
- package/vscode-ext/dist/extension.js.map +1 -0
- package/vscode-ext/dist/types.js +3 -0
- package/vscode-ext/dist/types.js.map +1 -0
- package/vscode-ext/icon.png +0 -0
- package/vscode-ext/package-lock.json +59 -0
- package/vscode-ext/package.json +59 -0
- package/vscode-ext/src/extension.ts +250 -0
- package/vscode-ext/src/types.ts +36 -0
- package/vscode-ext/tsconfig.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serhioromano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Pi VS Code
|
|
2
|
+
|
|
3
|
+
Pi extension that integrates with VS Code's diff editor for reviewing code changes proposed by the Pi agent.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The project has **two components** in one repository:
|
|
8
|
+
|
|
9
|
+
### 1. Pi Extension (root)
|
|
10
|
+
|
|
11
|
+
- **`src/index.ts`** — Extension loaded by Pi agent (`@earendil-works/pi-tui`)
|
|
12
|
+
- Published as npm package `pi-vscode-sr`
|
|
13
|
+
- **Overrides `write` tool** — instead of writing directly, creates a review request in `.pi/review-requests/{uuid}.json`
|
|
14
|
+
- Polls `.pi/review-results/{uuid}.json` for user's decision, then writes if approved
|
|
15
|
+
|
|
16
|
+
### 2. VS Code Extension (`vscode-ext/`) — **Pi Companion**
|
|
17
|
+
|
|
18
|
+
- **`vscode-ext/src/extension.ts`** — VS Code extension (package name: `vscode-pi-sr`) with approve/reject buttons in diff editor
|
|
19
|
+
- Watches `.pi/review-requests/` for new review requests from Pi
|
|
20
|
+
- Opens diff editors with **✓ Approve / ✗ Reject** buttons in the editor title bar
|
|
21
|
+
- Writes results to `.pi/review-results/` for Pi to read
|
|
22
|
+
|
|
23
|
+
## Protocol
|
|
24
|
+
|
|
25
|
+
### Review Request (Pi → VS Code Extension)
|
|
26
|
+
|
|
27
|
+
File: `.pi/review-requests/{uuid}.json`
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
32
|
+
"title": "Add input validation to login",
|
|
33
|
+
"files": [
|
|
34
|
+
{
|
|
35
|
+
"path": "src/auth/login.ts",
|
|
36
|
+
"original": "export function login(email: string, password: string) {\n return api.post('/login', { email, password });\n}",
|
|
37
|
+
"proposed": "export function login(email: string, password: string) {\n if (!email.includes('@')) throw new Error('Invalid email');\n if (password.length < 8) throw new Error('Password too short');\n return api.post('/login', { email, password });\n}",
|
|
38
|
+
"description": "Added email and password validation",
|
|
39
|
+
"language": "typescript"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Review Result (VS Code Extension → Pi)
|
|
46
|
+
|
|
47
|
+
File: `.pi/review-results/{uuid}.json`
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
52
|
+
"status": "approved",
|
|
53
|
+
"files": [
|
|
54
|
+
{
|
|
55
|
+
"path": "src/auth/login.ts",
|
|
56
|
+
"status": "approved",
|
|
57
|
+
"final": "export function login(email: string, password: string) {\n if (!email.includes('@')) throw new Error('Invalid email');\n if (password.length < 8) throw new Error('Password too short');\n return api.post('/login', { email, password });\n}"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### VS Code Extension Dev
|
|
66
|
+
1. Open this project in VS Code
|
|
67
|
+
2. Press **F5** (or run "Run VS Code Extension" in Run & Debug)
|
|
68
|
+
3. A new VS Code window opens with the extension loaded
|
|
69
|
+
4. In that window, create a test request:
|
|
70
|
+
```bash
|
|
71
|
+
mkdir -p .pi/review-requests
|
|
72
|
+
cp /home/sergey/www/pi-vscode/.pi/review-requests/test.json .pi/review-requests/
|
|
73
|
+
```
|
|
74
|
+
5. The extension will open a diff editor with **✓ Approve / ✗ Reject** buttons
|
|
75
|
+
6. Edit the right side, then click Approve or Reject
|
|
76
|
+
|
|
77
|
+
### Pi Extension (npm)
|
|
78
|
+
Run Pi with the extension:
|
|
79
|
+
```bash
|
|
80
|
+
pi -e /path/to/pi-vscode/src/index.ts
|
|
81
|
+
```
|
|
82
|
+
When Pi calls `write`, the extension creates a review request instead of writing directly.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Compile Pi extension (root)
|
|
88
|
+
npm run compile
|
|
89
|
+
|
|
90
|
+
# Compile VS Code extension
|
|
91
|
+
cd vscode-ext && npm run compile
|
|
92
|
+
|
|
93
|
+
# Watch mode (VS Code extension)
|
|
94
|
+
cd vscode-ext && npm run watch
|
|
95
|
+
|
|
96
|
+
# Package VS Code extension as .vsix
|
|
97
|
+
cd vscode-ext && npm run package
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = default_1;
|
|
4
|
+
const pi_coding_agent_1 = require("@earendil-works/pi-coding-agent");
|
|
5
|
+
const typebox_1 = require("typebox");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
// ─── Session state ───────────────────────────────────────────────────
|
|
10
|
+
const sessionReviewIds = new Set();
|
|
11
|
+
const sessionApproveAll = new Set(); // review IDs auto-approved
|
|
12
|
+
async function createReviewAndWait(ctx, filePath, original, proposed, description) {
|
|
13
|
+
const uuid = (0, crypto_1.randomUUID)();
|
|
14
|
+
const requestsDir = (0, path_1.join)(ctx.cwd, ".pi", "review-requests");
|
|
15
|
+
const resultsDir = (0, path_1.join)(ctx.cwd, ".pi", "review-results");
|
|
16
|
+
(0, fs_1.mkdirSync)(requestsDir, { recursive: true });
|
|
17
|
+
(0, fs_1.mkdirSync)(resultsDir, { recursive: true });
|
|
18
|
+
const reviewRequest = {
|
|
19
|
+
id: uuid,
|
|
20
|
+
title: description,
|
|
21
|
+
files: [{ path: filePath, original, proposed, description }],
|
|
22
|
+
};
|
|
23
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(requestsDir, `${uuid}.json`), JSON.stringify(reviewRequest, null, 2), "utf-8");
|
|
24
|
+
ctx.ui.notify(`📝 Review: ${filePath} — check VS Code diff`, "info");
|
|
25
|
+
sessionReviewIds.add(uuid);
|
|
26
|
+
// Check if approve-all was already chosen
|
|
27
|
+
if (sessionApproveAll.size > 0) {
|
|
28
|
+
writeSyncResult(resultsDir, uuid, "approved", proposed);
|
|
29
|
+
return { status: "approved", final: proposed };
|
|
30
|
+
}
|
|
31
|
+
// Phase 1: give VS Code a head start (2s, poll every 100ms) so TUI doesn't
|
|
32
|
+
// pop up when the user is already reviewing in editor.
|
|
33
|
+
// 2 seconds with 100ms intervals = 20 checks — catches VS Code response quickly.
|
|
34
|
+
const resultPath = (0, path_1.join)(resultsDir, `${uuid}.json`);
|
|
35
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
36
|
+
const early = await pollResultFile(resultPath, Date.now() + 2000, 100);
|
|
37
|
+
if (early !== "timeout") {
|
|
38
|
+
if (early === "file-rejected")
|
|
39
|
+
return { status: "rejected" };
|
|
40
|
+
return { status: "approved", final: proposed };
|
|
41
|
+
}
|
|
42
|
+
// Sync check: VS Code may have written the result file between poll intervals.
|
|
43
|
+
if ((0, fs_1.existsSync)(resultPath)) {
|
|
44
|
+
const result = JSON.parse((0, fs_1.readFileSync)(resultPath, "utf-8"));
|
|
45
|
+
if (result.status === "rejected" || result.files?.[0]?.status === "rejected") {
|
|
46
|
+
return { status: "rejected" };
|
|
47
|
+
}
|
|
48
|
+
return { status: "approved", final: proposed };
|
|
49
|
+
}
|
|
50
|
+
// Phase 2: show TUI and keep polling VS Code in parallel (every 500ms)
|
|
51
|
+
const tuiPromise = showTuiSelector(ctx, filePath);
|
|
52
|
+
const pollPromise = pollResultFile(resultPath, deadline, 500);
|
|
53
|
+
const outcome = await Promise.race([tuiPromise, pollPromise]);
|
|
54
|
+
// ── Process outcome (return result, never throw) ──
|
|
55
|
+
if (outcome === "abort") {
|
|
56
|
+
writeSyncResult(resultsDir, uuid, "rejected");
|
|
57
|
+
ctx.abort();
|
|
58
|
+
return { status: "rejected" };
|
|
59
|
+
}
|
|
60
|
+
if (outcome === "file-rejected" || outcome === "rejected") {
|
|
61
|
+
return { status: "rejected" };
|
|
62
|
+
}
|
|
63
|
+
if (outcome === "file-approved" || outcome === "approved") {
|
|
64
|
+
return { status: "approved", final: proposed };
|
|
65
|
+
}
|
|
66
|
+
if (outcome === "approve-all") {
|
|
67
|
+
for (const rid of sessionReviewIds) {
|
|
68
|
+
sessionApproveAll.add(rid);
|
|
69
|
+
}
|
|
70
|
+
writeSyncResult(resultsDir, uuid, "approved", proposed);
|
|
71
|
+
return { status: "approved", final: proposed };
|
|
72
|
+
}
|
|
73
|
+
return { status: "timeout" };
|
|
74
|
+
}
|
|
75
|
+
function writeSyncResult(resultsDir, uuid, status, content) {
|
|
76
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(resultsDir, `${uuid}.json`), JSON.stringify({
|
|
77
|
+
id: uuid,
|
|
78
|
+
files: [{ path: "", status, final: content ?? "" }],
|
|
79
|
+
}, null, 2), "utf-8");
|
|
80
|
+
}
|
|
81
|
+
async function pollResultFile(resultPath, deadline, interval = 500) {
|
|
82
|
+
while (Date.now() < deadline) {
|
|
83
|
+
try {
|
|
84
|
+
if ((0, fs_1.existsSync)(resultPath)) {
|
|
85
|
+
const raw = (0, fs_1.readFileSync)(resultPath, "utf-8");
|
|
86
|
+
if (!raw.trim()) {
|
|
87
|
+
// File exists but is empty — still being written
|
|
88
|
+
await sleep(200);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const result = JSON.parse(raw);
|
|
92
|
+
const fileResult = result.files?.[0];
|
|
93
|
+
if (result.status === "rejected" || fileResult?.status === "rejected") {
|
|
94
|
+
return "file-rejected";
|
|
95
|
+
}
|
|
96
|
+
return "file-approved";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// File may be partially written or malformed — retry
|
|
101
|
+
}
|
|
102
|
+
await sleep(interval);
|
|
103
|
+
}
|
|
104
|
+
return "timeout";
|
|
105
|
+
}
|
|
106
|
+
async function showTuiSelector(ctx, filePath) {
|
|
107
|
+
const choice = await ctx.ui.select(`📝 Review: ${filePath}`, [
|
|
108
|
+
"✅ Approve",
|
|
109
|
+
"❌ Reject",
|
|
110
|
+
"⭐ Approve All for this session",
|
|
111
|
+
"🚪 Abort",
|
|
112
|
+
]);
|
|
113
|
+
if (!choice)
|
|
114
|
+
return "timeout";
|
|
115
|
+
if (choice.startsWith("🚪"))
|
|
116
|
+
return "abort";
|
|
117
|
+
if (choice.startsWith("⭐"))
|
|
118
|
+
return "approve-all";
|
|
119
|
+
if (choice.startsWith("✅"))
|
|
120
|
+
return "approved";
|
|
121
|
+
if (choice.startsWith("❌"))
|
|
122
|
+
return "rejected";
|
|
123
|
+
return "timeout";
|
|
124
|
+
}
|
|
125
|
+
function sleep(ms) {
|
|
126
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
127
|
+
}
|
|
128
|
+
// ─── Apply edits in-memory (mirrors built-in edit logic) ─────────────
|
|
129
|
+
function applyEdits(content, edits) {
|
|
130
|
+
let result = content;
|
|
131
|
+
for (const edit of edits) {
|
|
132
|
+
const idx = result.indexOf(edit.oldText);
|
|
133
|
+
if (idx === -1) {
|
|
134
|
+
throw new Error(`oldText not found in file`);
|
|
135
|
+
}
|
|
136
|
+
const nextIdx = result.indexOf(edit.oldText, idx + 1);
|
|
137
|
+
if (nextIdx !== -1) {
|
|
138
|
+
throw new Error(`oldText is not unique in file`);
|
|
139
|
+
}
|
|
140
|
+
result = result.replace(edit.oldText, edit.newText);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
// ─── Override `write` ────────────────────────────────────────────────
|
|
145
|
+
function registerWriteOverride(pi) {
|
|
146
|
+
pi.registerTool({
|
|
147
|
+
name: "write",
|
|
148
|
+
label: "write (with review)",
|
|
149
|
+
description: "Write content to a file. Instead of writing directly, creates a review request so the user " +
|
|
150
|
+
"can approve or reject the change in VS Code or directly in the terminal. Returns only after " +
|
|
151
|
+
"the user makes a decision.",
|
|
152
|
+
promptSnippet: "Create or overwrite files with user review",
|
|
153
|
+
promptGuidelines: [
|
|
154
|
+
"Use write for any file creation or complete rewrite — user review is required.",
|
|
155
|
+
"The tool blocks until the user approves or rejects.",
|
|
156
|
+
"If content is identical to existing file, no review is created.",
|
|
157
|
+
],
|
|
158
|
+
parameters: typebox_1.Type.Object({
|
|
159
|
+
path: typebox_1.Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
|
160
|
+
content: typebox_1.Type.String({ description: "Content to write to the file" }),
|
|
161
|
+
}),
|
|
162
|
+
executionMode: "sequential",
|
|
163
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
164
|
+
const absolutePath = (0, path_1.resolve)(ctx.cwd, params.path);
|
|
165
|
+
let original = "";
|
|
166
|
+
let fileExists = false;
|
|
167
|
+
try {
|
|
168
|
+
original = (0, fs_1.readFileSync)(absolutePath, "utf-8");
|
|
169
|
+
fileExists = true;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// new file
|
|
173
|
+
}
|
|
174
|
+
if (fileExists && original === params.content) {
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: `No changes — ${params.path} content is identical.` }],
|
|
177
|
+
details: { path: params.path, status: "no-change" },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const description = fileExists ? `Update: ${params.path}` : `Create: ${params.path}`;
|
|
181
|
+
const result = await createReviewAndWait(ctx, params.path, original, params.content, description);
|
|
182
|
+
switch (result.status) {
|
|
183
|
+
case "timeout":
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: `⏰ Review timed out for ${params.path} (10m)` }],
|
|
186
|
+
details: { path: params.path, status: "timeout" },
|
|
187
|
+
};
|
|
188
|
+
case "approved":
|
|
189
|
+
return (0, pi_coding_agent_1.withFileMutationQueue)(absolutePath, async () => {
|
|
190
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(absolutePath), { recursive: true });
|
|
191
|
+
(0, fs_1.writeFileSync)(absolutePath, result.final, "utf-8");
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text", text: `✅ ${params.path} — approved (${result.final.length} bytes)` }],
|
|
194
|
+
details: { path: params.path, status: "approved", bytes: result.final.length },
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
case "rejected":
|
|
198
|
+
return {
|
|
199
|
+
isError: true,
|
|
200
|
+
content: [{ type: "text", text: `❌ ${params.path} — change REJECTED by user. File was NOT modified.` }],
|
|
201
|
+
details: { path: params.path, status: "rejected" },
|
|
202
|
+
};
|
|
203
|
+
default:
|
|
204
|
+
throw new Error(`Unexpected review status: ${result.status}`);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// ─── Override `edit` ─────────────────────────────────────────────────
|
|
210
|
+
function registerEditOverride(pi) {
|
|
211
|
+
pi.registerTool({
|
|
212
|
+
name: "edit",
|
|
213
|
+
label: "edit (with review)",
|
|
214
|
+
description: "Edit a file by replacing exact text passages. Instead of editing directly, creates a review " +
|
|
215
|
+
"request so the user can approve or reject in VS Code or directly in the terminal.",
|
|
216
|
+
promptSnippet: "Make targeted edits to existing files with user review",
|
|
217
|
+
promptGuidelines: [
|
|
218
|
+
"Use edit for targeted changes to existing files — user review is required.",
|
|
219
|
+
"The tool blocks until the user approves or rejects.",
|
|
220
|
+
],
|
|
221
|
+
parameters: typebox_1.Type.Object({
|
|
222
|
+
path: typebox_1.Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
223
|
+
edits: typebox_1.Type.Array(typebox_1.Type.Object({
|
|
224
|
+
oldText: typebox_1.Type.String({ description: "Exact unique text to replace" }),
|
|
225
|
+
newText: typebox_1.Type.String({ description: "Replacement text" }),
|
|
226
|
+
}), {
|
|
227
|
+
description: "Targeted replacements. Each oldText must be unique and non-overlapping.",
|
|
228
|
+
}),
|
|
229
|
+
}),
|
|
230
|
+
executionMode: "sequential",
|
|
231
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
232
|
+
const absolutePath = (0, path_1.resolve)(ctx.cwd, params.path);
|
|
233
|
+
let original;
|
|
234
|
+
try {
|
|
235
|
+
original = (0, fs_1.readFileSync)(absolutePath, "utf-8");
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: `❌ File not found: ${params.path}` }],
|
|
240
|
+
details: { path: params.path, status: "error", error: "not found" },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// Apply edits in-memory to get proposed content
|
|
244
|
+
let proposed;
|
|
245
|
+
try {
|
|
246
|
+
proposed = applyEdits(original, params.edits);
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: "text", text: `❌ Edit failed: ${e.message} in ${params.path}` }],
|
|
251
|
+
details: { path: params.path, status: "error", error: e.message },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (original === proposed) {
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: "text", text: `No changes — ${params.path} content is identical after edits.` }],
|
|
257
|
+
details: { path: params.path, status: "no-change" },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const result = await createReviewAndWait(ctx, params.path, original, proposed, `Edit: ${params.path}`);
|
|
261
|
+
switch (result.status) {
|
|
262
|
+
case "timeout":
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text: `⏰ Review timed out for ${params.path} (10m)` }],
|
|
265
|
+
details: { path: params.path, status: "timeout" },
|
|
266
|
+
};
|
|
267
|
+
case "approved":
|
|
268
|
+
return (0, pi_coding_agent_1.withFileMutationQueue)(absolutePath, async () => {
|
|
269
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(absolutePath), { recursive: true });
|
|
270
|
+
(0, fs_1.writeFileSync)(absolutePath, result.final, "utf-8");
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: "text", text: `✅ ${params.path} — edit approved (${result.final.length} bytes)` }],
|
|
273
|
+
details: { path: params.path, status: "approved", bytes: result.final.length },
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
case "rejected":
|
|
277
|
+
return {
|
|
278
|
+
isError: true,
|
|
279
|
+
content: [{ type: "text", text: `❌ ${params.path} — edit REJECTED by user. File was NOT modified.` }],
|
|
280
|
+
details: { path: params.path, status: "rejected" },
|
|
281
|
+
};
|
|
282
|
+
default:
|
|
283
|
+
throw new Error(`Unexpected review status: ${result.status}`);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// ─── Extension entry point ───────────────────────────────────────────
|
|
289
|
+
function default_1(pi) {
|
|
290
|
+
// Reset review ID tracking on new session
|
|
291
|
+
pi.on("session_start", () => {
|
|
292
|
+
sessionReviewIds.clear();
|
|
293
|
+
});
|
|
294
|
+
// Reset Approve All on message boundaries
|
|
295
|
+
const clearApproveAll = () => {
|
|
296
|
+
sessionApproveAll.clear();
|
|
297
|
+
};
|
|
298
|
+
pi.on("message_start", clearApproveAll);
|
|
299
|
+
pi.on("message_end", clearApproveAll);
|
|
300
|
+
registerWriteOverride(pi);
|
|
301
|
+
registerEditOverride(pi);
|
|
302
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-vscode-sr",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Code diff assistant for VS Code.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Serhioromano",
|
|
7
|
+
"repository": "github:Serhioromano/pi-vscode",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "mkdir -p ~/.pi"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pi",
|
|
13
|
+
"ai-agent",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"pi-package",
|
|
16
|
+
"security",
|
|
17
|
+
"protection",
|
|
18
|
+
"vs-code-extension"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
22
|
+
"yaml": "^2.7.0"
|
|
23
|
+
},
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./src/index.ts"
|
|
27
|
+
],
|
|
28
|
+
"image": "https://raw.githubusercontent.com/Serhioromano/pi-vscode/refs/heads/master/images/pi-vscode.png"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
32
|
+
"@types/node": "^25.7.0",
|
|
33
|
+
"typescript": "^6.0.3"
|
|
34
|
+
}
|
|
35
|
+
}
|