vzcode 2.18.0 → 2.22.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.
- package/README.md +359 -0
- package/dist/assets/index-CfDs-dAu.js +476 -0
- package/dist/assets/{index-BvGPtSrr.css → index-fSroLVgi.css} +1 -1
- package/dist/assets/{worker-y5jhqCTR.js → worker-Cm3I3tnR.js} +57 -57
- package/dist/assets/{worker-ClF0pBYr.js → worker-KiuLM-X4.js} +71 -71
- package/dist/index.html +2 -2
- package/dist/llm-streaming-server/aiEditing.js +58 -0
- package/dist/llm-streaming-server/chatOperations.js +494 -0
- package/dist/llm-streaming-server/errorHandling.js +49 -0
- package/dist/llm-streaming-server/index.js +20 -0
- package/dist/llm-streaming-server/llmStreaming.js +265 -0
- package/dist/llm-streaming-server/validation.js +19 -0
- package/dist/server/aiChatHandler/chatOperations.js +3 -4
- package/dist/server/aiChatHandler/index.js +5 -5
- package/dist/server/aiChatHandler/llmStreaming.js +57 -4
- package/dist/server/prettier.js +18 -16
- package/dist/utils/fileDiff.js +25 -1
- package/package.json +45 -45
- package/src/client/CodeEditor/getOrCreateEditor.tsx +272 -258
- package/src/client/CodeEditor/index.tsx +3 -3
- package/src/client/VZRight.tsx +4 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -1
- package/src/client/VZSidebar/AIChat/DiffView.scss +37 -1
- package/src/client/VZSidebar/AIChat/DiffView.tsx +55 -16
- package/src/client/VZSidebar/AIChat/styles.scss +38 -0
- package/src/client/VZSidebar/FileTypeIcon.tsx +1 -0
- package/src/client/VZSidebar/index.tsx +1 -1
- package/src/client/featureFlags.ts +7 -0
- package/src/llm-streaming-server/README.md +71 -0
- package/src/llm-streaming-server/aiEditing.ts +102 -0
- package/src/llm-streaming-server/chatOperations.ts +655 -0
- package/src/llm-streaming-server/errorHandling.ts +66 -0
- package/src/llm-streaming-server/index.ts +51 -0
- package/src/llm-streaming-server/llmStreaming.ts +403 -0
- package/src/llm-streaming-server/validation.ts +24 -0
- package/src/llm-streaming-ui/README.md +103 -0
- package/src/llm-streaming-ui/components/ChatInput.tsx +237 -0
- package/src/llm-streaming-ui/components/DiffView.scss +173 -0
- package/src/llm-streaming-ui/components/DiffView.tsx +236 -0
- package/src/llm-streaming-ui/components/FileEditingIndicator.tsx +89 -0
- package/src/llm-streaming-ui/components/IndividualFileDiff.tsx +86 -0
- package/src/llm-streaming-ui/components/JumpToLatestButton.tsx +64 -0
- package/src/llm-streaming-ui/components/Message.tsx +145 -0
- package/src/llm-streaming-ui/components/MessageList.tsx +241 -0
- package/src/llm-streaming-ui/components/ThinkingScratchpad.tsx +42 -0
- package/src/llm-streaming-ui/components/TypingIndicator.tsx +19 -0
- package/src/llm-streaming-ui/components/index.tsx +388 -0
- package/src/llm-streaming-ui/components/styles.scss +831 -0
- package/src/llm-streaming-ui/components/useSpeechRecognition.ts +116 -0
- package/src/llm-streaming-ui/index.ts +34 -0
- package/src/server/aiChatHandler/chatOperations.ts +3 -4
- package/src/server/aiChatHandler/index.ts +5 -5
- package/src/server/aiChatHandler/llmStreaming.ts +87 -4
- package/src/server/prettier.ts +30 -31
- package/src/types.ts +5 -0
- package/src/utils/fileDiff.ts +36 -2
- package/dist/assets/index-DuDXdJWC.js +0 -477
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
Dispatch,
|
|
6
|
+
SetStateAction,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
export interface UseSpeechRecognitionResult {
|
|
10
|
+
isSpeaking: boolean;
|
|
11
|
+
toggleSpeechRecognition: () => void;
|
|
12
|
+
stopSpeaking: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useSpeechRecognition = (
|
|
16
|
+
onTranscriptChange: Dispatch<SetStateAction<string>>,
|
|
17
|
+
): UseSpeechRecognitionResult => {
|
|
18
|
+
const [isSpeaking, setIsSpeaking] =
|
|
19
|
+
useState<boolean>(false);
|
|
20
|
+
const [recognition, setRecognition] =
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
useState<SpeechRecognition | null>(null);
|
|
23
|
+
const [, setFinalTranscript] = useState<string>('');
|
|
24
|
+
|
|
25
|
+
// Speech recognition setup
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// Check if browser supports SpeechRecognition
|
|
28
|
+
const SpeechRecognition =
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
window.SpeechRecognition ||
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
window.webkitSpeechRecognition;
|
|
33
|
+
|
|
34
|
+
if (SpeechRecognition) {
|
|
35
|
+
const recognitionInstance = new SpeechRecognition();
|
|
36
|
+
recognitionInstance.continuous = true;
|
|
37
|
+
recognitionInstance.interimResults = true;
|
|
38
|
+
|
|
39
|
+
recognitionInstance.onresult = (event) => {
|
|
40
|
+
let finalText = '';
|
|
41
|
+
let interimText = '';
|
|
42
|
+
|
|
43
|
+
// Process all results to separate final and interim text
|
|
44
|
+
for (let i = 0; i < event.results.length; i++) {
|
|
45
|
+
const result = event.results[i];
|
|
46
|
+
if (result.isFinal) {
|
|
47
|
+
finalText += result[0].transcript;
|
|
48
|
+
} else {
|
|
49
|
+
interimText += result[0].transcript;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Update our final transcript state
|
|
54
|
+
setFinalTranscript(finalText);
|
|
55
|
+
|
|
56
|
+
// Combine final and interim text and update the prompt
|
|
57
|
+
const fullTranscript = finalText + interimText;
|
|
58
|
+
onTranscriptChange(fullTranscript);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
recognitionInstance.onerror = (event) => {
|
|
62
|
+
console.error(
|
|
63
|
+
'Speech recognition error',
|
|
64
|
+
event.error,
|
|
65
|
+
);
|
|
66
|
+
setIsSpeaking(false);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
recognitionInstance.onend = () => {
|
|
70
|
+
setIsSpeaking(false);
|
|
71
|
+
// Reset final transcript when recognition ends
|
|
72
|
+
setFinalTranscript('');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
setRecognition(recognitionInstance);
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
recognitionInstance.abort();
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}, [onTranscriptChange]);
|
|
82
|
+
|
|
83
|
+
const toggleSpeechRecognition = useCallback(() => {
|
|
84
|
+
if (!recognition) {
|
|
85
|
+
console.error(
|
|
86
|
+
'Speech recognition not supported in this browser',
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isSpeaking) {
|
|
92
|
+
recognition.stop();
|
|
93
|
+
setIsSpeaking(false);
|
|
94
|
+
} else {
|
|
95
|
+
// Reset final transcript when starting new recognition
|
|
96
|
+
setFinalTranscript('');
|
|
97
|
+
recognition.start();
|
|
98
|
+
setIsSpeaking(true);
|
|
99
|
+
}
|
|
100
|
+
}, [isSpeaking, recognition]);
|
|
101
|
+
|
|
102
|
+
const stopSpeaking = useCallback(() => {
|
|
103
|
+
if (recognition && isSpeaking) {
|
|
104
|
+
recognition.stop();
|
|
105
|
+
setIsSpeaking(false);
|
|
106
|
+
// Reset final transcript when manually stopping
|
|
107
|
+
setFinalTranscript('');
|
|
108
|
+
}
|
|
109
|
+
}, [isSpeaking, recognition]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
isSpeaking,
|
|
113
|
+
toggleSpeechRecognition,
|
|
114
|
+
stopSpeaking,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vizhub/llm-streaming-ui
|
|
3
|
+
*
|
|
4
|
+
* Client-side React UI components for displaying streaming AI edits and chat.
|
|
5
|
+
* This module provides functionality for:
|
|
6
|
+
* - Displaying AI chat messages with streaming support
|
|
7
|
+
* - Showing real-time diffs and code changes
|
|
8
|
+
* - Rendering thinking/reasoning scratchpads
|
|
9
|
+
* - Managing file editing indicators
|
|
10
|
+
* - Voice input support
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Main AI Chat component
|
|
14
|
+
export { AIChat } from './components/index.js';
|
|
15
|
+
|
|
16
|
+
// Message display components
|
|
17
|
+
export { MessageList } from './components/MessageList.js';
|
|
18
|
+
export { Message } from './components/Message.js';
|
|
19
|
+
|
|
20
|
+
// Diff visualization components
|
|
21
|
+
export { DiffView } from './components/DiffView.js';
|
|
22
|
+
export { IndividualFileDiff } from './components/IndividualFileDiff.js';
|
|
23
|
+
|
|
24
|
+
// Status and indicator components
|
|
25
|
+
export { TypingIndicator } from './components/TypingIndicator.js';
|
|
26
|
+
export { ThinkingScratchpad } from './components/ThinkingScratchpad.js';
|
|
27
|
+
export { AIEditingStatusIndicator as FileEditingIndicator } from './components/FileEditingIndicator';
|
|
28
|
+
export { JumpToLatestButton } from './components/JumpToLatestButton.js';
|
|
29
|
+
|
|
30
|
+
// Input components
|
|
31
|
+
export { ChatInput } from './components/ChatInput.js';
|
|
32
|
+
|
|
33
|
+
// Custom hooks
|
|
34
|
+
export { useSpeechRecognition } from './components/useSpeechRecognition.js';
|
|
@@ -51,6 +51,7 @@ export const ensureChatExists = (
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Adds a user message to the chat
|
|
54
|
+
* Clears old messages to reflect that each prompt is a self-contained code transformation
|
|
54
55
|
*/
|
|
55
56
|
export const addUserMessage = (
|
|
56
57
|
shareDBDoc: ShareDBDoc<VizContent>,
|
|
@@ -70,10 +71,7 @@ export const addUserMessage = (
|
|
|
70
71
|
...shareDBDoc.data.chats,
|
|
71
72
|
[chatId]: {
|
|
72
73
|
...shareDBDoc.data.chats[chatId],
|
|
73
|
-
messages: [
|
|
74
|
-
...shareDBDoc.data.chats[chatId].messages,
|
|
75
|
-
userMessage,
|
|
76
|
-
],
|
|
74
|
+
messages: [userMessage], // Replace old messages with just the new user message
|
|
77
75
|
updatedAt: dateToTimestamp(new Date()),
|
|
78
76
|
},
|
|
79
77
|
},
|
|
@@ -379,6 +377,7 @@ export const updateFiles = (
|
|
|
379
377
|
DEBUG && console.log('updateFiles op:');
|
|
380
378
|
DEBUG && console.log(JSON.stringify(filesOp, null, 2));
|
|
381
379
|
shareDBDoc.submitOp(filesOp);
|
|
380
|
+
return filesOp;
|
|
382
381
|
};
|
|
383
382
|
|
|
384
383
|
/**
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { validateRequest } from '
|
|
1
|
+
import { validateRequest } from '../../llm-streaming-server/validation.js';
|
|
2
2
|
import {
|
|
3
3
|
ensureChatsExist,
|
|
4
4
|
ensureChatExists,
|
|
5
5
|
addUserMessage,
|
|
6
6
|
setAIStatus,
|
|
7
|
-
} from '
|
|
8
|
-
import { createLLMFunction } from '
|
|
9
|
-
import { performAIEditing } from '
|
|
7
|
+
} from '../../llm-streaming-server/chatOperations.js';
|
|
8
|
+
import { createLLMFunction } from '../../llm-streaming-server/llmStreaming.js';
|
|
9
|
+
import { performAIEditing } from '../../llm-streaming-server/aiEditing.js';
|
|
10
10
|
import {
|
|
11
11
|
handleError,
|
|
12
12
|
handleBackgroundError,
|
|
13
|
-
} from '
|
|
13
|
+
} from '../../llm-streaming-server/errorHandling.js';
|
|
14
14
|
import { createRunCodeFunction } from '../../runCode.js';
|
|
15
15
|
import { ShareDBDoc } from '../../types.js';
|
|
16
16
|
import { VizContent } from '@vizhub/viz-types';
|
|
@@ -7,6 +7,7 @@ import { mergeFileChanges } from 'editcodewithai';
|
|
|
7
7
|
import {
|
|
8
8
|
FileCollection,
|
|
9
9
|
VizChatId,
|
|
10
|
+
VizFiles,
|
|
10
11
|
} from '@vizhub/viz-types';
|
|
11
12
|
import {
|
|
12
13
|
updateFiles,
|
|
@@ -22,11 +23,18 @@ import {
|
|
|
22
23
|
} from '../../types.js';
|
|
23
24
|
import { formatFiles } from '../prettier.js';
|
|
24
25
|
|
|
26
|
+
// Verbose logs
|
|
25
27
|
const DEBUG = false;
|
|
26
28
|
|
|
27
29
|
// Useful for testing/debugging the streaming behavior
|
|
28
30
|
const slowMode = false;
|
|
29
31
|
|
|
32
|
+
// If the `EMIT_FIXTURES` variable is true,
|
|
33
|
+
// then an output file in the `test/fixtures` folder
|
|
34
|
+
// with the before and after file states for testing purposes.
|
|
35
|
+
// This feeds into tests in codemirror-ot.
|
|
36
|
+
const EMIT_FIXTURES = false;
|
|
37
|
+
|
|
30
38
|
/**
|
|
31
39
|
* Creates and configures the LLM function for streaming with reasoning tokens
|
|
32
40
|
*/
|
|
@@ -109,7 +117,7 @@ export const createLLMFunction = ({
|
|
|
109
117
|
const completeFileEditing = async (
|
|
110
118
|
fileName: string,
|
|
111
119
|
) => {
|
|
112
|
-
if (fileName
|
|
120
|
+
if (fileName) {
|
|
113
121
|
DEBUG &&
|
|
114
122
|
console.log(
|
|
115
123
|
`LLMStreaming: Completing file editing for ${fileName}`,
|
|
@@ -185,6 +193,38 @@ export const createLLMFunction = ({
|
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
},
|
|
196
|
+
onFileDelete: async (fileName: string) => {
|
|
197
|
+
DEBUG &&
|
|
198
|
+
console.log(
|
|
199
|
+
`LLMStreaming: File marked for deletion: ${fileName}`,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Emit any accumulated text chunk first
|
|
203
|
+
await emitTextChunk();
|
|
204
|
+
|
|
205
|
+
// Complete previous file if any
|
|
206
|
+
if (currentEditingFileName) {
|
|
207
|
+
await completeFileEditing(currentEditingFileName);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reset current editing state
|
|
211
|
+
currentEditingFileName = null;
|
|
212
|
+
currentFileContent = '';
|
|
213
|
+
|
|
214
|
+
// Emit file delete event
|
|
215
|
+
await addStreamingEvent(shareDBDoc, chatId, {
|
|
216
|
+
type: 'file_delete',
|
|
217
|
+
fileName,
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Update status
|
|
222
|
+
updateStreamingStatus(
|
|
223
|
+
shareDBDoc,
|
|
224
|
+
chatId,
|
|
225
|
+
`Deleting ${fileName}...`,
|
|
226
|
+
);
|
|
227
|
+
},
|
|
188
228
|
};
|
|
189
229
|
|
|
190
230
|
const parser = new StreamingMarkdownParser(callbacks);
|
|
@@ -297,17 +337,60 @@ export const createLLMFunction = ({
|
|
|
297
337
|
const newFilesUnformatted: FileCollection =
|
|
298
338
|
parseMarkdownFiles(fullContent, 'bold').files;
|
|
299
339
|
|
|
300
|
-
// Run Prettier on `newFiles` before applying them
|
|
340
|
+
// Run Prettier on `newFiles` before applying them,
|
|
341
|
+
// preserving empty files as empty
|
|
342
|
+
// since that is the cue to delete a file.
|
|
301
343
|
const newFilesFormatted = await formatFiles(
|
|
302
344
|
newFilesUnformatted,
|
|
303
345
|
);
|
|
304
346
|
|
|
347
|
+
// Capture the current state of files before applying changes
|
|
348
|
+
let vizFilesBefore: VizFiles;
|
|
349
|
+
if (EMIT_FIXTURES) {
|
|
350
|
+
vizFilesBefore = JSON.parse(
|
|
351
|
+
JSON.stringify(shareDBDoc.data.files),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
305
355
|
// Apply all the edits at once
|
|
306
|
-
const
|
|
356
|
+
const vizFilesAfter: VizFiles = mergeFileChanges(
|
|
307
357
|
shareDBDoc.data.files,
|
|
308
358
|
newFilesFormatted,
|
|
309
359
|
);
|
|
310
|
-
|
|
360
|
+
|
|
361
|
+
const filesOp = updateFiles(shareDBDoc, vizFilesAfter);
|
|
362
|
+
|
|
363
|
+
if (EMIT_FIXTURES) {
|
|
364
|
+
const fs = await import('fs');
|
|
365
|
+
const path = await import('path');
|
|
366
|
+
const testCasesDir = path.resolve(
|
|
367
|
+
process.cwd(),
|
|
368
|
+
'../',
|
|
369
|
+
'fixtures',
|
|
370
|
+
);
|
|
371
|
+
if (!fs.existsSync(testCasesDir)) {
|
|
372
|
+
fs.mkdirSync(testCasesDir, { recursive: true });
|
|
373
|
+
}
|
|
374
|
+
const timestamp = new Date()
|
|
375
|
+
.toISOString()
|
|
376
|
+
.replace(/[:.]/g, '-');
|
|
377
|
+
const testCasePath = path.join(
|
|
378
|
+
testCasesDir,
|
|
379
|
+
`ai-chat-${timestamp}.json`,
|
|
380
|
+
);
|
|
381
|
+
const testCaseData = {
|
|
382
|
+
vizFilesBefore,
|
|
383
|
+
vizFilesAfter,
|
|
384
|
+
filesOp,
|
|
385
|
+
};
|
|
386
|
+
fs.writeFileSync(
|
|
387
|
+
testCasePath,
|
|
388
|
+
JSON.stringify(testCaseData, null, 2),
|
|
389
|
+
);
|
|
390
|
+
console.log(
|
|
391
|
+
`AI chat test case written to ${testCasePath}`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
311
394
|
|
|
312
395
|
// Finalize streaming message
|
|
313
396
|
finalizeStreamingMessage(shareDBDoc, chatId);
|
package/src/server/prettier.ts
CHANGED
|
@@ -5,10 +5,7 @@ import * as prettierPluginHtml from 'prettier/plugins/html';
|
|
|
5
5
|
import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
|
|
6
6
|
import * as prettierPluginCSS from 'prettier/plugins/postcss';
|
|
7
7
|
import * as prettierPluginTypescript from 'prettier/plugins/typescript';
|
|
8
|
-
import {
|
|
9
|
-
VizFileId,
|
|
10
|
-
FileCollection,
|
|
11
|
-
} from '@vizhub/viz-types';
|
|
8
|
+
import { FileCollection } from '@vizhub/viz-types';
|
|
12
9
|
|
|
13
10
|
// Parser mappings - matches client-side implementation
|
|
14
11
|
const parsers = {
|
|
@@ -85,43 +82,45 @@ export const formatFile = async (
|
|
|
85
82
|
}
|
|
86
83
|
};
|
|
87
84
|
|
|
85
|
+
// Keys are file names, values are file text contents
|
|
86
|
+
//export type FileCollection = Record<string, string>;
|
|
87
|
+
|
|
88
88
|
/**
|
|
89
89
|
* Formats multiple files using Prettier
|
|
90
90
|
* Only formats files that have supported extensions
|
|
91
91
|
* Returns a map of fileId -> formatted text for successfully formatted files
|
|
92
92
|
*/
|
|
93
93
|
export const formatFiles = async (
|
|
94
|
-
fileCollection:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const results: { [fileId: VizFileId]: string } = {};
|
|
99
|
-
const targetFileIds = Object.keys(fileCollection);
|
|
94
|
+
fileCollection: FileCollection,
|
|
95
|
+
): Promise<FileCollection> => {
|
|
96
|
+
const results: FileCollection = {};
|
|
97
|
+
const targetFileNames = Object.keys(fileCollection);
|
|
100
98
|
|
|
101
99
|
// Process files in parallel for better performance
|
|
102
|
-
const formatPromises =
|
|
103
|
-
async (
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Handle both FileCollection format and test format
|
|
108
|
-
const fileName =
|
|
109
|
-
typeof fileData === 'string'
|
|
110
|
-
? fileId
|
|
111
|
-
: fileData.name;
|
|
112
|
-
const fileText =
|
|
113
|
-
typeof fileData === 'string'
|
|
114
|
-
? fileData
|
|
115
|
-
: fileData.text;
|
|
100
|
+
const formatPromises = targetFileNames.map(
|
|
101
|
+
async (fileName: string) => {
|
|
102
|
+
const fileText = fileCollection[fileName];
|
|
103
|
+
results[fileName] = fileText;
|
|
116
104
|
|
|
117
|
-
|
|
105
|
+
// Preserve empty files as empty,
|
|
106
|
+
// since this is the cue to delete a file.
|
|
107
|
+
if (!fileText || fileText.trim() === '') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
111
|
+
try {
|
|
112
|
+
const formatted = await formatFile(
|
|
113
|
+
fileText,
|
|
114
|
+
fileName,
|
|
115
|
+
);
|
|
116
|
+
if (formatted !== null && formatted !== fileText) {
|
|
117
|
+
results[fileName] = formatted;
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(
|
|
121
|
+
`Error formatting ${fileName}:`,
|
|
122
|
+
error,
|
|
123
|
+
);
|
|
125
124
|
}
|
|
126
125
|
},
|
|
127
126
|
);
|
package/src/types.ts
CHANGED
package/src/utils/fileDiff.ts
CHANGED
|
@@ -2,7 +2,33 @@ import { createTwoFilesPatch } from 'diff';
|
|
|
2
2
|
import { VizFiles, VizFileId } from '@vizhub/viz-types';
|
|
3
3
|
|
|
4
4
|
export interface UnifiedFilesDiff {
|
|
5
|
-
[fileId: VizFileId]: string; // Unified diff string
|
|
5
|
+
[fileId: VizFileId]: string; // Unified diff string or deletion marker
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Special marker to indicate a file deletion
|
|
9
|
+
export const FILE_DELETION_MARKER = '__FILE_DELETED__';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a diff string represents a file deletion
|
|
13
|
+
*/
|
|
14
|
+
export function isFileDeletion(
|
|
15
|
+
diffString: string,
|
|
16
|
+
): boolean {
|
|
17
|
+
return diffString.startsWith(FILE_DELETION_MARKER);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract file name from a deletion marker
|
|
22
|
+
*/
|
|
23
|
+
export function getDeletedFileName(
|
|
24
|
+
diffString: string,
|
|
25
|
+
): string {
|
|
26
|
+
if (!isFileDeletion(diffString)) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
return diffString.substring(
|
|
30
|
+
FILE_DELETION_MARKER.length + 1,
|
|
31
|
+
);
|
|
6
32
|
}
|
|
7
33
|
|
|
8
34
|
/**
|
|
@@ -19,6 +45,11 @@ export function generateFileUnifiedDiff(
|
|
|
19
45
|
return '';
|
|
20
46
|
}
|
|
21
47
|
|
|
48
|
+
// Handle file deletion case - when file existed before but is now empty/deleted
|
|
49
|
+
if (beforeContent && !afterContent) {
|
|
50
|
+
return `${FILE_DELETION_MARKER}:${fileName}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
// Use the diff library's createTwoFilesPatch function to generate unified diff
|
|
23
54
|
const unifiedDiff = createTwoFilesPatch(
|
|
24
55
|
fileName,
|
|
@@ -109,9 +140,12 @@ export function parseUnifiedDiffStats(
|
|
|
109
140
|
|
|
110
141
|
/**
|
|
111
142
|
* Combine multiple unified diffs into a single diff string
|
|
143
|
+
* Note: File deletion markers are excluded from combination
|
|
112
144
|
*/
|
|
113
145
|
export function combineUnifiedDiffs(
|
|
114
146
|
unifiedDiffs: UnifiedFilesDiff,
|
|
115
147
|
): string {
|
|
116
|
-
return Object.values(unifiedDiffs)
|
|
148
|
+
return Object.values(unifiedDiffs)
|
|
149
|
+
.filter((diff) => !isFileDeletion(diff))
|
|
150
|
+
.join('\n');
|
|
117
151
|
}
|