vzcode 1.44.0 → 1.45.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.
@@ -3,6 +3,10 @@ import {
3
3
  prepareFilesForPrompt,
4
4
  } from 'editcodewithai';
5
5
  import { formatMarkdownFiles } from 'llm-code-format';
6
+ import {
7
+ createFilesSnapshot,
8
+ generateFilesUnifiedDiff,
9
+ } from '../../utils/fileDiff.js';
6
10
 
7
11
  // Dev flag for waiting 1 second before starting the LLM function.
8
12
  // Useful for debugging and testing purposes, e.g. checking the typing indicator.
@@ -17,6 +21,11 @@ export const performAIEditing = async ({
17
21
  llmFunction,
18
22
  runCode,
19
23
  }) => {
24
+ // 1. Capture the current state of files before editing
25
+ const beforeFiles = createFilesSnapshot(
26
+ shareDBDoc.data.files,
27
+ );
28
+
20
29
  const preparedFiles = prepareFilesForPrompt(
21
30
  shareDBDoc.data.files,
22
31
  );
@@ -40,8 +49,18 @@ export const performAIEditing = async ({
40
49
 
41
50
  runCode();
42
51
 
52
+ // 3. Capture the state of files after editing and generate diff
53
+ const afterFiles = createFilesSnapshot(
54
+ shareDBDoc.data.files,
55
+ );
56
+ const diffData = generateFilesUnifiedDiff(
57
+ beforeFiles,
58
+ afterFiles,
59
+ );
60
+
43
61
  return {
44
62
  content: result.content,
45
63
  generationId: result.generationId,
64
+ diffData,
46
65
  };
47
66
  };
@@ -246,6 +246,46 @@ export const finalizeAIMessage = (
246
246
  shareDBDoc.submitOp(op);
247
247
  };
248
248
 
249
+ /**
250
+ * Adds diff data to the most recent AI message
251
+ */
252
+ export const addDiffToAIMessage = (
253
+ shareDBDoc: ShareDBDoc<VizContent>,
254
+ chatId: VizChatId,
255
+ diffData: any,
256
+ ) => {
257
+ const chat = shareDBDoc.data.chats[chatId];
258
+ const messages = [...chat.messages];
259
+
260
+ // Find the most recent AI message
261
+ const lastAIMessageIndex = messages.length - 1;
262
+ if (
263
+ lastAIMessageIndex >= 0 &&
264
+ messages[lastAIMessageIndex].role === 'assistant'
265
+ ) {
266
+ const newMessage = {
267
+ ...messages[lastAIMessageIndex],
268
+ diffData,
269
+ };
270
+ // Use type assertion to extend the message with diffData
271
+ (messages[lastAIMessageIndex] as any) = newMessage;
272
+
273
+ const messageOp = diff(shareDBDoc.data, {
274
+ ...shareDBDoc.data,
275
+ chats: {
276
+ ...shareDBDoc.data.chats,
277
+ [chatId]: {
278
+ ...chat,
279
+ messages,
280
+ updatedAt: dateToTimestamp(new Date()),
281
+ },
282
+ },
283
+ });
284
+
285
+ shareDBDoc.submitOp(messageOp);
286
+ }
287
+ };
288
+
249
289
  /**
250
290
  * Adds an AI response message to the chat (legacy function, kept for compatibility)
251
291
  */
@@ -3,6 +3,7 @@ import {
3
3
  ensureChatsExist,
4
4
  ensureChatExists,
5
5
  addUserMessage,
6
+ addDiffToAIMessage,
6
7
  } from './chatOperations.js';
7
8
  import { createLLMFunction } from './llmStreaming.js';
8
9
  import { performAIEditing } from './aiEditing.js';
@@ -72,6 +73,18 @@ export const handleAIChatMessage =
72
73
  runCode,
73
74
  });
74
75
 
76
+ // Add diff data to the AI message if there are changes
77
+ if (
78
+ editResult.diffData &&
79
+ Object.keys(editResult.diffData).length > 0
80
+ ) {
81
+ addDiffToAIMessage(
82
+ shareDBDoc,
83
+ chatId,
84
+ editResult.diffData,
85
+ );
86
+ }
87
+
75
88
  // Handle credit deduction if callback is provided
76
89
  if (
77
90
  onCreditDeduction &&
@@ -10,6 +10,15 @@ import createIgnore from 'ignore';
10
10
  import { ignoreFilePattern, baseIgnore } from './config.js';
11
11
  import { isDirectory } from './isDirectory.js';
12
12
 
13
+ // Import the image file utility
14
+ const isImageFile = (fileName) => {
15
+ return (
16
+ fileName.match(
17
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
18
+ ) !== null
19
+ );
20
+ };
21
+
13
22
  /**
14
23
  * @param {string} fullPath - absolut path of the workspace root
15
24
  * @param {string} currentDirectoryPath - path where the ignore file is found, relative to fullPath
@@ -180,13 +189,23 @@ export const computeInitialDocument = ({ fullPath }) => {
180
189
 
181
190
  files.forEach((file) => {
182
191
  const id = randomId();
192
+ let text = null;
193
+
194
+ if (!isDirectory(file)) {
195
+ const filePath = path.join(fullPath, file);
196
+
197
+ if (isImageFile(file)) {
198
+ // Read image files as binary and convert to base64
199
+ const buffer = fs.readFileSync(filePath);
200
+ text = buffer.toString('base64');
201
+ } else {
202
+ // Read non-image files as UTF-8 text
203
+ text = fs.readFileSync(filePath, 'utf-8');
204
+ }
205
+ }
206
+
183
207
  initialDocument.files[id] = {
184
- text: isDirectory(file)
185
- ? null
186
- : fs.readFileSync(
187
- path.join(fullPath, file),
188
- 'utf-8',
189
- ),
208
+ text,
190
209
  name: file,
191
210
  };
192
211
  });
@@ -18,6 +18,15 @@ import { isDirectory } from './isDirectory.js';
18
18
  import { createToken } from './livekit.js';
19
19
  import './setupEnv.js';
20
20
 
21
+ // Import the image file utility
22
+ const isImageFile = (fileName) => {
23
+ return (
24
+ fileName.match(
25
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
26
+ ) !== null
27
+ );
28
+ };
29
+
21
30
  // The time in milliseconds by which auto-saving is debounced.
22
31
  const autoSaveDebounceTimeMS = 800;
23
32
 
@@ -161,7 +170,17 @@ const save = () => {
161
170
  if (previous && current) {
162
171
  // Handle changing of text content.
163
172
  if (previous.text !== current.text) {
164
- fs.writeFileSync(current.name, current.text);
173
+ if (isImageFile(current.name)) {
174
+ // Write image files as binary from base64
175
+ const buffer = Buffer.from(
176
+ current.text,
177
+ 'base64',
178
+ );
179
+ fs.writeFileSync(current.name, buffer);
180
+ } else {
181
+ // Write non-image files as text
182
+ fs.writeFileSync(current.name, current.text);
183
+ }
165
184
  }
166
185
 
167
186
  // // Handle renaming files.
@@ -256,7 +275,17 @@ const save = () => {
256
275
  if (!previous && current) {
257
276
  //File Creation
258
277
  if (!isDirectory(current.name)) {
259
- fs.writeFileSync(current.name, current.text);
278
+ if (isImageFile(current.name)) {
279
+ // Write image files as binary from base64
280
+ const buffer = Buffer.from(
281
+ current.text,
282
+ 'base64',
283
+ );
284
+ fs.writeFileSync(current.name, buffer);
285
+ } else {
286
+ // Write non-image files as text
287
+ fs.writeFileSync(current.name, current.text);
288
+ }
260
289
  } else {
261
290
  fs.mkdirSync(current.name, { recursive: true });
262
291
  }
@@ -0,0 +1,114 @@
1
+ import { createTwoFilesPatch } from 'diff';
2
+ import { VizFiles, VizFileId } from '@vizhub/viz-types';
3
+
4
+ export interface UnifiedFilesDiff {
5
+ [fileId: VizFileId]: string; // Unified diff string
6
+ }
7
+
8
+ /**
9
+ * Generate a unified diff for a single file using the diff library
10
+ */
11
+ export function generateFileUnifiedDiff(
12
+ fileId: VizFileId,
13
+ fileName: string,
14
+ beforeContent: string,
15
+ afterContent: string,
16
+ ): string {
17
+ // Only generate diff if there are actual changes
18
+ if (beforeContent === afterContent) {
19
+ return '';
20
+ }
21
+
22
+ // Use the diff library's createTwoFilesPatch function to generate unified diff
23
+ const unifiedDiff = createTwoFilesPatch(
24
+ fileName,
25
+ fileName,
26
+ beforeContent,
27
+ afterContent,
28
+ );
29
+
30
+ return unifiedDiff;
31
+ }
32
+
33
+ /**
34
+ * Generate unified diffs for multiple files
35
+ */
36
+ export function generateFilesUnifiedDiff(
37
+ beforeFiles: VizFiles,
38
+ afterFiles: VizFiles,
39
+ ): UnifiedFilesDiff {
40
+ const result: UnifiedFilesDiff = {};
41
+ const allFileIds = new Set([
42
+ ...Object.keys(beforeFiles),
43
+ ...Object.keys(afterFiles),
44
+ ]);
45
+
46
+ for (const fileId of allFileIds) {
47
+ const beforeFile = beforeFiles[fileId];
48
+ const afterFile = afterFiles[fileId];
49
+
50
+ const beforeContent = beforeFile?.text || '';
51
+ const afterContent = afterFile?.text || '';
52
+ const fileName =
53
+ afterFile?.name || beforeFile?.name || fileId;
54
+
55
+ const unifiedDiff = generateFileUnifiedDiff(
56
+ fileId,
57
+ fileName,
58
+ beforeContent,
59
+ afterContent,
60
+ );
61
+
62
+ // Only include files that have changes
63
+ if (unifiedDiff) {
64
+ result[fileId] = unifiedDiff;
65
+ }
66
+ }
67
+
68
+ return result;
69
+ }
70
+
71
+ /**
72
+ * Create a snapshot of current files for diff comparison
73
+ */
74
+ export function createFilesSnapshot(
75
+ files: VizFiles,
76
+ ): VizFiles {
77
+ return JSON.parse(JSON.stringify(files));
78
+ }
79
+
80
+ /**
81
+ * Parse unified diff to extract basic statistics
82
+ */
83
+ export function parseUnifiedDiffStats(
84
+ unifiedDiff: string,
85
+ ): {
86
+ additions: number;
87
+ deletions: number;
88
+ } {
89
+ const lines = unifiedDiff.split('\n');
90
+ let additions = 0;
91
+ let deletions = 0;
92
+
93
+ for (const line of lines) {
94
+ if (line.startsWith('+') && !line.startsWith('+++')) {
95
+ additions++;
96
+ } else if (
97
+ line.startsWith('-') &&
98
+ !line.startsWith('---')
99
+ ) {
100
+ deletions++;
101
+ }
102
+ }
103
+
104
+ return { additions, deletions };
105
+ }
106
+
107
+ /**
108
+ * Combine multiple unified diffs into a single diff string
109
+ */
110
+ export function combineUnifiedDiffs(
111
+ unifiedDiffs: UnifiedFilesDiff,
112
+ ): string {
113
+ return Object.values(unifiedDiffs).join('\n');
114
+ }