@aprovan/patchwork-editor 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.
package/dist/index.js ADDED
@@ -0,0 +1,1597 @@
1
+ import { Node, textblockTypeInputRule } from '@tiptap/core';
2
+ import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent, useEditor, EditorContent } from '@tiptap/react';
3
+ import { useRef, useCallback, useMemo, useState, useEffect } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { Pencil, Loader2, RotateCcw, FileCode, FolderTree, Eye, Code, X, AlertCircle, Send, MessageSquare, Cloud, Check, Server, ChevronDown, ChevronRight, Folder, File } from 'lucide-react';
6
+ import { createSingleFileProject, LocalFSBackend, VFSStore, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
7
+ import Markdown from 'react-markdown';
8
+ import remarkGfm from 'remark-gfm';
9
+ import StarterKit from '@tiptap/starter-kit';
10
+ import Placeholder from '@tiptap/extension-placeholder';
11
+ import Typography from '@tiptap/extension-typography';
12
+ import { Markdown as Markdown$1 } from 'tiptap-markdown';
13
+ import { TextSelection } from '@tiptap/pm/state';
14
+ import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
15
+ import { clsx } from 'clsx';
16
+ import { twMerge } from 'tailwind-merge';
17
+
18
+ // src/components/CodeBlockExtension.tsx
19
+ var BACKTICK_INPUT_REGEX = /^```([a-z]*)?$/;
20
+ function CodeBlockComponent({ node, updateAttributes, editor, getPos }) {
21
+ const inputRef = useRef(null);
22
+ const focusInput = useCallback(() => {
23
+ requestAnimationFrame(() => inputRef.current?.focus());
24
+ }, []);
25
+ const focusCodeContent = useCallback(() => {
26
+ const pos = typeof getPos === "function" ? getPos() : void 0;
27
+ if (pos !== void 0) {
28
+ editor.chain().focus().setTextSelection(pos + 1).run();
29
+ }
30
+ }, [editor, getPos]);
31
+ const focusPreviousNode = useCallback(() => {
32
+ const pos = typeof getPos === "function" ? getPos() : void 0;
33
+ if (pos !== void 0 && pos > 1) {
34
+ const $pos = editor.state.doc.resolve(pos);
35
+ if ($pos.nodeBefore) {
36
+ editor.chain().focus().setTextSelection(pos - 1).run();
37
+ } else {
38
+ editor.chain().focus().setTextSelection(1).run();
39
+ }
40
+ }
41
+ }, [editor, getPos]);
42
+ const handleLanguageChange = useCallback(
43
+ (e) => {
44
+ updateAttributes({ language: e.target.value });
45
+ },
46
+ [updateAttributes]
47
+ );
48
+ const handleInputKeyDown = useCallback((e) => {
49
+ e.stopPropagation();
50
+ switch (e.key) {
51
+ case "Enter":
52
+ case "ArrowDown":
53
+ e.preventDefault();
54
+ focusCodeContent();
55
+ break;
56
+ case "ArrowUp":
57
+ e.preventDefault();
58
+ focusPreviousNode();
59
+ break;
60
+ case "Escape":
61
+ e.preventDefault();
62
+ focusCodeContent();
63
+ break;
64
+ }
65
+ }, [focusCodeContent, focusPreviousNode]);
66
+ const handleInputMouseDown = useCallback((e) => {
67
+ e.stopPropagation();
68
+ e.preventDefault();
69
+ focusInput();
70
+ }, [focusInput]);
71
+ return /* @__PURE__ */ jsxs(NodeViewWrapper, { className: "code-block-wrapper my-2", "data-type": "codeBlock", children: [
72
+ /* @__PURE__ */ jsxs("div", { className: "language-input-wrapper flex items-center gap-1 mb-1", contentEditable: false, children: [
73
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground select-none", children: "```" }),
74
+ /* @__PURE__ */ jsx(
75
+ "input",
76
+ {
77
+ ref: inputRef,
78
+ type: "text",
79
+ value: node.attrs.language || "",
80
+ onChange: handleLanguageChange,
81
+ onKeyDown: handleInputKeyDown,
82
+ onKeyUp: (e) => e.stopPropagation(),
83
+ onMouseDown: handleInputMouseDown,
84
+ onClick: (e) => {
85
+ e.stopPropagation();
86
+ focusInput();
87
+ },
88
+ onFocus: (e) => e.stopPropagation(),
89
+ onBlur: (e) => e.stopPropagation(),
90
+ placeholder: "language",
91
+ className: "language-input bg-transparent text-xs text-muted-foreground outline-none border-none min-w-[60px] max-w-[120px] focus:ring-1 focus:ring-ring rounded px-1",
92
+ style: { width: `${Math.max(60, (node.attrs.language?.length || 8) * 7)}px` },
93
+ spellCheck: false,
94
+ autoComplete: "off",
95
+ autoCorrect: "off",
96
+ autoCapitalize: "off"
97
+ }
98
+ )
99
+ ] }),
100
+ /* @__PURE__ */ jsx("pre", { className: "bg-muted rounded-md px-3 py-2 font-mono text-sm overflow-x-auto !mt-0", children: /* @__PURE__ */ jsx(NodeViewContent, { as: "code" }) })
101
+ ] });
102
+ }
103
+ var CodeBlockExtension = Node.create({
104
+ name: "codeBlock",
105
+ addOptions() {
106
+ return {
107
+ languageClassPrefix: "language-",
108
+ HTMLAttributes: {}
109
+ };
110
+ },
111
+ content: "text*",
112
+ marks: "",
113
+ group: "block",
114
+ code: true,
115
+ defining: true,
116
+ addAttributes() {
117
+ return {
118
+ language: {
119
+ default: null,
120
+ parseHTML: (element) => {
121
+ const { languageClassPrefix } = this.options;
122
+ const classNames = [...element.firstElementChild?.classList || []];
123
+ const languages = classNames.filter((className) => className.startsWith(languageClassPrefix)).map((className) => className.replace(languageClassPrefix, ""));
124
+ return languages[0] || null;
125
+ },
126
+ rendered: false
127
+ }
128
+ };
129
+ },
130
+ parseHTML() {
131
+ return [{ tag: "pre", preserveWhitespace: "full" }];
132
+ },
133
+ renderHTML({ node, HTMLAttributes }) {
134
+ return [
135
+ "pre",
136
+ HTMLAttributes,
137
+ [
138
+ "code",
139
+ {
140
+ class: node.attrs.language ? this.options.languageClassPrefix + node.attrs.language : null
141
+ },
142
+ 0
143
+ ]
144
+ ];
145
+ },
146
+ addNodeView() {
147
+ return ReactNodeViewRenderer(CodeBlockComponent, {
148
+ stopEvent: ({ event }) => {
149
+ const target = event.target;
150
+ return target.classList.contains("language-input") || !!target.closest(".language-input-wrapper");
151
+ }
152
+ });
153
+ },
154
+ addInputRules() {
155
+ return [
156
+ textblockTypeInputRule({
157
+ find: BACKTICK_INPUT_REGEX,
158
+ type: this.type,
159
+ getAttributes: (match) => ({
160
+ language: match[1] || ""
161
+ })
162
+ })
163
+ ];
164
+ },
165
+ addKeyboardShortcuts() {
166
+ return {
167
+ "Mod-Alt-c": () => this.editor.commands.toggleCodeBlock(),
168
+ Backspace: () => {
169
+ const { empty, $anchor } = this.editor.state.selection;
170
+ const isAtStart = $anchor.pos === 1;
171
+ if (!empty || $anchor.parent.type.name !== this.name) {
172
+ return false;
173
+ }
174
+ if (isAtStart || !$anchor.parent.textContent.length) {
175
+ return this.editor.commands.clearNodes();
176
+ }
177
+ return false;
178
+ }
179
+ };
180
+ }
181
+ });
182
+
183
+ // src/components/edit/types.ts
184
+ function getActiveContent(state) {
185
+ return state.project.files.get(state.activeFile)?.content ?? "";
186
+ }
187
+ function getFiles(project) {
188
+ return Array.from(project.files.values());
189
+ }
190
+
191
+ // src/lib/diff.ts
192
+ var DIFF_BLOCK_REGEX = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;
193
+ var DIFF_MARKER_PATTERNS = [
194
+ /^<<<<<<< SEARCH\s*$/m,
195
+ /^=======\s*$/m,
196
+ /^>>>>>>> REPLACE\s*$/m
197
+ ];
198
+ var CODE_FENCE_REGEX = /^```(\w*)\s*((?:[a-zA-Z_][\w-]*="[^"]*"\s*)*)\s*$/;
199
+ var ATTRIBUTE_REGEX = /([a-zA-Z_][\w-]*)="([^"]*)"/g;
200
+ function parseCodeBlockAttributes(attrString) {
201
+ const attrs = {};
202
+ if (!attrString) return attrs;
203
+ const regex = new RegExp(ATTRIBUTE_REGEX.source, "g");
204
+ let match;
205
+ while ((match = regex.exec(attrString)) !== null) {
206
+ const key = match[1];
207
+ const value = match[2];
208
+ if (key && value !== void 0) {
209
+ attrs[key] = value;
210
+ }
211
+ }
212
+ return attrs;
213
+ }
214
+ function parseCodeBlocks(text) {
215
+ const blocks = [];
216
+ const lines = text.split("\n");
217
+ let i = 0;
218
+ while (i < lines.length) {
219
+ const line = lines[i];
220
+ if (!line) {
221
+ i++;
222
+ continue;
223
+ }
224
+ const fenceMatch = line.match(CODE_FENCE_REGEX);
225
+ if (fenceMatch) {
226
+ const language = fenceMatch[1] || "";
227
+ const attributes = parseCodeBlockAttributes(fenceMatch[2] ?? "");
228
+ const contentLines = [];
229
+ i++;
230
+ while (i < lines.length) {
231
+ const currentLine = lines[i];
232
+ if (currentLine !== void 0 && currentLine.match(/^```\s*$/)) {
233
+ break;
234
+ }
235
+ contentLines.push(currentLine ?? "");
236
+ i++;
237
+ }
238
+ blocks.push({
239
+ language,
240
+ attributes,
241
+ content: contentLines.join("\n")
242
+ });
243
+ }
244
+ i++;
245
+ }
246
+ return blocks;
247
+ }
248
+ function findDiffMarkers(text) {
249
+ for (const pattern of DIFF_MARKER_PATTERNS) {
250
+ const match = text.match(pattern);
251
+ if (match) {
252
+ return match[0].trim();
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+ function sanitizeDiffMarkers(text) {
258
+ let result = text;
259
+ result = result.replace(/^<<<<<<< SEARCH\s*\n?/gm, "");
260
+ result = result.replace(/^=======\s*\n?/gm, "");
261
+ result = result.replace(/^>>>>>>> REPLACE\s*\n?/gm, "");
262
+ result = result.replace(/\n{3,}/g, "\n\n");
263
+ return result;
264
+ }
265
+ function parseEditResponse(text) {
266
+ const progressNotes = [];
267
+ const diffs = [];
268
+ const codeBlocks = parseCodeBlocks(text);
269
+ for (const block of codeBlocks) {
270
+ if (block.attributes.note) {
271
+ progressNotes.push(block.attributes.note);
272
+ }
273
+ const diffMatch = block.content.match(
274
+ /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/
275
+ );
276
+ if (diffMatch && diffMatch[1] !== void 0 && diffMatch[2] !== void 0) {
277
+ diffs.push({
278
+ note: block.attributes.note,
279
+ path: block.attributes.path,
280
+ search: diffMatch[1],
281
+ replace: diffMatch[2]
282
+ });
283
+ }
284
+ }
285
+ const summary = extractSummary(text);
286
+ return { progressNotes, diffs, summary };
287
+ }
288
+ function parseDiffs(text) {
289
+ const blocks = [];
290
+ const codeBlocks = parseCodeBlocks(text);
291
+ for (const block of codeBlocks) {
292
+ const diffMatch = block.content.match(
293
+ /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/
294
+ );
295
+ if (diffMatch && diffMatch[1] !== void 0 && diffMatch[2] !== void 0) {
296
+ blocks.push({
297
+ note: block.attributes.note,
298
+ path: block.attributes.path,
299
+ search: diffMatch[1],
300
+ replace: diffMatch[2]
301
+ });
302
+ }
303
+ }
304
+ if (blocks.length === 0) {
305
+ const regex = new RegExp(DIFF_BLOCK_REGEX.source, "g");
306
+ let match;
307
+ while ((match = regex.exec(text)) !== null) {
308
+ if (match[1] !== void 0 && match[2] !== void 0) {
309
+ blocks.push({ search: match[1], replace: match[2] });
310
+ }
311
+ }
312
+ }
313
+ return blocks;
314
+ }
315
+ function applyDiffs(code, diffs, options = {}) {
316
+ let result = code;
317
+ let applied = 0;
318
+ const failed = [];
319
+ for (const diff of diffs) {
320
+ if (result.includes(diff.search)) {
321
+ result = result.replace(diff.search, diff.replace);
322
+ applied++;
323
+ } else {
324
+ const lines = diff.search.split("\n").slice(0, 3);
325
+ const preview = lines.join("\n").slice(0, 100);
326
+ const suffix = diff.search.length > preview.length ? "..." : "";
327
+ failed.push(preview + suffix);
328
+ }
329
+ }
330
+ const marker = findDiffMarkers(result);
331
+ let warning;
332
+ if (marker) {
333
+ if (options.sanitize) {
334
+ result = sanitizeDiffMarkers(result);
335
+ warning = `Removed stray diff marker "${marker}" from output`;
336
+ } else {
337
+ warning = `Output contains diff marker "${marker}" - the LLM may have generated a malformed response`;
338
+ }
339
+ }
340
+ return { code: result, applied, failed, warning };
341
+ }
342
+ function hasDiffBlocks(text) {
343
+ return DIFF_BLOCK_REGEX.test(text);
344
+ }
345
+ function extractTextWithoutDiffs(text) {
346
+ return text.replace(DIFF_BLOCK_REGEX, "").trim();
347
+ }
348
+ var CODE_BLOCK_FULL_REGEX = /```\w*(?:\s+[a-zA-Z_][\w-]*="[^"]*")*\s*\n[\s\S]*?\n```/g;
349
+ function extractSummary(text) {
350
+ let summary = text.replace(CODE_BLOCK_FULL_REGEX, "");
351
+ summary = summary.replace(/^<<<<<<< SEARCH\s*$/gm, "");
352
+ summary = summary.replace(/^=======\s*$/gm, "");
353
+ summary = summary.replace(/^>>>>>>> REPLACE\s*$/gm, "");
354
+ summary = summary.replace(/^```[\w]*(?:\s+[a-zA-Z_][\w-]*="[^"]*")*\s*$/gm, "");
355
+ summary = summary.replace(/\n{3,}/g, "\n\n").trim();
356
+ return summary;
357
+ }
358
+
359
+ // src/components/edit/api.ts
360
+ async function sendEditRequest(request, options = {}) {
361
+ const { endpoint = "/api/edit", onProgress, sanitize = true } = options;
362
+ const response = await fetch(endpoint, {
363
+ method: "POST",
364
+ headers: { "Content-Type": "application/json" },
365
+ body: JSON.stringify(request)
366
+ });
367
+ if (!response.ok) {
368
+ throw new Error("Edit request failed");
369
+ }
370
+ const text = await streamResponse(response, onProgress);
371
+ if (!hasDiffBlocks(text)) {
372
+ throw new Error("No valid diffs in response");
373
+ }
374
+ const parsed = parseEditResponse(text);
375
+ const result = applyDiffs(request.code, parsed.diffs, { sanitize });
376
+ if (result.applied === 0) {
377
+ const failedDetails = result.failed.map((f, i) => `[${i + 1}] "${f}"`).join("\n");
378
+ throw new Error(
379
+ `Failed to apply ${parsed.diffs.length} diff(s). None of the SEARCH blocks matched the code.
380
+
381
+ Failed searches:
382
+ ${failedDetails}
383
+
384
+ This usually means the code has changed or the SEARCH text doesn't match exactly.`
385
+ );
386
+ }
387
+ let summary = parsed.summary || `Applied ${result.applied} change(s)`;
388
+ if (result.warning) {
389
+ summary = `\u26A0\uFE0F ${result.warning}
390
+
391
+ ${summary}`;
392
+ }
393
+ return {
394
+ newCode: result.code,
395
+ summary,
396
+ progressNotes: parsed.progressNotes
397
+ };
398
+ }
399
+ async function streamResponse(response, onProgress) {
400
+ const reader = response.body?.getReader();
401
+ if (!reader) {
402
+ return response.text();
403
+ }
404
+ const decoder = new TextDecoder();
405
+ let fullText = "";
406
+ const emittedNotes = /* @__PURE__ */ new Set();
407
+ let done = false;
408
+ while (!done) {
409
+ const result = await reader.read();
410
+ done = result.done;
411
+ if (result.value) {
412
+ fullText += decoder.decode(result.value, { stream: true });
413
+ if (onProgress) {
414
+ const noteAttrRegex = /```\w*\s+note="([^"]+)"/g;
415
+ let match;
416
+ while ((match = noteAttrRegex.exec(fullText)) !== null) {
417
+ const noteMatch = match[1];
418
+ if (noteMatch) {
419
+ const note = noteMatch.trim();
420
+ if (!emittedNotes.has(note)) {
421
+ emittedNotes.add(note);
422
+ onProgress(note);
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+ return fullText;
430
+ }
431
+ function cloneProject(project) {
432
+ return {
433
+ ...project,
434
+ files: new Map(project.files)
435
+ };
436
+ }
437
+ function useEditSession(options) {
438
+ const { originalCode, compile, apiEndpoint } = options;
439
+ const originalProject = useMemo(
440
+ () => createSingleFileProject(originalCode),
441
+ [originalCode]
442
+ );
443
+ const [project, setProject] = useState(originalProject);
444
+ const [activeFile, setActiveFile] = useState(originalProject.entry);
445
+ const [history, setHistory] = useState([]);
446
+ const [isApplying, setIsApplying] = useState(false);
447
+ const [error, setError] = useState(null);
448
+ const [streamingNotes, setStreamingNotes] = useState([]);
449
+ const [pendingPrompt, setPendingPrompt] = useState(null);
450
+ const performEdit = useCallback(
451
+ async (currentCode2, prompt, isRetry = false) => {
452
+ const entries = [];
453
+ const response = await sendEditRequest(
454
+ { code: currentCode2, prompt },
455
+ {
456
+ endpoint: apiEndpoint,
457
+ onProgress: (note) => setStreamingNotes((prev) => [...prev, note])
458
+ }
459
+ );
460
+ entries.push({
461
+ prompt: isRetry ? `Fix: ${prompt}` : prompt,
462
+ summary: response.summary,
463
+ isRetry
464
+ });
465
+ if (compile) {
466
+ const compileResult = await compile(response.newCode);
467
+ if (!compileResult.success && compileResult.error) {
468
+ setStreamingNotes([]);
469
+ const errorPrompt = `Compilation error: ${compileResult.error}
470
+
471
+ Please fix this error.`;
472
+ const retryResult = await performEdit(
473
+ response.newCode,
474
+ errorPrompt,
475
+ true
476
+ );
477
+ return {
478
+ newCode: retryResult.newCode,
479
+ entries: [...entries, ...retryResult.entries]
480
+ };
481
+ }
482
+ }
483
+ return { newCode: response.newCode, entries };
484
+ },
485
+ [compile, apiEndpoint]
486
+ );
487
+ const currentCode = useMemo(
488
+ () => project.files.get(activeFile)?.content ?? "",
489
+ [project, activeFile]
490
+ );
491
+ const submitEdit = useCallback(
492
+ async (prompt) => {
493
+ if (!prompt.trim() || isApplying) return;
494
+ setIsApplying(true);
495
+ setError(null);
496
+ setStreamingNotes([]);
497
+ setPendingPrompt(prompt);
498
+ try {
499
+ const result = await performEdit(currentCode, prompt);
500
+ setProject((prev) => {
501
+ const updated = cloneProject(prev);
502
+ const file = updated.files.get(activeFile);
503
+ if (file) {
504
+ updated.files.set(activeFile, { ...file, content: result.newCode });
505
+ }
506
+ return updated;
507
+ });
508
+ setHistory((prev) => [...prev, ...result.entries]);
509
+ } catch (err) {
510
+ setError(err instanceof Error ? err.message : "Edit failed");
511
+ } finally {
512
+ setIsApplying(false);
513
+ setStreamingNotes([]);
514
+ setPendingPrompt(null);
515
+ }
516
+ },
517
+ [currentCode, activeFile, isApplying, performEdit]
518
+ );
519
+ const revert = useCallback(() => {
520
+ setProject(originalProject);
521
+ setActiveFile(originalProject.entry);
522
+ setHistory([]);
523
+ setError(null);
524
+ setStreamingNotes([]);
525
+ }, [originalProject]);
526
+ const updateActiveFile = useCallback(
527
+ (content) => {
528
+ setProject((prev) => {
529
+ const updated = cloneProject(prev);
530
+ const file = updated.files.get(activeFile);
531
+ if (file) {
532
+ updated.files.set(activeFile, { ...file, content });
533
+ }
534
+ return updated;
535
+ });
536
+ },
537
+ [activeFile]
538
+ );
539
+ const clearError = useCallback(() => {
540
+ setError(null);
541
+ }, []);
542
+ return {
543
+ project,
544
+ originalProject,
545
+ activeFile,
546
+ history,
547
+ isApplying,
548
+ error,
549
+ streamingNotes,
550
+ pendingPrompt,
551
+ submitEdit,
552
+ revert,
553
+ updateActiveFile,
554
+ setActiveFile,
555
+ clearError
556
+ };
557
+ }
558
+ function ProgressNote({ text, isLatest }) {
559
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground/60", children: [
560
+ isLatest && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
561
+ /* @__PURE__ */ jsx("p", { className: "text-xs italic", children: text })
562
+ ] });
563
+ }
564
+ function EditHistory({
565
+ entries,
566
+ streamingNotes,
567
+ isStreaming,
568
+ pendingPrompt,
569
+ className = ""
570
+ }) {
571
+ const scrollRef = useRef(null);
572
+ useEffect(() => {
573
+ if (scrollRef.current) {
574
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
575
+ }
576
+ }, [entries, streamingNotes, pendingPrompt]);
577
+ return /* @__PURE__ */ jsxs(
578
+ "div",
579
+ {
580
+ ref: scrollRef,
581
+ className: `overflow-y-auto p-4 space-y-4 bg-background ${className}`,
582
+ children: [
583
+ entries.map((entry, i) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
584
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx("div", { className: "bg-primary text-primary-foreground rounded-lg px-4 py-2 max-w-[85%]", children: /* @__PURE__ */ jsx("div", { className: "prose prose-sm prose-invert max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0", children: /* @__PURE__ */ jsx(Markdown, { remarkPlugins: [remarkGfm], children: entry.prompt }) }) }) }),
585
+ /* @__PURE__ */ jsx("div", { className: "flex justify-start", children: /* @__PURE__ */ jsx("div", { className: "bg-primary/10 rounded-lg px-4 py-2 max-w-[85%]", children: /* @__PURE__ */ jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0", children: /* @__PURE__ */ jsx(Markdown, { remarkPlugins: [remarkGfm], children: entry.summary }) }) }) })
586
+ ] }, i)),
587
+ pendingPrompt && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
588
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx("div", { className: "bg-primary text-primary-foreground rounded-lg px-4 py-2 max-w-[85%]", children: /* @__PURE__ */ jsx("div", { className: "prose prose-sm prose-invert max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0", children: /* @__PURE__ */ jsx(Markdown, { remarkPlugins: [remarkGfm], children: pendingPrompt }) }) }) }),
589
+ isStreaming && streamingNotes.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-1 py-2 px-3", children: streamingNotes.map((note, i) => /* @__PURE__ */ jsx(
590
+ ProgressNote,
591
+ {
592
+ text: note,
593
+ isLatest: i === streamingNotes.length - 1
594
+ },
595
+ i
596
+ )) })
597
+ ] })
598
+ ]
599
+ }
600
+ );
601
+ }
602
+ function MarkdownEditor({
603
+ value,
604
+ onChange,
605
+ onSubmit,
606
+ placeholder = "Type a message...",
607
+ disabled = false,
608
+ className = ""
609
+ }) {
610
+ const containerRef = useRef(null);
611
+ const editor = useEditor({
612
+ extensions: [
613
+ StarterKit.configure({
614
+ heading: { levels: [1, 2, 3] },
615
+ bulletList: { keepMarks: true, keepAttributes: false },
616
+ orderedList: { keepMarks: true, keepAttributes: false },
617
+ codeBlock: false,
618
+ // Use our custom CodeBlockExtension
619
+ code: {
620
+ HTMLAttributes: {
621
+ class: "bg-muted rounded px-1 py-0.5 font-mono text-sm"
622
+ }
623
+ },
624
+ blockquote: {
625
+ HTMLAttributes: {
626
+ class: "border-l-4 border-muted-foreground/30 pl-4 italic"
627
+ }
628
+ },
629
+ horizontalRule: false,
630
+ hardBreak: { keepMarks: false }
631
+ }),
632
+ CodeBlockExtension,
633
+ Placeholder.configure({
634
+ placeholder,
635
+ emptyEditorClass: "is-editor-empty"
636
+ }),
637
+ Typography,
638
+ Markdown$1.configure({
639
+ html: false,
640
+ transformPastedText: true,
641
+ transformCopiedText: true
642
+ })
643
+ ],
644
+ content: value,
645
+ editable: !disabled,
646
+ editorProps: {
647
+ attributes: {
648
+ class: `outline-none min-h-[40px] max-h-[200px] overflow-y-auto px-3 py-2 ${className}`
649
+ },
650
+ handleKeyDown: (view, event) => {
651
+ const { state } = view;
652
+ const { selection } = state;
653
+ const { $from, $to, empty } = selection;
654
+ const parentType = $from.parent.type.name;
655
+ const isInList = parentType === "listItem";
656
+ const isInCodeBlock = parentType === "codeBlock";
657
+ if ((event.metaKey || event.ctrlKey) && event.key === "a" && isInCodeBlock) {
658
+ event.preventDefault();
659
+ const { tr } = state;
660
+ tr.setSelection(TextSelection.create(tr.doc, $from.start(), $from.end()));
661
+ view.dispatch(tr);
662
+ return true;
663
+ }
664
+ if (event.key === "ArrowUp" && isInCodeBlock) {
665
+ const textBefore = $from.parent.textContent.slice(0, $from.parentOffset);
666
+ if (!textBefore.includes("\n")) {
667
+ const domPos = view.domAtPos($from.start());
668
+ const wrapper = domPos.node.parentElement?.closest(".code-block-wrapper");
669
+ const langInput = wrapper?.querySelector(".language-input");
670
+ if (langInput) {
671
+ event.preventDefault();
672
+ langInput.focus();
673
+ langInput.select();
674
+ return true;
675
+ }
676
+ }
677
+ }
678
+ if (event.key === "Tab" && isInCodeBlock) {
679
+ event.preventDefault();
680
+ const { tr } = state;
681
+ if (empty) {
682
+ if (event.shiftKey) {
683
+ const textBefore = $from.parent.textContent.slice(0, $from.parentOffset);
684
+ if (textBefore.endsWith(" ")) {
685
+ tr.delete($from.pos - 2, $from.pos);
686
+ } else if (textBefore.endsWith(" ")) {
687
+ tr.delete($from.pos - 1, $from.pos);
688
+ }
689
+ } else {
690
+ tr.insertText(" ");
691
+ }
692
+ } else {
693
+ const text = $from.parent.textContent;
694
+ const lineStart = text.lastIndexOf("\n", $from.parentOffset - 1) + 1;
695
+ const lineEnd = text.indexOf("\n", $to.parentOffset);
696
+ const actualEnd = lineEnd === -1 ? text.length : lineEnd;
697
+ const selectedText = text.slice(lineStart, actualEnd);
698
+ const lines = selectedText.split("\n");
699
+ const newText = event.shiftKey ? lines.map((line) => line.replace(/^ ?/, "")).join("\n") : lines.map((line) => " " + line).join("\n");
700
+ const blockStart = $from.start();
701
+ tr.replaceWith(blockStart + lineStart, blockStart + actualEnd, state.schema.text(newText));
702
+ }
703
+ view.dispatch(tr);
704
+ return true;
705
+ }
706
+ if (event.key === "Enter") {
707
+ if (isInCodeBlock) {
708
+ if (event.shiftKey) {
709
+ event.preventDefault();
710
+ const { tr } = state;
711
+ tr.insertText("\n");
712
+ view.dispatch(tr);
713
+ return true;
714
+ }
715
+ event.preventDefault();
716
+ onSubmit();
717
+ return true;
718
+ }
719
+ if (event.shiftKey) {
720
+ event.preventDefault();
721
+ const { tr } = state;
722
+ view.dispatch(tr.split($from.pos));
723
+ return true;
724
+ }
725
+ if (!isInList) {
726
+ event.preventDefault();
727
+ onSubmit();
728
+ return true;
729
+ }
730
+ }
731
+ return false;
732
+ }
733
+ },
734
+ onUpdate: ({ editor: editor2 }) => {
735
+ const markdownStorage = editor2.storage.markdown;
736
+ if (markdownStorage?.getMarkdown) {
737
+ onChange(markdownStorage.getMarkdown());
738
+ } else {
739
+ onChange(editor2.getText());
740
+ }
741
+ }
742
+ });
743
+ useEffect(() => {
744
+ if (!editor) return;
745
+ const handlePaste = (event) => {
746
+ const plainText = event.clipboardData?.getData("text/plain");
747
+ const htmlText = event.clipboardData?.getData("text/html");
748
+ if (!plainText) return;
749
+ const { $from } = editor.state.selection;
750
+ const isInCodeBlock = $from.parent.type.name === "codeBlock";
751
+ if (isInCodeBlock) {
752
+ event.preventDefault();
753
+ event.stopPropagation();
754
+ const { tr } = editor.state;
755
+ tr.insertText(plainText);
756
+ editor.view.dispatch(tr);
757
+ return;
758
+ }
759
+ if (htmlText) {
760
+ event.preventDefault();
761
+ event.stopPropagation();
762
+ const markdownStorage = editor.storage.markdown;
763
+ if (markdownStorage?.parser) {
764
+ try {
765
+ const parsed = markdownStorage.parser.parse(plainText);
766
+ if (parsed?.content?.size > 0) {
767
+ const nodes = [];
768
+ parsed.content.forEach((node) => nodes.push(node.toJSON()));
769
+ editor.chain().focus().insertContent(nodes).run();
770
+ return;
771
+ }
772
+ } catch (e) {
773
+ console.warn("Markdown parse failed, falling back to plain text", e);
774
+ }
775
+ }
776
+ editor.chain().focus().insertContent(plainText).run();
777
+ }
778
+ };
779
+ const container = containerRef.current;
780
+ container?.addEventListener("paste", handlePaste, { capture: true });
781
+ return () => container?.removeEventListener("paste", handlePaste, { capture: true });
782
+ }, [editor]);
783
+ useEffect(() => {
784
+ if (editor && value === "" && editor.getText() !== "") {
785
+ editor.commands.clearContent();
786
+ }
787
+ }, [editor, value]);
788
+ useEffect(() => {
789
+ editor?.setEditable(!disabled);
790
+ }, [editor, disabled]);
791
+ const focus = useCallback(() => {
792
+ editor?.commands.focus();
793
+ }, [editor]);
794
+ useEffect(() => {
795
+ if (editor) {
796
+ editor.focusInput = focus;
797
+ }
798
+ }, [editor, focus]);
799
+ return /* @__PURE__ */ jsx(
800
+ "div",
801
+ {
802
+ ref: containerRef,
803
+ className: `
804
+ flex-1 rounded-md border border-input bg-background text-sm
805
+ ring-offset-background
806
+ focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2
807
+ ${disabled ? "cursor-not-allowed opacity-50" : ""}
808
+ `,
809
+ onClick: () => editor?.commands.focus(),
810
+ children: /* @__PURE__ */ jsx(EditorContent, { editor, className: "markdown-editor" })
811
+ }
812
+ );
813
+ }
814
+ function buildTree(files) {
815
+ const root = { name: "", path: "", isDir: true, children: [] };
816
+ for (const file of files) {
817
+ const parts = file.path.split("/");
818
+ let current = root;
819
+ for (let i = 0; i < parts.length; i++) {
820
+ const part = parts[i];
821
+ const isLast = i === parts.length - 1;
822
+ const currentPath = parts.slice(0, i + 1).join("/");
823
+ let child = current.children.find((c) => c.name === part);
824
+ if (!child) {
825
+ child = {
826
+ name: part,
827
+ path: currentPath,
828
+ isDir: !isLast,
829
+ children: []
830
+ };
831
+ current.children.push(child);
832
+ }
833
+ current = child;
834
+ }
835
+ }
836
+ root.children.sort((a, b) => {
837
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
838
+ return a.name.localeCompare(b.name);
839
+ });
840
+ return root;
841
+ }
842
+ function TreeNodeComponent({ node, activeFile, onSelect, depth = 0 }) {
843
+ const [expanded, setExpanded] = useState(true);
844
+ if (!node.name) {
845
+ return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
846
+ TreeNodeComponent,
847
+ {
848
+ node: child,
849
+ activeFile,
850
+ onSelect,
851
+ depth
852
+ },
853
+ child.path
854
+ )) });
855
+ }
856
+ const isActive = node.path === activeFile;
857
+ if (node.isDir) {
858
+ return /* @__PURE__ */ jsxs("div", { children: [
859
+ /* @__PURE__ */ jsxs(
860
+ "button",
861
+ {
862
+ onClick: () => setExpanded(!expanded),
863
+ className: "flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded",
864
+ style: { paddingLeft: `${depth * 12 + 8}px` },
865
+ children: [
866
+ expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
867
+ /* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
868
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: node.name })
869
+ ]
870
+ }
871
+ ),
872
+ expanded && /* @__PURE__ */ jsx("div", { children: node.children.map((child) => /* @__PURE__ */ jsx(
873
+ TreeNodeComponent,
874
+ {
875
+ node: child,
876
+ activeFile,
877
+ onSelect,
878
+ depth: depth + 1
879
+ },
880
+ child.path
881
+ )) })
882
+ ] });
883
+ }
884
+ return /* @__PURE__ */ jsxs(
885
+ "button",
886
+ {
887
+ onClick: () => onSelect(node.path),
888
+ className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
889
+ style: { paddingLeft: `${depth * 12 + 20}px` },
890
+ children: [
891
+ /* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
892
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: node.name })
893
+ ]
894
+ }
895
+ );
896
+ }
897
+ function FileTree({ files, activeFile, onSelectFile }) {
898
+ const tree = useMemo(() => buildTree(files), [files]);
899
+ return /* @__PURE__ */ jsxs("div", { className: "w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
900
+ /* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Files" }),
901
+ /* @__PURE__ */ jsx("div", { className: "p-1", children: /* @__PURE__ */ jsx(
902
+ TreeNodeComponent,
903
+ {
904
+ node: tree,
905
+ activeFile,
906
+ onSelect: onSelectFile
907
+ }
908
+ ) })
909
+ ] });
910
+ }
911
+ function hashCode(str) {
912
+ let hash = 0;
913
+ for (let i = 0; i < str.length; i++) {
914
+ hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
915
+ }
916
+ return hash;
917
+ }
918
+ function EditModal({
919
+ isOpen,
920
+ onClose,
921
+ renderPreview,
922
+ renderLoading,
923
+ renderError,
924
+ previewError,
925
+ previewLoading,
926
+ ...sessionOptions
927
+ }) {
928
+ const [showPreview, setShowPreview] = useState(true);
929
+ const [showTree, setShowTree] = useState(false);
930
+ const [editInput, setEditInput] = useState("");
931
+ const [bobbinChanges, setBobbinChanges] = useState([]);
932
+ const [previewContainer, setPreviewContainer] = useState(null);
933
+ const session = useEditSession(sessionOptions);
934
+ const code = getActiveContent(session);
935
+ const files = useMemo(() => getFiles(session.project), [session.project]);
936
+ const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
937
+ const handleBobbinChanges = useCallback((changes) => {
938
+ setBobbinChanges(changes);
939
+ }, []);
940
+ const handleSubmit = () => {
941
+ if (!editInput.trim() && bobbinChanges.length === 0 || session.isApplying) return;
942
+ let promptWithContext = editInput;
943
+ if (bobbinChanges.length > 0) {
944
+ const bobbinYaml = serializeChangesToYAML(bobbinChanges, []);
945
+ promptWithContext = `${editInput}
946
+
947
+ ---
948
+ Visual Changes (apply these styles/modifications):
949
+ \`\`\`yaml
950
+ ${bobbinYaml}
951
+ \`\`\``;
952
+ }
953
+ session.submitEdit(promptWithContext);
954
+ setEditInput("");
955
+ setBobbinChanges([]);
956
+ };
957
+ const handleClose = () => {
958
+ const editCount = session.history.length;
959
+ const finalCode = code;
960
+ setEditInput("");
961
+ session.clearError();
962
+ onClose(finalCode, editCount);
963
+ };
964
+ if (!isOpen) return null;
965
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-8", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col bg-background rounded-lg shadow-xl w-full h-full max-w-6xl max-h-[90vh] overflow-hidden", children: [
966
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 bg-background border-b-2", children: [
967
+ /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-primary" }),
968
+ session.isApplying && /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-primary flex items-center gap-1 ml-2", children: [
969
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
970
+ "Applying edits..."
971
+ ] }),
972
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-2", children: [
973
+ hasChanges && /* @__PURE__ */ jsx(
974
+ "button",
975
+ {
976
+ onClick: session.revert,
977
+ className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary",
978
+ title: "Revert to original",
979
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "h-3 w-3" })
980
+ }
981
+ ),
982
+ /* @__PURE__ */ jsx(
983
+ "button",
984
+ {
985
+ onClick: () => setShowTree(!showTree),
986
+ className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showTree ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
987
+ title: showTree ? "Single file" : "File tree",
988
+ children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
989
+ }
990
+ ),
991
+ /* @__PURE__ */ jsxs(
992
+ "button",
993
+ {
994
+ onClick: () => setShowPreview(!showPreview),
995
+ className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
996
+ children: [
997
+ showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
998
+ showPreview ? "Preview" : "Code"
999
+ ]
1000
+ }
1001
+ ),
1002
+ /* @__PURE__ */ jsxs(
1003
+ "button",
1004
+ {
1005
+ onClick: handleClose,
1006
+ className: "px-2 py-1 text-xs rounded flex items-center gap-1 bg-primary text-primary-foreground hover:bg-primary/90",
1007
+ title: "Exit edit mode",
1008
+ children: [
1009
+ /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }),
1010
+ "Done"
1011
+ ]
1012
+ }
1013
+ )
1014
+ ] })
1015
+ ] }),
1016
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 border-b-2 overflow-hidden flex", children: [
1017
+ showTree && /* @__PURE__ */ jsx(
1018
+ FileTree,
1019
+ {
1020
+ files,
1021
+ activeFile: session.activeFile,
1022
+ onSelectFile: session.setActiveFile
1023
+ }
1024
+ ),
1025
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto", children: showPreview ? /* @__PURE__ */ jsxs("div", { className: "bg-white h-full relative", ref: setPreviewContainer, children: [
1026
+ previewError && renderError ? renderError(previewError) : previewError ? /* @__PURE__ */ jsxs("div", { className: "p-4 text-sm text-destructive flex items-center gap-2", children: [
1027
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
1028
+ /* @__PURE__ */ jsx("span", { children: previewError })
1029
+ ] }) : previewLoading && renderLoading ? renderLoading() : previewLoading ? /* @__PURE__ */ jsxs("div", { className: "p-4 flex items-center gap-2 text-muted-foreground", children: [
1030
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
1031
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
1032
+ ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: renderPreview(code) }, hashCode(code)),
1033
+ !renderLoading && !renderError && !previewLoading && /* @__PURE__ */ jsx(
1034
+ Bobbin,
1035
+ {
1036
+ container: previewContainer,
1037
+ pillContainer: previewContainer,
1038
+ defaultActive: false,
1039
+ showInspector: true,
1040
+ onChanges: handleBobbinChanges,
1041
+ exclude: [".bobbin-pill", "[data-bobbin]"]
1042
+ }
1043
+ )
1044
+ ] }) : /* @__PURE__ */ jsx("div", { className: "p-4 bg-muted/10 h-full overflow-auto", children: /* @__PURE__ */ jsx("pre", { className: "text-xs whitespace-pre-wrap break-words m-0", children: /* @__PURE__ */ jsx("code", { children: code }) }) }) })
1045
+ ] }),
1046
+ /* @__PURE__ */ jsx(
1047
+ EditHistory,
1048
+ {
1049
+ entries: session.history,
1050
+ streamingNotes: session.streamingNotes,
1051
+ isStreaming: session.isApplying,
1052
+ pendingPrompt: session.pendingPrompt,
1053
+ className: "h-48"
1054
+ }
1055
+ ),
1056
+ session.error && /* @__PURE__ */ jsxs("div", { className: "px-4 py-2 bg-destructive/10 text-destructive text-sm flex items-center gap-2 border-t-2 border-destructive", children: [
1057
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
1058
+ session.error
1059
+ ] }),
1060
+ bobbinChanges.length > 0 && /* @__PURE__ */ jsxs("div", { className: "px-4 py-2 bg-blue-50 text-blue-700 text-sm flex items-center gap-2 border-t", children: [
1061
+ /* @__PURE__ */ jsxs("span", { children: [
1062
+ bobbinChanges.length,
1063
+ " visual change",
1064
+ bobbinChanges.length !== 1 ? "s" : ""
1065
+ ] }),
1066
+ /* @__PURE__ */ jsx(
1067
+ "button",
1068
+ {
1069
+ onClick: () => setBobbinChanges([]),
1070
+ className: "text-xs underline hover:no-underline",
1071
+ children: "Clear"
1072
+ }
1073
+ )
1074
+ ] }),
1075
+ /* @__PURE__ */ jsxs("div", { className: "p-4 border-t-2 bg-primary/5 flex gap-2 items-end", children: [
1076
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
1077
+ MarkdownEditor,
1078
+ {
1079
+ value: editInput,
1080
+ onChange: setEditInput,
1081
+ onSubmit: handleSubmit,
1082
+ placeholder: "Describe changes...",
1083
+ disabled: session.isApplying
1084
+ }
1085
+ ) }),
1086
+ /* @__PURE__ */ jsx(
1087
+ "button",
1088
+ {
1089
+ onClick: handleSubmit,
1090
+ disabled: !editInput.trim() && bobbinChanges.length === 0 || session.isApplying,
1091
+ className: "px-3 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 flex items-center gap-1 shrink-0",
1092
+ children: session.isApplying ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
1093
+ }
1094
+ )
1095
+ ] })
1096
+ ] }) });
1097
+ }
1098
+ var VFS_BASE_URL = "/vfs";
1099
+ var vfsConfigCache = null;
1100
+ async function getVFSConfig() {
1101
+ if (vfsConfigCache) return vfsConfigCache;
1102
+ try {
1103
+ const res = await fetch(`${VFS_BASE_URL}/config`);
1104
+ if (res.ok) {
1105
+ vfsConfigCache = await res.json();
1106
+ return vfsConfigCache;
1107
+ }
1108
+ } catch {
1109
+ }
1110
+ return { usePaths: false };
1111
+ }
1112
+ var storeInstance = null;
1113
+ function getVFSStore() {
1114
+ if (!storeInstance) {
1115
+ const backend = new LocalFSBackend({ baseUrl: VFS_BASE_URL });
1116
+ storeInstance = new VFSStore(backend);
1117
+ }
1118
+ return storeInstance;
1119
+ }
1120
+ async function saveProject(project) {
1121
+ const store = getVFSStore();
1122
+ await store.saveProject(project);
1123
+ }
1124
+ async function loadProject(id) {
1125
+ const store = getVFSStore();
1126
+ return store.loadProject(id);
1127
+ }
1128
+ async function listProjects() {
1129
+ const store = getVFSStore();
1130
+ const files = await store.listFiles();
1131
+ const projectIds = /* @__PURE__ */ new Set();
1132
+ for (const file of files) {
1133
+ const id = file.split("/")[0];
1134
+ if (id) projectIds.add(id);
1135
+ }
1136
+ return Array.from(projectIds);
1137
+ }
1138
+ async function saveFile(file) {
1139
+ const store = getVFSStore();
1140
+ await store.putFile(file);
1141
+ }
1142
+ async function isVFSAvailable() {
1143
+ try {
1144
+ const res = await fetch(VFS_BASE_URL, { method: "HEAD" });
1145
+ return res.ok || res.status === 404;
1146
+ } catch {
1147
+ return false;
1148
+ }
1149
+ }
1150
+ function createManifest(services) {
1151
+ return {
1152
+ name: "preview",
1153
+ version: "1.0.0",
1154
+ platform: "browser",
1155
+ image: "@aprovan/patchwork-image-shadcn",
1156
+ services
1157
+ };
1158
+ }
1159
+ function useCodeCompiler(compiler, code, enabled, services) {
1160
+ const [loading, setLoading] = useState(false);
1161
+ const [error, setError] = useState(null);
1162
+ const containerRef = useRef(null);
1163
+ const mountedRef = useRef(null);
1164
+ useEffect(() => {
1165
+ if (!enabled || !compiler || !containerRef.current) return;
1166
+ let cancelled = false;
1167
+ async function compileAndMount() {
1168
+ if (!containerRef.current || !compiler) return;
1169
+ setLoading(true);
1170
+ setError(null);
1171
+ try {
1172
+ if (mountedRef.current) {
1173
+ compiler.unmount(mountedRef.current);
1174
+ mountedRef.current = null;
1175
+ }
1176
+ const widget = await compiler.compile(
1177
+ code,
1178
+ createManifest(services),
1179
+ { typescript: true }
1180
+ );
1181
+ if (cancelled) {
1182
+ return;
1183
+ }
1184
+ const mounted = await compiler.mount(widget, {
1185
+ target: containerRef.current,
1186
+ mode: "embedded"
1187
+ });
1188
+ mountedRef.current = mounted;
1189
+ } catch (err) {
1190
+ if (!cancelled) {
1191
+ setError(err instanceof Error ? err.message : "Failed to render JSX");
1192
+ }
1193
+ } finally {
1194
+ if (!cancelled) {
1195
+ setLoading(false);
1196
+ }
1197
+ }
1198
+ }
1199
+ compileAndMount();
1200
+ return () => {
1201
+ cancelled = true;
1202
+ if (mountedRef.current && compiler) {
1203
+ compiler.unmount(mountedRef.current);
1204
+ mountedRef.current = null;
1205
+ }
1206
+ };
1207
+ }, [code, compiler, enabled, services]);
1208
+ return { containerRef, loading, error };
1209
+ }
1210
+ function CodePreview({ code: originalCode, compiler, services, filePath }) {
1211
+ const [isEditing, setIsEditing] = useState(false);
1212
+ const [showPreview, setShowPreview] = useState(true);
1213
+ const [currentCode, setCurrentCode] = useState(originalCode);
1214
+ const [editCount, setEditCount] = useState(0);
1215
+ const [saveStatus, setSaveStatus] = useState("unsaved");
1216
+ const fallbackId = useMemo(() => crypto.randomUUID(), []);
1217
+ const getProjectId = useCallback(async () => {
1218
+ if (filePath) {
1219
+ const config = await getVFSConfig();
1220
+ if (config.usePaths) {
1221
+ const parts = filePath.split("/");
1222
+ if (parts.length > 1) {
1223
+ return parts.slice(0, -1).join("/");
1224
+ }
1225
+ return filePath.replace(/\.[^.]+$/, "");
1226
+ }
1227
+ }
1228
+ return fallbackId;
1229
+ }, [filePath, fallbackId]);
1230
+ const getEntryFile = useCallback(() => {
1231
+ if (filePath) {
1232
+ const parts = filePath.split("/");
1233
+ return parts[parts.length - 1] || "main.tsx";
1234
+ }
1235
+ return "main.tsx";
1236
+ }, [filePath]);
1237
+ const handleSave = useCallback(async () => {
1238
+ setSaveStatus("saving");
1239
+ try {
1240
+ const projectId = await getProjectId();
1241
+ const entryFile = getEntryFile();
1242
+ const project = createSingleFileProject(currentCode, entryFile, projectId);
1243
+ await saveProject(project);
1244
+ setSaveStatus("saved");
1245
+ } catch (err) {
1246
+ console.warn("[VFS] Failed to save project:", err);
1247
+ setSaveStatus("error");
1248
+ }
1249
+ }, [currentCode, getProjectId, getEntryFile]);
1250
+ const { containerRef, loading, error } = useCodeCompiler(
1251
+ compiler,
1252
+ currentCode,
1253
+ showPreview && !isEditing,
1254
+ services
1255
+ );
1256
+ const compile = useCallback(
1257
+ async (code) => {
1258
+ if (!compiler) return { success: true };
1259
+ const errors = [];
1260
+ const originalError = console.error;
1261
+ console.error = (...args) => {
1262
+ errors.push(args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
1263
+ originalError.apply(console, args);
1264
+ };
1265
+ try {
1266
+ await compiler.compile(
1267
+ code,
1268
+ createManifest(services),
1269
+ { typescript: true }
1270
+ );
1271
+ return { success: true };
1272
+ } catch (err) {
1273
+ const errorMessage = err instanceof Error ? err.message : "Compilation failed";
1274
+ const consoleErrors = errors.length > 0 ? `
1275
+
1276
+ Console errors:
1277
+ ${errors.join("\n")}` : "";
1278
+ return {
1279
+ success: false,
1280
+ error: errorMessage + consoleErrors
1281
+ };
1282
+ } finally {
1283
+ console.error = originalError;
1284
+ }
1285
+ },
1286
+ [compiler, services]
1287
+ );
1288
+ const handleRevert = () => {
1289
+ setCurrentCode(originalCode);
1290
+ setEditCount(0);
1291
+ };
1292
+ const hasChanges = currentCode !== originalCode;
1293
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1294
+ /* @__PURE__ */ jsxs("div", { className: "my-3 border rounded-lg", children: [
1295
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b rounded-t-lg", children: [
1296
+ /* @__PURE__ */ jsx(Code, { className: "h-4 w-4 text-muted-foreground" }),
1297
+ editCount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
1298
+ /* @__PURE__ */ jsx(MessageSquare, { className: "h-3 w-3" }),
1299
+ editCount,
1300
+ " edit",
1301
+ editCount !== 1 ? "s" : ""
1302
+ ] }),
1303
+ /* @__PURE__ */ jsx(
1304
+ "button",
1305
+ {
1306
+ onClick: handleSave,
1307
+ disabled: saveStatus === "saving",
1308
+ className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${saveStatus === "saved" ? "text-green-600" : saveStatus === "error" ? "text-destructive hover:bg-muted" : "text-muted-foreground hover:bg-muted"}`,
1309
+ title: saveStatus === "saved" ? "Saved to disk" : saveStatus === "saving" ? "Saving..." : saveStatus === "error" ? "Save failed - click to retry" : "Click to save",
1310
+ children: saveStatus === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs("span", { className: "relative", children: [
1311
+ /* @__PURE__ */ jsx(Cloud, { className: "h-3 w-3" }),
1312
+ saveStatus === "saved" && /* @__PURE__ */ jsx(Check, { className: "h-2 w-2 absolute -bottom-0.5 -right-0.5 stroke-[3]" })
1313
+ ] })
1314
+ }
1315
+ ),
1316
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-1", children: [
1317
+ hasChanges && /* @__PURE__ */ jsx(
1318
+ "button",
1319
+ {
1320
+ onClick: handleRevert,
1321
+ className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted text-muted-foreground",
1322
+ title: "Revert to original",
1323
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "h-3 w-3" })
1324
+ }
1325
+ ),
1326
+ /* @__PURE__ */ jsx(
1327
+ "button",
1328
+ {
1329
+ onClick: () => setIsEditing(true),
1330
+ className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted",
1331
+ title: "Edit component",
1332
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
1333
+ }
1334
+ ),
1335
+ /* @__PURE__ */ jsxs(
1336
+ "button",
1337
+ {
1338
+ onClick: () => setShowPreview(!showPreview),
1339
+ className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
1340
+ children: [
1341
+ showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
1342
+ showPreview ? "Preview" : "Code"
1343
+ ]
1344
+ }
1345
+ )
1346
+ ] })
1347
+ ] }),
1348
+ showPreview ? /* @__PURE__ */ jsxs("div", { className: "bg-white", children: [
1349
+ error ? /* @__PURE__ */ jsxs("div", { className: "p-3 text-sm text-destructive flex items-center gap-2", children: [
1350
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
1351
+ /* @__PURE__ */ jsx("span", { children: error })
1352
+ ] }) : loading ? /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
1353
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
1354
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
1355
+ ] }) : !compiler ? /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }) : null,
1356
+ /* @__PURE__ */ jsx("div", { ref: containerRef })
1357
+ ] }) : /* @__PURE__ */ jsx("div", { className: "p-3 bg-muted/30 overflow-auto max-h-96", children: /* @__PURE__ */ jsx("pre", { className: "text-xs whitespace-pre-wrap break-words m-0", children: /* @__PURE__ */ jsx("code", { children: currentCode }) }) })
1358
+ ] }),
1359
+ /* @__PURE__ */ jsx(
1360
+ EditModal,
1361
+ {
1362
+ isOpen: isEditing,
1363
+ onClose: (finalCode, edits) => {
1364
+ setCurrentCode(finalCode);
1365
+ setEditCount((prev) => prev + edits);
1366
+ setIsEditing(false);
1367
+ if (edits > 0) {
1368
+ setSaveStatus("saving");
1369
+ (async () => {
1370
+ try {
1371
+ const projectId = await getProjectId();
1372
+ const entryFile = getEntryFile();
1373
+ const project = createSingleFileProject(finalCode, entryFile, projectId);
1374
+ await saveProject(project);
1375
+ setSaveStatus("saved");
1376
+ } catch (err) {
1377
+ console.warn("[VFS] Failed to save project:", err);
1378
+ setSaveStatus("error");
1379
+ }
1380
+ })();
1381
+ }
1382
+ },
1383
+ originalCode: currentCode,
1384
+ compile,
1385
+ renderPreview: (code) => /* @__PURE__ */ jsx(ModalPreview, { code, compiler, services })
1386
+ }
1387
+ )
1388
+ ] });
1389
+ }
1390
+ function ModalPreview({
1391
+ code,
1392
+ compiler,
1393
+ services
1394
+ }) {
1395
+ const { containerRef, loading, error } = useCodeCompiler(compiler, code, true, services);
1396
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1397
+ error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2", children: [
1398
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
1399
+ /* @__PURE__ */ jsx("span", { children: error })
1400
+ ] }),
1401
+ loading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
1402
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
1403
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
1404
+ ] }),
1405
+ !compiler && !loading && !error && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Compiler not initialized" }),
1406
+ /* @__PURE__ */ jsx("div", { ref: containerRef })
1407
+ ] });
1408
+ }
1409
+ function DefaultBadge({ children, className = "" }) {
1410
+ return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`, children });
1411
+ }
1412
+ function DefaultDialog({ children, open, onOpenChange }) {
1413
+ if (!open) return null;
1414
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 bg-black/50", onClick: () => onOpenChange?.(false), children: /* @__PURE__ */ jsx("div", { className: "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg", onClick: (e) => e.stopPropagation(), children }) });
1415
+ }
1416
+ function ServicesInspector({
1417
+ namespaces,
1418
+ services = [],
1419
+ BadgeComponent = DefaultBadge,
1420
+ DialogComponent = DefaultDialog
1421
+ }) {
1422
+ const [open, setOpen] = useState(false);
1423
+ if (namespaces.length === 0) return null;
1424
+ const groupedServices = services.reduce((acc, svc) => {
1425
+ (acc[svc.namespace] ??= []).push(svc);
1426
+ return acc;
1427
+ }, {});
1428
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1429
+ /* @__PURE__ */ jsxs(
1430
+ "button",
1431
+ {
1432
+ onClick: () => setOpen(true),
1433
+ className: "flex items-center gap-2 hover:opacity-80 transition-opacity",
1434
+ children: [
1435
+ /* @__PURE__ */ jsx(Server, { className: "h-4 w-4 text-muted-foreground" }),
1436
+ /* @__PURE__ */ jsxs(BadgeComponent, { className: "text-xs", children: [
1437
+ namespaces.length,
1438
+ " service",
1439
+ namespaces.length !== 1 ? "s" : ""
1440
+ ] })
1441
+ ]
1442
+ }
1443
+ ),
1444
+ /* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
1445
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center mb-4", children: [
1446
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Available Services" }),
1447
+ /* @__PURE__ */ jsx("button", { onClick: () => setOpen(false), className: "text-muted-foreground hover:text-foreground", children: "\xD7" })
1448
+ ] }),
1449
+ /* @__PURE__ */ jsx("div", { className: "space-y-3 max-h-96 overflow-auto", children: namespaces.map((ns) => /* @__PURE__ */ jsxs("details", { open: namespaces.length === 1, children: [
1450
+ /* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full p-2 rounded bg-muted/50 hover:bg-muted transition-colors cursor-pointer", children: [
1451
+ /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
1452
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ns }),
1453
+ groupedServices[ns] && /* @__PURE__ */ jsxs(BadgeComponent, { className: "ml-auto text-xs", children: [
1454
+ groupedServices[ns].length,
1455
+ " tool",
1456
+ groupedServices[ns].length !== 1 ? "s" : ""
1457
+ ] })
1458
+ ] }),
1459
+ /* @__PURE__ */ jsx("div", { className: "ml-6 mt-2 space-y-2", children: groupedServices[ns]?.map((svc) => /* @__PURE__ */ jsxs("details", { children: [
1460
+ /* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full text-left text-sm hover:text-foreground text-muted-foreground transition-colors cursor-pointer", children: [
1461
+ /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
1462
+ /* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: svc.procedure }),
1463
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs opacity-70", children: svc.description })
1464
+ ] }),
1465
+ /* @__PURE__ */ jsx("div", { className: "ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48", children: /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0", children: JSON.stringify(svc.parameters.jsonSchema, null, 2) }) })
1466
+ ] }, svc.name)) ?? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No tool details available" }) })
1467
+ ] }, ns)) })
1468
+ ] })
1469
+ ] });
1470
+ }
1471
+ var CODE_BLOCK_REGEX = /```([a-zA-Z0-9_+-]*)((?:\s+[a-zA-Z_][\w-]*="[^"]*")*)\s*\n([\s\S]*?)```/g;
1472
+ var UNCLOSED_BLOCK_REGEX = /```([a-zA-Z0-9_+-]*)((?:\s+[a-zA-Z_][\w-]*="[^"]*")*)\s*\n([\s\S]*)$/;
1473
+ var ATTRIBUTE_REGEX2 = /([a-zA-Z_][\w-]*)="([^"]*)"/g;
1474
+ function parseAttributes(attrString) {
1475
+ const attrs = {};
1476
+ if (!attrString) return attrs;
1477
+ const regex = new RegExp(ATTRIBUTE_REGEX2.source, "g");
1478
+ let match;
1479
+ while ((match = regex.exec(attrString)) !== null) {
1480
+ const key = match[1];
1481
+ const value = match[2];
1482
+ if (key && value !== void 0) {
1483
+ attrs[key] = value;
1484
+ }
1485
+ }
1486
+ return attrs;
1487
+ }
1488
+ function extractCodeBlocks(text, options = {}) {
1489
+ const { filterLanguages, includeUnclosed = false } = options;
1490
+ const parts = [];
1491
+ let lastIndex = 0;
1492
+ const allMatches = [];
1493
+ const regex = new RegExp(CODE_BLOCK_REGEX.source, "g");
1494
+ let match;
1495
+ while ((match = regex.exec(text)) !== null) {
1496
+ const language = match[1]?.toLowerCase() || "";
1497
+ const attributes = parseAttributes(match[2] || "");
1498
+ const content = match[3] ?? "";
1499
+ const included = !filterLanguages || filterLanguages.has(language);
1500
+ allMatches.push({ match, language, content, attributes, included });
1501
+ }
1502
+ for (const { match: match2, language, content, attributes, included } of allMatches) {
1503
+ if (match2.index > lastIndex) {
1504
+ const textBefore = text.slice(lastIndex, match2.index);
1505
+ if (textBefore.trim()) {
1506
+ parts.push({ type: "text", content: textBefore });
1507
+ }
1508
+ }
1509
+ lastIndex = match2.index + match2[0].length;
1510
+ if (included) {
1511
+ parts.push({ type: "code", content, language, attributes });
1512
+ }
1513
+ }
1514
+ const remainingText = text.slice(lastIndex);
1515
+ if (includeUnclosed && remainingText.includes("```")) {
1516
+ const unclosedMatch = remainingText.match(UNCLOSED_BLOCK_REGEX);
1517
+ if (unclosedMatch) {
1518
+ const language = unclosedMatch[1]?.toLowerCase() || "";
1519
+ const attributes = parseAttributes(unclosedMatch[2] || "");
1520
+ const content = unclosedMatch[3] ?? "";
1521
+ const included = !filterLanguages || filterLanguages.has(language);
1522
+ const unclosedIndex = remainingText.indexOf("```");
1523
+ if (unclosedIndex > 0) {
1524
+ const textBefore = remainingText.slice(0, unclosedIndex);
1525
+ if (textBefore.trim()) {
1526
+ parts.push({ type: "text", content: textBefore });
1527
+ }
1528
+ }
1529
+ if (included) {
1530
+ parts.push({ type: "code", content, language, attributes });
1531
+ }
1532
+ lastIndex = text.length;
1533
+ }
1534
+ }
1535
+ if (lastIndex < text.length) {
1536
+ const remaining = text.slice(lastIndex);
1537
+ if (remaining.trim()) {
1538
+ parts.push({ type: "text", content: remaining });
1539
+ }
1540
+ }
1541
+ if (parts.length === 0) {
1542
+ parts.push({ type: "text", content: text });
1543
+ }
1544
+ return parts;
1545
+ }
1546
+ function findFirstCodeBlock(text) {
1547
+ const parts = extractCodeBlocks(text);
1548
+ return parts.find((p) => p.type === "code") ?? null;
1549
+ }
1550
+ function hasCodeBlock(text) {
1551
+ return findFirstCodeBlock(text) !== null;
1552
+ }
1553
+ function getCodeBlockLanguages(text) {
1554
+ const parts = extractCodeBlocks(text);
1555
+ const languages = /* @__PURE__ */ new Set();
1556
+ for (const part of parts) {
1557
+ if (part.type === "code") {
1558
+ languages.add(part.language);
1559
+ }
1560
+ }
1561
+ return languages;
1562
+ }
1563
+ function extractProject(text, options) {
1564
+ const parts = extractCodeBlocks(text, options);
1565
+ const files = [];
1566
+ const textParts = [];
1567
+ for (const part of parts) {
1568
+ if (part.type === "text") {
1569
+ textParts.push(part);
1570
+ } else if (part.type === "code") {
1571
+ const codePart = part;
1572
+ if (codePart.attributes?.path) {
1573
+ files.push({
1574
+ path: codePart.attributes.path,
1575
+ content: codePart.content,
1576
+ language: codePart.language,
1577
+ note: codePart.attributes.note
1578
+ });
1579
+ } else {
1580
+ files.push({
1581
+ path: detectMainFile(codePart.language),
1582
+ content: codePart.content,
1583
+ language: codePart.language
1584
+ });
1585
+ }
1586
+ }
1587
+ }
1588
+ return {
1589
+ project: createProjectFromFiles(files),
1590
+ textParts
1591
+ };
1592
+ }
1593
+ function cn(...inputs) {
1594
+ return twMerge(clsx(inputs));
1595
+ }
1596
+
1597
+ export { CodeBlockExtension, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, ServicesInspector, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFiles, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isVFSAvailable, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };