diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba12ac52..3397298e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add search context to ask sourcebot context selector. [#397](https://github.com/sourcebot-dev/sourcebot/pull/397)
- Add ability to include/exclude connection in search context. [#399](https://github.com/sourcebot-dev/sourcebot/pull/399)
+- Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405)
### Fixed
- Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398)
diff --git a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx
index 48824d76..92f3ab6e 100644
--- a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx
+++ b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx
@@ -2,13 +2,12 @@
import { ResizablePanel } from '@/components/ui/resizable';
import { ChatThread } from '@/features/chat/components/chatThread';
-import { LanguageModelInfo, SBChatMessage, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from '@/features/chat/types';
+import { LanguageModelInfo, SBChatMessage, SearchScope, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from '@/features/chat/types';
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
import { CreateUIMessage } from 'ai';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useChatId } from '../../useChatId';
-import { ContextItem } from '@/features/chat/components/chatBox/contextSelector';
interface ChatThreadPanelProps {
languageModels: LanguageModelInfo[];
@@ -33,33 +32,12 @@ export const ChatThreadPanel = ({
const router = useRouter();
const searchParams = useSearchParams();
const [inputMessage, setInputMessage] = useState | undefined>(undefined);
-
+
// Use the last user's last message to determine what repos and contexts we should select by default.
const lastUserMessage = messages.findLast((message) => message.role === "user");
- const defaultSelectedRepos = lastUserMessage?.metadata?.selectedRepos ?? [];
- const defaultSelectedContexts = lastUserMessage?.metadata?.selectedContexts ?? [];
+ const defaultSelectedSearchScopes = lastUserMessage?.metadata?.selectedSearchScopes ?? [];
+ const [selectedSearchScopes, setSelectedSearchScopes] = useState(defaultSelectedSearchScopes);
- const [selectedItems, setSelectedItems] = useState([
- ...defaultSelectedRepos.map(repoName => {
- const repoInfo = repos.find(r => r.repoName === repoName);
- return {
- type: 'repo' as const,
- value: repoName,
- name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName,
- codeHostType: repoInfo?.codeHostType || ''
- };
- }),
- ...defaultSelectedContexts.map(contextName => {
- const context = searchContexts.find(c => c.name === contextName);
- return {
- type: 'context' as const,
- value: contextName,
- name: contextName,
- repoCount: context?.repoNames.length || 0
- };
- })
- ]);
-
useEffect(() => {
const setChatState = searchParams.get(SET_CHAT_STATE_QUERY_PARAM);
if (!setChatState) {
@@ -67,28 +45,9 @@ export const ChatThreadPanel = ({
}
try {
- const { inputMessage, selectedRepos, selectedContexts } = JSON.parse(setChatState) as SetChatStatePayload;
+ const { inputMessage, selectedSearchScopes } = JSON.parse(setChatState) as SetChatStatePayload;
setInputMessage(inputMessage);
- setSelectedItems([
- ...selectedRepos.map(repoName => {
- const repoInfo = repos.find(r => r.repoName === repoName);
- return {
- type: 'repo' as const,
- value: repoName,
- name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName,
- codeHostType: repoInfo?.codeHostType || ''
- };
- }),
- ...selectedContexts.map(contextName => {
- const context = searchContexts.find(c => c.name === contextName);
- return {
- type: 'context' as const,
- value: contextName,
- name: contextName,
- repoCount: context?.repoNames.length || 0
- };
- })
- ]);
+ setSelectedSearchScopes(selectedSearchScopes);
} catch {
console.error('Invalid message in URL');
}
@@ -97,7 +56,7 @@ export const ChatThreadPanel = ({
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.delete(SET_CHAT_STATE_QUERY_PARAM);
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
- }, [searchParams, router, repos, searchContexts]);
+ }, [searchParams, router]);
return (
diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx
index a2addcd4..91ae3f96 100644
--- a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx
+++ b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx
@@ -5,12 +5,11 @@ import { ChatBox } from "@/features/chat/components/chatBox";
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
-import { LanguageModelInfo } from "@/features/chat/types";
+import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
import { useCallback, useState } from "react";
import { Descendant } from "slate";
import { useLocalStorage } from "usehooks-ts";
-import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
interface NewChatPanelProps {
languageModels: LanguageModelInfo[];
@@ -25,13 +24,13 @@ export const NewChatPanel = ({
searchContexts,
order,
}: NewChatPanelProps) => {
- const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false });
+ const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false });
const { createNewChatThread, isLoading } = useCreateNewChatThread();
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
const onSubmit = useCallback((children: Descendant[]) => {
- createNewChatThread(children, selectedItems);
- }, [createNewChatThread, selectedItems]);
+ createNewChatThread(children, selectedSearchScopes);
+ }, [createNewChatThread, selectedSearchScopes]);
return (
@@ -50,7 +49,7 @@ export const NewChatPanel = ({
preferredSuggestionsBoxPlacement="bottom-start"
isRedirecting={isLoading}
languageModels={languageModels}
- selectedItems={selectedItems}
+ selectedSearchScopes={selectedSearchScopes}
searchContexts={searchContexts}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
@@ -59,8 +58,8 @@ export const NewChatPanel = ({
languageModels={languageModels}
repos={repos}
searchContexts={searchContexts}
- selectedItems={selectedItems}
- onSelectedItemsChange={setSelectedItems}
+ selectedSearchScopes={selectedSearchScopes}
+ onSelectedSearchScopesChange={setSelectedSearchScopes}
isContextSelectorOpen={isContextSelectorOpen}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx
index c8bd472f..259c738b 100644
--- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx
+++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx
@@ -3,13 +3,12 @@
import { Separator } from "@/components/ui/separator";
import { ChatBox } from "@/features/chat/components/chatBox";
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
-import { LanguageModelInfo } from "@/features/chat/types";
+import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
import { useState } from "react";
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
import { useLocalStorage } from "usehooks-ts";
-import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
import { DemoExamples } from "@/types";
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
@@ -34,7 +33,7 @@ export const AgenticSearch = ({
demoExamples,
}: AgenticSearchProps) => {
const { createNewChatThread, isLoading } = useCreateNewChatThread();
- const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false });
+ const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false });
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
return (
@@ -42,12 +41,12 @@ export const AgenticSearch = ({
{
- createNewChatThread(children, selectedItems);
+ createNewChatThread(children, selectedSearchScopes);
}}
className="min-h-[50px]"
isRedirecting={isLoading}
languageModels={languageModels}
- selectedItems={selectedItems}
+ selectedSearchScopes={selectedSearchScopes}
searchContexts={searchContexts}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
@@ -58,8 +57,8 @@ export const AgenticSearch = ({
languageModels={languageModels}
repos={repos}
searchContexts={searchContexts}
- selectedItems={selectedItems}
- onSelectedItemsChange={setSelectedItems}
+ selectedSearchScopes={selectedSearchScopes}
+ onSelectedSearchScopesChange={setSelectedSearchScopes}
isContextSelectorOpen={isContextSelectorOpen}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
@@ -74,10 +73,6 @@ export const AgenticSearch = ({
{demoExamples && (
)}
diff --git a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx b/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx
index e8bb264c..61f64a14 100644
--- a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx
+++ b/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx
@@ -1,32 +1,25 @@
'use client';
+import { useState } from "react";
import Image from "next/image";
-import { Search, LibraryBigIcon, Code, Layers } from "lucide-react";
+import { Search, LibraryBigIcon, Code, Info } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { CardContent } from "@/components/ui/card";
-import { ContextItem, RepoContextItem, SearchContextItem } from "@/features/chat/components/chatBox/contextSelector";
-import { DemoExamples, DemoSearchExample, DemoSearchContextExample, DemoSearchContext } from "@/types";
+import { DemoExamples, DemoSearchExample, DemoSearchScope } from "@/types";
import { cn, getCodeHostIcon } from "@/lib/utils";
-import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
import useCaptureEvent from "@/hooks/useCaptureEvent";
+import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard";
interface AskSourcebotDemoCardsProps {
demoExamples: DemoExamples;
- selectedItems: ContextItem[];
- setSelectedItems: (items: ContextItem[]) => void;
- searchContexts: SearchContextQuery[];
- repos: RepositoryQuery[];
}
export const AskSourcebotDemoCards = ({
demoExamples,
- selectedItems,
- setSelectedItems,
- searchContexts,
- repos,
}: AskSourcebotDemoCardsProps) => {
const captureEvent = useCaptureEvent();
+ const [selectedFilterSearchScope, setSelectedFilterSearchScope] = useState(null);
const handleExampleClick = (example: DemoSearchExample) => {
captureEvent('wa_demo_search_example_card_pressed', {
@@ -39,87 +32,37 @@ export const AskSourcebotDemoCards = ({
}
}
- const getContextIcon = (context: DemoSearchContext, size: number = 20) => {
+ const getSearchScopeIcon = (searchScope: DemoSearchScope, size: number = 20, isSelected: boolean = false) => {
const sizeClass = size === 12 ? "h-3 w-3" : "h-5 w-5";
+ const colorClass = isSelected ? "text-primary-foreground" : "text-muted-foreground";
- if (context.type === "set") {
- return ;
+ if (searchScope.type === "reposet") {
+ return ;
}
- if (context.codeHostType) {
- const codeHostIcon = getCodeHostIcon(context.codeHostType);
+ if (searchScope.codeHostType) {
+ const codeHostIcon = getCodeHostIcon(searchScope.codeHostType);
if (codeHostIcon) {
+ // When selected, icons need to match the inverted badge colors
+ // In light mode selected: light icon on dark bg (invert)
+ // In dark mode selected: dark icon on light bg (no invert, override dark:invert)
+ const selectedIconClass = isSelected
+ ? "invert dark:invert-0"
+ : codeHostIcon.className;
+
return (
);
}
}
- return ;
- }
-
- const handleContextClick = (demoSearchContexts: DemoSearchContext[], contextExample: DemoSearchContextExample) => {
- const context = demoSearchContexts.find((context) => context.id === contextExample.searchContext)
- if (!context) {
- console.error(`Search context ${contextExample.searchContext} not found on handleContextClick`);
- return;
- }
-
- captureEvent('wa_demo_search_context_card_pressed', {
- contextType: context.type,
- contextName: context.value,
- contextDisplayName: context.displayName,
- });
-
- const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo";
- const isSelected = selectedItems.some((item) => item.value === context.value);
- if (isSelected) {
- setSelectedItems(selectedItems.filter((item) => item.value !== context.value));
- return;
- }
-
- const getNewSelectedItem = (): ContextItem | null => {
- if (context.type === "set") {
- const searchContext = searchContexts.find((item) => item.name === context.value);
- if (!searchContext) {
- console.error(`Search context ${context.value} not found on handleContextClick`);
- return null;
- }
-
- return {
- type: 'context',
- value: context.value,
- name: context.displayName,
- repoCount: searchContext.repoNames.length
- } as SearchContextItem;
- } else {
- const repo = repos.find((repo) => repo.repoName === context.value);
- if (!repo) {
- console.error(`Repo ${context.value} not found on handleContextClick`);
- return null;
- }
-
- return {
- type: 'repo',
- value: context.value,
- name: context.displayName,
- codeHostType: repo.codeHostType
- } as RepoContextItem;
- }
- }
-
- const newSelectedItem = getNewSelectedItem();
- if (newSelectedItem) {
- setSelectedItems(isDemoMode ? [newSelectedItem] : [...selectedItems, newSelectedItem]);
- } else {
- console.error(`No new selected item found on handleContextClick`);
- }
+ return ;
}
return (
@@ -139,110 +82,91 @@ export const AskSourcebotDemoCards = ({
)}
-
- {/* Search Context Row */}
-
-
-
-
-
Search Contexts
-
-
Select the context you want to ask questions about
-
-
- {demoExamples.searchContextExamples.map((contextExample) => {
- const context = demoExamples.searchContexts.find((context) => context.id === contextExample.searchContext)
- if (!context) {
- console.error(`Search context ${contextExample.searchContext} not found on handleContextClick`);
- return null;
- }
-
- const isSelected = selectedItems.some(
- (selected) => (selected.type === 'context' && selected.value === context.value) ||
- (selected.type === 'repo' && selected.value === context.value)
- );
-
- const searchContext = searchContexts.find((item) => item.name === context.value);
- const numRepos = searchContext ? searchContext.repoNames.length : undefined;
- return (
-
handleContextClick(demoExamples.searchContexts, contextExample)}
- >
-
-
-
- {getContextIcon(context)}
-
-
-
-
- {context.displayName}
-
- {numRepos && (
-
- {numRepos} repos
-
- )}
-
-
{contextExample.description}
-
-
-
-
- )
- })}
-
-
-
+
{/* Example Searches Row */}
-
-
-
-
Community Ask Results
+
+
+
+
Community Ask Results
-
Check out these featured ask results from the community
+
+ {/* Search Scope Filter */}
+
+
+
{
+ setSelectedFilterSearchScope(null);
+ }}
+ >
+ All
+
+ {demoExamples.searchScopes.map((searchScope) => (
+
{
+ setSelectedFilterSearchScope(searchScope.id);
+ }}
+ >
+ {getSearchScopeIcon(searchScope, 12, selectedFilterSearchScope === searchScope.id)}
+ {searchScope.displayName}
+
+ ))}
+
+
- {demoExamples.searchExamples.map((example) => {
- const searchContexts = demoExamples.searchContexts.filter((context) => example.searchContext.includes(context.id))
- return (
-
handleExampleClick(example)}
- >
-
-
-
- {searchContexts.map((context) => (
-
- {getContextIcon(context, 12)}
- {context.displayName}
-
- ))}
+ {demoExamples.searchExamples
+ .filter((example) => {
+ if (selectedFilterSearchScope === null) return true;
+ return example.searchScopes.includes(selectedFilterSearchScope);
+ })
+ .map((example) => {
+ const searchScopes = demoExamples.searchScopes.filter((searchScope) => example.searchScopes.includes(searchScope.id))
+ return (
+
handleExampleClick(example)}
+ >
+
+
+
+ {searchScopes.map((searchScope) => (
+
+ {getSearchScopeIcon(searchScope, 12)}
+ {searchScope.displayName}
+
+ ))}
+
+
+
+ {example.title}
+
+
+ {example.description}
+
+
-
-
- {example.title}
-
-
- {example.description}
-
-
-
-
-
- )
- })}
+
+
+ )
+ })}
diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts
index c52b9e18..54325599 100644
--- a/packages/web/src/app/api/(server)/chat/route.ts
+++ b/packages/web/src/app/api/(server)/chat/route.ts
@@ -2,7 +2,7 @@ import { sew, withAuth, withOrgMembership } from "@/actions";
import { env } from "@/env.mjs";
import { _getConfiguredLanguageModelsFull, updateChatMessages, updateChatName } from "@/features/chat/actions";
import { createAgentStream } from "@/features/chat/agent";
-import { additionalChatRequestParamsSchema, SBChatMessage } from "@/features/chat/types";
+import { additionalChatRequestParamsSchema, SBChatMessage, SearchScope } from "@/features/chat/types";
import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils";
import { ErrorCode } from "@/lib/errorCodes";
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
@@ -64,12 +64,11 @@ export async function POST(req: Request) {
return serviceErrorResponse(schemaValidationError(parsed.error));
}
- const { messages, id, selectedRepos, selectedContexts, languageModelId } = parsed.data;
+ const { messages, id, selectedSearchScopes, languageModelId } = parsed.data;
const response = await chatHandler({
messages,
id,
- selectedRepos,
- selectedContexts,
+ selectedSearchScopes,
languageModelId,
}, domain);
@@ -93,12 +92,11 @@ const mergeStreamAsync = async (stream: StreamTextResult
, writer: UIMe
interface ChatHandlerProps {
messages: SBChatMessage[];
id: string;
- selectedRepos: string[];
- selectedContexts?: string[];
+ selectedSearchScopes: SearchScope[];
languageModelId: string;
}
-const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageModelId }: ChatHandlerProps, domain: string) => sew(async () =>
+const chatHandler = ({ messages, id, selectedSearchScopes, languageModelId }: ChatHandlerProps, domain: string) => sew(async () =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
const chat = await prisma.chat.findUnique({
@@ -188,26 +186,30 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo
const startTime = new Date();
- // Expand search contexts to repos
- let expandedRepos = [...selectedRepos];
- if (selectedContexts && selectedContexts.length > 0) {
- const searchContexts = await prisma.searchContext.findMany({
- where: {
- orgId: org.id,
- name: { in: selectedContexts }
- },
- include: {
- repos: true
+ const expandedReposArrays = await Promise.all(selectedSearchScopes.map(async (scope) => {
+ if (scope.type === 'repo') {
+ return [scope.value];
+ }
+
+ if (scope.type === 'reposet') {
+ const reposet = await prisma.searchContext.findFirst({
+ where: {
+ orgId: org.id,
+ name: scope.value
+ },
+ include: {
+ repos: true
+ }
+ });
+
+ if (reposet) {
+ return reposet.repos.map(repo => repo.name);
}
- });
+ }
- const contextRepos = searchContexts.flatMap(context =>
- context.repos.map(repo => repo.name)
- );
-
- // Combine and deduplicate repos
- expandedRepos = Array.from(new Set([...selectedRepos, ...contextRepos]));
- }
+ return [];
+ }));
+ const expandedRepos = expandedReposArrays.flat();
const researchStream = await createAgentStream({
model,
@@ -215,7 +217,7 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo
headers,
inputMessages: messageHistory,
inputSources: sources,
- selectedRepos: expandedRepos,
+ searchScopeRepoNames: expandedRepos,
onWriteSource: (source) => {
writer.write({
type: 'data-source',
@@ -241,6 +243,7 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo
totalOutputTokens: totalUsage.outputTokens,
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
+ selectedSearchScopes,
traceId,
}
})
diff --git a/packages/web/src/components/atMentionInfoCard.tsx b/packages/web/src/components/atMentionInfoCard.tsx
new file mode 100644
index 00000000..74424074
--- /dev/null
+++ b/packages/web/src/components/atMentionInfoCard.tsx
@@ -0,0 +1,15 @@
+import { AtSignIcon } from "lucide-react";
+
+export const AtMentionInfoCard = () => {
+ return (
+
+
+
+ When asking Sourcebot a question, you can @ mention files to include them in the context of the search.
+
+
+ );
+};
\ No newline at end of file
diff --git a/packages/web/src/components/searchScopeInfoCard.tsx b/packages/web/src/components/searchScopeInfoCard.tsx
new file mode 100644
index 00000000..633dcbbb
--- /dev/null
+++ b/packages/web/src/components/searchScopeInfoCard.tsx
@@ -0,0 +1,41 @@
+import Image from "next/image";
+import { LibraryBigIcon, Code, ScanSearchIcon } from "lucide-react";
+import { cn, getCodeHostIcon } from "@/lib/utils";
+
+export const SearchScopeInfoCard = () => {
+ return (
+
+
+
+
Search Scope
+
+
+ When asking Sourcebot a question, you can select one or more scopes to constrain the search.
+ There are two different types of search scopes:
+
+
+ {(() => {
+ const githubIcon = getCodeHostIcon("github");
+ return githubIcon ? (
+
+ ) : (
+
+ );
+ })()}
+ Repository: A single repository, indicated by the code host icon.
+
+
+
+ Reposet: A set of repositories, indicated by the library icon.
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts
index 7f3ef93d..d443b99b 100644
--- a/packages/web/src/features/chat/agent.ts
+++ b/packages/web/src/features/chat/agent.ts
@@ -16,7 +16,7 @@ interface AgentOptions {
model: LanguageModel;
providerOptions?: ProviderOptions;
headers?: Record;
- selectedRepos: string[];
+ searchScopeRepoNames: string[];
inputMessages: ModelMessage[];
inputSources: Source[];
onWriteSource: (source: Source) => void;
@@ -35,12 +35,12 @@ export const createAgentStream = async ({
headers,
inputMessages,
inputSources,
- selectedRepos,
+ searchScopeRepoNames,
onWriteSource,
traceId,
}: AgentOptions) => {
const baseSystemPrompt = createBaseSystemPrompt({
- selectedRepos,
+ searchScopeRepoNames,
});
const stream = streamText({
@@ -50,7 +50,7 @@ export const createAgentStream = async ({
system: baseSystemPrompt,
messages: inputMessages,
tools: {
- [toolNames.searchCode]: createCodeSearchTool(selectedRepos),
+ [toolNames.searchCode]: createCodeSearchTool(searchScopeRepoNames),
[toolNames.readFiles]: readFilesTool,
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
@@ -150,11 +150,11 @@ export const createAgentStream = async ({
}
interface BaseSystemPromptOptions {
- selectedRepos: string[];
+ searchScopeRepoNames: string[];
}
export const createBaseSystemPrompt = ({
- selectedRepos,
+ searchScopeRepoNames,
}: BaseSystemPromptOptions) => {
return `
You are a powerful agentic AI code assistant built into Sourcebot, the world's best code-intelligence platform. Your job is to help developers understand and navigate their large codebases.
@@ -176,7 +176,7 @@ Your workflow has two distinct phases:
The user has selected the following repositories for analysis:
-${selectedRepos.map(repo => `- ${repo}`).join('\n')}
+${searchScopeRepoNames.map(repo => `- ${repo}`).join('\n')}
diff --git a/packages/web/src/features/chat/components/chatBox/chatBox.tsx b/packages/web/src/features/chat/components/chatBox/chatBox.tsx
index c33cbdf1..f5cfc974 100644
--- a/packages/web/src/features/chat/components/chatBox/chatBox.tsx
+++ b/packages/web/src/features/chat/components/chatBox/chatBox.tsx
@@ -3,7 +3,7 @@
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor } from "@/features/chat/types";
+import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
import { insertMention, slateContentToString } from "@/features/chat/utils";
import { cn, IS_MAC } from "@/lib/utils";
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
@@ -18,7 +18,6 @@ import { Suggestion } from "./types";
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
import { useSuggestionsData } from "./useSuggestionsData";
import { useToast } from "@/components/hooks/use-toast";
-import { ContextItem } from "./contextSelector";
import { SearchContextQuery } from "@/lib/types";
interface ChatBoxProps {
@@ -29,7 +28,7 @@ interface ChatBoxProps {
isRedirecting?: boolean;
isGenerating?: boolean;
languageModels: LanguageModelInfo[];
- selectedItems: ContextItem[];
+ selectedSearchScopes: SearchScope[];
searchContexts: SearchContextQuery[];
onContextSelectorOpenChanged: (isOpen: boolean) => void;
}
@@ -42,7 +41,7 @@ export const ChatBox = ({
isRedirecting,
isGenerating,
languageModels,
- selectedItems,
+ selectedSearchScopes,
searchContexts,
onContextSelectorOpenChanged,
}: ChatBoxProps) => {
@@ -53,15 +52,15 @@ export const ChatBox = ({
const { suggestions, isLoading } = useSuggestionsData({
suggestionMode,
suggestionQuery,
- selectedRepos: selectedItems.map((item) => {
+ selectedRepos: selectedSearchScopes.map((item) => {
if (item.type === 'repo') {
return [item.value];
}
- if (item.type === 'context') {
- const context = searchContexts.find((context) => context.name === item.value);
- if (context) {
- return context.repoNames;
+ if (item.type === 'reposet') {
+ const reposet = searchContexts.find((reposet) => reposet.name === item.value);
+ if (reposet) {
+ return reposet.repoNames;
}
}
@@ -130,7 +129,7 @@ export const ChatBox = ({
}
}
- if (selectedItems.length === 0) {
+ if (selectedSearchScopes.length === 0) {
return {
isSubmitDisabled: true,
isSubmitDisabledReason: "no-repos-selected",
@@ -154,7 +153,7 @@ export const ChatBox = ({
editor.children,
isRedirecting,
isGenerating,
- selectedItems.length,
+ selectedSearchScopes.length,
selectedLanguageModel,
])
@@ -162,7 +161,7 @@ export const ChatBox = ({
if (isSubmitDisabled) {
if (isSubmitDisabledReason === "no-repos-selected") {
toast({
- description: "⚠️ You must select at least one search context",
+ description: "⚠️ You must select at least one search scope",
variant: "destructive",
});
onContextSelectorOpenChanged(true);
@@ -284,7 +283,7 @@ export const ChatBox = ({
>
- You must select at least one search context
+ You must select at least one search scope
)}
diff --git a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx
index 8744e9ac..eba67729 100644
--- a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx
+++ b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx
@@ -1,25 +1,25 @@
'use client';
-import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
-import { LanguageModelInfo } from "@/features/chat/types";
+import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
import { AtSignIcon } from "lucide-react";
import { useCallback } from "react";
-import { useHotkeys } from "react-hotkeys-hook";
import { ReactEditor, useSlate } from "slate-react";
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
import { LanguageModelSelector } from "./languageModelSelector";
-import { ContextSelector, type ContextItem } from "./contextSelector";
+import { SearchScopeSelector } from "./searchScopeSelector";
+import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard";
+import { AtMentionInfoCard } from "@/components/atMentionInfoCard";
export interface ChatBoxToolbarProps {
languageModels: LanguageModelInfo[];
repos: RepositoryQuery[];
searchContexts: SearchContextQuery[];
- selectedItems: ContextItem[];
- onSelectedItemsChange: (items: ContextItem[]) => void;
+ selectedSearchScopes: SearchScope[];
+ onSelectedSearchScopesChange: (items: SearchScope[]) => void;
isContextSelectorOpen: boolean;
onContextSelectorOpenChanged: (isOpen: boolean) => void;
}
@@ -28,8 +28,8 @@ export const ChatBoxToolbar = ({
languageModels,
repos,
searchContexts,
- selectedItems,
- onSelectedItemsChange,
+ selectedSearchScopes,
+ onSelectedSearchScopesChange,
isContextSelectorOpen,
onContextSelectorOpenChanged,
}: ChatBoxToolbarProps) => {
@@ -40,15 +40,6 @@ export const ChatBoxToolbar = ({
ReactEditor.focus(editor);
}, [editor]);
- useHotkeys("alt+mod+p", (e) => {
- e.preventDefault();
- onAddContext();
- }, {
- enableOnFormTags: true,
- enableOnContentEditable: true,
- description: "Add context",
- });
-
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
});
@@ -66,30 +57,25 @@ export const ChatBoxToolbar = ({
-
-
-
- Add context
+
+
-
-
- Search contexts and repositories to scope conversation to.
+
+
{languageModels.length > 0 && (
diff --git a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx
similarity index 64%
rename from packages/web/src/features/chat/components/chatBox/contextSelector.tsx
rename to packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx
index 103b0195..a1c8f595 100644
--- a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx
+++ b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx
@@ -4,13 +4,10 @@ import * as React from "react";
import {
CheckIcon,
ChevronDown,
- FolderIcon,
- LayersIcon,
- LibraryBigIcon,
+ ScanSearchIcon,
} from "lucide-react";
-import Image from "next/image";
-import { cn, getCodeHostIcon } from "@/lib/utils";
+import { cn } from "@/lib/utils";
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -28,44 +25,30 @@ import {
CommandList,
CommandSeparator,
} from "@/components/ui/command";
+import { RepoSetSearchScope, RepoSearchScope, SearchScope } from "../../types";
+import { SearchScopeIcon } from "../searchScopeIcon";
-export type RepoContextItem = {
- type: 'repo';
- value: string;
- name: string;
- codeHostType: string;
-}
-
-export type SearchContextItem = {
- type: 'context';
- value: string;
- name: string;
- repoCount: number;
-}
-
-export type ContextItem = RepoContextItem | SearchContextItem;
-
-interface ContextSelectorProps extends React.ButtonHTMLAttributes {
+interface SearchScopeSelectorProps extends React.ButtonHTMLAttributes {
repos: RepositoryQuery[];
searchContexts: SearchContextQuery[];
- selectedItems: ContextItem[];
- onSelectedItemsChange: (items: ContextItem[]) => void;
+ selectedSearchScopes: SearchScope[];
+ onSelectedSearchScopesChange: (items: SearchScope[]) => void;
className?: string;
isOpen: boolean;
onOpenChanged: (isOpen: boolean) => void;
}
-export const ContextSelector = React.forwardRef<
+export const SearchScopeSelector = React.forwardRef<
HTMLButtonElement,
- ContextSelectorProps
+ SearchScopeSelectorProps
>(
(
{
repos,
searchContexts,
- onSelectedItemsChange,
className,
- selectedItems,
+ selectedSearchScopes,
+ onSelectedSearchScopesChange,
isOpen,
onOpenChanged,
...props
@@ -81,72 +64,62 @@ export const ContextSelector = React.forwardRef<
if (event.key === "Enter") {
onOpenChanged(true);
} else if (event.key === "Backspace" && !event.currentTarget.value) {
- const newSelectedItems = [...selectedItems];
+ const newSelectedItems = [...selectedSearchScopes];
newSelectedItems.pop();
- onSelectedItemsChange(newSelectedItems);
+ onSelectedSearchScopesChange(newSelectedItems);
}
};
- const toggleItem = (item: ContextItem) => {
+ const toggleItem = (item: SearchScope) => {
// Store current scroll position before state update
if (scrollContainerRef.current) {
scrollPosition.current = scrollContainerRef.current.scrollTop;
}
- const isSelected = selectedItems.some(
+ const isSelected = selectedSearchScopes.some(
(selected) => selected.type === item.type && selected.value === item.value
);
- const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo";
-
- let newSelectedItems: ContextItem[];
- if (isSelected) {
- newSelectedItems = selectedItems.filter(
+ const newSelectedItems = isSelected ?
+ selectedSearchScopes.filter(
(selected) => !(selected.type === item.type && selected.value === item.value)
- );
- } else {
- // Limit selected context to 1 in demo mode
- if (isDemoMode) {
- newSelectedItems = [item];
- } else {
- newSelectedItems = [...selectedItems, item];
- }
- }
+ ) :
+ [...selectedSearchScopes, item];
- onSelectedItemsChange(newSelectedItems);
+ onSelectedSearchScopesChange(newSelectedItems);
};
const handleClear = () => {
- onSelectedItemsChange([]);
+ onSelectedSearchScopesChange([]);
};
const handleTogglePopover = () => {
onOpenChanged(!isOpen);
};
- const allItems = React.useMemo(() => {
- const contextItems: ContextItem[] = searchContexts.map(context => ({
- type: 'context' as const,
+ const allSearchScopeItems = React.useMemo(() => {
+ const repoSetSearchScopeItems: RepoSetSearchScope[] = searchContexts.map(context => ({
+ type: 'reposet' as const,
value: context.name,
name: context.name,
repoCount: context.repoNames.length
}));
- const repoItems: ContextItem[] = repos.map(repo => ({
+ const repoSearchScopeItems: RepoSearchScope[] = repos.map(repo => ({
type: 'repo' as const,
value: repo.repoName,
name: repo.repoDisplayName || repo.repoName.split('/').pop() || repo.repoName,
codeHostType: repo.codeHostType,
}));
- return [...contextItems, ...repoItems];
+ return [...repoSetSearchScopeItems, ...repoSearchScopeItems];
}, [repos, searchContexts]);
- const sortedItems = React.useMemo(() => {
- return allItems
+ const sortedSearchScopeItems = React.useMemo(() => {
+ return allSearchScopeItems
.map((item) => ({
item,
- isSelected: selectedItems.some(
+ isSelected: selectedSearchScopes.some(
(selected) => selected.type === item.type && selected.value === item.value
)
}))
@@ -154,19 +127,19 @@ export const ContextSelector = React.forwardRef<
// Selected items first
if (a.isSelected && !b.isSelected) return -1;
if (!a.isSelected && b.isSelected) return 1;
- // Then contexts before repos
- if (a.item.type === 'context' && b.item.type === 'repo') return -1;
- if (a.item.type === 'repo' && b.item.type === 'context') return 1;
+ // Then reposets before repos
+ if (a.item.type === 'reposet' && b.item.type === 'repo') return -1;
+ if (a.item.type === 'repo' && b.item.type === 'reposet') return 1;
return 0;
})
- }, [allItems, selectedItems]);
+ }, [allSearchScopeItems, selectedSearchScopes]);
// Restore scroll position after re-render
React.useEffect(() => {
if (scrollContainerRef.current && scrollPosition.current > 0) {
scrollContainerRef.current.scrollTop = scrollPosition.current;
}
- }, [sortedItems]);
+ }, [sortedSearchScopeItems]);
return (
-
+
{
- selectedItems.length === 0 ? `Select context` :
- selectedItems.length === 1 ? selectedItems[0].name :
- `${selectedItems.length} selected`
+ selectedSearchScopes.length === 0 ? `Search scopes` :
+ selectedSearchScopes.length === 1 ? selectedSearchScopes[0].name :
+ `${selectedSearchScopes.length} selected`
}
@@ -205,13 +178,13 @@ export const ContextSelector = React.forwardRef<
>
No results found.
- {sortedItems.map(({ item, isSelected }) => {
+ {sortedSearchScopeItems.map(({ item, isSelected }) => {
return (
- {item.type === 'context' ? (
-
- ) : (
- // Render code host icon for repos
- (() => {
- const codeHostIcon = item.codeHostType ? getCodeHostIcon(item.codeHostType) : null;
- return codeHostIcon ? (
-
- ) : (
-
- );
- })()
- )}
+
{item.name}
- {item.type === 'context' && (
+ {item.type === 'reposet' && (
- {selectedItems.length > 0 && (
+ {selectedSearchScopes.length > 0 && (
<>
void;
+ selectedSearchScopes: SearchScope[];
+ onSelectedSearchScopesChange: (items: SearchScope[]) => void;
isChatReadonly: boolean;
}
@@ -48,8 +47,8 @@ export const ChatThread = ({
languageModels,
repos,
searchContexts,
- selectedItems,
- onSelectedItemsChange,
+ selectedSearchScopes,
+ onSelectedSearchScopesChange,
isChatReadonly,
}: ChatThreadProps) => {
const domain = useDomain();
@@ -62,12 +61,6 @@ export const ChatThread = ({
const router = useRouter();
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
- const { selectedRepos, selectedContexts } = useMemo(() => {
- const repos = selectedItems.filter(item => item.type === 'repo').map(item => item.value);
- const contexts = selectedItems.filter(item => item.type === 'context').map(item => item.value);
- return { selectedRepos: repos, selectedContexts: contexts };
- }, [selectedItems]);
-
// Initial state is from attachments that exist in in the chat history.
const [sources, setSources] = useState(
initialMessages?.flatMap((message) =>
@@ -122,12 +115,11 @@ export const ChatThread = ({
_sendMessage(message, {
body: {
- selectedRepos,
- selectedContexts,
+ selectedSearchScopes,
languageModelId: selectedLanguageModel.model,
} satisfies AdditionalChatRequestParams,
- });
- }, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedContexts]);
+ });
+ }, [_sendMessage, selectedLanguageModel, toast, selectedSearchScopes]);
const messagePairs = useMessagePairs(messages);
@@ -243,13 +235,13 @@ export const ChatThread = ({
const text = slateContentToString(children);
const mentions = getAllMentionElements(children);
- const message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos, selectedContexts);
+ const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes);
sendMessage(message);
setIsAutoScrollEnabled(true);
resetEditor(editor);
- }, [sendMessage, selectedRepos, selectedContexts]);
+ }, [sendMessage, selectedSearchScopes]);
return (
<>
@@ -327,7 +319,7 @@ export const ChatThread = ({
isGenerating={status === "streaming" || status === "submitted"}
onStop={stop}
languageModels={languageModels}
- selectedItems={selectedItems}
+ selectedSearchScopes={selectedSearchScopes}
searchContexts={searchContexts}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
@@ -336,8 +328,8 @@ export const ChatThread = ({
languageModels={languageModels}
repos={repos}
searchContexts={searchContexts}
- selectedItems={selectedItems}
- onSelectedItemsChange={onSelectedItemsChange}
+ selectedSearchScopes={selectedSearchScopes}
+ onSelectedSearchScopesChange={onSelectedSearchScopesChange}
isContextSelectorOpen={isContextSelectorOpen}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
diff --git a/packages/web/src/features/chat/components/chatThread/detailsCard.tsx b/packages/web/src/features/chat/components/chatThread/detailsCard.tsx
index 7bfe49a0..ab3b676c 100644
--- a/packages/web/src/features/chat/components/chatThread/detailsCard.tsx
+++ b/packages/web/src/features/chat/components/chatThread/detailsCard.tsx
@@ -4,14 +4,16 @@ import { Card, CardContent } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Separator } from '@/components/ui/separator';
import { Skeleton } from '@/components/ui/skeleton';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
-import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, Zap } from 'lucide-react';
+import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, ScanSearchIcon, Zap } from 'lucide-react';
import { MarkdownRenderer } from './markdownRenderer';
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
import { SBChatMessageMetadata, SBChatMessagePart } from '../../types';
+import { SearchScopeIcon } from '../searchScopeIcon';
interface DetailsCardProps {
@@ -61,6 +63,28 @@ export const DetailsCard = ({
{!isStreaming && (
<>
+ {metadata?.selectedSearchScopes && (
+
+
+
+
+ {metadata.selectedSearchScopes.length} search scope{metadata.selectedSearchScopes.length === 1 ? '' : 's'}
+
+
+
+
+
+ {metadata.selectedSearchScopes.map((item) => (
+
+
+ {item.name}
+
+ ))}
+
+
+
+
+ )}
{metadata?.modelName && (
diff --git a/packages/web/src/features/chat/components/searchScopeIcon.tsx b/packages/web/src/features/chat/components/searchScopeIcon.tsx
new file mode 100644
index 00000000..933471f4
--- /dev/null
+++ b/packages/web/src/features/chat/components/searchScopeIcon.tsx
@@ -0,0 +1,32 @@
+import { cn, getCodeHostIcon } from "@/lib/utils";
+import { FolderIcon, LibraryBigIcon } from "lucide-react";
+import Image from "next/image";
+import { SearchScope } from "../types";
+
+interface SearchScopeIconProps {
+ searchScope: SearchScope;
+ className?: string;
+}
+
+export const SearchScopeIcon = ({ searchScope, className = "h-4 w-4" }: SearchScopeIconProps) => {
+ if (searchScope.type === 'reposet') {
+ return ;
+ } else {
+ // Render code host icon for repos
+ const codeHostIcon = searchScope.codeHostType ? getCodeHostIcon(searchScope.codeHostType) : null;
+ if (codeHostIcon) {
+ const size = className.includes('h-3') ? 12 : 16;
+ return (
+
+ );
+ } else {
+ return ;
+ }
+ }
+};
\ No newline at end of file
diff --git a/packages/web/src/features/chat/types.ts b/packages/web/src/features/chat/types.ts
index b4c6bc8d..18aa55a2 100644
--- a/packages/web/src/features/chat/types.ts
+++ b/packages/web/src/features/chat/types.ts
@@ -39,6 +39,28 @@ export const referenceSchema = z.discriminatedUnion('type', [
]);
export type Reference = z.infer;
+export const repoSearchScopeSchema = z.object({
+ type: z.literal('repo'),
+ value: z.string(),
+ name: z.string(),
+ codeHostType: z.string(),
+});
+export type RepoSearchScope = z.infer;
+
+export const repoSetSearchScopeSchema = z.object({
+ type: z.literal('reposet'),
+ value: z.string(),
+ name: z.string(),
+ repoCount: z.number(),
+});
+export type RepoSetSearchScope = z.infer;
+
+export const searchScopeSchema = z.discriminatedUnion('type', [
+ repoSearchScopeSchema,
+ repoSetSearchScopeSchema,
+]);
+export type SearchScope = z.infer;
+
export const sbChatMessageMetadataSchema = z.object({
modelName: z.string().optional(),
totalInputTokens: z.number().optional(),
@@ -50,8 +72,7 @@ export const sbChatMessageMetadataSchema = z.object({
timestamp: z.string(), // ISO date string
userId: z.string(),
})).optional(),
- selectedRepos: z.array(z.string()).optional(),
- selectedContexts: z.array(z.string()).optional(),
+ selectedSearchScopes: z.array(searchScopeSchema).optional(),
traceId: z.string().optional(),
});
@@ -139,8 +160,7 @@ export const SET_CHAT_STATE_QUERY_PARAM = 'setChatState';
export type SetChatStatePayload = {
inputMessage: CreateUIMessage;
- selectedRepos: string[];
- selectedContexts: string[];
+ selectedSearchScopes: SearchScope[];
}
@@ -157,7 +177,6 @@ export type LanguageModelInfo = {
// Additional request body data that we send along to the chat API.
export const additionalChatRequestParamsSchema = z.object({
languageModelId: z.string(),
- selectedRepos: z.array(z.string()),
- selectedContexts: z.array(z.string()),
+ selectedSearchScopes: z.array(searchScopeSchema),
});
export type AdditionalChatRequestParams = z.infer;
\ No newline at end of file
diff --git a/packages/web/src/features/chat/useCreateNewChatThread.ts b/packages/web/src/features/chat/useCreateNewChatThread.ts
index 54aaf14d..f0de7e1a 100644
--- a/packages/web/src/features/chat/useCreateNewChatThread.ts
+++ b/packages/web/src/features/chat/useCreateNewChatThread.ts
@@ -10,8 +10,7 @@ import { useRouter } from "next/navigation";
import { createChat } from "./actions";
import { isServiceError } from "@/lib/utils";
import { createPathWithQueryParams } from "@/lib/utils";
-import { SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from "./types";
-import { ContextItem } from "./components/chatBox/contextSelector";
+import { SearchScope, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from "./types";
export const useCreateNewChatThread = () => {
const domain = useDomain();
@@ -19,15 +18,11 @@ export const useCreateNewChatThread = () => {
const { toast } = useToast();
const router = useRouter();
- const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: ContextItem[]) => {
+ const createNewChatThread = useCallback(async (children: Descendant[], selectedSearchScopes: SearchScope[]) => {
const text = slateContentToString(children);
const mentions = getAllMentionElements(children);
- // Extract repos and contexts from selectedItems
- const selectedRepos = selectedItems.filter(item => item.type === 'repo').map(item => item.value);
- const selectedContexts = selectedItems.filter(item => item.type === 'context').map(item => item.value);
-
- const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedContexts);
+ const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedSearchScopes);
setIsLoading(true);
const response = await createChat(domain);
@@ -42,8 +37,7 @@ export const useCreateNewChatThread = () => {
const url = createPathWithQueryParams(`/${domain}/chat/${response.id}`,
[SET_CHAT_STATE_QUERY_PARAM, JSON.stringify({
inputMessage,
- selectedRepos,
- selectedContexts,
+ selectedSearchScopes,
} satisfies SetChatStatePayload)],
);
diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts
index e2495664..d84835ef 100644
--- a/packages/web/src/features/chat/utils.ts
+++ b/packages/web/src/features/chat/utils.ts
@@ -12,6 +12,7 @@ import {
SBChatMessage,
SBChatMessagePart,
SBChatMessageToolTypes,
+ SearchScope,
Source,
} from "./types"
@@ -172,7 +173,7 @@ export const addLineNumbers = (source: string, lineOffset = 1) => {
return source.split('\n').map((line, index) => `${index + lineOffset}:${line}`).join('\n');
}
-export const createUIMessage = (text: string, mentions: MentionData[], selectedRepos: string[], selectedContexts: string[]): CreateUIMessage => {
+export const createUIMessage = (text: string, mentions: MentionData[], selectedSearchScopes: SearchScope[]): CreateUIMessage => {
// Converts applicable mentions into sources.
const sources: Source[] = mentions
.map((mention) => {
@@ -205,8 +206,7 @@ export const createUIMessage = (text: string, mentions: MentionData[], selectedR
})) as UIMessagePart<{ source: Source }, SBChatMessageToolTypes>[],
],
metadata: {
- selectedRepos,
- selectedContexts,
+ selectedSearchScopes,
},
}
}
diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts
index 5b55b9db..2ceb5d30 100644
--- a/packages/web/src/types.ts
+++ b/packages/web/src/types.ts
@@ -4,11 +4,11 @@ export const orgMetadataSchema = z.object({
anonymousAccessEnabled: z.boolean().optional(),
})
-export const demoSearchContextSchema = z.object({
+export const demoSearchScopeSchema = z.object({
id: z.number(),
displayName: z.string(),
value: z.string(),
- type: z.enum(["repo", "set"]),
+ type: z.enum(["repo", "reposet"]),
codeHostType: z.string().optional(),
})
@@ -16,22 +16,15 @@ export const demoSearchExampleSchema = z.object({
title: z.string(),
description: z.string(),
url: z.string(),
- searchContext: z.array(z.number())
-})
-
-export const demoSearchContextExampleSchema = z.object({
- searchContext: z.number(),
- description: z.string(),
+ searchScopes: z.array(z.number())
})
export const demoExamplesSchema = z.object({
- searchContexts: demoSearchContextSchema.array(),
+ searchScopes: demoSearchScopeSchema.array(),
searchExamples: demoSearchExampleSchema.array(),
- searchContextExamples: demoSearchContextExampleSchema.array(),
})
export type OrgMetadata = z.infer;
export type DemoExamples = z.infer;
-export type DemoSearchContext = z.infer;
-export type DemoSearchExample = z.infer;
-export type DemoSearchContextExample = z.infer;
\ No newline at end of file
+export type DemoSearchScope = z.infer;
+export type DemoSearchExample = z.infer;
\ No newline at end of file