vzcode 1.45.0 → 1.46.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-CC0CUUDi.js} +113 -113
  2. package/dist/assets/{index-DeeOcIzw.css → index-CQyHWTWE.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 +148 -12
  17. package/src/client/VZSidebar/AIChat/styles.scss +223 -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
@@ -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
  };
@@ -1,4 +1,7 @@
1
- import { dateToTimestamp } from '@vizhub/viz-utils';
1
+ import {
2
+ dateToTimestamp,
3
+ generateRunId,
4
+ } from '@vizhub/viz-utils';
2
5
  import { randomId } from '../../randomId.js';
3
6
  import { ShareDBDoc } from '../../types.js';
4
7
  import { diff } from '../../ot.js';
@@ -253,6 +256,7 @@ export const addDiffToAIMessage = (
253
256
  shareDBDoc: ShareDBDoc<VizContent>,
254
257
  chatId: VizChatId,
255
258
  diffData: any,
259
+ beforeFiles?: VizFiles, // Optional snapshot for undo functionality
256
260
  ) => {
257
261
  const chat = shareDBDoc.data.chats[chatId];
258
262
  const messages = [...chat.messages];
@@ -266,6 +270,7 @@ export const addDiffToAIMessage = (
266
270
  const newMessage = {
267
271
  ...messages[lastAIMessageIndex],
268
272
  diffData,
273
+ ...(beforeFiles && { beforeFiles }), // Add beforeFiles only if provided
269
274
  };
270
275
  // Use type assertion to extend the message with diffData
271
276
  (messages[lastAIMessageIndex] as any) = newMessage;
@@ -447,3 +452,35 @@ export const appendLineToFile = (
447
452
 
448
453
  shareDBDoc.submitOp(diff(shareDBDoc.data, newDocState));
449
454
  };
455
+
456
+ /**
457
+ * Undoes the last AI edit by restoring files and removing the AI message
458
+ */
459
+ export const undoAIEdit = (
460
+ shareDBDoc: ShareDBDoc<VizContent>,
461
+ chatId: VizChatId,
462
+ messageId: string,
463
+ beforeFiles: VizFiles,
464
+ ) => {
465
+ const chat = shareDBDoc.data.chats[chatId];
466
+ const messages = chat.messages.filter(
467
+ (msg) => msg.id !== messageId,
468
+ );
469
+
470
+ const op = diff(shareDBDoc.data, {
471
+ ...shareDBDoc.data,
472
+ files: beforeFiles,
473
+ chats: {
474
+ ...shareDBDoc.data.chats,
475
+ [chatId]: {
476
+ ...chat,
477
+ messages,
478
+ updatedAt: dateToTimestamp(new Date()),
479
+ },
480
+ },
481
+ // Trigger a re-run
482
+ runId: generateRunId(),
483
+ });
484
+
485
+ shareDBDoc.submitOp(op);
486
+ };
@@ -6,7 +6,10 @@ import {
6
6
  addDiffToAIMessage,
7
7
  } from './chatOperations.js';
8
8
  import { createLLMFunction } from './llmStreaming.js';
9
- import { performAIEditing } from './aiEditing.js';
9
+ import {
10
+ performAIEditing,
11
+ performAIChat,
12
+ } from './aiEditing.js';
10
13
  import { handleError } from './errorHandling.js';
11
14
  import { createRunCodeFunction } from '../../runCode.js';
12
15
  import { ShareDBDoc } from '../../types.js';
@@ -26,7 +29,7 @@ export const handleAIChatMessage =
26
29
  onCreditDeduction?: any;
27
30
  }) =>
28
31
  async (req: any, res: any) => {
29
- const { content, chatId } = req.body;
32
+ const { content, chatId, mode = 'edit' } = req.body;
30
33
 
31
34
  if (DEBUG) {
32
35
  console.log(
@@ -65,13 +68,20 @@ export const handleAIChatMessage =
65
68
  const runCode =
66
69
  createRunCodeFunction(submitOperation);
67
70
 
68
- // Perform AI editing
69
- const editResult = await performAIEditing({
70
- prompt: content,
71
- shareDBDoc,
72
- llmFunction,
73
- runCode,
74
- });
71
+ // Perform AI editing or chat based on mode
72
+ const editResult =
73
+ mode === 'ask'
74
+ ? await performAIChat({
75
+ prompt: content,
76
+ shareDBDoc,
77
+ llmFunction,
78
+ })
79
+ : await performAIEditing({
80
+ prompt: content,
81
+ shareDBDoc,
82
+ llmFunction,
83
+ runCode,
84
+ });
75
85
 
76
86
  // Add diff data to the AI message if there are changes
77
87
  if (
@@ -82,6 +92,7 @@ export const handleAIChatMessage =
82
92
  shareDBDoc,
83
93
  chatId,
84
94
  editResult.diffData,
95
+ (editResult as any).beforeFiles, // Pass the beforeFiles snapshot for undo (only available for edit mode)
85
96
  );
86
97
  }
87
98
 
@@ -3,7 +3,9 @@ import {
3
3
  StreamingMarkdownParser,
4
4
  } from 'llm-code-format';
5
5
  import { ChatOpenAI } from '@langchain/openai';
6
+ import OpenAI from 'openai';
6
7
  import fs from 'fs';
8
+ import { generateRunId } from '@vizhub/viz-utils';
7
9
  import {
8
10
  updateAIStatus,
9
11
  createAIMessage,
@@ -13,8 +15,12 @@ import {
13
15
  clearFileContent,
14
16
  appendLineToFile,
15
17
  updateFiles,
18
+ updateAIScratchpad,
16
19
  } from './chatOperations.js';
17
20
  import { mergeFileChanges } from 'editcodewithai';
21
+ import { diff } from '../../ot.js';
22
+ import { VizChatId, VizContent } from '@vizhub/viz-types';
23
+ import { ShareDBDoc } from '../../types.js';
18
24
 
19
25
  const DEBUG = false;
20
26
 
@@ -34,28 +40,30 @@ const DEBUG = false;
34
40
  const enableStreamingEditing = false;
35
41
 
36
42
  /**
37
- * Creates and configures the LLM function for streaming
43
+ * Creates and configures the LLM function for streaming with reasoning tokens
38
44
  */
39
45
  export const createLLMFunction = ({
40
46
  shareDBDoc,
41
47
  createVizBotLocalPresence,
42
48
  chatId,
49
+ }: {
50
+ shareDBDoc: ShareDBDoc<VizContent>;
51
+ createVizBotLocalPresence: () => any;
52
+ chatId: VizChatId;
43
53
  }) => {
44
54
  return async (fullPrompt: string) => {
45
55
  const localPresence = createVizBotLocalPresence();
46
- const chatModel = new ChatOpenAI({
47
- modelName:
48
- process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
49
- 'anthropic/claude-sonnet-4',
50
- configuration: {
51
- apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
52
- baseURL: process.env.VIZHUB_EDIT_WITH_AI_BASE_URL,
53
- defaultHeaders: {
54
- 'HTTP-Referer': 'https://vizhub.com',
55
- 'X-Title': 'VizHub',
56
- },
56
+
57
+ // Create OpenRouter client for reasoning token support
58
+ const openRouterClient = new OpenAI({
59
+ apiKey: process.env.VIZHUB_EDIT_WITH_AI_API_KEY,
60
+ baseURL:
61
+ process.env.VIZHUB_EDIT_WITH_AI_BASE_URL ||
62
+ 'https://openrouter.ai/api/v1',
63
+ defaultHeaders: {
64
+ 'HTTP-Referer': 'https://vizhub.com',
65
+ 'X-Title': 'VizHub',
57
66
  },
58
- streaming: true,
59
67
  });
60
68
 
61
69
  let fullContent = '';
@@ -177,12 +185,58 @@ export const createLLMFunction = ({
177
185
  const parser = new StreamingMarkdownParser(callbacks);
178
186
 
179
187
  const chunks = [];
188
+ let reasoningContent = '';
189
+
190
+ // Stream the response with reasoning tokens
191
+ const modelName =
192
+ process.env.VIZHUB_EDIT_WITH_AI_MODEL_NAME ||
193
+ 'anthropic/claude-3.5-sonnet';
194
+ const stream = await (
195
+ openRouterClient.chat.completions.create as any
196
+ )({
197
+ model: modelName,
198
+ messages: [{ role: 'user', content: fullPrompt }],
199
+ max_tokens: 8192,
200
+ reasoning: {
201
+ effort: 'medium',
202
+ exclude: false,
203
+ },
204
+ usage: { include: true },
205
+ stream: true,
206
+ });
207
+
208
+ let reasoningStarted = false;
209
+ let contentStarted = false;
180
210
 
181
- // Stream the response
182
- const stream = await chatModel.stream(fullPrompt);
183
211
  for await (const chunk of stream) {
184
- if (chunk && chunk.content) {
185
- const chunkContent = String(chunk.content);
212
+ const delta = chunk.choices[0]?.delta as any; // Type assertion for OpenRouter-specific reasoning fields
213
+
214
+ if (delta?.reasoning) {
215
+ // Handle reasoning tokens (thinking)
216
+ if (!reasoningStarted) {
217
+ reasoningStarted = true;
218
+ updateAIStatus(shareDBDoc, chatId, 'Thinking...');
219
+ }
220
+ reasoningContent += delta.reasoning;
221
+ updateAIScratchpad(
222
+ shareDBDoc,
223
+ chatId,
224
+ reasoningContent,
225
+ );
226
+ } else if (delta?.content) {
227
+ // Handle regular content tokens
228
+ if (reasoningStarted && !contentStarted) {
229
+ // Clear reasoning when content starts
230
+ contentStarted = true;
231
+ updateAIScratchpad(shareDBDoc, chatId, '');
232
+ updateAIStatus(
233
+ shareDBDoc,
234
+ chatId,
235
+ 'Generating response...',
236
+ );
237
+ }
238
+
239
+ const chunkContent = delta.content;
186
240
  chunks.push(chunkContent);
187
241
 
188
242
  if (enableStreamingEditing) {
@@ -196,14 +250,20 @@ export const createLLMFunction = ({
196
250
  fullContent,
197
251
  );
198
252
  }
253
+ } else if (chunk.usage) {
254
+ // Handle usage information
255
+ DEBUG && console.log('Usage:', chunk.usage);
199
256
  }
200
257
 
201
- if (!generationId && chunk.lc_kwargs?.id) {
202
- generationId = chunk.lc_kwargs.id;
258
+ if (!generationId && chunk.id) {
259
+ generationId = chunk.id;
203
260
  }
204
261
  }
205
262
  await parser.flushRemaining();
206
263
  reportFileEdited();
264
+
265
+ // Final cleanup - clear scratchpad and set final status
266
+ updateAIScratchpad(shareDBDoc, chatId, '');
207
267
  updateAIStatus(shareDBDoc, chatId, 'Done editing.');
208
268
  updateAIMessageContent(
209
269
  shareDBDoc,
@@ -242,6 +302,28 @@ export const createLLMFunction = ({
242
302
  );
243
303
  }
244
304
 
305
+ // Generate a new runId to trigger a run when AI finishes editing
306
+ // This will trigger a re-run without hot reloading
307
+ const newRunId = generateRunId();
308
+ const runIdOp = diff(shareDBDoc.data, {
309
+ ...shareDBDoc.data,
310
+ runId: newRunId,
311
+ });
312
+ shareDBDoc.submitOp(runIdOp, (error) => {
313
+ if (error) {
314
+ console.warn(
315
+ 'Error setting runId after AI editing:',
316
+ error,
317
+ );
318
+ } else {
319
+ DEBUG &&
320
+ console.log(
321
+ 'Set new runId after AI editing:',
322
+ newRunId,
323
+ );
324
+ }
325
+ });
326
+
245
327
  // Write chunks file for debugging
246
328
  if (DEBUG) {
247
329
  const chunksFileJSONpath = `./ai-chunks-${chatId}.json`;
@@ -150,6 +150,10 @@ export const generateAIResponse = async ({
150
150
  // model: 'gpt-3.5-turbo',
151
151
  model: 'gpt-4o',
152
152
  messages,
153
+ reasoning: {
154
+ effort: 'medium',
155
+ exclude: false,
156
+ } as any, // Type assertion for OpenRouter-specific reasoning parameter
153
157
  stream: true,
154
158
  });
155
159