vzcode 2.22.0 → 2.25.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 (44) hide show
  1. package/dist/assets/{buildWorker-Bi6wsfQk.js → buildWorker-DylWm2VX.js} +64 -72
  2. package/dist/assets/index-CkFweu5-.js +403 -0
  3. package/dist/assets/{index-fSroLVgi.css → index-Dyt9tpmu.css} +1 -5
  4. package/dist/assets/{worker-BSzbM0Uo.js → worker-VPLhQVba.js} +230 -230
  5. package/dist/assets/worker-b9bkZkXK.js +116 -0
  6. package/dist/assets/{worker-Cm3I3tnR.js → worker-k7HZ3t-V.js} +3 -20
  7. package/dist/index.html +2 -2
  8. package/dist/llm-streaming-server/chatOperations.js +19 -0
  9. package/dist/llm-streaming-server/llmStreaming.js +18 -13
  10. package/dist/server/aiChatHandler/index.js +4 -2
  11. package/package.json +22 -21
  12. package/src/llm-streaming-server/chatOperations.ts +27 -0
  13. package/src/llm-streaming-server/llmStreaming.ts +24 -14
  14. package/src/llm-streaming-ui/components/Message.tsx +8 -7
  15. package/src/llm-streaming-ui/components/MessageList.tsx +5 -0
  16. package/src/llm-streaming-ui/components/index.tsx +2 -2
  17. package/src/llm-streaming-ui/components/styles.scss +10 -0
  18. package/src/server/aiChatHandler/index.ts +6 -0
  19. package/src/types.ts +1 -0
  20. package/dist/assets/index-CfDs-dAu.js +0 -476
  21. package/dist/assets/worker-KiuLM-X4.js +0 -136
  22. package/dist/server/aiChatHandler/aiEditing.js +0 -58
  23. package/dist/server/aiChatHandler/chatOperations.js +0 -494
  24. package/dist/server/aiChatHandler/errorHandling.js +0 -49
  25. package/dist/server/aiChatHandler/llmStreaming.js +0 -265
  26. package/dist/server/aiChatHandler/validation.js +0 -19
  27. package/src/client/VZSidebar/AIChat/ChatInput.tsx +0 -237
  28. package/src/client/VZSidebar/AIChat/DiffView.scss +0 -173
  29. package/src/client/VZSidebar/AIChat/DiffView.tsx +0 -236
  30. package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +0 -89
  31. package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +0 -86
  32. package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +0 -64
  33. package/src/client/VZSidebar/AIChat/Message.tsx +0 -145
  34. package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -241
  35. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +0 -42
  36. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +0 -19
  37. package/src/client/VZSidebar/AIChat/index.tsx +0 -388
  38. package/src/client/VZSidebar/AIChat/styles.scss +0 -831
  39. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +0 -116
  40. package/src/server/aiChatHandler/aiEditing.ts +0 -102
  41. package/src/server/aiChatHandler/chatOperations.ts +0 -655
  42. package/src/server/aiChatHandler/errorHandling.ts +0 -66
  43. package/src/server/aiChatHandler/llmStreaming.ts +0 -403
  44. package/src/server/aiChatHandler/validation.ts +0 -24
@@ -1,265 +0,0 @@
1
- import OpenAI from 'openai';
2
- import { parseMarkdownFiles, StreamingMarkdownParser, } from 'llm-code-format';
3
- import { mergeFileChanges } from 'editcodewithai';
4
- import { updateFiles, updateAIScratchpad, createStreamingAIMessage, addStreamingEvent, updateStreamingStatus, finalizeStreamingMessage, } from './chatOperations.js';
5
- import { formatFiles } from '../prettier.js';
6
- // Verbose logs
7
- const DEBUG = false;
8
- // Useful for testing/debugging the streaming behavior
9
- const slowMode = false;
10
- // If the `EMIT_FIXTURES` variable is true,
11
- // then an output file in the `test/fixtures` folder
12
- // with the before and after file states for testing purposes.
13
- // This feeds into tests in codemirror-ot.
14
- const EMIT_FIXTURES = false;
15
- /**
16
- * Creates and configures the LLM function for streaming with reasoning tokens
17
- */
18
- export const createLLMFunction = ({ shareDBDoc, chatId,
19
- // Feature flag to enable/disable reasoning tokens.
20
- // When false, reasoning tokens are not requested from the API
21
- // and reasoning content is not processed in the streaming response.
22
- enableReasoningTokens = false, model, aiRequestOptions, }) => {
23
- return async (fullPrompt) => {
24
- // Create OpenRouter client for reasoning token support
25
- const openRouterClient = new OpenAI({
26
- apiKey: process.env.VZCODE_EDIT_WITH_AI_API_KEY,
27
- baseURL: process.env.VZCODE_EDIT_WITH_AI_BASE_URL ||
28
- 'https://openrouter.ai/api/v1',
29
- defaultHeaders: {
30
- 'HTTP-Referer': 'https://vizhub.com',
31
- 'X-Title': 'VizHub',
32
- },
33
- });
34
- let fullContent = '';
35
- let generationId = '';
36
- let currentEditingFileName = null;
37
- let accumulatedTextChunk = '';
38
- let currentFileContent = '';
39
- // Create streaming AI message
40
- createStreamingAIMessage(shareDBDoc, chatId);
41
- // Set initial content generation status
42
- updateStreamingStatus(shareDBDoc, chatId, 'Formulating a plan...');
43
- // Helper to get original file content
44
- const getOriginalFileContent = (fileName) => {
45
- const files = shareDBDoc.data.files;
46
- for (const file of Object.values(files)) {
47
- if (file.name === fileName) {
48
- return file.text || '';
49
- }
50
- }
51
- return '';
52
- };
53
- // Helper to emit text chunk when accumulated
54
- const emitTextChunk = async () => {
55
- if (accumulatedTextChunk.trim()) {
56
- DEBUG &&
57
- console.log('LLMStreaming: Emitting text chunk:', accumulatedTextChunk.substring(0, 100) + '...');
58
- await addStreamingEvent(shareDBDoc, chatId, {
59
- type: 'text_chunk',
60
- content: accumulatedTextChunk,
61
- timestamp: Date.now(),
62
- });
63
- accumulatedTextChunk = '';
64
- }
65
- };
66
- // Helper to complete file editing
67
- const completeFileEditing = async (fileName) => {
68
- if (fileName) {
69
- DEBUG &&
70
- console.log(`LLMStreaming: Completing file editing for ${fileName}`);
71
- await addStreamingEvent(shareDBDoc, chatId, {
72
- type: 'file_complete',
73
- fileName,
74
- beforeContent: getOriginalFileContent(fileName),
75
- afterContent: currentFileContent,
76
- timestamp: Date.now(),
77
- });
78
- currentFileContent = '';
79
- }
80
- };
81
- // Define callbacks for streaming parser
82
- const callbacks = {
83
- onFileNameChange: async (fileName, format) => {
84
- DEBUG &&
85
- console.log(`LLMStreaming: File changed to: ${fileName} (${format})`);
86
- // Emit any accumulated text chunk first
87
- await emitTextChunk();
88
- // Complete previous file if any
89
- if (currentEditingFileName) {
90
- await completeFileEditing(currentEditingFileName);
91
- }
92
- // Start new file
93
- currentEditingFileName = fileName;
94
- currentFileContent = '';
95
- // Emit file start event
96
- await addStreamingEvent(shareDBDoc, chatId, {
97
- type: 'file_start',
98
- fileName,
99
- timestamp: Date.now(),
100
- });
101
- // Update status
102
- updateStreamingStatus(shareDBDoc, chatId, `Editing ${fileName}...`);
103
- },
104
- onCodeLine: async (line) => {
105
- DEBUG && console.log(`Code line: ${line}`);
106
- // Accumulate code content for the current file
107
- currentFileContent += line + '\n';
108
- },
109
- onNonCodeLine: async (line) => {
110
- DEBUG && console.log(`Non-code line: ${line}`);
111
- // Accumulate non-code content as text chunk
112
- if (line.trim() !== '') {
113
- accumulatedTextChunk += line + '\n';
114
- // Update status for subsequent non-code chunks
115
- if (firstNonCodeChunkProcessed) {
116
- updateStreamingStatus(shareDBDoc, chatId, 'Describing changes...');
117
- }
118
- else {
119
- firstNonCodeChunkProcessed = true;
120
- }
121
- }
122
- },
123
- onFileDelete: async (fileName) => {
124
- DEBUG &&
125
- console.log(`LLMStreaming: File marked for deletion: ${fileName}`);
126
- // Emit any accumulated text chunk first
127
- await emitTextChunk();
128
- // Complete previous file if any
129
- if (currentEditingFileName) {
130
- await completeFileEditing(currentEditingFileName);
131
- }
132
- // Reset current editing state
133
- currentEditingFileName = null;
134
- currentFileContent = '';
135
- // Emit file delete event
136
- await addStreamingEvent(shareDBDoc, chatId, {
137
- type: 'file_delete',
138
- fileName,
139
- timestamp: Date.now(),
140
- });
141
- // Update status
142
- updateStreamingStatus(shareDBDoc, chatId, `Deleting ${fileName}...`);
143
- },
144
- };
145
- const parser = new StreamingMarkdownParser(callbacks);
146
- const chunks = [];
147
- let reasoningContent = '';
148
- // Stream the response with reasoning tokens
149
- const modelName = model ||
150
- process.env.VZCODE_EDIT_WITH_AI_MODEL_NAME ||
151
- 'anthropic/claude-3.5-sonnet';
152
- // Configure reasoning tokens based on enableReasoningTokens flag
153
- const requestConfig = {
154
- model: modelName,
155
- messages: [{ role: 'user', content: fullPrompt }],
156
- usage: { include: true },
157
- stream: true,
158
- ...aiRequestOptions,
159
- };
160
- // Only include reasoning configuration if reasoning tokens are enabled
161
- if (enableReasoningTokens) {
162
- requestConfig.reasoning = {
163
- effort: 'low',
164
- exclude: false,
165
- };
166
- }
167
- const stream = await openRouterClient.chat.completions.create(requestConfig);
168
- let reasoningStarted = false;
169
- let contentStarted = false;
170
- let firstNonCodeChunkProcessed = false;
171
- for await (const chunk of stream) {
172
- if (slowMode) {
173
- await new Promise((resolve) => setTimeout(resolve, 500));
174
- }
175
- const delta = chunk.choices[0]?.delta; // Type assertion for OpenRouter-specific reasoning fields
176
- if (delta?.reasoning && enableReasoningTokens) {
177
- // Handle reasoning tokens (thinking) - only if enabled
178
- if (!reasoningStarted) {
179
- reasoningStarted = true;
180
- updateStreamingStatus(shareDBDoc, chatId, 'Thinking...');
181
- }
182
- reasoningContent += delta.reasoning;
183
- updateAIScratchpad(shareDBDoc, chatId, reasoningContent);
184
- }
185
- else if (delta?.content) {
186
- // Handle regular content tokens
187
- if (!contentStarted) {
188
- contentStarted = true;
189
- if (reasoningStarted) {
190
- // Clear reasoning when content starts
191
- updateAIScratchpad(shareDBDoc, chatId, '');
192
- }
193
- // // Set initial content generation status
194
- // updateStreamingStatus(
195
- // shareDBDoc,
196
- // chatId,
197
- // 'Formulating a plan...',
198
- // );
199
- }
200
- const chunkContent = delta.content;
201
- chunks.push(chunkContent);
202
- await parser.processChunk(chunkContent);
203
- fullContent += chunkContent;
204
- }
205
- else if (chunk.usage) {
206
- // Handle usage information
207
- DEBUG && console.log('Usage:', chunk.usage);
208
- }
209
- if (!generationId && chunk.id) {
210
- generationId = chunk.id;
211
- }
212
- }
213
- await parser.flushRemaining();
214
- // Emit any remaining text chunk
215
- await emitTextChunk();
216
- // Complete final file if any
217
- if (currentEditingFileName) {
218
- await completeFileEditing(currentEditingFileName);
219
- }
220
- // // Capture the current state of files before applying changes
221
- // const beforeFiles = createFilesSnapshot(
222
- // shareDBDoc.data.files,
223
- // );
224
- // Parse the full content to extract file changes
225
- // export type FileCollection = Record<string, string>;
226
- const newFilesUnformatted = parseMarkdownFiles(fullContent, 'bold').files;
227
- // Run Prettier on `newFiles` before applying them,
228
- // preserving empty files as empty
229
- // since that is the cue to delete a file.
230
- const newFilesFormatted = await formatFiles(newFilesUnformatted);
231
- // Capture the current state of files before applying changes
232
- let vizFilesBefore;
233
- if (EMIT_FIXTURES) {
234
- vizFilesBefore = JSON.parse(JSON.stringify(shareDBDoc.data.files));
235
- }
236
- // Apply all the edits at once
237
- const vizFilesAfter = mergeFileChanges(shareDBDoc.data.files, newFilesFormatted);
238
- const filesOp = updateFiles(shareDBDoc, vizFilesAfter);
239
- if (EMIT_FIXTURES) {
240
- const fs = await import('fs');
241
- const path = await import('path');
242
- const testCasesDir = path.resolve(process.cwd(), '../', 'fixtures');
243
- if (!fs.existsSync(testCasesDir)) {
244
- fs.mkdirSync(testCasesDir, { recursive: true });
245
- }
246
- const timestamp = new Date()
247
- .toISOString()
248
- .replace(/[:.]/g, '-');
249
- const testCasePath = path.join(testCasesDir, `ai-chat-${timestamp}.json`);
250
- const testCaseData = {
251
- vizFilesBefore,
252
- vizFilesAfter,
253
- filesOp,
254
- };
255
- fs.writeFileSync(testCasePath, JSON.stringify(testCaseData, null, 2));
256
- console.log(`AI chat test case written to ${testCasePath}`);
257
- }
258
- // Finalize streaming message
259
- finalizeStreamingMessage(shareDBDoc, chatId);
260
- return {
261
- content: fullContent,
262
- generationId: generationId,
263
- };
264
- };
265
- };
@@ -1,19 +0,0 @@
1
- /**
2
- * Validates incoming request data for AI chat messages
3
- */
4
- export const validateRequest = (req, res) => {
5
- const { content, chatId } = req.body;
6
- if (!content || typeof content !== 'string') {
7
- res.status(400).json({
8
- error: 'Invalid request: content is required and must be a string',
9
- });
10
- return false;
11
- }
12
- if (!chatId || typeof chatId !== 'string') {
13
- res.status(400).json({
14
- error: 'Invalid request: chatId is required and must be a string',
15
- });
16
- return false;
17
- }
18
- return true;
19
- };
@@ -1,237 +0,0 @@
1
- import {
2
- useRef,
3
- useEffect,
4
- useCallback,
5
- memo,
6
- } from 'react';
7
- import {
8
- Form,
9
- Button,
10
- ButtonGroup,
11
- ToggleButton,
12
- } from '../../bootstrap';
13
- import { enableAskMode } from '../../featureFlags';
14
- import { useSpeechRecognition } from './useSpeechRecognition';
15
- import { MicSVG, MicOffSVG } from '../../Icons';
16
-
17
- interface ChatInputProps {
18
- aiChatMessage: string;
19
- setAIChatMessage: (message: string) => void;
20
- onSendMessage: () => void;
21
- focused: boolean;
22
- aiChatMode: 'ask' | 'edit';
23
- setAIChatMode: (mode: 'ask' | 'edit') => void;
24
- navigateMessageHistoryUp: () => void;
25
- navigateMessageHistoryDown: () => void;
26
- resetMessageHistoryNavigation: () => void;
27
- }
28
-
29
- const ChatInputComponent = ({
30
- aiChatMessage,
31
- setAIChatMessage,
32
- onSendMessage,
33
- focused,
34
- aiChatMode,
35
- setAIChatMode,
36
- navigateMessageHistoryUp,
37
- navigateMessageHistoryDown,
38
- resetMessageHistoryNavigation,
39
- }: ChatInputProps) => {
40
- const inputRef = useRef<HTMLTextAreaElement>(null);
41
-
42
- // Use the speech recognition hook
43
- const {
44
- isSpeaking,
45
- toggleSpeechRecognition,
46
- stopSpeaking,
47
- } = useSpeechRecognition(setAIChatMessage);
48
-
49
- useEffect(() => {
50
- // Focus the input when the AI chat is focused
51
- if (focused && inputRef.current) {
52
- inputRef.current.focus();
53
- }
54
- }, [focused]);
55
-
56
- const handleKeyDown = useCallback(
57
- (event: React.KeyboardEvent) => {
58
- if (event.key === 'Enter' && !event.shiftKey) {
59
- event.preventDefault();
60
- onSendMessage();
61
- } else if (event.key === 'Enter' && event.shiftKey) {
62
- // Shift+Enter should add a newline, not trigger run code
63
- // We prevent the event from bubbling up to the global keyboard handler
64
- event.stopPropagation();
65
- } else if (event.key === 'ArrowUp') {
66
- // Only navigate history if cursor is at the beginning of the first line
67
- const textarea =
68
- event.target as HTMLTextAreaElement;
69
- const { selectionStart, value } = textarea;
70
- const lines = value
71
- .substring(0, selectionStart)
72
- .split('\n');
73
-
74
- // Check if we're at the beginning of the first line
75
- if (lines.length === 1 && selectionStart === 0) {
76
- event.preventDefault();
77
- navigateMessageHistoryUp();
78
- }
79
- } else if (event.key === 'ArrowDown') {
80
- // Only navigate history if cursor is at the end of the last line
81
- const textarea =
82
- event.target as HTMLTextAreaElement;
83
- const { selectionStart, value } = textarea;
84
- const remainingText =
85
- value.substring(selectionStart);
86
- const remainingLines = remainingText.split('\n');
87
-
88
- // Check if we're at the end of the last line
89
- if (
90
- remainingLines.length === 1 &&
91
- remainingLines[0] === ''
92
- ) {
93
- event.preventDefault();
94
- navigateMessageHistoryDown();
95
- }
96
- }
97
- },
98
- [
99
- onSendMessage,
100
- navigateMessageHistoryUp,
101
- navigateMessageHistoryDown,
102
- ],
103
- );
104
-
105
- const handleSendClick = useCallback(() => {
106
- // If speech recognition is active, stop it
107
- if (isSpeaking) {
108
- stopSpeaking();
109
- }
110
- onSendMessage();
111
- }, [onSendMessage, isSpeaking, stopSpeaking]);
112
-
113
- const handleChange = useCallback(
114
- (event: React.ChangeEvent<HTMLTextAreaElement>) => {
115
- setAIChatMessage(event.target.value);
116
- // Reset history navigation when user starts typing
117
- resetMessageHistoryNavigation();
118
- },
119
- [setAIChatMessage, resetMessageHistoryNavigation],
120
- );
121
-
122
- return (
123
- <div className="ai-chat-input-container">
124
- {enableAskMode && (
125
- <div
126
- className="ai-chat-mode-toggle"
127
- style={{ marginBottom: '8px' }}
128
- >
129
- <ButtonGroup size="sm">
130
- <ToggleButton
131
- id="ai-chat-mode-ask"
132
- type="radio"
133
- variant={
134
- aiChatMode === 'ask'
135
- ? 'primary'
136
- : 'outline-primary'
137
- }
138
- name="ai-chat-mode"
139
- value="ask"
140
- checked={aiChatMode === 'ask'}
141
- onChange={() => setAIChatMode('ask')}
142
- >
143
- 💬 Ask
144
- </ToggleButton>
145
- <ToggleButton
146
- id="ai-chat-mode-edit"
147
- type="radio"
148
- variant={
149
- aiChatMode === 'edit'
150
- ? 'primary'
151
- : 'outline-primary'
152
- }
153
- name="ai-chat-mode"
154
- value="edit"
155
- checked={aiChatMode === 'edit'}
156
- onChange={() => setAIChatMode('edit')}
157
- >
158
- ✏️ Edit
159
- </ToggleButton>
160
- </ButtonGroup>
161
- <div className="ai-chat-mode-description">
162
- {aiChatMode === 'ask'
163
- ? 'Ask questions without editing files'
164
- : 'Get answers and code edits'}
165
- </div>
166
- </div>
167
- )}
168
- <Form.Group className="ai-chat-input-group">
169
- <Form.Control
170
- as="textarea"
171
- rows={10}
172
- value={aiChatMessage}
173
- onChange={handleChange}
174
- onKeyDown={handleKeyDown}
175
- ref={inputRef}
176
- placeholder={
177
- aiChatMode === 'edit'
178
- ? 'Ask me to make code changes...'
179
- : 'Ask me anything about your code...'
180
- }
181
- spellCheck="false"
182
- aria-label="Chat message input"
183
- />
184
- <div className="ai-chat-input-footer">
185
- <span className="ai-chat-hint">
186
- {aiChatMessage ? 'Press Enter to send' : ''}
187
- </span>
188
- <div style={{ display: 'flex', gap: '8px' }}>
189
- <Button
190
- variant={
191
- isSpeaking ? 'danger' : 'outline-secondary'
192
- }
193
- size="sm"
194
- onClick={toggleSpeechRecognition}
195
- aria-label={
196
- isSpeaking
197
- ? 'Stop voice typing'
198
- : 'Start voice typing'
199
- }
200
- title={
201
- isSpeaking
202
- ? 'Stop voice typing'
203
- : 'Start voice typing'
204
- }
205
- style={{
206
- borderColor: isSpeaking
207
- ? undefined
208
- : '#dee2e6',
209
- borderRadius: '0.375rem',
210
- borderWidth: '1px',
211
- borderStyle: 'solid',
212
- }}
213
- >
214
- {isSpeaking ? <MicOffSVG /> : <MicSVG />}
215
- </Button>
216
- <Button
217
- variant={
218
- aiChatMessage.trim()
219
- ? 'primary'
220
- : 'outline-secondary'
221
- }
222
- onClick={handleSendClick}
223
- disabled={!aiChatMessage.trim()}
224
- className="ai-chat-send-button"
225
- aria-label="Send message"
226
- title="Send message (Enter)"
227
- >
228
- Send
229
- </Button>
230
- </div>
231
- </div>
232
- </Form.Group>
233
- </div>
234
- );
235
- };
236
-
237
- export const ChatInput = memo(ChatInputComponent);
@@ -1,173 +0,0 @@
1
- .diff-view {
2
- margin: 12px 0;
3
-
4
- .diff-summary {
5
- margin-bottom: 8px;
6
- display: flex;
7
- justify-content: space-between;
8
- align-items: center;
9
-
10
- .diff-stats {
11
- display: flex;
12
- align-items: center;
13
- gap: 8px;
14
- font-size: 14px;
15
- color: var(--bs-secondary-color);
16
-
17
- .files-changed {
18
- color: white;
19
- font-weight: 600;
20
- }
21
-
22
- .additions {
23
- color: #28a745;
24
- font-weight: 600;
25
- }
26
-
27
- .deletions {
28
- color: #dc3545;
29
- font-weight: 600;
30
- }
31
- }
32
- }
33
-
34
- .diff-files {
35
- display: flex;
36
- flex-direction: column;
37
- gap: 4px;
38
-
39
- // Fix diff2html styling issues for dark theme and layout
40
- .d2h-wrapper {
41
- // Override diff2html variables for dark theme
42
- --d2h-bg-color: rgb(41, 44, 52);
43
- --d2h-color: #e6edf3;
44
- --d2h-border-color: #30363d;
45
- --d2h-dim-color: #6e7681;
46
- --d2h-info-bg-color: #161b22;
47
- --d2h-file-header-bg-color: #161b22;
48
- --d2h-file-header-border-color: #30363d;
49
- --d2h-line-border-color: #21262d;
50
- --d2h-ins-bg-color: rgba(46, 160, 67, 0.15);
51
- --d2h-ins-border-color: rgba(46, 160, 67, 0.4);
52
- --d2h-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
53
- --d2h-ins-label-color: #3fb950;
54
- --d2h-del-bg-color: rgba(248, 81, 73, 0.1);
55
- --d2h-del-border-color: rgba(248, 81, 73, 0.4);
56
- --d2h-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
57
- --d2h-del-label-color: #f85149;
58
-
59
- background-color: var(--d2h-bg-color);
60
- color: var(--d2h-color);
61
- border-radius: 6px;
62
- overflow: hidden;
63
- }
64
-
65
- // Fix layout issues with line numbers
66
- .d2h-diff-table {
67
- position: relative;
68
- background-color: var(--d2h-bg-color);
69
- }
70
-
71
- .d2h-code-linenumber {
72
- position: relative !important;
73
- float: none !important;
74
- display: table-cell !important;
75
- width: auto !important;
76
- background-color: var(--d2h-bg-color) !important;
77
- border-color: var(--d2h-line-border-color) !important;
78
- color: var(--d2h-dim-color) !important;
79
- }
80
-
81
- .d2h-code-side-linenumber {
82
- position: relative !important;
83
- float: none !important;
84
- display: table-cell !important;
85
- width: auto !important;
86
- background-color: var(--d2h-bg-color) !important;
87
- border-color: var(--d2h-line-border-color) !important;
88
- color: var(--d2h-dim-color) !important;
89
- }
90
-
91
- // Ensure proper table layout
92
- .d2h-diff-tbody tr {
93
- display: table-row;
94
- }
95
-
96
- .d2h-diff-tbody td {
97
- display: table-cell;
98
- vertical-align: top;
99
- }
100
-
101
- // Fix file header styling
102
- .d2h-file-header {
103
- display: none !important;
104
- background-color: var(
105
- --d2h-file-header-bg-color
106
- ) !important;
107
- border-bottom: 1px solid
108
- var(--d2h-file-header-border-color) !important;
109
- color: var(--d2h-color);
110
- }
111
-
112
- // Ensure proper line styling
113
- .d2h-del {
114
- background-color: var(--d2h-del-bg-color) !important;
115
- border-color: var(--d2h-del-border-color) !important;
116
- }
117
-
118
- .d2h-ins {
119
- background-color: var(--d2h-ins-bg-color) !important;
120
- border-color: var(--d2h-ins-border-color) !important;
121
- }
122
-
123
- // Style clickable file names
124
- .d2h-file-name {
125
- &:hover {
126
- text-decoration: underline !important;
127
- opacity: 0.8;
128
- }
129
- }
130
-
131
- // Fix wrapper border
132
- .d2h-file-wrapper {
133
- border: 1px solid var(--d2h-border-color) !important;
134
- background-color: var(--d2h-bg-color);
135
- }
136
- }
137
-
138
- .deleted-file {
139
- margin-bottom: 8px;
140
- border: 1px solid #30363d;
141
- border-radius: 6px;
142
- background-color: rgb(41, 44, 52);
143
- overflow: hidden;
144
-
145
- .deleted-file-header {
146
- display: flex;
147
- justify-content: space-between;
148
- align-items: center;
149
- padding: 12px 16px;
150
- background-color: #161b22;
151
- border-bottom: 1px solid #30363d;
152
-
153
- .deleted-file-name {
154
- color: #e6edf3;
155
- font-weight: 600;
156
- font-family:
157
- 'SFMono-Regular', Consolas, 'Liberation Mono',
158
- Menlo, monospace;
159
- font-size: 14px;
160
- }
161
-
162
- .deleted-file-status {
163
- color: #f85149;
164
- font-weight: 600;
165
- font-size: 14px;
166
- background-color: rgba(248, 81, 73, 0.1);
167
- padding: 4px 8px;
168
- border-radius: 4px;
169
- border: 1px solid rgba(248, 81, 73, 0.4);
170
- }
171
- }
172
- }
173
- }