vzcode 2.3.0 → 2.4.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.
Files changed (196) hide show
  1. package/package.json +3 -2
  2. package/src/cli.ts +208 -0
  3. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
  4. package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
  5. package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
  6. package/src/client/AIAssist/Spinner.jsx +76 -0
  7. package/src/client/AIAssist/formatFile.ts +27 -0
  8. package/src/client/AIAssist/generateFilesContext.ts +25 -0
  9. package/src/client/AIAssist/index.ts +2 -0
  10. package/src/client/AIAssist/startAIAssist.ts +95 -0
  11. package/src/client/App/index.tsx +152 -0
  12. package/src/client/App/style.scss +82 -0
  13. package/src/client/App/usePending.ts +51 -0
  14. package/src/client/App/useShareDB.ts +161 -0
  15. package/src/client/CodeEditor/Copilot/index.ts +74 -0
  16. package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
  17. package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
  18. package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
  19. package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
  20. package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
  21. package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
  22. package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
  23. package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
  24. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
  25. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
  26. package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
  27. package/src/client/CodeEditor/basicSetup.ts +73 -0
  28. package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
  29. package/src/client/CodeEditor/index.tsx +202 -0
  30. package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
  31. package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
  32. package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
  33. package/src/client/CodeEditor/style.scss +156 -0
  34. package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
  35. package/src/client/CodeErrorOverlay/index.tsx +77 -0
  36. package/src/client/CodeErrorOverlay/style.scss +33 -0
  37. package/src/client/Fonts/fonts.ts +8 -0
  38. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  39. package/src/client/Icons/BugSVG.tsx +20 -0
  40. package/src/client/Icons/CloseSVG.tsx +19 -0
  41. package/src/client/Icons/CssSVG.tsx +16 -0
  42. package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
  43. package/src/client/Icons/EditSVG.tsx +19 -0
  44. package/src/client/Icons/FileSVG.tsx +19 -0
  45. package/src/client/Icons/FolderSVG.tsx +20 -0
  46. package/src/client/Icons/GearSVG.tsx +16 -0
  47. package/src/client/Icons/HtmlSVG.tsx +16 -0
  48. package/src/client/Icons/JavaScriptSVG.tsx +16 -0
  49. package/src/client/Icons/JsonSVG.tsx +16 -0
  50. package/src/client/Icons/MarkdownSVG.tsx +16 -0
  51. package/src/client/Icons/MicOffSVG.tsx +35 -0
  52. package/src/client/Icons/MicSVG.tsx +25 -0
  53. package/src/client/Icons/NewSVG.tsx +19 -0
  54. package/src/client/Icons/PinSVG.tsx +14 -0
  55. package/src/client/Icons/PlaySVG.tsx +16 -0
  56. package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
  57. package/src/client/Icons/ReactSVG.tsx +16 -0
  58. package/src/client/Icons/SearchSVG.tsx +20 -0
  59. package/src/client/Icons/SparklesSVG.tsx +23 -0
  60. package/src/client/Icons/SplitEditorSVG.tsx +34 -0
  61. package/src/client/Icons/StopSVG.tsx +15 -0
  62. package/src/client/Icons/SvelteSVG.tsx +16 -0
  63. package/src/client/Icons/TrashSVG.tsx +19 -0
  64. package/src/client/Icons/TypeScriptSVG.tsx +16 -0
  65. package/src/client/Icons/index.tsx +26 -0
  66. package/src/client/ImageViewer/index.tsx +91 -0
  67. package/src/client/ImageViewer/style.scss +41 -0
  68. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  69. package/src/client/PresenceNotifications/index.tsx +158 -0
  70. package/src/client/PresenceNotifications/style.scss +17 -0
  71. package/src/client/RunCodeWidget/index.tsx +117 -0
  72. package/src/client/RunCodeWidget/style.scss +24 -0
  73. package/src/client/SplitPaneResizeContext.tsx +159 -0
  74. package/src/client/TabList/Tab.tsx +73 -0
  75. package/src/client/TabList/index.tsx +35 -0
  76. package/src/client/TabList/style.scss +51 -0
  77. package/src/client/VZCodeContext/index.tsx +29 -0
  78. package/src/client/VZCodeContext/types.ts +248 -0
  79. package/src/client/VZCodeContext/useVZCodeState.ts +602 -0
  80. package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
  81. package/src/client/VZLeft.tsx +28 -0
  82. package/src/client/VZMiddle.tsx +172 -0
  83. package/src/client/VZResizer/index.tsx +128 -0
  84. package/src/client/VZResizer/styles.scss +33 -0
  85. package/src/client/VZRight.tsx +113 -0
  86. package/src/client/VZSettings.tsx +275 -0
  87. package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
  88. package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
  89. package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
  90. package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
  91. package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
  92. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
  93. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
  94. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
  95. package/src/client/VZSidebar/AIChat/index.tsx +357 -0
  96. package/src/client/VZSidebar/AIChat/styles.scss +646 -0
  97. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
  98. package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
  99. package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
  100. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
  101. package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
  102. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  103. package/src/client/VZSidebar/FileListing.tsx +91 -0
  104. package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
  105. package/src/client/VZSidebar/Item.tsx +237 -0
  106. package/src/client/VZSidebar/Listing.tsx +47 -0
  107. package/src/client/VZSidebar/Search.tsx +527 -0
  108. package/src/client/VZSidebar/VisualEditor.tsx +282 -0
  109. package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
  110. package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
  111. package/src/client/VZSidebar/index.tsx +728 -0
  112. package/src/client/VZSidebar/styles.scss +612 -0
  113. package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
  114. package/src/client/bootstrap.ts +26 -0
  115. package/src/client/buildWorker.ts +3 -0
  116. package/src/client/featureFlags.ts +8 -0
  117. package/src/client/generateRequestId.ts +6 -0
  118. package/src/client/getFileTree.ts +53 -0
  119. package/src/client/index.ts +19 -0
  120. package/src/client/main.jsx +10 -0
  121. package/src/client/presenceColor.ts +8 -0
  122. package/src/client/shouldTriggerRun.ts +31 -0
  123. package/src/client/sortFileTree.ts +21 -0
  124. package/src/client/tabsSearchParameters.test.ts +79 -0
  125. package/src/client/tabsSearchParameters.ts +137 -0
  126. package/src/client/themes/index.ts +7 -0
  127. package/src/client/themes/themes.ts +77 -0
  128. package/src/client/themes/uiwImports.ts +32 -0
  129. package/src/client/themes/useDynamicTheme.ts +29 -0
  130. package/src/client/themes/vizhubTheme/colors.ts +80 -0
  131. package/src/client/themes/vizhubTheme/index.ts +193 -0
  132. package/src/client/useActions.ts +288 -0
  133. package/src/client/useESLint/index.ts +72 -0
  134. package/src/client/useESLint/worker.ts +113 -0
  135. package/src/client/useEditorCache.ts +35 -0
  136. package/src/client/useFileCRUD.ts +228 -0
  137. package/src/client/useKeyboardShortcuts.ts +451 -0
  138. package/src/client/useOpenDirectories.ts +99 -0
  139. package/src/client/usePresenceAutoFollow.ts +85 -0
  140. package/src/client/usePrettier/index.ts +178 -0
  141. package/src/client/usePrettier/worker.ts +109 -0
  142. package/src/client/useRunCode.tsx +15 -0
  143. package/src/client/useRuntimeError.ts +32 -0
  144. package/src/client/useSubmitOperation.ts +15 -0
  145. package/src/client/useURLSync.ts +137 -0
  146. package/src/client/usernameLocalStorage.ts +43 -0
  147. package/src/client/utils/fileExtension.ts +10 -0
  148. package/src/client/utils/isImageFile.ts +12 -0
  149. package/src/client/vzReducer/aiChatReducer.ts +44 -0
  150. package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
  151. package/src/client/vzReducer/closeTabsReducer.ts +96 -0
  152. package/src/client/vzReducer/createInitialState.ts +38 -0
  153. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  154. package/src/client/vzReducer/findPane.ts +19 -0
  155. package/src/client/vzReducer/index.ts +250 -0
  156. package/src/client/vzReducer/openTabReducer.test.ts +127 -0
  157. package/src/client/vzReducer/openTabReducer.ts +74 -0
  158. package/src/client/vzReducer/searchReducer.test.ts +292 -0
  159. package/src/client/vzReducer/searchReducer.ts +181 -0
  160. package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
  161. package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
  162. package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
  163. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  164. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  165. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  166. package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
  167. package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
  168. package/src/client/vzReducer/updatePane.ts +40 -0
  169. package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
  170. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  171. package/src/ot.ts +24 -0
  172. package/src/randomId.ts +15 -0
  173. package/src/reactRouterExports.ts +15 -0
  174. package/src/runCode.ts +27 -0
  175. package/src/server/aiChatHandler/aiEditing.ts +151 -0
  176. package/src/server/aiChatHandler/chatOperations.ts +476 -0
  177. package/src/server/aiChatHandler/errorHandling.ts +66 -0
  178. package/src/server/aiChatHandler/index.ts +199 -0
  179. package/src/server/aiChatHandler/llmStreaming.ts +419 -0
  180. package/src/server/aiChatHandler/validation.ts +24 -0
  181. package/src/server/computeInitialDocument.ts +220 -0
  182. package/src/server/config.ts +24 -0
  183. package/src/server/featureFlags.ts +8 -0
  184. package/src/server/generateAIResponse.ts +188 -0
  185. package/src/server/handleAIAssist.ts +53 -0
  186. package/src/server/handleAIChatMessage.ts +2 -0
  187. package/src/server/handleAICopilot.ts +97 -0
  188. package/src/server/index.ts +379 -0
  189. package/src/server/isDirectory.ts +1 -0
  190. package/src/server/livekit.ts +24 -0
  191. package/src/server/prettier.ts +115 -0
  192. package/src/server/setupEnv.ts +8 -0
  193. package/src/submitOperation.ts +25 -0
  194. package/src/types.ts +205 -0
  195. package/src/utils/fileDiff.ts +117 -0
  196. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,199 @@
1
+ import { validateRequest } from './validation.js';
2
+ import {
3
+ ensureChatsExist,
4
+ ensureChatExists,
5
+ addUserMessage,
6
+ addDiffToAIMessage,
7
+ setAIStatus,
8
+ } from './chatOperations.js';
9
+ import { createLLMFunction } from './llmStreaming.js';
10
+ import {
11
+ performAIEditing,
12
+ performAIChat,
13
+ } from './aiEditing.js';
14
+ import {
15
+ handleError,
16
+ handleBackgroundError,
17
+ } from './errorHandling.js';
18
+ import { createRunCodeFunction } from '../../runCode.js';
19
+ import { ShareDBDoc } from '../../types.js';
20
+ import { VizContent } from '@vizhub/viz-types';
21
+ import { createSubmitOperation } from '../../submitOperation.js';
22
+
23
+ const DEBUG = false;
24
+
25
+ export const handleAIChatMessage =
26
+ ({
27
+ shareDBDoc,
28
+ createAIEditLocalPresence,
29
+ onCreditDeduction,
30
+ getCurrentCommitId,
31
+ model,
32
+ aiRequestOptions,
33
+ }: {
34
+ shareDBDoc: ShareDBDoc<VizContent>;
35
+ createAIEditLocalPresence: () => any;
36
+ onCreditDeduction?: any;
37
+ getCurrentCommitId?: () => string | null;
38
+ model?: string;
39
+ aiRequestOptions?: any;
40
+ }) =>
41
+ async (req: any, res: any) => {
42
+ const { content, chatId, mode = 'edit' } = req.body;
43
+
44
+ if (DEBUG) {
45
+ console.log(
46
+ '[handleAIChatMessage] content:',
47
+ content,
48
+ 'chatId:',
49
+ chatId,
50
+ 'shareDBDoc:',
51
+ shareDBDoc,
52
+ );
53
+ }
54
+
55
+ // Validate request
56
+ if (!validateRequest(req, res)) {
57
+ return;
58
+ }
59
+
60
+ try {
61
+ // Ensure chats structure exists
62
+ ensureChatsExist(shareDBDoc);
63
+ ensureChatExists(shareDBDoc, chatId);
64
+
65
+ // Add user message to chat
66
+ addUserMessage(shareDBDoc, chatId, content);
67
+
68
+ // Return success immediately - AI generation continues in background
69
+ res.status(200).json('success');
70
+
71
+ // Set AI status to indicate generation is starting
72
+ setAIStatus(shareDBDoc, chatId, 'generating');
73
+
74
+ // Continue AI processing in background (don't await)
75
+ processAIRequestAsync({
76
+ shareDBDoc,
77
+ chatId,
78
+ content,
79
+ mode,
80
+ createAIEditLocalPresence,
81
+ getCurrentCommitId,
82
+ model,
83
+ aiRequestOptions,
84
+ onCreditDeduction,
85
+ }).catch((error) => {
86
+ console.error(
87
+ 'Background AI processing error:',
88
+ error,
89
+ );
90
+ // Handle error without HTTP response
91
+ handleBackgroundError(shareDBDoc, chatId, error);
92
+ });
93
+ } catch (error) {
94
+ handleError(shareDBDoc, chatId, error, res);
95
+ }
96
+ };
97
+
98
+ /**
99
+ * Processes the AI request asynchronously in the background
100
+ */
101
+ const processAIRequestAsync = async ({
102
+ shareDBDoc,
103
+ chatId,
104
+ content,
105
+ mode,
106
+ createAIEditLocalPresence,
107
+ getCurrentCommitId,
108
+ model,
109
+ aiRequestOptions,
110
+ onCreditDeduction,
111
+ }: {
112
+ shareDBDoc: ShareDBDoc<VizContent>;
113
+ chatId: string;
114
+ content: string;
115
+ mode: string;
116
+ createAIEditLocalPresence: () => any;
117
+ getCurrentCommitId?: () => string | null;
118
+ model?: string;
119
+ aiRequestOptions?: any;
120
+ onCreditDeduction?: any;
121
+ }) => {
122
+ try {
123
+ // Capture the current commit ID before making changes (for VizHub integration)
124
+ const beforeCommitId = getCurrentCommitId
125
+ ? getCurrentCommitId()
126
+ : null;
127
+
128
+ // Create LLM function for streaming
129
+ const llmFunction = createLLMFunction({
130
+ shareDBDoc,
131
+ createAIEditLocalPresence,
132
+ chatId,
133
+ model,
134
+ aiRequestOptions,
135
+ });
136
+
137
+ // Create server-side runCode function using shareDBDoc
138
+ const submitOperation =
139
+ createSubmitOperation(shareDBDoc);
140
+ const runCode = createRunCodeFunction(submitOperation);
141
+
142
+ // Perform AI editing or chat based on mode
143
+ const editResult =
144
+ mode === 'ask'
145
+ ? await performAIChat({
146
+ prompt: content,
147
+ shareDBDoc,
148
+ llmFunction,
149
+ })
150
+ : await performAIEditing({
151
+ prompt: content,
152
+ shareDBDoc,
153
+ llmFunction,
154
+ runCode,
155
+ });
156
+
157
+ // Add diff data to the AI message if there are changes
158
+ if (
159
+ editResult.diffData &&
160
+ Object.keys(editResult.diffData).length > 0
161
+ ) {
162
+ addDiffToAIMessage(
163
+ shareDBDoc,
164
+ chatId,
165
+ editResult.diffData,
166
+ beforeCommitId, // Pass the commit ID before AI changes (for VizHub integration)
167
+ );
168
+ }
169
+
170
+ // Handle credit deduction if callback is provided
171
+ if (
172
+ onCreditDeduction &&
173
+ (editResult as any).upstreamCostCents
174
+ ) {
175
+ try {
176
+ await onCreditDeduction({
177
+ upstreamCostCents: (editResult as any)
178
+ .upstreamCostCents,
179
+ provider: (editResult as any).provider,
180
+ inputTokens: (editResult as any).inputTokens,
181
+ outputTokens: (editResult as any).outputTokens,
182
+ });
183
+ } catch (creditError) {
184
+ console.error(
185
+ 'Credit deduction error:',
186
+ creditError,
187
+ );
188
+ // Don't fail the request if credit deduction fails
189
+ }
190
+ }
191
+
192
+ // Clear the AI status to indicate completion
193
+ setAIStatus(shareDBDoc, chatId, undefined);
194
+ } catch (error) {
195
+ // Set error status and add error message to chat
196
+ setAIStatus(shareDBDoc, chatId, 'error');
197
+ handleBackgroundError(shareDBDoc, chatId, error);
198
+ }
199
+ };
@@ -0,0 +1,419 @@
1
+ import {
2
+ parseMarkdownFiles,
3
+ StreamingMarkdownParser,
4
+ } from 'llm-code-format';
5
+ import OpenAI from 'openai';
6
+ import fs from 'fs';
7
+ import { generateRunId } from '@vizhub/viz-utils';
8
+ import {
9
+ updateAIStatus,
10
+ createAIMessage,
11
+ updateAIMessageContent,
12
+ finalizeAIMessage,
13
+ ensureFileExists,
14
+ clearFileContent,
15
+ appendLineToFile,
16
+ updateFiles,
17
+ updateAIScratchpad,
18
+ } from './chatOperations.js';
19
+ import { mergeFileChanges } from 'editcodewithai';
20
+ import { diff } from '../../ot.js';
21
+ import { VizChatId, VizContent } from '@vizhub/viz-types';
22
+ import { ShareDBDoc } from '../../types.js';
23
+
24
+ const DEBUG = false;
25
+
26
+ // Useful for testing/debugging the streaming behavior
27
+ const slowMode = false;
28
+
29
+ // Throttle the streaming updates, so that we don't
30
+ // overwhelm the ShareDB server with too many updates.
31
+ // It happened actually, before adding this.
32
+ // MongoDB VizHub server got in fact overloaded with
33
+ // too many updates from the AI streaming response, with
34
+ // warning: "Replication Oplog Window has gone below 1 hour"
35
+ const THROTTLE_INTERVAL_MS = 100;
36
+
37
+ // Feature flag to enable/disable streaming editing.
38
+ // * If `true`, the AI streaming response will be used to
39
+ // edit files in real-time by submitting ShareDB ops.
40
+ // * If `false`, the updates to code files will be applied
41
+ // only after the AI has finished generating the entire response.
42
+ //
43
+ // Current status: there's a tricky bug with the streaming
44
+ // where the AI edits sometimes don't apply correctly in CodeMirror.
45
+ // It seems that sometimes the op that clears the file content
46
+ // is not applied correctly in the front end, leading to
47
+ // a situation where the AI streaming edits are concatenated into the middle
48
+ // of the file instead of replacing it.
49
+ // See https://github.com/codemirror/codemirror.next/issues/1234
50
+ const enableStreamingEditing = false;
51
+
52
+ /**
53
+ * Creates and configures the LLM function for streaming with reasoning tokens
54
+ */
55
+ export const createLLMFunction = ({
56
+ shareDBDoc,
57
+ createAIEditLocalPresence,
58
+ chatId,
59
+ // Feature flag to enable/disable reasoning tokens.
60
+ // When false, reasoning tokens are not requested from the API
61
+ // and reasoning content is not processed in the streaming response.
62
+ enableReasoningTokens = false,
63
+ model,
64
+ aiRequestOptions,
65
+ }: {
66
+ shareDBDoc: ShareDBDoc<VizContent>;
67
+ createAIEditLocalPresence: () => any;
68
+ chatId: VizChatId;
69
+ enableReasoningTokens?: boolean;
70
+ model?: string;
71
+ aiRequestOptions?: any;
72
+ }) => {
73
+ return async (fullPrompt: string) => {
74
+ const localPresence = enableStreamingEditing
75
+ ? createAIEditLocalPresence()
76
+ : null;
77
+
78
+ // Create OpenRouter client for reasoning token support
79
+ const openRouterClient = new OpenAI({
80
+ apiKey: process.env.VZCODE_EDIT_WITH_AI_API_KEY,
81
+ baseURL:
82
+ process.env.VZCODE_EDIT_WITH_AI_BASE_URL ||
83
+ 'https://openrouter.ai/api/v1',
84
+ defaultHeaders: {
85
+ 'HTTP-Referer': 'https://vizhub.com',
86
+ 'X-Title': 'VizHub',
87
+ },
88
+ });
89
+
90
+ let fullContent = '';
91
+ let generationId = '';
92
+ let currentEditingFileId = null;
93
+ let currentEditingFileName = null;
94
+
95
+ // Create initial AI message for streaming
96
+ const aiMessageId = createAIMessage(shareDBDoc, chatId);
97
+
98
+ // --- throttle wrapper ---
99
+ function makeThrottledUpdater() {
100
+ let lastCall = 0;
101
+ let latestContent = '';
102
+ let timer: NodeJS.Timeout | null = null;
103
+
104
+ function invoke() {
105
+ updateAIMessageContent(
106
+ shareDBDoc,
107
+ chatId,
108
+ aiMessageId,
109
+ latestContent,
110
+ );
111
+ lastCall = Date.now();
112
+ }
113
+
114
+ const fn = (content: string) => {
115
+ latestContent = content;
116
+ const now = Date.now();
117
+
118
+ if (now - lastCall >= THROTTLE_INTERVAL_MS) {
119
+ // safe to call immediately
120
+ invoke();
121
+ } else if (!timer) {
122
+ // schedule for later
123
+ timer = setTimeout(
124
+ () => {
125
+ timer = null;
126
+ invoke();
127
+ },
128
+ THROTTLE_INTERVAL_MS - (now - lastCall),
129
+ );
130
+ }
131
+ };
132
+
133
+ // expose a flush() helper to force an immediate write
134
+ fn.flush = () => {
135
+ if (timer) {
136
+ clearTimeout(timer);
137
+ timer = null;
138
+ }
139
+ invoke();
140
+ };
141
+
142
+ return fn;
143
+ }
144
+
145
+ const throttledUpdateAIMessageContent =
146
+ makeThrottledUpdater();
147
+
148
+ // Function to report file edited
149
+ // This is called when the AI has finished editing a file
150
+ // and we want to update the message content with the file name.
151
+ const reportFileEdited = () => {
152
+ if (currentEditingFileName) {
153
+ fullContent += ` * Edited ${currentEditingFileName}\n`;
154
+ throttledUpdateAIMessageContent(fullContent);
155
+ currentEditingFileName = null;
156
+ }
157
+ };
158
+
159
+ // Define callbacks for streaming parser
160
+ const callbacks = {
161
+ onFileNameChange: async (
162
+ fileName: string,
163
+ format: string,
164
+ ) => {
165
+ DEBUG &&
166
+ console.log(
167
+ `File changed to: ${fileName} (${format})`,
168
+ );
169
+
170
+ // Find existing file or create new one
171
+ currentEditingFileId = ensureFileExists(
172
+ shareDBDoc,
173
+ fileName,
174
+ );
175
+
176
+ reportFileEdited();
177
+ currentEditingFileName = fileName;
178
+
179
+ // Clear the file content to start fresh
180
+ // (AI will regenerate the entire file content)
181
+ clearFileContent(shareDBDoc, currentEditingFileId);
182
+
183
+ // Update AI status
184
+ updateAIStatus(
185
+ shareDBDoc,
186
+ chatId,
187
+ 'Editing ' + fileName,
188
+ );
189
+ },
190
+ onCodeLine: async (line: string) => {
191
+ DEBUG && console.log(`Code line: ${line}`);
192
+
193
+ // If streaming is enabled, we apply the line immediately
194
+ if (currentEditingFileId) {
195
+ // Apply OT operation for this line immediately
196
+ appendLineToFile(
197
+ shareDBDoc,
198
+ currentEditingFileId,
199
+ line,
200
+ );
201
+
202
+ // Update AI presence to show cursor at the end of the file
203
+ const currentFile =
204
+ shareDBDoc.data.files[currentEditingFileId];
205
+ if (currentFile && currentFile.text) {
206
+ const textLength = currentFile.text.length;
207
+ const filePresence = {
208
+ username: 'AI Editor',
209
+ start: [
210
+ 'files',
211
+ currentEditingFileId,
212
+ 'text',
213
+ textLength,
214
+ ],
215
+ end: [
216
+ 'files',
217
+ currentEditingFileId,
218
+ 'text',
219
+ textLength,
220
+ ],
221
+ };
222
+
223
+ if (localPresence) {
224
+ localPresence.submit(
225
+ filePresence,
226
+ (error) => {
227
+ if (error) {
228
+ console.warn(
229
+ 'AI Editor line presence submission error:',
230
+ error,
231
+ );
232
+ }
233
+ },
234
+ );
235
+ }
236
+ }
237
+ }
238
+ },
239
+ onNonCodeLine: async (line: string) => {
240
+ // We want to report a file edited only if the line is not empty,
241
+ // because sometimes the LLMs leave a newline between the file name
242
+ // declaration and th
243
+ if (line.trim() !== '') {
244
+ reportFileEdited();
245
+ }
246
+ fullContent += line + '\n';
247
+ throttledUpdateAIMessageContent(fullContent);
248
+ },
249
+ };
250
+
251
+ const parser = new StreamingMarkdownParser(callbacks);
252
+
253
+ const chunks = [];
254
+ let reasoningContent = '';
255
+
256
+ // Stream the response with reasoning tokens
257
+ const modelName =
258
+ model ||
259
+ process.env.VZCODE_EDIT_WITH_AI_MODEL_NAME ||
260
+ 'anthropic/claude-3.5-sonnet';
261
+
262
+ // Configure reasoning tokens based on enableReasoningTokens flag
263
+ const requestConfig: any = {
264
+ model: modelName,
265
+ messages: [{ role: 'user', content: fullPrompt }],
266
+ usage: { include: true },
267
+ stream: true,
268
+ ...aiRequestOptions,
269
+ };
270
+
271
+ // Only include reasoning configuration if reasoning tokens are enabled
272
+ if (enableReasoningTokens) {
273
+ requestConfig.reasoning = {
274
+ effort: 'low',
275
+ exclude: false,
276
+ };
277
+ }
278
+
279
+ const stream = await (
280
+ openRouterClient.chat.completions.create as any
281
+ )(requestConfig);
282
+
283
+ let reasoningStarted = false;
284
+ let contentStarted = false;
285
+
286
+ for await (const chunk of stream) {
287
+ if (slowMode) {
288
+ await new Promise((resolve) =>
289
+ setTimeout(resolve, 500),
290
+ );
291
+ }
292
+ const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
293
+
294
+ if (delta?.reasoning && enableReasoningTokens) {
295
+ // Handle reasoning tokens (thinking) - only if enabled
296
+ if (!reasoningStarted) {
297
+ reasoningStarted = true;
298
+ updateAIStatus(shareDBDoc, chatId, 'Thinking...');
299
+ }
300
+ reasoningContent += delta.reasoning;
301
+ updateAIScratchpad(
302
+ shareDBDoc,
303
+ chatId,
304
+ reasoningContent,
305
+ );
306
+ } else if (delta?.content) {
307
+ // Handle regular content tokens
308
+ if (reasoningStarted && !contentStarted) {
309
+ // Clear reasoning when content starts
310
+ contentStarted = true;
311
+ updateAIScratchpad(shareDBDoc, chatId, '');
312
+ updateAIStatus(
313
+ shareDBDoc,
314
+ chatId,
315
+ 'Generating response...',
316
+ );
317
+ }
318
+
319
+ const chunkContent = delta.content;
320
+ chunks.push(chunkContent);
321
+
322
+ if (enableStreamingEditing) {
323
+ await parser.processChunk(chunkContent);
324
+ } else {
325
+ fullContent += chunkContent;
326
+ throttledUpdateAIMessageContent(fullContent);
327
+ }
328
+ } else if (chunk.usage) {
329
+ // Handle usage information
330
+ DEBUG && console.log('Usage:', chunk.usage);
331
+ }
332
+
333
+ if (!generationId && chunk.id) {
334
+ generationId = chunk.id;
335
+ }
336
+ }
337
+ await parser.flushRemaining();
338
+ reportFileEdited();
339
+
340
+ // Final cleanup - clear scratchpad and set final status
341
+ updateAIScratchpad(shareDBDoc, chatId, '');
342
+ updateAIStatus(shareDBDoc, chatId, 'Done editing.');
343
+ throttledUpdateAIMessageContent(fullContent);
344
+
345
+ // Flush to ensure the final content is written immediately
346
+ throttledUpdateAIMessageContent.flush();
347
+
348
+ // Finalize the AI message by clearing temporary fields
349
+ finalizeAIMessage(shareDBDoc, chatId);
350
+
351
+ // Clear AI Editor presence when done
352
+ DEBUG &&
353
+ console.log(
354
+ 'AI editing done, clearing AI Editor presence',
355
+ );
356
+ if (localPresence) {
357
+ localPresence.submit(null, (error) => {
358
+ DEBUG && console.log('AI Editor presence cleared');
359
+ if (error) {
360
+ console.warn(
361
+ 'AI Editor presence cleanup error:',
362
+ error,
363
+ );
364
+ }
365
+ });
366
+ }
367
+
368
+ // If streaming editing is not enabled, we need to
369
+ // apply all the edits at once
370
+ if (!enableStreamingEditing) {
371
+ updateFiles(
372
+ shareDBDoc,
373
+ mergeFileChanges(
374
+ shareDBDoc.data.files,
375
+ parseMarkdownFiles(fullContent, 'bold').files,
376
+ ),
377
+ );
378
+ }
379
+
380
+ // Generate a new runId to trigger a run when AI finishes editing
381
+ // This will trigger a re-run without hot reloading
382
+ const newRunId = generateRunId();
383
+ const runIdOp = diff(shareDBDoc.data, {
384
+ ...shareDBDoc.data,
385
+ runId: newRunId,
386
+ });
387
+ shareDBDoc.submitOp(runIdOp, (error) => {
388
+ if (error) {
389
+ console.warn(
390
+ 'Error setting runId after AI editing:',
391
+ error,
392
+ );
393
+ } else {
394
+ DEBUG &&
395
+ console.log(
396
+ 'Set new runId after AI editing:',
397
+ newRunId,
398
+ );
399
+ }
400
+ });
401
+
402
+ // Write chunks file for debugging
403
+ if (DEBUG) {
404
+ const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
405
+ fs.writeFileSync(
406
+ chunksFileJSONpath,
407
+ JSON.stringify(chunks, null, 2),
408
+ );
409
+ console.log(
410
+ `AI chunks written to ${chunksFileJSONpath}`,
411
+ );
412
+ }
413
+
414
+ return {
415
+ content: fullContent,
416
+ generationId: generationId,
417
+ };
418
+ };
419
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Validates incoming request data for AI chat messages
3
+ */
4
+ export const validateRequest = (req, res) => {
5
+ const { content, chatId } = req.body;
6
+
7
+ if (!content || typeof content !== 'string') {
8
+ res.status(400).json({
9
+ error:
10
+ 'Invalid request: content is required and must be a string',
11
+ });
12
+ return false;
13
+ }
14
+
15
+ if (!chatId || typeof chatId !== 'string') {
16
+ res.status(400).json({
17
+ error:
18
+ 'Invalid request: chatId is required and must be a string',
19
+ });
20
+ return false;
21
+ }
22
+
23
+ return true;
24
+ };