@@ -5,21 +5,24 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
55import { Separator } from '@/components/ui/separator' ;
66import { Skeleton } from '@/components/ui/skeleton' ;
77import { Tooltip , TooltipContent , TooltipTrigger } from '@/components/ui/tooltip' ;
8- import { cn , getShortenedNumberDisplayString } from '@/lib/utils' ;
9- import { Brain , ChevronDown , ChevronRight , Clock , InfoIcon , Loader2 , List , ScanSearchIcon , Zap } from 'lucide-react' ;
10- import { memo , useCallback } from 'react' ;
118import useCaptureEvent from '@/hooks/useCaptureEvent' ;
9+ import { cn , getShortenedNumberDisplayString } from '@/lib/utils' ;
10+ import isEqual from "fast-deep-equal/react" ;
11+ import { useStickToBottom } from 'use-stick-to-bottom' ;
12+ import { Brain , ChevronDown , ChevronRight , Clock , InfoIcon , Loader2 , ScanSearchIcon , Zap } from 'lucide-react' ;
13+ import { memo , useCallback , useEffect , useState } from 'react' ;
14+ import { usePrevious } from '@uidotdev/usehooks' ;
15+ import { SBChatMessageMetadata , SBChatMessagePart } from '../../types' ;
16+ import { SearchScopeIcon } from '../searchScopeIcon' ;
1217import { MarkdownRenderer } from './markdownRenderer' ;
1318import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent' ;
1419import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent' ;
15- import { ReadFileToolComponent } from './tools/readFileToolComponent' ;
1620import { GrepToolComponent } from './tools/grepToolComponent' ;
17- import { ListReposToolComponent } from './tools/listReposToolComponent' ;
1821import { ListCommitsToolComponent } from './tools/listCommitsToolComponent' ;
22+ import { ListReposToolComponent } from './tools/listReposToolComponent' ;
1923import { ListTreeToolComponent } from './tools/listTreeToolComponent' ;
20- import { SBChatMessageMetadata , SBChatMessagePart } from '../../types' ;
21- import { SearchScopeIcon } from '../searchScopeIcon' ;
22- import isEqual from "fast-deep-equal/react" ;
24+ import { ReadFileToolComponent } from './tools/readFileToolComponent' ;
25+ import { ToolLoadingGuard } from './tools/toolLoadingGuard' ;
2326
2427
2528interface DetailsCardProps {
@@ -32,6 +35,43 @@ interface DetailsCardProps {
3235 metadata ?: SBChatMessageMetadata ;
3336}
3437
38+ const ThinkingStepsScroller = ( { thinkingSteps, isStreaming, isThinking } : { thinkingSteps : SBChatMessagePart [ ] [ ] , isStreaming : boolean , isThinking : boolean } ) => {
39+ const { scrollRef, contentRef, scrollToBottom } = useStickToBottom ( ) ;
40+ const [ shouldStick , setShouldStick ] = useState ( isThinking ) ;
41+ const prevIsThinking = usePrevious ( isThinking ) ;
42+
43+ useEffect ( ( ) => {
44+ if ( prevIsThinking && ! isThinking ) {
45+ scrollToBottom ( ) ;
46+ setShouldStick ( false ) ;
47+ } else if ( ! prevIsThinking && isThinking ) {
48+ setShouldStick ( true ) ;
49+ }
50+ } , [ isThinking , prevIsThinking , scrollToBottom ] ) ;
51+
52+ return (
53+ < div ref = { scrollRef } className = "max-h-[300px] overflow-y-auto px-6 py-2" >
54+ < div ref = { shouldStick ? contentRef : undefined } >
55+ { thinkingSteps . length === 0 ? (
56+ isStreaming ? (
57+ < Skeleton className = "h-24 w-full" />
58+ ) : (
59+ < p className = "text-sm text-muted-foreground" > No thinking steps</ p >
60+ )
61+ ) : thinkingSteps . map ( ( step , index ) => (
62+ < div key = { index } >
63+ { step . map ( ( part , index ) => (
64+ < div key = { index } className = "mb-2" >
65+ < StepPartRenderer part = { part } />
66+ </ div >
67+ ) ) }
68+ </ div >
69+ ) ) }
70+ </ div >
71+ </ div >
72+ ) ;
73+ }
74+
3575const DetailsCardComponent = ( {
3676 chatId,
3777 isExpanded,
@@ -137,10 +177,6 @@ const DetailsCardComponent = ({
137177 { Math . round ( metadata . totalResponseTimeMs / 1000 ) } seconds
138178 </ div >
139179 ) }
140- < div className = "flex items-center text-xs" >
141- < List className = "w-3 h-3 mr-1 flex-shrink-0" />
142- { `${ thinkingSteps . length } step${ thinkingSteps . length === 1 ? '' : 's' } ` }
143- </ div >
144180 </ >
145181 ) }
146182 </ div >
@@ -154,109 +190,93 @@ const DetailsCardComponent = ({
154190 </ CardContent >
155191 </ CollapsibleTrigger >
156192 < CollapsibleContent >
157- < CardContent className = "mt-2 space-y-6" >
158- { thinkingSteps . length === 0 ? (
159- isStreaming ? (
160- < Skeleton className = "h-24 w-full" />
161- ) : (
162- < p className = "text-sm text-muted-foreground" > No thinking steps</ p >
163- )
164- ) : thinkingSteps . map ( ( step , index ) => {
165- return (
166- < div
167- key = { index }
168- className = "border-l-2 pl-4 relative border-muted"
169- >
170- < div
171- className = { `absolute left-[-9px] top-1 w-4 h-4 rounded-full flex items-center justify-center bg-muted` }
172- >
173- < span
174- className = { `text-xs font-semibold` }
175- >
176- { index + 1 }
177- </ span >
178- </ div >
179- { step . map ( ( part , index ) => {
180- switch ( part . type ) {
181- case 'reasoning' :
182- case 'text' :
183- return (
184- < MarkdownRenderer
185- key = { index }
186- content = { part . text }
187- className = "text-sm"
188- />
189- )
190- case 'tool-read_file' :
191- return (
192- < ReadFileToolComponent
193- key = { index }
194- part = { part }
195- />
196- )
197- case 'tool-grep' :
198- return (
199- < GrepToolComponent
200- key = { index }
201- part = { part }
202- />
203- )
204- case 'tool-find_symbol_definitions' :
205- return (
206- < FindSymbolDefinitionsToolComponent
207- key = { index }
208- part = { part }
209- />
210- )
211- case 'tool-find_symbol_references' :
212- return (
213- < FindSymbolReferencesToolComponent
214- key = { index }
215- part = { part }
216- />
217- )
218- case 'tool-list_repos' :
219- return (
220- < ListReposToolComponent
221- key = { index }
222- part = { part }
223- />
224- )
225- case 'tool-list_commits' :
226- return (
227- < ListCommitsToolComponent
228- key = { index }
229- part = { part }
230- />
231- )
232- case 'tool-list_tree' :
233- return (
234- < ListTreeToolComponent
235- key = { index }
236- part = { part }
237- />
238- )
239- case 'data-source' :
240- case 'dynamic-tool' :
241- case 'file' :
242- case 'source-document' :
243- case 'source-url' :
244- case 'step-start' :
245- return null ;
246- default :
247- // Guarantees this switch-case to be exhaustive
248- part satisfies never ;
249- return null ;
250- }
251- } ) }
252- </ div >
253- )
254- } ) }
193+ < CardContent className = "mt-2 p-0" >
194+ < ThinkingStepsScroller
195+ thinkingSteps = { thinkingSteps }
196+ isStreaming = { isStreaming }
197+ isThinking = { isThinking }
198+ />
255199 </ CardContent >
256200 </ CollapsibleContent >
257201 </ Collapsible >
258202 </ Card >
259203 )
260204}
261205
262- export const DetailsCard = memo ( DetailsCardComponent , isEqual ) ;
206+ export const DetailsCard = memo ( DetailsCardComponent , isEqual ) ;
207+
208+
209+ export const StepPartRenderer = ( { part } : { part : SBChatMessagePart } ) => {
210+ switch ( part . type ) {
211+ case 'reasoning' :
212+ case 'text' :
213+ return (
214+ < MarkdownRenderer
215+ content = { part . text }
216+ className = "text-sm prose-p:m-0 prose-code:text-xs"
217+ />
218+ )
219+ case 'tool-read_file' :
220+ return (
221+ < ToolLoadingGuard
222+ part = { part }
223+ loadingText = "Reading file..."
224+ >
225+ { ( output ) => < ReadFileToolComponent { ...output } /> }
226+ </ ToolLoadingGuard >
227+ )
228+ case 'tool-grep' :
229+ return (
230+ < ToolLoadingGuard
231+ part = { part }
232+ loadingText = { 'Searching...' }
233+ >
234+ { ( output ) => < GrepToolComponent { ...output } /> }
235+ </ ToolLoadingGuard >
236+ )
237+ case 'tool-find_symbol_definitions' :
238+ return (
239+ < FindSymbolDefinitionsToolComponent
240+ part = { part }
241+ />
242+ )
243+ case 'tool-find_symbol_references' :
244+ return (
245+ < FindSymbolReferencesToolComponent
246+ part = { part }
247+ />
248+ )
249+ case 'tool-list_repos' :
250+ return (
251+ < ListReposToolComponent
252+ part = { part }
253+ />
254+ )
255+ case 'tool-list_commits' :
256+ return (
257+ < ListCommitsToolComponent
258+ part = { part }
259+ />
260+ )
261+ case 'tool-list_tree' :
262+ return (
263+ < ToolLoadingGuard
264+ part = { part }
265+ loadingText = "Listing tree..."
266+ >
267+ { ( output ) => < ListTreeToolComponent { ...output } /> }
268+ </ ToolLoadingGuard >
269+ )
270+ case 'data-source' :
271+ case 'dynamic-tool' :
272+ case 'file' :
273+ case 'source-document' :
274+ case 'source-url' :
275+ case 'step-start' :
276+ return null ;
277+ default :
278+ // Guarantees this switch-case to be exhaustive
279+ part satisfies never ;
280+ return null ;
281+ }
282+ }
0 commit comments