@aprovan/patchwork-vscode 0.1.0-dev.03aaf5b
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/.turbo/turbo-build.log +17 -0
- package/LICENSE +373 -0
- package/README.md +31 -0
- package/dist/extension.d.ts +6 -0
- package/dist/extension.js +1405 -0
- package/dist/extension.js.map +1 -0
- package/media/outline.png +0 -0
- package/media/outline.svg +70 -0
- package/media/patchwork.png +0 -0
- package/media/patchwork.svg +72 -0
- package/package.json +144 -0
- package/src/extension.ts +612 -0
- package/src/providers/PatchworkFileSystemProvider.ts +205 -0
- package/src/providers/PatchworkTreeProvider.ts +177 -0
- package/src/providers/PreviewPanelProvider.ts +536 -0
- package/src/services/EditService.ts +24 -0
- package/src/services/EmbeddedStitchery.ts +82 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +11 -0
package/src/extension.ts
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as vscode from "vscode";
|
|
3
|
+
import {
|
|
4
|
+
createProjectFromFiles,
|
|
5
|
+
type VirtualFile,
|
|
6
|
+
type VirtualProject,
|
|
7
|
+
} from "@aprovan/patchwork-compiler";
|
|
8
|
+
import { PatchworkFileSystemProvider } from "./providers/PatchworkFileSystemProvider";
|
|
9
|
+
import { PatchworkTreeProvider } from "./providers/PatchworkTreeProvider";
|
|
10
|
+
import { PreviewPanelProvider } from "./providers/PreviewPanelProvider";
|
|
11
|
+
import { EditService } from "./services/EditService";
|
|
12
|
+
import {
|
|
13
|
+
EmbeddedStitchery,
|
|
14
|
+
type ServiceCallMessage,
|
|
15
|
+
} from "./services/EmbeddedStitchery";
|
|
16
|
+
|
|
17
|
+
export function activate(context: vscode.ExtensionContext) {
|
|
18
|
+
const treeProvider = new PatchworkTreeProvider();
|
|
19
|
+
const fileSystemProvider = new PatchworkFileSystemProvider();
|
|
20
|
+
const diagnostics = vscode.languages.createDiagnosticCollection("patchwork");
|
|
21
|
+
const projectRoots = new Map<string, vscode.Uri>();
|
|
22
|
+
const projects = new Map<string, VirtualProject>();
|
|
23
|
+
const editHistory = new Map<
|
|
24
|
+
string,
|
|
25
|
+
Array<{ prompt: string; summary: string }>
|
|
26
|
+
>();
|
|
27
|
+
const embeddedStitchery = new EmbeddedStitchery();
|
|
28
|
+
const statusBar = vscode.window.createStatusBarItem(
|
|
29
|
+
vscode.StatusBarAlignment.Right,
|
|
30
|
+
);
|
|
31
|
+
statusBar.command = "patchwork.testConnection";
|
|
32
|
+
statusBar.tooltip = "Patchwork: Copilot proxy status";
|
|
33
|
+
statusBar.show();
|
|
34
|
+
const previewProvider = new PreviewPanelProvider(context, {
|
|
35
|
+
onCompileError: (payload, document) => {
|
|
36
|
+
if (!document) return;
|
|
37
|
+
diagnostics.set(document.uri, toDiagnostics(payload, document));
|
|
38
|
+
},
|
|
39
|
+
onCompileSuccess: (document) => {
|
|
40
|
+
if (!document) return;
|
|
41
|
+
diagnostics.delete(document.uri);
|
|
42
|
+
},
|
|
43
|
+
onEditRequest: async (payload, document) => {
|
|
44
|
+
if (!document) return;
|
|
45
|
+
const request = parseEditRequest(payload);
|
|
46
|
+
if (!request) return;
|
|
47
|
+
await runEditRequest(
|
|
48
|
+
request.prompt,
|
|
49
|
+
document,
|
|
50
|
+
previewProvider,
|
|
51
|
+
editHistory,
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
onServiceCall: async (payload) => {
|
|
55
|
+
const call = parseServiceCall(payload);
|
|
56
|
+
if (!call) return;
|
|
57
|
+
const result = await embeddedStitchery.handleServiceCall(call);
|
|
58
|
+
previewProvider.postMessage({ type: "serviceResult", payload: result });
|
|
59
|
+
},
|
|
60
|
+
onWebviewReady: () => {
|
|
61
|
+
previewProvider.postMessage({
|
|
62
|
+
type: "setServices",
|
|
63
|
+
payload: { namespaces: embeddedStitchery.getNamespaces() },
|
|
64
|
+
});
|
|
65
|
+
const doc = vscode.window.activeTextEditor?.document;
|
|
66
|
+
if (doc) {
|
|
67
|
+
previewProvider.postMessage({
|
|
68
|
+
type: "editHistorySet",
|
|
69
|
+
payload: { entries: getHistoryForDoc(editHistory, doc) },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
context.subscriptions.push(
|
|
75
|
+
vscode.window.registerTreeDataProvider("patchworkExplorer", treeProvider),
|
|
76
|
+
);
|
|
77
|
+
context.subscriptions.push(diagnostics);
|
|
78
|
+
context.subscriptions.push(statusBar);
|
|
79
|
+
context.subscriptions.push(
|
|
80
|
+
vscode.workspace.registerFileSystemProvider(
|
|
81
|
+
"patchwork",
|
|
82
|
+
fileSystemProvider,
|
|
83
|
+
{ isCaseSensitive: true },
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
context.subscriptions.push(
|
|
88
|
+
vscode.commands.registerCommand("patchwork.openProject", async () => {
|
|
89
|
+
const selection = await vscode.window.showOpenDialog({
|
|
90
|
+
canSelectFolders: true,
|
|
91
|
+
canSelectFiles: false,
|
|
92
|
+
canSelectMany: false,
|
|
93
|
+
openLabel: "Open Patchwork Project",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const folderUri = selection?.[0];
|
|
97
|
+
if (!folderUri) return;
|
|
98
|
+
|
|
99
|
+
const project = await loadProjectFromFolder(folderUri);
|
|
100
|
+
if (!project) {
|
|
101
|
+
vscode.window.showWarningMessage(
|
|
102
|
+
"Patchwork: no supported files found in the selected folder.",
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
treeProvider.setProject(project.id, project);
|
|
108
|
+
fileSystemProvider.setProject(project.id, project);
|
|
109
|
+
projectRoots.set(project.id, folderUri);
|
|
110
|
+
projects.set(project.id, project);
|
|
111
|
+
|
|
112
|
+
await vscode.commands.executeCommand(
|
|
113
|
+
"patchwork.openFile",
|
|
114
|
+
project.id,
|
|
115
|
+
project.entry,
|
|
116
|
+
);
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
context.subscriptions.push(
|
|
121
|
+
vscode.commands.registerCommand("patchwork.showPreview", async () => {
|
|
122
|
+
const editor = vscode.window.activeTextEditor;
|
|
123
|
+
if (!editor) {
|
|
124
|
+
vscode.window.showInformationMessage(
|
|
125
|
+
"Patchwork: open a file to preview.",
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
previewProvider.showPreview(editor.document);
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
context.subscriptions.push(
|
|
135
|
+
vscode.commands.registerCommand(
|
|
136
|
+
"patchwork.openFile",
|
|
137
|
+
async (projectId: string, filePath: string) => {
|
|
138
|
+
const uri = buildPatchworkUri(projectId, filePath);
|
|
139
|
+
await vscode.commands.executeCommand("vscode.open", uri);
|
|
140
|
+
},
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
context.subscriptions.push(
|
|
145
|
+
vscode.commands.registerCommand("patchwork.editWithAI", async () => {
|
|
146
|
+
const editor = vscode.window.activeTextEditor;
|
|
147
|
+
if (!editor) {
|
|
148
|
+
vscode.window.showInformationMessage("Patchwork: open a file to edit.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const prompt = await vscode.window.showInputBox({
|
|
153
|
+
prompt: "Describe the edit you want",
|
|
154
|
+
});
|
|
155
|
+
if (!prompt) return;
|
|
156
|
+
|
|
157
|
+
await runEditRequest(
|
|
158
|
+
prompt,
|
|
159
|
+
editor.document,
|
|
160
|
+
previewProvider,
|
|
161
|
+
editHistory,
|
|
162
|
+
);
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
context.subscriptions.push(
|
|
167
|
+
vscode.commands.registerCommand("patchwork.showEditHistory", async () => {
|
|
168
|
+
previewProvider.postMessage({ type: "editHistoryToggle" });
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
context.subscriptions.push(
|
|
173
|
+
vscode.commands.registerCommand("patchwork.testConnection", async () => {
|
|
174
|
+
const ok = await updateProxyStatus(statusBar);
|
|
175
|
+
if (ok) {
|
|
176
|
+
vscode.window.showInformationMessage(
|
|
177
|
+
"Patchwork: Copilot proxy is reachable.",
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
vscode.window.showWarningMessage(
|
|
181
|
+
"Patchwork: Copilot proxy is unreachable.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
context.subscriptions.push(
|
|
188
|
+
vscode.commands.registerCommand("patchwork.exportProject", async () => {
|
|
189
|
+
if (projects.size === 0) {
|
|
190
|
+
vscode.window.showInformationMessage(
|
|
191
|
+
"Patchwork: open a project before exporting.",
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const projectId = await pickProjectId(projects);
|
|
197
|
+
if (!projectId) return;
|
|
198
|
+
const project = projects.get(projectId);
|
|
199
|
+
if (!project) return;
|
|
200
|
+
|
|
201
|
+
const target = await vscode.window.showOpenDialog({
|
|
202
|
+
canSelectFolders: true,
|
|
203
|
+
canSelectFiles: false,
|
|
204
|
+
canSelectMany: false,
|
|
205
|
+
openLabel: "Export Patchwork Project",
|
|
206
|
+
});
|
|
207
|
+
const targetDir = target?.[0];
|
|
208
|
+
if (!targetDir) return;
|
|
209
|
+
|
|
210
|
+
await exportProject(project, targetDir);
|
|
211
|
+
vscode.window.showInformationMessage(
|
|
212
|
+
`Patchwork: exported ${project.id}.`,
|
|
213
|
+
);
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
context.subscriptions.push(
|
|
218
|
+
vscode.window.onDidChangeActiveTextEditor((editor) => {
|
|
219
|
+
if (!editor) return;
|
|
220
|
+
previewProvider.updateDocument(editor.document);
|
|
221
|
+
previewProvider.postMessage({
|
|
222
|
+
type: "editHistorySet",
|
|
223
|
+
payload: { entries: getHistoryForDoc(editHistory, editor.document) },
|
|
224
|
+
});
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
context.subscriptions.push(
|
|
229
|
+
vscode.languages.registerCodeActionsProvider(
|
|
230
|
+
["typescriptreact", "javascriptreact"],
|
|
231
|
+
{
|
|
232
|
+
provideCodeActions(document, range) {
|
|
233
|
+
const action = new vscode.CodeAction(
|
|
234
|
+
"Edit with Patchwork AI",
|
|
235
|
+
vscode.CodeActionKind.QuickFix,
|
|
236
|
+
);
|
|
237
|
+
action.command = {
|
|
238
|
+
command: "patchwork.editWithAI",
|
|
239
|
+
title: "Edit with Patchwork AI",
|
|
240
|
+
arguments: [document.uri, range],
|
|
241
|
+
};
|
|
242
|
+
return [action];
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix],
|
|
247
|
+
},
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
context.subscriptions.push(
|
|
252
|
+
vscode.workspace.onDidSaveTextDocument(async (document) => {
|
|
253
|
+
if (document.uri.scheme !== "patchwork") return;
|
|
254
|
+
const parsed = parsePatchworkUri(document.uri);
|
|
255
|
+
if (!parsed) return;
|
|
256
|
+
const root = projectRoots.get(parsed.projectId);
|
|
257
|
+
if (!root) return;
|
|
258
|
+
await writeProjectFile(root, parsed.filePath, document.getText());
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
context.subscriptions.push(
|
|
263
|
+
vscode.workspace.onDidChangeTextDocument((event) => {
|
|
264
|
+
const editor = vscode.window.activeTextEditor;
|
|
265
|
+
if (!editor) return;
|
|
266
|
+
if (event.document !== editor.document) return;
|
|
267
|
+
previewProvider.updateDocument(event.document);
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
context.subscriptions.push(
|
|
272
|
+
vscode.workspace.onDidChangeConfiguration((event) => {
|
|
273
|
+
if (event.affectsConfiguration("patchwork.copilotProxyUrl")) {
|
|
274
|
+
void updateProxyStatus(statusBar);
|
|
275
|
+
}
|
|
276
|
+
if (
|
|
277
|
+
event.affectsConfiguration("patchwork.mcpServers") ||
|
|
278
|
+
event.affectsConfiguration("patchwork.utcpConfig")
|
|
279
|
+
) {
|
|
280
|
+
void initializeEmbeddedStitchery(embeddedStitchery, previewProvider);
|
|
281
|
+
}
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
void updateProxyStatus(statusBar);
|
|
286
|
+
void initializeEmbeddedStitchery(embeddedStitchery, previewProvider);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function deactivate() {}
|
|
290
|
+
|
|
291
|
+
async function loadProjectFromFolder(
|
|
292
|
+
folderUri: vscode.Uri,
|
|
293
|
+
): Promise<VirtualProject | null> {
|
|
294
|
+
const files: VirtualFile[] = [];
|
|
295
|
+
const ignoredDirs = new Set([
|
|
296
|
+
".git",
|
|
297
|
+
"node_modules",
|
|
298
|
+
".turbo",
|
|
299
|
+
"dist",
|
|
300
|
+
"build",
|
|
301
|
+
".next",
|
|
302
|
+
".cache",
|
|
303
|
+
]);
|
|
304
|
+
const ignoredFiles = new Set([".DS_Store"]);
|
|
305
|
+
|
|
306
|
+
const walk = async (dirUri: vscode.Uri): Promise<void> => {
|
|
307
|
+
const entries = await vscode.workspace.fs.readDirectory(dirUri);
|
|
308
|
+
for (const [name, type] of entries) {
|
|
309
|
+
if (type === vscode.FileType.Directory && ignoredDirs.has(name)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (type === vscode.FileType.File && ignoredFiles.has(name)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const entryUri = vscode.Uri.joinPath(dirUri, name);
|
|
317
|
+
if (type === vscode.FileType.Directory) {
|
|
318
|
+
await walk(entryUri);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (type !== vscode.FileType.File) continue;
|
|
322
|
+
|
|
323
|
+
const bytes = await vscode.workspace.fs.readFile(entryUri);
|
|
324
|
+
const hasNull = bytes.some((value) => value === 0);
|
|
325
|
+
const content = hasNull
|
|
326
|
+
? Buffer.from(bytes).toString("base64")
|
|
327
|
+
: Buffer.from(bytes).toString("utf8");
|
|
328
|
+
|
|
329
|
+
const relative = path
|
|
330
|
+
.relative(folderUri.fsPath, entryUri.fsPath)
|
|
331
|
+
.split(path.sep)
|
|
332
|
+
.join("/");
|
|
333
|
+
|
|
334
|
+
files.push({
|
|
335
|
+
path: relative,
|
|
336
|
+
content,
|
|
337
|
+
encoding: hasNull ? "base64" : "utf8",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
await walk(folderUri);
|
|
343
|
+
if (files.length === 0) return null;
|
|
344
|
+
|
|
345
|
+
const projectId = path.basename(folderUri.fsPath);
|
|
346
|
+
return createProjectFromFiles(files, projectId);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildPatchworkUri(projectId: string, filePath: string): vscode.Uri {
|
|
350
|
+
return vscode.Uri.parse(`patchwork://${projectId}/${filePath}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parsePatchworkUri(
|
|
354
|
+
uri: vscode.Uri,
|
|
355
|
+
): { projectId: string; filePath: string } | null {
|
|
356
|
+
if (uri.scheme !== "patchwork") return null;
|
|
357
|
+
const projectId = uri.authority;
|
|
358
|
+
const filePath = uri.path.replace(/^\/+/, "");
|
|
359
|
+
if (!projectId || !filePath) return null;
|
|
360
|
+
return { projectId, filePath };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function writeProjectFile(
|
|
364
|
+
root: vscode.Uri,
|
|
365
|
+
filePath: string,
|
|
366
|
+
content: string,
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
const segments = filePath.split("/");
|
|
369
|
+
const target = vscode.Uri.joinPath(root, ...segments);
|
|
370
|
+
if (segments.length > 1) {
|
|
371
|
+
const dir = vscode.Uri.joinPath(root, ...segments.slice(0, -1));
|
|
372
|
+
await vscode.workspace.fs.createDirectory(dir);
|
|
373
|
+
}
|
|
374
|
+
await vscode.workspace.fs.writeFile(target, Buffer.from(content, "utf8"));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function exportProject(
|
|
378
|
+
project: VirtualProject,
|
|
379
|
+
targetDir: vscode.Uri,
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
for (const [filePath, file] of project.files) {
|
|
382
|
+
const segments = filePath.split("/");
|
|
383
|
+
const target = vscode.Uri.joinPath(targetDir, ...segments);
|
|
384
|
+
if (segments.length > 1) {
|
|
385
|
+
const dir = vscode.Uri.joinPath(targetDir, ...segments.slice(0, -1));
|
|
386
|
+
await vscode.workspace.fs.createDirectory(dir);
|
|
387
|
+
}
|
|
388
|
+
const content =
|
|
389
|
+
file.encoding === "base64"
|
|
390
|
+
? Buffer.from(file.content, "base64")
|
|
391
|
+
: Buffer.from(file.content, "utf8");
|
|
392
|
+
await vscode.workspace.fs.writeFile(target, content);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function pickProjectId(
|
|
397
|
+
projects: Map<string, VirtualProject>,
|
|
398
|
+
): Promise<string | undefined> {
|
|
399
|
+
if (projects.size === 1) {
|
|
400
|
+
return projects.keys().next().value;
|
|
401
|
+
}
|
|
402
|
+
const options = Array.from(projects.keys()).sort();
|
|
403
|
+
return vscode.window.showQuickPick(options, {
|
|
404
|
+
placeHolder: "Select a Patchwork project",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function toDiagnostics(
|
|
409
|
+
payload: unknown,
|
|
410
|
+
document: vscode.TextDocument,
|
|
411
|
+
): vscode.Diagnostic[] {
|
|
412
|
+
const data =
|
|
413
|
+
payload && typeof payload === "object"
|
|
414
|
+
? (payload as Record<string, unknown>)
|
|
415
|
+
: {};
|
|
416
|
+
const message =
|
|
417
|
+
typeof data.message === "string" ? data.message : "Patchwork compile error";
|
|
418
|
+
const line = typeof data.line === "number" ? data.line : 1;
|
|
419
|
+
const column = typeof data.column === "number" ? data.column : 1;
|
|
420
|
+
const position = new vscode.Position(
|
|
421
|
+
clampLine(line - 1, document),
|
|
422
|
+
Math.max(0, column - 1),
|
|
423
|
+
);
|
|
424
|
+
const range = new vscode.Range(position, position);
|
|
425
|
+
const diagnostic = new vscode.Diagnostic(
|
|
426
|
+
range,
|
|
427
|
+
message,
|
|
428
|
+
vscode.DiagnosticSeverity.Error,
|
|
429
|
+
);
|
|
430
|
+
return [diagnostic];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function updateProxyStatus(
|
|
434
|
+
statusBar: vscode.StatusBarItem,
|
|
435
|
+
): Promise<boolean> {
|
|
436
|
+
statusBar.text = "$(sync~spin) Patchwork";
|
|
437
|
+
const config = vscode.workspace.getConfiguration("patchwork");
|
|
438
|
+
const baseUrl = config.get<string>(
|
|
439
|
+
"copilotProxyUrl",
|
|
440
|
+
"http://localhost:3000",
|
|
441
|
+
);
|
|
442
|
+
try {
|
|
443
|
+
const response = await fetch(`${baseUrl}/health`);
|
|
444
|
+
if (response.ok) {
|
|
445
|
+
statusBar.text = "$(plug) Patchwork";
|
|
446
|
+
statusBar.tooltip = "Copilot Proxy: Connected";
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
// Ignore network errors and fall through to disconnected state.
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
statusBar.text = "$(debug-disconnect) Patchwork";
|
|
454
|
+
statusBar.tooltip = "Copilot Proxy: Disconnected";
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function clampLine(line: number, document: vscode.TextDocument): number {
|
|
459
|
+
const maxLine = Math.max(0, document.lineCount - 1);
|
|
460
|
+
return Math.min(Math.max(0, line), maxLine);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function parseEditRequest(payload: unknown): { prompt: string } | null {
|
|
464
|
+
if (!payload || typeof payload !== "object") return null;
|
|
465
|
+
const data = payload as Record<string, unknown>;
|
|
466
|
+
if (typeof data.prompt !== "string" || !data.prompt.trim()) return null;
|
|
467
|
+
return { prompt: data.prompt.trim() };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function parseServiceCall(payload: unknown): ServiceCallMessage | null {
|
|
471
|
+
if (!payload || typeof payload !== "object") return null;
|
|
472
|
+
const data = payload as Record<string, unknown>;
|
|
473
|
+
const id = typeof data.id === "string" ? data.id : null;
|
|
474
|
+
const namespace = typeof data.namespace === "string" ? data.namespace : null;
|
|
475
|
+
const procedure = typeof data.procedure === "string" ? data.procedure : null;
|
|
476
|
+
const args =
|
|
477
|
+
data.args && typeof data.args === "object"
|
|
478
|
+
? (data.args as Record<string, unknown>)
|
|
479
|
+
: {};
|
|
480
|
+
if (!id || !namespace || !procedure) return null;
|
|
481
|
+
return { id, namespace, procedure, args };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function runEditRequest(
|
|
485
|
+
prompt: string,
|
|
486
|
+
document: vscode.TextDocument,
|
|
487
|
+
previewProvider: PreviewPanelProvider,
|
|
488
|
+
historyStore: Map<string, Array<{ prompt: string; summary: string }>>,
|
|
489
|
+
): Promise<void> {
|
|
490
|
+
const history = getHistoryForDoc(historyStore, document);
|
|
491
|
+
const editService = createEditService();
|
|
492
|
+
let combined = "";
|
|
493
|
+
|
|
494
|
+
previewProvider.postMessage({
|
|
495
|
+
type: "editProgress",
|
|
496
|
+
payload: { chunk: "", done: false },
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
for await (const chunk of editService.streamEdit(
|
|
501
|
+
document.getText(),
|
|
502
|
+
prompt,
|
|
503
|
+
)) {
|
|
504
|
+
combined += chunk;
|
|
505
|
+
previewProvider.postMessage({
|
|
506
|
+
type: "editProgress",
|
|
507
|
+
payload: { chunk, done: false },
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const updated = extractEditedCode(combined);
|
|
512
|
+
await applyDocumentEdit(document, updated);
|
|
513
|
+
history.push({ prompt, summary: summarizeEdit(combined) });
|
|
514
|
+
setHistoryForDoc(historyStore, document, history);
|
|
515
|
+
previewProvider.postMessage({
|
|
516
|
+
type: "editHistorySet",
|
|
517
|
+
payload: { entries: history },
|
|
518
|
+
});
|
|
519
|
+
previewProvider.postMessage({
|
|
520
|
+
type: "editProgress",
|
|
521
|
+
payload: { chunk: "", done: true },
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
const message = error instanceof Error ? error.message : "Edit failed";
|
|
525
|
+
previewProvider.postMessage({
|
|
526
|
+
type: "editError",
|
|
527
|
+
payload: { message },
|
|
528
|
+
});
|
|
529
|
+
vscode.window.showErrorMessage(`Patchwork edit failed: ${message}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function createEditService(): EditService {
|
|
534
|
+
const config = vscode.workspace.getConfiguration("patchwork");
|
|
535
|
+
const baseUrl = config.get<string>(
|
|
536
|
+
"copilotProxyUrl",
|
|
537
|
+
"http://localhost:3000",
|
|
538
|
+
);
|
|
539
|
+
return new EditService(baseUrl);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function extractEditedCode(response: string): string {
|
|
543
|
+
const fence = response.match(/```[a-zA-Z0-9]*\n([\s\S]*?)```/);
|
|
544
|
+
if (fence && fence[1]) {
|
|
545
|
+
return fence[1].trimEnd();
|
|
546
|
+
}
|
|
547
|
+
return response.trimEnd();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function summarizeEdit(response: string): string {
|
|
551
|
+
const cleaned = response.replace(/```[\s\S]*?```/g, "").trim();
|
|
552
|
+
if (!cleaned) return "Edit applied.";
|
|
553
|
+
const firstLine = cleaned.split("\n").find((line) => line.trim());
|
|
554
|
+
return (firstLine ?? "Edit applied.").slice(0, 200);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function getHistoryForDoc(
|
|
558
|
+
history: Map<string, Array<{ prompt: string; summary: string }>>,
|
|
559
|
+
document: vscode.TextDocument,
|
|
560
|
+
): Array<{ prompt: string; summary: string }> {
|
|
561
|
+
return history.get(document.uri.toString()) ?? [];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function setHistoryForDoc(
|
|
565
|
+
history: Map<string, Array<{ prompt: string; summary: string }>>,
|
|
566
|
+
document: vscode.TextDocument,
|
|
567
|
+
entries: Array<{ prompt: string; summary: string }>,
|
|
568
|
+
): void {
|
|
569
|
+
history.set(document.uri.toString(), entries);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function applyDocumentEdit(
|
|
573
|
+
document: vscode.TextDocument,
|
|
574
|
+
text: string,
|
|
575
|
+
): Promise<void> {
|
|
576
|
+
const editor = await vscode.window.showTextDocument(document, {
|
|
577
|
+
preserveFocus: true,
|
|
578
|
+
preview: false,
|
|
579
|
+
});
|
|
580
|
+
const fullRange = new vscode.Range(
|
|
581
|
+
document.positionAt(0),
|
|
582
|
+
document.positionAt(document.getText().length),
|
|
583
|
+
);
|
|
584
|
+
await editor.edit((builder) => {
|
|
585
|
+
builder.replace(fullRange, text);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function initializeEmbeddedStitchery(
|
|
590
|
+
embeddedStitchery: EmbeddedStitchery,
|
|
591
|
+
previewProvider: PreviewPanelProvider,
|
|
592
|
+
): Promise<void> {
|
|
593
|
+
const config = vscode.workspace.getConfiguration("patchwork");
|
|
594
|
+
const mcpServers = config.get("mcpServers") as
|
|
595
|
+
| Array<{ name: string; command: string; args?: string[] }>
|
|
596
|
+
| undefined;
|
|
597
|
+
const utcp = config.get("utcpConfig") as Record<string, unknown> | undefined;
|
|
598
|
+
|
|
599
|
+
await embeddedStitchery.initialize({
|
|
600
|
+
mcpServers: (mcpServers ?? []).map((server) => ({
|
|
601
|
+
name: server.name,
|
|
602
|
+
command: server.command,
|
|
603
|
+
args: server.args ?? [],
|
|
604
|
+
})),
|
|
605
|
+
utcp,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
previewProvider.postMessage({
|
|
609
|
+
type: "setServices",
|
|
610
|
+
payload: { namespaces: embeddedStitchery.getNamespaces() },
|
|
611
|
+
});
|
|
612
|
+
}
|