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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-vscode-review",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "pi-vscode-review",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/node": "^25.9.2",
|
|
13
|
+
"@types/vscode": "^1.82.0",
|
|
14
|
+
"typescript": "^5.3.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"vscode": "^1.82.0"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"node_modules/@types/node": {
|
|
21
|
+
"version": "25.9.2",
|
|
22
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz",
|
|
23
|
+
"integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==",
|
|
24
|
+
"dev": true,
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"undici-types": ">=7.24.0 <7.24.7"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"node_modules/@types/vscode": {
|
|
31
|
+
"version": "1.120.0",
|
|
32
|
+
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz",
|
|
33
|
+
"integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==",
|
|
34
|
+
"dev": true,
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
},
|
|
37
|
+
"node_modules/typescript": {
|
|
38
|
+
"version": "5.9.3",
|
|
39
|
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
40
|
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
41
|
+
"dev": true,
|
|
42
|
+
"license": "Apache-2.0",
|
|
43
|
+
"bin": {
|
|
44
|
+
"tsc": "bin/tsc",
|
|
45
|
+
"tsserver": "bin/tsserver"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=14.17"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"node_modules/undici-types": {
|
|
52
|
+
"version": "7.24.6",
|
|
53
|
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
|
54
|
+
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
|
55
|
+
"dev": true,
|
|
56
|
+
"license": "MIT"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vscode-pi-sr",
|
|
3
|
+
"displayName": "Pi Companion",
|
|
4
|
+
"description": "Review and approve file changes proposed by Pi agent",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"publisher": "Serhioromano",
|
|
7
|
+
"icon": "icon.png",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": "github:Serhioromano/pi-vscode",
|
|
10
|
+
"engines": {
|
|
11
|
+
"vscode": "^1.82.0"
|
|
12
|
+
},
|
|
13
|
+
"categories": [
|
|
14
|
+
"Other"
|
|
15
|
+
],
|
|
16
|
+
"activationEvents": [
|
|
17
|
+
"onStartupFinished"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/extension.js",
|
|
20
|
+
"contributes": {
|
|
21
|
+
"commands": [
|
|
22
|
+
{
|
|
23
|
+
"command": "pi-sr.approveCurrent",
|
|
24
|
+
"title": "Pi SR: Accept",
|
|
25
|
+
"icon": "$(check)"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"command": "pi-sr.rejectCurrent",
|
|
29
|
+
"title": "Pi SR: Reject",
|
|
30
|
+
"icon": "$(close)"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"menus": {
|
|
34
|
+
"editor/title": [
|
|
35
|
+
{
|
|
36
|
+
"command": "pi-sr.approveCurrent",
|
|
37
|
+
"when": "piSr.isActive",
|
|
38
|
+
"group": "navigation@1"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"command": "pi-sr.rejectCurrent",
|
|
42
|
+
"when": "piSr.isActive",
|
|
43
|
+
"group": "navigation@2"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"compile": "tsc -p tsconfig.json",
|
|
50
|
+
"watch": "tsc -watch -p tsconfig.json",
|
|
51
|
+
"package": "npx @vscode/vsce package"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^25.9.2",
|
|
55
|
+
"@types/vscode": "^1.82.0",
|
|
56
|
+
"@vscode/vsce": "^3.2.0",
|
|
57
|
+
"typescript": "^5.3.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { ReviewRequest, ReviewResult, ReviewResultFile, DiffSession } from './types';
|
|
5
|
+
|
|
6
|
+
// Global state
|
|
7
|
+
let workspaceRoot: string;
|
|
8
|
+
let requestsDir: string;
|
|
9
|
+
let resultsDir: string;
|
|
10
|
+
let watcher: fs.FSWatcher | null = null;
|
|
11
|
+
|
|
12
|
+
// key = tmpFsPath (URI.fsPath of the active editor)
|
|
13
|
+
const sessions = new Map<string, DiffSession>();
|
|
14
|
+
// key = reviewId, value = set of file paths in this review
|
|
15
|
+
const reviewFiles = new Map<string, Set<string>>();
|
|
16
|
+
|
|
17
|
+
export function activate(context: vscode.ExtensionContext) {
|
|
18
|
+
const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
19
|
+
if (!root) {
|
|
20
|
+
vscode.window.showWarningMessage('Pi Companion: open a workspace first');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
workspaceRoot = root;
|
|
24
|
+
requestsDir = path.join(workspaceRoot, '.pi', 'review-requests');
|
|
25
|
+
resultsDir = path.join(workspaceRoot, '.pi', 'review-results');
|
|
26
|
+
|
|
27
|
+
// Create directories
|
|
28
|
+
fs.mkdirSync(requestsDir, { recursive: true });
|
|
29
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
// Watch for new review requests
|
|
32
|
+
watcher = fs.watch(requestsDir, (_, filename) => {
|
|
33
|
+
if (!filename?.endsWith('.json')) return;
|
|
34
|
+
const fp = path.join(requestsDir, filename);
|
|
35
|
+
if (fs.existsSync(fp)) handleRequest(fp);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Recover incomplete reviews
|
|
39
|
+
for (const f of fs.readdirSync(requestsDir)) {
|
|
40
|
+
if (f.endsWith('.json')) handleRequest(path.join(requestsDir, f));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Commands for editor/title buttons
|
|
44
|
+
context.subscriptions.push(
|
|
45
|
+
vscode.commands.registerCommand('pi-sr.approveCurrent', () => approveCurrent()),
|
|
46
|
+
vscode.commands.registerCommand('pi-sr.rejectCurrent', () => rejectCurrent()),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function deactivate() {
|
|
51
|
+
watcher?.close();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Handle new review request ────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function handleRequest(requestPath: string) {
|
|
57
|
+
let req: ReviewRequest;
|
|
58
|
+
try {
|
|
59
|
+
req = JSON.parse(fs.readFileSync(requestPath, 'utf-8'));
|
|
60
|
+
} catch {
|
|
61
|
+
vscode.window.showErrorMessage(`Pi Review: malformed JSON in ${requestPath}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!req.id || !req.files?.length) return;
|
|
66
|
+
|
|
67
|
+
// Skip if this review is already being processed
|
|
68
|
+
if (reviewFiles.has(req.id)) return;
|
|
69
|
+
|
|
70
|
+
const fileSet = new Set<string>();
|
|
71
|
+
reviewFiles.set(req.id, fileSet);
|
|
72
|
+
|
|
73
|
+
// For each file: create tmp, open diff
|
|
74
|
+
req.files.forEach(file => {
|
|
75
|
+
fileSet.add(file.path);
|
|
76
|
+
|
|
77
|
+
const tmpDir = path.join(workspaceRoot, '.pi', 'tmp', req.id);
|
|
78
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
79
|
+
|
|
80
|
+
const tmpPath = path.join(tmpDir, path.basename(file.path));
|
|
81
|
+
fs.writeFileSync(tmpPath, file.proposed, 'utf-8');
|
|
82
|
+
|
|
83
|
+
// Create original file if it doesn't exist
|
|
84
|
+
const origPath = path.join(workspaceRoot, file.path);
|
|
85
|
+
if (!fs.existsSync(origPath)) {
|
|
86
|
+
fs.mkdirSync(path.dirname(origPath), { recursive: true });
|
|
87
|
+
fs.writeFileSync(origPath, file.original || '', 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const session: DiffSession = {
|
|
91
|
+
reviewId: req.id,
|
|
92
|
+
filePath: file.path,
|
|
93
|
+
originalFsPath: origPath,
|
|
94
|
+
tmpFsPath: tmpPath,
|
|
95
|
+
status: 'pending',
|
|
96
|
+
};
|
|
97
|
+
sessions.set(tmpPath, session);
|
|
98
|
+
|
|
99
|
+
// Open diff
|
|
100
|
+
vscode.commands.executeCommand(
|
|
101
|
+
'vscode.diff',
|
|
102
|
+
vscode.Uri.file(origPath),
|
|
103
|
+
vscode.Uri.file(tmpPath),
|
|
104
|
+
`Pi: ${file.path}`
|
|
105
|
+
).then(() => {
|
|
106
|
+
vscode.commands.executeCommand('setContext', 'piSr.isActive', true);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Approve / Reject ─────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
function getCurrentSession(): DiffSession | undefined {
|
|
114
|
+
// Tier 1: active editor (fast path — works when tmp side has focus)
|
|
115
|
+
const active = vscode.window.activeTextEditor;
|
|
116
|
+
if (active) {
|
|
117
|
+
const s = sessions.get(active.document.uri.fsPath);
|
|
118
|
+
if (s) return s;
|
|
119
|
+
}
|
|
120
|
+
// Tier 2: all visible editors (catches original side of diff)
|
|
121
|
+
for (const editor of vscode.window.visibleTextEditors) {
|
|
122
|
+
const s = sessions.get(editor.document.uri.fsPath);
|
|
123
|
+
if (s) return s;
|
|
124
|
+
}
|
|
125
|
+
// Tier 3: if exactly one pending session exists, return it
|
|
126
|
+
// (handles edge case where diff editor sides aren't in visibleTextEditors)
|
|
127
|
+
const pending: DiffSession[] = [];
|
|
128
|
+
for (const s of sessions.values()) {
|
|
129
|
+
if (s.status === 'pending') pending.push(s);
|
|
130
|
+
}
|
|
131
|
+
if (pending.length === 1) return pending[0];
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function approveCurrent() {
|
|
136
|
+
const s = getCurrentSession();
|
|
137
|
+
if (!s) {
|
|
138
|
+
vscode.window.showErrorMessage('Pi Companion: no review session found. Is the diff editor open?');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Read edited content from tmp file
|
|
143
|
+
const edited = fs.readFileSync(s.tmpFsPath, 'utf-8');
|
|
144
|
+
|
|
145
|
+
// Write to original
|
|
146
|
+
fs.writeFileSync(s.originalFsPath, edited, 'utf-8');
|
|
147
|
+
|
|
148
|
+
// Remove tmp
|
|
149
|
+
try { fs.unlinkSync(s.tmpFsPath); } catch {}
|
|
150
|
+
|
|
151
|
+
s.status = 'approved';
|
|
152
|
+
|
|
153
|
+
// Close diff tab
|
|
154
|
+
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
|
155
|
+
|
|
156
|
+
checkReviewComplete(s.reviewId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function rejectCurrent() {
|
|
160
|
+
const s = getCurrentSession();
|
|
161
|
+
if (!s) {
|
|
162
|
+
vscode.window.showErrorMessage('Pi Companion: no review session found. Is the diff editor open?');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Remove tmp
|
|
167
|
+
try { fs.unlinkSync(s.tmpFsPath); } catch {}
|
|
168
|
+
|
|
169
|
+
s.status = 'rejected';
|
|
170
|
+
|
|
171
|
+
// Close diff tab
|
|
172
|
+
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
|
|
173
|
+
|
|
174
|
+
checkReviewComplete(s.reviewId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Complete review ──────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function checkReviewComplete(reviewId: string) {
|
|
180
|
+
// Any pending sessions left for this review?
|
|
181
|
+
for (const s of sessions.values()) {
|
|
182
|
+
if (s.reviewId === reviewId && s.status === 'pending') return; // not done yet
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// All files processed — build result
|
|
186
|
+
const files: ReviewResultFile[] = [];
|
|
187
|
+
const fileSet = reviewFiles.get(reviewId);
|
|
188
|
+
if (!fileSet) return;
|
|
189
|
+
|
|
190
|
+
let allApproved = true;
|
|
191
|
+
let processed = false;
|
|
192
|
+
|
|
193
|
+
for (const fp of fileSet) {
|
|
194
|
+
const session = [...sessions.values()].find(s => s.filePath === fp);
|
|
195
|
+
|
|
196
|
+
// Pending — shouldn't happen (checked above), but guard anyway
|
|
197
|
+
if (session?.status === 'pending') continue;
|
|
198
|
+
|
|
199
|
+
let status: 'approved' | 'rejected';
|
|
200
|
+
let final = '';
|
|
201
|
+
|
|
202
|
+
if (session?.status === 'rejected') {
|
|
203
|
+
status = 'rejected';
|
|
204
|
+
} else if (session?.status === 'approved') {
|
|
205
|
+
status = 'approved';
|
|
206
|
+
final = fs.readFileSync(path.join(workspaceRoot, fp), 'utf-8');
|
|
207
|
+
} else {
|
|
208
|
+
// Session went missing — fallback. Safer to treat as rejected.
|
|
209
|
+
console.error(`[Pi Companion] checkReviewComplete: session not found for ${fp} in review ${reviewId}`);
|
|
210
|
+
status = 'rejected';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
files.push({ path: fp, status, final });
|
|
214
|
+
processed = true;
|
|
215
|
+
|
|
216
|
+
if (status !== 'approved') allApproved = false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Clean up sessions for this review
|
|
220
|
+
for (const [key, s] of sessions) {
|
|
221
|
+
if (s.reviewId === reviewId) sessions.delete(key);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result: ReviewResult = {
|
|
225
|
+
id: reviewId,
|
|
226
|
+
status: !processed ? 'rejected' : allApproved ? 'approved' : 'rejected',
|
|
227
|
+
files,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Write result
|
|
231
|
+
const resultPath = path.join(resultsDir, `${reviewId}.json`);
|
|
232
|
+
fs.writeFileSync(resultPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
233
|
+
|
|
234
|
+
// Remove request file
|
|
235
|
+
const requestPath = path.join(requestsDir, `${reviewId}.json`);
|
|
236
|
+
try { fs.unlinkSync(requestPath); } catch {}
|
|
237
|
+
|
|
238
|
+
// Clean up tmp directory
|
|
239
|
+
const tmpDir = path.join(workspaceRoot, '.pi', 'tmp', reviewId);
|
|
240
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
241
|
+
|
|
242
|
+
// Reset context
|
|
243
|
+
vscode.commands.executeCommand('setContext', 'piSr.isActive', false);
|
|
244
|
+
|
|
245
|
+
reviewFiles.delete(reviewId);
|
|
246
|
+
|
|
247
|
+
vscode.window.showInformationMessage(
|
|
248
|
+
`Pi Companion: ${result.status === 'approved' ? 'accepted' : 'rejected'} (${files.filter(f => f.status === 'approved').length}/${files.length})`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ReviewFile {
|
|
2
|
+
path: string;
|
|
3
|
+
original: string;
|
|
4
|
+
proposed: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
language?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ReviewRequest {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
files: ReviewFile[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type FileStatus = 'pending' | 'approved' | 'rejected';
|
|
16
|
+
|
|
17
|
+
export interface ReviewResultFile {
|
|
18
|
+
path: string;
|
|
19
|
+
status: 'approved' | 'rejected';
|
|
20
|
+
final: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReviewResult {
|
|
24
|
+
id: string;
|
|
25
|
+
status: 'approved' | 'rejected';
|
|
26
|
+
files: ReviewResultFile[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Внутреннее состояние одного файла в ревью */
|
|
30
|
+
export interface DiffSession {
|
|
31
|
+
reviewId: string;
|
|
32
|
+
filePath: string;
|
|
33
|
+
originalFsPath: string; // путь к оригинальному файлу на диске
|
|
34
|
+
tmpFsPath: string; // путь к временному файлу с proposed-контентом
|
|
35
|
+
status: FileStatus;
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|