vzcode 1.58.0 → 1.60.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.
@@ -1,230 +1,33 @@
1
1
  import {
2
- Context,
3
- createContext,
4
2
  useCallback,
5
3
  useReducer,
6
4
  useRef,
7
5
  useState,
8
6
  } from 'react';
7
+ import { VizContent } from '@vizhub/viz-types';
8
+ import { defaultTheme, useDynamicTheme } from '../themes';
9
+ import { useActions } from '../useActions';
10
+ import { useEditorCache } from '../useEditorCache';
11
+ import { useFileCRUD } from '../useFileCRUD';
12
+ import { useKeyboardShortcuts } from '../useKeyboardShortcuts';
13
+ import { useOpenDirectories } from '../useOpenDirectories';
14
+ import { usePrettier } from '../usePrettier';
15
+ import { useRunCode } from '../useRunCode';
16
+ import { useRuntimeError } from '../useRuntimeError';
17
+ import { useURLSync } from '../useURLSync';
9
18
  import {
10
- ItemId,
11
- LeafPane,
12
- Pane,
13
- PaneId,
14
- PresenceIndicator,
15
- SearchFileVisibility,
16
- SearchResults,
17
- ShareDBDoc,
18
- SubmitOperation,
19
- TabState,
20
- Username,
21
- } from '../types';
22
- import { VizFiles, VizContent } from '@vizhub/viz-types';
23
- import {
24
- ThemeLabel,
25
- defaultTheme,
26
- useDynamicTheme,
27
- } from './themes';
28
- import { useActions } from './useActions';
29
- import {
30
- EditorCache,
31
- useEditorCache,
32
- } from './useEditorCache';
33
- import { useFileCRUD } from './useFileCRUD';
34
- import { useKeyboardShortcuts } from './useKeyboardShortcuts';
35
- import { useOpenDirectories } from './useOpenDirectories';
36
- import { usePrettier } from './usePrettier';
37
- import { useRunCode } from './useRunCode';
38
- import { useRuntimeError } from './useRuntimeError';
39
- import { useURLSync } from './useURLSync';
40
- import { createInitialState, vzReducer } from './vzReducer';
41
- import { findPane } from './vzReducer/findPane';
42
- import { usePresenceAutoFollow } from './usePresenceAutoFollow';
19
+ createInitialState,
20
+ vzReducer,
21
+ } from '../vzReducer';
22
+ import { findPane } from '../vzReducer/findPane';
23
+ import { usePresenceAutoFollow } from '../usePresenceAutoFollow';
43
24
  import { v4 as uuidv4 } from 'uuid';
25
+ import {
26
+ VZCodeContextValue,
27
+ VZCodeProviderProps,
28
+ } from './types';
44
29
 
45
- // This context centralizes all the "smart" logic
46
- // to do with the application state. This includes
47
- // * Accessing and manipulating ShareDB data
48
- // * Centralized application state
49
- export const VZCodeContext =
50
- createContext<VZCodeContextValue>(null);
51
-
52
- // The type of the object provided by this context.
53
- export type VZCodeContextValue = {
54
- content: VizContent | null;
55
- shareDBDoc: ShareDBDoc<VizContent> | null;
56
- submitOperation: (
57
- next: (content: VizContent) => VizContent,
58
- ) => void;
59
- // TODO pull in this type from ShareDB if possible
60
- localPresence: any;
61
- // TODO pull in this type from ShareDB if possible
62
- docPresence: any;
63
-
64
- files: VizFiles | null;
65
- createFile: (fileName: string, text?: string) => void;
66
- renameFile: (fileId: string, fileName: string) => void;
67
- deleteFile: (fileId: string) => void;
68
- createDirectory: (
69
- fileName: string,
70
- text?: string,
71
- ) => void;
72
- renameDirectory: (
73
- directoryId: string,
74
- directoryOldName: string,
75
- directoryName: string,
76
- ) => void;
77
- deleteDirectory: (directoryId: string) => void;
78
-
79
- setActiveFileId: (fileId: string | null) => void;
80
- setActiveFileLeft: () => void;
81
- setActiveFileRight: () => void;
82
-
83
- activePaneId: PaneId;
84
- pane: Pane;
85
- activePane: LeafPane;
86
-
87
- openTab: (tabState: TabState) => void;
88
- closeTabs: (fileIds: string[]) => void;
89
-
90
- isSettingsOpen: boolean;
91
- setIsSettingsOpen: (isSettingsOpen: boolean) => void;
92
- closeSettings: () => void;
93
-
94
- isDocOpen: boolean;
95
- setIsDocOpen: (isDocOpen: boolean) => void;
96
- closeDoc: () => void;
97
-
98
- theme: ThemeLabel;
99
- setTheme: (theme: ThemeLabel) => void;
100
-
101
- username: Username;
102
- setUsername: (username: Username) => void;
103
-
104
- isDirectoryOpen: (path: string) => boolean;
105
- toggleDirectory: (path: string) => void;
106
-
107
- editorCache: EditorCache;
108
-
109
- // Whether the editor should be focused.
110
- editorWantsFocus: boolean;
111
- // Signals that the editor no longer wants focus.
112
- editorNoLongerWantsFocus: () => void;
113
-
114
- errorMessage: string | null;
115
-
116
- // Runtime error handling
117
- handleRuntimeError: (
118
- formattedErrorMessage: string,
119
- ) => void;
120
- clearRuntimeError: () => void;
121
-
122
- search: SearchResults;
123
- isSearchOpen: boolean;
124
- setIsSearchOpen: (isSearchOpen: boolean) => void;
125
- setSearch: (pattern: string) => void;
126
-
127
- isAIChatOpen: boolean;
128
- setIsAIChatOpen: (isAIChatOpen: boolean) => void;
129
- aiChatFocused: boolean;
130
- toggleAIChatFocused: () => void;
131
- aiChatMode: 'ask' | 'edit';
132
- setAIChatMode: (mode: 'ask' | 'edit') => void;
133
- aiChatEndpoint?: string;
134
- aiChatUndoEndpoint?: string;
135
- aiChatOptions?: { [key: string]: any };
136
- setSearchResults: (files: ShareDBDoc<VizContent>) => void;
137
- setSearchFileVisibility: (
138
- files: ShareDBDoc<VizContent>,
139
- id: string,
140
- visibility: SearchFileVisibility,
141
- ) => void;
142
- setSearchLineVisibility: (
143
- files: ShareDBDoc<VizContent>,
144
- id: string,
145
- line: number,
146
- ) => void;
147
- setSearchFocusedIndex: (
148
- focusedIndex: number,
149
- childIndex: number,
150
- ) => void;
151
- toggleSearchFocused: () => void;
152
-
153
- isCreateFileModalOpen: boolean;
154
- handleOpenCreateFileModal: () => void;
155
- handleCloseCreateFileModal: () => void;
156
- handleCreateFileClick: (newFileName: string) => void;
157
-
158
- isCreateDirModalOpen: boolean;
159
- handleOpenCreateDirModal: () => void;
160
- handleCloseCreateDirModal: () => void;
161
- handleCreateDirClick: (newFileName: string) => void;
162
-
163
- runPrettierRef: React.MutableRefObject<
164
- null | (() => void)
165
- >;
166
- runCodeRef: React.MutableRefObject<
167
- null | ((hardRerun?: boolean) => void)
168
- >;
169
-
170
- sidebarRef: React.RefObject<HTMLDivElement>;
171
-
172
- codeEditorRef: React.RefObject<HTMLDivElement>;
173
-
174
- connected: boolean;
175
- pending: boolean;
176
-
177
- hoveredItemId: ItemId | null;
178
- setHoveredItemId: (itemId: ItemId | null) => void;
179
-
180
- enableAutoFollow: boolean;
181
- toggleAutoFollow: () => void;
182
-
183
- updatePresenceIndicator: (
184
- presenceIndicator: PresenceIndicator,
185
- ) => void;
186
-
187
- sidebarPresenceIndicators: Array<PresenceIndicator>;
188
- splitCurrentPane: () => void;
189
-
190
- liveKitToken: string;
191
- setLiveKitToken: (state: string) => void;
192
- liveKitRoomName: string;
193
- setLiveKitRoom: (state: string) => void;
194
- liveKitConnection: boolean;
195
- setLiveKitConnection: (state: boolean) => void;
196
- voiceChatModalOpen: boolean;
197
- setVoiceChatModalOpen: (state: boolean) => void;
198
- aiChatMessage: string;
199
- setAIChatMessage: (message: string) => void;
200
- isLoading: boolean;
201
- setIsLoading: (state: boolean) => void;
202
- currentChatId: string;
203
- aiErrorMessage: string | null;
204
- setAIErrorMessage: (state: string | null) => void;
205
- handleSendMessage: (messageToSend?: string) => void;
206
-
207
- // Auto-fork functions for VizHub integration
208
- autoForkAndRetryAI?: (
209
- prompt: string,
210
- modelName: string,
211
- commitId?: string,
212
- ) => Promise<void>;
213
- clearStoredAIPrompt?: () => void;
214
- getStoredAIPrompt?: () => {
215
- prompt: string;
216
- modelName: string;
217
- } | null;
218
-
219
- // Additional widgets that can be rendered in AI chat messages
220
- additionalWidgets?: React.ComponentType<{
221
- messageId: string;
222
- chatId: string;
223
- canUndo: boolean;
224
- }>;
225
- };
226
-
227
- export const VZCodeProvider = ({
30
+ export const useVZCodeState = ({
228
31
  content,
229
32
  shareDBDoc,
230
33
  submitOperation,
@@ -232,7 +35,6 @@ export const VZCodeProvider = ({
232
35
  docPresence,
233
36
  prettierWorker,
234
37
  initialUsername,
235
- children,
236
38
  codeError = null,
237
39
  connected,
238
40
  pending,
@@ -249,44 +51,11 @@ export const VZCodeProvider = ({
249
51
  clearStoredAIPrompt,
250
52
  getStoredAIPrompt,
251
53
  additionalWidgets,
252
- }: {
253
- content: VizContent;
254
- shareDBDoc: ShareDBDoc<VizContent>;
255
- submitOperation: SubmitOperation;
256
- localPresence: any;
257
- docPresence: any;
258
- prettierWorker: Worker;
259
- initialUsername: Username;
260
- children: React.ReactNode;
261
- codeError?: string | null;
262
- connected?: boolean;
263
- pending?: boolean;
264
- liveKitToken?: string | undefined;
265
- setLiveKitToken?: (state: string) => void;
266
- liveKitRoomName?: string | undefined;
267
- setLiveKitRoom?: (state: string) => void;
268
- liveKitConnection?: boolean;
269
- setLiveKitConnection?: (state: boolean) => void;
270
- aiChatEndpoint?: string;
271
- aiChatUndoEndpoint?: string;
272
- aiChatOptions?: { [key: string]: any };
273
- autoForkAndRetryAI?: (
274
- prompt: string,
275
- modelName: string,
276
- commitId?: string,
277
- ) => Promise<void>;
278
- clearStoredAIPrompt?: () => void;
279
- getStoredAIPrompt?: () => {
280
- prompt: string;
281
- modelName: string;
282
- } | null;
283
- additionalWidgets?: React.ComponentType<{
284
- messageId: string;
285
- chatId: string;
286
- canUndo: boolean;
287
- }>;
288
- }) => {
289
- // Auto-run Pretter after local changes.
54
+ }: Omit<
55
+ VZCodeProviderProps,
56
+ 'children'
57
+ >): VZCodeContextValue => {
58
+ // Auto-run Prettier after local changes.
290
59
  const { prettierError, runPrettierRef } = usePrettier({
291
60
  shareDBDoc,
292
61
  submitOperation,
@@ -329,8 +98,6 @@ export const VZCodeProvider = ({
329
98
  );
330
99
 
331
100
  // Unpack state.
332
- // print the state object to see what it contains
333
- // console.log('state: ', state);
334
101
  const {
335
102
  pane,
336
103
  activePaneId,
@@ -348,7 +115,7 @@ export const VZCodeProvider = ({
348
115
  aiChatMode,
349
116
  } = state;
350
117
 
351
- const activePane: Pane = findPane(pane, activePaneId);
118
+ const activePane = findPane(pane, activePaneId);
352
119
  if (activePane.type !== 'leafPane') {
353
120
  // Should never happen
354
121
  throw new Error('Expected leafPane');
@@ -393,12 +160,11 @@ export const VZCodeProvider = ({
393
160
  });
394
161
 
395
162
  // The set of open directories.
396
- // TODO move this into reducer/useActions
397
163
  const { isDirectoryOpen, toggleDirectory } =
398
164
  useOpenDirectories({ activePane, content });
399
165
 
400
166
  // Cache of CodeMirror editors by file id.
401
- const editorCache: EditorCache = useEditorCache();
167
+ const editorCache = useEditorCache();
402
168
 
403
169
  // Handle dynamic theme changes.
404
170
  useDynamicTheme(editorCache, theme);
@@ -460,13 +226,10 @@ export const VZCodeProvider = ({
460
226
  );
461
227
 
462
228
  // Isolate the files object from the document.
463
- const files: VizFiles | null = content
464
- ? content.files
465
- : null;
229
+ const files = content ? content.files : null;
466
230
 
467
231
  useKeyboardShortcuts({
468
232
  closeTabs,
469
- // TODO verify that this makes sense
470
233
  activeFileId: activePane.activeFileId,
471
234
  activePaneId,
472
235
  handleOpenCreateFileModal,
@@ -481,12 +244,9 @@ export const VZCodeProvider = ({
481
244
  });
482
245
 
483
246
  // Track the currently hovered file id.
484
- const [hoveredItemId, setHoveredItemId] =
485
- useState<ItemId | null>(null);
247
+ const [hoveredItemId, setHoveredItemId] = useState(null);
486
248
 
487
249
  // Handle presence-based auto-following
488
- // This hook manages opening tabs when presence is received on files
489
- // that are not currently open, independent of CodeMirror extensions
490
250
  usePresenceAutoFollow({
491
251
  docPresence,
492
252
  enableAutoFollow,
@@ -495,7 +255,6 @@ export const VZCodeProvider = ({
495
255
  });
496
256
 
497
257
  // Livekit Voice Chat Modal
498
-
499
258
  const [voiceChatModalOpen, setVoiceChatModalOpen] =
500
259
  useState(false);
501
260
 
@@ -510,7 +269,10 @@ export const VZCodeProvider = ({
510
269
  >(null);
511
270
 
512
271
  const handleSendMessage = useCallback(
513
- async (messageToSend?: string) => {
272
+ async (
273
+ messageToSend?: string,
274
+ options?: Record<string, string>,
275
+ ) => {
514
276
  DEBUG &&
515
277
  console.log(
516
278
  'AIChat: handleSendMessage called with:',
@@ -535,7 +297,6 @@ export const VZCodeProvider = ({
535
297
  setAIErrorMessage(null); // Clear any previous errors
536
298
 
537
299
  // Call backend endpoint for AI response
538
- // The server will handle all ShareDB operations including adding the user message
539
300
  try {
540
301
  const response = await fetch(aiChatEndpoint, {
541
302
  method: 'POST',
@@ -544,10 +305,10 @@ export const VZCodeProvider = ({
544
305
  },
545
306
  body: JSON.stringify({
546
307
  ...aiChatOptions,
547
- vizId: aiChatOptions.vizId,
548
308
  content: messageContent.trim(),
549
309
  chatId: currentChatId,
550
310
  mode: aiChatMode,
311
+ ...options,
551
312
  }),
552
313
  });
553
314
 
@@ -569,7 +330,7 @@ export const VZCodeProvider = ({
569
330
 
570
331
  // Check if this is the specific permission error that should trigger auto-fork
571
332
  if (
572
- aiErrorMessage ===
333
+ errorMessage ===
573
334
  'You do not have permission to use AI chat on this visualization. Only users with edit access can use this feature. Fork the viz to edit it.'
574
335
  ) {
575
336
  // Trigger auto-fork instead of showing error
@@ -590,7 +351,7 @@ export const VZCodeProvider = ({
590
351
  }
591
352
 
592
353
  // For other errors, show the error message
593
- setAIErrorMessage(aiErrorMessage);
354
+ setAIErrorMessage(errorMessage);
594
355
  return;
595
356
  }
596
357
 
@@ -615,8 +376,8 @@ export const VZCodeProvider = ({
615
376
  ],
616
377
  );
617
378
 
618
- // The value provided by this context.
619
- const value: VZCodeContextValue = {
379
+ // Return the context value
380
+ return {
620
381
  content,
621
382
  shareDBDoc,
622
383
  submitOperation,
@@ -740,10 +501,4 @@ export const VZCodeProvider = ({
740
501
  // Additional widgets that can be rendered in AI chat messages
741
502
  additionalWidgets,
742
503
  };
743
-
744
- return (
745
- <VZCodeContext.Provider value={value}>
746
- {children}
747
- </VZCodeContext.Provider>
748
- );
749
504
  };
@@ -40,6 +40,7 @@ const MessageComponent = ({
40
40
  aiChatUndoEndpoint,
41
41
  aiChatOptions = {},
42
42
  additionalWidgets,
43
+ handleSendMessage,
43
44
  } = useContext(VZCodeContext);
44
45
 
45
46
  // Memoize date formatting to avoid repeated computation
@@ -117,6 +118,7 @@ const MessageComponent = ({
117
118
  messageId: id,
118
119
  chatId: chatId,
119
120
  canUndo: canUndo,
121
+ handleSendMessage,
120
122
  })}
121
123
  {!additionalWidgets && canUndo && beforeFiles && (
122
124
  <div className="undo-button-container">
@@ -12,13 +12,11 @@ import { VizChatMessage } from '@vizhub/viz-types';
12
12
 
13
13
  const MessageListComponent = ({
14
14
  messages,
15
- aiStatus,
16
15
  isLoading,
17
16
  chatId, // Add chatId prop
18
17
  aiScratchpad, // Add aiScratchpad prop
19
18
  }: {
20
19
  messages: VizChatMessage[];
21
- aiStatus?: string;
22
20
  isLoading: boolean;
23
21
  chatId?: string; // Add chatId to the type
24
22
  aiScratchpad?: string; // Add aiScratchpad to the type
@@ -1,4 +1,10 @@
1
- import { memo } from 'react';
1
+ import {
2
+ memo,
3
+ useRef,
4
+ useEffect,
5
+ useCallback,
6
+ useState,
7
+ } from 'react';
2
8
  import Markdown from 'react-markdown';
3
9
  import remarkGfm from 'remark-gfm';
4
10
 
@@ -11,6 +17,115 @@ const ThinkingScratchpadComponent = ({
11
17
  content,
12
18
  isVisible,
13
19
  }: 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
+
14
129
  if (!isVisible || !content) {
15
130
  return null;
16
131
  }
@@ -23,7 +138,11 @@ const ThinkingScratchpadComponent = ({
23
138
  AI is thinking...
24
139
  </span>
25
140
  </div>
26
- <div className="thinking-scratchpad-content">
141
+ <div
142
+ className="thinking-scratchpad-content"
143
+ ref={contentRef}
144
+ onScroll={handleScroll}
145
+ >
27
146
  <Markdown remarkPlugins={[remarkGfm]}>
28
147
  {content}
29
148
  </Markdown>
@@ -1,40 +1,27 @@
1
- import {
2
- useContext,
3
- useState,
4
- useCallback,
5
- useMemo,
6
- useEffect,
7
- } from 'react';
1
+ import { useContext, useMemo, useEffect } from 'react';
8
2
  import { VZCodeContext } from '../../VZCodeContext';
9
- import { v4 as uuidv4 } from 'uuid';
10
3
  import { MessageList } from './MessageList';
11
4
  import { ChatInput } from './ChatInput';
12
5
  import './styles.scss';
13
6
 
14
- const defaultAIChatEndpoint = '/api/ai-chat/';
15
-
16
7
  const DEBUG = false;
17
8
 
18
- const showSuggestedRequests = true;
9
+ const showSuggestedRequests = false;
19
10
 
20
11
  export const AIChat = () => {
21
12
  const {
22
13
  aiChatFocused,
23
14
  content,
24
- aiChatEndpoint = defaultAIChatEndpoint,
25
- aiChatOptions = {},
26
15
  aiChatMode,
27
16
  aiChatMessage,
28
17
  isLoading,
29
18
  currentChatId,
30
19
  aiErrorMessage,
31
20
  setAIChatMode,
32
- autoForkAndRetryAI,
33
21
  clearStoredAIPrompt,
34
22
  getStoredAIPrompt,
35
23
  setAIChatMessage,
36
24
  handleSendMessage,
37
- setIsLoading,
38
25
  setAIErrorMessage,
39
26
  } = useContext(VZCodeContext);
40
27
 
@@ -17,6 +17,8 @@
17
17
  overflow-y: auto;
18
18
  padding: 10px;
19
19
  padding-bottom: 0;
20
+ display: flex;
21
+ flex-direction: column;
20
22
  }
21
23
 
22
24
  .ai-chat-input-fixed {
@@ -182,6 +184,7 @@
182
184
  .undo-button-container {
183
185
  display: flex;
184
186
  justify-content: flex-end;
187
+ gap: 8px;
185
188
  margin-top: 16px;
186
189
  }
187
190
 
@@ -560,6 +563,21 @@
560
563
  max-height: 200px;
561
564
  overflow-y: auto;
562
565
  background: var(--vh-color-neutral-02);
566
+ scroll-behavior: smooth;
567
+
568
+ &::-webkit-scrollbar {
569
+ width: 6px;
570
+ }
571
+
572
+ &::-webkit-scrollbar-track {
573
+ background: var(--vh-color-neutral-02);
574
+ border-radius: 3px;
575
+ }
576
+
577
+ &::-webkit-scrollbar-thumb {
578
+ background: var(--vh-color-neutral-04);
579
+ border-radius: 3px;
580
+ }
563
581
 
564
582
  // Style markdown content in thinking scratchpad
565
583
  p {