vzcode 1.37.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,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-BRU-VQpy.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-lvLw6dCW.css">
23
+ <script type="module" crossorigin src="/assets/index-0y6ZxgSv.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-DKLiE98a.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.37.0",
3
+ "version": "1.39.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -114,7 +114,7 @@
114
114
  "@codemirror/state": "^6.5.2",
115
115
  "@codemirror/theme-one-dark": "^6.1.3",
116
116
  "@codemirror/view": "^6.38.1",
117
- "@langchain/core": "^0.3.65",
117
+ "@langchain/core": "^0.3.66",
118
118
  "@langchain/openai": "^0.6.2",
119
119
  "@lezer/highlight": "^1.2.1",
120
120
  "@livekit/components-react": "^2.9.14",
@@ -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
- "editcodewithai": "^2.0.0",
149
- "eslint": "^9.31.0",
148
+ "editcodewithai": "^2.2.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] =
@@ -0,0 +1,124 @@
1
+ import {
2
+ formatMarkdownFiles,
3
+ parseMarkdownFiles,
4
+ } from 'llm-code-format';
5
+ import {
6
+ FORMAT_INSTRUCTIONS,
7
+ mergeFileChanges,
8
+ } from 'editcodewithai';
9
+ import { VizFiles } from '@vizhub/viz-types';
10
+ import { vizFilesToFileCollection } from '@vizhub/viz-utils';
11
+
12
+ export const createAICopyPasteHandlers = (
13
+ files: VizFiles,
14
+ submitOperation: any,
15
+ setCopyButtonText: (text: string) => void,
16
+ setPasteButtonText: (text: string) => void,
17
+ ) => {
18
+ // Copy for AI - formats all files and copies to clipboard
19
+ const handleCopyForAI = async () => {
20
+ if (!files) return;
21
+
22
+ try {
23
+ const fileCollection =
24
+ vizFilesToFileCollection(files);
25
+
26
+ // Format files for AI consumption
27
+ const formattedFiles =
28
+ formatMarkdownFiles(fileCollection) +
29
+ '\n\n' +
30
+ FORMAT_INSTRUCTIONS.whole;
31
+
32
+ // Copy to clipboard
33
+ await navigator.clipboard.writeText(formattedFiles);
34
+
35
+ // Show success feedback
36
+ setCopyButtonText('Copied!');
37
+ setTimeout(
38
+ () => setCopyButtonText('Copy for AI'),
39
+ 2000,
40
+ );
41
+ } catch (error) {
42
+ console.error('Failed to copy files for AI:', error);
43
+ setCopyButtonText('Error');
44
+ setTimeout(
45
+ () => setCopyButtonText('Copy for AI'),
46
+ 2000,
47
+ );
48
+ }
49
+ };
50
+
51
+ // Paste for AI - parses clipboard content and updates files
52
+ const handlePasteForAI = async () => {
53
+ if (!submitOperation) return;
54
+
55
+ try {
56
+ // Read from clipboard
57
+ const clipboardText =
58
+ await navigator.clipboard.readText();
59
+
60
+ // Handle various line endings
61
+ const normalized = clipboardText.replace(
62
+ /\r\n?/g,
63
+ '\n',
64
+ );
65
+
66
+ if (!normalized.trim()) {
67
+ setPasteButtonText('Empty clipboard');
68
+ setTimeout(
69
+ () => setPasteButtonText('Paste for AI'),
70
+ 2000,
71
+ );
72
+ return;
73
+ }
74
+
75
+ // Parse the markdown files format
76
+ const parsed = parseMarkdownFiles(normalized, 'bold');
77
+
78
+ if (
79
+ parsed.files &&
80
+ Object.keys(parsed.files).length > 0
81
+ ) {
82
+ // Use mergeFileChanges utility from editcodewithai
83
+ const mergedFiles = mergeFileChanges(
84
+ files,
85
+ parsed.files,
86
+ );
87
+
88
+ // Update files using the submit operation
89
+ submitOperation((document) => ({
90
+ ...document,
91
+ files: mergedFiles,
92
+ }));
93
+
94
+ const fileCount = Object.keys(parsed.files).length;
95
+ setPasteButtonText('Pasted!');
96
+ setTimeout(
97
+ () => setPasteButtonText('Paste for AI'),
98
+ 2000,
99
+ );
100
+ } else {
101
+ setPasteButtonText('No files found');
102
+ setTimeout(
103
+ () => setPasteButtonText('Paste for AI'),
104
+ 2000,
105
+ );
106
+ }
107
+ } catch (error) {
108
+ console.error(
109
+ 'Failed to paste files from AI:',
110
+ error,
111
+ );
112
+ setPasteButtonText('Error');
113
+ setTimeout(
114
+ () => setPasteButtonText('Paste for AI'),
115
+ 2000,
116
+ );
117
+ }
118
+ };
119
+
120
+ return {
121
+ handleCopyForAI,
122
+ handlePasteForAI,
123
+ };
124
+ };
@@ -35,15 +35,13 @@ import { AIChat } from './AIChat';
35
35
  import { Listing } from './Listing';
36
36
  import { Search } from './Search';
37
37
  import { useDragAndDrop } from './useDragAndDrop';
38
+ import { createAICopyPasteHandlers } from './aiCopyPaste';
38
39
  import {
39
40
  enableLiveKit,
40
41
  enableAIChat,
41
42
  } from '../featureFlags';
42
43
  import './styles.scss';
43
44
 
44
- // TODO turn this UI back on when we are actually detecting
45
- // the connection status.
46
- // See https://github.com/vizhub-core/vzcode/issues/456
47
45
  const enableConnectionStatus = true;
48
46
 
49
47
  export const VZSidebar = ({
@@ -146,10 +144,8 @@ export const VZSidebar = ({
146
144
  toggleAutoFollow,
147
145
  docPresence,
148
146
  updatePresenceIndicator,
149
- sidebarPresenceIndicators,
150
- liveKitConnection,
151
- setLiveKitConnection,
152
147
  setVoiceChatModalOpen,
148
+ submitOperation,
153
149
  } = useContext(VZCodeContext);
154
150
 
155
151
  const fileTree = useMemo(
@@ -163,6 +159,24 @@ export const VZSidebar = ({
163
159
  setIsSettingsOpen(true);
164
160
  }, []);
165
161
 
162
+ // State for AI operation feedback
163
+ const [copyButtonText, setCopyButtonText] =
164
+ useState('Copy for AI');
165
+ const [pasteButtonText, setPasteButtonText] =
166
+ useState('Paste for AI');
167
+
168
+ // Create AI copy/paste handlers
169
+ const { handleCopyForAI, handlePasteForAI } = useMemo(
170
+ () =>
171
+ createAICopyPasteHandlers(
172
+ files,
173
+ submitOperation,
174
+ setCopyButtonText,
175
+ setPasteButtonText,
176
+ ),
177
+ [files, submitOperation],
178
+ );
179
+
166
180
  const { sidebarWidth } = useContext(
167
181
  SplitPaneResizeContext,
168
182
  );
@@ -210,17 +224,17 @@ export const VZSidebar = ({
210
224
  presenceId: PresenceId,
211
225
  update: Presence,
212
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
+
213
233
  const presenceIndicator: PresenceIndicator = {
214
234
  username: update.username,
215
235
  fileId: update.start[1] as VizFileId,
216
236
  };
217
237
 
218
- // console.log('Got presence!');
219
- // // console.log({presenceId,update})
220
- // console.log(
221
- // JSON.stringify(presenceIndicator, null, 2),
222
- // );
223
-
224
238
  updatePresenceIndicator(presenceIndicator);
225
239
  };
226
240
 
@@ -532,6 +546,24 @@ export const VZSidebar = ({
532
546
  )}
533
547
  </div>
534
548
  </div>
549
+ {!isAIChatOpen && !isSearchOpen && filesExist && (
550
+ <div className="ai-buttons">
551
+ <button
552
+ className="ai-button copy-button"
553
+ onClick={handleCopyForAI}
554
+ title="Copy files formatted for AI"
555
+ >
556
+ {copyButtonText}
557
+ </button>
558
+ <button
559
+ className="ai-button paste-button"
560
+ onClick={handlePasteForAI}
561
+ title="Paste files from AI"
562
+ >
563
+ {pasteButtonText}
564
+ </button>
565
+ </div>
566
+ )}
535
567
  {enableConnectionStatus && (
536
568
  <div className="connection-status">
537
569
  {isConnecting
@@ -297,6 +297,55 @@
297
297
  }
298
298
  }
299
299
  }
300
+
301
+ .ai-buttons {
302
+ display: flex;
303
+ flex-direction: column;
304
+ gap: 8px;
305
+ padding: 10px 10px 0 10px;
306
+ border-top: 1px solid var(--vh-color-neutral-02);
307
+
308
+ .ai-button {
309
+ padding: 8px 12px;
310
+ border: 1px solid var(--vh-color-neutral-03);
311
+ border-radius: 6px;
312
+ background: var(--vh-color-neutral-01);
313
+ color: var(--vh-color-neutral-04);
314
+ font-size: 12px;
315
+ font-weight: 500;
316
+ font-family: var(--vzcode-font-family);
317
+ cursor: pointer;
318
+ transition: all 0.2s ease;
319
+
320
+ &:hover {
321
+ background: var(--vh-color-hover-dark);
322
+ border-color: var(--vh-color-neutral-04);
323
+ }
324
+
325
+ &:active {
326
+ background: var(--vh-color-neutral-03);
327
+ }
328
+
329
+ &.copy-button {
330
+ border-color: var(--vh-color-primary-03);
331
+
332
+ &:hover {
333
+ background: var(--vh-color-hover-dark);
334
+ border-color: var(--vh-color-primary-04);
335
+ }
336
+ }
337
+
338
+ &.paste-button {
339
+ border-color: var(--vh-color-success-03);
340
+
341
+ &:hover {
342
+ background: var(--vh-color-hover-dark);
343
+ border-color: var(--vh-color-success-04);
344
+ }
345
+ }
346
+ }
347
+ }
348
+
300
349
  .name {
301
350
  display: flex;
302
351
  align-items: center;
@@ -14,5 +14,6 @@ export {
14
14
  } from './SplitPaneResizeContext';
15
15
  export { useSubmitOperation } from './useSubmitOperation';
16
16
  export { shouldTriggerRun } from './shouldTriggerRun';
17
+ export { mergeFileChanges } from 'editcodewithai';
17
18
 
18
19
  export * from './Icons';
@@ -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
+ };