@aprovan/patchwork-editor 0.1.2-dev.ba8f277 → 0.1.2-dev.c1ac1dc

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.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/components/CodePreview.d.ts +2 -3
  3. package/dist/components/MarkdownEditor.d.ts +1 -1
  4. package/dist/components/MarkdownPreview.d.ts +1 -1
  5. package/dist/components/SaveStatusButton.d.ts +1 -1
  6. package/dist/components/ServicesInspector.d.ts +1 -1
  7. package/dist/components/WidgetPreview.d.ts +1 -1
  8. package/dist/components/edit/CodeBlockView.d.ts +1 -1
  9. package/dist/components/edit/EditHistory.d.ts +1 -1
  10. package/dist/components/edit/EditModal.d.ts +1 -1
  11. package/dist/components/edit/FileTree.d.ts +1 -1
  12. package/dist/components/edit/MediaPreview.d.ts +1 -1
  13. package/dist/components/edit/SaveConfirmDialog.d.ts +1 -1
  14. package/dist/components/edit/api.d.ts +1 -1
  15. package/dist/components/edit/fileTypes.d.ts +1 -1
  16. package/dist/components/edit/useEditSession.d.ts +1 -1
  17. package/dist/index.js +287 -267
  18. package/package.json +3 -3
  19. package/src/components/CodePreview.tsx +5 -6
  20. package/src/components/MarkdownEditor.tsx +3 -3
  21. package/src/components/MarkdownPreview.tsx +3 -3
  22. package/src/components/ServicesInspector.tsx +2 -2
  23. package/src/components/WidgetPreview.tsx +1 -1
  24. package/src/components/edit/EditHistory.tsx +1 -1
  25. package/src/components/edit/EditModal.tsx +6 -6
  26. package/src/components/edit/FileTree.tsx +2 -3
  27. package/src/components/edit/MediaPreview.tsx +1 -1
  28. package/src/components/edit/api.ts +8 -8
  29. package/src/components/edit/fileTypes.ts +75 -55
  30. package/src/components/edit/useEditSession.ts +2 -2
  31. package/src/lib/code-extractor.ts +1 -1
@@ -9,5 +9,5 @@
9
9
  CLI Target: es2022
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
- ESM dist/index.js 104.90 KB
13
- ESM ⚡️ Build success in 261ms
12
+ ESM dist/index.js 104.93 KB
13
+ ESM ⚡️ Build success in 450ms
@@ -1,5 +1,4 @@
1
- import type { Compiler } from '@aprovan/patchwork-compiler';
2
- import type { VirtualProject } from '@aprovan/patchwork-compiler';
1
+ import type { Compiler, VirtualProject } from '@aprovan/patchwork-compiler';
3
2
  interface CodePreviewProps {
4
3
  code: string;
5
4
  compiler: Compiler | null;
@@ -18,5 +17,5 @@ interface CodePreviewProps {
18
17
  initialProject: VirtualProject;
19
18
  }) => void;
20
19
  }
21
- export declare function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint, onOpenEditSession, }: CodePreviewProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint, onOpenEditSession, }: CodePreviewProps): import("react").JSX.Element;
22
21
  export {};
@@ -6,5 +6,5 @@ interface MarkdownEditorProps {
6
6
  disabled?: boolean;
7
7
  className?: string;
8
8
  }
9
- export declare function MarkdownEditor({ value, onChange, onSubmit, placeholder, disabled, className, }: MarkdownEditorProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function MarkdownEditor({ value, onChange, onSubmit, placeholder, disabled, className, }: MarkdownEditorProps): import("react").JSX.Element;
10
10
  export {};
@@ -4,5 +4,5 @@ interface MarkdownPreviewProps {
4
4
  editable?: boolean;
5
5
  className?: string;
6
6
  }
7
- export declare function MarkdownPreview({ value, onChange, editable, className, }: MarkdownPreviewProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function MarkdownPreview({ value, onChange, editable, className, }: MarkdownPreviewProps): import("react").JSX.Element;
8
8
  export {};
@@ -5,5 +5,5 @@ interface SaveStatusButtonProps {
5
5
  disabled?: boolean;
6
6
  tone: 'muted' | 'primary';
7
7
  }
8
- export declare function SaveStatusButton({ status, onClick, disabled, tone, }: SaveStatusButtonProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function SaveStatusButton({ status, onClick, disabled, tone, }: SaveStatusButtonProps): import("react").JSX.Element;
9
9
  export {};
@@ -44,5 +44,5 @@ interface ServicesInspectorProps {
44
44
  onClose?: () => void;
45
45
  }>;
46
46
  }
47
- export declare function ServicesInspector({ namespaces, services, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogContentComponent, DialogCloseComponent, }: ServicesInspectorProps): import("react/jsx-runtime").JSX.Element;
47
+ export declare function ServicesInspector({ namespaces, services, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogContentComponent, DialogCloseComponent, }: ServicesInspectorProps): import("react").JSX.Element;
48
48
  export {};
@@ -5,4 +5,4 @@ export interface WidgetPreviewProps {
5
5
  services?: string[];
6
6
  enabled?: boolean;
7
7
  }
8
- export declare function WidgetPreview({ code, compiler, services, enabled, }: WidgetPreviewProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function WidgetPreview({ code, compiler, services, enabled, }: WidgetPreviewProps): import("react").JSX.Element;
@@ -4,4 +4,4 @@ export interface CodeBlockViewProps {
4
4
  editable?: boolean;
5
5
  onChange?: (content: string) => void;
6
6
  }
7
- export declare function CodeBlockView({ content, language, editable, onChange }: CodeBlockViewProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function CodeBlockView({ content, language, editable, onChange }: CodeBlockViewProps): import("react").JSX.Element;
@@ -6,5 +6,5 @@ interface EditHistoryProps {
6
6
  pendingPrompt?: string | null;
7
7
  className?: string;
8
8
  }
9
- export declare function EditHistory({ entries, streamingNotes, isStreaming, pendingPrompt, className, }: EditHistoryProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function EditHistory({ entries, streamingNotes, isStreaming, pendingPrompt, className, }: EditHistoryProps): import("react").JSX.Element;
10
10
  export {};
@@ -18,4 +18,4 @@ export interface EditModalProps extends UseEditSessionOptions {
18
18
  previewError?: string | null;
19
19
  previewLoading?: boolean;
20
20
  }
21
- export declare function EditModal({ isOpen, onClose, onSave, onSaveProject, renderPreview, renderLoading, renderError, previewError, previewLoading, initialTreePath, initialState, hideFileTree, ...sessionOptions }: EditModalProps): import("react/jsx-runtime").JSX.Element;
21
+ export declare function EditModal({ isOpen, onClose, onSave, onSaveProject, renderPreview, renderLoading, renderError, previewError, previewLoading, initialTreePath, initialState, hideFileTree, ...sessionOptions }: EditModalProps): import("react").JSX.Element;
@@ -24,4 +24,4 @@ export interface FileTreeProps {
24
24
  pageSize?: number;
25
25
  reloadToken?: number;
26
26
  }
27
- export declare function FileTree({ files, activeFile, activePath, title, onSelectFile, onSelectDirectory, onReplaceFile, onOpenInEditor, openInEditorMode, openInEditorIcon, openInEditorTitle, pinnedPaths, onTogglePin, directoryLoader, pageSize, reloadToken, }: FileTreeProps): import("react/jsx-runtime").JSX.Element;
27
+ export declare function FileTree({ files, activeFile, activePath, title, onSelectFile, onSelectDirectory, onReplaceFile, onOpenInEditor, openInEditorMode, openInEditorIcon, openInEditorTitle, pinnedPaths, onTogglePin, directoryLoader, pageSize, reloadToken, }: FileTreeProps): import("react").JSX.Element;
@@ -3,4 +3,4 @@ export interface MediaPreviewProps {
3
3
  mimeType: string;
4
4
  fileName: string;
5
5
  }
6
- export declare function MediaPreview({ content, mimeType, fileName }: MediaPreviewProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function MediaPreview({ content, mimeType, fileName }: MediaPreviewProps): import("react").JSX.Element;
@@ -6,4 +6,4 @@ export interface SaveConfirmDialogProps {
6
6
  onDiscard: () => void;
7
7
  onCancel: () => void;
8
8
  }
9
- export declare function SaveConfirmDialog({ isOpen, isSaving, error, onSave, onDiscard, onCancel, }: SaveConfirmDialogProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function SaveConfirmDialog({ isOpen, isSaving, error, onSave, onDiscard, onCancel, }: SaveConfirmDialogProps): import("react").JSX.Element;
@@ -1,4 +1,4 @@
1
- import type { EditRequest, EditResponse } from './types';
1
+ import type { EditRequest, EditResponse } from "./types";
2
2
  export interface EditApiOptions {
3
3
  endpoint?: string;
4
4
  onProgress?: (note: string) => void;
@@ -1,4 +1,4 @@
1
- export type FileCategory = 'compilable' | 'text' | 'media' | 'binary';
1
+ export type FileCategory = "compilable" | "text" | "media" | "binary";
2
2
  export interface FileTypeInfo {
3
3
  category: FileCategory;
4
4
  language: string | null;
@@ -1,5 +1,5 @@
1
- import type { VirtualProject } from "@aprovan/patchwork-compiler";
2
1
  import type { EditSessionState, EditSessionActions, CompileFn } from "./types";
2
+ import type { VirtualProject } from "@aprovan/patchwork-compiler";
3
3
  export interface UseEditSessionOptions {
4
4
  originalCode?: string;
5
5
  originalProject?: VirtualProject;
package/dist/index.js CHANGED
@@ -2,17 +2,17 @@ import { Node, textblockTypeInputRule } from '@tiptap/core';
2
2
  import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent, useEditor, EditorContent } from '@tiptap/react';
3
3
  import { useRef, useCallback, useMemo, useState, useEffect } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { HttpBackend, VFSStore, createSingleFileProject, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
5
6
  import { Folder, Pin, X, Loader2, AlertCircle, FileImage, FileVideo, Pencil, RotateCcw, FileCode, FolderTree, Eye, Code, Send, MessageSquare, Server, ChevronDown, File, PinOff, ChevronRight, ChevronsDown, Upload, AlertTriangle, Save } from 'lucide-react';
6
- import { createSingleFileProject, HttpBackend, VFSStore, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
7
7
  import Markdown from 'react-markdown';
8
8
  import remarkGfm from 'remark-gfm';
9
- import StarterKit from '@tiptap/starter-kit';
9
+ import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
10
10
  import Placeholder from '@tiptap/extension-placeholder';
11
11
  import Typography from '@tiptap/extension-typography';
12
- import { Markdown as Markdown$1 } from 'tiptap-markdown';
13
12
  import { TextSelection } from '@tiptap/pm/state';
13
+ import StarterKit from '@tiptap/starter-kit';
14
+ import { Markdown as Markdown$1 } from 'tiptap-markdown';
14
15
  import { createHighlighter } from 'shiki';
15
- import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
16
16
  import { clsx } from 'clsx';
17
17
  import { twMerge } from 'tailwind-merge';
18
18
 
@@ -180,6 +180,69 @@ var CodeBlockExtension = Node.create({
180
180
  };
181
181
  }
182
182
  });
183
+ var VFS_BASE_URL = "/vfs";
184
+ var vfsConfigCache = null;
185
+ async function getVFSConfig() {
186
+ if (vfsConfigCache) return vfsConfigCache;
187
+ try {
188
+ const res = await fetch(`${VFS_BASE_URL}/config`);
189
+ if (res.ok) {
190
+ vfsConfigCache = await res.json();
191
+ return vfsConfigCache;
192
+ }
193
+ } catch {
194
+ }
195
+ return { usePaths: false };
196
+ }
197
+ var storeInstance = null;
198
+ function getVFSStore() {
199
+ if (!storeInstance) {
200
+ const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
201
+ storeInstance = new VFSStore(provider, {
202
+ sync: true,
203
+ conflictStrategy: "local-wins"
204
+ });
205
+ }
206
+ return storeInstance;
207
+ }
208
+ async function saveProject(project) {
209
+ const store = getVFSStore();
210
+ await store.saveProject(project);
211
+ }
212
+ async function loadProject(id) {
213
+ const store = getVFSStore();
214
+ return store.loadProject(id);
215
+ }
216
+ async function listProjects() {
217
+ const store = getVFSStore();
218
+ const files = await store.listFiles();
219
+ const projectIds = /* @__PURE__ */ new Set();
220
+ for (const file of files) {
221
+ const id = file.split("/")[0];
222
+ if (id) projectIds.add(id);
223
+ }
224
+ return Array.from(projectIds);
225
+ }
226
+ async function saveFile(path, content) {
227
+ const store = getVFSStore();
228
+ await store.writeFile(path, content);
229
+ }
230
+ async function loadFile(path, encoding) {
231
+ const store = getVFSStore();
232
+ return store.readFile(path, encoding);
233
+ }
234
+ function subscribeToChanges(callback) {
235
+ const store = getVFSStore();
236
+ return store.on("change", callback);
237
+ }
238
+ async function isVFSAvailable() {
239
+ try {
240
+ const res = await fetch(VFS_BASE_URL, { method: "HEAD" });
241
+ return res.ok || res.status === 404;
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
183
246
 
184
247
  // src/components/edit/types.ts
185
248
  function getActiveContent(state) {
@@ -1010,11 +1073,179 @@ function SaveStatusButton({
1010
1073
  }
1011
1074
  );
1012
1075
  }
1076
+ var highlighterPromise = null;
1077
+ var COMMON_LANGUAGES = [
1078
+ "typescript",
1079
+ "javascript",
1080
+ "tsx",
1081
+ "jsx",
1082
+ "json",
1083
+ "html",
1084
+ "css",
1085
+ "markdown",
1086
+ "yaml",
1087
+ "python",
1088
+ "bash",
1089
+ "sql"
1090
+ ];
1091
+ function getHighlighter() {
1092
+ if (!highlighterPromise) {
1093
+ highlighterPromise = createHighlighter({
1094
+ themes: ["github-light"],
1095
+ langs: COMMON_LANGUAGES
1096
+ });
1097
+ }
1098
+ return highlighterPromise;
1099
+ }
1100
+ function normalizeLanguage(lang) {
1101
+ if (!lang) return "typescript";
1102
+ const normalized = lang.toLowerCase();
1103
+ const mapping = {
1104
+ ts: "typescript",
1105
+ tsx: "tsx",
1106
+ js: "javascript",
1107
+ jsx: "jsx",
1108
+ json: "json",
1109
+ html: "html",
1110
+ css: "css",
1111
+ md: "markdown",
1112
+ markdown: "markdown",
1113
+ yml: "yaml",
1114
+ yaml: "yaml",
1115
+ py: "python",
1116
+ python: "python",
1117
+ sh: "bash",
1118
+ bash: "bash",
1119
+ sql: "sql",
1120
+ typescript: "typescript",
1121
+ javascript: "javascript"
1122
+ };
1123
+ return mapping[normalized] || "typescript";
1124
+ }
1125
+ function CodeBlockView({ content, language, editable = false, onChange }) {
1126
+ const textareaRef = useRef(null);
1127
+ const containerRef = useRef(null);
1128
+ const [highlighter, setHighlighter] = useState(null);
1129
+ useEffect(() => {
1130
+ let mounted = true;
1131
+ getHighlighter().then((h) => {
1132
+ if (mounted) setHighlighter(h);
1133
+ });
1134
+ return () => {
1135
+ mounted = false;
1136
+ };
1137
+ }, []);
1138
+ useEffect(() => {
1139
+ if (textareaRef.current) {
1140
+ textareaRef.current.style.height = "auto";
1141
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
1142
+ }
1143
+ }, [content]);
1144
+ const handleChange = useCallback(
1145
+ (e) => {
1146
+ onChange?.(e.target.value);
1147
+ },
1148
+ [onChange]
1149
+ );
1150
+ const handleKeyDown = useCallback(
1151
+ (e) => {
1152
+ if (e.key === "Tab") {
1153
+ e.preventDefault();
1154
+ const target = e.target;
1155
+ const start = target.selectionStart;
1156
+ const end = target.selectionEnd;
1157
+ const value = target.value;
1158
+ const newValue = value.substring(0, start) + " " + value.substring(end);
1159
+ onChange?.(newValue);
1160
+ requestAnimationFrame(() => {
1161
+ target.selectionStart = target.selectionEnd = start + 2;
1162
+ });
1163
+ }
1164
+ },
1165
+ [onChange]
1166
+ );
1167
+ const langLabel = language || "text";
1168
+ const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
1169
+ const highlightedHtml = useMemo(() => {
1170
+ if (!highlighter) return null;
1171
+ try {
1172
+ return highlighter.codeToHtml(content, {
1173
+ lang: shikiLang,
1174
+ theme: "github-light"
1175
+ });
1176
+ } catch {
1177
+ return null;
1178
+ }
1179
+ }, [highlighter, content, shikiLang]);
1180
+ return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-[#ffffff]", children: [
1181
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-[#f6f8fa] border-b border-[#d0d7de] text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-[#57606a]", children: langLabel }) }),
1182
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: editable ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-full", children: [
1183
+ /* @__PURE__ */ jsx(
1184
+ "div",
1185
+ {
1186
+ ref: containerRef,
1187
+ className: "absolute top-0 left-0 right-0 pointer-events-none p-4",
1188
+ "aria-hidden": "true",
1189
+ children: highlightedHtml ? /* @__PURE__ */ jsx(
1190
+ "div",
1191
+ {
1192
+ className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1193
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1194
+ }
1195
+ ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words text-[#24292f] m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
1196
+ }
1197
+ ),
1198
+ /* @__PURE__ */ jsx(
1199
+ "textarea",
1200
+ {
1201
+ ref: textareaRef,
1202
+ value: content,
1203
+ onChange: handleChange,
1204
+ onKeyDown: handleKeyDown,
1205
+ className: "relative w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none p-4 text-transparent whitespace-pre-wrap break-words",
1206
+ spellCheck: false,
1207
+ style: {
1208
+ tabSize: 2,
1209
+ caretColor: "#24292f",
1210
+ wordBreak: "break-word",
1211
+ overflowWrap: "break-word"
1212
+ }
1213
+ }
1214
+ )
1215
+ ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: highlightedHtml ? /* @__PURE__ */ jsx(
1216
+ "div",
1217
+ {
1218
+ className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1219
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1220
+ }
1221
+ ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed text-[#24292f]", children: /* @__PURE__ */ jsx("code", { children: content }) }) }) })
1222
+ ] });
1223
+ }
1013
1224
 
1014
1225
  // src/components/edit/fileTypes.ts
1015
1226
  var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
1016
- var MEDIA_EXTENSIONS = [".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp4", ".mov", ".webm"];
1017
- var TEXT_EXTENSIONS = [".json", ".yaml", ".yml", ".md", ".txt", ".css", ".html", ".xml", ".toml"];
1227
+ var MEDIA_EXTENSIONS = [
1228
+ ".svg",
1229
+ ".png",
1230
+ ".jpg",
1231
+ ".jpeg",
1232
+ ".gif",
1233
+ ".webp",
1234
+ ".mp4",
1235
+ ".mov",
1236
+ ".webm"
1237
+ ];
1238
+ var TEXT_EXTENSIONS = [
1239
+ ".json",
1240
+ ".yaml",
1241
+ ".yml",
1242
+ ".md",
1243
+ ".txt",
1244
+ ".css",
1245
+ ".html",
1246
+ ".xml",
1247
+ ".toml"
1248
+ ];
1018
1249
  var EXTENSION_TO_LANGUAGE = {
1019
1250
  ".tsx": "tsx",
1020
1251
  ".jsx": "jsx",
@@ -1673,203 +1904,6 @@ function FileTree({
1673
1904
  ) })
1674
1905
  ] });
1675
1906
  }
1676
- function SaveConfirmDialog({
1677
- isOpen,
1678
- isSaving,
1679
- error,
1680
- onSave,
1681
- onDiscard,
1682
- onCancel
1683
- }) {
1684
- if (!isOpen) return null;
1685
- return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-lg shadow-lg w-full max-w-md", children: [
1686
- /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
1687
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: "Unsaved Changes" }),
1688
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: "You have unsaved changes. Would you like to save them before closing?" }),
1689
- error && /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-3", children: [
1690
- "Save failed: ",
1691
- error
1692
- ] })
1693
- ] }),
1694
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 p-6 pt-0", children: [
1695
- /* @__PURE__ */ jsx(
1696
- "button",
1697
- {
1698
- onClick: onCancel,
1699
- disabled: isSaving,
1700
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
1701
- children: "Cancel"
1702
- }
1703
- ),
1704
- /* @__PURE__ */ jsx(
1705
- "button",
1706
- {
1707
- onClick: onDiscard,
1708
- disabled: isSaving,
1709
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background text-destructive hover:bg-destructive/10 disabled:opacity-50",
1710
- children: "Discard"
1711
- }
1712
- ),
1713
- /* @__PURE__ */ jsx(
1714
- "button",
1715
- {
1716
- onClick: onSave,
1717
- disabled: isSaving,
1718
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
1719
- children: isSaving ? "Saving..." : "Save"
1720
- }
1721
- )
1722
- ] })
1723
- ] }) });
1724
- }
1725
- var highlighterPromise = null;
1726
- var COMMON_LANGUAGES = [
1727
- "typescript",
1728
- "javascript",
1729
- "tsx",
1730
- "jsx",
1731
- "json",
1732
- "html",
1733
- "css",
1734
- "markdown",
1735
- "yaml",
1736
- "python",
1737
- "bash",
1738
- "sql"
1739
- ];
1740
- function getHighlighter() {
1741
- if (!highlighterPromise) {
1742
- highlighterPromise = createHighlighter({
1743
- themes: ["github-light"],
1744
- langs: COMMON_LANGUAGES
1745
- });
1746
- }
1747
- return highlighterPromise;
1748
- }
1749
- function normalizeLanguage(lang) {
1750
- if (!lang) return "typescript";
1751
- const normalized = lang.toLowerCase();
1752
- const mapping = {
1753
- ts: "typescript",
1754
- tsx: "tsx",
1755
- js: "javascript",
1756
- jsx: "jsx",
1757
- json: "json",
1758
- html: "html",
1759
- css: "css",
1760
- md: "markdown",
1761
- markdown: "markdown",
1762
- yml: "yaml",
1763
- yaml: "yaml",
1764
- py: "python",
1765
- python: "python",
1766
- sh: "bash",
1767
- bash: "bash",
1768
- sql: "sql",
1769
- typescript: "typescript",
1770
- javascript: "javascript"
1771
- };
1772
- return mapping[normalized] || "typescript";
1773
- }
1774
- function CodeBlockView({ content, language, editable = false, onChange }) {
1775
- const textareaRef = useRef(null);
1776
- const containerRef = useRef(null);
1777
- const [highlighter, setHighlighter] = useState(null);
1778
- useEffect(() => {
1779
- let mounted = true;
1780
- getHighlighter().then((h) => {
1781
- if (mounted) setHighlighter(h);
1782
- });
1783
- return () => {
1784
- mounted = false;
1785
- };
1786
- }, []);
1787
- useEffect(() => {
1788
- if (textareaRef.current) {
1789
- textareaRef.current.style.height = "auto";
1790
- textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
1791
- }
1792
- }, [content]);
1793
- const handleChange = useCallback(
1794
- (e) => {
1795
- onChange?.(e.target.value);
1796
- },
1797
- [onChange]
1798
- );
1799
- const handleKeyDown = useCallback(
1800
- (e) => {
1801
- if (e.key === "Tab") {
1802
- e.preventDefault();
1803
- const target = e.target;
1804
- const start = target.selectionStart;
1805
- const end = target.selectionEnd;
1806
- const value = target.value;
1807
- const newValue = value.substring(0, start) + " " + value.substring(end);
1808
- onChange?.(newValue);
1809
- requestAnimationFrame(() => {
1810
- target.selectionStart = target.selectionEnd = start + 2;
1811
- });
1812
- }
1813
- },
1814
- [onChange]
1815
- );
1816
- const langLabel = language || "text";
1817
- const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
1818
- const highlightedHtml = useMemo(() => {
1819
- if (!highlighter) return null;
1820
- try {
1821
- return highlighter.codeToHtml(content, {
1822
- lang: shikiLang,
1823
- theme: "github-light"
1824
- });
1825
- } catch {
1826
- return null;
1827
- }
1828
- }, [highlighter, content, shikiLang]);
1829
- return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-[#ffffff]", children: [
1830
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-[#f6f8fa] border-b border-[#d0d7de] text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-[#57606a]", children: langLabel }) }),
1831
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: editable ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-full", children: [
1832
- /* @__PURE__ */ jsx(
1833
- "div",
1834
- {
1835
- ref: containerRef,
1836
- className: "absolute top-0 left-0 right-0 pointer-events-none p-4",
1837
- "aria-hidden": "true",
1838
- children: highlightedHtml ? /* @__PURE__ */ jsx(
1839
- "div",
1840
- {
1841
- className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1842
- dangerouslySetInnerHTML: { __html: highlightedHtml }
1843
- }
1844
- ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words text-[#24292f] m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
1845
- }
1846
- ),
1847
- /* @__PURE__ */ jsx(
1848
- "textarea",
1849
- {
1850
- ref: textareaRef,
1851
- value: content,
1852
- onChange: handleChange,
1853
- onKeyDown: handleKeyDown,
1854
- className: "relative w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none p-4 text-transparent whitespace-pre-wrap break-words",
1855
- spellCheck: false,
1856
- style: {
1857
- tabSize: 2,
1858
- caretColor: "#24292f",
1859
- wordBreak: "break-word",
1860
- overflowWrap: "break-word"
1861
- }
1862
- }
1863
- )
1864
- ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: highlightedHtml ? /* @__PURE__ */ jsx(
1865
- "div",
1866
- {
1867
- className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1868
- dangerouslySetInnerHTML: { __html: highlightedHtml }
1869
- }
1870
- ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed text-[#24292f]", children: /* @__PURE__ */ jsx("code", { children: content }) }) }) })
1871
- ] });
1872
- }
1873
1907
  function formatFileSize(bytes) {
1874
1908
  if (bytes < 1024) return `${bytes} B`;
1875
1909
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -1960,6 +1994,55 @@ function MediaPreview({ content, mimeType, fileName }) {
1960
1994
  ] })
1961
1995
  ] });
1962
1996
  }
1997
+ function SaveConfirmDialog({
1998
+ isOpen,
1999
+ isSaving,
2000
+ error,
2001
+ onSave,
2002
+ onDiscard,
2003
+ onCancel
2004
+ }) {
2005
+ if (!isOpen) return null;
2006
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-lg shadow-lg w-full max-w-md", children: [
2007
+ /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
2008
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: "Unsaved Changes" }),
2009
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: "You have unsaved changes. Would you like to save them before closing?" }),
2010
+ error && /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-3", children: [
2011
+ "Save failed: ",
2012
+ error
2013
+ ] })
2014
+ ] }),
2015
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 p-6 pt-0", children: [
2016
+ /* @__PURE__ */ jsx(
2017
+ "button",
2018
+ {
2019
+ onClick: onCancel,
2020
+ disabled: isSaving,
2021
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
2022
+ children: "Cancel"
2023
+ }
2024
+ ),
2025
+ /* @__PURE__ */ jsx(
2026
+ "button",
2027
+ {
2028
+ onClick: onDiscard,
2029
+ disabled: isSaving,
2030
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background text-destructive hover:bg-destructive/10 disabled:opacity-50",
2031
+ children: "Discard"
2032
+ }
2033
+ ),
2034
+ /* @__PURE__ */ jsx(
2035
+ "button",
2036
+ {
2037
+ onClick: onSave,
2038
+ disabled: isSaving,
2039
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
2040
+ children: isSaving ? "Saving..." : "Save"
2041
+ }
2042
+ )
2043
+ ] })
2044
+ ] }) });
2045
+ }
1963
2046
  function hashCode(str) {
1964
2047
  let hash = 0;
1965
2048
  for (let i = 0; i < str.length; i++) {
@@ -2396,69 +2479,6 @@ function WidgetPreview({
2396
2479
  /* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
2397
2480
  ] });
2398
2481
  }
2399
- var VFS_BASE_URL = "/vfs";
2400
- var vfsConfigCache = null;
2401
- async function getVFSConfig() {
2402
- if (vfsConfigCache) return vfsConfigCache;
2403
- try {
2404
- const res = await fetch(`${VFS_BASE_URL}/config`);
2405
- if (res.ok) {
2406
- vfsConfigCache = await res.json();
2407
- return vfsConfigCache;
2408
- }
2409
- } catch {
2410
- }
2411
- return { usePaths: false };
2412
- }
2413
- var storeInstance = null;
2414
- function getVFSStore() {
2415
- if (!storeInstance) {
2416
- const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
2417
- storeInstance = new VFSStore(provider, {
2418
- sync: true,
2419
- conflictStrategy: "local-wins"
2420
- });
2421
- }
2422
- return storeInstance;
2423
- }
2424
- async function saveProject(project) {
2425
- const store = getVFSStore();
2426
- await store.saveProject(project);
2427
- }
2428
- async function loadProject(id) {
2429
- const store = getVFSStore();
2430
- return store.loadProject(id);
2431
- }
2432
- async function listProjects() {
2433
- const store = getVFSStore();
2434
- const files = await store.listFiles();
2435
- const projectIds = /* @__PURE__ */ new Set();
2436
- for (const file of files) {
2437
- const id = file.split("/")[0];
2438
- if (id) projectIds.add(id);
2439
- }
2440
- return Array.from(projectIds);
2441
- }
2442
- async function saveFile(path, content) {
2443
- const store = getVFSStore();
2444
- await store.writeFile(path, content);
2445
- }
2446
- async function loadFile(path, encoding) {
2447
- const store = getVFSStore();
2448
- return store.readFile(path, encoding);
2449
- }
2450
- function subscribeToChanges(callback) {
2451
- const store = getVFSStore();
2452
- return store.on("change", callback);
2453
- }
2454
- async function isVFSAvailable() {
2455
- try {
2456
- const res = await fetch(VFS_BASE_URL, { method: "HEAD" });
2457
- return res.ok || res.status === 404;
2458
- } catch {
2459
- return false;
2460
- }
2461
- }
2462
2482
  function createManifest2(services) {
2463
2483
  return {
2464
2484
  name: "preview",
@@ -2841,7 +2861,7 @@ function ServicesInspector({
2841
2861
  ),
2842
2862
  /* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
2843
2863
  /* @__PURE__ */ jsxs(DialogHeaderComponent, { children: [
2844
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Available Services" }),
2864
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Services" }),
2845
2865
  /* @__PURE__ */ jsx(DialogCloseComponent, { onClose: () => setOpen(false) })
2846
2866
  ] }),
2847
2867
  /* @__PURE__ */ jsx(DialogContentComponent, { className: "space-y-3 max-h-96 overflow-auto", children: namespaces.map((ns) => /* @__PURE__ */ jsxs("details", { open: namespaces.length === 1, children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aprovan/patchwork-editor",
3
- "version": "0.1.2-dev.ba8f277",
3
+ "version": "0.1.2-dev.c1ac1dc",
4
4
  "description": "Components for facilitating widget generation and editing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,8 +25,8 @@
25
25
  "shiki": "^3.22.0",
26
26
  "tailwind-merge": "^3.4.0",
27
27
  "tiptap-markdown": "^0.9.0",
28
- "@aprovan/patchwork-compiler": "0.1.2-dev.ba8f277",
29
- "@aprovan/bobbin": "0.1.0-dev.ba8f277"
28
+ "@aprovan/bobbin": "0.1.0-dev.c1ac1dc",
29
+ "@aprovan/patchwork-compiler": "0.1.2-dev.c1ac1dc"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "^18.0.0 || ^19.0.0",
@@ -1,13 +1,12 @@
1
- import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
2
- import { Code, Eye, Pencil, RotateCcw, MessageSquare } from 'lucide-react';
3
- import type { Compiler, Manifest } from '@aprovan/patchwork-compiler';
4
1
  import { createSingleFileProject } from '@aprovan/patchwork-compiler';
2
+ import { Code, Eye, Pencil, RotateCcw, MessageSquare } from 'lucide-react';
3
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
+ import { saveProject, getVFSConfig, loadFile, subscribeToChanges } from '../lib/vfs';
5
5
  import { EditModal, type CompileFn, CodeBlockView, MediaPreview, getFileType } from './edit';
6
+ import { MarkdownPreview } from './MarkdownPreview';
6
7
  import { SaveStatusButton, type SaveStatus } from './SaveStatusButton';
7
8
  import { WidgetPreview } from './WidgetPreview';
8
- import { MarkdownPreview } from './MarkdownPreview';
9
- import { saveProject, getVFSConfig, loadFile, subscribeToChanges } from '../lib/vfs';
10
- import type { VirtualProject } from '@aprovan/patchwork-compiler';
9
+ import type { Compiler, Manifest , VirtualProject } from '@aprovan/patchwork-compiler';
11
10
 
12
11
  interface CodePreviewProps {
13
12
  code: string;
@@ -1,10 +1,10 @@
1
- import { useEditor, EditorContent } from '@tiptap/react';
2
- import StarterKit from '@tiptap/starter-kit';
3
1
  import Placeholder from '@tiptap/extension-placeholder';
4
2
  import Typography from '@tiptap/extension-typography';
5
- import { Markdown } from 'tiptap-markdown';
6
3
  import { TextSelection } from '@tiptap/pm/state';
4
+ import { useEditor, EditorContent } from '@tiptap/react';
5
+ import StarterKit from '@tiptap/starter-kit';
7
6
  import { useEffect, useCallback, useRef } from 'react';
7
+ import { Markdown } from 'tiptap-markdown';
8
8
  import { CodeBlockExtension } from './CodeBlockExtension';
9
9
 
10
10
  interface MarkdownEditorProps {
@@ -1,14 +1,14 @@
1
+ import Typography from '@tiptap/extension-typography';
1
2
  import { useEditor, EditorContent } from '@tiptap/react';
2
3
  import StarterKit from '@tiptap/starter-kit';
3
- import Typography from '@tiptap/extension-typography';
4
- import { Markdown } from 'tiptap-markdown';
5
4
  import { useEffect, useCallback, useRef, useState } from 'react';
5
+ import { Markdown } from 'tiptap-markdown';
6
6
  import { CodeBlockExtension } from './CodeBlockExtension';
7
7
 
8
8
  function parseFrontmatter(content: string): { frontmatter: string; body: string } {
9
9
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
10
10
  if (!match) return { frontmatter: '', body: content };
11
- return { frontmatter: match[1], body: match[2] };
11
+ return { frontmatter: match[1]!, body: match[2]! };
12
12
  }
13
13
 
14
14
  function assembleFrontmatter(frontmatter: string, body: string): string {
@@ -1,5 +1,5 @@
1
- import { useState } from "react";
2
1
  import { ChevronDown, Server } from "lucide-react";
2
+ import { useState } from "react";
3
3
 
4
4
  export interface ServiceInfo {
5
5
  name: string;
@@ -133,7 +133,7 @@ export function ServicesInspector({
133
133
 
134
134
  <DialogComponent open={open} onOpenChange={setOpen}>
135
135
  <DialogHeaderComponent>
136
- <h2 className="text-lg font-semibold">Available Services</h2>
136
+ <h2 className="text-lg font-semibold">Services</h2>
137
137
  <DialogCloseComponent onClose={() => setOpen(false)} />
138
138
  </DialogHeaderComponent>
139
139
  <DialogContentComponent className="space-y-3 max-h-96 overflow-auto">
@@ -1,5 +1,5 @@
1
- import { useEffect, useRef, useState } from 'react';
2
1
  import { AlertCircle, Loader2 } from 'lucide-react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import type { Compiler, Manifest, MountedWidget } from '@aprovan/patchwork-compiler';
4
4
 
5
5
  export interface WidgetPreviewProps {
@@ -1,5 +1,5 @@
1
- import { useRef, useEffect } from 'react';
2
1
  import { Loader2 } from 'lucide-react';
2
+ import { useRef, useEffect } from 'react';
3
3
  import Markdown from 'react-markdown';
4
4
  import remarkGfm from 'remark-gfm';
5
5
  import type { EditHistoryEntry } from './types';
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react';
1
+ import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
2
2
  import {
3
3
  Code,
4
4
  Eye,
@@ -11,18 +11,18 @@ import {
11
11
  FolderTree,
12
12
  FileCode,
13
13
  } from 'lucide-react';
14
+ import { useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react';
14
15
  import { MarkdownEditor } from '../MarkdownEditor';
15
16
  import { MarkdownPreview } from '../MarkdownPreview';
16
17
  import { SaveStatusButton, type SaveStatus } from '../SaveStatusButton';
18
+ import { CodeBlockView } from './CodeBlockView';
17
19
  import { EditHistory } from './EditHistory';
18
20
  import { FileTree } from './FileTree';
19
- import { SaveConfirmDialog } from './SaveConfirmDialog';
20
- import { CodeBlockView } from './CodeBlockView';
21
+ import { getFileType, isCompilable, isMarkdownFile, getMimeType } from './fileTypes';
21
22
  import { MediaPreview } from './MediaPreview';
22
- import { useEditSession, type UseEditSessionOptions } from './useEditSession';
23
+ import { SaveConfirmDialog } from './SaveConfirmDialog';
23
24
  import { getActiveContent, getFiles } from './types';
24
- import { getFileType, isCompilable, isMarkdownFile, getMimeType } from './fileTypes';
25
- import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
25
+ import { useEditSession, type UseEditSessionOptions } from './useEditSession';
26
26
  import type { VirtualProject } from '@aprovan/patchwork-compiler';
27
27
 
28
28
  // Simple hash for React key to force re-render on code changes
@@ -1,4 +1,3 @@
1
- import { useMemo, useState, useRef, useCallback, useEffect, type ReactNode } from 'react';
2
1
  import {
3
2
  ChevronRight,
4
3
  ChevronDown,
@@ -12,8 +11,9 @@ import {
12
11
  PinOff,
13
12
  X,
14
13
  } from 'lucide-react';
15
- import type { VirtualFile } from '@aprovan/patchwork-compiler';
14
+ import { useMemo, useState, useRef, useCallback, useEffect, type ReactNode } from 'react';
16
15
  import { isMediaFile } from './fileTypes';
16
+ import type { VirtualFile } from '@aprovan/patchwork-compiler';
17
17
 
18
18
  interface TreeNode {
19
19
  name: string;
@@ -384,7 +384,6 @@ function LazyTreeNode({
384
384
  .catch((err) => setLoadError(err instanceof Error ? err.message : 'Failed to load directory'))
385
385
  .finally(() => setLoading(false));
386
386
  }
387
- // eslint-disable-next-line react-hooks/exhaustive-deps
388
387
  }, [reloadToken]);
389
388
 
390
389
  const isActive = entry.path === activePath;
@@ -1,5 +1,5 @@
1
- import { useState, useEffect } from 'react';
2
1
  import { FileImage, FileVideo, AlertCircle } from 'lucide-react';
2
+ import { useState, useEffect } from 'react';
3
3
  import { isImageFile, isVideoFile } from './fileTypes';
4
4
 
5
5
  export interface MediaPreviewProps {
@@ -1,5 +1,5 @@
1
- import { applyDiffs, hasDiffBlocks, parseEditResponse } from '../../lib/diff';
2
- import type { EditRequest, EditResponse } from './types';
1
+ import { applyDiffs, hasDiffBlocks, parseEditResponse } from "../../lib/diff";
2
+ import type { EditRequest, EditResponse } from "./types";
3
3
 
4
4
  export interface EditApiOptions {
5
5
  endpoint?: string;
@@ -12,16 +12,16 @@ export async function sendEditRequest(
12
12
  request: EditRequest,
13
13
  options: EditApiOptions = {},
14
14
  ): Promise<EditResponse> {
15
- const { endpoint = '/api/edit', onProgress, sanitize = true } = options;
15
+ const { endpoint = "/api/edit", onProgress, sanitize = true } = options;
16
16
 
17
17
  const response = await fetch(endpoint, {
18
- method: 'POST',
19
- headers: { 'Content-Type': 'application/json' },
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
20
  body: JSON.stringify(request),
21
21
  });
22
22
 
23
23
  if (!response.ok) {
24
- throw new Error('Edit request failed');
24
+ throw new Error("Edit request failed");
25
25
  }
26
26
 
27
27
  const text = await streamResponse(response, onProgress);
@@ -42,7 +42,7 @@ export async function sendEditRequest(
42
42
  // Provide detailed context about failed diffs for better error feedback
43
43
  const failedDetails = result.failed
44
44
  .map((f, i) => `[${i + 1}] "${f}"`)
45
- .join('\n');
45
+ .join("\n");
46
46
  throw new Error(
47
47
  `Failed to apply ${parsed.diffs.length} diff(s). None of the SEARCH blocks matched the code.\n\nFailed searches:\n${failedDetails}\n\nThis usually means the code has changed or the SEARCH text doesn't match exactly.`,
48
48
  );
@@ -71,7 +71,7 @@ async function streamResponse(
71
71
  }
72
72
 
73
73
  const decoder = new TextDecoder();
74
- let fullText = '';
74
+ let fullText = "";
75
75
  const emittedNotes = new Set<string>();
76
76
 
77
77
  let done = false;
@@ -1,4 +1,4 @@
1
- export type FileCategory = 'compilable' | 'text' | 'media' | 'binary';
1
+ export type FileCategory = "compilable" | "text" | "media" | "binary";
2
2
 
3
3
  export interface FileTypeInfo {
4
4
  category: FileCategory;
@@ -6,55 +6,75 @@ export interface FileTypeInfo {
6
6
  mimeType: string;
7
7
  }
8
8
 
9
- const COMPILABLE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
10
- const MEDIA_EXTENSIONS = ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.webm'];
11
- const TEXT_EXTENSIONS = ['.json', '.yaml', '.yml', '.md', '.txt', '.css', '.html', '.xml', '.toml'];
9
+ const COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
10
+ const MEDIA_EXTENSIONS = [
11
+ ".svg",
12
+ ".png",
13
+ ".jpg",
14
+ ".jpeg",
15
+ ".gif",
16
+ ".webp",
17
+ ".mp4",
18
+ ".mov",
19
+ ".webm",
20
+ ];
21
+ const TEXT_EXTENSIONS = [
22
+ ".json",
23
+ ".yaml",
24
+ ".yml",
25
+ ".md",
26
+ ".txt",
27
+ ".css",
28
+ ".html",
29
+ ".xml",
30
+ ".toml",
31
+ ];
12
32
 
13
33
  const EXTENSION_TO_LANGUAGE: Record<string, string> = {
14
- '.tsx': 'tsx',
15
- '.jsx': 'jsx',
16
- '.ts': 'typescript',
17
- '.js': 'javascript',
18
- '.json': 'json',
19
- '.yaml': 'yaml',
20
- '.yml': 'yaml',
21
- '.md': 'markdown',
22
- '.txt': 'text',
23
- '.css': 'css',
24
- '.html': 'html',
25
- '.xml': 'xml',
26
- '.toml': 'toml',
27
- '.svg': 'xml',
34
+ ".tsx": "tsx",
35
+ ".jsx": "jsx",
36
+ ".ts": "typescript",
37
+ ".js": "javascript",
38
+ ".json": "json",
39
+ ".yaml": "yaml",
40
+ ".yml": "yaml",
41
+ ".md": "markdown",
42
+ ".txt": "text",
43
+ ".css": "css",
44
+ ".html": "html",
45
+ ".xml": "xml",
46
+ ".toml": "toml",
47
+ ".svg": "xml",
28
48
  };
29
49
 
30
50
  const EXTENSION_TO_MIME: Record<string, string> = {
31
- '.tsx': 'text/typescript-jsx',
32
- '.jsx': 'text/javascript-jsx',
33
- '.ts': 'text/typescript',
34
- '.js': 'text/javascript',
35
- '.json': 'application/json',
36
- '.yaml': 'text/yaml',
37
- '.yml': 'text/yaml',
38
- '.md': 'text/markdown',
39
- '.txt': 'text/plain',
40
- '.css': 'text/css',
41
- '.html': 'text/html',
42
- '.xml': 'application/xml',
43
- '.toml': 'text/toml',
44
- '.svg': 'image/svg+xml',
45
- '.png': 'image/png',
46
- '.jpg': 'image/jpeg',
47
- '.jpeg': 'image/jpeg',
48
- '.gif': 'image/gif',
49
- '.webp': 'image/webp',
50
- '.mp4': 'video/mp4',
51
- '.mov': 'video/quicktime',
52
- '.webm': 'video/webm',
51
+ ".tsx": "text/typescript-jsx",
52
+ ".jsx": "text/javascript-jsx",
53
+ ".ts": "text/typescript",
54
+ ".js": "text/javascript",
55
+ ".json": "application/json",
56
+ ".yaml": "text/yaml",
57
+ ".yml": "text/yaml",
58
+ ".md": "text/markdown",
59
+ ".txt": "text/plain",
60
+ ".css": "text/css",
61
+ ".html": "text/html",
62
+ ".xml": "application/xml",
63
+ ".toml": "text/toml",
64
+ ".svg": "image/svg+xml",
65
+ ".png": "image/png",
66
+ ".jpg": "image/jpeg",
67
+ ".jpeg": "image/jpeg",
68
+ ".gif": "image/gif",
69
+ ".webp": "image/webp",
70
+ ".mp4": "video/mp4",
71
+ ".mov": "video/quicktime",
72
+ ".webm": "video/webm",
53
73
  };
54
74
 
55
75
  function getExtension(path: string): string {
56
- const lastDot = path.lastIndexOf('.');
57
- if (lastDot === -1) return '';
76
+ const lastDot = path.lastIndexOf(".");
77
+ if (lastDot === -1) return "";
58
78
  return path.slice(lastDot).toLowerCase();
59
79
  }
60
80
 
@@ -63,32 +83,32 @@ export function getFileType(path: string): FileTypeInfo {
63
83
 
64
84
  if (COMPILABLE_EXTENSIONS.includes(ext)) {
65
85
  return {
66
- category: 'compilable',
86
+ category: "compilable",
67
87
  language: EXTENSION_TO_LANGUAGE[ext] ?? null,
68
- mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
88
+ mimeType: EXTENSION_TO_MIME[ext] ?? "text/plain",
69
89
  };
70
90
  }
71
91
 
72
92
  if (TEXT_EXTENSIONS.includes(ext)) {
73
93
  return {
74
- category: 'text',
94
+ category: "text",
75
95
  language: EXTENSION_TO_LANGUAGE[ext] ?? null,
76
- mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
96
+ mimeType: EXTENSION_TO_MIME[ext] ?? "text/plain",
77
97
  };
78
98
  }
79
99
 
80
100
  if (MEDIA_EXTENSIONS.includes(ext)) {
81
101
  return {
82
- category: 'media',
83
- language: ext === '.svg' ? 'xml' : null,
84
- mimeType: EXTENSION_TO_MIME[ext] ?? 'application/octet-stream',
102
+ category: "media",
103
+ language: ext === ".svg" ? "xml" : null,
104
+ mimeType: EXTENSION_TO_MIME[ext] ?? "application/octet-stream",
85
105
  };
86
106
  }
87
107
 
88
108
  return {
89
- category: 'binary',
109
+ category: "binary",
90
110
  language: null,
91
- mimeType: 'application/octet-stream',
111
+ mimeType: "application/octet-stream",
92
112
  };
93
113
  }
94
114
 
@@ -105,7 +125,7 @@ export function isTextFile(path: string): boolean {
105
125
  }
106
126
 
107
127
  export function isMarkdownFile(path: string): boolean {
108
- return getExtension(path) === '.md';
128
+ return getExtension(path) === ".md";
109
129
  }
110
130
 
111
131
  export function isPreviewable(path: string): boolean {
@@ -119,15 +139,15 @@ export function getLanguageFromExt(path: string): string | null {
119
139
 
120
140
  export function getMimeType(path: string): string {
121
141
  const ext = getExtension(path);
122
- return EXTENSION_TO_MIME[ext] ?? 'application/octet-stream';
142
+ return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
123
143
  }
124
144
 
125
145
  export function isImageFile(path: string): boolean {
126
146
  const ext = getExtension(path);
127
- return ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext);
147
+ return [".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(ext);
128
148
  }
129
149
 
130
150
  export function isVideoFile(path: string): boolean {
131
151
  const ext = getExtension(path);
132
- return ['.mp4', '.mov', '.webm'].includes(ext);
152
+ return [".mp4", ".mov", ".webm"].includes(ext);
133
153
  }
@@ -1,6 +1,5 @@
1
- import { useState, useCallback, useMemo, useEffect, useRef } from "react";
2
- import type { VirtualProject } from "@aprovan/patchwork-compiler";
3
1
  import { createSingleFileProject } from "@aprovan/patchwork-compiler";
2
+ import { useState, useCallback, useMemo, useEffect, useRef } from "react";
4
3
  import { sendEditRequest } from "./api";
5
4
  import type {
6
5
  EditHistoryEntry,
@@ -8,6 +7,7 @@ import type {
8
7
  EditSessionActions,
9
8
  CompileFn,
10
9
  } from "./types";
10
+ import type { VirtualProject } from "@aprovan/patchwork-compiler";
11
11
 
12
12
  export interface UseEditSessionOptions {
13
13
  originalCode?: string;
@@ -1,5 +1,5 @@
1
- import type { VirtualFile, VirtualProject } from '@aprovan/patchwork-compiler';
2
1
  import { createProjectFromFiles, detectMainFile } from '@aprovan/patchwork-compiler';
2
+ import type { VirtualFile, VirtualProject } from '@aprovan/patchwork-compiler';
3
3
 
4
4
  // Matches fenced code blocks with optional attributes: ```language attr="value"\n...content...```
5
5
  // Captures: [1] = language (optional), [2] = attributes (optional), [3] = content