vzcode 1.62.0 → 2.0.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.html CHANGED
@@ -20,8 +20,8 @@
20
20
  href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
21
21
  rel="stylesheet"
22
22
  />
23
- <script type="module" crossorigin src="/assets/index-vbF7Aejt.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-Br76WttW.css">
23
+ <script type="module" crossorigin src="/assets/index-BgqNq-ww.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-BM8tDh6M.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.62.0",
3
+ "version": "2.0.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -41,6 +41,7 @@ export type VZCodeContextValue = {
41
41
  directoryName: string,
42
42
  ) => void;
43
43
  deleteDirectory: (directoryId: string) => void;
44
+ deleteAllFiles: () => void;
44
45
 
45
46
  setActiveFileId: (fileId: string | null) => void;
46
47
  setActiveFileLeft: () => void;
@@ -184,6 +184,7 @@ export const useVZCodeState = ({
184
184
  createDirectory,
185
185
  renameDirectory,
186
186
  deleteDirectory,
187
+ deleteAllFiles,
187
188
  } = useFileCRUD({
188
189
  submitOperation,
189
190
  closeTabs,
@@ -461,6 +462,7 @@ export const useVZCodeState = ({
461
462
  createDirectory,
462
463
  renameDirectory,
463
464
  deleteDirectory,
465
+ deleteAllFiles,
464
466
 
465
467
  setActiveFileId,
466
468
  setActiveFileLeft,
@@ -107,6 +107,29 @@ const MessageListComponent = ({
107
107
  scrollToBottom,
108
108
  ]);
109
109
 
110
+ // Track previous loading state to detect when AI generation completes
111
+ const [prevIsLoading, setPrevIsLoading] =
112
+ useState(isLoading);
113
+
114
+ // Scroll to bottom when AI generation completes (loading changes from true to false)
115
+ useEffect(() => {
116
+ // If AI was generating and now it's finished, scroll to bottom
117
+ if (prevIsLoading && !isLoading) {
118
+ // Force scroll to bottom regardless of user scroll state
119
+ // This ensures we always scroll to bottom when generation completes
120
+ messagesEndRef.current?.scrollIntoView({
121
+ behavior: 'smooth',
122
+ block: 'end',
123
+ });
124
+
125
+ // Re-enable auto-scroll for future messages
126
+ setIsUserScrolled(false);
127
+ setAutoScrollEnabled(true);
128
+ }
129
+
130
+ setPrevIsLoading(isLoading);
131
+ }, [isLoading, prevIsLoading]);
132
+
110
133
  // Clean up timeout on unmount
111
134
  useEffect(() => {
112
135
  return () => {
@@ -36,15 +36,26 @@ export const DeleteConfirmationModal = ({
36
36
  <Modal show={show} onHide={onClose} centered>
37
37
  <Modal.Header closeButton onClick={onClose}>
38
38
  <Modal.Title>
39
- Delete {isDirectory ? 'Directory' : 'File'}
39
+ Delete{' '}
40
+ {name === 'all files'
41
+ ? 'All Files'
42
+ : isDirectory
43
+ ? 'Directory'
44
+ : 'File'}
40
45
  </Modal.Title>
41
46
  </Modal.Header>
42
47
 
43
48
  <Modal.Body>
44
49
  <p>
45
- This will delete the{' '}
46
- {isDirectory ? 'directory' : 'file'}{' '}
47
- <strong>{name}</strong>.
50
+ {name === 'all files' ? (
51
+ 'This will delete all files in the project.'
52
+ ) : (
53
+ <>
54
+ This will delete the{' '}
55
+ {isDirectory ? 'directory' : 'file'}{' '}
56
+ <strong>{name}</strong>.
57
+ </>
58
+ )}
48
59
  </p>
49
60
  </Modal.Body>
50
61
 
@@ -102,7 +102,7 @@ export const createAICopyPasteHandlers = (
102
102
  ) {
103
103
  // Use mergeFileChanges utility from editcodewithai
104
104
  const mergedFiles = mergeFileChanges(
105
- files,
105
+ files || {},
106
106
  parsed.files,
107
107
  );
108
108
 
@@ -35,6 +35,7 @@ import { VZCodeContext } from '../VZCodeContext';
35
35
  import { AIChat } from './AIChat';
36
36
  import { Listing } from './Listing';
37
37
  import { Search } from './Search';
38
+ import { DeleteConfirmationModal } from './DeleteConfirmationModal';
38
39
  import { useDragAndDrop } from './useDragAndDrop';
39
40
  import { createAICopyPasteHandlers } from './aiCopyPaste';
40
41
  import {
@@ -157,6 +158,7 @@ export const VZSidebar = ({
157
158
  updatePresenceIndicator,
158
159
  setVoiceChatModalOpen,
159
160
  submitOperation,
161
+ deleteAllFiles,
160
162
  } = useContext(VZCodeContext);
161
163
 
162
164
  const fileTree = useMemo(
@@ -178,6 +180,10 @@ export const VZSidebar = ({
178
180
  const [exportButtonText, setExportButtonText] =
179
181
  useState('Export to ZIP');
180
182
 
183
+ // State for delete all confirmation modal
184
+ const [showDeleteAllModal, setShowDeleteAllModal] =
185
+ useState(false);
186
+
181
187
  // Create AI copy/paste handlers
182
188
  const {
183
189
  handleCopyForAI,
@@ -195,6 +201,20 @@ export const VZSidebar = ({
195
201
  [files, submitOperation],
196
202
  );
197
203
 
204
+ // Delete all handlers
205
+ const handleDeleteAllClick = useCallback(() => {
206
+ setShowDeleteAllModal(true);
207
+ }, []);
208
+
209
+ const handleDeleteAllModalClose = useCallback(() => {
210
+ setShowDeleteAllModal(false);
211
+ }, []);
212
+
213
+ const handleDeleteAllConfirm = useCallback(() => {
214
+ setShowDeleteAllModal(false);
215
+ deleteAllFiles();
216
+ }, [deleteAllFiles]);
217
+
198
218
  const { sidebarWidth, setSidebarView } = useContext(
199
219
  SplitPaneResizeContext,
200
220
  );
@@ -653,6 +673,17 @@ export const VZSidebar = ({
653
673
  <i className="bi bi-clipboard-plus"></i>
654
674
  {pasteButtonText}
655
675
  </button>
676
+
677
+ {filesExist && (
678
+ <button
679
+ className="ai-button delete-all-button"
680
+ onClick={handleDeleteAllClick}
681
+ title="Delete all files"
682
+ >
683
+ <i className="bi bi-trash"></i>
684
+ delete all
685
+ </button>
686
+ )}
656
687
  </div>
657
688
  )}
658
689
  {enableConnectionStatus && (
@@ -681,6 +712,17 @@ export const VZSidebar = ({
681
712
  </div>
682
713
  </div>
683
714
  )}
715
+
716
+ {/* Delete All Files Confirmation Modal */}
717
+ {showDeleteAllModal && (
718
+ <DeleteConfirmationModal
719
+ show={showDeleteAllModal}
720
+ onClose={handleDeleteAllModalClose}
721
+ onConfirm={handleDeleteAllConfirm}
722
+ isDirectory={false}
723
+ name="all files"
724
+ />
725
+ )}
684
726
  </div>
685
727
  );
686
728
  };
@@ -357,6 +357,21 @@
357
357
  border-color: var(--vh-color-success-04);
358
358
  }
359
359
  }
360
+
361
+ &.delete-all-button {
362
+ border-color: var(--vh-color-neutral-02);
363
+ color: var(--vh-color-neutral-03);
364
+ font-size: 11px;
365
+ font-weight: 400;
366
+ opacity: 0.7;
367
+
368
+ &:hover {
369
+ background: var(--vh-color-hover-dark);
370
+ border-color: var(--vh-color-danger-03);
371
+ color: var(--vh-color-danger-04);
372
+ opacity: 1;
373
+ }
374
+ }
360
375
  }
361
376
  }
362
377
 
@@ -203,6 +203,19 @@ export const useFileCRUD = ({
203
203
  [submitOperation, closeTabs],
204
204
  );
205
205
 
206
+ // Deletes all files
207
+ const deleteAllFiles = useCallback(() => {
208
+ const tabsToClose: Array<VizFileId> = [];
209
+ submitOperation((document: VizContent) => {
210
+ // Collect all file IDs for closing tabs
211
+ for (const fileId in document.files) {
212
+ tabsToClose.push(fileId);
213
+ }
214
+ return { ...document, files: {} };
215
+ });
216
+ closeTabs(tabsToClose);
217
+ }, [submitOperation, closeTabs]);
218
+
206
219
  return {
207
220
  createFile,
208
221
  renameFile,
@@ -210,5 +223,6 @@ export const useFileCRUD = ({
210
223
  createDirectory,
211
224
  renameDirectory,
212
225
  deleteDirectory,
226
+ deleteAllFiles,
213
227
  };
214
228
  };
@@ -7,6 +7,9 @@ import {
7
7
  createFilesSnapshot,
8
8
  generateFilesUnifiedDiff,
9
9
  } from '../../utils/fileDiff.js';
10
+ import { formatFiles } from '../prettier.js';
11
+ import { createSubmitOperation } from '../../submitOperation.js';
12
+ import { VizContent } from '@vizhub/viz-types';
10
13
 
11
14
  // Dev flag for waiting 1 second before starting the LLM function.
12
15
  // Useful for debugging and testing purposes, e.g. checking the typing indicator.
@@ -83,9 +86,55 @@ export const performAIEditing = async ({
83
86
  // Call the LLM function which will handle streaming and incremental file updates
84
87
  const result = await llmFunction(fullPrompt);
85
88
 
89
+ // 3. Run Prettier on changed files before running code
90
+ const afterLLMFiles = createFilesSnapshot(
91
+ shareDBDoc.data.files,
92
+ );
93
+
94
+ // Find files that were changed by the AI
95
+ const changedFileIds = Object.keys(afterLLMFiles).filter(
96
+ (fileId) =>
97
+ beforeFiles[fileId]?.text !==
98
+ afterLLMFiles[fileId]?.text,
99
+ );
100
+
101
+ if (changedFileIds.length > 0) {
102
+ // Format the changed files
103
+ const formattedFiles = await formatFiles(
104
+ shareDBDoc.data.files,
105
+ changedFileIds,
106
+ );
107
+
108
+ // Apply formatted versions to shareDBDoc if formatting succeeded
109
+ if (Object.keys(formattedFiles).length > 0) {
110
+ const submitOperation =
111
+ createSubmitOperation(shareDBDoc);
112
+ submitOperation((document: VizContent) => {
113
+ const updatedFiles = { ...document.files };
114
+
115
+ // Apply each formatted file
116
+ Object.entries(formattedFiles).forEach(
117
+ ([fileId, formattedText]) => {
118
+ if (updatedFiles[fileId]) {
119
+ updatedFiles[fileId] = {
120
+ ...updatedFiles[fileId],
121
+ text: formattedText,
122
+ };
123
+ }
124
+ },
125
+ );
126
+
127
+ return {
128
+ ...document,
129
+ files: updatedFiles,
130
+ };
131
+ });
132
+ }
133
+ }
134
+
86
135
  runCode();
87
136
 
88
- // 3. Capture the state of files after editing and generate diff
137
+ // 4. Capture the state of files after editing and formatting, then generate diff
89
138
  const afterFiles = createFilesSnapshot(
90
139
  shareDBDoc.data.files,
91
140
  );
@@ -0,0 +1,115 @@
1
+ import { format } from 'prettier/standalone';
2
+ import * as prettierPluginBabel from 'prettier/plugins/babel';
3
+ import * as prettierPluginEstree from 'prettier/plugins/estree';
4
+ import * as prettierPluginHtml from 'prettier/plugins/html';
5
+ import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
6
+ import * as prettierPluginCSS from 'prettier/plugins/postcss';
7
+ import * as prettierPluginTypescript from 'prettier/plugins/typescript';
8
+ import { VizFiles, VizFileId } from '@vizhub/viz-types';
9
+
10
+ // Parser mappings - matches client-side implementation
11
+ const parsers = {
12
+ js: 'babel',
13
+ jsx: 'babel',
14
+ mjs: 'babel',
15
+ cjs: 'babel',
16
+ ts: 'typescript',
17
+ tsx: 'typescript',
18
+ css: 'css',
19
+ html: 'html',
20
+ json: 'json',
21
+ json5: 'json5',
22
+ md: 'markdown',
23
+ markdown: 'markdown',
24
+ } as const;
25
+
26
+ // Prettier plugins - matches client-side implementation
27
+ const plugins = [
28
+ prettierPluginBabel,
29
+ prettierPluginEstree,
30
+ prettierPluginHtml,
31
+ prettierPluginMarkdown,
32
+ prettierPluginTypescript,
33
+ prettierPluginCSS,
34
+ ] as any;
35
+
36
+ // Prettier options - matches client-side implementation and .prettierrc
37
+ const prettierOptions = {
38
+ proseWrap: 'always' as const,
39
+ singleQuote: true,
40
+ printWidth: 60,
41
+ };
42
+
43
+ /**
44
+ * Extracts file extension from filename
45
+ * Example: 'foo.js' => 'js'
46
+ */
47
+ const getFileExtension = (fileName: string): string => {
48
+ const match = fileName.match(/\.([^.]+)$/);
49
+ return match ? match[1] : '';
50
+ };
51
+
52
+ /**
53
+ * Formats a single file using Prettier
54
+ */
55
+ export const formatFile = async (
56
+ fileText: string,
57
+ fileName: string,
58
+ ): Promise<string | null> => {
59
+ const fileExtension = getFileExtension(fileName);
60
+ const parser =
61
+ parsers[fileExtension as keyof typeof parsers];
62
+
63
+ // If no parser is found, return null (no formatting)
64
+ if (!parser) {
65
+ return null;
66
+ }
67
+
68
+ try {
69
+ const formatted = await format(fileText, {
70
+ parser,
71
+ plugins,
72
+ ...prettierOptions,
73
+ });
74
+ return formatted;
75
+ } catch (error) {
76
+ // Log error but don't throw - return original text
77
+ console.error(
78
+ `Prettier formatting error for ${fileName}:`,
79
+ error,
80
+ );
81
+ return null;
82
+ }
83
+ };
84
+
85
+ /**
86
+ * Formats multiple files using Prettier
87
+ * Only formats files that have supported extensions
88
+ * Returns a map of fileId -> formatted text for successfully formatted files
89
+ */
90
+ export const formatFiles = async (
91
+ files: VizFiles,
92
+ fileIds?: VizFileId[],
93
+ ): Promise<{ [fileId: VizFileId]: string }> => {
94
+ const results: { [fileId: VizFileId]: string } = {};
95
+ const targetFileIds = fileIds || Object.keys(files);
96
+
97
+ // Process files in parallel for better performance
98
+ const formatPromises = targetFileIds.map(
99
+ async (fileId) => {
100
+ const file = files[fileId];
101
+ if (!file) return;
102
+
103
+ const formatted = await formatFile(
104
+ file.text,
105
+ file.name,
106
+ );
107
+ if (formatted !== null && formatted !== file.text) {
108
+ results[fileId] = formatted;
109
+ }
110
+ },
111
+ );
112
+
113
+ await Promise.all(formatPromises);
114
+ return results;
115
+ };