vzcode 2.2.0 → 2.3.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 (220) hide show
  1. package/dist/assets/{buildWorker-Dsx6rOdK.js → buildWorker-wLkQ0yz1.js} +64 -64
  2. package/dist/assets/{index-BM8tDh6M.css → index-BoFeK4GL.css} +1 -1
  3. package/dist/assets/{index-CpxqGhXj.js → index-DuUjtrt6.js} +150 -150
  4. package/dist/assets/{worker-BeBZGG1n.js → worker-DHxSPM7N.js} +78 -78
  5. package/dist/cli.js +113 -0
  6. package/dist/index.html +2 -2
  7. package/{src/ot.ts → dist/ot.js} +2 -14
  8. package/{src/randomId.ts → dist/randomId.js} +5 -6
  9. package/dist/runCode.js +20 -0
  10. package/dist/server/aiChatHandler/aiEditing.js +89 -0
  11. package/dist/server/aiChatHandler/chatOperations.js +360 -0
  12. package/dist/server/aiChatHandler/errorHandling.js +49 -0
  13. package/dist/server/aiChatHandler/index.js +112 -0
  14. package/dist/server/aiChatHandler/llmStreaming.js +284 -0
  15. package/dist/server/aiChatHandler/validation.js +19 -0
  16. package/dist/server/computeInitialDocument.js +146 -0
  17. package/{src/server/config.ts → dist/server/config.js} +4 -7
  18. package/{src/server/featureFlags.ts → dist/server/featureFlags.js} +0 -3
  19. package/dist/server/generateAIResponse.js +129 -0
  20. package/dist/server/handleAIAssist.js +39 -0
  21. package/dist/server/handleAICopilot.js +81 -0
  22. package/dist/server/index.js +297 -0
  23. package/dist/server/livekit.js +17 -0
  24. package/dist/server/prettier.js +90 -0
  25. package/{src/server/setupEnv.ts → dist/server/setupEnv.js} +1 -2
  26. package/dist/submitOperation.js +14 -0
  27. package/dist/types.js +1 -0
  28. package/dist/utils/fileDiff.js +66 -0
  29. package/package.json +22 -22
  30. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +0 -42
  31. package/src/client/AIAssist/AIAssistWidget/index.tsx +0 -135
  32. package/src/client/AIAssist/AIAssistWidget/style.scss +0 -47
  33. package/src/client/AIAssist/Spinner.jsx +0 -76
  34. package/src/client/AIAssist/formatFile.ts +0 -27
  35. package/src/client/AIAssist/generateFilesContext.ts +0 -25
  36. package/src/client/AIAssist/index.ts +0 -2
  37. package/src/client/AIAssist/startAIAssist.ts +0 -95
  38. package/src/client/App/index.tsx +0 -153
  39. package/src/client/App/style.scss +0 -82
  40. package/src/client/App/usePending.ts +0 -51
  41. package/src/client/App/useShareDB.ts +0 -161
  42. package/src/client/CodeEditor/Copilot/index.ts +0 -74
  43. package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +0 -12
  44. package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +0 -68
  45. package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +0 -174
  46. package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +0 -79
  47. package/src/client/CodeEditor/InteractiveWidgets/index.ts +0 -44
  48. package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +0 -12
  49. package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +0 -20
  50. package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +0 -143
  51. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +0 -48
  52. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +0 -12
  53. package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +0 -25
  54. package/src/client/CodeEditor/basicSetup.ts +0 -73
  55. package/src/client/CodeEditor/getOrCreateEditor.tsx +0 -615
  56. package/src/client/CodeEditor/index.tsx +0 -202
  57. package/src/client/CodeEditor/json1PresenceBroadcast.ts +0 -41
  58. package/src/client/CodeEditor/json1PresenceDisplay.ts +0 -350
  59. package/src/client/CodeEditor/rainbowBrackets.ts +0 -122
  60. package/src/client/CodeEditor/style.scss +0 -156
  61. package/src/client/CodeEditor/typescriptExtension/worker.ts +0 -32
  62. package/src/client/CodeErrorOverlay/index.tsx +0 -77
  63. package/src/client/CodeErrorOverlay/style.scss +0 -33
  64. package/src/client/Fonts/fonts.ts +0 -8
  65. package/src/client/Icons/AdjustmentSVG.tsx +0 -19
  66. package/src/client/Icons/BugSVG.tsx +0 -20
  67. package/src/client/Icons/CloseSVG.tsx +0 -19
  68. package/src/client/Icons/CssSVG.tsx +0 -16
  69. package/src/client/Icons/DirectoryArrowSVG.tsx +0 -19
  70. package/src/client/Icons/EditSVG.tsx +0 -19
  71. package/src/client/Icons/FileSVG.tsx +0 -19
  72. package/src/client/Icons/FolderSVG.tsx +0 -20
  73. package/src/client/Icons/GearSVG.tsx +0 -16
  74. package/src/client/Icons/HtmlSVG.tsx +0 -16
  75. package/src/client/Icons/JavaScriptSVG.tsx +0 -16
  76. package/src/client/Icons/JsonSVG.tsx +0 -16
  77. package/src/client/Icons/MarkdownSVG.tsx +0 -16
  78. package/src/client/Icons/MicOffSVG.tsx +0 -35
  79. package/src/client/Icons/MicSVG.tsx +0 -25
  80. package/src/client/Icons/NewSVG.tsx +0 -19
  81. package/src/client/Icons/PinSVG.tsx +0 -14
  82. package/src/client/Icons/PlaySVG.tsx +0 -16
  83. package/src/client/Icons/QuestionMarkSVG.tsx +0 -18
  84. package/src/client/Icons/ReactSVG.tsx +0 -16
  85. package/src/client/Icons/SearchSVG.tsx +0 -20
  86. package/src/client/Icons/SparklesSVG.tsx +0 -23
  87. package/src/client/Icons/SplitEditorSVG.tsx +0 -34
  88. package/src/client/Icons/StopSVG.tsx +0 -15
  89. package/src/client/Icons/SvelteSVG.tsx +0 -16
  90. package/src/client/Icons/TrashSVG.tsx +0 -19
  91. package/src/client/Icons/TypeScriptSVG.tsx +0 -16
  92. package/src/client/Icons/index.tsx +0 -26
  93. package/src/client/ImageViewer/index.tsx +0 -91
  94. package/src/client/ImageViewer/style.scss +0 -41
  95. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +0 -134
  96. package/src/client/PresenceNotifications/index.tsx +0 -158
  97. package/src/client/PresenceNotifications/style.scss +0 -17
  98. package/src/client/RunCodeWidget/index.tsx +0 -117
  99. package/src/client/RunCodeWidget/style.scss +0 -24
  100. package/src/client/SplitPaneResizeContext.tsx +0 -159
  101. package/src/client/TabList/Tab.tsx +0 -73
  102. package/src/client/TabList/index.tsx +0 -35
  103. package/src/client/TabList/style.scss +0 -51
  104. package/src/client/VZCodeContext/index.tsx +0 -29
  105. package/src/client/VZCodeContext/types.ts +0 -249
  106. package/src/client/VZCodeContext/useVZCodeState.ts +0 -584
  107. package/src/client/VZKeyboardShortcutsDoc.tsx +0 -148
  108. package/src/client/VZLeft.tsx +0 -28
  109. package/src/client/VZMiddle.tsx +0 -172
  110. package/src/client/VZResizer/index.tsx +0 -128
  111. package/src/client/VZResizer/styles.scss +0 -33
  112. package/src/client/VZRight.tsx +0 -113
  113. package/src/client/VZSettings.tsx +0 -275
  114. package/src/client/VZSidebar/AIChat/ChatInput.tsx +0 -239
  115. package/src/client/VZSidebar/AIChat/DiffView.scss +0 -136
  116. package/src/client/VZSidebar/AIChat/DiffView.tsx +0 -176
  117. package/src/client/VZSidebar/AIChat/Message.tsx +0 -143
  118. package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -205
  119. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +0 -35
  120. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +0 -156
  121. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +0 -19
  122. package/src/client/VZSidebar/AIChat/index.tsx +0 -273
  123. package/src/client/VZSidebar/AIChat/styles.scss +0 -627
  124. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +0 -117
  125. package/src/client/VZSidebar/CreateDirModal.tsx +0 -111
  126. package/src/client/VZSidebar/CreateFileModal.tsx +0 -111
  127. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +0 -76
  128. package/src/client/VZSidebar/DirectoryListing.tsx +0 -89
  129. package/src/client/VZSidebar/EmptyState.tsx +0 -11
  130. package/src/client/VZSidebar/FileListing.tsx +0 -91
  131. package/src/client/VZSidebar/FileTypeIcon.tsx +0 -49
  132. package/src/client/VZSidebar/Item.tsx +0 -237
  133. package/src/client/VZSidebar/Listing.tsx +0 -47
  134. package/src/client/VZSidebar/Search.tsx +0 -527
  135. package/src/client/VZSidebar/VisualEditor.tsx +0 -282
  136. package/src/client/VZSidebar/VoiceChatModal.tsx +0 -121
  137. package/src/client/VZSidebar/aiCopyPaste.ts +0 -199
  138. package/src/client/VZSidebar/index.tsx +0 -728
  139. package/src/client/VZSidebar/styles.scss +0 -612
  140. package/src/client/VZSidebar/useDragAndDrop.tsx +0 -408
  141. package/src/client/bootstrap.ts +0 -26
  142. package/src/client/buildWorker.ts +0 -3
  143. package/src/client/featureFlags.ts +0 -8
  144. package/src/client/generateRequestId.ts +0 -6
  145. package/src/client/getFileTree.ts +0 -53
  146. package/src/client/index.ts +0 -19
  147. package/src/client/main.jsx +0 -10
  148. package/src/client/presenceColor.ts +0 -8
  149. package/src/client/shouldTriggerRun.ts +0 -31
  150. package/src/client/sortFileTree.ts +0 -21
  151. package/src/client/tabsSearchParameters.test.ts +0 -79
  152. package/src/client/tabsSearchParameters.ts +0 -137
  153. package/src/client/themes/index.ts +0 -7
  154. package/src/client/themes/themes.ts +0 -77
  155. package/src/client/themes/uiwImports.ts +0 -32
  156. package/src/client/themes/useDynamicTheme.ts +0 -29
  157. package/src/client/themes/vizhubTheme/colors.ts +0 -80
  158. package/src/client/themes/vizhubTheme/index.ts +0 -193
  159. package/src/client/useActions.ts +0 -288
  160. package/src/client/useESLint/index.ts +0 -72
  161. package/src/client/useESLint/worker.ts +0 -113
  162. package/src/client/useEditorCache.ts +0 -35
  163. package/src/client/useFileCRUD.ts +0 -228
  164. package/src/client/useKeyboardShortcuts.ts +0 -451
  165. package/src/client/useOpenDirectories.ts +0 -99
  166. package/src/client/usePresenceAutoFollow.ts +0 -85
  167. package/src/client/usePrettier/index.ts +0 -178
  168. package/src/client/usePrettier/worker.ts +0 -109
  169. package/src/client/useRunCode.tsx +0 -15
  170. package/src/client/useRuntimeError.ts +0 -32
  171. package/src/client/useSubmitOperation.ts +0 -15
  172. package/src/client/useURLSync.ts +0 -137
  173. package/src/client/usernameLocalStorage.ts +0 -43
  174. package/src/client/utils/fileExtension.ts +0 -10
  175. package/src/client/utils/isImageFile.ts +0 -12
  176. package/src/client/vzReducer/aiChatReducer.ts +0 -44
  177. package/src/client/vzReducer/closeTabReducer.test.ts +0 -205
  178. package/src/client/vzReducer/closeTabsReducer.ts +0 -96
  179. package/src/client/vzReducer/createInitialState.ts +0 -38
  180. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +0 -9
  181. package/src/client/vzReducer/findPane.ts +0 -19
  182. package/src/client/vzReducer/index.ts +0 -250
  183. package/src/client/vzReducer/openTabReducer.test.ts +0 -127
  184. package/src/client/vzReducer/openTabReducer.ts +0 -74
  185. package/src/client/vzReducer/searchReducer.test.ts +0 -292
  186. package/src/client/vzReducer/searchReducer.ts +0 -181
  187. package/src/client/vzReducer/setActiveFileIdReducer.ts +0 -28
  188. package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +0 -76
  189. package/src/client/vzReducer/setIsDocOpenReducer.ts +0 -9
  190. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +0 -9
  191. package/src/client/vzReducer/setThemeReducer.ts +0 -9
  192. package/src/client/vzReducer/setUsernameReducer.ts +0 -9
  193. package/src/client/vzReducer/splitCurrentPaneReducer.ts +0 -66
  194. package/src/client/vzReducer/toggleAutoFollowReducer.ts +0 -12
  195. package/src/client/vzReducer/updatePane.ts +0 -40
  196. package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +0 -30
  197. package/src/client/vzReducer/visualEditorReducer.ts +0 -9
  198. package/src/reactRouterExports.ts +0 -15
  199. package/src/runCode.ts +0 -27
  200. package/src/server/aiChatHandler/aiEditing.ts +0 -152
  201. package/src/server/aiChatHandler/chatOperations.ts +0 -510
  202. package/src/server/aiChatHandler/errorHandling.ts +0 -66
  203. package/src/server/aiChatHandler/index.ts +0 -200
  204. package/src/server/aiChatHandler/llmStreaming.ts +0 -419
  205. package/src/server/aiChatHandler/undoHandler.ts +0 -97
  206. package/src/server/aiChatHandler/validation.ts +0 -24
  207. package/src/server/computeInitialDocument.ts +0 -220
  208. package/src/server/generateAIResponse.ts +0 -188
  209. package/src/server/handleAIAssist.ts +0 -53
  210. package/src/server/handleAIChatUndo.ts +0 -2
  211. package/src/server/handleAICopilot.ts +0 -97
  212. package/src/server/index.ts +0 -388
  213. package/src/server/livekit.ts +0 -24
  214. package/src/server/prettier.ts +0 -115
  215. package/src/submitOperation.ts +0 -25
  216. package/src/types.ts +0 -205
  217. package/src/utils/fileDiff.ts +0 -117
  218. package/src/vite-env.d.ts +0 -10
  219. /package/{src/server/handleAIChatMessage.ts → dist/server/handleAIChatMessage.js} +0 -0
  220. /package/{src/server/isDirectory.ts → dist/server/isDirectory.js} +0 -0
@@ -0,0 +1,129 @@
1
+ import OpenAI from 'openai';
2
+ import { json1Presence } from '../ot.js';
3
+ // This module implements the generation of the AI response,
4
+ // using the OpenAI client.
5
+ // These environment variables are used to configure the OpenAI client.
6
+ // See `.env.sample` for the expected values.
7
+ const { VZCODE_AI_API_KEY, VZCODE_AI_BASE_URL } = process.env;
8
+ // If neither of these are not set, the OpenAI client will not be initialized,
9
+ // e.g. for local development where no AI is needed.
10
+ const isAIEnabled = VZCODE_AI_API_KEY !== undefined &&
11
+ VZCODE_AI_BASE_URL !== undefined;
12
+ const { editOp, type } = json1Presence;
13
+ // Debug flag to log more information during development.
14
+ const debug = false;
15
+ // Feature flag to slow down AI for development/testing.
16
+ // Particularly relevant for debugging ShareDB and CodeMirror sync issues.
17
+ // This allows you to test the case of editing the document at the
18
+ // same time as the AI generation is happening.
19
+ const slowdown = false;
20
+ // The options passed into the OpenAI client
21
+ // new OpenAI(openAIOptions)
22
+ const openAIOptions = {};
23
+ // Support specifying the API key via an environment variable
24
+ // If VZCODE_AI_API_KEY is not set, note that the OpenAI client
25
+ // will look for the OPENAI_API_KEY environment variable instead.
26
+ if (process.env.VZCODE_AI_API_KEY !== undefined) {
27
+ openAIOptions.apiKey = process.env.VZCODE_AI_API_KEY;
28
+ }
29
+ // Support for local AI server
30
+ if (process.env.VZCODE_AI_BASE_URL !== undefined) {
31
+ // The OpenAI client errors if the API key is not set,
32
+ // so in the case where we don't need an API key,
33
+ // e.g. for testing with a local AI server like LM Studio or LocalAI,
34
+ // we populate the option with a fake API key, so it doesn't error.
35
+ if (!openAIOptions.apiKey) {
36
+ openAIOptions.apiKey = 'Fake API Key';
37
+ }
38
+ openAIOptions.baseURL = process.env.VZCODE_AI_BASE_URL;
39
+ }
40
+ debug &&
41
+ console.log('openAIOptions: ' +
42
+ JSON.stringify(openAIOptions, null, 2));
43
+ let openai;
44
+ if (isAIEnabled) {
45
+ openai = new OpenAI(openAIOptions);
46
+ }
47
+ // The name of the source that the AI responses
48
+ // will be attributed to in ShareDB operations.
49
+ const AIShareDBSourceName = 'AIAssist';
50
+ // Returns trie if the operation comes from AI.
51
+ const opComesFromAIAssist = (ops, source) => source === AIShareDBSourceName;
52
+ // Keeps track of the currently ongoing AI streams.
53
+ // There could be many streams at the same time.
54
+ export const streams = {};
55
+ export const generateAIResponse = async ({ inputText, insertionCursor, fileId, streamId, shareDBDoc, }) => {
56
+ if (!isAIEnabled) {
57
+ console.log('[generateAIResponse] AI is not enabled. Skipping AI generation.');
58
+ console.log('[generateAIResponse] To enable AI, see .env.sample for the required environment variables.');
59
+ return;
60
+ }
61
+ if (debug) {
62
+ console.log('[generateAIResponse] inputText:', inputText);
63
+ console.log('[generateAIResponse] insertionCursor:', insertionCursor);
64
+ console.log('[generateAIResponse] fileId:', fileId);
65
+ console.log('[generateAIResponse] streamId:', streamId);
66
+ console.log('[generateAIResponse] shareDBDoc:', shareDBDoc);
67
+ }
68
+ // Handle the case that a user edits the text in the document
69
+ // that comes becofore the insertion cursor.
70
+ const accomodateDocChanges = (op, source) => {
71
+ if (!opComesFromAIAssist(op, source)) {
72
+ if (op !== null) {
73
+ insertionCursor = type
74
+ .transformPosition(['files', fileId, 'text', insertionCursor], op)
75
+ .slice(-1)[0];
76
+ }
77
+ }
78
+ };
79
+ shareDBDoc.on('op', accomodateDocChanges);
80
+ // The prompt!
81
+ const messages = [
82
+ {
83
+ role: 'system',
84
+ content: [
85
+ 'You are an expert programmer.',
86
+ 'Your task is to output ONLY the code that replaces <FILL_ME> correctly.',
87
+ 'Do not add any markdown around.',
88
+ 'Do not duplicate the code before or after <FILL_ME>.',
89
+ 'Do not make any changes outside of <FILL_ME>.',
90
+ 'Do not enclose the output with backticks.',
91
+ 'If any additional instructions are required, they will be provided in comments.',
92
+ ].join(' '),
93
+ },
94
+ { role: 'user', content: inputText },
95
+ ];
96
+ if (debug) {
97
+ console.log('[generateAIResponse] messages:');
98
+ console.log(JSON.stringify(messages, null, 2));
99
+ }
100
+ streams[streamId] = await openai.chat.completions.create({
101
+ // model: 'gpt-3.5-turbo',
102
+ model: 'gpt-4o',
103
+ messages,
104
+ reasoning: {
105
+ effort: 'medium',
106
+ exclude: false,
107
+ }, // Type assertion for OpenRouter-specific reasoning parameter
108
+ provider: {
109
+ sort: 'throughput',
110
+ }, // Type assertion for OpenRouter-specific provider parameter
111
+ stream: true,
112
+ });
113
+ for await (const part of streams[streamId]) {
114
+ const op = editOp(['files', fileId, 'text'], 'text-unicode', [
115
+ insertionCursor,
116
+ part.choices[0]?.delta?.content || '',
117
+ ]);
118
+ shareDBDoc.submitOp(op, {
119
+ source: AIShareDBSourceName,
120
+ });
121
+ if (slowdown) {
122
+ await new Promise((resolve) => {
123
+ setTimeout(resolve, 2000);
124
+ });
125
+ }
126
+ insertionCursor += (part.choices[0]?.delta?.content || '').length;
127
+ }
128
+ shareDBDoc.off('op', accomodateDocChanges);
129
+ };
@@ -0,0 +1,39 @@
1
+ import { generateAIResponse, streams, } from './generateAIResponse.js';
2
+ const debug = false;
3
+ export const handleAIAssist = (shareDBDoc) => async (req, res) => {
4
+ const { inputText, insertionCursor, fileId } = req.body;
5
+ if (debug) {
6
+ console.log('[handleAIAssist] inputText:', inputText);
7
+ console.log('[handleAIAssist] insertionCursor:', insertionCursor);
8
+ console.log('[handleAIAssist] fileId:', fileId);
9
+ }
10
+ try {
11
+ // Generate a unique streamId for this request
12
+ const streamId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
13
+ await generateAIResponse({
14
+ inputText,
15
+ insertionCursor,
16
+ fileId,
17
+ streamId,
18
+ shareDBDoc,
19
+ });
20
+ res
21
+ .status(200)
22
+ .send({ message: 'Operation successful!' });
23
+ }
24
+ catch (error) {
25
+ console.error('handleAIAssist error:', error);
26
+ res.status(500).send({
27
+ message: 'Internal Server Error',
28
+ error: error.message,
29
+ });
30
+ }
31
+ };
32
+ export function haltGeneration(streamId) {
33
+ const stream = streams[streamId];
34
+ // Stream can be undefined here if the user
35
+ // clicks start and stop very quickly.
36
+ if (stream) {
37
+ stream.controller.abort();
38
+ }
39
+ }
@@ -0,0 +1,81 @@
1
+ import { ChatOpenAI } from '@langchain/openai';
2
+ import { StringOutputParser } from '@langchain/core/output_parsers';
3
+ // This module implements the generation of the copilot response.
4
+ // Debug flag to log more information during development.
5
+ const debug = false;
6
+ export const handleAICopilot = () => {
7
+ // These environment variables are used to configure the OpenAI client.
8
+ // See `.env.sample` for the expected values.
9
+ const { VZCODE_AI_COPILOT_API_KEY, VZCODE_AI_COPILOT_BASE_URL, VZCODE_AI_COPILOT_MODEL, } = process.env;
10
+ // If neither of these are not set, the OpenAI client will not be initialized,
11
+ // e.g. for local development where no AI is needed.
12
+ const isCopilotEnabled = VZCODE_AI_COPILOT_API_KEY !== undefined &&
13
+ VZCODE_AI_COPILOT_BASE_URL !== undefined &&
14
+ VZCODE_AI_COPILOT_MODEL !== undefined;
15
+ debug &&
16
+ console.log('isCopilotEnabled: ' + isCopilotEnabled);
17
+ debug &&
18
+ console.log({
19
+ VZCODE_AI_COPILOT_API_KEY,
20
+ VZCODE_AI_COPILOT_BASE_URL,
21
+ VZCODE_AI_COPILOT_MODEL,
22
+ });
23
+ let chatModel;
24
+ if (isCopilotEnabled) {
25
+ const options = {
26
+ modelName: VZCODE_AI_COPILOT_MODEL,
27
+ configuration: {
28
+ apiKey: VZCODE_AI_COPILOT_API_KEY,
29
+ baseURL: VZCODE_AI_COPILOT_BASE_URL,
30
+ },
31
+ additionalParameters: {
32
+ provider: { sort: 'throughput' },
33
+ },
34
+ streaming: false,
35
+ };
36
+ debug && console.log('chatModel options:', options);
37
+ chatModel = new ChatOpenAI(options);
38
+ }
39
+ return async (req, res) => {
40
+ // Don't break if the AI is not enabled.
41
+ // This is useful for local development.
42
+ if (!isCopilotEnabled) {
43
+ res.status(200).send({ text: '' });
44
+ return;
45
+ }
46
+ const { prefix, suffix } = req.body;
47
+ const messages = [
48
+ {
49
+ role: 'user',
50
+ content: [
51
+ 'Complete the missing part of the text below:\n\n',
52
+ '\`\`\`\n',
53
+ prefix,
54
+ '<MISSING_PART>',
55
+ suffix,
56
+ '\`\`\`\n\n',
57
+ 'ONLY output the text that replaces <MISSING_PART>.\n\n',
58
+ 'NEVER include \`\`\`javascript or similar code blocks.\n\n',
59
+ ].join(''),
60
+ },
61
+ ];
62
+ if (debug) {
63
+ console.log('[generateAIResponse] prompt:\n```\n' +
64
+ messages[0].content +
65
+ '\n```\n');
66
+ }
67
+ try {
68
+ const result = await chatModel.invoke(messages);
69
+ const parser = new StringOutputParser();
70
+ const text = await parser.invoke(result);
71
+ res.status(200).send({ text });
72
+ }
73
+ catch (error) {
74
+ console.error('handleAICopilot error:', error);
75
+ res.status(500).send({
76
+ message: 'Internal Server Error',
77
+ error: error.message,
78
+ });
79
+ }
80
+ };
81
+ };
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ import WebSocketJSONStream from '@teamwork/websocket-json-stream';
3
+ import bodyParser from 'body-parser';
4
+ import express from 'express';
5
+ import fs from 'fs';
6
+ import http from 'http';
7
+ import open from 'open';
8
+ import path from 'path';
9
+ import ShareDB from 'sharedb';
10
+ import { fileURLToPath } from 'url';
11
+ import { WebSocketServer } from 'ws';
12
+ import { json1Presence } from '../ot.js';
13
+ import { computeInitialDocument } from './computeInitialDocument.js';
14
+ import { handleAIAssist } from './handleAIAssist.js';
15
+ import { handleAICopilot } from './handleAICopilot.js';
16
+ import { handleAIChatMessage } from './handleAIChatMessage.js';
17
+ import { isDirectory } from './isDirectory.js';
18
+ import { createToken } from './livekit.js';
19
+ import './setupEnv.js';
20
+ // Import the image file utility
21
+ const isImageFile = (fileName) => {
22
+ return (fileName.match(/\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i) !== null);
23
+ };
24
+ // The time in milliseconds by which auto-saving is debounced.
25
+ const autoSaveDebounceTimeMS = 800;
26
+ // The time in milliseconds by which auto-saving is throttled
27
+ // when the user is interacting with the widgets in the editor.
28
+ const throttleTimeMS = 100;
29
+ // Helper function to get the port from command line arguments or use default.
30
+ function getPortFromArgs(defaultPort = 3030) {
31
+ const portArg = process.argv.find((arg) => arg.startsWith('--port='));
32
+ if (portArg) {
33
+ return parseInt(portArg.split('=')[1], 10);
34
+ }
35
+ return defaultPort;
36
+ }
37
+ // Server port.
38
+ const port = getPortFromArgs();
39
+ const initialDocument = computeInitialDocument({
40
+ // Use the current working directory to look for files.
41
+ fullPath: process.cwd(),
42
+ });
43
+ // Register our custom OT type,
44
+ // because it does not ship with ShareDB.
45
+ ShareDB.types.register(json1Presence.type);
46
+ const app = express();
47
+ // Use ShareDB over WebSocket
48
+ const shareDBBackend = new ShareDB({
49
+ // Enable presence
50
+ // See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/server.js#L9
51
+ presence: true,
52
+ doNotForwardSendPresenceErrorsToClient: true,
53
+ });
54
+ const server = http.createServer(app);
55
+ const wss = new WebSocketServer({ server });
56
+ wss.on('connection', (ws) => {
57
+ const clientStream = new WebSocketJSONStream(ws);
58
+ shareDBBackend.listen(clientStream);
59
+ // Prevent server crashes on errors.
60
+ clientStream.on('error', (error) => {
61
+ console.log('clientStream error: ' + error.message);
62
+ });
63
+ // Handle errors
64
+ ws.on('error', (error) => {
65
+ console.log('ws error: ' + error.message);
66
+ });
67
+ // Handle disconnections
68
+ ws.on('close', () => {
69
+ clientStream.end();
70
+ });
71
+ });
72
+ // Serve static files
73
+ const filename = fileURLToPath(import.meta.url);
74
+ const dirname = path.dirname(filename);
75
+ const dir = path.join(dirname, '..', '..', 'dist');
76
+ app.use(express.static(dir));
77
+ // Create the initial "document",
78
+ // which is a representation of files on disk.
79
+ const shareDBConnection = shareDBBackend.connect();
80
+ const shareDBDoc = shareDBConnection.get('documents', '1');
81
+ // Set up presence for AI editing following the same pattern as useShareDB.ts
82
+ const docPresence = shareDBConnection.getDocPresence('documents', '1');
83
+ // Create local presence for AI editing with a unique ID
84
+ const generateAIEditId = () => {
85
+ const timestamp = Date.now().toString(36);
86
+ return `ai-edit-${timestamp}`;
87
+ };
88
+ const createAIEditLocalPresence = () => docPresence.create(generateAIEditId());
89
+ shareDBDoc.create(initialDocument, json1Presence.type.uri);
90
+ // Handle AI Assist requests.
91
+ app.post('/ai-assist', bodyParser.json(), handleAIAssist(shareDBDoc));
92
+ // Handle AI Copilot requests.
93
+ app.post('/ai-copilot', bodyParser.json(), handleAICopilot());
94
+ // Handle AI Chat Message requests.
95
+ app.post('/ai-chat-message', bodyParser.json(), handleAIChatMessage({
96
+ shareDBDoc,
97
+ createAIEditLocalPresence,
98
+ onCreditDeduction: undefined,
99
+ }));
100
+ // Livekit Token Generator
101
+ app.get('/livekit-token', async (req, res) => {
102
+ const { room, username } = req.query;
103
+ res.send(await createToken(room, username));
104
+ });
105
+ // The state of the document when files were last auto-saved.
106
+ let previousDocument = initialDocument;
107
+ // Saves the files that changed.
108
+ const save = () => {
109
+ const currentDocument = shareDBDoc.data;
110
+ // Take a look at each file (key) previously and currently.
111
+ const allKeys = Object.keys({
112
+ ...currentDocument.files,
113
+ ...previousDocument.files,
114
+ });
115
+ const directoriesToDelete = [];
116
+ for (const key of allKeys) {
117
+ const previous = previousDocument.files[key];
118
+ const current = currentDocument.files[key];
119
+ // If this file was neither created nor deleted...
120
+ if (previous && current) {
121
+ // Handle changing of text content.
122
+ if (previous.text !== current.text) {
123
+ if (isImageFile(current.name)) {
124
+ // Write image files as binary from base64
125
+ const buffer = Buffer.from(current.text, 'base64');
126
+ fs.writeFileSync(current.name, buffer);
127
+ }
128
+ else {
129
+ // Write non-image files as text
130
+ fs.writeFileSync(current.name, current.text);
131
+ }
132
+ }
133
+ // // Handle renaming files.
134
+ // if (previous.name !== current.name) {
135
+ // const previousPath = previous.name.split('/');
136
+ // const currentPath = current.name.split('/');
137
+ // // - If the path is length 1 for previous and current,
138
+ // // then this should be true.
139
+ // // - If the path length is the sane for previous and current,
140
+ // // AND all the entries leading up to the last one are the same
141
+ // // between previous and current, AND the last entry in the arrays
142
+ // // (the file name itself) is different between previous and current
143
+ // const onlyFileNameChanged =
144
+ // (previousPath.length === 1 &&
145
+ // currentPath.length === 1) ||
146
+ // (previousPath.length === currentPath.length &&
147
+ // previousPath
148
+ // .slice(0, -1)
149
+ // .every(
150
+ // (val, index) => val === currentPath[index],
151
+ // ) &&
152
+ // previousPath.at(-1) !== currentPath.at(-1));
153
+ // // This logic works if only the file name itself changes.
154
+ // if (onlyFileNameChanged) {
155
+ // fs.renameSync(previous.name, current.name);
156
+ // }
157
+ // {
158
+ // // If we get here, then the directory may have been renamed,
159
+ // // or the file has been moved between directories.
160
+ // // If we get here, then the directory may have been renamed,
161
+ // // or the file has been moved between directories.
162
+ // const newDir = path.dirname(current.name);
163
+ // // Check if the new directory exists, if not, create it
164
+ // if (!fs.existsSync(newDir)) {
165
+ // fs.mkdirSync(newDir, { recursive: true });
166
+ // }
167
+ // // Move the file to the new directory
168
+ // fs.renameSync(previous.name, current.name);
169
+ // }
170
+ // }
171
+ // Handle renaming files.
172
+ if (previous.name !== current.name) {
173
+ if (isDirectory(previous.name)) {
174
+ // If the directory itself is being renamed,
175
+ // Phase I: Ignore it, and get the files moved
176
+ // Phase II: Keep track of these, and delete them after
177
+ // all the files moved.
178
+ // directoriesToDelete.push(previous.name)
179
+ directoriesToDelete.push(previous.name);
180
+ }
181
+ else {
182
+ const newDir = path.dirname(current.name);
183
+ // Check if the new directory exists, if not, create it
184
+ if (!fs.existsSync(newDir)) {
185
+ fs.mkdirSync(newDir, { recursive: true });
186
+ }
187
+ // Move the file to the new directory
188
+ fs.renameSync(previous.name, current.name);
189
+ }
190
+ }
191
+ }
192
+ // handle deleting files and directories.
193
+ if (previous && !current) {
194
+ let stats = fs.statSync(previous.name);
195
+ //Check if the file path we are trying to delete is a directory
196
+ if (!stats.isDirectory()) {
197
+ fs.unlinkSync(previous.name);
198
+ }
199
+ else {
200
+ //Performs directory deletion.
201
+ fs.rm(previous.name, {
202
+ recursive: true, //Makes sure that all files in directory are deleted.
203
+ }, (error) => {
204
+ if (error) {
205
+ console.log(error);
206
+ }
207
+ });
208
+ }
209
+ }
210
+ // Handle creating files and Directories.
211
+ if (!previous && current) {
212
+ //File Creation
213
+ if (!isDirectory(current.name)) {
214
+ if (isImageFile(current.name)) {
215
+ // Write image files as binary from base64
216
+ const buffer = Buffer.from(current.text, 'base64');
217
+ fs.writeFileSync(current.name, buffer);
218
+ }
219
+ else {
220
+ // Write non-image files as text
221
+ fs.writeFileSync(current.name, current.text);
222
+ }
223
+ }
224
+ else {
225
+ fs.mkdirSync(current.name, { recursive: true });
226
+ }
227
+ }
228
+ }
229
+ // TODO deleted all directories under directoriesToDelete
230
+ for (const dir of directoriesToDelete) {
231
+ //Performs directory deletion.
232
+ fs.rm(dir, {
233
+ recursive: true, //Makes sure that all files in directory are deleted.
234
+ }, (error) => {
235
+ if (error) {
236
+ console.log(error);
237
+ }
238
+ });
239
+ }
240
+ previousDocument = currentDocument;
241
+ };
242
+ // // Listen for when users modify files.
243
+ // // Files get written to disk after `autoSaveDebounceTimeMS`
244
+ // // milliseconds of inactivity.
245
+ // let timeout;
246
+ // shareDBDoc.subscribe(() => {
247
+ // shareDBDoc.on('op', () => {
248
+ // // console.log(shareDBDoc.data.isInteracting);
249
+ // clearTimeout(timeout);
250
+ // timeout = setTimeout(save, autoSaveDebounceTimeMS);
251
+ // });
252
+ // });
253
+ let lastExecutedTime = Date.now();
254
+ let lastChangeTimeout; // This timeout ensures the last change is saved
255
+ // Function to throttle the saving
256
+ function throttleSave() {
257
+ const now = Date.now();
258
+ const timeSinceLastExecution = now - lastExecutedTime;
259
+ if (timeSinceLastExecution > throttleTimeMS) {
260
+ save();
261
+ lastExecutedTime = now;
262
+ }
263
+ else {
264
+ // Clear the previous timeout and set a new one to save the last change
265
+ clearTimeout(lastChangeTimeout);
266
+ lastChangeTimeout = setTimeout(() => {
267
+ save();
268
+ lastExecutedTime = now;
269
+ }, throttleTimeMS - timeSinceLastExecution);
270
+ }
271
+ }
272
+ // Function to debounce the saving
273
+ let debounceTimeout;
274
+ function debounceSave() {
275
+ clearTimeout(debounceTimeout);
276
+ debounceTimeout = setTimeout(save, autoSaveDebounceTimeMS);
277
+ }
278
+ // Subscribe to listen for modifications
279
+ shareDBDoc.subscribe(() => {
280
+ shareDBDoc.on('op', () => {
281
+ if (shareDBDoc.data.isInteracting) {
282
+ throttleSave();
283
+ }
284
+ else {
285
+ debounceSave();
286
+ }
287
+ });
288
+ });
289
+ server.listen(port, async () => {
290
+ // Note: ngrok support can be added when the package is properly installed
291
+ // Sets the port to the one specified in the environment
292
+ // variable (for development) or the default port.
293
+ let livePort = process.env.EDITOR_PORT || port;
294
+ console.log(`EDITOR_PORT: ${process.env.EDITOR_PORT}`);
295
+ console.log(`Editor is live at http://localhost:${livePort}`);
296
+ open(`http://localhost:${livePort}`);
297
+ });
@@ -0,0 +1,17 @@
1
+ import { AccessToken } from 'livekit-server-sdk';
2
+ const { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } = process.env;
3
+ export const createToken = async (roomName, username) => {
4
+ // If this room doesn't exist, it'll be automatically created when the first
5
+ // client joins
6
+ // Identifier to be used for participant.
7
+ // It's available as LocalParticipant.identity with livekit-client SDK
8
+ // console.log(username);
9
+ const participantName = `${username}`;
10
+ const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
11
+ identity: participantName,
12
+ // Token to expire after 10 minutes
13
+ ttl: '10m',
14
+ });
15
+ at.addGrant({ roomJoin: true, room: roomName });
16
+ return await at.toJwt();
17
+ };
@@ -0,0 +1,90 @@
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
+ // Parser mappings - matches client-side implementation
9
+ const parsers = {
10
+ js: 'babel',
11
+ jsx: 'babel',
12
+ mjs: 'babel',
13
+ cjs: 'babel',
14
+ ts: 'typescript',
15
+ tsx: 'typescript',
16
+ css: 'css',
17
+ html: 'html',
18
+ json: 'json',
19
+ json5: 'json5',
20
+ md: 'markdown',
21
+ markdown: 'markdown',
22
+ };
23
+ // Prettier plugins - matches client-side implementation
24
+ const plugins = [
25
+ prettierPluginBabel,
26
+ prettierPluginEstree,
27
+ prettierPluginHtml,
28
+ prettierPluginMarkdown,
29
+ prettierPluginTypescript,
30
+ prettierPluginCSS,
31
+ ];
32
+ // Prettier options - matches client-side implementation and .prettierrc
33
+ const prettierOptions = {
34
+ proseWrap: 'always',
35
+ singleQuote: true,
36
+ printWidth: 60,
37
+ };
38
+ /**
39
+ * Extracts file extension from filename
40
+ * Example: 'foo.js' => 'js'
41
+ */
42
+ const getFileExtension = (fileName) => {
43
+ const match = fileName.match(/\.([^.]+)$/);
44
+ return match ? match[1] : '';
45
+ };
46
+ /**
47
+ * Formats a single file using Prettier
48
+ */
49
+ export const formatFile = async (fileText, fileName) => {
50
+ const fileExtension = getFileExtension(fileName);
51
+ const parser = parsers[fileExtension];
52
+ // If no parser is found, return null (no formatting)
53
+ if (!parser) {
54
+ return null;
55
+ }
56
+ try {
57
+ const formatted = await format(fileText, {
58
+ parser,
59
+ plugins,
60
+ ...prettierOptions,
61
+ });
62
+ return formatted;
63
+ }
64
+ catch (error) {
65
+ // Log error but don't throw - return original text
66
+ console.error(`Prettier formatting error for ${fileName}:`, error);
67
+ return null;
68
+ }
69
+ };
70
+ /**
71
+ * Formats multiple files using Prettier
72
+ * Only formats files that have supported extensions
73
+ * Returns a map of fileId -> formatted text for successfully formatted files
74
+ */
75
+ export const formatFiles = async (files, fileIds) => {
76
+ const results = {};
77
+ const targetFileIds = fileIds || Object.keys(files);
78
+ // Process files in parallel for better performance
79
+ const formatPromises = targetFileIds.map(async (fileId) => {
80
+ const file = files[fileId];
81
+ if (!file)
82
+ return;
83
+ const formatted = await formatFile(file.text, file.name);
84
+ if (formatted !== null && formatted !== file.text) {
85
+ results[fileId] = formatted;
86
+ }
87
+ });
88
+ await Promise.all(formatPromises);
89
+ return results;
90
+ };
@@ -1,8 +1,7 @@
1
1
  import dotenv from 'dotenv';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { join } from 'node:path';
4
-
5
4
  const dir = fileURLToPath(import.meta.url);
6
5
  dotenv.config({
7
- path: join(dir, '../../../.env'),
6
+ path: join(dir, '../../../.env'),
8
7
  });
@@ -0,0 +1,14 @@
1
+ import { diff } from './ot.js';
2
+ /**
3
+ * Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
4
+ * This is the core logic extracted from useSubmitOperation for reuse across client and server.
5
+ */
6
+ export const createSubmitOperation = (shareDBDoc) => {
7
+ return (next) => {
8
+ const data = shareDBDoc.data;
9
+ const op = diff(data, next(data));
10
+ if (op && shareDBDoc) {
11
+ shareDBDoc.submitOp(op);
12
+ }
13
+ };
14
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};