vzcode 1.40.0 → 1.41.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,7 +20,7 @@
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-ubSLcdJf.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-DPAeYEiH.js"></script>
24
24
  <link rel="stylesheet" crossorigin href="/assets/index-DKLiE98a.css">
25
25
  </head>
26
26
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -115,7 +115,7 @@
115
115
  "@codemirror/theme-one-dark": "^6.1.3",
116
116
  "@codemirror/view": "^6.38.1",
117
117
  "@langchain/core": "^0.3.66",
118
- "@langchain/openai": "^0.6.2",
118
+ "@langchain/openai": "^0.6.3",
119
119
  "@lezer/highlight": "^1.2.1",
120
120
  "@livekit/components-react": "^2.9.14",
121
121
  "@replit/codemirror-indentation-markers": "^6.5.3",
@@ -134,33 +134,34 @@
134
134
  "@uiw/codemirror-theme-xcode": "^4.24.1",
135
135
  "@uiw/codemirror-themes": "^4.24.1",
136
136
  "@valtown/codemirror-ts": "^2.3.1",
137
+ "@vizhub/runtime": "^4.1.0",
137
138
  "@vizhub/viz-types": "^0.3.0",
138
139
  "@vizhub/viz-utils": "^1.3.0",
139
140
  "body-parser": "^2.2.0",
140
141
  "codemirror": "^6.0.2",
141
142
  "codemirror-copilot": "^0.0.7",
142
- "codemirror-ot": "^4.5.0",
143
+ "codemirror-ot": "^4.6.0",
143
144
  "color-hash": "^2.0.2",
144
145
  "comlink": "^4.4.2",
145
146
  "d3-array": "^3.2.4",
146
147
  "diff-match-patch": "^1.0.5",
147
- "dotenv": "^17.2.0",
148
- "editcodewithai": "^2.2.0",
148
+ "dotenv": "^17.2.1",
149
+ "editcodewithai": "^2.3.0",
149
150
  "eslint-linter-browserify": "^9.31.0",
150
151
  "express": "^5.1.0",
151
152
  "ignore": "^7.0.5",
152
153
  "json0-ot-diff": "^1.1.2",
153
154
  "livekit-server-sdk": "^2.13.1",
154
- "llm-code-format": "^2.1.0",
155
+ "llm-code-format": "^3.0.0",
155
156
  "ngrok": "^5.0.0-beta.2",
156
- "npm": "^11.4.2",
157
+ "npm": "^11.5.1",
157
158
  "open": "^10.2.0",
158
159
  "prettier-plugin-svelte": "^3.4.0",
159
160
  "react": "^18",
160
161
  "react-bootstrap": "^2.10.10",
161
162
  "react-dom": "^18",
162
163
  "react-markdown": "^10.1.0",
163
- "react-router-dom": "^7.7.0",
164
+ "react-router-dom": "^7.7.1",
164
165
  "remark-gfm": "^4.0.1",
165
166
  "sharedb": "^5.2.2",
166
167
  "sharedb-client-browser": "^5.2.2",
@@ -169,20 +170,20 @@
169
170
  "ws": "^8.18.3"
170
171
  },
171
172
  "devDependencies": {
172
- "@eslint/js": "^9.31.0",
173
+ "@eslint/js": "^9.32.0",
173
174
  "@types/react": "^18",
174
175
  "@types/react-dom": "^18",
175
176
  "@vitejs/plugin-react": "^4.7.0",
176
177
  "concurrently": "^9.2.0",
177
178
  "cross-env": "^7.0.3",
178
- "eslint": "^9.31.0",
179
+ "eslint": "^9.32.0",
179
180
  "globals": "^16.3.0",
180
- "npm-check-updates": "^18.0.1",
181
+ "npm-check-updates": "^18.0.2",
181
182
  "prettier": "^3.6.2",
182
183
  "sass": "^1.89.2",
183
184
  "ts-node": "^10.9.2",
184
185
  "typescript": "^5.8.3",
185
- "vite": "^7.0.5",
186
+ "vite": "^7.0.6",
186
187
  "vitest": "^3.2.4"
187
188
  },
188
189
  "optionalDependencies": {
@@ -166,7 +166,6 @@ export const getOrCreateEditor = async ({
166
166
  paneId = 'root',
167
167
  fileId,
168
168
  shareDBDoc,
169
- content,
170
169
  filesPath,
171
170
  localPresence,
172
171
  docPresence,
@@ -194,10 +193,7 @@ export const getOrCreateEditor = async ({
194
193
  // This can be `undefined` in the case where we are
195
194
  // viewing files read-only, in which case multiplayer
196
195
  // editing is not enabled.
197
- shareDBDoc: ShareDBDoc<VizContent> | undefined;
198
-
199
- // The initial content to present.
200
- content: VizContent;
196
+ shareDBDoc: ShareDBDoc<VizContent>;
201
197
 
202
198
  filesPath: string[];
203
199
  localPresence: any;
@@ -233,6 +229,7 @@ export const getOrCreateEditor = async ({
233
229
  // Compute `text` and `fileExtension` from the ShareDB document.
234
230
  const textPath = [...filesPath, fileId, 'text'];
235
231
  const namePath = [...filesPath, fileId, 'name'];
232
+ const content = shareDBDoc.data;
236
233
  const text = getAtPath(content, textPath);
237
234
  const name = getAtPath(content, namePath);
238
235
 
@@ -91,13 +91,13 @@ export const CodeEditor = ({
91
91
  // Get the editor corresponding to the active file.
92
92
  // Looks in `editorCache` first, and if not found, creates a new editor.
93
93
  useEffect(() => {
94
+ if (!shareDBDoc) return;
94
95
  let isMounted = true;
95
96
 
96
97
  const initEditor = async () => {
97
98
  const editor = await getOrCreateEditor({
98
99
  fileId: activePane.activeFileId,
99
100
  shareDBDoc,
100
- content,
101
101
  filesPath,
102
102
  localPresence,
103
103
  docPresence,
@@ -1,14 +1,75 @@
1
- // Displays the Vite devserver iframe.
2
- // TODO test this out, think this through.
3
- // Purpose: support folks using VZCode as the editor
4
- // for their Vite-based projects.
5
- const enableIframe = false;
1
+ import { useContext, useEffect, useMemo } from 'react';
2
+ import {
3
+ createRuntime,
4
+ VizHubRuntime,
5
+ } from '@vizhub/runtime';
6
+ import BuildWorker from './buildWorker?worker';
7
+ import { VZCodeContext } from './VZCodeContext';
8
+ import { vizFilesToFileCollection } from '@vizhub/viz-utils';
9
+
10
+ const enableIframe = true;
11
+
12
+ // Singleton runtime instance
13
+ // TODO use refs, and cleanup on unmount
14
+ let runtime: VizHubRuntime = null;
15
+ let isFirstRun = true;
6
16
 
7
17
  export const VZRight = () => {
18
+ // Get access to the current files.
19
+ const { content } = useContext(VZCodeContext);
20
+
21
+ const files = useMemo(
22
+ () =>
23
+ content
24
+ ? vizFilesToFileCollection(content.files)
25
+ : null,
26
+ [content?.files],
27
+ );
28
+
29
+ const isInteracting = content?.isInteracting || false;
30
+
31
+ useEffect(() => {
32
+ if (!files) return;
33
+
34
+ // Initialize the runtime only once
35
+ if (!runtime) {
36
+ const worker = new BuildWorker();
37
+ const iframe = document.getElementById(
38
+ 'viz-iframe',
39
+ ) as HTMLIFrameElement;
40
+ runtime = createRuntime({
41
+ iframe,
42
+ worker,
43
+ setBuildErrorMessage: (error) => {
44
+ if (error) {
45
+ console.error('Build error:', error);
46
+ }
47
+ },
48
+ });
49
+ }
50
+
51
+ // Run code in the iframe
52
+ if (isFirstRun || isInteracting) {
53
+ runtime.run({
54
+ files,
55
+ enableHotReloading: true,
56
+ enableSourcemap: true,
57
+ vizId: 'example-viz',
58
+ });
59
+ isFirstRun = false;
60
+ }
61
+
62
+ // TODO use refs, and cleanup on unmount
63
+ // return () => {
64
+ // runtime.cleanup();
65
+ // worker.terminate();
66
+ // };
67
+ }, [files, isInteracting]);
68
+
8
69
  return (
9
70
  <div className="right">
10
71
  {enableIframe ? (
11
- <iframe src="http://localhost:5173/"></iframe>
72
+ <iframe id="viz-iframe"></iframe>
12
73
  ) : null}
13
74
  </div>
14
75
  );
@@ -1,4 +1,9 @@
1
- import { useRef, useEffect, useCallback } from 'react';
1
+ import {
2
+ useRef,
3
+ useEffect,
4
+ useCallback,
5
+ memo,
6
+ } from 'react';
2
7
  import { Form, Button } from '../../bootstrap';
3
8
 
4
9
  interface ChatInputProps {
@@ -9,7 +14,7 @@ interface ChatInputProps {
9
14
  focused: boolean;
10
15
  }
11
16
 
12
- export const ChatInput = ({
17
+ const ChatInputComponent = ({
13
18
  message,
14
19
  setMessage,
15
20
  onSendMessage,
@@ -35,6 +40,13 @@ export const ChatInput = ({
35
40
  [onSendMessage],
36
41
  );
37
42
 
43
+ const handleChange = useCallback(
44
+ (event: React.ChangeEvent<HTMLTextAreaElement>) => {
45
+ setMessage(event.target.value);
46
+ },
47
+ [setMessage],
48
+ );
49
+
38
50
  return (
39
51
  <div className="ai-chat-input-container">
40
52
  <Form.Group className="ai-chat-input-group">
@@ -42,9 +54,7 @@ export const ChatInput = ({
42
54
  as="textarea"
43
55
  rows={2}
44
56
  value={message}
45
- onChange={(event) =>
46
- setMessage(event.target.value)
47
- }
57
+ onChange={handleChange}
48
58
  onKeyDown={handleKeyDown}
49
59
  ref={inputRef}
50
60
  placeholder="Describe what you'd like me to change in your code..."
@@ -63,3 +73,5 @@ export const ChatInput = ({
63
73
  </div>
64
74
  );
65
75
  };
76
+
77
+ export const ChatInput = memo(ChatInputComponent);
@@ -1,6 +1,7 @@
1
1
  import { timestampToDate } from '@vizhub/viz-utils';
2
2
  import Markdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
+ import { useMemo, memo } from 'react';
4
5
 
5
6
  interface MessageProps {
6
7
  id: string;
@@ -10,27 +11,39 @@ interface MessageProps {
10
11
  isStreaming?: boolean;
11
12
  }
12
13
 
13
- export const Message = ({
14
+ const MessageComponent = ({
14
15
  role,
15
16
  content,
16
17
  timestamp,
17
18
  isStreaming,
18
19
  }: MessageProps) => {
20
+ // Memoize date formatting to avoid repeated computation
21
+ const formattedTime = useMemo(() => {
22
+ return timestampToDate(timestamp).toLocaleTimeString(
23
+ [],
24
+ {
25
+ hour: '2-digit',
26
+ minute: '2-digit',
27
+ },
28
+ );
29
+ }, [timestamp]);
30
+
31
+ // Memoize the className string to avoid recreation
32
+ const messageClassName = useMemo(() => {
33
+ return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
34
+ }, [role, isStreaming]);
19
35
  return (
20
- <div
21
- className={`ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`}
22
- >
36
+ <div className={messageClassName}>
23
37
  <div className="ai-chat-message-content">
24
38
  <Markdown remarkPlugins={[remarkGfm]}>
25
39
  {content}
26
40
  </Markdown>
27
41
  </div>
28
42
  <div className="ai-chat-message-time">
29
- {timestampToDate(timestamp).toLocaleTimeString([], {
30
- hour: '2-digit',
31
- minute: '2-digit',
32
- })}
43
+ {formattedTime}
33
44
  </div>
34
45
  </div>
35
46
  );
36
47
  };
48
+
49
+ export const Message = memo(MessageComponent);
@@ -1,4 +1,9 @@
1
- import { useRef, useEffect } from 'react';
1
+ import {
2
+ useRef,
3
+ useEffect,
4
+ useCallback,
5
+ memo,
6
+ } from 'react';
2
7
  import { Message } from './Message';
3
8
  import { StreamingMessage } from './StreamingMessage';
4
9
  import { TypingIndicator } from './TypingIndicator';
@@ -17,7 +22,7 @@ interface MessageListProps {
17
22
  isLoading: boolean;
18
23
  }
19
24
 
20
- export const MessageList = ({
25
+ const MessageListComponent = ({
21
26
  messages,
22
27
  aiScratchpad,
23
28
  aiStatus,
@@ -25,11 +30,11 @@ export const MessageList = ({
25
30
  }: MessageListProps) => {
26
31
  const messagesEndRef = useRef<HTMLDivElement>(null);
27
32
 
28
- const scrollToBottom = () => {
33
+ const scrollToBottom = useCallback(() => {
29
34
  messagesEndRef.current?.scrollIntoView({
30
35
  behavior: 'smooth',
31
36
  });
32
- };
37
+ }, []);
33
38
 
34
39
  useEffect(() => {
35
40
  scrollToBottom();
@@ -60,3 +65,5 @@ export const MessageList = ({
60
65
  </div>
61
66
  );
62
67
  };
68
+
69
+ export const MessageList = memo(MessageListComponent);
@@ -1,12 +1,13 @@
1
1
  import Markdown from 'react-markdown';
2
2
  import remarkGfm from 'remark-gfm';
3
+ import { memo } from 'react';
3
4
 
4
5
  interface StreamingMessageProps {
5
6
  content: string;
6
7
  status?: string;
7
8
  }
8
9
 
9
- export const StreamingMessage = ({
10
+ const StreamingMessageComponent = ({
10
11
  content,
11
12
  status,
12
13
  }: StreamingMessageProps) => {
@@ -23,3 +24,7 @@ export const StreamingMessage = ({
23
24
  </div>
24
25
  );
25
26
  };
27
+
28
+ export const StreamingMessage = memo(
29
+ StreamingMessageComponent,
30
+ );
@@ -1,4 +1,6 @@
1
- export const TypingIndicator = () => {
1
+ import { memo } from 'react';
2
+
3
+ const TypingIndicatorComponent = () => {
2
4
  return (
3
5
  <div className="ai-chat-message assistant">
4
6
  <div className="ai-chat-message-content">
@@ -11,3 +13,7 @@ export const TypingIndicator = () => {
11
13
  </div>
12
14
  );
13
15
  };
16
+
17
+ export const TypingIndicator = memo(
18
+ TypingIndicatorComponent,
19
+ );
@@ -1,4 +1,9 @@
1
- import { useContext, useState, useCallback } from 'react';
1
+ import {
2
+ useContext,
3
+ useState,
4
+ useCallback,
5
+ useMemo,
6
+ } from 'react';
2
7
  import { VZCodeContext } from '../../VZCodeContext';
3
8
  import { v4 as uuidv4 } from 'uuid';
4
9
  import { MessageList } from './MessageList';
@@ -25,11 +30,15 @@ export const AIChat = () => {
25
30
  const aiScratchpad = currentChat?.aiScratchpad;
26
31
  const aiStatus = currentChat?.aiStatus;
27
32
 
28
- // Transform messages to ensure they have required id field
29
- const messages = rawMessages.map((msg, index) => ({
30
- ...msg,
31
- id: msg.id || `msg-${index}`,
32
- }));
33
+ // Transform messages to ensure they have required id field - memoized to avoid recreation
34
+ const messages = useMemo(
35
+ () =>
36
+ rawMessages.map((msg, index) => ({
37
+ ...msg,
38
+ id: msg.id || `msg-${index}`,
39
+ })),
40
+ [rawMessages],
41
+ );
33
42
 
34
43
  const handleSendMessage = useCallback(async () => {
35
44
  if (!message.trim() || isLoading) return;
@@ -66,7 +75,13 @@ export const AIChat = () => {
66
75
  } finally {
67
76
  setIsLoading(false);
68
77
  }
69
- }, [message, isLoading, currentChatId]);
78
+ }, [
79
+ message,
80
+ isLoading,
81
+ aiChatEndpoint,
82
+ aiChatOptions,
83
+ currentChatId,
84
+ ]);
70
85
 
71
86
  return (
72
87
  <div className="ai-chat-container">
@@ -0,0 +1,3 @@
1
+ // @ts-ignore TODO fix this type error
2
+ import { initWorker } from '@vizhub/runtime/worker';
3
+ initWorker();
@@ -69,7 +69,10 @@ export const createLLMFunction = ({
69
69
 
70
70
  // Define callbacks for streaming parser
71
71
  const callbacks = {
72
- onFileNameChange: (fileName, format) => {
72
+ onFileNameChange: async (
73
+ fileName: string,
74
+ format: string,
75
+ ) => {
73
76
  DEBUG &&
74
77
  console.log(
75
78
  `File changed to: ${fileName} (${format})`,
@@ -111,7 +114,7 @@ export const createLLMFunction = ({
111
114
  'Editing ' + fileName,
112
115
  );
113
116
  },
114
- onCodeLine: (line) => {
117
+ onCodeLine: async (line: string) => {
115
118
  DEBUG && console.log(`Code line: ${line}`);
116
119
 
117
120
  if (currentEditingFileId) {
@@ -154,7 +157,7 @@ export const createLLMFunction = ({
154
157
  }
155
158
  }
156
159
  },
157
- onNonCodeLine: (line) => {
160
+ onNonCodeLine: async (line: string) => {
158
161
  // We want to report a file edited only if the line is not empty,
159
162
  // because sometimes the LLMs leave a newline between the file name
160
163
  // declaration and th
@@ -176,14 +179,14 @@ export const createLLMFunction = ({
176
179
  if (chunk.content) {
177
180
  const chunkContent = String(chunk.content);
178
181
  chunks.push(chunkContent);
179
- parser.processChunk(chunkContent);
182
+ await parser.processChunk(chunkContent);
180
183
  }
181
184
 
182
185
  if (!generationId && chunk.lc_kwargs?.id) {
183
186
  generationId = chunk.lc_kwargs.id;
184
187
  }
185
188
  }
186
- parser.flushRemaining();
189
+ await parser.flushRemaining();
187
190
  reportFileEdited();
188
191
  updateAIStatus(shareDBDoc, chatId, 'Done editing.');
189
192
  updateAIScratchpad(shareDBDoc, chatId, fullContent);