vscode-pi-companion 0.1.2 → 0.1.4
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/.vscodeignore +0 -2
- package/extensions/index.ts +147 -179
- package/package.json +1 -1
package/.vscodeignore
CHANGED
package/extensions/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PI Companion Extension
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Connects to the VS Code extension via SSE for real-time context updates.
|
|
5
|
-
* No polling, instant updates, no tool calls visible.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
@@ -13,78 +12,11 @@ import * as http from "http";
|
|
|
13
12
|
import { execSync } from "child_process";
|
|
14
13
|
import { Type } from "@sinclair/typebox";
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// Check if running in VS Code integrated terminal
|
|
23
|
-
function isRunningInVSCode(): boolean {
|
|
24
|
-
return process.env.TERM_PROGRAM === 'vscode';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Install VS Code extension from marketplace
|
|
28
|
-
function installFromMarketplace(ctx: ExtensionContext): void {
|
|
29
|
-
try {
|
|
30
|
-
execSync('code --install-extension ravshansbox.vscode-pi-companion --force', { stdio: 'pipe' });
|
|
31
|
-
ctx.ui.notify("PI Companion: VS Code extension installed. Reload window to activate.", "info");
|
|
32
|
-
} catch {
|
|
33
|
-
// Silently ignore
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check if extension is installed
|
|
38
|
-
function isExtensionInstalled(): boolean {
|
|
39
|
-
try {
|
|
40
|
-
const result = execSync('code --list-extensions', { encoding: 'utf8' });
|
|
41
|
-
return result.includes('ravshansbox.vscode-pi-companion');
|
|
42
|
-
} catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Install/update VS Code extension if running in VS Code terminal
|
|
48
|
-
async function installVSCodeExtension(ctx: ExtensionContext): Promise<void> {
|
|
49
|
-
if (!isRunningInVSCode()) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const isInstalled = isExtensionInstalled();
|
|
54
|
-
|
|
55
|
-
if (isInstalled) {
|
|
56
|
-
// Already installed - check for updates, but skip for local package installs
|
|
57
|
-
if (!isLocalPackageInstall()) {
|
|
58
|
-
const latestVersion = await getLatestVersion();
|
|
59
|
-
if (latestVersion) {
|
|
60
|
-
console.log(`[PI Companion] Update available: v${latestVersion}`);
|
|
61
|
-
console.log(`[PI Companion] Run: pi install git:github.com/ravshansbox/vscode-pi-companion`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Not installed - install from marketplace
|
|
68
|
-
installFromMarketplace(ctx);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Get latest version from GitHub
|
|
72
|
-
async function getLatestVersion(): Promise<string | null> {
|
|
73
|
-
try {
|
|
74
|
-
const response = await fetch('https://api.github.com/repos/ravshansbox/vscode-pi-companion/releases/latest', {
|
|
75
|
-
headers: { 'Accept': 'application/vnd.github.v3+json' }
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const release = await response.json() as { tag_name?: string };
|
|
83
|
-
return release.tag_name?.replace('v', '') || null;
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
15
|
+
const CONNECTION_DIR = path.join(os.tmpdir(), "pi-companion");
|
|
16
|
+
const WIDGET_DEBOUNCE_MS = 50;
|
|
17
|
+
const RECONNECT_DELAY_MS = 2000;
|
|
18
|
+
const PORT_VALIDATE_TIMEOUT_MS = 750;
|
|
19
|
+
const MAX_INJECTED_LINES = 100;
|
|
88
20
|
|
|
89
21
|
interface IdeContext {
|
|
90
22
|
workspaceState: {
|
|
@@ -103,6 +35,12 @@ interface IdeContext {
|
|
|
103
35
|
};
|
|
104
36
|
}
|
|
105
37
|
|
|
38
|
+
interface ConnectionInfo {
|
|
39
|
+
port: number;
|
|
40
|
+
workspacePath: string;
|
|
41
|
+
authToken: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
106
44
|
let currentContext: IdeContext | undefined;
|
|
107
45
|
let currentCtx: ExtensionContext | undefined;
|
|
108
46
|
let lastWidgetKey = "";
|
|
@@ -110,34 +48,95 @@ let widgetDebounceTimer: NodeJS.Timeout | undefined;
|
|
|
110
48
|
let reconnectTimer: NodeJS.Timeout | undefined;
|
|
111
49
|
let sseRequest: http.ClientRequest | undefined;
|
|
112
50
|
|
|
113
|
-
function
|
|
114
|
-
|
|
51
|
+
function isRunningInVSCode(): boolean {
|
|
52
|
+
return process.env.TERM_PROGRAM === "vscode";
|
|
53
|
+
}
|
|
115
54
|
|
|
55
|
+
function isExtensionInstalled(): boolean {
|
|
116
56
|
try {
|
|
117
|
-
const
|
|
57
|
+
const result = execSync("code --list-extensions", { encoding: "utf8" });
|
|
58
|
+
return result.includes("ravshansbox.vscode-pi-companion");
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function installFromMarketplace(ctx: ExtensionContext): void {
|
|
65
|
+
try {
|
|
66
|
+
execSync("code --install-extension ravshansbox.vscode-pi-companion --force", { stdio: "pipe" });
|
|
67
|
+
ctx.ui.notify("PI Companion: VS Code extension installed. Reload window to activate.", "info");
|
|
68
|
+
} catch {
|
|
69
|
+
ctx.ui.notify("PI Companion: Failed to auto-install VS Code extension.", "warning");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureVSCodeExtensionInstalled(ctx: ExtensionContext): void {
|
|
74
|
+
if (!isRunningInVSCode()) return;
|
|
75
|
+
if (!isExtensionInstalled()) {
|
|
76
|
+
installFromMarketplace(ctx);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readLatestConnectionInfo(): ConnectionInfo | undefined {
|
|
81
|
+
try {
|
|
82
|
+
const files = fs.readdirSync(CONNECTION_DIR);
|
|
118
83
|
const connectionFiles = files
|
|
119
84
|
.filter((f) => f.startsWith("connection-") && f.endsWith(".json"))
|
|
120
85
|
.map((f) => ({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
mtime: fs.statSync(path.join(configDir, f)).mtime.getTime(),
|
|
86
|
+
path: path.join(CONNECTION_DIR, f),
|
|
87
|
+
mtime: fs.statSync(path.join(CONNECTION_DIR, f)).mtime.getTime(),
|
|
124
88
|
}))
|
|
125
89
|
.sort((a, b) => b.mtime - a.mtime);
|
|
126
90
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
91
|
+
for (const file of connectionFiles) {
|
|
92
|
+
try {
|
|
93
|
+
const content = fs.readFileSync(file.path, "utf-8");
|
|
94
|
+
const info = JSON.parse(content) as ConnectionInfo;
|
|
95
|
+
if (typeof info.port === "number" && info.port > 0) {
|
|
96
|
+
return info;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore malformed files and try the next one.
|
|
100
|
+
}
|
|
131
101
|
}
|
|
132
102
|
} catch {
|
|
133
|
-
// No connection files found
|
|
103
|
+
// No connection files found.
|
|
134
104
|
}
|
|
135
105
|
return undefined;
|
|
136
106
|
}
|
|
137
107
|
|
|
108
|
+
function isPortAlive(port: number): Promise<boolean> {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const req = http.get(
|
|
111
|
+
{
|
|
112
|
+
hostname: "127.0.0.1",
|
|
113
|
+
port,
|
|
114
|
+
path: "/context",
|
|
115
|
+
timeout: PORT_VALIDATE_TIMEOUT_MS,
|
|
116
|
+
},
|
|
117
|
+
(res) => {
|
|
118
|
+
res.resume();
|
|
119
|
+
resolve(res.statusCode === 200);
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
req.on("timeout", () => {
|
|
124
|
+
req.destroy();
|
|
125
|
+
resolve(false);
|
|
126
|
+
});
|
|
127
|
+
req.on("error", () => resolve(false));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function getLivePort(): Promise<number | undefined> {
|
|
132
|
+
const info = readLatestConnectionInfo();
|
|
133
|
+
if (!info) return undefined;
|
|
134
|
+
return (await isPortAlive(info.port)) ? info.port : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
138
137
|
function updateWidget(): void {
|
|
139
138
|
if (!currentCtx) return;
|
|
140
|
-
|
|
139
|
+
|
|
141
140
|
if (!currentContext || !currentContext.workspaceState.openFiles.length) {
|
|
142
141
|
currentCtx.ui.setWidget("pi-companion", ["PI Companion: No files open"]);
|
|
143
142
|
return;
|
|
@@ -145,25 +144,16 @@ function updateWidget(): void {
|
|
|
145
144
|
|
|
146
145
|
const { workspaceState } = currentContext;
|
|
147
146
|
const lines: string[] = [];
|
|
148
|
-
|
|
149
147
|
const activeFile = workspaceState.openFiles.find((f) => f.isActive);
|
|
150
148
|
|
|
151
149
|
if (activeFile) {
|
|
152
150
|
const filename = activeFile.path.split("/").pop() || activeFile.path;
|
|
153
|
-
|
|
154
|
-
// Build location info - ONLY show when text is actually selected
|
|
155
151
|
let location = "";
|
|
156
152
|
if (activeFile.selectionStart && activeFile.selectionEnd) {
|
|
157
153
|
const start = activeFile.selectionStart.line;
|
|
158
154
|
const end = activeFile.selectionEnd.line;
|
|
159
|
-
|
|
160
|
-
location = ` L${start}`;
|
|
161
|
-
} else {
|
|
162
|
-
location = ` L${start}:${end}`;
|
|
163
|
-
}
|
|
155
|
+
location = start === end ? ` L${start}` : ` L${start}:${end}`;
|
|
164
156
|
}
|
|
165
|
-
// Don't show cursor position when nothing is selected
|
|
166
|
-
|
|
167
157
|
lines.push(`📄 ${filename}${location}`);
|
|
168
158
|
}
|
|
169
159
|
|
|
@@ -173,129 +163,105 @@ function updateWidget(): void {
|
|
|
173
163
|
}
|
|
174
164
|
|
|
175
165
|
const widgetKey = lines.join("|");
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (widgetDebounceTimer) {
|
|
179
|
-
clearTimeout(widgetDebounceTimer);
|
|
180
|
-
}
|
|
181
|
-
|
|
166
|
+
if (widgetDebounceTimer) clearTimeout(widgetDebounceTimer);
|
|
167
|
+
|
|
182
168
|
widgetDebounceTimer = setTimeout(() => {
|
|
183
169
|
if (widgetKey !== lastWidgetKey) {
|
|
184
170
|
lastWidgetKey = widgetKey;
|
|
185
171
|
currentCtx!.ui.setWidget("pi-companion", lines);
|
|
186
172
|
}
|
|
187
173
|
widgetDebounceTimer = undefined;
|
|
188
|
-
},
|
|
174
|
+
}, WIDGET_DEBOUNCE_MS);
|
|
189
175
|
}
|
|
190
176
|
|
|
191
|
-
function connectSSE(): void {
|
|
192
|
-
const port =
|
|
177
|
+
async function connectSSE(): Promise<void> {
|
|
178
|
+
const port = await getLivePort();
|
|
193
179
|
if (!port) return;
|
|
194
180
|
|
|
195
|
-
// Close existing connection
|
|
196
181
|
if (sseRequest) {
|
|
197
182
|
sseRequest.destroy();
|
|
198
183
|
sseRequest = undefined;
|
|
199
184
|
}
|
|
200
185
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
for (const line of lines) {
|
|
218
|
-
if (line.startsWith("data: ")) {
|
|
186
|
+
sseRequest = http.get(
|
|
187
|
+
{
|
|
188
|
+
hostname: "127.0.0.1",
|
|
189
|
+
port,
|
|
190
|
+
path: "/context/stream",
|
|
191
|
+
},
|
|
192
|
+
(res) => {
|
|
193
|
+
let buffer = "";
|
|
194
|
+
|
|
195
|
+
res.on("data", (chunk: string) => {
|
|
196
|
+
buffer += chunk;
|
|
197
|
+
const lines = buffer.split("\n");
|
|
198
|
+
buffer = lines.pop() || "";
|
|
199
|
+
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
if (!line.startsWith("data: ")) continue;
|
|
219
202
|
const data = line.slice(6);
|
|
220
|
-
if (data
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Ignore parse errors
|
|
227
|
-
}
|
|
203
|
+
if (!data || data === "[object Object]") continue;
|
|
204
|
+
try {
|
|
205
|
+
currentContext = JSON.parse(data);
|
|
206
|
+
updateWidget();
|
|
207
|
+
} catch {
|
|
208
|
+
// Ignore malformed SSE payloads.
|
|
228
209
|
}
|
|
229
210
|
}
|
|
230
|
-
}
|
|
231
|
-
});
|
|
211
|
+
});
|
|
232
212
|
|
|
233
|
-
|
|
234
|
-
scheduleReconnect
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
res.on("error", () => {
|
|
238
|
-
scheduleReconnect();
|
|
239
|
-
});
|
|
240
|
-
});
|
|
213
|
+
res.on("end", scheduleReconnect);
|
|
214
|
+
res.on("error", scheduleReconnect);
|
|
215
|
+
},
|
|
216
|
+
);
|
|
241
217
|
|
|
242
|
-
sseRequest.on("error",
|
|
243
|
-
scheduleReconnect();
|
|
244
|
-
});
|
|
218
|
+
sseRequest.on("error", scheduleReconnect);
|
|
245
219
|
}
|
|
246
220
|
|
|
247
221
|
function scheduleReconnect(): void {
|
|
248
222
|
if (reconnectTimer || !currentCtx) return;
|
|
249
223
|
reconnectTimer = setTimeout(() => {
|
|
250
224
|
reconnectTimer = undefined;
|
|
251
|
-
connectSSE();
|
|
252
|
-
},
|
|
225
|
+
void connectSSE();
|
|
226
|
+
}, RECONNECT_DELAY_MS);
|
|
253
227
|
}
|
|
254
228
|
|
|
255
229
|
export default function (pi: ExtensionAPI) {
|
|
256
|
-
|
|
257
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
230
|
+
pi.on("before_agent_start", async () => {
|
|
258
231
|
if (!currentContext) return;
|
|
259
|
-
|
|
232
|
+
|
|
260
233
|
const activeFile = currentContext.workspaceState.openFiles.find((f) => f.isActive);
|
|
261
234
|
if (!activeFile) return;
|
|
262
235
|
|
|
263
236
|
let contextText = "";
|
|
264
|
-
|
|
237
|
+
|
|
265
238
|
if (activeFile.selectedText) {
|
|
266
|
-
// Inject selected text
|
|
267
239
|
contextText = `Current selection in ${activeFile.path}:\n\`\`\`\n${activeFile.selectedText}\n\`\`\``;
|
|
268
240
|
} else if (activeFile.content) {
|
|
269
|
-
// Inject full file content for small files, or first 100 lines
|
|
270
241
|
const lines = activeFile.content.split("\n");
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
contextText = `Current file ${activeFile.path} (first 100 lines):\n\`\`\`\n${lines.slice(0, 100).join("\n")}\n\`\`\``;
|
|
275
|
-
}
|
|
242
|
+
contextText = lines.length <= MAX_INJECTED_LINES
|
|
243
|
+
? `Current file ${activeFile.path}:\n\`\`\`\n${activeFile.content}\n\`\`\``
|
|
244
|
+
: `Current file ${activeFile.path} (first ${MAX_INJECTED_LINES} lines):\n\`\`\`\n${lines.slice(0, MAX_INJECTED_LINES).join("\n")}\n\`\`\``;
|
|
276
245
|
}
|
|
277
246
|
|
|
278
|
-
if (contextText)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
247
|
+
if (!contextText) return;
|
|
248
|
+
return {
|
|
249
|
+
message: {
|
|
250
|
+
customType: "pi-companion",
|
|
251
|
+
content: contextText,
|
|
252
|
+
display: false,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
287
255
|
});
|
|
288
256
|
|
|
289
257
|
pi.on("session_start", async (_event, ctx) => {
|
|
290
258
|
currentCtx = ctx;
|
|
259
|
+
ensureVSCodeExtensionInstalled(ctx);
|
|
291
260
|
|
|
292
|
-
|
|
293
|
-
installVSCodeExtension(ctx);
|
|
294
|
-
|
|
295
|
-
const port = getPort();
|
|
261
|
+
const port = await getLivePort();
|
|
296
262
|
if (port) {
|
|
297
263
|
ctx.ui.notify("PI Companion: Connected to VS Code", "info");
|
|
298
|
-
connectSSE();
|
|
264
|
+
await connectSSE();
|
|
299
265
|
} else {
|
|
300
266
|
ctx.ui.notify("PI Companion: VS Code companion server not running. Run 'PI Companion: Start Server' in VS Code.", "warning");
|
|
301
267
|
}
|
|
@@ -312,7 +278,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
312
278
|
}
|
|
313
279
|
});
|
|
314
280
|
|
|
315
|
-
// Register tools
|
|
316
281
|
pi.registerTool({
|
|
317
282
|
name: "get_ide_context",
|
|
318
283
|
label: "Get IDE Context",
|
|
@@ -346,12 +311,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
346
311
|
return { content: [{ type: "text", text: "No active file" }], details: {} };
|
|
347
312
|
}
|
|
348
313
|
return {
|
|
349
|
-
content: [{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
314
|
+
content: [{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: JSON.stringify({
|
|
317
|
+
path: activeFile.path,
|
|
318
|
+
cursor: activeFile.cursor,
|
|
319
|
+
selection: activeFile.selectedText,
|
|
320
|
+
content: activeFile.content,
|
|
321
|
+
}, null, 2),
|
|
322
|
+
}],
|
|
355
323
|
details: {},
|
|
356
324
|
};
|
|
357
325
|
},
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "PI Companion",
|
|
4
4
|
"description": "Share VS Code context with pi coding agent",
|
|
5
5
|
"repository": "https://github.com/ravshansbox/vscode-pi-companion",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.4",
|
|
7
7
|
"publisher": "ravshansbox",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"pi-package"
|