@aprovan/patchwork-editor 0.1.0 → 0.1.2-dev.03aaf5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +4 -7
  2. package/dist/components/CodeBlockExtension.d.ts +2 -0
  3. package/dist/components/CodePreview.d.ts +13 -0
  4. package/dist/components/MarkdownEditor.d.ts +10 -0
  5. package/dist/components/ServicesInspector.d.ts +49 -0
  6. package/dist/components/edit/CodeBlockView.d.ts +7 -0
  7. package/dist/components/edit/EditHistory.d.ts +10 -0
  8. package/dist/components/edit/EditModal.d.ts +20 -0
  9. package/dist/components/edit/FileTree.d.ts +8 -0
  10. package/dist/components/edit/MediaPreview.d.ts +6 -0
  11. package/dist/components/edit/SaveConfirmDialog.d.ts +9 -0
  12. package/dist/components/edit/api.d.ts +8 -0
  13. package/dist/components/edit/fileTypes.d.ts +14 -0
  14. package/dist/components/edit/index.d.ts +10 -0
  15. package/dist/components/edit/types.d.ts +41 -0
  16. package/dist/components/edit/useEditSession.d.ts +9 -0
  17. package/dist/components/index.d.ts +5 -0
  18. package/dist/index.d.ts +3 -331
  19. package/dist/index.js +812 -142
  20. package/dist/lib/code-extractor.d.ts +44 -0
  21. package/dist/lib/diff.d.ts +90 -0
  22. package/dist/lib/index.d.ts +4 -0
  23. package/dist/lib/utils.d.ts +2 -0
  24. package/dist/lib/vfs.d.ts +36 -0
  25. package/package.json +5 -4
  26. package/src/components/CodeBlockExtension.tsx +1 -1
  27. package/src/components/CodePreview.tsx +64 -4
  28. package/src/components/edit/CodeBlockView.tsx +188 -0
  29. package/src/components/edit/EditModal.tsx +172 -30
  30. package/src/components/edit/FileTree.tsx +67 -13
  31. package/src/components/edit/MediaPreview.tsx +124 -0
  32. package/src/components/edit/SaveConfirmDialog.tsx +60 -0
  33. package/src/components/edit/fileTypes.ts +125 -0
  34. package/src/components/edit/index.ts +4 -0
  35. package/src/components/edit/types.ts +3 -0
  36. package/src/components/edit/useEditSession.ts +56 -11
  37. package/src/index.ts +17 -0
  38. package/src/lib/diff.ts +2 -1
  39. package/src/lib/vfs.ts +28 -10
  40. package/tsup.config.ts +10 -5
@@ -0,0 +1,125 @@
1
+ export type FileCategory = 'compilable' | 'text' | 'media' | 'binary';
2
+
3
+ export interface FileTypeInfo {
4
+ category: FileCategory;
5
+ language: string | null;
6
+ mimeType: string;
7
+ }
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'];
12
+
13
+ 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',
28
+ };
29
+
30
+ 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',
53
+ };
54
+
55
+ function getExtension(path: string): string {
56
+ const lastDot = path.lastIndexOf('.');
57
+ if (lastDot === -1) return '';
58
+ return path.slice(lastDot).toLowerCase();
59
+ }
60
+
61
+ export function getFileType(path: string): FileTypeInfo {
62
+ const ext = getExtension(path);
63
+
64
+ if (COMPILABLE_EXTENSIONS.includes(ext)) {
65
+ return {
66
+ category: 'compilable',
67
+ language: EXTENSION_TO_LANGUAGE[ext] ?? null,
68
+ mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
69
+ };
70
+ }
71
+
72
+ if (TEXT_EXTENSIONS.includes(ext)) {
73
+ return {
74
+ category: 'text',
75
+ language: EXTENSION_TO_LANGUAGE[ext] ?? null,
76
+ mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
77
+ };
78
+ }
79
+
80
+ if (MEDIA_EXTENSIONS.includes(ext)) {
81
+ return {
82
+ category: 'media',
83
+ language: ext === '.svg' ? 'xml' : null,
84
+ mimeType: EXTENSION_TO_MIME[ext] ?? 'application/octet-stream',
85
+ };
86
+ }
87
+
88
+ return {
89
+ category: 'binary',
90
+ language: null,
91
+ mimeType: 'application/octet-stream',
92
+ };
93
+ }
94
+
95
+ export function isCompilable(path: string): boolean {
96
+ return COMPILABLE_EXTENSIONS.includes(getExtension(path));
97
+ }
98
+
99
+ export function isMediaFile(path: string): boolean {
100
+ return MEDIA_EXTENSIONS.includes(getExtension(path));
101
+ }
102
+
103
+ export function isTextFile(path: string): boolean {
104
+ return TEXT_EXTENSIONS.includes(getExtension(path));
105
+ }
106
+
107
+ export function getLanguageFromExt(path: string): string | null {
108
+ const ext = getExtension(path);
109
+ return EXTENSION_TO_LANGUAGE[ext] ?? null;
110
+ }
111
+
112
+ export function getMimeType(path: string): string {
113
+ const ext = getExtension(path);
114
+ return EXTENSION_TO_MIME[ext] ?? 'application/octet-stream';
115
+ }
116
+
117
+ export function isImageFile(path: string): boolean {
118
+ const ext = getExtension(path);
119
+ return ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext);
120
+ }
121
+
122
+ export function isVideoFile(path: string): boolean {
123
+ const ext = getExtension(path);
124
+ return ['.mp4', '.mov', '.webm'].includes(ext);
125
+ }
@@ -4,3 +4,7 @@ export * from './useEditSession';
4
4
  export * from './EditHistory';
5
5
  export * from './EditModal';
6
6
  export * from './FileTree';
7
+ export * from './SaveConfirmDialog';
8
+ export * from './fileTypes';
9
+ export * from './CodeBlockView';
10
+ export * from './MediaPreview';
@@ -23,8 +23,11 @@ export interface EditSessionActions {
23
23
  updateActiveFile: (content: string) => void;
24
24
  setActiveFile: (path: string) => void;
25
25
  clearError: () => void;
26
+ replaceFile: (path: string, content: string, encoding?: 'utf8' | 'base64') => void;
26
27
  }
27
28
 
29
+ export type FileEncoding = 'utf8' | 'base64';
30
+
28
31
  // Convenience getters
29
32
  export function getActiveContent(state: EditSessionState): string {
30
33
  return state.project.files.get(state.activeFile)?.content ?? '';
@@ -1,16 +1,17 @@
1
- import { useState, useCallback, useMemo } from 'react';
2
- import type { VirtualProject } from '@aprovan/patchwork-compiler';
3
- import { createSingleFileProject } from '@aprovan/patchwork-compiler';
4
- import { sendEditRequest } from './api';
1
+ import { useState, useCallback, useMemo, useEffect, useRef } from "react";
2
+ import type { VirtualProject } from "@aprovan/patchwork-compiler";
3
+ import { createSingleFileProject } from "@aprovan/patchwork-compiler";
4
+ import { sendEditRequest } from "./api";
5
5
  import type {
6
6
  EditHistoryEntry,
7
7
  EditSessionState,
8
8
  EditSessionActions,
9
9
  CompileFn,
10
- } from './types';
10
+ } from "./types";
11
11
 
12
12
  export interface UseEditSessionOptions {
13
- originalCode: string;
13
+ originalCode?: string;
14
+ originalProject?: VirtualProject;
14
15
  compile?: CompileFn;
15
16
  apiEndpoint?: string;
16
17
  }
@@ -25,13 +26,28 @@ function cloneProject(project: VirtualProject): VirtualProject {
25
26
  export function useEditSession(
26
27
  options: UseEditSessionOptions,
27
28
  ): EditSessionState & EditSessionActions {
28
- const { originalCode, compile, apiEndpoint } = options;
29
+ const {
30
+ originalCode,
31
+ originalProject: providedProject,
32
+ compile,
33
+ apiEndpoint,
34
+ } = options;
35
+
36
+ console.log(
37
+ "[useEditSession] providedProject:",
38
+ providedProject?.id,
39
+ "files:",
40
+ providedProject ? Array.from(providedProject.files.keys()) : "none",
41
+ );
29
42
 
30
43
  const originalProject = useMemo(
31
- () => createSingleFileProject(originalCode),
32
- [originalCode],
44
+ () => providedProject ?? createSingleFileProject(originalCode ?? ""),
45
+ [providedProject, originalCode],
33
46
  );
34
47
 
48
+ // Track the last project we synced from to detect reference changes
49
+ const lastSyncedProjectRef = useRef<VirtualProject>(originalProject);
50
+
35
51
  const [project, setProject] = useState<VirtualProject>(originalProject);
36
52
  const [activeFile, setActiveFile] = useState(originalProject.entry);
37
53
  const [history, setHistory] = useState<EditHistoryEntry[]>([]);
@@ -40,6 +56,18 @@ export function useEditSession(
40
56
  const [streamingNotes, setStreamingNotes] = useState<string[]>([]);
41
57
  const [pendingPrompt, setPendingPrompt] = useState<string | null>(null);
42
58
 
59
+ // Sync state when the original project reference changes (new project or files loaded)
60
+ useEffect(() => {
61
+ if (originalProject !== lastSyncedProjectRef.current) {
62
+ lastSyncedProjectRef.current = originalProject;
63
+ setProject(originalProject);
64
+ setActiveFile(originalProject.entry);
65
+ setHistory([]);
66
+ setError(null);
67
+ setStreamingNotes([]);
68
+ }
69
+ }, [originalProject]);
70
+
43
71
  const performEdit = useCallback(
44
72
  async (
45
73
  currentCode: string,
@@ -85,7 +113,7 @@ export function useEditSession(
85
113
  );
86
114
 
87
115
  const currentCode = useMemo(
88
- () => project.files.get(activeFile)?.content ?? '',
116
+ () => project.files.get(activeFile)?.content ?? "",
89
117
  [project, activeFile],
90
118
  );
91
119
 
@@ -110,7 +138,7 @@ export function useEditSession(
110
138
  });
111
139
  setHistory((prev) => [...prev, ...result.entries]);
112
140
  } catch (err) {
113
- setError(err instanceof Error ? err.message : 'Edit failed');
141
+ setError(err instanceof Error ? err.message : "Edit failed");
114
142
  } finally {
115
143
  setIsApplying(false);
116
144
  setStreamingNotes([]);
@@ -142,6 +170,22 @@ export function useEditSession(
142
170
  [activeFile],
143
171
  );
144
172
 
173
+ const replaceFile = useCallback(
174
+ (path: string, content: string, encoding: "utf8" | "base64" = "utf8") => {
175
+ setProject((prev) => {
176
+ const updated = cloneProject(prev);
177
+ const file = updated.files.get(path);
178
+ if (file) {
179
+ updated.files.set(path, { ...file, content, encoding });
180
+ } else {
181
+ updated.files.set(path, { path, content, encoding });
182
+ }
183
+ return updated;
184
+ });
185
+ },
186
+ [],
187
+ );
188
+
145
189
  const clearError = useCallback(() => {
146
190
  setError(null);
147
191
  }, []);
@@ -160,5 +204,6 @@ export function useEditSession(
160
204
  updateActiveFile,
161
205
  setActiveFile,
162
206
  clearError,
207
+ replaceFile,
163
208
  };
164
209
  }
package/src/index.ts CHANGED
@@ -12,6 +12,9 @@ export {
12
12
  EditModal,
13
13
  EditHistory,
14
14
  FileTree,
15
+ SaveConfirmDialog,
16
+ CodeBlockView,
17
+ MediaPreview,
15
18
  useEditSession,
16
19
  sendEditRequest,
17
20
  type EditModalProps,
@@ -25,8 +28,22 @@ export {
25
28
  type CompileFn,
26
29
  type EditApiOptions,
27
30
  type FileTreeProps,
31
+ type SaveConfirmDialogProps,
32
+ type CodeBlockViewProps,
33
+ type MediaPreviewProps,
34
+ type FileCategory,
35
+ type FileTypeInfo,
36
+ type FileEncoding,
28
37
  getActiveContent,
29
38
  getFiles,
39
+ getFileType,
40
+ isCompilable,
41
+ isMediaFile,
42
+ isTextFile,
43
+ isImageFile,
44
+ isVideoFile,
45
+ getLanguageFromExt,
46
+ getMimeType,
30
47
  } from './components/edit';
31
48
 
32
49
  // Lib utilities
package/src/lib/diff.ts CHANGED
@@ -155,7 +155,6 @@ export interface ParsedEditResponse {
155
155
  /**
156
156
  * Parse progress notes and diffs from an edit response.
157
157
  *
158
- * New format uses tagged attributes on code fences:
159
158
  * ```diff note="Adding handler" path="@/components/Button.tsx"
160
159
  * <<<<<<< SEARCH
161
160
  * exact code
@@ -275,6 +274,8 @@ export function applyDiffs(
275
274
  }
276
275
 
277
276
  export function hasDiffBlocks(text: string): boolean {
277
+ // Reset lastIndex to avoid state issues from the global regex flag
278
+ DIFF_BLOCK_REGEX.lastIndex = 0;
278
279
  return DIFF_BLOCK_REGEX.test(text);
279
280
  }
280
281
 
package/src/lib/vfs.ts CHANGED
@@ -1,13 +1,13 @@
1
- import {
2
- VFSStore,
3
- LocalFSBackend,
1
+ import {
2
+ VFSStore,
3
+ HttpBackend,
4
+ type ChangeRecord,
4
5
  type VirtualProject,
5
- type VirtualFile
6
6
  } from '@aprovan/patchwork-compiler';
7
7
 
8
8
  /**
9
9
  * VFS client for persisting virtual projects to the stitchery server.
10
- * Uses LocalFSBackend which makes HTTP requests to /vfs routes.
10
+ * Uses HttpBackend which makes HTTP requests to /vfs routes.
11
11
  */
12
12
 
13
13
  // VFS base URL - points to stitchery server's VFS routes
@@ -41,12 +41,15 @@ let storeInstance: VFSStore | null = null;
41
41
 
42
42
  /**
43
43
  * Get the VFS store instance (creates one if needed).
44
- * Store uses LocalFSBackend to persist to the stitchery server.
44
+ * Store uses HttpBackend to persist to the stitchery server.
45
45
  */
46
46
  export function getVFSStore(): VFSStore {
47
47
  if (!storeInstance) {
48
- const backend = new LocalFSBackend({ baseUrl: VFS_BASE_URL });
49
- storeInstance = new VFSStore(backend);
48
+ const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
49
+ storeInstance = new VFSStore(provider, {
50
+ sync: true,
51
+ conflictStrategy: 'local-wins',
52
+ });
50
53
  }
51
54
  return storeInstance;
52
55
  }
@@ -88,9 +91,24 @@ export async function listProjects(): Promise<string[]> {
88
91
  /**
89
92
  * Save a single file to the VFS.
90
93
  */
91
- export async function saveFile(file: VirtualFile): Promise<void> {
94
+ export async function saveFile(path: string, content: string): Promise<void> {
92
95
  const store = getVFSStore();
93
- await store.putFile(file);
96
+ await store.writeFile(path, content);
97
+ }
98
+
99
+ export async function loadFile(
100
+ path: string,
101
+ encoding?: 'utf8' | 'base64',
102
+ ): Promise<string> {
103
+ const store = getVFSStore();
104
+ return store.readFile(path, encoding);
105
+ }
106
+
107
+ export function subscribeToChanges(
108
+ callback: (record: ChangeRecord) => void,
109
+ ): () => void {
110
+ const store = getVFSStore();
111
+ return store.on('change', callback);
94
112
  }
95
113
 
96
114
  /**
package/tsup.config.ts CHANGED
@@ -1,10 +1,15 @@
1
- import { defineConfig } from 'tsup';
1
+ import { defineConfig } from "tsup";
2
2
 
3
3
  export default defineConfig({
4
- entry: ['src/index.ts'],
5
- format: ['esm'],
6
- dts: true,
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ dts: false,
7
7
  clean: true,
8
- external: ['react', 'react-dom'],
8
+ external: [
9
+ "react",
10
+ "react-dom",
11
+ "@aprovan/bobbin",
12
+ "@aprovan/patchwork-compiler",
13
+ ],
9
14
  treeshake: true,
10
15
  });