@aprovan/patchwork-vscode 0.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,536 @@
1
+ import * as path from "path";
2
+ import * as vscode from "vscode";
3
+
4
+ interface PreviewMessage {
5
+ type: string;
6
+ payload?: unknown;
7
+ }
8
+
9
+ export interface PreviewPanelHandlers {
10
+ onCompileError?: (payload: unknown, document?: vscode.TextDocument) => void;
11
+ onCompileSuccess?: (document?: vscode.TextDocument) => void;
12
+ onEditRequest?: (payload: unknown, document?: vscode.TextDocument) => void;
13
+ onServiceCall?: (payload: unknown) => void;
14
+ onWebviewReady?: () => void;
15
+ }
16
+
17
+ export class PreviewPanelProvider {
18
+ private panel: vscode.WebviewPanel | undefined;
19
+ private activeDocument: vscode.TextDocument | undefined;
20
+
21
+ constructor(
22
+ private readonly context: vscode.ExtensionContext,
23
+ private readonly handlers: PreviewPanelHandlers = {},
24
+ ) {}
25
+
26
+ showPreview(document: vscode.TextDocument): void {
27
+ if (!this.isPreviewableDocument(document)) {
28
+ vscode.window.showInformationMessage(
29
+ "Patchwork: preview supports .tsx/.jsx files only.",
30
+ );
31
+ return;
32
+ }
33
+ this.activeDocument = document;
34
+
35
+ if (this.panel) {
36
+ this.panel.reveal(vscode.ViewColumn.Beside);
37
+ this.postActiveDocument();
38
+ return;
39
+ }
40
+
41
+ this.panel = vscode.window.createWebviewPanel(
42
+ "patchworkPreview",
43
+ "Patchwork Preview",
44
+ vscode.ViewColumn.Beside,
45
+ {
46
+ enableScripts: true,
47
+ retainContextWhenHidden: true,
48
+ },
49
+ );
50
+
51
+ this.panel.webview.html = this.getWebviewHtml(this.panel.webview);
52
+ this.panel.webview.onDidReceiveMessage(
53
+ (message: PreviewMessage) => {
54
+ if (message?.type === "ready") {
55
+ this.postActiveDocument();
56
+ this.handlers.onWebviewReady?.();
57
+ return;
58
+ }
59
+
60
+ if (message?.type === "compileError") {
61
+ this.handlers.onCompileError?.(message.payload, this.activeDocument);
62
+ return;
63
+ }
64
+
65
+ if (message?.type === "compileSuccess") {
66
+ this.handlers.onCompileSuccess?.(this.activeDocument);
67
+ return;
68
+ }
69
+
70
+ if (message?.type === "editRequest") {
71
+ this.handlers.onEditRequest?.(message.payload, this.activeDocument);
72
+ return;
73
+ }
74
+
75
+ if (message?.type === "serviceCall") {
76
+ this.handlers.onServiceCall?.(message.payload);
77
+ }
78
+ },
79
+ undefined,
80
+ this.context.subscriptions,
81
+ );
82
+
83
+ this.panel.onDidDispose(() => {
84
+ this.panel = undefined;
85
+ this.activeDocument = undefined;
86
+ });
87
+ }
88
+
89
+ updateDocument(document: vscode.TextDocument): void {
90
+ if (!this.panel) return;
91
+ if (!this.isPreviewableDocument(document)) return;
92
+ this.activeDocument = document;
93
+ this.postActiveDocument();
94
+ }
95
+
96
+ private postActiveDocument(): void {
97
+ if (!this.panel || !this.activeDocument) return;
98
+ const payload = {
99
+ uri: this.activeDocument.uri.toString(),
100
+ languageId: this.activeDocument.languageId,
101
+ text: this.activeDocument.getText(),
102
+ };
103
+ this.panel.webview.postMessage({ type: "updateFile", payload });
104
+ }
105
+
106
+ postMessage(message: PreviewMessage): void {
107
+ if (!this.panel) return;
108
+ this.panel.webview.postMessage(message);
109
+ }
110
+
111
+ private getWebviewHtml(webview: vscode.Webview): string {
112
+ const nonce = this.getNonce();
113
+ const csp = `default-src 'none'; img-src ${webview.cspSource} https:; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}' https:; connect-src https:;`;
114
+ const compilerUri = webview.asWebviewUri(
115
+ vscode.Uri.joinPath(
116
+ this.context.extensionUri,
117
+ "node_modules",
118
+ "@aprovan",
119
+ "patchwork-compiler",
120
+ "dist",
121
+ "index.js",
122
+ ),
123
+ );
124
+
125
+ return `<!DOCTYPE html>
126
+ <html lang="en">
127
+ <head>
128
+ <meta charset="UTF-8" />
129
+ <meta http-equiv="Content-Security-Policy" content="${csp}" />
130
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
131
+ <title>Patchwork Preview</title>
132
+ <style>
133
+ body {
134
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
135
+ margin: 0;
136
+ padding: 16px;
137
+ color: var(--vscode-editor-foreground);
138
+ background: var(--vscode-editor-background);
139
+ }
140
+ .header {
141
+ font-size: 12px;
142
+ letter-spacing: 0.08em;
143
+ text-transform: uppercase;
144
+ opacity: 0.7;
145
+ margin-bottom: 12px;
146
+ }
147
+ pre {
148
+ white-space: pre-wrap;
149
+ word-break: break-word;
150
+ border-radius: 8px;
151
+ padding: 12px;
152
+ background: var(--vscode-editorWidget-background);
153
+ }
154
+ #preview {
155
+ min-height: 120px;
156
+ border-radius: 8px;
157
+ padding: 12px;
158
+ background: var(--vscode-editorWidget-background);
159
+ margin-bottom: 12px;
160
+ }
161
+ .error {
162
+ background: var(--vscode-inputValidation-errorBackground);
163
+ color: var(--vscode-inputValidation-errorForeground);
164
+ border: 1px solid var(--vscode-inputValidation-errorBorder);
165
+ border-radius: 6px;
166
+ padding: 8px 10px;
167
+ margin-bottom: 12px;
168
+ font-size: 12px;
169
+ white-space: pre-wrap;
170
+ }
171
+ .edit-bar {
172
+ display: flex;
173
+ gap: 8px;
174
+ align-items: center;
175
+ margin-bottom: 12px;
176
+ }
177
+ .edit-input {
178
+ flex: 1;
179
+ background: var(--vscode-input-background);
180
+ color: var(--vscode-input-foreground);
181
+ border: 1px solid var(--vscode-input-border);
182
+ border-radius: 6px;
183
+ padding: 6px 8px;
184
+ }
185
+ .edit-submit {
186
+ background: var(--vscode-button-background);
187
+ color: var(--vscode-button-foreground);
188
+ border: none;
189
+ border-radius: 6px;
190
+ padding: 6px 10px;
191
+ cursor: pointer;
192
+ }
193
+ .edit-submit:hover {
194
+ background: var(--vscode-button-hoverBackground);
195
+ }
196
+ .edit-secondary {
197
+ background: var(--vscode-button-secondaryBackground);
198
+ color: var(--vscode-button-secondaryForeground);
199
+ border: none;
200
+ border-radius: 6px;
201
+ padding: 6px 10px;
202
+ cursor: pointer;
203
+ }
204
+ .edit-secondary:hover {
205
+ background: var(--vscode-button-secondaryHoverBackground);
206
+ }
207
+ .edit-status {
208
+ font-size: 11px;
209
+ opacity: 0.7;
210
+ }
211
+ .history-panel {
212
+ border-radius: 8px;
213
+ border: 1px solid var(--vscode-panel-border);
214
+ background: var(--vscode-sideBar-background);
215
+ margin-bottom: 12px;
216
+ }
217
+ .history-header {
218
+ padding: 8px 10px;
219
+ font-size: 12px;
220
+ text-transform: uppercase;
221
+ letter-spacing: 0.08em;
222
+ opacity: 0.7;
223
+ border-bottom: 1px solid var(--vscode-panel-border);
224
+ }
225
+ .history-list {
226
+ max-height: 200px;
227
+ overflow-y: auto;
228
+ padding: 8px 10px;
229
+ display: grid;
230
+ gap: 10px;
231
+ }
232
+ .history-item {
233
+ background: var(--vscode-editorWidget-background);
234
+ border-radius: 6px;
235
+ padding: 8px 10px;
236
+ border: 1px solid var(--vscode-panel-border);
237
+ }
238
+ .history-prompt {
239
+ font-size: 12px;
240
+ font-weight: 600;
241
+ margin-bottom: 6px;
242
+ }
243
+ .history-summary {
244
+ font-size: 12px;
245
+ opacity: 0.8;
246
+ }
247
+ </style>
248
+ </head>
249
+ <body>
250
+ <div class="header">
251
+ <span>Patchwork Preview</span>
252
+ <span id="services-label" class="services-label"></span>
253
+ </div>
254
+ <div id="error" class="error" hidden></div>
255
+ <form id="edit-form" class="edit-bar">
256
+ <input id="edit-input" class="edit-input" placeholder="Ask Patchwork to edit" />
257
+ <button id="edit-submit" class="edit-submit" type="submit">Edit</button>
258
+ <button id="history-toggle" class="edit-secondary" type="button">History</button>
259
+ <span id="edit-status" class="edit-status"></span>
260
+ </form>
261
+ <section id="history-panel" class="history-panel" hidden>
262
+ <div class="history-header">Edit History</div>
263
+ <div id="history-list" class="history-list"></div>
264
+ </section>
265
+ <div id="preview"></div>
266
+ <pre id="payload">Waiting for file...</pre>
267
+ <script type="module" nonce="${nonce}">
268
+ const vscode = acquireVsCodeApi();
269
+ const output = document.getElementById('payload');
270
+ const previewRoot = document.getElementById('preview');
271
+ const errorBox = document.getElementById('error');
272
+ const compilerUrl = "${compilerUri}";
273
+ const fallbackCompilerUrl = "https://esm.sh/@aprovan/patchwork-compiler@0.1.0";
274
+ const imagePackage = "@aprovan/patchwork-image-shadcn";
275
+ const proxyBase = "https://patchwork.local/api/proxy";
276
+ let compiler = null;
277
+ let mounted = null;
278
+ let editBuffer = '';
279
+ const pendingServices = new Map();
280
+ let serviceNamespaces = [];
281
+ const servicesLabel = document.getElementById('services-label');
282
+
283
+ async function loadCompiler() {
284
+ try {
285
+ return await import(compilerUrl);
286
+ } catch (error) {
287
+ console.warn('[patchwork-vscode] Failed to load local compiler:', error);
288
+ return import(fallbackCompilerUrl);
289
+ }
290
+ }
291
+
292
+ async function ensureCompiler() {
293
+ if (compiler) return compiler;
294
+ const mod = await loadCompiler();
295
+ compiler = await mod.createCompiler({
296
+ image: imagePackage,
297
+ proxyUrl: proxyBase,
298
+ });
299
+ return compiler;
300
+ }
301
+
302
+ function setError(message) {
303
+ if (!message) {
304
+ errorBox.hidden = true;
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: space-between;
308
+ errorBox.textContent = '';
309
+ .services-label {
310
+ font-size: 10px;
311
+ text-transform: none;
312
+ letter-spacing: 0.02em;
313
+ padding: 4px 6px;
314
+ border-radius: 999px;
315
+ background: var(--vscode-badge-background);
316
+ color: var(--vscode-badge-foreground);
317
+ opacity: 0.9;
318
+ }
319
+ return;
320
+ }
321
+ errorBox.hidden = false;
322
+ errorBox.textContent = message;
323
+ }
324
+
325
+ async function compileAndMount(text) {
326
+ if (!previewRoot) return;
327
+ try {
328
+ setError('');
329
+ const activeCompiler = await ensureCompiler();
330
+ if (mounted) {
331
+ activeCompiler.unmount(mounted);
332
+ mounted = null;
333
+ }
334
+ const manifest = {
335
+ name: 'preview',
336
+ version: '0.0.0',
337
+ platform: 'browser',
338
+ image: imagePackage,
339
+ };
340
+ const widget = await activeCompiler.compile(text, manifest, {
341
+ typescript: true,
342
+ });
343
+ mounted = await activeCompiler.mount(widget, {
344
+ target: previewRoot,
345
+ mode: 'embedded',
346
+ });
347
+ vscode.postMessage({ type: 'compileSuccess' });
348
+ } catch (error) {
349
+ const err = error instanceof Error ? error : new Error('Compile failed');
350
+ setError(err.message);
351
+ vscode.postMessage({
352
+ type: 'compileError',
353
+ payload: { message: err.message, line: 1, column: 1 },
354
+ });
355
+ }
356
+ }
357
+
358
+ const originalFetch = window.fetch.bind(window);
359
+ window.fetch = async (input, init) => {
360
+ const url = typeof input === 'string' ? input : input.url;
361
+ if (url && url.startsWith(proxyBase)) {
362
+ const parsed = new URL(url);
363
+ const parts = parsed.pathname.split('/').filter(Boolean);
364
+ const namespace = parts[2];
365
+ const procedure = parts.slice(3).join('/');
366
+ const body = init?.body ? JSON.parse(init.body) : {};
367
+ const args = body.args || {};
368
+ try {
369
+ const result = await callService(namespace, procedure, args);
370
+ if (result && result.error) {
371
+ return new Response(JSON.stringify({ error: result.error }), {
372
+ status: 500,
373
+ headers: { 'Content-Type': 'application/json' },
374
+ });
375
+ }
376
+ return new Response(JSON.stringify(result?.result ?? result), {
377
+ status: 200,
378
+ headers: { 'Content-Type': 'application/json' },
379
+ });
380
+ } catch (error) {
381
+ const err = error instanceof Error ? error.message : 'Service call failed';
382
+ return new Response(JSON.stringify({ error: err }), {
383
+ status: 500,
384
+ headers: { 'Content-Type': 'application/json' },
385
+ });
386
+ }
387
+ }
388
+ return originalFetch(input, init);
389
+ };
390
+
391
+ function callService(namespace, procedure, args) {
392
+ const id = Date.now() + '-' + Math.random().toString(36).slice(2, 10);
393
+ return new Promise((resolve) => {
394
+ pendingServices.set(id, resolve);
395
+ vscode.postMessage({
396
+ type: 'serviceCall',
397
+ payload: { id, namespace, procedure, args },
398
+ });
399
+ });
400
+ }
401
+
402
+ window.addEventListener('message', (event) => {
403
+ const message = event.data;
404
+ if (!message) return;
405
+ if (message.type === 'updateFile') {
406
+ const payload = message.payload || {};
407
+ output.textContent = payload.text || 'No content.';
408
+ const code = payload.text || '';
409
+ compileAndMount(code);
410
+ return;
411
+ }
412
+ if (message.type === 'serviceResult') {
413
+ const payload = message.payload || {};
414
+ const handler = pendingServices.get(payload.id);
415
+ if (handler) {
416
+ pendingServices.delete(payload.id);
417
+ handler(payload);
418
+ }
419
+ return;
420
+ }
421
+ if (message.type === 'editHistorySet') {
422
+ const payload = message.payload || {};
423
+ renderHistory(payload.entries || []);
424
+ return;
425
+ }
426
+ if (message.type === 'editHistoryToggle') {
427
+ toggleHistory();
428
+ return;
429
+ }
430
+ if (message.type === 'setServices') {
431
+ const payload = message.payload || {};
432
+ serviceNamespaces = payload.namespaces || [];
433
+ if (servicesLabel) {
434
+ const count = serviceNamespaces.length;
435
+ servicesLabel.textContent = count === 0
436
+ ? 'No services'
437
+ : 'Services: ' + serviceNamespaces.join(', ');
438
+ }
439
+ return;
440
+ }
441
+ if (message.type === 'editProgress') {
442
+ const payload = message.payload || {};
443
+ if (payload.chunk) {
444
+ editBuffer += payload.chunk;
445
+ }
446
+ if (payload.done) {
447
+ output.textContent = editBuffer || output.textContent;
448
+ editBuffer = '';
449
+ if (status) status.textContent = 'Applied.';
450
+ }
451
+ return;
452
+ }
453
+ if (message.type === 'editError') {
454
+ const payload = message.payload || {};
455
+ setError(payload.message || 'Edit failed');
456
+ if (status) status.textContent = 'Failed.';
457
+ }
458
+ });
459
+
460
+ const form = document.getElementById('edit-form');
461
+ const input = document.getElementById('edit-input');
462
+ const submit = document.getElementById('edit-submit');
463
+ const status = document.getElementById('edit-status');
464
+ const historyToggle = document.getElementById('history-toggle');
465
+ const historyPanel = document.getElementById('history-panel');
466
+ const historyList = document.getElementById('history-list');
467
+
468
+ form?.addEventListener('submit', (event) => {
469
+ event.preventDefault();
470
+ const prompt = input?.value?.trim();
471
+ if (!prompt) return;
472
+ if (status) status.textContent = 'Editing...';
473
+ editBuffer = '';
474
+ vscode.postMessage({
475
+ type: 'editRequest',
476
+ payload: { prompt },
477
+ });
478
+ if (input) input.value = '';
479
+ });
480
+
481
+ historyToggle?.addEventListener('click', () => {
482
+ toggleHistory();
483
+ vscode.postMessage({ type: 'editHistoryToggle' });
484
+ });
485
+
486
+ function toggleHistory() {
487
+ if (!historyPanel) return;
488
+ historyPanel.hidden = !historyPanel.hidden;
489
+ }
490
+
491
+ function renderHistory(entries) {
492
+ if (!historyList) return;
493
+ historyList.innerHTML = '';
494
+ if (!entries || entries.length === 0) {
495
+ const empty = document.createElement('div');
496
+ empty.className = 'history-item';
497
+ empty.textContent = 'No edits yet.';
498
+ historyList.appendChild(empty);
499
+ return;
500
+ }
501
+ entries.forEach((entry) => {
502
+ const item = document.createElement('div');
503
+ item.className = 'history-item';
504
+ const prompt = document.createElement('div');
505
+ prompt.className = 'history-prompt';
506
+ prompt.textContent = entry.prompt || 'Edit';
507
+ const summary = document.createElement('div');
508
+ summary.className = 'history-summary';
509
+ summary.textContent = entry.summary || '';
510
+ item.appendChild(prompt);
511
+ item.appendChild(summary);
512
+ historyList.appendChild(item);
513
+ });
514
+ }
515
+
516
+ vscode.postMessage({ type: 'ready' });
517
+ </script>
518
+ </body>
519
+ </html>`;
520
+ }
521
+
522
+ private getNonce(): string {
523
+ let text = "";
524
+ const possible =
525
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
526
+ for (let i = 0; i < 32; i += 1) {
527
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
528
+ }
529
+ return text;
530
+ }
531
+
532
+ private isPreviewableDocument(document: vscode.TextDocument): boolean {
533
+ const ext = path.extname(document.uri.path).toLowerCase();
534
+ return ext === ".tsx" || ext === ".jsx";
535
+ }
536
+ }
@@ -0,0 +1,24 @@
1
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
+ import { streamText } from "ai";
3
+ import { EDIT_PROMPT } from "@aprovan/stitchery";
4
+
5
+ export class EditService {
6
+ constructor(private readonly baseUrl: string) {}
7
+
8
+ async *streamEdit(code: string, prompt: string): AsyncGenerator<string> {
9
+ const provider = createOpenAICompatible({
10
+ name: "copilot-proxy",
11
+ baseURL: this.baseUrl,
12
+ });
13
+
14
+ const result = streamText({
15
+ model: provider("claude-opus-4.5"),
16
+ system: `Current component code:\n\`\`\`tsx\n${code}\n\`\`\`\n\n${EDIT_PROMPT}`,
17
+ messages: [{ role: "user", content: prompt }],
18
+ });
19
+
20
+ for await (const chunk of result.textStream) {
21
+ yield chunk;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,82 @@
1
+ import { createMCPClient } from "@ai-sdk/mcp";
2
+ import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
3
+ import { createUtcpBackend } from "@aprovan/patchwork-utcp";
4
+ import { ServiceRegistry } from "@aprovan/stitchery";
5
+ import type { McpServerConfig, UtcpConfig } from "@aprovan/stitchery";
6
+
7
+ export interface EmbeddedStitcheryConfig {
8
+ utcp?: UtcpConfig;
9
+ mcpServers?: McpServerConfig[];
10
+ }
11
+
12
+ export interface ServiceCallMessage {
13
+ id: string;
14
+ namespace: string;
15
+ procedure: string;
16
+ args: Record<string, unknown>;
17
+ }
18
+
19
+ export interface ServiceResultMessage {
20
+ id: string;
21
+ result?: unknown;
22
+ error?: string;
23
+ }
24
+
25
+ export class EmbeddedStitchery {
26
+ private registry = new ServiceRegistry();
27
+
28
+ async initialize(config: EmbeddedStitcheryConfig = {}): Promise<void> {
29
+ this.registry = new ServiceRegistry();
30
+ const { utcp, mcpServers = [] } = config;
31
+
32
+ if (mcpServers.length > 0) {
33
+ await this.initMcpTools(mcpServers);
34
+ }
35
+
36
+ if (utcp) {
37
+ try {
38
+ const { backend, toolInfos } = await createUtcpBackend(
39
+ utcp as Parameters<typeof createUtcpBackend>[0],
40
+ utcp.cwd,
41
+ );
42
+ this.registry.registerBackend(backend, toolInfos);
43
+ } catch (error) {
44
+ console.error("[patchwork-vscode] UTCP init failed:", error);
45
+ }
46
+ }
47
+ }
48
+
49
+ async handleServiceCall(
50
+ msg: ServiceCallMessage,
51
+ ): Promise<ServiceResultMessage> {
52
+ try {
53
+ const result = await this.registry.call(
54
+ msg.namespace,
55
+ msg.procedure,
56
+ msg.args,
57
+ );
58
+ return { id: msg.id, result };
59
+ } catch (error) {
60
+ return {
61
+ id: msg.id,
62
+ error: error instanceof Error ? error.message : "Service call failed",
63
+ };
64
+ }
65
+ }
66
+
67
+ getNamespaces(): string[] {
68
+ return this.registry.getNamespaces();
69
+ }
70
+
71
+ private async initMcpTools(servers: McpServerConfig[]): Promise<void> {
72
+ for (const server of servers) {
73
+ const client = await createMCPClient({
74
+ transport: new Experimental_StdioMCPTransport({
75
+ command: server.command,
76
+ args: server.args,
77
+ }),
78
+ });
79
+ this.registry.registerTools(await client.tools(), server.name);
80
+ }
81
+ }
82
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "module": "commonjs",
7
+ "target": "ES2022",
8
+ "lib": ["ES2022"],
9
+ "types": ["vscode", "node"]
10
+ },
11
+ "include": ["src/**/*"],
12
+ "exclude": ["node_modules", "dist"]
13
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/extension.ts"],
5
+ format: ["cjs"],
6
+ target: "node20",
7
+ dts: true,
8
+ clean: true,
9
+ sourcemap: true,
10
+ external: ["vscode"],
11
+ });