mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
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
This commit is contained in:
parent
4343b3c3d5
commit
211ad8fb12
16 changed files with 490 additions and 96 deletions
|
|
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405)
|
- Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405)
|
||||||
- Add GitHub star toast. [#409](https://github.com/sourcebot-dev/sourcebot/pull/409)
|
- Add GitHub star toast. [#409](https://github.com/sourcebot-dev/sourcebot/pull/409)
|
||||||
- Added a onboarding modal when first visiting the homepage when `ask` mode is selected. [#408](https://github.com/sourcebot-dev/sourcebot/pull/408)
|
- Added a onboarding modal when first visiting the homepage when `ask` mode is selected. [#408](https://github.com/sourcebot-dev/sourcebot/pull/408)
|
||||||
|
- [ask sb] Added `searchReposTool` and `listAllReposTool`. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398)
|
- 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
|
||||||
- Bumped AI SDK and associated packages version. [#404](https://github.com/sourcebot-dev/sourcebot/pull/404)
|
- Bumped AI SDK and associated packages version. [#404](https://github.com/sourcebot-dev/sourcebot/pull/404)
|
||||||
- Bumped form-data package version. [#407](https://github.com/sourcebot-dev/sourcebot/pull/407)
|
- Bumped form-data package version. [#407](https://github.com/sourcebot-dev/sourcebot/pull/407)
|
||||||
- Bumped next version. [#406](https://github.com/sourcebot-dev/sourcebot/pull/406)
|
- Bumped next version. [#406](https://github.com/sourcebot-dev/sourcebot/pull/406)
|
||||||
|
- [ask sb] Improved search code tool with filter options. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400)
|
||||||
|
- [ask sb] Removed search scope constraint. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400)
|
||||||
|
|
||||||
## [4.6.0] - 2025-07-25
|
## [4.6.0] - 2025-07-25
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ export const NewChatPanel = ({
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
selectedSearchScopes={selectedSearchScopes}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
|
||||||
/>
|
/>
|
||||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||||
<ChatBoxToolbar
|
<ChatBoxToolbar
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ export const AgenticSearch = ({
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
selectedSearchScopes={selectedSearchScopes}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
|
||||||
/>
|
/>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ProviderOptions } from "@ai-sdk/provider-utils";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { LanguageModel, ModelMessage, StopCondition, streamText } from "ai";
|
import { LanguageModel, ModelMessage, StopCondition, streamText } from "ai";
|
||||||
import { ANSWER_TAG, FILE_REFERENCE_PREFIX, toolNames } from "./constants";
|
import { ANSWER_TAG, FILE_REFERENCE_PREFIX, toolNames } from "./constants";
|
||||||
import { createCodeSearchTool, findSymbolDefinitionsTool, findSymbolReferencesTool, readFilesTool } from "./tools";
|
import { createCodeSearchTool, findSymbolDefinitionsTool, findSymbolReferencesTool, readFilesTool, searchReposTool, listAllReposTool } from "./tools";
|
||||||
import { FileSource, Source } from "./types";
|
import { FileSource, Source } from "./types";
|
||||||
import { addLineNumbers, fileReferenceToString } from "./utils";
|
import { addLineNumbers, fileReferenceToString } from "./utils";
|
||||||
|
|
||||||
|
|
@ -54,6 +54,8 @@ export const createAgentStream = async ({
|
||||||
[toolNames.readFiles]: readFilesTool,
|
[toolNames.readFiles]: readFilesTool,
|
||||||
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
|
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
|
||||||
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
|
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
|
||||||
|
[toolNames.searchRepos]: searchReposTool,
|
||||||
|
[toolNames.listAllRepos]: listAllReposTool,
|
||||||
},
|
},
|
||||||
prepareStep: async ({ stepNumber }) => {
|
prepareStep: async ({ stepNumber }) => {
|
||||||
// The first step attaches any mentioned sources to the system prompt.
|
// The first step attaches any mentioned sources to the system prompt.
|
||||||
|
|
@ -185,13 +187,7 @@ ${searchScopeRepoNames.map(repo => `- ${repo}`).join('\n')}
|
||||||
</available_repositories>
|
</available_repositories>
|
||||||
|
|
||||||
<research_phase_instructions>
|
<research_phase_instructions>
|
||||||
During the research phase, you have these tools available:
|
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.
|
||||||
- \`${toolNames.searchCode}\`: Search for code patterns, functions, or text across repositories
|
|
||||||
- \`${toolNames.readFiles}\`: Read the contents of specific files
|
|
||||||
- \`${toolNames.findSymbolReferences}\`: Find where symbols are referenced
|
|
||||||
- \`${toolNames.findSymbolDefinitions}\`: Find where symbols are defined
|
|
||||||
|
|
||||||
Use these tools to gather comprehensive context before answering. Always explain why you're using each tool.
|
|
||||||
</research_phase_instructions>
|
</research_phase_instructions>
|
||||||
|
|
||||||
${answerInstructions}
|
${answerInstructions}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
|
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
|
||||||
import { insertMention, slateContentToString } from "@/features/chat/utils";
|
import { insertMention, slateContentToString } from "@/features/chat/utils";
|
||||||
|
import { SearchContextQuery } from "@/lib/types";
|
||||||
import { cn, IS_MAC } from "@/lib/utils";
|
import { cn, IS_MAC } from "@/lib/utils";
|
||||||
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
|
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
|
||||||
import { ArrowUp, Loader2, StopCircleIcon, TriangleAlertIcon } from "lucide-react";
|
import { ArrowUp, Loader2, StopCircleIcon } from "lucide-react";
|
||||||
import { Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { Descendant, insertText } from "slate";
|
import { Descendant, insertText } from "slate";
|
||||||
|
|
@ -17,8 +18,6 @@ import { SuggestionBox } from "./suggestionsBox";
|
||||||
import { Suggestion } from "./types";
|
import { Suggestion } from "./types";
|
||||||
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
|
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
|
||||||
import { useSuggestionsData } from "./useSuggestionsData";
|
import { useSuggestionsData } from "./useSuggestionsData";
|
||||||
import { useToast } from "@/components/hooks/use-toast";
|
|
||||||
import { SearchContextQuery } from "@/lib/types";
|
|
||||||
|
|
||||||
interface ChatBoxProps {
|
interface ChatBoxProps {
|
||||||
onSubmit: (children: Descendant[], editor: CustomEditor) => void;
|
onSubmit: (children: Descendant[], editor: CustomEditor) => void;
|
||||||
|
|
@ -30,7 +29,6 @@ interface ChatBoxProps {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
selectedSearchScopes: SearchScope[];
|
selectedSearchScopes: SearchScope[];
|
||||||
searchContexts: SearchContextQuery[];
|
searchContexts: SearchContextQuery[];
|
||||||
onContextSelectorOpenChanged: (isOpen: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatBox = ({
|
export const ChatBox = ({
|
||||||
|
|
@ -43,7 +41,6 @@ export const ChatBox = ({
|
||||||
languageModels,
|
languageModels,
|
||||||
selectedSearchScopes,
|
selectedSearchScopes,
|
||||||
searchContexts,
|
searchContexts,
|
||||||
onContextSelectorOpenChanged,
|
|
||||||
}: ChatBoxProps) => {
|
}: ChatBoxProps) => {
|
||||||
const suggestionsBoxRef = useRef<HTMLDivElement>(null);
|
const suggestionsBoxRef = useRef<HTMLDivElement>(null);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
|
|
@ -70,7 +67,6 @@ export const ChatBox = ({
|
||||||
const { selectedLanguageModel } = useSelectedLanguageModel({
|
const { selectedLanguageModel } = useSelectedLanguageModel({
|
||||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||||
});
|
});
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
// Reset the index when the suggestion mode changes.
|
// Reset the index when the suggestion mode changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -101,9 +97,9 @@ export const ChatBox = ({
|
||||||
return <Leaf {...props} />
|
return <Leaf {...props} />
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { isSubmitDisabled, isSubmitDisabledReason } = useMemo((): {
|
const { isSubmitDisabled } = useMemo((): {
|
||||||
isSubmitDisabled: true,
|
isSubmitDisabled: true,
|
||||||
isSubmitDisabledReason: "empty" | "redirecting" | "generating" | "no-repos-selected" | "no-language-model-selected"
|
isSubmitDisabledReason: "empty" | "redirecting" | "generating" | "no-language-model-selected"
|
||||||
} | {
|
} | {
|
||||||
isSubmitDisabled: false,
|
isSubmitDisabled: false,
|
||||||
isSubmitDisabledReason: undefined,
|
isSubmitDisabledReason: undefined,
|
||||||
|
|
@ -129,13 +125,6 @@ export const ChatBox = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedSearchScopes.length === 0) {
|
|
||||||
return {
|
|
||||||
isSubmitDisabled: true,
|
|
||||||
isSubmitDisabledReason: "no-repos-selected",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedLanguageModel === undefined) {
|
if (selectedLanguageModel === undefined) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -149,29 +138,11 @@ export const ChatBox = ({
|
||||||
isSubmitDisabledReason: undefined,
|
isSubmitDisabledReason: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [
|
}, [editor.children, isRedirecting, isGenerating, selectedLanguageModel])
|
||||||
editor.children,
|
|
||||||
isRedirecting,
|
|
||||||
isGenerating,
|
|
||||||
selectedSearchScopes.length,
|
|
||||||
selectedLanguageModel,
|
|
||||||
])
|
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
const onSubmit = useCallback(() => {
|
||||||
if (isSubmitDisabled) {
|
|
||||||
if (isSubmitDisabledReason === "no-repos-selected") {
|
|
||||||
toast({
|
|
||||||
description: "⚠️ You must select at least one search scope",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
onContextSelectorOpenChanged(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSubmit(editor.children, editor);
|
_onSubmit(editor.children, editor);
|
||||||
}, [_onSubmit, editor, isSubmitDisabled, isSubmitDisabledReason, toast, onContextSelectorOpenChanged]);
|
}, [_onSubmit, editor]);
|
||||||
|
|
||||||
const onInsertSuggestion = useCallback((suggestion: Suggestion) => {
|
const onInsertSuggestion = useCallback((suggestion: Suggestion) => {
|
||||||
switch (suggestion.type) {
|
switch (suggestion.type) {
|
||||||
|
|
@ -310,39 +281,15 @@ export const ChatBox = ({
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant={isSubmitDisabled ? "outline" : "default"}
|
||||||
<div
|
size="sm"
|
||||||
onClick={() => {
|
className="w-6 h-6"
|
||||||
// @hack: When submission is disabled, we still want to issue
|
onClick={onSubmit}
|
||||||
// a warning to the user as to why the submission is disabled.
|
disabled={isSubmitDisabled}
|
||||||
// onSubmit on the Button will not be called because of the
|
>
|
||||||
// disabled prop, hence the call here.
|
<ArrowUp className="w-4 h-4" />
|
||||||
if (isSubmitDisabled) {
|
</Button>
|
||||||
onSubmit();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant={isSubmitDisabled ? "outline" : "default"}
|
|
||||||
size="sm"
|
|
||||||
className="w-6 h-6"
|
|
||||||
onClick={onSubmit}
|
|
||||||
disabled={isSubmitDisabled}
|
|
||||||
>
|
|
||||||
<ArrowUp className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
{(isSubmitDisabled && isSubmitDisabledReason === "no-repos-selected") && (
|
|
||||||
<TooltipContent>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<TriangleAlertIcon className="h-4 w-4 text-warning mr-1" />
|
|
||||||
<span className="text-destructive">You must select at least one search scope</span>
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{suggestionMode !== "none" && (
|
{suggestionMode !== "none" && (
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,6 @@ export const ChatThread = ({
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
selectedSearchScopes={selectedSearchScopes}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
|
||||||
/>
|
/>
|
||||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||||
<ChatBoxToolbar
|
<ChatBoxToolbar
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinition
|
||||||
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
|
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
|
||||||
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
|
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
|
||||||
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
|
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
|
||||||
|
import { SearchReposToolComponent } from './tools/searchReposToolComponent';
|
||||||
|
import { ListAllReposToolComponent } from './tools/listAllReposToolComponent';
|
||||||
import { SBChatMessageMetadata, SBChatMessagePart } from '../../types';
|
import { SBChatMessageMetadata, SBChatMessagePart } from '../../types';
|
||||||
import { SearchScopeIcon } from '../searchScopeIcon';
|
import { SearchScopeIcon } from '../searchScopeIcon';
|
||||||
|
|
||||||
|
|
@ -63,7 +65,7 @@ export const DetailsCard = ({
|
||||||
{!isStreaming && (
|
{!isStreaming && (
|
||||||
<>
|
<>
|
||||||
<Separator orientation="vertical" className="h-4" />
|
<Separator orientation="vertical" className="h-4" />
|
||||||
{metadata?.selectedSearchScopes && (
|
{(metadata?.selectedSearchScopes && metadata.selectedSearchScopes.length > 0) && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center text-xs cursor-help">
|
<div className="flex items-center text-xs cursor-help">
|
||||||
|
|
@ -181,6 +183,20 @@ export const DetailsCard = ({
|
||||||
part={part}
|
part={part}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'tool-searchRepos':
|
||||||
|
return (
|
||||||
|
<SearchReposToolComponent
|
||||||
|
key={index}
|
||||||
|
part={part}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'tool-listAllRepos':
|
||||||
|
return (
|
||||||
|
<ListAllReposToolComponent
|
||||||
|
key={index}
|
||||||
|
part={part}
|
||||||
|
/>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ export const ReferencedSourcesListView = ({
|
||||||
<span className="text-sm font-medium">{fileName}</span>
|
<span className="text-sm font-medium">{fileName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-sm text-destructive bg-destructive/10 rounded border">
|
<div className="p-4 text-sm text-destructive bg-destructive/10 rounded border">
|
||||||
Failed to load file: {isServiceError(query.data) ? query.data.message : 'Unknown error'}
|
Failed to load file: {isServiceError(query.data) ? query.data.message : query.error?.message ?? 'Unknown error'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ListAllReposToolUIPart } from "@/features/chat/tools";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { ToolHeader, TreeList } from "./shared";
|
||||||
|
import { CodeSnippet } from "@/app/components/codeSnippet";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { FolderOpenIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export const ListAllReposToolComponent = ({ part }: { part: ListAllReposToolUIPart }) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const label = useMemo(() => {
|
||||||
|
switch (part.state) {
|
||||||
|
case 'input-streaming':
|
||||||
|
return 'Loading all repositories...';
|
||||||
|
case 'output-error':
|
||||||
|
return '"List all repositories" tool call failed';
|
||||||
|
case 'input-available':
|
||||||
|
case 'output-available':
|
||||||
|
return 'Listed all repositories';
|
||||||
|
}
|
||||||
|
}, [part]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-4">
|
||||||
|
<ToolHeader
|
||||||
|
isLoading={part.state !== 'output-available' && part.state !== 'output-error'}
|
||||||
|
isError={part.state === 'output-error' || (part.state === 'output-available' && isServiceError(part.output))}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
label={label}
|
||||||
|
Icon={FolderOpenIcon}
|
||||||
|
onExpand={setIsExpanded}
|
||||||
|
/>
|
||||||
|
{part.state === 'output-available' && isExpanded && (
|
||||||
|
<>
|
||||||
|
{isServiceError(part.output) ? (
|
||||||
|
<TreeList>
|
||||||
|
<span>Failed with the following error: <CodeSnippet className="text-sm text-destructive">{part.output.message}</CodeSnippet></span>
|
||||||
|
</TreeList>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{part.output.length === 0 ? (
|
||||||
|
<span className="text-sm text-muted-foreground ml-[25px]">No repositories found</span>
|
||||||
|
) : (
|
||||||
|
<TreeList>
|
||||||
|
<div className="text-sm text-muted-foreground mb-2">
|
||||||
|
Found {part.output.length} repositories:
|
||||||
|
</div>
|
||||||
|
{part.output.map((repoName, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2 text-sm">
|
||||||
|
<FolderOpenIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="truncate">{repoName}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TreeList>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Separator className='ml-[7px] my-2' />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -11,24 +11,38 @@ import { SearchIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { SearchQueryParams } from "@/lib/types";
|
import { SearchQueryParams } from "@/lib/types";
|
||||||
import { PlayIcon } from "@radix-ui/react-icons";
|
import { PlayIcon } from "@radix-ui/react-icons";
|
||||||
|
import { buildSearchQuery } from "@/features/chat/utils";
|
||||||
|
|
||||||
export const SearchCodeToolComponent = ({ part }: { part: SearchCodeToolUIPart }) => {
|
export const SearchCodeToolComponent = ({ part }: { part: SearchCodeToolUIPart }) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const domain = useDomain();
|
const domain = useDomain();
|
||||||
|
|
||||||
|
const displayQuery = useMemo(() => {
|
||||||
|
if (part.state !== 'input-available' && part.state !== 'output-available') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = buildSearchQuery({
|
||||||
|
query: part.input.queryRegexp,
|
||||||
|
repoNamesFilterRegexp: part.input.repoNamesFilterRegexp,
|
||||||
|
languageNamesFilter: part.input.languageNamesFilter,
|
||||||
|
fileNamesFilterRegexp: part.input.fileNamesFilterRegexp,
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}, [part]);
|
||||||
|
|
||||||
const label = useMemo(() => {
|
const label = useMemo(() => {
|
||||||
switch (part.state) {
|
switch (part.state) {
|
||||||
case 'input-streaming':
|
case 'input-streaming':
|
||||||
return 'Searching...';
|
return 'Searching...';
|
||||||
case 'input-available':
|
|
||||||
return <span>Searching for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
|
|
||||||
case 'output-error':
|
case 'output-error':
|
||||||
return '"Search code" tool call failed';
|
return '"Search code" tool call failed';
|
||||||
|
case 'input-available':
|
||||||
case 'output-available':
|
case 'output-available':
|
||||||
return <span>Searched for <CodeSnippet>{part.input.query}</CodeSnippet></span>;
|
return <span>Searched for <CodeSnippet>{displayQuery}</CodeSnippet></span>;
|
||||||
}
|
}
|
||||||
}, [part]);
|
}, [part, displayQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SearchReposToolUIPart } from "@/features/chat/tools";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { ToolHeader, TreeList } from "./shared";
|
||||||
|
import { CodeSnippet } from "@/app/components/codeSnippet";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { BookMarkedIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export const SearchReposToolComponent = ({ part }: { part: SearchReposToolUIPart }) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const label = useMemo(() => {
|
||||||
|
switch (part.state) {
|
||||||
|
case 'input-streaming':
|
||||||
|
return 'Searching repositories...';
|
||||||
|
case 'output-error':
|
||||||
|
return '"Search repositories" tool call failed';
|
||||||
|
case 'input-available':
|
||||||
|
case 'output-available':
|
||||||
|
return <span>Searched for repositories: <CodeSnippet className="truncate">{part.input.query}</CodeSnippet></span>;
|
||||||
|
}
|
||||||
|
}, [part]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-4">
|
||||||
|
<ToolHeader
|
||||||
|
isLoading={part.state !== 'output-available' && part.state !== 'output-error'}
|
||||||
|
isError={part.state === 'output-error' || (part.state === 'output-available' && isServiceError(part.output))}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
label={label}
|
||||||
|
Icon={BookMarkedIcon}
|
||||||
|
onExpand={setIsExpanded}
|
||||||
|
/>
|
||||||
|
{part.state === 'output-available' && isExpanded && (
|
||||||
|
<>
|
||||||
|
{isServiceError(part.output) ? (
|
||||||
|
<TreeList>
|
||||||
|
<span>Failed with the following error: <CodeSnippet className="text-sm text-destructive">{part.output.message}</CodeSnippet></span>
|
||||||
|
</TreeList>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{part.output.length === 0 ? (
|
||||||
|
<span className="text-sm text-muted-foreground ml-[25px]">No repositories found</span>
|
||||||
|
) : (
|
||||||
|
<TreeList>
|
||||||
|
{part.output.map((repoName, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2 text-sm">
|
||||||
|
<BookMarkedIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="truncate">{repoName}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TreeList>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Separator className='ml-[7px] my-2' />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@ export const toolNames = {
|
||||||
readFiles: 'readFiles',
|
readFiles: 'readFiles',
|
||||||
findSymbolReferences: 'findSymbolReferences',
|
findSymbolReferences: 'findSymbolReferences',
|
||||||
findSymbolDefinitions: 'findSymbolDefinitions',
|
findSymbolDefinitions: 'findSymbolDefinitions',
|
||||||
|
searchRepos: 'searchRepos',
|
||||||
|
listAllRepos: 'listAllRepos',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// These part types are visible in the UI.
|
// These part types are visible in the UI.
|
||||||
|
|
@ -24,4 +26,6 @@ export const uiVisiblePartTypes: SBChatMessagePart['type'][] = [
|
||||||
'tool-readFiles',
|
'tool-readFiles',
|
||||||
'tool-findSymbolDefinitions',
|
'tool-findSymbolDefinitions',
|
||||||
'tool-findSymbolReferences',
|
'tool-findSymbolReferences',
|
||||||
|
'tool-searchRepos',
|
||||||
|
'tool-listAllRepos',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
@ -6,8 +6,10 @@ import { isServiceError } from "@/lib/utils";
|
||||||
import { getFileSource } from "../search/fileSourceApi";
|
import { getFileSource } from "../search/fileSourceApi";
|
||||||
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/actions";
|
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/actions";
|
||||||
import { FileSourceResponse } from "../search/types";
|
import { FileSourceResponse } from "../search/types";
|
||||||
import { addLineNumbers } from "./utils";
|
import { addLineNumbers, buildSearchQuery } from "./utils";
|
||||||
import { toolNames } from "./constants";
|
import { toolNames } from "./constants";
|
||||||
|
import { getRepos } from "@/actions";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
// @NOTE: When adding a new tool, follow these steps:
|
// @NOTE: When adding a new tool, follow these steps:
|
||||||
// 1. Add the tool to the `toolNames` constant in `constants.ts`.
|
// 1. Add the tool to the `toolNames` constant in `constants.ts`.
|
||||||
|
|
@ -139,17 +141,48 @@ export const createCodeSearchTool = (selectedRepos: string[]) => tool({
|
||||||
description: `Fetches code that matches the provided regex pattern in \`query\`. This is NOT a semantic search.
|
description: `Fetches code that matches the provided regex pattern in \`query\`. This is NOT a semantic search.
|
||||||
Results are returned as an array of matching files, with the file's URL, repository, and language.`,
|
Results are returned as an array of matching files, with the file's URL, repository, and language.`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
query: z.string().describe("The regex pattern to search for in the code"),
|
queryRegexp: z
|
||||||
|
.string()
|
||||||
|
.describe(`The regex pattern to search for in the code.
|
||||||
|
|
||||||
|
Queries consist of space-seperated regular expressions. Wrapping expressions in "" combines them. By default, a file must have at least one match for each expression to be included. Examples:
|
||||||
|
|
||||||
|
\`foo\` - Match files with regex /foo/
|
||||||
|
\`foo bar\` - Match files with regex /foo/ and /bar/
|
||||||
|
\`"foo bar"\` - Match files with regex /foo bar/
|
||||||
|
\`console.log\` - Match files with regex /console.log/
|
||||||
|
|
||||||
|
Multiple expressions can be or'd together with or, negated with -, or grouped with (). Examples:
|
||||||
|
\`foo or bar\` - Match files with regex /foo/ or /bar/
|
||||||
|
\`foo -bar\` - Match files with regex /foo/ but not /bar/
|
||||||
|
\`foo (bar or baz)\` - Match files with regex /foo/ and either /bar/ or /baz/
|
||||||
|
`),
|
||||||
|
repoNamesFilterRegexp: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe(`Filter results from repos that match the regex. By default all repos are searched.`)
|
||||||
|
.optional(),
|
||||||
|
languageNamesFilter: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe(`Scope the search to the provided languages. The language MUST be formatted as a GitHub linguist language. Examples: Python, JavaScript, TypeScript, Java, C#, C++, PHP, Go, Rust, Ruby, Swift, Kotlin, Shell, C, Dart, HTML, CSS, PowerShell, SQL, R`)
|
||||||
|
.optional(),
|
||||||
|
fileNamesFilterRegexp: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe(`Filter results from filepaths that match the regex. When this option is not specified, all files are searched.`)
|
||||||
|
.optional(),
|
||||||
|
limit: z.number().default(10).describe("Maximum number of matches to return (default: 100)"),
|
||||||
}),
|
}),
|
||||||
execute: async ({ query: _query }) => {
|
execute: async ({ queryRegexp: _query, repoNamesFilterRegexp, languageNamesFilter, fileNamesFilterRegexp, limit }) => {
|
||||||
let query = `${_query}`;
|
const query = buildSearchQuery({
|
||||||
if (selectedRepos.length > 0) {
|
query: _query,
|
||||||
query += ` reposet:${selectedRepos.join(',')}`;
|
repoNamesFilter: selectedRepos,
|
||||||
}
|
repoNamesFilterRegexp,
|
||||||
|
languageNamesFilter,
|
||||||
|
fileNamesFilterRegexp,
|
||||||
|
});
|
||||||
|
|
||||||
const response = await search({
|
const response = await search({
|
||||||
query,
|
query,
|
||||||
matches: 100,
|
matches: limit ?? 100,
|
||||||
// @todo: we can make this configurable.
|
// @todo: we can make this configurable.
|
||||||
contextLines: 3,
|
contextLines: 3,
|
||||||
whole: false,
|
whole: false,
|
||||||
|
|
@ -180,3 +213,59 @@ export type SearchCodeTool = InferUITool<ReturnType<typeof createCodeSearchTool>
|
||||||
export type SearchCodeToolInput = InferToolInput<ReturnType<typeof createCodeSearchTool>>;
|
export type SearchCodeToolInput = InferToolInput<ReturnType<typeof createCodeSearchTool>>;
|
||||||
export type SearchCodeToolOutput = InferToolOutput<ReturnType<typeof createCodeSearchTool>>;
|
export type SearchCodeToolOutput = InferToolOutput<ReturnType<typeof createCodeSearchTool>>;
|
||||||
export type SearchCodeToolUIPart = ToolUIPart<{ [toolNames.searchCode]: SearchCodeTool }>;
|
export type SearchCodeToolUIPart = ToolUIPart<{ [toolNames.searchCode]: SearchCodeTool }>;
|
||||||
|
|
||||||
|
export const searchReposTool = tool({
|
||||||
|
description: `Search for repositories by name using fuzzy search. This helps find repositories in the codebase when you know part of their name.`,
|
||||||
|
inputSchema: z.object({
|
||||||
|
query: z.string().describe("The search query to find repositories by name (supports fuzzy matching)"),
|
||||||
|
limit: z.number().default(10).describe("Maximum number of repositories to return (default: 10)")
|
||||||
|
}),
|
||||||
|
execute: async ({ query, limit }) => {
|
||||||
|
const reposResponse = await getRepos(SINGLE_TENANT_ORG_DOMAIN);
|
||||||
|
|
||||||
|
if (isServiceError(reposResponse)) {
|
||||||
|
return reposResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure Fuse.js for fuzzy searching
|
||||||
|
const fuse = new Fuse(reposResponse, {
|
||||||
|
keys: [
|
||||||
|
{ name: 'repoName', weight: 0.7 },
|
||||||
|
{ name: 'repoDisplayName', weight: 0.3 }
|
||||||
|
],
|
||||||
|
threshold: 0.4, // Lower threshold = more strict matching
|
||||||
|
includeScore: true,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchResults = fuse.search(query, { limit: limit ?? 10 });
|
||||||
|
|
||||||
|
searchResults.sort((a, b) => (a.score ?? 0) - (b.score ?? 0));
|
||||||
|
|
||||||
|
return searchResults.map(({ item }) => item.repoName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SearchReposTool = InferUITool<typeof searchReposTool>;
|
||||||
|
export type SearchReposToolInput = InferToolInput<typeof searchReposTool>;
|
||||||
|
export type SearchReposToolOutput = InferToolOutput<typeof searchReposTool>;
|
||||||
|
export type SearchReposToolUIPart = ToolUIPart<{ [toolNames.searchRepos]: SearchReposTool }>;
|
||||||
|
|
||||||
|
export const listAllReposTool = tool({
|
||||||
|
description: `Lists all repositories in the codebase. This provides a complete overview of all available repositories.`,
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
execute: async () => {
|
||||||
|
const reposResponse = await getRepos(SINGLE_TENANT_ORG_DOMAIN);
|
||||||
|
|
||||||
|
if (isServiceError(reposResponse)) {
|
||||||
|
return reposResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reposResponse.map((repo) => repo.repoName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListAllReposTool = InferUITool<typeof listAllReposTool>;
|
||||||
|
export type ListAllReposToolInput = InferToolInput<typeof listAllReposTool>;
|
||||||
|
export type ListAllReposToolOutput = InferToolOutput<typeof listAllReposTool>;
|
||||||
|
export type ListAllReposToolUIPart = ToolUIPart<{ [toolNames.listAllRepos]: ListAllReposTool }>;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { BaseEditor, Descendant } from "slate";
|
||||||
import { HistoryEditor } from "slate-history";
|
import { HistoryEditor } from "slate-history";
|
||||||
import { ReactEditor, RenderElementProps } from "slate-react";
|
import { ReactEditor, RenderElementProps } from "slate-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { FindSymbolDefinitionsTool, FindSymbolReferencesTool, ReadFilesTool, SearchCodeTool } from "./tools";
|
import { FindSymbolDefinitionsTool, FindSymbolReferencesTool, ReadFilesTool, SearchCodeTool, SearchReposTool, ListAllReposTool } from "./tools";
|
||||||
import { toolNames } from "./constants";
|
import { toolNames } from "./constants";
|
||||||
import { LanguageModel } from "@sourcebot/schemas/v3/index.type";
|
import { LanguageModel } from "@sourcebot/schemas/v3/index.type";
|
||||||
|
|
||||||
|
|
@ -83,6 +83,8 @@ export type SBChatMessageToolTypes = {
|
||||||
[toolNames.readFiles]: ReadFilesTool,
|
[toolNames.readFiles]: ReadFilesTool,
|
||||||
[toolNames.findSymbolReferences]: FindSymbolReferencesTool,
|
[toolNames.findSymbolReferences]: FindSymbolReferencesTool,
|
||||||
[toolNames.findSymbolDefinitions]: FindSymbolDefinitionsTool,
|
[toolNames.findSymbolDefinitions]: FindSymbolDefinitionsTool,
|
||||||
|
[toolNames.searchRepos]: SearchReposTool,
|
||||||
|
[toolNames.listAllRepos]: ListAllReposTool,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SBChatMessageDataParts = {
|
export type SBChatMessageDataParts = {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect, test, vi } from 'vitest'
|
import { expect, test, vi } from 'vitest'
|
||||||
import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from './utils'
|
import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences, buildSearchQuery } from './utils'
|
||||||
import { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants';
|
import { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants';
|
||||||
import { SBChatMessage, SBChatMessagePart } from './types';
|
import { SBChatMessage, SBChatMessagePart } from './types';
|
||||||
|
|
||||||
|
|
@ -351,3 +351,164 @@ test('repairReferences handles malformed inline code blocks', () => {
|
||||||
const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts} for details.';
|
const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts} for details.';
|
||||||
expect(repairReferences(input)).toBe(expected);
|
expect(repairReferences(input)).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery returns base query when no filters provided', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'console.log'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('console.log');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds repoNamesFilter correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'function test',
|
||||||
|
repoNamesFilter: ['repo1', 'repo2']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('function test reposet:repo1,repo2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds single repoNamesFilter correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'function test',
|
||||||
|
repoNamesFilter: ['myrepo']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('function test reposet:myrepo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery ignores empty repoNamesFilter', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'function test',
|
||||||
|
repoNamesFilter: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('function test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds languageNamesFilter correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'class definition',
|
||||||
|
languageNamesFilter: ['typescript', 'javascript']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('class definition ( lang:typescript or lang:javascript )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds single languageNamesFilter correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'class definition',
|
||||||
|
languageNamesFilter: ['python']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('class definition ( lang:python )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery ignores empty languageNamesFilter', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'class definition',
|
||||||
|
languageNamesFilter: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('class definition');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds fileNamesFilterRegexp correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'import statement',
|
||||||
|
fileNamesFilterRegexp: ['*.ts', '*.js']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('import statement ( file:*.ts or file:*.js )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds single fileNamesFilterRegexp correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'import statement',
|
||||||
|
fileNamesFilterRegexp: ['*.tsx']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('import statement ( file:*.tsx )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery ignores empty fileNamesFilterRegexp', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'import statement',
|
||||||
|
fileNamesFilterRegexp: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('import statement');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds repoNamesFilterRegexp correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'bug fix',
|
||||||
|
repoNamesFilterRegexp: ['org/repo1', 'org/repo2']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('bug fix ( repo:org/repo1 or repo:org/repo2 )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery adds single repoNamesFilterRegexp correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'bug fix',
|
||||||
|
repoNamesFilterRegexp: ['myorg/myrepo']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('bug fix ( repo:myorg/myrepo )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery ignores empty repoNamesFilterRegexp', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'bug fix',
|
||||||
|
repoNamesFilterRegexp: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('bug fix');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery combines multiple filters correctly', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'authentication',
|
||||||
|
repoNamesFilter: ['backend', 'frontend'],
|
||||||
|
languageNamesFilter: ['typescript', 'javascript'],
|
||||||
|
fileNamesFilterRegexp: ['*.ts', '*.js'],
|
||||||
|
repoNamesFilterRegexp: ['org/auth-*']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
'authentication reposet:backend,frontend ( lang:typescript or lang:javascript ) ( file:*.ts or file:*.js ) ( repo:org/auth-* )'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery handles mixed empty and non-empty filters', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'error handling',
|
||||||
|
repoNamesFilter: [],
|
||||||
|
languageNamesFilter: ['python'],
|
||||||
|
fileNamesFilterRegexp: [],
|
||||||
|
repoNamesFilterRegexp: ['error/*']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('error handling ( lang:python ) ( repo:error/* )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery handles empty base query', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: '',
|
||||||
|
repoNamesFilter: ['repo1'],
|
||||||
|
languageNamesFilter: ['typescript']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(' reposet:repo1 ( lang:typescript )');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSearchQuery handles query with special characters', () => {
|
||||||
|
const result = buildSearchQuery({
|
||||||
|
query: 'console.log("hello world")',
|
||||||
|
repoNamesFilter: ['test-repo']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe('console.log("hello world") reposet:test-repo');
|
||||||
|
});
|
||||||
|
|
@ -330,3 +330,39 @@ export const getAnswerPartFromAssistantMessage = (message: SBChatMessage, isStre
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const buildSearchQuery = (options: {
|
||||||
|
query: string,
|
||||||
|
repoNamesFilter?: string[],
|
||||||
|
repoNamesFilterRegexp?: string[],
|
||||||
|
languageNamesFilter?: string[],
|
||||||
|
fileNamesFilterRegexp?: string[],
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
query: _query,
|
||||||
|
repoNamesFilter,
|
||||||
|
repoNamesFilterRegexp,
|
||||||
|
languageNamesFilter,
|
||||||
|
fileNamesFilterRegexp,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let query = `${_query}`;
|
||||||
|
|
||||||
|
if (repoNamesFilter && repoNamesFilter.length > 0) {
|
||||||
|
query += ` reposet:${repoNamesFilter.join(',')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageNamesFilter && languageNamesFilter.length > 0) {
|
||||||
|
query += ` ( lang:${languageNamesFilter.join(' or lang:')} )`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileNamesFilterRegexp && fileNamesFilterRegexp.length > 0) {
|
||||||
|
query += ` ( file:${fileNamesFilterRegexp.join(' or file:')} )`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repoNamesFilterRegexp && repoNamesFilterRegexp.length > 0) {
|
||||||
|
query += ` ( repo:${repoNamesFilterRegexp.join(' or repo:')} )`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue