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.
Files changed (45) hide show
  1. package/dist/assets/{index-BpUpNto4.js → index-C2BLPyQP.js} +138 -125
  2. package/dist/assets/{index-DLm5FQ0E.css → index-DVR90LwF.css} +1 -1
  3. package/dist/index.html +2 -2
  4. package/dist/server/aiChatHandler/chatOperations.js +168 -35
  5. package/dist/server/aiChatHandler/index.js +11 -30
  6. package/dist/server/aiChatHandler/llmStreaming.js +105 -118
  7. package/package.json +11 -11
  8. package/src/client/AIAssist/AIAssistWidget/index.tsx +12 -1
  9. package/src/client/App/useShareDB.ts +1 -1
  10. package/src/client/CodeEditor/index.tsx +9 -10
  11. package/src/client/VZCodeContext/types.ts +3 -1
  12. package/src/client/VZCodeContext/useVZCodeState.ts +7 -5
  13. package/src/client/VZRight.tsx +10 -2
  14. package/src/client/VZSidebar/AIChat/ChatInput.tsx +1 -7
  15. package/src/client/VZSidebar/AIChat/DiffView.scss +1 -0
  16. package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -29
  17. package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +81 -0
  18. package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +86 -0
  19. package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +64 -0
  20. package/src/client/VZSidebar/AIChat/Message.tsx +68 -53
  21. package/src/client/VZSidebar/AIChat/MessageList.tsx +139 -118
  22. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +63 -21
  23. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +4 -118
  24. package/src/client/VZSidebar/AIChat/index.tsx +62 -19
  25. package/src/client/VZSidebar/AIChat/styles.scss +159 -16
  26. package/src/client/VZSidebar/Item.tsx +2 -2
  27. package/src/client/VZSidebar/Search.tsx +13 -6
  28. package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +39 -23
  29. package/src/client/VZSidebar/VisualEditor/utils.ts +1 -1
  30. package/src/client/VZSidebar/index.tsx +12 -10
  31. package/src/client/VZSidebar/useDragAndDrop.tsx +225 -214
  32. package/src/client/featureFlags.ts +3 -0
  33. package/src/client/hooks/useAutoScroll.ts +329 -0
  34. package/src/client/tabsSearchParameters.ts +1 -17
  35. package/src/client/useFileCRUD.ts +5 -2
  36. package/src/client/useKeyboardShortcuts.ts +7 -0
  37. package/src/client/useOpenDirectories.ts +2 -1
  38. package/src/client/usePrettier/index.ts +1 -1
  39. package/src/client/useURLSync.ts +1 -0
  40. package/src/client/utils/scrollUtils.ts +170 -0
  41. package/src/client/vzReducer/searchReducer.ts +6 -3
  42. package/src/server/aiChatHandler/chatOperations.ts +224 -43
  43. package/src/server/aiChatHandler/index.ts +8 -48
  44. package/src/server/aiChatHandler/llmStreaming.ts +149 -170
  45. package/src/types.ts +81 -1
@@ -1,10 +1,4 @@
1
- import {
2
- memo,
3
- useRef,
4
- useEffect,
5
- useCallback,
6
- useState,
7
- } from 'react';
1
+ import { memo } from 'react';
8
2
  import Markdown from 'react-markdown';
9
3
  import remarkGfm from 'remark-gfm';
10
4
 
@@ -17,115 +11,6 @@ const ThinkingScratchpadComponent = ({
17
11
  content,
18
12
  isVisible,
19
13
  }: ThinkingScratchpadProps) => {
20
- const contentRef = useRef<HTMLDivElement>(null);
21
- const [isUserScrolled, setIsUserScrolled] =
22
- useState(false);
23
- const [autoScrollEnabled, setAutoScrollEnabled] =
24
- useState(true);
25
- const scrollTimeoutRef =
26
- useRef<ReturnType<typeof setTimeout>>();
27
-
28
- // Check if the user is scrolled to the bottom
29
- const isScrolledToBottom = useCallback(() => {
30
- const container = contentRef.current;
31
- if (!container) return true;
32
-
33
- const threshold = 10; // Allow 10px tolerance for "at bottom"
34
- const { scrollTop, scrollHeight, clientHeight } =
35
- container;
36
- return (
37
- scrollHeight - scrollTop - clientHeight < threshold
38
- );
39
- }, []);
40
-
41
- // Smooth scroll to bottom
42
- const scrollToBottom = useCallback(() => {
43
- if (!autoScrollEnabled) return;
44
-
45
- const container = contentRef.current;
46
- if (container) {
47
- container.scrollTo({
48
- top: container.scrollHeight,
49
- behavior: 'smooth',
50
- });
51
- }
52
- }, [autoScrollEnabled]);
53
-
54
- // Handle scroll events to detect user manual scrolling
55
- const handleScroll = useCallback(() => {
56
- const container = contentRef.current;
57
- if (!container) return;
58
-
59
- const isAtBottom = isScrolledToBottom();
60
-
61
- // Clear any existing timeout
62
- if (scrollTimeoutRef.current) {
63
- clearTimeout(scrollTimeoutRef.current);
64
- }
65
-
66
- // If user scrolled up from bottom, disable auto-scroll
67
- if (!isAtBottom && !isUserScrolled) {
68
- setIsUserScrolled(true);
69
- setAutoScrollEnabled(false);
70
- }
71
-
72
- // If user scrolled back to bottom, re-enable auto-scroll after a brief delay
73
- if (isAtBottom && isUserScrolled) {
74
- scrollTimeoutRef.current = setTimeout(() => {
75
- setIsUserScrolled(false);
76
- setAutoScrollEnabled(true);
77
- }, 300); // 300ms delay to prevent flickering
78
- }
79
- }, [isUserScrolled, isScrolledToBottom]);
80
-
81
- // Auto-scroll when content changes, but only if auto-scroll is enabled
82
- useEffect(() => {
83
- if (
84
- autoScrollEnabled &&
85
- !isUserScrolled &&
86
- isVisible &&
87
- content
88
- ) {
89
- // Use a short debounce to prevent multiple scroll calls during rapid updates
90
- if (scrollTimeoutRef.current) {
91
- clearTimeout(scrollTimeoutRef.current);
92
- }
93
-
94
- scrollTimeoutRef.current = setTimeout(() => {
95
- scrollToBottom();
96
- }, 50); // 50ms debounce
97
- }
98
-
99
- return () => {
100
- if (scrollTimeoutRef.current) {
101
- clearTimeout(scrollTimeoutRef.current);
102
- }
103
- };
104
- }, [
105
- content,
106
- autoScrollEnabled,
107
- isUserScrolled,
108
- isVisible,
109
- scrollToBottom,
110
- ]);
111
-
112
- // Clean up timeout on unmount
113
- useEffect(() => {
114
- return () => {
115
- if (scrollTimeoutRef.current) {
116
- clearTimeout(scrollTimeoutRef.current);
117
- }
118
- };
119
- }, []);
120
-
121
- // Reset scroll state when component becomes visible
122
- useEffect(() => {
123
- if (isVisible) {
124
- setIsUserScrolled(false);
125
- setAutoScrollEnabled(true);
126
- }
127
- }, [isVisible]);
128
-
129
14
  if (!isVisible || !content) {
130
15
  return null;
131
16
  }
@@ -140,8 +25,9 @@ const ThinkingScratchpadComponent = ({
140
25
  </div>
141
26
  <div
142
27
  className="thinking-scratchpad-content"
143
- ref={contentRef}
144
- onScroll={handleScroll}
28
+ role="log"
29
+ aria-live="polite"
30
+ aria-relevant="additions"
145
31
  >
146
32
  <Markdown remarkPlugins={[remarkGfm]}>
147
33
  {content}
@@ -7,6 +7,9 @@ import {
7
7
  import { VZCodeContext } from '../../VZCodeContext';
8
8
  import { MessageList } from './MessageList';
9
9
  import { ChatInput } from './ChatInput';
10
+ import { ExtendedVizChat } from '../../../types.js';
11
+ import { useAutoScroll } from '../../hooks/useAutoScroll';
12
+ import { JumpToLatestButton } from './JumpToLatestButton';
10
13
  import './styles.scss';
11
14
 
12
15
  const DEBUG = false;
@@ -50,13 +53,11 @@ export const AIChat = () => {
50
53
  content,
51
54
  aiChatMode,
52
55
  aiChatMessage,
53
- isLoading,
54
56
  currentChatId,
55
57
  selectedChatId,
56
58
  setSelectedChatId,
57
59
  aiErrorMessage,
58
60
  setAIChatMode,
59
- clearStoredAIPrompt,
60
61
  getStoredAIPrompt,
61
62
  setAIChatMessage,
62
63
  handleSendMessage,
@@ -64,17 +65,45 @@ export const AIChat = () => {
64
65
  navigateMessageHistoryUp,
65
66
  navigateMessageHistoryDown,
66
67
  resetMessageHistoryNavigation,
68
+ enableMinimalEditFlow,
67
69
  } = useContext(VZCodeContext);
68
70
 
71
+ // Use the new simplified auto-scroll hook
72
+ const {
73
+ containerRef: messagesContainerRef,
74
+ showJumpButton,
75
+ onNewEvent,
76
+ onJumpToLatest,
77
+ beforeRender,
78
+ afterRender,
79
+ } = useAutoScroll({ threshold: 24 });
80
+
81
+ // TODO fix this - currently always false
82
+ console.log('showJumpButton:', showJumpButton);
83
+
69
84
  // Get the active chat ID and chat data
70
85
  // If selectedChatId is set, use it; otherwise, if no chat selected, don't default to currentChatId
71
86
  const activeChatId = selectedChatId;
72
87
  const currentChat = activeChatId
73
88
  ? content?.chats?.[activeChatId]
74
89
  : null;
75
- const rawMessages = currentChat?.messages || [];
90
+ const rawMessages = useMemo(
91
+ () => currentChat?.messages || [],
92
+ [currentChat?.messages],
93
+ );
76
94
  const aiStatus = currentChat?.aiStatus;
77
95
  const aiScratchpad = currentChat?.aiScratchpad;
96
+ const currentStatus = (currentChat as ExtendedVizChat)
97
+ ?.currentStatus;
98
+
99
+ // Debug logging for AI status
100
+ DEBUG && console.log('AIChat: currentChat:', currentChat);
101
+ DEBUG && console.log('AIChat: aiStatus:', aiStatus);
102
+ DEBUG &&
103
+ console.log(
104
+ 'AIChat: enableMinimalEditFlow:',
105
+ enableMinimalEditFlow,
106
+ );
78
107
 
79
108
  // Transform messages to ensure they have required id field - memoized to avoid recreation
80
109
  const messages = useMemo(
@@ -166,7 +195,10 @@ export const AIChat = () => {
166
195
  return (
167
196
  <div className="ai-chat-container">
168
197
  <div className="ai-chat-content">
169
- <div className="ai-chat-messages-container">
198
+ <div
199
+ className="ai-chat-messages-container"
200
+ ref={messagesContainerRef}
201
+ >
170
202
  {isEmptyState ? (
171
203
  <div className="ai-chat-empty">
172
204
  <div className="ai-chat-empty-icon">✨</div>
@@ -198,7 +230,7 @@ export const AIChat = () => {
198
230
  )
199
231
  }
200
232
  >
201
- "Explain how this works"
233
+ &quot;Explain how this works&quot;
202
234
  </button>
203
235
  <button
204
236
  className="ai-chat-suggested-prompt"
@@ -208,8 +240,8 @@ export const AIChat = () => {
208
240
  )
209
241
  }
210
242
  >
211
- "How could I change it so that the
212
- circles are bigger?"
243
+ &quot;How could I change it so
244
+ that the circles are bigger?&quot;
213
245
  </button>
214
246
  <button
215
247
  className="ai-chat-suggested-prompt"
@@ -219,7 +251,8 @@ export const AIChat = () => {
219
251
  )
220
252
  }
221
253
  >
222
- "What does this function do?"
254
+ &quot;What does this function
255
+ do?&quot;
223
256
  </button>
224
257
  <button
225
258
  className="ai-chat-suggested-prompt"
@@ -229,8 +262,8 @@ export const AIChat = () => {
229
262
  )
230
263
  }
231
264
  >
232
- "How can I make this more
233
- accessible?"
265
+ &quot;How can I make this more
266
+ accessible?&quot;
234
267
  </button>
235
268
  </div>
236
269
  </>
@@ -246,7 +279,8 @@ export const AIChat = () => {
246
279
  )
247
280
  }
248
281
  >
249
- "Change the circles to squares"
282
+ &quot;Change the circles to
283
+ squares&quot;
250
284
  </button>
251
285
  <button
252
286
  className="ai-chat-suggested-prompt"
@@ -256,8 +290,8 @@ export const AIChat = () => {
256
290
  )
257
291
  }
258
292
  >
259
- "Add a button that toggles the
260
- animation"
293
+ &quot;Add a button that toggles
294
+ the animation&quot;
261
295
  </button>
262
296
  <button
263
297
  className="ai-chat-suggested-prompt"
@@ -267,8 +301,8 @@ export const AIChat = () => {
267
301
  )
268
302
  }
269
303
  >
270
- "Fix the CSS so the layout is
271
- responsive"
304
+ &quot;Fix the CSS so the layout is
305
+ responsive&quot;
272
306
  </button>
273
307
  <button
274
308
  className="ai-chat-suggested-prompt"
@@ -278,8 +312,8 @@ export const AIChat = () => {
278
312
  )
279
313
  }
280
314
  >
281
- "Refactor this function to use
282
- async/await"
315
+ &quot;Refactor this function to
316
+ use async/await&quot;
283
317
  </button>
284
318
  </div>
285
319
  </>
@@ -297,9 +331,14 @@ export const AIChat = () => {
297
331
  ) : (
298
332
  <MessageList
299
333
  messages={messages}
300
- isLoading={isLoading}
334
+ isLoading={false}
301
335
  chatId={selectedChatId || currentChatId}
302
336
  aiScratchpad={aiScratchpad}
337
+ currentStatus={currentStatus}
338
+ onNewEvent={onNewEvent}
339
+ onJumpToLatest={onJumpToLatest}
340
+ beforeRender={beforeRender}
341
+ afterRender={afterRender}
303
342
  />
304
343
  )}
305
344
  {aiErrorMessage && (
@@ -321,6 +360,11 @@ export const AIChat = () => {
321
360
  </div>
322
361
  </div>
323
362
  )}
363
+ {/* Jump to Latest Button */}
364
+ <JumpToLatestButton
365
+ visible={showJumpButton}
366
+ onClick={onJumpToLatest}
367
+ />
324
368
  </div>
325
369
  </div>
326
370
  <div className="ai-chat-input-fixed">
@@ -328,7 +372,6 @@ export const AIChat = () => {
328
372
  aiChatMessage={aiChatMessage}
329
373
  setAIChatMessage={setAIChatMessage}
330
374
  onSendMessage={handleSendMessageWithSelection}
331
- isLoading={isLoading}
332
375
  focused={aiChatFocused}
333
376
  aiChatMode={aiChatMode}
334
377
  setAIChatMode={setAIChatMode}
@@ -10,6 +10,7 @@
10
10
  overflow: hidden;
11
11
  display: flex;
12
12
  flex-direction: column;
13
+ position: relative;
13
14
  }
14
15
 
15
16
  .ai-chat-messages-container {
@@ -19,6 +20,21 @@
19
20
  padding-bottom: 0;
20
21
  display: flex;
21
22
  flex-direction: column;
23
+ scroll-behavior: smooth;
24
+
25
+ &::-webkit-scrollbar {
26
+ width: 6px;
27
+ }
28
+
29
+ &::-webkit-scrollbar-track {
30
+ background: var(--vh-color-neutral-02);
31
+ border-radius: 3px;
32
+ }
33
+
34
+ &::-webkit-scrollbar-thumb {
35
+ background: var(--vh-color-neutral-04);
36
+ border-radius: 3px;
37
+ }
22
38
  }
23
39
 
24
40
  .ai-chat-input-fixed {
@@ -111,6 +127,94 @@
111
127
  }
112
128
  }
113
129
 
130
+ // ============================================================================
131
+ // Streaming Message Components
132
+ // ============================================================================
133
+
134
+ .streaming-message {
135
+ .text-chunk {
136
+ margin-bottom: 1rem;
137
+ }
138
+ }
139
+
140
+ .file-editing-indicator {
141
+ display: flex;
142
+ align-items: center;
143
+ padding: 0.75rem;
144
+ margin: 0.5rem 0;
145
+ background-color: var(--vscode-editor-background);
146
+ border: 1px solid var(--vscode-panel-border);
147
+ border-radius: 4px;
148
+
149
+ .file-editing-header {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 0.5rem;
153
+ }
154
+
155
+ .file-editing-icon {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ }
160
+
161
+ .file-editing-text {
162
+ color: var(--vscode-foreground);
163
+ font-size: 0.9rem;
164
+
165
+ code {
166
+ background-color: var(
167
+ --vscode-textCodeBlock-background
168
+ );
169
+ padding: 0.2rem 0.4rem;
170
+ border-radius: 3px;
171
+ font-family: var(--vscode-editor-font-family);
172
+ }
173
+ }
174
+ }
175
+
176
+ .status-indicator {
177
+ display: flex;
178
+ align-items: center;
179
+ padding: 0.5rem;
180
+ margin: 0.5rem 0;
181
+ background-color: var(--vscode-notifications-background);
182
+ border-left: 3px solid
183
+ var(--vscode-progressBar-foreground);
184
+ border-radius: 0 4px 4px 0;
185
+
186
+ .status-content {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 0.5rem;
190
+ }
191
+
192
+ .status-icon {
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+
197
+ .done-icon {
198
+ font-size: 1rem;
199
+ }
200
+ }
201
+
202
+ .status-text {
203
+ color: var(--vscode-foreground);
204
+ font-size: 0.9rem;
205
+ font-weight: 500;
206
+ }
207
+ }
208
+
209
+ @keyframes spin {
210
+ 0% {
211
+ transform: rotate(0deg);
212
+ }
213
+ 100% {
214
+ transform: rotate(360deg);
215
+ }
216
+ }
217
+
114
218
  .ai-chat-list {
115
219
  margin-top: 20px;
116
220
 
@@ -126,24 +230,8 @@
126
230
 
127
231
  .ai-chat-messages {
128
232
  flex: 1;
129
- overflow-y: auto;
130
233
  margin-bottom: 10px;
131
234
  padding-right: 5px;
132
- scroll-behavior: smooth;
133
-
134
- &::-webkit-scrollbar {
135
- width: 6px;
136
- }
137
-
138
- &::-webkit-scrollbar-track {
139
- background: var(--vh-color-neutral-02);
140
- border-radius: 3px;
141
- }
142
-
143
- &::-webkit-scrollbar-thumb {
144
- background: var(--vh-color-neutral-04);
145
- border-radius: 3px;
146
- }
147
235
  }
148
236
 
149
237
  .ai-chat-message {
@@ -549,6 +637,61 @@
549
637
  }
550
638
  }
551
639
 
640
+ // Jump to latest button styles
641
+ .jump-to-latest-button {
642
+ position: absolute;
643
+ left: 50%;
644
+ transform: translateX(-50%);
645
+ bottom: 20px; // Position above the input area
646
+ width: 44px;
647
+ height: 44px;
648
+ border-radius: 50%;
649
+ background: var(--vh-color-primary-01);
650
+ border: none;
651
+ color: white;
652
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
653
+ cursor: pointer;
654
+ display: flex;
655
+ align-items: center;
656
+ justify-content: center;
657
+ z-index: 10;
658
+ transition: all 0.2s ease;
659
+
660
+ &:hover {
661
+ background: var(--vh-color-primary-02);
662
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
663
+ transform: translateX(-50%) scale(1.05);
664
+ }
665
+
666
+ &:focus {
667
+ outline: 2px solid var(--vh-color-primary-01);
668
+ outline-offset: 2px;
669
+ }
670
+
671
+ &:active {
672
+ transform: translateX(-50%) scale(0.95);
673
+ }
674
+
675
+ .jump-to-latest-icon {
676
+ font-size: 16px;
677
+ font-weight: bold;
678
+ line-height: 1;
679
+ }
680
+
681
+ // Respect prefers-reduced-motion
682
+ @media (prefers-reduced-motion: reduce) {
683
+ transition: none;
684
+
685
+ &:hover {
686
+ transform: translateX(-50%);
687
+ }
688
+
689
+ &:active {
690
+ transform: translateX(-50%);
691
+ }
692
+ }
693
+ }
694
+
552
695
  // Thinking scratchpad styles
553
696
  .thinking-scratchpad {
554
697
  background: var(--vh-color-neutral-02);
@@ -140,11 +140,11 @@ export const Item = ({
140
140
 
141
141
  const handleMouseEnter = useCallback(() => {
142
142
  setIsHovered(true);
143
- }, []);
143
+ }, [setIsHovered]);
144
144
 
145
145
  const handleMouseLeave = useCallback(() => {
146
146
  setIsHovered(false);
147
- }, []);
147
+ }, [setIsHovered]);
148
148
 
149
149
  const onChange = useCallback(() => {
150
150
  setRenameValue(renameInputRef.current.value);
@@ -98,7 +98,7 @@ export const Search = () => {
98
98
  } else {
99
99
  setIsMounted(true);
100
100
  }
101
- }, [pattern]);
101
+ }, [pattern, isMounted, setSearchResults, shareDBDoc]);
102
102
 
103
103
  const flattenResult = useCallback(
104
104
  (fileId: string, file: SearchFile) => {
@@ -111,19 +111,26 @@ export const Search = () => {
111
111
  : 'open',
112
112
  );
113
113
  },
114
- [focusedIndex, focusedChildIndex],
114
+ [
115
+ setSearchFileVisibility,
116
+ shareDBDoc,
117
+ focusedChildIndex,
118
+ ],
115
119
  );
116
120
 
117
- const closeResult = useCallback((fileId: VizFileId) => {
118
- setSearchFileVisibility(shareDBDoc, fileId, 'closed');
119
- }, []);
121
+ const closeResult = useCallback(
122
+ (fileId: VizFileId) => {
123
+ setSearchFileVisibility(shareDBDoc, fileId, 'closed');
124
+ },
125
+ [setSearchFileVisibility, shareDBDoc],
126
+ );
120
127
 
121
128
  const focusFileElement = useCallback(
122
129
  (fileId: VizFileId, index: number) => {
123
130
  setActiveFileId(fileId);
124
131
  setSearchFocusedIndex(index, null);
125
132
  },
126
- [],
133
+ [setActiveFileId, setSearchFocusedIndex],
127
134
  );
128
135
 
129
136
  const handleKeyDown = (event) => {