Skip to content

Commit 211ad8f

Browse files
feat(ask_sb): Improved search tool ; tools for listing / searching repos ; removed search scope constraint (#400)
* wip * Add additional tools for repo searching and listing * Remove search scope constraint * Only show the selected search scopes when there is > 0 * changelog * fix build
1 parent 4343b3c commit 211ad8f

File tree

16 files changed

+490
-96
lines changed

16 files changed

+490
-96
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405)
1414
- Add GitHub star toast. [#409](https://github.com/sourcebot-dev/sourcebot/pull/409)
1515
- Added a onboarding modal when first visiting the homepage when `ask` mode is selected. [#408](https://github.com/sourcebot-dev/sourcebot/pull/408)
16+
- [ask sb] Added `searchReposTool` and `listAllReposTool`. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400)
1617

1718
### Fixed
1819
- Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398)
@@ -21,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2122
- Bumped AI SDK and associated packages version. [#404](https://github.com/sourcebot-dev/sourcebot/pull/404)
2223
- Bumped form-data package version. [#407](https://github.com/sourcebot-dev/sourcebot/pull/407)
2324
- Bumped next version. [#406](https://github.com/sourcebot-dev/sourcebot/pull/406)
25+
- [ask sb] Improved search code tool with filter options. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400)
26+
- [ask sb] Removed search scope constraint. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400)
2427

2528
## [4.6.0] - 2025-07-25
2629

packages/web/src/app/[domain]/chat/components/newChatPanel.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export const NewChatPanel = ({
5151
languageModels={languageModels}
5252
selectedSearchScopes={selectedSearchScopes}
5353
searchContexts={searchContexts}
54-
onContextSelectorOpenChanged={setIsContextSelectorOpen}
5554
/>
5655
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
5756
<ChatBoxToolbar

packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export const AgenticSearch = ({
5858
languageModels={languageModels}
5959
selectedSearchScopes={selectedSearchScopes}
6060
searchContexts={searchContexts}
61-
onContextSelectorOpenChanged={setIsContextSelectorOpen}
6261
/>
6362
<Separator />
6463
<div className="relative">

packages/web/src/features/chat/agent.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ProviderOptions } from "@ai-sdk/provider-utils";
66
import { createLogger } from "@sourcebot/logger";
77
import { LanguageModel, ModelMessage, StopCondition, streamText } from "ai";
88
import { ANSWER_TAG, FILE_REFERENCE_PREFIX, toolNames } from "./constants";
9-
import { createCodeSearchTool, findSymbolDefinitionsTool, findSymbolReferencesTool, readFilesTool } from "./tools";
9+
import { createCodeSearchTool, findSymbolDefinitionsTool, findSymbolReferencesTool, readFilesTool, searchReposTool, listAllReposTool } from "./tools";
1010
import { FileSource, Source } from "./types";
1111
import { addLineNumbers, fileReferenceToString } from "./utils";
1212

@@ -54,6 +54,8 @@ export const createAgentStream = async ({
5454
[toolNames.readFiles]: readFilesTool,
5555
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
5656
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
57+
[toolNames.searchRepos]: searchReposTool,
58+
[toolNames.listAllRepos]: listAllReposTool,
5759
},
5860
prepareStep: async ({ stepNumber }) => {
5961
// The first step attaches any mentioned sources to the system prompt.
@@ -185,13 +187,7 @@ ${searchScopeRepoNames.map(repo => `- ${repo}`).join('\n')}
185187
</available_repositories>
186188
187189
<research_phase_instructions>
188-
During the research phase, you have these tools available:
189-
- \`${toolNames.searchCode}\`: Search for code patterns, functions, or text across repositories
190-
- \`${toolNames.readFiles}\`: Read the contents of specific files
191-
- \`${toolNames.findSymbolReferences}\`: Find where symbols are referenced
192-
- \`${toolNames.findSymbolDefinitions}\`: Find where symbols are defined
193-
194-
Use these tools to gather comprehensive context before answering. Always explain why you're using each tool.
190+
During the research phase, use the tools available to you to gather comprehensive context before answering. Always explain why you're using each tool. Depending on the user's question, you may need to use multiple tools. If the question is vague, ask the user for more information.
195191
</research_phase_instructions>
196192
197193
${answerInstructions}

packages/web/src/features/chat/components/chatBox/chatBox.tsx

Lines changed: 15 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { Button } from "@/components/ui/button";
55
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
66
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
77
import { insertMention, slateContentToString } from "@/features/chat/utils";
8+
import { SearchContextQuery } from "@/lib/types";
89
import { cn, IS_MAC } from "@/lib/utils";
910
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
10-
import { ArrowUp, Loader2, StopCircleIcon, TriangleAlertIcon } from "lucide-react";
11+
import { ArrowUp, Loader2, StopCircleIcon } from "lucide-react";
1112
import { Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
1213
import { useHotkeys } from "react-hotkeys-hook";
1314
import { Descendant, insertText } from "slate";
@@ -17,8 +18,6 @@ import { SuggestionBox } from "./suggestionsBox";
1718
import { Suggestion } from "./types";
1819
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
1920
import { useSuggestionsData } from "./useSuggestionsData";
20-
import { useToast } from "@/components/hooks/use-toast";
21-
import { SearchContextQuery } from "@/lib/types";
2221

2322
interface ChatBoxProps {
2423
onSubmit: (children: Descendant[], editor: CustomEditor) => void;
@@ -30,7 +29,6 @@ interface ChatBoxProps {
3029
languageModels: LanguageModelInfo[];
3130
selectedSearchScopes: SearchScope[];
3231
searchContexts: SearchContextQuery[];
33-
onContextSelectorOpenChanged: (isOpen: boolean) => void;
3432
}
3533

3634
export const ChatBox = ({
@@ -43,7 +41,6 @@ export const ChatBox = ({
4341
languageModels,
4442
selectedSearchScopes,
4543
searchContexts,
46-
onContextSelectorOpenChanged,
4744
}: ChatBoxProps) => {
4845
const suggestionsBoxRef = useRef<HTMLDivElement>(null);
4946
const [index, setIndex] = useState(0);
@@ -70,7 +67,6 @@ export const ChatBox = ({
7067
const { selectedLanguageModel } = useSelectedLanguageModel({
7168
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
7269
});
73-
const { toast } = useToast();
7470

7571
// Reset the index when the suggestion mode changes.
7672
useEffect(() => {
@@ -101,9 +97,9 @@ export const ChatBox = ({
10197
return <Leaf {...props} />
10298
}, []);
10399

104-
const { isSubmitDisabled, isSubmitDisabledReason } = useMemo((): {
100+
const { isSubmitDisabled } = useMemo((): {
105101
isSubmitDisabled: true,
106-
isSubmitDisabledReason: "empty" | "redirecting" | "generating" | "no-repos-selected" | "no-language-model-selected"
102+
isSubmitDisabledReason: "empty" | "redirecting" | "generating" | "no-language-model-selected"
107103
} | {
108104
isSubmitDisabled: false,
109105
isSubmitDisabledReason: undefined,
@@ -129,13 +125,6 @@ export const ChatBox = ({
129125
}
130126
}
131127

132-
if (selectedSearchScopes.length === 0) {
133-
return {
134-
isSubmitDisabled: true,
135-
isSubmitDisabledReason: "no-repos-selected",
136-
}
137-
}
138-
139128
if (selectedLanguageModel === undefined) {
140129

141130
return {
@@ -149,29 +138,11 @@ export const ChatBox = ({
149138
isSubmitDisabledReason: undefined,
150139
}
151140

152-
}, [
153-
editor.children,
154-
isRedirecting,
155-
isGenerating,
156-
selectedSearchScopes.length,
157-
selectedLanguageModel,
158-
])
141+
}, [editor.children, isRedirecting, isGenerating, selectedLanguageModel])
159142

160143
const onSubmit = useCallback(() => {
161-
if (isSubmitDisabled) {
162-
if (isSubmitDisabledReason === "no-repos-selected") {
163-
toast({
164-
description: "⚠️ You must select at least one search scope",
165-
variant: "destructive",
166-
});
167-
onContextSelectorOpenChanged(true);
168-
}
169-
170-
return;
171-
}
172-
173144
_onSubmit(editor.children, editor);
174-
}, [_onSubmit, editor, isSubmitDisabled, isSubmitDisabledReason, toast, onContextSelectorOpenChanged]);
145+
}, [_onSubmit, editor]);
175146

176147
const onInsertSuggestion = useCallback((suggestion: Suggestion) => {
177148
switch (suggestion.type) {
@@ -310,39 +281,15 @@ export const ChatBox = ({
310281
Stop
311282
</Button>
312283
) : (
313-
<Tooltip>
314-
<TooltipTrigger asChild>
315-
<div
316-
onClick={() => {
317-
// @hack: When submission is disabled, we still want to issue
318-
// a warning to the user as to why the submission is disabled.
319-
// onSubmit on the Button will not be called because of the
320-
// disabled prop, hence the call here.
321-
if (isSubmitDisabled) {
322-
onSubmit();
323-
}
324-
}}
325-
>
326-
<Button
327-
variant={isSubmitDisabled ? "outline" : "default"}
328-
size="sm"
329-
className="w-6 h-6"
330-
onClick={onSubmit}
331-
disabled={isSubmitDisabled}
332-
>
333-
<ArrowUp className="w-4 h-4" />
334-
</Button>
335-
</div>
336-
</TooltipTrigger>
337-
{(isSubmitDisabled && isSubmitDisabledReason === "no-repos-selected") && (
338-
<TooltipContent>
339-
<div className="flex flex-row items-center">
340-
<TriangleAlertIcon className="h-4 w-4 text-warning mr-1" />
341-
<span className="text-destructive">You must select at least one search scope</span>
342-
</div>
343-
</TooltipContent>
344-
)}
345-
</Tooltip>
284+
<Button
285+
variant={isSubmitDisabled ? "outline" : "default"}
286+
size="sm"
287+
className="w-6 h-6"
288+
onClick={onSubmit}
289+
disabled={isSubmitDisabled}
290+
>
291+
<ArrowUp className="w-4 h-4" />
292+
</Button>
346293
)}
347294
</div>
348295
{suggestionMode !== "none" && (

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,6 @@ export const ChatThread = ({
321321
languageModels={languageModels}
322322
selectedSearchScopes={selectedSearchScopes}
323323
searchContexts={searchContexts}
324-
onContextSelectorOpenChanged={setIsContextSelectorOpen}
325324
/>
326325
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
327326
<ChatBoxToolbar

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinition
1212
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
1313
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
1414
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
15+
import { SearchReposToolComponent } from './tools/searchReposToolComponent';
16+
import { ListAllReposToolComponent } from './tools/listAllReposToolComponent';
1517
import { SBChatMessageMetadata, SBChatMessagePart } from '../../types';
1618
import { SearchScopeIcon } from '../searchScopeIcon';
1719

@@ -63,7 +65,7 @@ export const DetailsCard = ({
6365
{!isStreaming && (
6466
<>
6567
<Separator orientation="vertical" className="h-4" />
66-
{metadata?.selectedSearchScopes && (
68+
{(metadata?.selectedSearchScopes && metadata.selectedSearchScopes.length > 0) && (
6769
<Tooltip>
6870
<TooltipTrigger asChild>
6971
<div className="flex items-center text-xs cursor-help">
@@ -181,6 +183,20 @@ export const DetailsCard = ({
181183
part={part}
182184
/>
183185
)
186+
case 'tool-searchRepos':
187+
return (
188+
<SearchReposToolComponent
189+
key={index}
190+
part={part}
191+
/>
192+
)
193+
case 'tool-listAllRepos':
194+
return (
195+
<ListAllReposToolComponent
196+
key={index}
197+
part={part}
198+
/>
199+
)
184200
default:
185201
return null;
186202
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export const ReferencedSourcesListView = ({
221221
<span className="text-sm font-medium">{fileName}</span>
222222
</div>
223223
<div className="p-4 text-sm text-destructive bg-destructive/10 rounded border">
224-
Failed to load file: {isServiceError(query.data) ? query.data.message : 'Unknown error'}
224+
Failed to load file: {isServiceError(query.data) ? query.data.message : query.error?.message ?? 'Unknown error'}
225225
</div>
226226
</div>
227227
);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client';
2+
3+
import { ListAllReposToolUIPart } from "@/features/chat/tools";
4+
import { isServiceError } from "@/lib/utils";
5+
import { useMemo, useState } from "react";
6+
import { ToolHeader, TreeList } from "./shared";
7+
import { CodeSnippet } from "@/app/components/codeSnippet";
8+
import { Separator } from "@/components/ui/separator";
9+
import { FolderOpenIcon } from "lucide-react";
10+
11+
export const ListAllReposToolComponent = ({ part }: { part: ListAllReposToolUIPart }) => {
12+
const [isExpanded, setIsExpanded] = useState(false);
13+
14+
const label = useMemo(() => {
15+
switch (part.state) {
16+
case 'input-streaming':
17+
return 'Loading all repositories...';
18+
case 'output-error':
19+
return '"List all repositories" tool call failed';
20+
case 'input-available':
21+
case 'output-available':
22+
return 'Listed all repositories';
23+
}
24+
}, [part]);
25+
26+
return (
27+
<div className="my-4">
28+
<ToolHeader
29+
isLoading={part.state !== 'output-available' && part.state !== 'output-error'}
30+
isError={part.state === 'output-error' || (part.state === 'output-available' && isServiceError(part.output))}
31+
isExpanded={isExpanded}
32+
label={label}
33+
Icon={FolderOpenIcon}
34+
onExpand={setIsExpanded}
35+
/>
36+
{part.state === 'output-available' && isExpanded && (
37+
<>
38+
{isServiceError(part.output) ? (
39+
<TreeList>
40+
<span>Failed with the following error: <CodeSnippet className="text-sm text-destructive">{part.output.message}</CodeSnippet></span>
41+
</TreeList>
42+
) : (
43+
<>
44+
{part.output.length === 0 ? (
45+
<span className="text-sm text-muted-foreground ml-[25px]">No repositories found</span>
46+
) : (
47+
<TreeList>
48+
<div className="text-sm text-muted-foreground mb-2">
49+
Found {part.output.length} repositories:
50+
</div>
51+
{part.output.map((repoName, index) => (
52+
<div key={index} className="flex items-center gap-2 text-sm">
53+
<FolderOpenIcon className="h-4 w-4 text-muted-foreground" />
54+
<span className="truncate">{repoName}</span>
55+
</div>
56+
))}
57+
</TreeList>
58+
)}
59+
</>
60+
)}
61+
<Separator className='ml-[7px] my-2' />
62+
</>
63+
)}
64+
</div>
65+
)
66+
}

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,38 @@ import { SearchIcon } from "lucide-react";
1111
import Link from "next/link";
1212
import { SearchQueryParams } from "@/lib/types";
1313
import { PlayIcon } from "@radix-ui/react-icons";
14-
14+
import { buildSearchQuery } from "@/features/chat/utils";
1515

1616
export const SearchCodeToolComponent = ({ part }: { part: SearchCodeToolUIPart }) => {
1717
const [isExpanded, setIsExpanded] = useState(false);
1818
const domain = useDomain();
1919

20+
const displayQuery = useMemo(() => {
21+
if (part.state !== 'input-available' && part.state !== 'output-available') {
22+
return '';
23+
}
24+
25+
const query = buildSearchQuery({
26+
query: part.input.queryRegexp,
27+
repoNamesFilterRegexp: part.input.repoNamesFilterRegexp,
28+
languageNamesFilter: part.input.languageNamesFilter,
29+
fileNamesFilterRegexp: part.input.fileNamesFilterRegexp,
30+
});
31+
32+
return query;
33+
}, [part]);
34+
2035
const label = useMemo(() => {
2136
switch (part.state) {
2237
case 'input-streaming':
2338
return 'Searching...';
24-
case 'input-available':
25-
return <span>Searching for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
2639
case 'output-error':
2740
return '"Search code" tool call failed';
41+
case 'input-available':
2842
case 'output-available':
29-
return <span>Searched for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
43+
return <span>Searched for <CodeSnippet>{displayQuery}</CodeSnippet></span>;
3044
}
31-
}, [part]);
45+
}, [part, displayQuery]);
3246

3347
return (
3448
<div className="my-4">

0 commit comments

Comments
 (0)