vzcode 1.45.0 → 1.47.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 (33) hide show
  1. package/dist/assets/{index-C1lTrzOZ.js → index-BMYXp1Uw.js} +114 -114
  2. package/dist/assets/{index-DeeOcIzw.css → index-BXyJ2hMf.css} +1 -1
  3. package/dist/index.html +2 -2
  4. package/package.json +7 -3
  5. package/src/client/CodeEditor/index.tsx +7 -4
  6. package/src/client/RunCodeWidget/index.tsx +18 -15
  7. package/src/client/SplitPaneResizeContext.tsx +26 -108
  8. package/src/client/VZCodeContext.tsx +8 -1
  9. package/src/client/VZRight.tsx +32 -4
  10. package/src/client/VZSidebar/AIChat/ChatInput.tsx +72 -2
  11. package/src/client/VZSidebar/AIChat/DiffView.scss +29 -0
  12. package/src/client/VZSidebar/AIChat/DiffView.tsx +62 -1
  13. package/src/client/VZSidebar/AIChat/Message.tsx +14 -1
  14. package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -13
  15. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
  16. package/src/client/VZSidebar/AIChat/index.tsx +187 -16
  17. package/src/client/VZSidebar/AIChat/styles.scss +278 -1
  18. package/src/client/VZSidebar/aiCopyPaste.ts +6 -1
  19. package/src/client/VZSidebar/index.tsx +10 -2
  20. package/src/client/bootstrap.ts +11 -1
  21. package/src/client/useActions.ts +12 -0
  22. package/src/client/usePrettier/index.ts +1 -3
  23. package/src/client/vzReducer/aiChatReducer.ts +13 -0
  24. package/src/client/vzReducer/createInitialState.ts +1 -0
  25. package/src/client/vzReducer/index.ts +9 -0
  26. package/src/client/vzReducer/searchReducer.test.ts +44 -2
  27. package/src/runCode.ts +5 -17
  28. package/src/server/aiChatHandler/aiEditing.ts +37 -0
  29. package/src/server/aiChatHandler/chatOperations.ts +38 -1
  30. package/src/server/aiChatHandler/index.ts +20 -9
  31. package/src/server/aiChatHandler/llmStreaming.ts +101 -19
  32. package/src/server/generateAIResponse.ts +4 -0
  33. package/src/server/index.ts +52 -0
@@ -2,7 +2,84 @@
2
2
  height: 100%;
3
3
  display: flex;
4
4
  flex-direction: column;
5
- padding: 10px;
5
+ padding: 0;
6
+ }
7
+
8
+ .ai-chat-empty {
9
+ flex: 1;
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ text-align: center;
14
+ padding: 20px;
15
+ overflow-y: auto;
16
+
17
+ .ai-chat-empty-icon {
18
+ font-size: 42px;
19
+ margin-bottom: 15px;
20
+ }
21
+
22
+ .ai-chat-empty-title {
23
+ font-size: 18px;
24
+ font-weight: 600;
25
+ color: var(--vh-color-neutral-04);
26
+ margin-bottom: 15px;
27
+ }
28
+
29
+ .ai-chat-empty-text {
30
+ font-size: 14px;
31
+ color: var(--vh-color-neutral-04);
32
+ margin-bottom: 20px;
33
+ line-height: 1.5;
34
+ }
35
+
36
+ .ai-chat-empty-examples {
37
+ background: var(--vh-color-neutral-02);
38
+ border-radius: 8px;
39
+ padding: 15px;
40
+ margin-bottom: 20px;
41
+ text-align: left;
42
+ width: 100%;
43
+
44
+ h4 {
45
+ font-size: 15px;
46
+ font-weight: 600;
47
+ color: var(--vh-color-neutral-04);
48
+ margin-bottom: 10px;
49
+ }
50
+
51
+ .ai-chat-suggested-prompts {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 8px;
55
+ margin: 0;
56
+
57
+ .ai-chat-suggested-prompt {
58
+ background: var(--vh-color-neutral-03);
59
+ border: 1px solid var(--vh-color-neutral-04);
60
+ border-radius: 6px;
61
+ padding: 8px 12px;
62
+ font-size: 13px;
63
+ color: var(--vh-color-neutral-04);
64
+ text-align: left;
65
+ cursor: pointer;
66
+ transition: all 0.2s ease;
67
+ line-height: 1.4;
68
+
69
+ &:hover {
70
+ background: var(--vh-color-neutral-04);
71
+ color: var(--vh-color-neutral-01);
72
+ border-color: var(--vh-color-primary-01);
73
+ }
74
+
75
+ &:active {
76
+ background: var(--vh-color-primary-01);
77
+ color: white;
78
+ border-color: var(--vh-color-primary-01);
79
+ }
80
+ }
81
+ }
82
+ }
6
83
  }
7
84
 
8
85
  .ai-chat-messages {
@@ -10,6 +87,7 @@
10
87
  overflow-y: auto;
11
88
  margin-bottom: 10px;
12
89
  padding-right: 5px;
90
+ scroll-behavior: smooth;
13
91
 
14
92
  &::-webkit-scrollbar {
15
93
  width: 6px;
@@ -194,13 +272,117 @@
194
272
  }
195
273
  }
196
274
 
275
+ .ai-chat-error {
276
+ margin: 10px 0;
277
+ padding: 0;
278
+ border-radius: 8px;
279
+ background: rgba(220, 53, 69, 0.1);
280
+ border: 1px solid rgba(220, 53, 69, 0.3);
281
+
282
+ .ai-chat-error-content {
283
+ display: flex;
284
+ align-items: center;
285
+ padding: 12px;
286
+ gap: 10px;
287
+
288
+ .ai-chat-error-icon {
289
+ font-size: 16px;
290
+ flex-shrink: 0;
291
+ }
292
+
293
+ .ai-chat-error-message {
294
+ flex: 1;
295
+ font-size: 14px;
296
+ color: #dc3545;
297
+ line-height: 1.4;
298
+ font-weight: 500;
299
+ }
300
+
301
+ .ai-chat-error-dismiss {
302
+ background: none;
303
+ border: none;
304
+ color: #dc3545;
305
+ font-size: 18px;
306
+ font-weight: bold;
307
+ cursor: pointer;
308
+ padding: 0;
309
+ width: 20px;
310
+ height: 20px;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ border-radius: 50%;
315
+ transition: background-color 0.2s ease;
316
+ flex-shrink: 0;
317
+
318
+ &:hover {
319
+ background: rgba(220, 53, 69, 0.1);
320
+ }
321
+
322
+ &:focus {
323
+ outline: 2px solid rgba(220, 53, 69, 0.3);
324
+ outline-offset: 1px;
325
+ }
326
+ }
327
+ }
328
+ }
329
+
197
330
  .ai-chat-input-container {
198
331
  border-top: 1px solid var(--vh-color-neutral-02);
199
332
  padding-top: 10px;
200
333
  }
201
334
 
335
+ .ai-chat-mode-toggle {
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: 4px;
339
+
340
+ .btn-group {
341
+ justify-content: center;
342
+ }
343
+
344
+ .btn-group .btn {
345
+ font-size: 12px;
346
+ font-weight: 500;
347
+ padding: 4px 12px;
348
+ border: 1px solid var(--vh-color-neutral-03);
349
+ background: var(--vh-color-neutral-02);
350
+ color: var(--vh-color-neutral-04);
351
+ transition: all 0.2s ease;
352
+
353
+ &:hover {
354
+ background: var(--vh-color-neutral-03);
355
+ border-color: var(--vh-color-neutral-04);
356
+ }
357
+
358
+ &.btn-primary {
359
+ background: var(--vh-color-primary-01);
360
+ border-color: var(--vh-color-primary-01);
361
+ color: white;
362
+
363
+ &:hover {
364
+ background: var(--vh-color-primary-02);
365
+ border-color: var(--vh-color-primary-02);
366
+ }
367
+ }
368
+
369
+ &:disabled {
370
+ opacity: 0.6;
371
+ cursor: not-allowed;
372
+ }
373
+ }
374
+
375
+ .ai-chat-mode-description {
376
+ font-size: 11px;
377
+ color: var(--vh-color-neutral-03);
378
+ text-align: center;
379
+ font-style: italic;
380
+ }
381
+ }
382
+
202
383
  .ai-chat-input-group {
203
384
  display: flex;
385
+ flex-direction: column;
204
386
  gap: 8px;
205
387
  margin-bottom: 0;
206
388
 
@@ -233,6 +415,18 @@
233
415
  }
234
416
  }
235
417
 
418
+ .ai-chat-input-info {
419
+ display: flex;
420
+ justify-content: space-between;
421
+ font-size: 12px;
422
+ color: var(--vh-color-neutral-03);
423
+ margin-bottom: 6px;
424
+
425
+ .ai-chat-hint {
426
+ font-style: italic;
427
+ }
428
+ }
429
+
236
430
  .ai-chat-send-button {
237
431
  background: var(--vh-color-primary-01);
238
432
  border: none;
@@ -243,6 +437,8 @@
243
437
  font-weight: 500;
244
438
  cursor: pointer;
245
439
  transition: background-color 0.2s ease;
440
+ align-self: flex-end;
441
+ min-width: 80px;
246
442
 
247
443
  &:hover:not(:disabled) {
248
444
  background: var(--vh-color-primary-02);
@@ -254,3 +450,84 @@
254
450
  }
255
451
  }
256
452
  }
453
+
454
+ // Thinking scratchpad styles
455
+ .thinking-scratchpad {
456
+ background: var(--vh-color-neutral-02);
457
+ border: 1px solid var(--vh-color-neutral-03);
458
+ border-radius: 8px;
459
+ margin: 10px 0;
460
+ padding: 0;
461
+ overflow: hidden;
462
+ font-size: 13px;
463
+
464
+ .thinking-scratchpad-header {
465
+ background: var(--vh-color-neutral-03);
466
+ color: var(--vh-color-neutral-04);
467
+ padding: 8px 12px;
468
+ font-weight: 600;
469
+ display: flex;
470
+ align-items: center;
471
+ gap: 8px;
472
+
473
+ .thinking-scratchpad-icon {
474
+ font-size: 16px;
475
+ }
476
+
477
+ .thinking-scratchpad-title {
478
+ font-size: 13px;
479
+ }
480
+ }
481
+
482
+ .thinking-scratchpad-content {
483
+ padding: 12px;
484
+ max-height: 200px;
485
+ overflow-y: auto;
486
+ background: var(--vh-color-neutral-02);
487
+
488
+ // Style markdown content in thinking scratchpad
489
+ p {
490
+ margin: 0 0 8px 0;
491
+ line-height: 1.4;
492
+ color: var(--vh-color-neutral-04);
493
+
494
+ &:last-child {
495
+ margin-bottom: 0;
496
+ }
497
+ }
498
+
499
+ code {
500
+ background: rgba(0, 0, 0, 0.3);
501
+ color: #e6e6e6;
502
+ padding: 1px 4px;
503
+ border-radius: 3px;
504
+ font-size: 12px;
505
+ border: 1px solid rgba(255, 255, 255, 0.1);
506
+ }
507
+
508
+ ul,
509
+ ol {
510
+ margin: 0 0 8px 0;
511
+ padding-left: 20px;
512
+
513
+ li {
514
+ margin-bottom: 4px;
515
+ color: var(--vh-color-neutral-04);
516
+ }
517
+ }
518
+
519
+ h1,
520
+ h2,
521
+ h3,
522
+ h4,
523
+ h5,
524
+ h6 {
525
+ margin: 8px 0 6px 0;
526
+ color: var(--vh-color-neutral-04);
527
+
528
+ &:first-child {
529
+ margin-top: 0;
530
+ }
531
+ }
532
+ }
533
+ }
@@ -7,8 +7,12 @@ import {
7
7
  mergeFileChanges,
8
8
  } from 'editcodewithai';
9
9
  import { VizFiles } from '@vizhub/viz-types';
10
- import { vizFilesToFileCollection } from '@vizhub/viz-utils';
10
+ import {
11
+ generateRunId,
12
+ vizFilesToFileCollection,
13
+ } from '@vizhub/viz-utils';
11
14
  import JSZip from 'jszip';
15
+ import { generate } from '@langchain/core/dist/utils/fast-json-patch';
12
16
 
13
17
  export const createAICopyPasteHandlers = (
14
18
  files: VizFiles,
@@ -106,6 +110,7 @@ export const createAICopyPasteHandlers = (
106
110
  submitOperation((document) => ({
107
111
  ...document,
108
112
  files: mergedFiles,
113
+ runId: generateRunId(),
109
114
  }));
110
115
 
111
116
  const fileCount = Object.keys(parsed.files).length;
@@ -110,7 +110,7 @@ export const VZSidebar = ({
110
110
  ),
111
111
  aiChatToolTipText = (
112
112
  <div>
113
- <strong>AI Chat (beta)</strong>
113
+ <strong>VizBot - AI Code Editor</strong>
114
114
  </div>
115
115
  ),
116
116
  }: {
@@ -184,7 +184,7 @@ export const VZSidebar = ({
184
184
  [files, submitOperation],
185
185
  );
186
186
 
187
- const { sidebarWidth } = useContext(
187
+ const { sidebarWidth, setSidebarView } = useContext(
188
188
  SplitPaneResizeContext,
189
189
  );
190
190
 
@@ -282,6 +282,11 @@ export const VZSidebar = ({
282
282
  previousPendingRef.current = pending;
283
283
  }, [pending, connected, isConnecting]);
284
284
 
285
+ // Automatically adjust sidebar width when view changes
286
+ useEffect(() => {
287
+ setSidebarView(isAIChatOpen);
288
+ }, [isAIChatOpen, setSidebarView]);
289
+
285
290
  return (
286
291
  <div
287
292
  className="vz-sidebar"
@@ -307,6 +312,7 @@ export const VZSidebar = ({
307
312
  onClick={() => {
308
313
  setIsSearchOpen(false);
309
314
  setIsAIChatOpen(false);
315
+ setSidebarView(false); // Switch to files view
310
316
  }}
311
317
  >
312
318
  <FolderSVG />
@@ -327,6 +333,7 @@ export const VZSidebar = ({
327
333
  onClick={() => {
328
334
  setIsSearchOpen(true);
329
335
  setIsAIChatOpen(false);
336
+ setSidebarView(false); // Switch to files view (search uses same width as files)
330
337
  }}
331
338
  >
332
339
  <SearchSVG />
@@ -348,6 +355,7 @@ export const VZSidebar = ({
348
355
  onClick={() => {
349
356
  setIsAIChatOpen(true);
350
357
  setIsSearchOpen(false);
358
+ setSidebarView(true); // Switch to AI chat view
351
359
  }}
352
360
  >
353
361
  <SparklesSVG />
@@ -3,6 +3,8 @@
3
3
  // Otherwise we get `Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import`
4
4
  import Form from 'react-bootstrap/cjs/Form.js';
5
5
  import Button from 'react-bootstrap/cjs/Button.js';
6
+ import ButtonGroup from 'react-bootstrap/cjs/ButtonGroup.js';
7
+ import ToggleButton from 'react-bootstrap/cjs/ToggleButton.js';
6
8
  import Modal from 'react-bootstrap/cjs/Modal.js';
7
9
  import OverlayTrigger from 'react-bootstrap/cjs/OverlayTrigger.js';
8
10
  import Tooltip from 'react-bootstrap/cjs/Tooltip.js';
@@ -10,4 +12,12 @@ import Tooltip from 'react-bootstrap/cjs/Tooltip.js';
10
12
  // Pull in custom CSS from vizhub-ui.
11
13
  import 'vizhub-ui/dist/vizhub-ui.css';
12
14
 
13
- export { Form, Button, Modal, OverlayTrigger, Tooltip };
15
+ export {
16
+ Form,
17
+ Button,
18
+ ButtonGroup,
19
+ ToggleButton,
20
+ Modal,
21
+ OverlayTrigger,
22
+ Tooltip,
23
+ };
@@ -168,6 +168,17 @@ export const useActions = (
168
168
  });
169
169
  }, [dispatch]);
170
170
 
171
+ // Set the AI chat mode (ask or edit)
172
+ const setAIChatMode = useCallback(
173
+ (mode: 'ask' | 'edit') => {
174
+ dispatch({
175
+ type: 'set_ai_chat_mode',
176
+ mode,
177
+ });
178
+ },
179
+ [dispatch],
180
+ );
181
+
171
182
  // True to show the settings modal.
172
183
  const setIsSettingsOpen = useCallback(
173
184
  (value: boolean) => {
@@ -251,6 +262,7 @@ export const useActions = (
251
262
  toggleSearchFocused,
252
263
  setIsAIChatOpen,
253
264
  toggleAIChatFocused,
265
+ setAIChatMode,
254
266
  setIsSettingsOpen,
255
267
  setIsDocOpen,
256
268
  closeSettings,
@@ -1,7 +1,5 @@
1
- import { shouldTriggerRun } from '../shouldTriggerRun';
2
- import { JSONOp } from '../../ot';
3
1
  import { VizFileId, VizContent } from '@vizhub/viz-types';
4
- import { ShareDBDoc } from '../../types';
2
+ import { JSONOp, ShareDBDoc } from '../../types';
5
3
  import { useEffect, useRef, useState } from 'react';
6
4
 
7
5
  // The time in milliseconds by which auto-saving is debounced.
@@ -29,3 +29,16 @@ export const toggleAIChatFocusedReducer = (
29
29
  }
30
30
  return state;
31
31
  };
32
+
33
+ export const setAIChatModeReducer = (
34
+ state: VZState,
35
+ action: VZAction,
36
+ ): VZState => {
37
+ if (action.type === 'set_ai_chat_mode') {
38
+ return {
39
+ ...state,
40
+ aiChatMode: action.mode,
41
+ };
42
+ }
43
+ return state;
44
+ };
@@ -27,6 +27,7 @@ export const createInitialState = ({
27
27
  isSearchOpen: false,
28
28
  isAIChatOpen: false,
29
29
  aiChatFocused: false,
30
+ aiChatMode: 'edit',
30
31
  isSettingsOpen: false,
31
32
  isDocOpen: false,
32
33
  editorWantsFocus: false,
@@ -33,6 +33,7 @@ import {
33
33
  import {
34
34
  setIsAIChatOpenReducer,
35
35
  toggleAIChatFocusedReducer,
36
+ setAIChatModeReducer,
36
37
  } from './aiChatReducer';
37
38
  import { toggleAutoFollowReducer } from './toggleAutoFollowReducer';
38
39
  import { updatePresenceIndicatorReducer } from './updatePresenceIndicatorReducer';
@@ -62,6 +63,9 @@ export type VZState = {
62
63
  // True if the AI chat input should focus on the next render.
63
64
  aiChatFocused: boolean;
64
65
 
66
+ // The current AI chat mode: 'ask' or 'edit'
67
+ aiChatMode: 'ask' | 'edit';
68
+
65
69
  // True to show the settings modal.
66
70
  isSettingsOpen: boolean;
67
71
 
@@ -133,6 +137,10 @@ export type VZAction =
133
137
  // * Toggles focused variable to trigger AI chat input focus
134
138
  | { type: 'toggle_ai_chat_focused' }
135
139
 
140
+ // `set_ai_chat_mode`
141
+ // * Sets the AI chat mode (ask or edit)
142
+ | { type: 'set_ai_chat_mode'; mode: 'ask' | 'edit' }
143
+
136
144
  // `set_search`
137
145
  // * Sets the current search pattern
138
146
  | { type: 'set_search'; value: string }
@@ -212,6 +220,7 @@ const reducers = [
212
220
  setIsSearchOpenReducer,
213
221
  setIsAIChatOpenReducer,
214
222
  toggleAIChatFocusedReducer,
223
+ setAIChatModeReducer,
215
224
  setIsSettingsOpenReducer,
216
225
  setIsDocOpenReducer,
217
226
  editorNoLongerWantsFocusReducer,
@@ -2,10 +2,52 @@ import { describe, expect, it } from 'vitest';
2
2
  import { setSearchResultsReducer } from './searchReducer';
3
3
  import { VZAction, VZState, createInitialState } from '.';
4
4
  import { defaultTheme } from '../themes';
5
+ import { ShareDBDoc } from '../../types';
6
+ import { VizContent } from '@vizhub/viz-types';
5
7
 
6
8
  // Mock ShareDBDoc for testing
7
- const createMockShareDBDoc = (files: any) => ({
8
- data: { files },
9
+ const createMockShareDBDoc = (
10
+ files: any,
11
+ ): ShareDBDoc<VizContent> => ({
12
+ data: { files, id: 'mock-id' },
13
+ ingestSnapshot: (snapshot: any, callback: any) => {
14
+ // No-op for testing
15
+ if (callback) callback();
16
+ },
17
+ subscribe: (callback: any) => {
18
+ // No-op for testing
19
+ if (callback) callback();
20
+ },
21
+ on: (
22
+ event: string,
23
+ callback: (op: any, source: boolean) => void,
24
+ ) => {
25
+ // No-op for testing
26
+ },
27
+ off: (
28
+ event: string,
29
+ callback: (op: any, source: boolean) => void,
30
+ ) => {
31
+ // No-op for testing
32
+ },
33
+ removeListener: (
34
+ event: string,
35
+ callback: (op: any, source: boolean) => void,
36
+ ) => {
37
+ // No-op for testing
38
+ },
39
+ submitOp: (
40
+ op: any,
41
+ options?: any,
42
+ callback?: () => void,
43
+ ) => {
44
+ // No-op for testing
45
+ if (callback) callback();
46
+ },
47
+ whenNothingPending: (callback: () => void) => {
48
+ // No-op for testing
49
+ if (callback) callback();
50
+ },
9
51
  });
10
52
 
11
53
  const initialState: VZState = createInitialState({
package/src/runCode.ts CHANGED
@@ -1,30 +1,18 @@
1
+ import { generateRunId } from '@vizhub/viz-utils';
1
2
  import { SubmitOperation } from './types';
2
3
  import { VizContent } from '@vizhub/viz-types';
3
4
 
4
5
  /**
5
- * Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
6
+ * Creates a runCode function that triggers code execution by changing the `runId` in the content.
6
7
  * This works for both client-side (with submitOperation) and server-side (with ShareDB document).
7
8
  */
8
- export const createRunCodeFunction = (
9
- submitOperation: SubmitOperation,
10
- ) => {
11
- return () => {
12
- // Use the unified submitOperation approach for both client and server
9
+ export const createRunCodeFunction =
10
+ (submitOperation: SubmitOperation) => () => {
13
11
  submitOperation((content: VizContent) => ({
14
12
  ...content,
15
- isInteracting: true,
13
+ runId: generateRunId(),
16
14
  }));
17
-
18
- setTimeout(() => {
19
- // This somewhat cryptic logic
20
- // deletes the `isInteracting` property
21
- // from the document.
22
- submitOperation(
23
- ({ isInteracting, ...newDocument }) => newDocument,
24
- );
25
- }, 100);
26
15
  };
27
- };
28
16
 
29
17
  /**
30
18
  * Creates a runCodeRef object compatible with React refs.
@@ -12,6 +12,42 @@ import {
12
12
  // Useful for debugging and testing purposes, e.g. checking the typing indicator.
13
13
  const delayStart = false;
14
14
 
15
+ /**
16
+ * Performs AI chat without editing - just generates a response
17
+ */
18
+ export const performAIChat = async ({
19
+ prompt,
20
+ shareDBDoc,
21
+ llmFunction,
22
+ }) => {
23
+ const preparedFiles = prepareFilesForPrompt(
24
+ shareDBDoc.data.files,
25
+ );
26
+ const filesContext = formatMarkdownFiles(preparedFiles);
27
+
28
+ // 2. Assemble the final prompt for Q&A mode
29
+ const fullPrompt = assembleFullPrompt({
30
+ filesContext,
31
+ prompt,
32
+ editFormat: 'whole',
33
+ });
34
+
35
+ if (delayStart) {
36
+ await new Promise((resolve) =>
37
+ setTimeout(resolve, 1000),
38
+ );
39
+ }
40
+
41
+ // Call the LLM function which will handle streaming but won't edit files
42
+ const result = await llmFunction(fullPrompt);
43
+
44
+ return {
45
+ content: result.content,
46
+ generationId: result.generationId,
47
+ diffData: {}, // No file changes in ask mode
48
+ };
49
+ };
50
+
15
51
  /**
16
52
  * Performs AI editing operations using streaming with incremental OT operations
17
53
  */
@@ -62,5 +98,6 @@ export const performAIEditing = async ({
62
98
  content: result.content,
63
99
  generationId: result.generationId,
64
100
  diffData,
101
+ beforeFiles, // Store the snapshot for undo functionality
65
102
  };
66
103
  };