vzcode 1.38.0 → 1.39.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-Cw7RI1uP.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-0y6ZxgSv.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.38.0",
3
+ "version": "1.39.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -139,21 +139,19 @@
139
139
  "body-parser": "^2.2.0",
140
140
  "codemirror": "^6.0.2",
141
141
  "codemirror-copilot": "^0.0.7",
142
- "codemirror-ot": "^4.4.0",
142
+ "codemirror-ot": "^4.5.0",
143
143
  "color-hash": "^2.0.2",
144
144
  "comlink": "^4.4.2",
145
145
  "d3-array": "^3.2.4",
146
146
  "diff-match-patch": "^1.0.5",
147
147
  "dotenv": "^17.2.0",
148
148
  "editcodewithai": "^2.2.0",
149
- "eslint": "^9.31.0",
150
149
  "eslint-linter-browserify": "^9.31.0",
151
150
  "express": "^5.1.0",
152
- "globals": "^16.3.0",
153
151
  "ignore": "^7.0.5",
154
152
  "json0-ot-diff": "^1.1.2",
155
153
  "livekit-server-sdk": "^2.13.1",
156
- "llm-code-format": "^2.0.1",
154
+ "llm-code-format": "^2.1.0",
157
155
  "ngrok": "^5.0.0-beta.2",
158
156
  "npm": "^11.4.2",
159
157
  "open": "^10.2.0",
@@ -171,11 +169,14 @@
171
169
  "ws": "^8.18.3"
172
170
  },
173
171
  "devDependencies": {
172
+ "@eslint/js": "^9.31.0",
174
173
  "@types/react": "^18",
175
174
  "@types/react-dom": "^18",
176
175
  "@vitejs/plugin-react": "^4.7.0",
177
176
  "concurrently": "^9.2.0",
178
177
  "cross-env": "^7.0.3",
178
+ "eslint": "^9.31.0",
179
+ "globals": "^16.3.0",
179
180
  "npm-check-updates": "^18.0.1",
180
181
  "prettier": "^3.6.2",
181
182
  "sass": "^1.89.2",
@@ -176,7 +176,6 @@ export const getOrCreateEditor = async ({
176
176
  usernameRef,
177
177
  customInteractRules,
178
178
  enableAutoFollowRef,
179
- openTab,
180
179
  aiCopilotEndpoint,
181
180
  esLintSource,
182
181
  rainbowBracketsEnabled = true,
@@ -213,7 +212,6 @@ export const getOrCreateEditor = async ({
213
212
  // Ref to a boolean that determines whether to
214
213
  // enable auto-following the cursors of remote users.
215
214
  enableAutoFollowRef: React.MutableRefObject<boolean>;
216
- openTab: (tabState: TabState) => void;
217
215
  aiCopilotEndpoint?: string;
218
216
  esLintSource: (
219
217
  view: EditorView,
@@ -285,7 +283,6 @@ export const getOrCreateEditor = async ({
285
283
  path: textPath,
286
284
  docPresence,
287
285
  enableAutoFollowRef,
288
- openTab,
289
286
  }),
290
287
  );
291
288
  }
@@ -107,7 +107,6 @@ export const CodeEditor = ({
107
107
  usernameRef,
108
108
  customInteractRules,
109
109
  enableAutoFollowRef,
110
- openTab,
111
110
  aiCopilotEndpoint,
112
111
  esLintSource,
113
112
  });
@@ -6,20 +6,14 @@ import {
6
6
  } from '@codemirror/view';
7
7
  import { Annotation, RangeSet } from '@codemirror/state';
8
8
  import { assignUserColor } from '../presenceColor';
9
- import { VizFileId } from '@vizhub/viz-types';
10
9
  import {
11
10
  Presence,
12
11
  PresenceId,
13
- TabState,
14
12
  Username,
15
13
  } from '../../types';
16
14
 
17
- const debug = false;
15
+ const DEBUG = true;
18
16
 
19
- // export let enableAutoFollow = false;
20
- // export const toggleAutoFollowButton = () => {
21
- // enableAutoFollow = !enableAutoFollow;
22
- // };
23
17
  // Deals with receiving the broadcasted presence cursor locations
24
18
  // from other clients and displaying them.
25
19
  //
@@ -32,12 +26,10 @@ export const json1PresenceDisplay = ({
32
26
  path,
33
27
  docPresence,
34
28
  enableAutoFollowRef,
35
- openTab,
36
29
  }: {
37
30
  path: Array<string>;
38
31
  docPresence: any;
39
32
  enableAutoFollowRef: React.MutableRefObject<boolean>;
40
- openTab: (tabState: TabState) => void;
41
33
  }) => [
42
34
  ViewPlugin.fromClass(
43
35
  class {
@@ -68,7 +60,7 @@ export const json1PresenceDisplay = ({
68
60
  docPresence.on(
69
61
  'receive',
70
62
  (id: PresenceId, presence: Presence) => {
71
- if (debug) {
63
+ if (DEBUG) {
72
64
  console.log(
73
65
  `Received presence for id ${id}`,
74
66
  presence,
@@ -94,14 +86,8 @@ export const json1PresenceDisplay = ({
94
86
  // Otherwise, delete the presence state.
95
87
  delete presenceState[id];
96
88
 
97
- // If auto-follow is enabled, and the presence is NOT
98
- // in the current file, then open the tab of the other user.
99
- if (enableAutoFollowRef.current) {
100
- openTab({
101
- fileId: presence.start[1] as VizFileId,
102
- isTransient: true,
103
- });
104
- }
89
+ // Note: Auto-follow tab opening is now handled by usePresenceAutoFollow hook
90
+ // in VZCodeContext, which works independently of CodeMirror extensions
105
91
  }
106
92
  // Update decorations to reflect new presence state.
107
93
  // TODO consider mutating this rather than recomputing it on each change.
@@ -37,6 +37,7 @@ import { useRunCode } from './useRunCode';
37
37
  import { useURLSync } from './useURLSync';
38
38
  import { createInitialState, vzReducer } from './vzReducer';
39
39
  import { findPane } from './vzReducer/findPane';
40
+ import { usePresenceAutoFollow } from './usePresenceAutoFollow';
40
41
 
41
42
  // This context centralizes all the "smart" logic
42
43
  // to do with the application state. This includes
@@ -408,6 +409,16 @@ export const VZCodeProvider = ({
408
409
  const [hoveredItemId, setHoveredItemId] =
409
410
  useState<ItemId | null>(null);
410
411
 
412
+ // Handle presence-based auto-following
413
+ // This hook manages opening tabs when presence is received on files
414
+ // that are not currently open, independent of CodeMirror extensions
415
+ usePresenceAutoFollow({
416
+ docPresence,
417
+ enableAutoFollow,
418
+ openTab,
419
+ activePane,
420
+ });
421
+
411
422
  // Livekit Voice Chat Modal
412
423
 
413
424
  const [voiceChatModalOpen, setVoiceChatModalOpen] =
@@ -224,6 +224,12 @@ export const VZSidebar = ({
224
224
  presenceId: PresenceId,
225
225
  update: Presence,
226
226
  ) => {
227
+ // Sometimes this can be null, so we check.
228
+ // This can happen when the user disconnects.
229
+ // If this happens, we do not update the presence indicator.
230
+ // TODO test removal of presence indicator.
231
+ if (!update) return;
232
+
227
233
  const presenceIndicator: PresenceIndicator = {
228
234
  username: update.username,
229
235
  fileId: update.start[1] as VizFileId,
@@ -378,7 +378,13 @@ export const useKeyboardShortcuts = ({
378
378
  return;
379
379
  }
380
380
 
381
- const position: number = editor.posAtDOM(element);
381
+ let position: number;
382
+ try {
383
+ position = editor.posAtDOM(element);
384
+ } catch (error) {
385
+ // Element is not within the current editor's document
386
+ return;
387
+ }
382
388
  const identifier: SyntaxNode = tree.resolveInner(
383
389
  position,
384
390
  1,
@@ -0,0 +1,86 @@
1
+ import { useContext, useEffect, useRef } from 'react';
2
+ import { VZCodeContext } from './VZCodeContext';
3
+ import { Presence, PresenceId, TabState } from '../types';
4
+ import { VizFileId } from '@vizhub/viz-types';
5
+
6
+ const DEBUG = true;
7
+
8
+ /**
9
+ * Custom hook that handles auto-following users by opening tabs when presence
10
+ * is received on files that are not currently open. This logic is independent
11
+ * of CodeMirror extensions and works regardless of editor cache state.
12
+ */
13
+ export const usePresenceAutoFollow = ({
14
+ docPresence,
15
+ enableAutoFollow,
16
+ openTab,
17
+ activePane,
18
+ }) => {
19
+ // Track enableAutoFollow on a ref for use in event handlers
20
+ const enableAutoFollowRef = useRef(enableAutoFollow);
21
+ useEffect(() => {
22
+ enableAutoFollowRef.current = enableAutoFollow;
23
+ }, [enableAutoFollow]);
24
+
25
+ // Track active file for comparison
26
+ const activeFileIdRef = useRef(activePane?.activeFileId);
27
+ useEffect(() => {
28
+ activeFileIdRef.current = activePane?.activeFileId;
29
+ }, [activePane?.activeFileId]);
30
+
31
+ useEffect(() => {
32
+ if (!docPresence) return;
33
+
34
+ const handlePresenceReceive = (
35
+ id: PresenceId,
36
+ presence: Presence,
37
+ ) => {
38
+ if (DEBUG) {
39
+ console.log(
40
+ `[usePresenceAutoFollow] Received presence for id ${id}`,
41
+ presence,
42
+ );
43
+ }
44
+
45
+ // If presence is null, the user has disconnected
46
+ if (!presence) {
47
+ return;
48
+ }
49
+
50
+ // If auto-follow is not enabled, do nothing
51
+ if (!enableAutoFollowRef.current) {
52
+ return;
53
+ }
54
+
55
+ // Extract the file ID from the presence path
56
+ // Presence path structure: [path, to, file, fileId, position]
57
+ // We want the fileId which should be at index 1 for files
58
+ const presenceFileId = presence.start[1] as VizFileId;
59
+
60
+ // If the presence is for the currently active file, do nothing
61
+ // (the CodeMirror extension will handle displaying the cursor)
62
+ if (presenceFileId === activeFileIdRef.current) {
63
+ return;
64
+ }
65
+
66
+ // If the presence is for a different file, open that tab
67
+ DEBUG &&
68
+ console.log(
69
+ `[usePresenceAutoFollow] Auto-opening tab for ${presence.username} in file ${presenceFileId}`,
70
+ );
71
+
72
+ openTab({
73
+ fileId: presenceFileId,
74
+ isTransient: true,
75
+ });
76
+ };
77
+
78
+ // Subscribe to presence changes
79
+ docPresence.on('receive', handlePresenceReceive);
80
+
81
+ // Cleanup subscription on unmount
82
+ return () => {
83
+ docPresence.off('receive', handlePresenceReceive);
84
+ };
85
+ }, [docPresence, openTab]);
86
+ };
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { SubmitOperation } from '../types';
3
- import { VizContent } from '@vizhub/viz-types';
3
+ import { createRunCodeFunction } from '../runCode';
4
4
 
5
5
  // Runs the code by flashing `isInteracting` to `true`.
6
6
  export const useRunCode = (
@@ -8,21 +8,7 @@ export const useRunCode = (
8
8
  ) => {
9
9
  const runCodeRef = useRef(null);
10
10
  useEffect(() => {
11
- const runCode = () => {
12
- submitOperation((content: VizContent) => ({
13
- ...content,
14
- isInteracting: true,
15
- }));
16
- setTimeout(() => {
17
- // This somewhat cryptic logic
18
- // deletes the `isInteracting` property
19
- // from the document.
20
- submitOperation(
21
- ({ isInteracting, ...newDocument }) =>
22
- newDocument,
23
- );
24
- }, 0);
25
- };
11
+ const runCode = createRunCodeFunction(submitOperation);
26
12
  runCodeRef.current = runCode;
27
13
  }, [submitOperation]);
28
14
  return runCodeRef;
@@ -1,22 +1,15 @@
1
1
  import { useCallback } from 'react';
2
2
  import { ShareDBDoc } from '../types'; // Assuming ShareDBDoc is already generic
3
- import { diff } from './diff';
3
+ import { createSubmitOperation } from '../submitOperation';
4
4
 
5
5
  // A generic helper function to submit diff-based operations to ShareDB
6
6
  export const useSubmitOperation = <T>(
7
7
  shareDBDoc: ShareDBDoc<T>,
8
8
  ): ((next: (data: T) => T) => void) => {
9
9
  const submitOperation: (next: (data: T) => T) => void =
10
- useCallback(
11
- (next) => {
12
- const data: T = shareDBDoc.data;
13
- const op = diff(data, next(data));
14
- if (op && shareDBDoc) {
15
- shareDBDoc.submitOp(op);
16
- }
17
- },
18
- [shareDBDoc],
19
- );
10
+ useCallback(() => {
11
+ return createSubmitOperation(shareDBDoc);
12
+ }, [shareDBDoc])();
20
13
 
21
14
  return submitOperation;
22
15
  };
package/src/runCode.js ADDED
@@ -0,0 +1,48 @@
1
+ import { createSubmitOperation } from './submitOperation.js';
2
+ // TODO migrate the server-side code to use TypeScript,
3
+ // and delete this file once the migration is complete (use src/runCode.ts instead).
4
+ /**
5
+ * Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
6
+ * This works for both client-side (with submitOperation) and server-side (with ShareDB document).
7
+ */
8
+ export const createRunCodeFunction = (
9
+ submitOperationOrDoc,
10
+ ) => {
11
+ return () => {
12
+ let submitOperation;
13
+
14
+ // Check if this is a client-side submitOperation or server-side ShareDB document
15
+ if (typeof submitOperationOrDoc === 'function') {
16
+ // Client-side: already a submitOperation function
17
+ submitOperation = submitOperationOrDoc;
18
+ } else {
19
+ // Server-side: create submitOperation from ShareDB document
20
+ submitOperation = createSubmitOperation(
21
+ submitOperationOrDoc,
22
+ );
23
+ }
24
+
25
+ // Use the unified submitOperation approach for both client and server
26
+ submitOperation((content) => ({
27
+ ...content,
28
+ isInteracting: true,
29
+ }));
30
+
31
+ setTimeout(() => {
32
+ submitOperation((content) => ({
33
+ ...content,
34
+ isInteracting: false,
35
+ }));
36
+ }, 0);
37
+ };
38
+ };
39
+
40
+ /**
41
+ * Creates a runCodeRef object compatible with React refs.
42
+ * This is useful for maintaining compatibility with existing code that expects a ref.
43
+ */
44
+ export const createRunCodeRef = (submitOperationOrDoc) => {
45
+ return {
46
+ current: createRunCodeFunction(submitOperationOrDoc),
47
+ };
48
+ };
package/src/runCode.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { SubmitOperation } from './types';
2
+ import { VizContent } from '@vizhub/viz-types';
3
+ import { createSubmitOperation } from './submitOperation';
4
+
5
+ /**
6
+ * Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
7
+ * This works for both client-side (with submitOperation) and server-side (with ShareDB document).
8
+ */
9
+ export const createRunCodeFunction = (
10
+ submitOperationOrDoc:
11
+ | SubmitOperation
12
+ | { data: any; submitOp: (ops: any) => void },
13
+ ) => {
14
+ return () => {
15
+ let submitOperation: SubmitOperation;
16
+
17
+ // Check if this is a client-side submitOperation or server-side ShareDB document
18
+ if (typeof submitOperationOrDoc === 'function') {
19
+ // Client-side: already a submitOperation function
20
+ submitOperation =
21
+ submitOperationOrDoc as SubmitOperation;
22
+ } else {
23
+ // Server-side: create submitOperation from ShareDB document
24
+ submitOperation = createSubmitOperation(
25
+ submitOperationOrDoc,
26
+ );
27
+ }
28
+
29
+ // Use the unified submitOperation approach for both client and server
30
+ submitOperation((content: VizContent) => ({
31
+ ...content,
32
+ isInteracting: true,
33
+ }));
34
+
35
+ setTimeout(() => {
36
+ // This somewhat cryptic logic
37
+ // deletes the `isInteracting` property
38
+ // from the document.
39
+ submitOperation(
40
+ ({ isInteracting, ...newDocument }) => newDocument,
41
+ );
42
+ }, 0);
43
+ };
44
+ };
45
+
46
+ /**
47
+ * Creates a runCodeRef object compatible with React refs.
48
+ * This is useful for maintaining compatibility with existing code that expects a ref.
49
+ */
50
+ export const createRunCodeRef = (
51
+ submitOperationOrDoc:
52
+ | SubmitOperation
53
+ | { data: any; submitOp: (ops: any) => void },
54
+ ) => {
55
+ return {
56
+ current: createRunCodeFunction(submitOperationOrDoc),
57
+ };
58
+ };
@@ -1,42 +1,44 @@
1
- import { performAiEdit } from 'editcodewithai';
2
1
  import {
3
- clearAIScratchpadAndStatus,
4
- updateFiles,
5
- setIsInteracting,
6
- } from './chatOperations.js';
2
+ assembleFullPrompt,
3
+ prepareFilesForPrompt,
4
+ } from 'editcodewithai';
5
+ import { formatMarkdownFiles } from 'llm-code-format';
7
6
 
8
7
  /**
9
- * Performs AI editing operations on the files
8
+ * Performs AI editing operations using streaming with incremental OT operations
10
9
  */
11
- export const performAIEditing = async (
10
+ export const performAIEditing = async ({
11
+ prompt,
12
12
  shareDBDoc,
13
- chatId,
14
- content,
15
- files,
16
13
  llmFunction,
17
- ) => {
18
- const editResult = await performAiEdit({
19
- prompt: content,
20
- files,
21
- llmFunction,
22
- apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
23
- });
24
-
25
- // Clear the scratchpad and update status
26
- clearAIScratchpadAndStatus(
27
- shareDBDoc,
28
- chatId,
29
- 'Done editing with AI.',
14
+ runCode,
15
+ }) => {
16
+ const preparedFiles = prepareFilesForPrompt(
17
+ shareDBDoc.data.files,
30
18
  );
19
+ const filesContext = formatMarkdownFiles(preparedFiles);
31
20
 
32
- // Apply AI edits to files
33
- updateFiles(shareDBDoc, editResult.changedFiles);
21
+ // 2. Assemble the final prompt
22
+ const fullPrompt = assembleFullPrompt({
23
+ filesContext,
24
+ prompt,
25
+ editFormat: 'whole',
26
+ });
27
+
28
+ // Call the LLM function which will handle streaming and incremental file updates
29
+ const result = await llmFunction(fullPrompt);
34
30
 
35
- // Wait for propagation
36
- await new Promise((resolve) => setTimeout(resolve, 100));
31
+ // Clear the scratchpad and update status
32
+ // clearAIScratchpadAndStatus(
33
+ // shareDBDoc,
34
+ // chatId,
35
+ // 'Done editing with AI.',
36
+ // );
37
37
 
38
- // Unset isInteracting
39
- setIsInteracting(shareDBDoc, false);
38
+ runCode();
40
39
 
41
- return editResult;
40
+ return {
41
+ content: result.content,
42
+ generationId: result.generationId,
43
+ };
42
44
  };