Skip to content

Commit b3f768d

Browse files
further wip
1 parent addb243 commit b3f768d

26 files changed

+570
-325
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Fixed
1616
- Fixed issue where ask responses would sometimes appear in the details panel while generating. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)
1717

18+
### Changed
19+
- Changed the `webUrl` property of the `/api/repos` api to return a URL rather than just a path. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)
20+
1821
## [4.15.9] - 2026-03-17
1922

2023
### Added

CLAUDE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ Exceptions:
3838
- Special files like `README.md`, `CHANGELOG.md`, `LICENSE`
3939
- Next.js conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, etc.
4040

41+
## Code Style
42+
43+
Always use curly braces for `if` statements, with the body on a new line — even for single-line bodies:
44+
45+
```ts
46+
// Correct
47+
if (!value) {
48+
return;
49+
}
50+
if (condition) {
51+
doSomething();
52+
}
53+
54+
// Incorrect
55+
if (!value) return;
56+
if (!value) { return; }
57+
if (condition) doSomething();
58+
```
59+
4160
## Tailwind CSS
4261

4362
Use Tailwind color classes directly instead of CSS variable syntax:

packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
"stripe": "^17.6.0",
191191
"tailwind-merge": "^2.5.2",
192192
"tailwindcss-animate": "^1.0.7",
193+
"use-stick-to-bottom": "^1.1.3",
193194
"usehooks-ts": "^3.1.0",
194195
"vscode-icons-js": "^11.6.1",
195196
"zod": "^3.25.74",

packages/web/src/features/chat/components/chatThread/detailsCard.tsx

Lines changed: 131 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,24 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
55
import { Separator } from '@/components/ui/separator';
66
import { Skeleton } from '@/components/ui/skeleton';
77
import { 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';
118
import 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';
1217
import { MarkdownRenderer } from './markdownRenderer';
1318
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
1419
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
15-
import { ReadFileToolComponent } from './tools/readFileToolComponent';
1620
import { GrepToolComponent } from './tools/grepToolComponent';
17-
import { ListReposToolComponent } from './tools/listReposToolComponent';
1821
import { ListCommitsToolComponent } from './tools/listCommitsToolComponent';
22+
import { ListReposToolComponent } from './tools/listReposToolComponent';
1923
import { 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

2528
interface 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+
3575
const 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+
}

packages/web/src/features/chat/components/chatThread/tools/findSymbolDefinitionsToolComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const FindSymbolDefinitionsToolComponent = ({ part }: { part: FindSymbolD
2525
}, [part]);
2626

2727
return (
28-
<div className="my-4">
28+
<div>
2929
<ToolHeader
3030
isLoading={part.state !== 'output-available' && part.state !== 'output-error'}
3131
isError={part.state === 'output-error'}

packages/web/src/features/chat/components/chatThread/tools/findSymbolReferencesToolComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const FindSymbolReferencesToolComponent = ({ part }: { part: FindSymbolRe
2525
}, [part]);
2626

2727
return (
28-
<div className="my-4">
28+
<div>
2929
<ToolHeader
3030
isLoading={part.state !== 'output-available' && part.state !== 'output-error'}
3131
isError={part.state === 'output-error'}

0 commit comments

Comments
 (0)