vzcode 2.15.0 → 2.16.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 (45) hide show
  1. package/dist/assets/{index-BpUpNto4.js → index-C2BLPyQP.js} +138 -125
  2. package/dist/assets/{index-DLm5FQ0E.css → index-DVR90LwF.css} +1 -1
  3. package/dist/index.html +2 -2
  4. package/dist/server/aiChatHandler/chatOperations.js +168 -35
  5. package/dist/server/aiChatHandler/index.js +11 -30
  6. package/dist/server/aiChatHandler/llmStreaming.js +105 -118
  7. package/package.json +11 -11
  8. package/src/client/AIAssist/AIAssistWidget/index.tsx +12 -1
  9. package/src/client/App/useShareDB.ts +1 -1
  10. package/src/client/CodeEditor/index.tsx +9 -10
  11. package/src/client/VZCodeContext/types.ts +3 -1
  12. package/src/client/VZCodeContext/useVZCodeState.ts +7 -5
  13. package/src/client/VZRight.tsx +10 -2
  14. package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -7
  15. package/src/client/VZSidebar/AIChat/DiffView.scss +1 -0
  16. package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -29
  17. package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +81 -0
  18. package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +86 -0
  19. package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +64 -0
  20. package/src/client/VZSidebar/AIChat/Message.tsx +68 -53
  21. package/src/client/VZSidebar/AIChat/MessageList.tsx +139 -118
  22. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +63 -21
  23. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +4 -118
  24. package/src/client/VZSidebar/AIChat/index.tsx +62 -19
  25. package/src/client/VZSidebar/AIChat/styles.scss +159 -16
  26. package/src/client/VZSidebar/Item.tsx +2 -2
  27. package/src/client/VZSidebar/Search.tsx +13 -6
  28. package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +39 -23
  29. package/src/client/VZSidebar/VisualEditor/utils.ts +1 -1
  30. package/src/client/VZSidebar/index.tsx +12 -10
  31. package/src/client/VZSidebar/useDragAndDrop.tsx +225 -214
  32. package/src/client/featureFlags.ts +3 -0
  33. package/src/client/hooks/useAutoScroll.ts +329 -0
  34. package/src/client/tabsSearchParameters.ts +1 -17
  35. package/src/client/useFileCRUD.ts +5 -2
  36. package/src/client/useKeyboardShortcuts.ts +7 -0
  37. package/src/client/useOpenDirectories.ts +2 -1
  38. package/src/client/usePrettier/index.ts +1 -1
  39. package/src/client/useURLSync.ts +1 -0
  40. package/src/client/utils/scrollUtils.ts +170 -0
  41. package/src/client/vzReducer/searchReducer.ts +6 -3
  42. package/src/server/aiChatHandler/chatOperations.ts +224 -43
  43. package/src/server/aiChatHandler/index.ts +8 -48
  44. package/src/server/aiChatHandler/llmStreaming.ts +149 -170
  45. package/src/types.ts +81 -1
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-BpUpNto4.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-DLm5FQ0E.css">
23
+ <script type="module" crossorigin src="/assets/index-C2BLPyQP.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-DVR90LwF.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
@@ -60,10 +60,13 @@ export const addUserMessage = (shareDBDoc, chatId, content) => {
60
60
  shareDBDoc.submitOp(userMessageOp);
61
61
  return userMessage;
62
62
  };
63
+ const DEBUG = false;
63
64
  /**
64
65
  * Updates AI status in the chat
65
66
  */
66
67
  export const updateAIStatus = (shareDBDoc, chatId, status) => {
68
+ DEBUG &&
69
+ console.log(`ChatOperations: updateAIStatus called with status: "${status}" for chatId: ${chatId}`);
67
70
  const op = diff(shareDBDoc.data, {
68
71
  ...shareDBDoc.data,
69
72
  chats: {
@@ -74,7 +77,11 @@ export const updateAIStatus = (shareDBDoc, chatId, status) => {
74
77
  },
75
78
  },
76
79
  });
80
+ DEBUG &&
81
+ console.log(`ChatOperations: Submitting operation for status update:`, op);
77
82
  shareDBDoc.submitOp(op);
83
+ DEBUG &&
84
+ console.log(`ChatOperations: Status update operation submitted successfully`);
78
85
  };
79
86
  /**
80
87
  * Updates AI scratchpad content
@@ -307,54 +314,180 @@ export const createNewFile = (shareDBDoc, fileName) => {
307
314
  shareDBDoc.submitOp(op);
308
315
  return newFileId;
309
316
  };
317
+ // ============================================================================
318
+ // Streaming Chat Operations
319
+ // ============================================================================
310
320
  /**
311
- * Ensures a file exists, creating it if necessary
321
+ * Creates a streaming AI message with events array
312
322
  */
313
- export const ensureFileExists = (shareDBDoc, fileName) => {
314
- let fileId = resolveFileId(fileName, shareDBDoc);
315
- if (!fileId) {
316
- // File doesn't exist, create it
317
- fileId = createNewFile(shareDBDoc, fileName);
318
- }
319
- return fileId;
323
+ export const createStreamingAIMessage = (shareDBDoc, chatId) => {
324
+ const aiMessage = {
325
+ id: `assistant-${Date.now()}`,
326
+ role: 'assistant',
327
+ content: '',
328
+ timestamp: dateToTimestamp(new Date()),
329
+ streamingEvents: [],
330
+ isProgressive: true,
331
+ };
332
+ const messageOp = diff(shareDBDoc.data, {
333
+ ...shareDBDoc.data,
334
+ chats: {
335
+ ...shareDBDoc.data.chats,
336
+ [chatId]: {
337
+ ...shareDBDoc.data.chats[chatId],
338
+ messages: [
339
+ ...shareDBDoc.data.chats[chatId].messages,
340
+ aiMessage,
341
+ ],
342
+ updatedAt: dateToTimestamp(new Date()),
343
+ isStreaming: true,
344
+ },
345
+ },
346
+ });
347
+ shareDBDoc.submitOp(messageOp);
348
+ return aiMessage.id;
320
349
  };
321
350
  /**
322
- * Clears the content of a file
351
+ * Adds a streaming event to the most recent AI message
323
352
  */
324
- export const clearFileContent = (shareDBDoc, fileId) => {
325
- const currentFile = shareDBDoc.data.files[fileId];
326
- if (currentFile && currentFile.text) {
327
- // Clear the file content
328
- const newState = {
353
+ export const addStreamingEvent = (shareDBDoc, chatId, event) => {
354
+ DEBUG &&
355
+ console.log(`ChatOperations: Adding streaming event:`, event);
356
+ const chat = shareDBDoc.data.chats[chatId];
357
+ const messages = [...chat.messages];
358
+ const lastMessageIndex = messages.length - 1;
359
+ if (lastMessageIndex >= 0 &&
360
+ messages[lastMessageIndex].role === 'assistant') {
361
+ const lastMessage = messages[lastMessageIndex];
362
+ const updatedEvents = [
363
+ ...(lastMessage.streamingEvents || []),
364
+ event,
365
+ ];
366
+ messages[lastMessageIndex] = {
367
+ ...lastMessage,
368
+ streamingEvents: updatedEvents,
369
+ };
370
+ const messageOp = diff(shareDBDoc.data, {
329
371
  ...shareDBDoc.data,
330
- files: {
331
- ...shareDBDoc.data.files,
332
- [fileId]: {
333
- ...currentFile,
334
- text: '',
372
+ chats: {
373
+ ...shareDBDoc.data.chats,
374
+ [chatId]: {
375
+ ...chat,
376
+ messages,
377
+ updatedAt: dateToTimestamp(new Date()),
335
378
  },
336
379
  },
337
- };
338
- const op = diff(shareDBDoc.data, newState);
339
- shareDBDoc.submitOp(op);
380
+ });
381
+ shareDBDoc.submitOp(messageOp);
340
382
  }
341
383
  };
342
384
  /**
343
- * Appends a line to a file using OT operations
385
+ * Updates streaming status for a chat
344
386
  */
345
- export const appendLineToFile = (shareDBDoc, fileId, line) => {
346
- const currentFile = shareDBDoc.data.files[fileId];
347
- const currentContent = currentFile?.text || '';
348
- const newContent = currentContent + line + '\n';
349
- const newDocState = {
387
+ export const updateStreamingStatus = (shareDBDoc, chatId, status, isStreaming = true) => {
388
+ DEBUG &&
389
+ console.log(`ChatOperations: Updating streaming status: "${status}"`);
390
+ const op = diff(shareDBDoc.data, {
350
391
  ...shareDBDoc.data,
351
- files: {
352
- ...shareDBDoc.data.files,
353
- [fileId]: {
354
- ...currentFile,
355
- text: newContent,
392
+ chats: {
393
+ ...shareDBDoc.data.chats,
394
+ [chatId]: {
395
+ ...shareDBDoc.data.chats[chatId],
396
+ currentStatus: status,
397
+ isStreaming,
398
+ updatedAt: dateToTimestamp(new Date()),
356
399
  },
357
400
  },
358
- };
359
- shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
401
+ });
402
+ shareDBDoc.submitOp(op);
403
+ };
404
+ /**
405
+ * Finalizes streaming message and clears streaming state
406
+ */
407
+ export const finalizeStreamingMessage = (shareDBDoc, chatId) => {
408
+ DEBUG &&
409
+ console.log(`ChatOperations: Finalizing streaming message`);
410
+ const chat = shareDBDoc.data.chats[chatId];
411
+ const messages = [...chat.messages];
412
+ const lastMessageIndex = messages.length - 1;
413
+ if (lastMessageIndex >= 0 &&
414
+ messages[lastMessageIndex].role === 'assistant') {
415
+ const lastMessage = messages[lastMessageIndex];
416
+ messages[lastMessageIndex] = {
417
+ ...lastMessage,
418
+ isComplete: true,
419
+ };
420
+ const messageOp = diff(shareDBDoc.data, {
421
+ ...shareDBDoc.data,
422
+ chats: {
423
+ ...shareDBDoc.data.chats,
424
+ [chatId]: {
425
+ ...chat,
426
+ messages,
427
+ currentStatus: 'Done',
428
+ isStreaming: false,
429
+ updatedAt: dateToTimestamp(new Date()),
430
+ },
431
+ },
432
+ });
433
+ shareDBDoc.submitOp(messageOp);
434
+ }
360
435
  };
436
+ // /**
437
+ // * Ensures a file exists, creating it if necessary
438
+ // */
439
+ // export const ensureFileExists = (shareDBDoc, fileName) => {
440
+ // let fileId = resolveFileId(fileName, shareDBDoc);
441
+ // if (!fileId) {
442
+ // // File doesn't exist, create it
443
+ // fileId = createNewFile(shareDBDoc, fileName);
444
+ // }
445
+ // return fileId;
446
+ // };
447
+ // /**
448
+ // * Clears the content of a file
449
+ // */
450
+ // export const clearFileContent = (
451
+ // shareDBDoc: ShareDBDoc<VizContent>,
452
+ // fileId: VizFileId,
453
+ // ) => {
454
+ // const currentFile = shareDBDoc.data.files[fileId];
455
+ // if (currentFile && currentFile.text) {
456
+ // // Clear the file content
457
+ // const newState = {
458
+ // ...shareDBDoc.data,
459
+ // files: {
460
+ // ...shareDBDoc.data.files,
461
+ // [fileId]: {
462
+ // ...currentFile,
463
+ // text: '',
464
+ // },
465
+ // },
466
+ // };
467
+ // const op = diff(shareDBDoc.data, newState);
468
+ // shareDBDoc.submitOp(op);
469
+ // }
470
+ // };
471
+ // /**
472
+ // * Appends a line to a file using OT operations
473
+ // */
474
+ // export const appendLineToFile = (
475
+ // shareDBDoc: ShareDBDoc<VizContent>,
476
+ // fileId: VizFileId,
477
+ // line: string,
478
+ // ) => {
479
+ // const currentFile = shareDBDoc.data.files[fileId];
480
+ // const currentContent = currentFile?.text || '';
481
+ // const newContent = currentContent + line + '\n';
482
+ // const newDocState = {
483
+ // ...shareDBDoc.data,
484
+ // files: {
485
+ // ...shareDBDoc.data.files,
486
+ // [fileId]: {
487
+ // ...currentFile,
488
+ // text: newContent,
489
+ // },
490
+ // },
491
+ // };
492
+ // shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
493
+ // };
@@ -1,14 +1,14 @@
1
1
  import { validateRequest } from './validation.js';
2
- import { ensureChatsExist, ensureChatExists, addUserMessage, addDiffToAIMessage, setAIStatus, } from './chatOperations.js';
2
+ import { ensureChatsExist, ensureChatExists, addUserMessage, setAIStatus, } from './chatOperations.js';
3
3
  import { createLLMFunction } from './llmStreaming.js';
4
- import { performAIEditing, performAIChat, } from './aiEditing.js';
4
+ import { performAIEditing } from './aiEditing.js';
5
5
  import { handleError, handleBackgroundError, } from './errorHandling.js';
6
6
  import { createRunCodeFunction } from '../../runCode.js';
7
7
  import { createSubmitOperation } from '../../submitOperation.js';
8
8
  import { getGenerationMetadata } from 'editcodewithai';
9
9
  const DEBUG = false;
10
- export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, getCurrentCommitId, model, aiRequestOptions, }) => async (req, res) => {
11
- const { content, chatId, mode = 'edit' } = req.body;
10
+ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, model, aiRequestOptions, }) => async (req, res) => {
11
+ const { content, chatId } = req.body;
12
12
  if (DEBUG) {
13
13
  console.log('[handleAIChatMessage] content:', content, 'chatId:', chatId, 'shareDBDoc:', shareDBDoc);
14
14
  }
@@ -24,15 +24,11 @@ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, getCurrentC
24
24
  addUserMessage(shareDBDoc, chatId, content);
25
25
  // Return success immediately - AI generation continues in background
26
26
  res.status(200).json('success');
27
- // Set AI status to indicate generation is starting
28
- setAIStatus(shareDBDoc, chatId, 'generating');
29
27
  // Continue AI processing in background (don't await)
30
28
  processAIRequestAsync({
31
29
  shareDBDoc,
32
30
  chatId,
33
31
  content,
34
- mode,
35
- getCurrentCommitId,
36
32
  model,
37
33
  aiRequestOptions,
38
34
  onCreditDeduction,
@@ -49,12 +45,8 @@ export const handleAIChatMessage = ({ shareDBDoc, onCreditDeduction, getCurrentC
49
45
  /**
50
46
  * Processes the AI request asynchronously in the background
51
47
  */
52
- const processAIRequestAsync = async ({ shareDBDoc, chatId, content, mode, getCurrentCommitId, model, aiRequestOptions, onCreditDeduction, }) => {
48
+ const processAIRequestAsync = async ({ shareDBDoc, chatId, content, model, aiRequestOptions, onCreditDeduction, }) => {
53
49
  try {
54
- // Capture the current commit ID before making changes (for VizHub integration)
55
- const beforeCommitId = getCurrentCommitId
56
- ? getCurrentCommitId()
57
- : null;
58
50
  // Create LLM function for streaming
59
51
  const llmFunction = createLLMFunction({
60
52
  shareDBDoc,
@@ -66,23 +58,12 @@ const processAIRequestAsync = async ({ shareDBDoc, chatId, content, mode, getCur
66
58
  const submitOperation = createSubmitOperation(shareDBDoc);
67
59
  const runCode = createRunCodeFunction(submitOperation);
68
60
  // Perform AI editing or chat based on mode
69
- const editResult = mode === 'ask'
70
- ? await performAIChat({
71
- prompt: content,
72
- shareDBDoc,
73
- llmFunction,
74
- })
75
- : await performAIEditing({
76
- prompt: content,
77
- shareDBDoc,
78
- llmFunction,
79
- runCode,
80
- });
81
- // Add diff data to the AI message if there are changes
82
- if (editResult.diffData &&
83
- Object.keys(editResult.diffData).length > 0) {
84
- addDiffToAIMessage(shareDBDoc, chatId, editResult.diffData, beforeCommitId);
85
- }
61
+ const editResult = await performAIEditing({
62
+ prompt: content,
63
+ shareDBDoc,
64
+ llmFunction,
65
+ runCode,
66
+ });
86
67
  // Handle credit deduction if callback is provided
87
68
  if (onCreditDeduction && editResult.generationId) {
88
69
  try {
@@ -1,34 +1,12 @@
1
- import fs from 'fs';
2
1
  import OpenAI from 'openai';
3
2
  import { parseMarkdownFiles, StreamingMarkdownParser, } from 'llm-code-format';
4
3
  import { mergeFileChanges } from 'editcodewithai';
5
4
  import { generateRunId } from '@vizhub/viz-utils';
6
- import { updateAIStatus, createAIMessage, updateAIMessageContent, finalizeAIMessage, ensureFileExists, clearFileContent, appendLineToFile, updateFiles, updateAIScratchpad, } from './chatOperations.js';
5
+ import { updateFiles, updateAIScratchpad, createStreamingAIMessage, addStreamingEvent, updateStreamingStatus, finalizeStreamingMessage, } from './chatOperations.js';
7
6
  import { diff } from '../../ot.js';
8
7
  const DEBUG = false;
9
8
  // Useful for testing/debugging the streaming behavior
10
9
  const slowMode = false;
11
- // Throttle the streaming updates, so that we don't
12
- // overwhelm the ShareDB server with too many updates.
13
- // It happened actually, before adding this.
14
- // MongoDB VizHub server got in fact overloaded with
15
- // too many updates from the AI streaming response, with
16
- // warning: "Replication Oplog Window has gone below 1 hour"
17
- const THROTTLE_INTERVAL_MS = 500;
18
- // Feature flag to enable/disable streaming editing.
19
- // * If `true`, the AI streaming response will be used to
20
- // edit files in real-time by submitting ShareDB ops.
21
- // * If `false`, the updates to code files will be applied
22
- // only after the AI has finished generating the entire response.
23
- //
24
- // Current status: there's a tricky bug with the streaming
25
- // where the AI edits sometimes don't apply correctly in CodeMirror.
26
- // It seems that sometimes the op that clears the file content
27
- // is not applied correctly in the front end, leading to
28
- // a situation where the AI streaming edits are concatenated into the middle
29
- // of the file instead of replacing it.
30
- // See https://github.com/codemirror/codemirror.next/issues/1234
31
- const enableStreamingEditing = false;
32
10
  /**
33
11
  * Creates and configures the LLM function for streaming with reasoning tokens
34
12
  */
@@ -50,87 +28,92 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
50
28
  });
51
29
  let fullContent = '';
52
30
  let generationId = '';
53
- let currentEditingFileId = null;
54
31
  let currentEditingFileName = null;
55
- // Create initial AI message for streaming
56
- const aiMessageId = createAIMessage(shareDBDoc, chatId);
57
- // --- throttle wrapper ---
58
- function makeThrottledUpdater() {
59
- let lastCall = 0;
60
- let latestContent = '';
61
- let timer = null;
62
- function invoke() {
63
- updateAIMessageContent(shareDBDoc, chatId, aiMessageId, latestContent);
64
- lastCall = Date.now();
65
- }
66
- const fn = (content) => {
67
- latestContent = content;
68
- const now = Date.now();
69
- if (now - lastCall >= THROTTLE_INTERVAL_MS) {
70
- // safe to call immediately
71
- invoke();
72
- }
73
- else if (!timer) {
74
- // schedule for later
75
- timer = setTimeout(() => {
76
- timer = null;
77
- invoke();
78
- }, THROTTLE_INTERVAL_MS - (now - lastCall));
32
+ let accumulatedTextChunk = '';
33
+ let currentFileContent = '';
34
+ // Create streaming AI message
35
+ createStreamingAIMessage(shareDBDoc, chatId);
36
+ // Set initial content generation status
37
+ updateStreamingStatus(shareDBDoc, chatId, 'Formulating a plan...');
38
+ // Helper to get original file content
39
+ const getOriginalFileContent = (fileName) => {
40
+ const files = shareDBDoc.data.files;
41
+ for (const file of Object.values(files)) {
42
+ if (file.name === fileName) {
43
+ return file.text || '';
79
44
  }
80
- };
81
- // expose a flush() helper to force an immediate write
82
- fn.flush = () => {
83
- if (timer) {
84
- clearTimeout(timer);
85
- timer = null;
86
- }
87
- invoke();
88
- };
89
- return fn;
90
- }
91
- const throttledUpdateAIMessageContent = makeThrottledUpdater();
92
- // Function to report file edited
93
- // This is called when the AI has finished editing a file
94
- // and we want to update the message content with the file name.
95
- const reportFileEdited = () => {
96
- if (currentEditingFileName) {
97
- fullContent += ` * Edited ${currentEditingFileName}\n`;
98
- throttledUpdateAIMessageContent(fullContent);
99
- currentEditingFileName = null;
45
+ }
46
+ return '';
47
+ };
48
+ // Helper to emit text chunk when accumulated
49
+ const emitTextChunk = async () => {
50
+ if (accumulatedTextChunk.trim()) {
51
+ DEBUG &&
52
+ console.log('LLMStreaming: Emitting text chunk:', accumulatedTextChunk.substring(0, 100) + '...');
53
+ await addStreamingEvent(shareDBDoc, chatId, {
54
+ type: 'text_chunk',
55
+ content: accumulatedTextChunk,
56
+ timestamp: Date.now(),
57
+ });
58
+ accumulatedTextChunk = '';
59
+ }
60
+ };
61
+ // Helper to complete file editing
62
+ const completeFileEditing = async (fileName) => {
63
+ if (fileName && currentFileContent) {
64
+ DEBUG &&
65
+ console.log(`LLMStreaming: Completing file editing for ${fileName}`);
66
+ await addStreamingEvent(shareDBDoc, chatId, {
67
+ type: 'file_complete',
68
+ fileName,
69
+ beforeContent: getOriginalFileContent(fileName),
70
+ afterContent: currentFileContent,
71
+ timestamp: Date.now(),
72
+ });
73
+ currentFileContent = '';
100
74
  }
101
75
  };
102
76
  // Define callbacks for streaming parser
103
77
  const callbacks = {
104
78
  onFileNameChange: async (fileName, format) => {
105
79
  DEBUG &&
106
- console.log(`File changed to: ${fileName} (${format})`);
107
- // Find existing file or create new one
108
- currentEditingFileId = ensureFileExists(shareDBDoc, fileName);
109
- reportFileEdited();
80
+ console.log(`LLMStreaming: File changed to: ${fileName} (${format})`);
81
+ // Emit any accumulated text chunk first
82
+ await emitTextChunk();
83
+ // Complete previous file if any
84
+ if (currentEditingFileName) {
85
+ await completeFileEditing(currentEditingFileName);
86
+ }
87
+ // Start new file
110
88
  currentEditingFileName = fileName;
111
- // Clear the file content to start fresh
112
- // (AI will regenerate the entire file content)
113
- clearFileContent(shareDBDoc, currentEditingFileId);
114
- // Update AI status
115
- updateAIStatus(shareDBDoc, chatId, 'Editing ' + fileName);
89
+ currentFileContent = '';
90
+ // Emit file start event
91
+ await addStreamingEvent(shareDBDoc, chatId, {
92
+ type: 'file_start',
93
+ fileName,
94
+ timestamp: Date.now(),
95
+ });
96
+ // Update status
97
+ updateStreamingStatus(shareDBDoc, chatId, `Editing ${fileName}...`);
116
98
  },
117
99
  onCodeLine: async (line) => {
118
100
  DEBUG && console.log(`Code line: ${line}`);
119
- // If streaming is enabled, we apply the line immediately
120
- if (currentEditingFileId) {
121
- // Apply OT operation for this line immediately
122
- appendLineToFile(shareDBDoc, currentEditingFileId, line);
123
- }
101
+ // Accumulate code content for the current file
102
+ currentFileContent += line + '\n';
124
103
  },
125
104
  onNonCodeLine: async (line) => {
126
- // We want to report a file edited only if the line is not empty,
127
- // because sometimes the LLMs leave a newline between the file name
128
- // declaration and th
105
+ DEBUG && console.log(`Non-code line: ${line}`);
106
+ // Accumulate non-code content as text chunk
129
107
  if (line.trim() !== '') {
130
- reportFileEdited();
108
+ accumulatedTextChunk += line + '\n';
109
+ // Update status for subsequent non-code chunks
110
+ if (firstNonCodeChunkProcessed) {
111
+ updateStreamingStatus(shareDBDoc, chatId, 'Describing changes...');
112
+ }
113
+ else {
114
+ firstNonCodeChunkProcessed = true;
115
+ }
131
116
  }
132
- fullContent += line + '\n';
133
- throttledUpdateAIMessageContent(fullContent);
134
117
  },
135
118
  };
136
119
  const parser = new StreamingMarkdownParser(callbacks);
@@ -158,6 +141,7 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
158
141
  const stream = await openRouterClient.chat.completions.create(requestConfig);
159
142
  let reasoningStarted = false;
160
143
  let contentStarted = false;
144
+ let firstNonCodeChunkProcessed = false;
161
145
  for await (const chunk of stream) {
162
146
  if (slowMode) {
163
147
  await new Promise((resolve) => setTimeout(resolve, 500));
@@ -167,28 +151,30 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
167
151
  // Handle reasoning tokens (thinking) - only if enabled
168
152
  if (!reasoningStarted) {
169
153
  reasoningStarted = true;
170
- updateAIStatus(shareDBDoc, chatId, 'Thinking...');
154
+ updateStreamingStatus(shareDBDoc, chatId, 'Thinking...');
171
155
  }
172
156
  reasoningContent += delta.reasoning;
173
157
  updateAIScratchpad(shareDBDoc, chatId, reasoningContent);
174
158
  }
175
159
  else if (delta?.content) {
176
160
  // Handle regular content tokens
177
- if (reasoningStarted && !contentStarted) {
178
- // Clear reasoning when content starts
161
+ if (!contentStarted) {
179
162
  contentStarted = true;
180
- updateAIScratchpad(shareDBDoc, chatId, '');
181
- updateAIStatus(shareDBDoc, chatId, 'Generating response...');
163
+ if (reasoningStarted) {
164
+ // Clear reasoning when content starts
165
+ updateAIScratchpad(shareDBDoc, chatId, '');
166
+ }
167
+ // // Set initial content generation status
168
+ // updateStreamingStatus(
169
+ // shareDBDoc,
170
+ // chatId,
171
+ // 'Formulating a plan...',
172
+ // );
182
173
  }
183
174
  const chunkContent = delta.content;
184
175
  chunks.push(chunkContent);
185
- if (enableStreamingEditing) {
186
- await parser.processChunk(chunkContent);
187
- }
188
- else {
189
- fullContent += chunkContent;
190
- throttledUpdateAIMessageContent(fullContent);
191
- }
176
+ await parser.processChunk(chunkContent);
177
+ fullContent += chunkContent;
192
178
  }
193
179
  else if (chunk.usage) {
194
180
  // Handle usage information
@@ -199,20 +185,16 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
199
185
  }
200
186
  }
201
187
  await parser.flushRemaining();
202
- reportFileEdited();
203
- // Final cleanup - clear scratchpad and set final status
204
- updateAIScratchpad(shareDBDoc, chatId, '');
205
- updateAIStatus(shareDBDoc, chatId, 'Done editing.');
206
- throttledUpdateAIMessageContent(fullContent);
207
- // Flush to ensure the final content is written immediately
208
- throttledUpdateAIMessageContent.flush();
209
- // Finalize the AI message by clearing temporary fields
210
- finalizeAIMessage(shareDBDoc, chatId);
211
- // If streaming editing is not enabled, we need to
212
- // apply all the edits at once
213
- if (!enableStreamingEditing) {
214
- updateFiles(shareDBDoc, mergeFileChanges(shareDBDoc.data.files, parseMarkdownFiles(fullContent, 'bold').files));
188
+ // Emit any remaining text chunk
189
+ await emitTextChunk();
190
+ // Complete final file if any
191
+ if (currentEditingFileName) {
192
+ await completeFileEditing(currentEditingFileName);
215
193
  }
194
+ // Apply all the edits at once
195
+ updateFiles(shareDBDoc, mergeFileChanges(shareDBDoc.data.files, parseMarkdownFiles(fullContent, 'bold').files));
196
+ // Finalize streaming message
197
+ await finalizeStreamingMessage(shareDBDoc, chatId);
216
198
  // Generate a new runId to trigger a run when AI finishes editing
217
199
  // This will trigger a re-run without hot reloading
218
200
  const newRunId = generateRunId();
@@ -230,11 +212,16 @@ enableReasoningTokens = false, model, aiRequestOptions, }) => {
230
212
  }
231
213
  });
232
214
  // Write chunks file for debugging
233
- if (DEBUG) {
234
- const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
235
- fs.writeFileSync(chunksFileJSONpath, JSON.stringify(chunks, null, 2));
236
- console.log(`AI chunks written to ${chunksFileJSONpath}`);
237
- }
215
+ // if (DEBUG) {
216
+ // const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
217
+ // fs.writeFileSync(
218
+ // chunksFileJSONpath,
219
+ // JSON.stringify(chunks, null, 2),
220
+ // );
221
+ // console.log(
222
+ // `AI chunks written to ${chunksFileJSONpath}`,
223
+ // );
224
+ // }
238
225
  return {
239
226
  content: fullContent,
240
227
  generationId: generationId,