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.
@@ -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
+ }