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.
- package/dist/assets/{index-BpUpNto4.js → index-C2BLPyQP.js} +138 -125
- package/dist/assets/{index-DLm5FQ0E.css → index-DVR90LwF.css} +1 -1
- package/dist/index.html +2 -2
- package/dist/server/aiChatHandler/chatOperations.js +168 -35
- package/dist/server/aiChatHandler/index.js +11 -30
- package/dist/server/aiChatHandler/llmStreaming.js +105 -118
- package/package.json +11 -11
- package/src/client/AIAssist/AIAssistWidget/index.tsx +12 -1
- package/src/client/App/useShareDB.ts +1 -1
- package/src/client/CodeEditor/index.tsx +9 -10
- package/src/client/VZCodeContext/types.ts +3 -1
- package/src/client/VZCodeContext/useVZCodeState.ts +7 -5
- package/src/client/VZRight.tsx +10 -2
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -7
- package/src/client/VZSidebar/AIChat/DiffView.scss +1 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -29
- package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +81 -0
- package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +86 -0
- package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +64 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +68 -53
- package/src/client/VZSidebar/AIChat/MessageList.tsx +139 -118
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +63 -21
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +4 -118
- package/src/client/VZSidebar/AIChat/index.tsx +62 -19
- package/src/client/VZSidebar/AIChat/styles.scss +159 -16
- package/src/client/VZSidebar/Item.tsx +2 -2
- package/src/client/VZSidebar/Search.tsx +13 -6
- package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +39 -23
- package/src/client/VZSidebar/VisualEditor/utils.ts +1 -1
- package/src/client/VZSidebar/index.tsx +12 -10
- package/src/client/VZSidebar/useDragAndDrop.tsx +225 -214
- package/src/client/featureFlags.ts +3 -0
- package/src/client/hooks/useAutoScroll.ts +329 -0
- package/src/client/tabsSearchParameters.ts +1 -17
- package/src/client/useFileCRUD.ts +5 -2
- package/src/client/useKeyboardShortcuts.ts +7 -0
- package/src/client/useOpenDirectories.ts +2 -1
- package/src/client/usePrettier/index.ts +1 -1
- package/src/client/useURLSync.ts +1 -0
- package/src/client/utils/scrollUtils.ts +170 -0
- package/src/client/vzReducer/searchReducer.ts +6 -3
- package/src/server/aiChatHandler/chatOperations.ts +224 -43
- package/src/server/aiChatHandler/index.ts +8 -48
- package/src/server/aiChatHandler/llmStreaming.ts +149 -170
- package/src/types.ts +81 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -120,8 +120,8 @@
|
|
|
120
120
|
"@codemirror/state": "^6.5.2",
|
|
121
121
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
122
122
|
"@codemirror/view": "^6.38.2",
|
|
123
|
-
"@langchain/core": "^0.3.
|
|
124
|
-
"@langchain/openai": "^0.6.
|
|
123
|
+
"@langchain/core": "^0.3.77",
|
|
124
|
+
"@langchain/openai": "^0.6.13",
|
|
125
125
|
"@lezer/highlight": "^1.2.1",
|
|
126
126
|
"@livekit/components-react": "^2.9.14",
|
|
127
127
|
"@replit/codemirror-indentation-markers": "^6.5.3",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"react-bootstrap": "^2.10.10",
|
|
173
173
|
"react-dom": "^18",
|
|
174
174
|
"react-markdown": "^10.1.0",
|
|
175
|
-
"react-router-dom": "^7.
|
|
175
|
+
"react-router-dom": "^7.9.1",
|
|
176
176
|
"remark-gfm": "^4.0.1",
|
|
177
177
|
"sharedb": "^5.2.2",
|
|
178
178
|
"sharedb-client-browser": "^5.2.2",
|
|
@@ -186,9 +186,9 @@
|
|
|
186
186
|
"@types/d3-color": "^3.1.3",
|
|
187
187
|
"@types/react": "^18",
|
|
188
188
|
"@types/react-dom": "^18",
|
|
189
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
190
|
-
"@typescript-eslint/parser": "^8.
|
|
191
|
-
"@vitejs/plugin-react": "^5.0.
|
|
189
|
+
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
|
190
|
+
"@typescript-eslint/parser": "^8.44.0",
|
|
191
|
+
"@vitejs/plugin-react": "^5.0.3",
|
|
192
192
|
"concurrently": "^9.2.1",
|
|
193
193
|
"cross-env": "^10.0.0",
|
|
194
194
|
"eslint": "^9.35.0",
|
|
@@ -196,16 +196,16 @@
|
|
|
196
196
|
"eslint-plugin-react": "^7.37.5",
|
|
197
197
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
198
198
|
"globals": "^16.4.0",
|
|
199
|
-
"npm-check-updates": "^18.1.
|
|
199
|
+
"npm-check-updates": "^18.1.1",
|
|
200
200
|
"prettier": "^3.6.2",
|
|
201
201
|
"sass": "^1.92.1",
|
|
202
202
|
"ts-node": "^10.9.2",
|
|
203
203
|
"typescript": "^5.9.2",
|
|
204
|
-
"vite": "^7.1.
|
|
204
|
+
"vite": "^7.1.6",
|
|
205
205
|
"vitest": "^3.2.4"
|
|
206
206
|
},
|
|
207
207
|
"optionalDependencies": {
|
|
208
|
-
"@rollup/rollup-darwin-arm64": "^4.50.
|
|
209
|
-
"@rollup/rollup-win32-x64-msvc": "^4.50.
|
|
208
|
+
"@rollup/rollup-darwin-arm64": "^4.50.2",
|
|
209
|
+
"@rollup/rollup-win32-x64-msvc": "^4.50.2"
|
|
210
210
|
}
|
|
211
211
|
}
|
|
@@ -101,7 +101,18 @@ export const AIAssistWidget = ({
|
|
|
101
101
|
: currentAIStreamId,
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
|
-
}, [
|
|
104
|
+
}, [
|
|
105
|
+
activeFileId,
|
|
106
|
+
paneId,
|
|
107
|
+
aiStreamId,
|
|
108
|
+
aiAssistEndpoint,
|
|
109
|
+
aiAssistOptions,
|
|
110
|
+
editorCache,
|
|
111
|
+
runCodeRef,
|
|
112
|
+
runPrettierRef,
|
|
113
|
+
shareDBDoc,
|
|
114
|
+
tabList,
|
|
115
|
+
]);
|
|
105
116
|
|
|
106
117
|
const showWidget = enableStopGeneration
|
|
107
118
|
? true
|
|
@@ -134,15 +134,16 @@ export const CodeEditor = ({
|
|
|
134
134
|
}, [
|
|
135
135
|
activePane,
|
|
136
136
|
shareDBDoc,
|
|
137
|
-
filesPath,
|
|
138
137
|
localPresence,
|
|
139
138
|
docPresence,
|
|
140
139
|
theme,
|
|
141
140
|
onInteract,
|
|
142
141
|
editorCache,
|
|
143
|
-
|
|
142
|
+
customInteractRules,
|
|
144
143
|
aiCopilotEndpoint,
|
|
145
144
|
esLintSource,
|
|
145
|
+
setIsAIChatOpen,
|
|
146
|
+
handleSendMessage,
|
|
146
147
|
]);
|
|
147
148
|
|
|
148
149
|
// Every time the active file switches from one file to another,
|
|
@@ -155,10 +156,10 @@ export const CodeEditor = ({
|
|
|
155
156
|
if (!editorCacheValue) return; // Guard against null editorCacheValue
|
|
156
157
|
// if (openSplitPane) return;
|
|
157
158
|
|
|
159
|
+
const codeEditorEl = codeEditorRef.current;
|
|
160
|
+
|
|
158
161
|
// Add the editor and apply the prior scroll position to the DOM.
|
|
159
|
-
|
|
160
|
-
editorCacheValue.editor.dom,
|
|
161
|
-
);
|
|
162
|
+
codeEditorEl.appendChild(editorCacheValue.editor.dom);
|
|
162
163
|
editorCacheValue.editor.scrollDOM.scrollTo({
|
|
163
164
|
top: editorCacheValue.scrollPosition ?? 0,
|
|
164
165
|
});
|
|
@@ -168,18 +169,16 @@ export const CodeEditor = ({
|
|
|
168
169
|
// This happens every time `activeFileId` changes.
|
|
169
170
|
if (
|
|
170
171
|
editorCacheValue &&
|
|
171
|
-
|
|
172
|
-
editorCacheValue.editor.dom,
|
|
173
|
-
)
|
|
172
|
+
codeEditorEl?.contains(editorCacheValue.editor.dom)
|
|
174
173
|
) {
|
|
175
174
|
editorCacheValue.scrollPosition =
|
|
176
175
|
editorCacheValue.editor.scrollDOM.scrollTop;
|
|
177
|
-
|
|
176
|
+
codeEditorEl.removeChild(
|
|
178
177
|
editorCacheValue.editor.dom,
|
|
179
178
|
);
|
|
180
179
|
}
|
|
181
180
|
};
|
|
182
|
-
}, [
|
|
181
|
+
}, [codeEditorRef, content, editorCacheValue]);
|
|
183
182
|
|
|
184
183
|
// Focus the editor
|
|
185
184
|
useEffect(() => {
|
|
@@ -170,7 +170,6 @@ export type VZCodeContextValue = {
|
|
|
170
170
|
setVoiceChatModalOpen: (state: boolean) => void;
|
|
171
171
|
aiChatMessage: string;
|
|
172
172
|
setAIChatMessage: (message: string) => void;
|
|
173
|
-
isLoading: boolean;
|
|
174
173
|
currentChatId: string;
|
|
175
174
|
selectedChatId: string | null;
|
|
176
175
|
setSelectedChatId: (chatId: string | null) => void;
|
|
@@ -207,6 +206,9 @@ export type VZCodeContextValue = {
|
|
|
207
206
|
options?: Record<string, string>,
|
|
208
207
|
) => void;
|
|
209
208
|
}) => React.ReactNode;
|
|
209
|
+
|
|
210
|
+
// Feature flags
|
|
211
|
+
enableMinimalEditFlow: boolean;
|
|
210
212
|
};
|
|
211
213
|
|
|
212
214
|
export interface VZCodeProviderProps {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
VZCodeContextValue,
|
|
28
28
|
VZCodeProviderProps,
|
|
29
29
|
} from './types';
|
|
30
|
+
import { enableMinimalEditFlow } from '../featureFlags';
|
|
30
31
|
|
|
31
32
|
export const useVZCodeState = ({
|
|
32
33
|
content,
|
|
@@ -280,7 +281,6 @@ export const useVZCodeState = ({
|
|
|
280
281
|
// Use selectedChatId if available, otherwise use currentChatId for backward compatibility
|
|
281
282
|
const activeChatId = selectedChatId || currentChatId;
|
|
282
283
|
const currentChat = content?.chats?.[activeChatId];
|
|
283
|
-
const isLoading = currentChat?.aiStatus === 'generating';
|
|
284
284
|
|
|
285
285
|
// Message history for up/down arrow navigation - using ShareDB document data
|
|
286
286
|
const messageHistory = useMemo(() => {
|
|
@@ -371,8 +371,7 @@ export const useVZCodeState = ({
|
|
|
371
371
|
if (
|
|
372
372
|
!messageContent ||
|
|
373
373
|
typeof messageContent !== 'string' ||
|
|
374
|
-
!messageContent.trim()
|
|
375
|
-
isLoading
|
|
374
|
+
!messageContent.trim()
|
|
376
375
|
) {
|
|
377
376
|
return;
|
|
378
377
|
}
|
|
@@ -473,12 +472,13 @@ export const useVZCodeState = ({
|
|
|
473
472
|
},
|
|
474
473
|
[
|
|
475
474
|
aiChatMessage,
|
|
476
|
-
isLoading,
|
|
477
475
|
aiChatEndpoint,
|
|
478
476
|
aiChatOptions,
|
|
479
477
|
activeChatId,
|
|
480
478
|
aiChatMode,
|
|
481
479
|
autoForkAndRetryAI,
|
|
480
|
+
DEBUG,
|
|
481
|
+
setAIErrorMessage,
|
|
482
482
|
],
|
|
483
483
|
);
|
|
484
484
|
|
|
@@ -679,7 +679,6 @@ export const useVZCodeState = ({
|
|
|
679
679
|
|
|
680
680
|
aiChatMessage,
|
|
681
681
|
setAIChatMessage,
|
|
682
|
-
isLoading,
|
|
683
682
|
currentChatId,
|
|
684
683
|
selectedChatId,
|
|
685
684
|
setSelectedChatId,
|
|
@@ -699,5 +698,8 @@ export const useVZCodeState = ({
|
|
|
699
698
|
|
|
700
699
|
// Additional widgets that can be rendered in AI chat messages
|
|
701
700
|
additionalWidgets,
|
|
701
|
+
|
|
702
|
+
// Feature flags
|
|
703
|
+
enableMinimalEditFlow,
|
|
702
704
|
};
|
|
703
705
|
};
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -42,7 +42,7 @@ export const VZRight = () => {
|
|
|
42
42
|
content
|
|
43
43
|
? vizFilesToFileCollection(content.files)
|
|
44
44
|
: null,
|
|
45
|
-
[content
|
|
45
|
+
[content],
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
const isInteracting = content?.isInteracting || false;
|
|
@@ -101,7 +101,15 @@ export const VZRight = () => {
|
|
|
101
101
|
// runtime.cleanup();
|
|
102
102
|
// worker.terminate();
|
|
103
103
|
// };
|
|
104
|
-
}, [
|
|
104
|
+
}, [
|
|
105
|
+
files,
|
|
106
|
+
isInteracting,
|
|
107
|
+
runIdChanged,
|
|
108
|
+
hardRerun,
|
|
109
|
+
clearRuntimeError,
|
|
110
|
+
handleRuntimeError,
|
|
111
|
+
iframeRef,
|
|
112
|
+
]);
|
|
105
113
|
|
|
106
114
|
return (
|
|
107
115
|
<div className="right">
|
|
@@ -18,7 +18,6 @@ interface ChatInputProps {
|
|
|
18
18
|
aiChatMessage: string;
|
|
19
19
|
setAIChatMessage: (message: string) => void;
|
|
20
20
|
onSendMessage: () => void;
|
|
21
|
-
isLoading: boolean;
|
|
22
21
|
focused: boolean;
|
|
23
22
|
aiChatMode: 'ask' | 'edit';
|
|
24
23
|
setAIChatMode: (mode: 'ask' | 'edit') => void;
|
|
@@ -31,7 +30,6 @@ const ChatInputComponent = ({
|
|
|
31
30
|
aiChatMessage,
|
|
32
31
|
setAIChatMessage,
|
|
33
32
|
onSendMessage,
|
|
34
|
-
isLoading,
|
|
35
33
|
focused,
|
|
36
34
|
aiChatMode,
|
|
37
35
|
setAIChatMode,
|
|
@@ -141,7 +139,6 @@ const ChatInputComponent = ({
|
|
|
141
139
|
value="ask"
|
|
142
140
|
checked={aiChatMode === 'ask'}
|
|
143
141
|
onChange={() => setAIChatMode('ask')}
|
|
144
|
-
disabled={isLoading}
|
|
145
142
|
>
|
|
146
143
|
💬 Ask
|
|
147
144
|
</ToggleButton>
|
|
@@ -157,7 +154,6 @@ const ChatInputComponent = ({
|
|
|
157
154
|
value="edit"
|
|
158
155
|
checked={aiChatMode === 'edit'}
|
|
159
156
|
onChange={() => setAIChatMode('edit')}
|
|
160
|
-
disabled={isLoading}
|
|
161
157
|
>
|
|
162
158
|
✏️ Edit
|
|
163
159
|
</ToggleButton>
|
|
@@ -183,7 +179,6 @@ const ChatInputComponent = ({
|
|
|
183
179
|
: 'Ask me anything about your code...'
|
|
184
180
|
}
|
|
185
181
|
spellCheck="false"
|
|
186
|
-
disabled={isLoading}
|
|
187
182
|
aria-label="Chat message input"
|
|
188
183
|
/>
|
|
189
184
|
<div className="ai-chat-input-footer">
|
|
@@ -197,7 +192,6 @@ const ChatInputComponent = ({
|
|
|
197
192
|
}
|
|
198
193
|
size="sm"
|
|
199
194
|
onClick={toggleSpeechRecognition}
|
|
200
|
-
disabled={isLoading}
|
|
201
195
|
aria-label={
|
|
202
196
|
isSpeaking
|
|
203
197
|
? 'Stop voice typing'
|
|
@@ -226,7 +220,7 @@ const ChatInputComponent = ({
|
|
|
226
220
|
: 'outline-secondary'
|
|
227
221
|
}
|
|
228
222
|
onClick={handleSendClick}
|
|
229
|
-
disabled={!aiChatMessage.trim()
|
|
223
|
+
disabled={!aiChatMessage.trim()}
|
|
230
224
|
className="ai-chat-send-button"
|
|
231
225
|
aria-label="Send message"
|
|
232
226
|
title="Send message (Enter)"
|
|
@@ -2,6 +2,8 @@ import React, {
|
|
|
2
2
|
useContext,
|
|
3
3
|
useEffect,
|
|
4
4
|
useRef,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useImperativeHandle,
|
|
5
7
|
} from 'react';
|
|
6
8
|
import {
|
|
7
9
|
UnifiedFilesDiff,
|
|
@@ -12,15 +14,29 @@ import * as Diff2Html from 'diff2html';
|
|
|
12
14
|
import 'diff2html/bundles/css/diff2html.min.css';
|
|
13
15
|
import './DiffView.scss';
|
|
14
16
|
import { VZCodeContext } from '../../VZCodeContext';
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
scrollToFirstDiff,
|
|
19
|
+
getHeaderOffset,
|
|
20
|
+
announceDiffSummary,
|
|
21
|
+
} from '../../utils/scrollUtils';
|
|
22
|
+
import { getFileId } from '@vizhub/viz-utils';
|
|
16
23
|
|
|
17
24
|
interface DiffViewProps {
|
|
18
25
|
diffData: UnifiedFilesDiff;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
// Methods that can be called on DiffView from parent components
|
|
29
|
+
export interface DiffViewRef {
|
|
30
|
+
scrollToFirstHunk: () => void;
|
|
31
|
+
focusDiffContainer: () => void;
|
|
32
|
+
announceSummary: () => void;
|
|
33
|
+
getFirstHunkElement: () => HTMLElement | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DiffView = forwardRef<
|
|
37
|
+
DiffViewRef,
|
|
38
|
+
DiffViewProps
|
|
39
|
+
>(({ diffData }, ref) => {
|
|
24
40
|
const { content, openTab, setIsAIChatOpen } =
|
|
25
41
|
useContext(VZCodeContext);
|
|
26
42
|
const diffContainerRef = useRef<HTMLDivElement>(null);
|
|
@@ -48,6 +64,42 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|
|
48
64
|
outputFormat: 'line-by-line',
|
|
49
65
|
});
|
|
50
66
|
|
|
67
|
+
// Expose methods for parent components to control scrolling and focus
|
|
68
|
+
useImperativeHandle(
|
|
69
|
+
ref,
|
|
70
|
+
() => ({
|
|
71
|
+
scrollToFirstHunk: () => {
|
|
72
|
+
if (diffContainerRef.current) {
|
|
73
|
+
const headerOffset = getHeaderOffset();
|
|
74
|
+
scrollToFirstDiff(
|
|
75
|
+
diffContainerRef.current,
|
|
76
|
+
headerOffset,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
focusDiffContainer: () => {
|
|
81
|
+
if (diffContainerRef.current) {
|
|
82
|
+
diffContainerRef.current.tabIndex = -1;
|
|
83
|
+
diffContainerRef.current.focus();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
announceSummary: () => {
|
|
87
|
+
if (diffContainerRef.current) {
|
|
88
|
+
announceDiffSummary(diffContainerRef.current);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
getFirstHunkElement: () => {
|
|
92
|
+
if (diffContainerRef.current) {
|
|
93
|
+
return diffContainerRef.current.querySelector(
|
|
94
|
+
'.d2h-diff-tbody tr',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
[],
|
|
101
|
+
);
|
|
102
|
+
|
|
51
103
|
// Add click handlers to file names after HTML is rendered
|
|
52
104
|
useEffect(() => {
|
|
53
105
|
if (!diffContainerRef.current) return;
|
|
@@ -63,29 +115,13 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|
|
63
115
|
const fileName = target.textContent?.trim();
|
|
64
116
|
|
|
65
117
|
console.log('Clicked file name:', fileName);
|
|
66
|
-
if (fileName) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// export type VizFile = {
|
|
74
|
-
// name: string;
|
|
75
|
-
// text: string;
|
|
76
|
-
// };
|
|
77
|
-
|
|
78
|
-
// TODO get fileId from content.files
|
|
79
|
-
const fileId = Object.entries(files).find(
|
|
80
|
-
([_id, file]) => file.name === fileName,
|
|
81
|
-
)?.[0];
|
|
82
|
-
console.log('Mapped file ID:', fileId);
|
|
83
|
-
|
|
84
|
-
if (fileId) {
|
|
85
|
-
// Open the file tab and switch to files view
|
|
86
|
-
openTab({ fileId, isTransient: false });
|
|
87
|
-
setIsAIChatOpen(false);
|
|
88
|
-
}
|
|
118
|
+
if (fileName && content) {
|
|
119
|
+
const fileId = getFileId(content, fileName);
|
|
120
|
+
|
|
121
|
+
if (fileId) {
|
|
122
|
+
// Open the file tab and switch to files view
|
|
123
|
+
openTab({ fileId, isTransient: false });
|
|
124
|
+
setIsAIChatOpen(false);
|
|
89
125
|
}
|
|
90
126
|
}
|
|
91
127
|
}
|
|
@@ -125,7 +161,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|
|
125
161
|
|
|
126
162
|
return (
|
|
127
163
|
<div className="diff-view">
|
|
128
|
-
<div className="diff-summary">
|
|
164
|
+
<div className="diff-summary" id="diff-summary">
|
|
129
165
|
<div className="diff-stats">
|
|
130
166
|
<span className="files-changed">
|
|
131
167
|
{unifiedDiffs.length} file
|
|
@@ -147,8 +183,15 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|
|
147
183
|
<div
|
|
148
184
|
className="diff-files"
|
|
149
185
|
ref={diffContainerRef}
|
|
186
|
+
tabIndex={-1}
|
|
187
|
+
role="region"
|
|
188
|
+
aria-label="Code diff content"
|
|
189
|
+
aria-describedby="diff-summary"
|
|
150
190
|
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
151
191
|
/>
|
|
152
192
|
</div>
|
|
153
193
|
);
|
|
154
|
-
};
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Add display name for debugging
|
|
197
|
+
DiffView.displayName = 'DiffView';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Spinner } from '../../AIAssist/Spinner';
|
|
2
|
+
|
|
3
|
+
interface AIEditingStatusIndicatorProps {
|
|
4
|
+
status: string;
|
|
5
|
+
fileName?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const AIEditingStatusIndicator: React.FC<
|
|
9
|
+
AIEditingStatusIndicatorProps
|
|
10
|
+
> = ({ status, fileName }) => {
|
|
11
|
+
const getStatusDisplay = (status: string) => {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case 'Analyzing request...':
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
🔍 <span>Analyzing request...</span>
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
case 'Formulating a plan...':
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
💭 <span>Formulating a plan...</span>
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
case 'Describing changes...':
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
📝 <span>Describing changes...</span>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
case 'Thinking...':
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
🤔 <span>Thinking...</span>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
case 'Done':
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
✅ <span>Done</span>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
default:
|
|
44
|
+
// Handle file editing status (e.g., "Editing filename.js...")
|
|
45
|
+
if (status.startsWith('Editing ') && fileName) {
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
✏️{' '}
|
|
49
|
+
<span>
|
|
50
|
+
Editing <code>{fileName}</code>...
|
|
51
|
+
</span>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
} else if (status.startsWith('Editing ')) {
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
✏️ <span>{status}</span>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return <span>{status}</span>;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const showSpinner = status !== 'Done';
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="file-editing-indicator">
|
|
69
|
+
<div className="file-editing-header">
|
|
70
|
+
{showSpinner && (
|
|
71
|
+
<div className="file-editing-icon">
|
|
72
|
+
<Spinner height={16} fadeIn={false} />
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
<div className="file-editing-text">
|
|
76
|
+
{getStatusDisplay(status)}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import * as Diff2Html from 'diff2html';
|
|
3
|
+
import 'diff2html/bundles/css/diff2html.min.css';
|
|
4
|
+
import './DiffView.scss';
|
|
5
|
+
import { generateFileUnifiedDiff } from '../../../utils/fileDiff';
|
|
6
|
+
|
|
7
|
+
interface IndividualFileDiffProps {
|
|
8
|
+
fileName: string;
|
|
9
|
+
beforeContent: string;
|
|
10
|
+
afterContent: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const IndividualFileDiff: React.FC<
|
|
14
|
+
IndividualFileDiffProps
|
|
15
|
+
> = ({ fileName, beforeContent, afterContent }) => {
|
|
16
|
+
// Generate unified diff for this single file
|
|
17
|
+
const unifiedDiff = useMemo(() => {
|
|
18
|
+
return generateFileUnifiedDiff(
|
|
19
|
+
'temp-id',
|
|
20
|
+
fileName,
|
|
21
|
+
beforeContent,
|
|
22
|
+
afterContent,
|
|
23
|
+
);
|
|
24
|
+
}, [fileName, beforeContent, afterContent]);
|
|
25
|
+
|
|
26
|
+
// Convert to HTML using diff2html
|
|
27
|
+
const diffHtml = useMemo(() => {
|
|
28
|
+
if (!unifiedDiff) return '';
|
|
29
|
+
|
|
30
|
+
return Diff2Html.html(unifiedDiff, {
|
|
31
|
+
drawFileList: false,
|
|
32
|
+
matching: 'words',
|
|
33
|
+
diffStyle: 'word',
|
|
34
|
+
outputFormat: 'line-by-line',
|
|
35
|
+
});
|
|
36
|
+
}, [unifiedDiff]);
|
|
37
|
+
|
|
38
|
+
// Calculate simple stats
|
|
39
|
+
const stats = useMemo(() => {
|
|
40
|
+
const lines = unifiedDiff.split('\n');
|
|
41
|
+
let additions = 0;
|
|
42
|
+
let deletions = 0;
|
|
43
|
+
|
|
44
|
+
lines.forEach((line) => {
|
|
45
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
46
|
+
additions++;
|
|
47
|
+
} else if (
|
|
48
|
+
line.startsWith('-') &&
|
|
49
|
+
!line.startsWith('---')
|
|
50
|
+
) {
|
|
51
|
+
deletions++;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return { additions, deletions };
|
|
56
|
+
}, [unifiedDiff]);
|
|
57
|
+
|
|
58
|
+
if (!unifiedDiff) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="diff-view" data-file={fileName}>
|
|
64
|
+
<div className="diff-summary">
|
|
65
|
+
<div className="diff-stats">
|
|
66
|
+
<span className="files-changed">{fileName}</span>
|
|
67
|
+
{stats.additions > 0 && (
|
|
68
|
+
<span className="additions">
|
|
69
|
+
+{stats.additions}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
{stats.deletions > 0 && (
|
|
73
|
+
<span className="deletions">
|
|
74
|
+
-{stats.deletions}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
className="diff-files"
|
|
82
|
+
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for the JumpToLatestButton component
|
|
5
|
+
*/
|
|
6
|
+
interface JumpToLatestButtonProps {
|
|
7
|
+
/** Whether the button is visible */
|
|
8
|
+
visible: boolean;
|
|
9
|
+
/** Callback when button is clicked */
|
|
10
|
+
onClick: () => void;
|
|
11
|
+
/** Additional CSS class name */
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Circular "down arrow" button that floats at the bottom center of scroll area
|
|
17
|
+
* Appears when auto-scroll is disabled and user is not at bottom
|
|
18
|
+
*/
|
|
19
|
+
export const JumpToLatestButton: React.FC<
|
|
20
|
+
JumpToLatestButtonProps
|
|
21
|
+
> = ({ visible, onClick, className = '' }) => {
|
|
22
|
+
/**
|
|
23
|
+
* Handle button click
|
|
24
|
+
*/
|
|
25
|
+
const handleClick = useCallback(() => {
|
|
26
|
+
onClick();
|
|
27
|
+
}, [onClick]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle keyboard events for accessibility
|
|
31
|
+
*/
|
|
32
|
+
const handleKeyDown = useCallback(
|
|
33
|
+
(event: React.KeyboardEvent) => {
|
|
34
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
onClick();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[onClick],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!visible) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
className={`jump-to-latest-button ${className}`.trim()}
|
|
49
|
+
onClick={handleClick}
|
|
50
|
+
onKeyDown={handleKeyDown}
|
|
51
|
+
aria-label="Jump to latest"
|
|
52
|
+
tabIndex={0}
|
|
53
|
+
type="button"
|
|
54
|
+
>
|
|
55
|
+
{/* Down arrow icon using Unicode or can be replaced with SVG */}
|
|
56
|
+
<span
|
|
57
|
+
className="jump-to-latest-icon"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
▼
|
|
61
|
+
</span>
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
};
|