From d40973898b29349d227808d7e560b1b50e57e400 Mon Sep 17 00:00:00 2001 From: msukkari Date: Fri, 25 Jul 2025 19:19:07 -0700 Subject: [PATCH 1/7] new context selector --- packages/web/src/actions.ts | 4 + .../chat/[id]/components/chatThreadPanel.tsx | 41 ++- .../[domain]/chat/components/newChatPanel.tsx | 21 +- .../components/homepage/agenticSearch.tsx | 21 +- .../web/src/app/api/(server)/chat/route.ts | 29 +- .../chat/components/chatBox/chatBox.tsx | 21 +- .../components/chatBox/chatBoxToolbar.tsx | 49 ++- .../components/chatBox/contextSelector.tsx | 278 ++++++++++++++++++ .../chat/components/chatBox/repoSelector.tsx | 191 ------------ .../chat/components/chatThread/chatThread.tsx | 37 ++- packages/web/src/features/chat/types.ts | 3 + .../features/chat/useCreateNewChatThread.ts | 11 +- packages/web/src/features/chat/utils.ts | 3 +- 13 files changed, 445 insertions(+), 264 deletions(-) create mode 100644 packages/web/src/features/chat/components/chatBox/contextSelector.tsx delete mode 100644 packages/web/src/features/chat/components/chatBox/repoSelector.tsx diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index a78c9e140..fa83d2b9e 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -1862,11 +1862,15 @@ export const getSearchContexts = async (domain: string) => sew(() => where: { orgId: org.id, }, + include: { + repos: true, + }, }); return searchContexts.map((context) => ({ name: context.name, description: context.description ?? undefined, + repoCount: context.repos.length, })); }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true )); 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 38d127815..468091aac 100644 --- a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx @@ -8,6 +8,7 @@ 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[]; @@ -31,8 +32,23 @@ export const ChatThreadPanel = ({ const searchParams = useSearchParams(); const [inputMessage, setInputMessage] = useState | undefined>(undefined); - // Use the last user's last message to determine what repos we should select by default. - const [selectedRepos, setSelectedRepos] = useState(messages.findLast((message) => message.role === "user")?.metadata?.selectedRepos ?? []); + // 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 [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(context => ({ type: 'context' as const, value: context, name: context })) + ]); useEffect(() => { const setChatState = searchParams.get(SET_CHAT_STATE_QUERY_PARAM); @@ -41,9 +57,20 @@ export const ChatThreadPanel = ({ } try { - const { inputMessage, selectedRepos } = JSON.parse(setChatState) as SetChatStatePayload; + const { inputMessage, selectedRepos, selectedContexts } = JSON.parse(setChatState) as SetChatStatePayload; setInputMessage(inputMessage); - setSelectedRepos(selectedRepos); + 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(context => ({ type: 'context' as const, value: context, name: context })) + ]); } catch { console.error('Invalid message in URL'); } @@ -52,7 +79,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]); + }, [searchParams, router, repos]); return ( diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx index e8573864d..5c9995d58 100644 --- a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx +++ b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx @@ -10,6 +10,7 @@ import { RepositoryQuery } 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[]; @@ -22,13 +23,13 @@ export const NewChatPanel = ({ repos, order, }: NewChatPanelProps) => { - const [selectedRepos, setSelectedRepos] = useLocalStorage("selectedRepos", [], { initializeWithValue: false }); + const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); const { createNewChatThread, isLoading } = useCreateNewChatThread(); - const [isRepoSelectorOpen, setIsRepoSelectorOpen] = useState(false); + const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); const onSubmit = useCallback((children: Descendant[]) => { - createNewChatThread(children, selectedRepos); - }, [createNewChatThread, selectedRepos]); + createNewChatThread(children, selectedItems); + }, [createNewChatThread, selectedItems]); return ( @@ -47,17 +48,17 @@ export const NewChatPanel = ({ preferredSuggestionsBoxPlacement="bottom-start" isRedirecting={isLoading} languageModels={languageModels} - selectedRepos={selectedRepos} - onRepoSelectorOpenChanged={setIsRepoSelectorOpen} + selectedItems={selectedItems} + 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 957b6c5f3..7c3ccc3e8 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx @@ -16,6 +16,7 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { ReactEditor, useSlate } from "slate-react"; import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar"; import { useLocalStorage } from "usehooks-ts"; +import { ContextItem } from "@/features/chat/components/chatBox/contextSelector"; // @todo: we should probably rename this to a different type since it sort-of clashes // with the Suggestion system we have built into the chat box. @@ -126,9 +127,9 @@ export const AgenticSearch = ({ const { createNewChatThread, isLoading } = useCreateNewChatThread(); const dropdownRef = useRef(null); const editor = useSlate(); - const [selectedRepos, setSelectedRepos] = useLocalStorage("selectedRepos", [], { initializeWithValue: false }); + const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); const domain = useDomain(); - const [isRepoSelectorOpen, setIsRepoSelectorOpen] = useState(false); + const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); const setSelectedSuggestionType = useCallback((type: SuggestionType | undefined) => { _setSelectedSuggestionType(type); @@ -158,13 +159,13 @@ export const AgenticSearch = ({ > { - createNewChatThread(children, selectedRepos); + createNewChatThread(children, selectedItems); }} className="min-h-[50px]" isRedirecting={isLoading} languageModels={languageModels} - selectedRepos={selectedRepos} - onRepoSelectorOpenChanged={setIsRepoSelectorOpen} + selectedItems={selectedItems} + onContextSelectorOpenChanged={setIsContextSelectorOpen} />
@@ -172,10 +173,10 @@ export const AgenticSearch = ({ sew(async () => +const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageModelId }: ChatHandlerProps, domain: string) => sew(async () => withAuth((userId) => withOrgMembership(userId, domain, async ({ org }) => { const chat = await prisma.chat.findUnique({ @@ -186,13 +188,34 @@ const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandl 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 contextRepos = searchContexts.flatMap(context => + context.repos.map(repo => repo.name) + ); + + // Combine and deduplicate repos + expandedRepos = Array.from(new Set([...selectedRepos, ...contextRepos])); + } + const researchStream = await createAgentStream({ model, providerOptions, headers, inputMessages: messageHistory, inputSources: sources, - selectedRepos, + selectedRepos: expandedRepos, onWriteSource: (source) => { writer.write({ type: 'data-source', diff --git a/packages/web/src/features/chat/components/chatBox/chatBox.tsx b/packages/web/src/features/chat/components/chatBox/chatBox.tsx index 47c94f6af..925f043e4 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBox.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBox.tsx @@ -18,6 +18,7 @@ import { Suggestion } from "./types"; import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery"; import { useSuggestionsData } from "./useSuggestionsData"; import { useToast } from "@/components/hooks/use-toast"; +import { ContextItem } from "./contextSelector"; interface ChatBoxProps { onSubmit: (children: Descendant[], editor: CustomEditor) => void; @@ -27,8 +28,8 @@ interface ChatBoxProps { isRedirecting?: boolean; isGenerating?: boolean; languageModels: LanguageModelInfo[]; - selectedRepos: string[]; - onRepoSelectorOpenChanged: (isOpen: boolean) => void; + selectedItems: ContextItem[]; + onContextSelectorOpenChanged: (isOpen: boolean) => void; } export const ChatBox = ({ @@ -39,8 +40,8 @@ export const ChatBox = ({ isRedirecting, isGenerating, languageModels, - selectedRepos, - onRepoSelectorOpenChanged, + selectedItems, + onContextSelectorOpenChanged, }: ChatBoxProps) => { const suggestionsBoxRef = useRef(null); const [index, setIndex] = useState(0); @@ -49,7 +50,7 @@ export const ChatBox = ({ const { suggestions, isLoading } = useSuggestionsData({ suggestionMode, suggestionQuery, - selectedRepos, + selectedRepos: selectedItems.filter(item => item.type === 'repo').map(item => item.value), }); const { selectedLanguageModel } = useSelectedLanguageModel({ initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined, @@ -113,7 +114,7 @@ export const ChatBox = ({ } } - if (selectedRepos.length === 0) { + if (selectedItems.length === 0) { return { isSubmitDisabled: true, isSubmitDisabledReason: "no-repos-selected", @@ -137,7 +138,7 @@ export const ChatBox = ({ editor.children, isRedirecting, isGenerating, - selectedRepos.length, + selectedItems.length, selectedLanguageModel, ]) @@ -145,17 +146,17 @@ export const ChatBox = ({ if (isSubmitDisabled) { if (isSubmitDisabledReason === "no-repos-selected") { toast({ - description: "⚠️ One or more repositories must be selected.", + description: "⚠️ One or more repositories or contexts must be selected.", variant: "destructive", }); - onRepoSelectorOpenChanged(true); + onContextSelectorOpenChanged(true); } return; } _onSubmit(editor.children, editor); - }, [_onSubmit, editor, isSubmitDisabled, isSubmitDisabledReason, toast, onRepoSelectorOpenChanged]); + }, [_onSubmit, editor, isSubmitDisabled, isSubmitDisabledReason, toast, onContextSelectorOpenChanged]); const onInsertSuggestion = useCallback((suggestion: Suggestion) => { switch (suggestion.type) { diff --git a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx index 0e83a19cc..c6da28739 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx @@ -12,26 +12,42 @@ import { useHotkeys } from "react-hotkeys-hook"; import { ReactEditor, useSlate } from "slate-react"; import { useSelectedLanguageModel } from "../../useSelectedLanguageModel"; import { LanguageModelSelector } from "./languageModelSelector"; -import { RepoSelector } from "./repoSelector"; +import { ContextSelector, type ContextItem } from "./contextSelector"; +import { useQuery } from "@tanstack/react-query"; +import { getSearchContexts } from "@/actions"; +import { useDomain } from "@/hooks/useDomain"; +import { isServiceError } from "@/lib/utils"; export interface ChatBoxToolbarProps { languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; - selectedRepos: string[]; - onSelectedReposChange: (repos: string[]) => void; - isRepoSelectorOpen: boolean; - onRepoSelectorOpenChanged: (isOpen: boolean) => void; + selectedItems: ContextItem[]; + onSelectedItemsChange: (items: ContextItem[]) => void; + isContextSelectorOpen: boolean; + onContextSelectorOpenChanged: (isOpen: boolean) => void; } export const ChatBoxToolbar = ({ languageModels, repos, - selectedRepos, - onSelectedReposChange, - isRepoSelectorOpen, - onRepoSelectorOpenChanged, + selectedItems, + onSelectedItemsChange, + isContextSelectorOpen, + onContextSelectorOpenChanged, }: ChatBoxToolbarProps) => { const editor = useSlate(); + const domain = useDomain(); + + const { data: searchContexts } = useQuery({ + queryKey: ["searchContexts", domain], + queryFn: () => getSearchContexts(domain), + select: (data) => { + if (isServiceError(data)) { + return []; + } + return data; + }, + }); const onAddContext = useCallback(() => { editor.insertText("@"); @@ -76,17 +92,18 @@ export const ChatBoxToolbar = ({ - repo.repoName)} - selectedRepos={selectedRepos} - onSelectedReposChange={onSelectedReposChange} - isOpen={isRepoSelectorOpen} - onOpenChanged={onRepoSelectorOpenChanged} + repos={repos} + searchContexts={searchContexts || []} + selectedItems={selectedItems} + onSelectedItemsChange={onSelectedItemsChange} + isOpen={isContextSelectorOpen} + onOpenChanged={onContextSelectorOpenChanged} /> - Repositories to scope conversation to. + 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/contextSelector.tsx new file mode 100644 index 000000000..d9bca580d --- /dev/null +++ b/packages/web/src/features/chat/components/chatBox/contextSelector.tsx @@ -0,0 +1,278 @@ +// Adapted from: web/src/components/ui/multi-select.tsx + +import * as React from "react"; +import { + CheckIcon, + ChevronDown, + BookMarkedIcon, + FolderIcon, + LayersIcon, +} from "lucide-react"; +import Image from "next/image"; + +import { cn, getCodeHostIcon } from "@/lib/utils"; +import { RepositoryQuery } from "@/lib/types"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; + +export type ContextItem = { + type: 'repo' | 'context'; + value: string; + name: string; + description?: string; + codeHostType?: string; // For repos only + repoCount?: number; // For search contexts only +}; + +interface ContextSelectorProps extends React.ButtonHTMLAttributes { + repos: RepositoryQuery[]; + searchContexts: { name: string; description?: string; repoCount: number }[]; + selectedItems: ContextItem[]; + onSelectedItemsChange: (items: ContextItem[]) => void; + className?: string; + isOpen: boolean; + onOpenChanged: (isOpen: boolean) => void; +} + +export const ContextSelector = React.forwardRef< + HTMLButtonElement, + ContextSelectorProps +>( + ( + { + repos, + searchContexts, + onSelectedItemsChange, + className, + selectedItems, + isOpen, + onOpenChanged, + ...props + }, + ref + ) => { + const scrollContainerRef = React.useRef(null); + const scrollPosition = React.useRef(0); + + const handleInputKeyDown = ( + event: React.KeyboardEvent + ) => { + if (event.key === "Enter") { + onOpenChanged(true); + } else if (event.key === "Backspace" && !event.currentTarget.value) { + const newSelectedItems = [...selectedItems]; + newSelectedItems.pop(); + onSelectedItemsChange(newSelectedItems); + } + }; + + const toggleItem = (item: ContextItem) => { + // Store current scroll position before state update + if (scrollContainerRef.current) { + scrollPosition.current = scrollContainerRef.current.scrollTop; + } + + const isSelected = selectedItems.some( + (selected) => selected.type === item.type && selected.value === item.value + ); + + const newSelectedItems = isSelected + ? selectedItems.filter( + (selected) => !(selected.type === item.type && selected.value === item.value) + ) + : [...selectedItems, item]; + onSelectedItemsChange(newSelectedItems); + }; + + const handleClear = () => { + onSelectedItemsChange([]); + }; + + const handleTogglePopover = () => { + onOpenChanged(!isOpen); + }; + + const allItems = React.useMemo(() => { + const contextItems: ContextItem[] = searchContexts.map(context => ({ + type: 'context' as const, + value: context.name, + name: context.name, + description: context.description, + repoCount: context.repoCount + })); + + const repoItems: ContextItem[] = 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]; + }, [repos, searchContexts]); + + const sortedItems = React.useMemo(() => { + return allItems + .map((item) => ({ + item, + isSelected: selectedItems.some( + (selected) => selected.type === item.type && selected.value === item.value + ) + })) + .sort((a, b) => { + // 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; + return 0; + }) + }, [allItems, selectedItems]); + + // Restore scroll position after re-render + React.useEffect(() => { + if (scrollContainerRef.current && scrollPosition.current > 0) { + scrollContainerRef.current.scrollTop = scrollPosition.current; + } + }, [sortedItems]); + + return ( + + + + + onOpenChanged(false)} + > + + + + No results found. + + {sortedItems.map(({ item, isSelected }) => { + return ( + toggleItem(item)} + className="cursor-pointer" + > +
+ +
+
+ {item.type === 'context' ? ( + + ) : ( + // Render code host icon for repos + (() => { + const codeHostIcon = item.codeHostType ? getCodeHostIcon(item.codeHostType) : null; + return codeHostIcon ? ( + {`${item.codeHostType} + ) : ( + + ); + })() + )} +
+
+ + {item.name} + + {item.type === 'context' && item.repoCount !== undefined && ( + + {item.repoCount} repo{item.repoCount === 1 ? '' : 's'} + + )} +
+ {item.description && ( + + {item.description} + + )} +
+
+
+ ); + })} +
+
+ {selectedItems.length > 0 && ( + <> + + + Clear + + + )} +
+
+
+ ); + } +); + +ContextSelector.displayName = "ContextSelector"; \ No newline at end of file diff --git a/packages/web/src/features/chat/components/chatBox/repoSelector.tsx b/packages/web/src/features/chat/components/chatBox/repoSelector.tsx deleted file mode 100644 index 8b2d7b13b..000000000 --- a/packages/web/src/features/chat/components/chatBox/repoSelector.tsx +++ /dev/null @@ -1,191 +0,0 @@ -// Adapted from: web/src/components/ui/multi-select.tsx - -import * as React from "react"; -import { - CheckIcon, - ChevronDown, - BookMarkedIcon, -} from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator, -} from "@/components/ui/command"; - -interface RepoSelectorProps extends React.ButtonHTMLAttributes { - repos: string[]; - selectedRepos: string[]; - onSelectedReposChange: (repos: string[]) => void; - className?: string; - isOpen: boolean; - onOpenChanged: (isOpen: boolean) => void; -} - -export const RepoSelector = React.forwardRef< - HTMLButtonElement, - RepoSelectorProps ->( - ( - { - repos, - onSelectedReposChange, - className, - selectedRepos, - isOpen, - onOpenChanged, - ...props - }, - ref - ) => { - const scrollContainerRef = React.useRef(null); - const scrollPosition = React.useRef(0); - - const handleInputKeyDown = ( - event: React.KeyboardEvent - ) => { - if (event.key === "Enter") { - onOpenChanged(true); - } else if (event.key === "Backspace" && !event.currentTarget.value) { - const newSelectedRepos = [...selectedRepos]; - newSelectedRepos.pop(); - onSelectedReposChange(newSelectedRepos); - } - }; - - const toggleRepo = (repo: string) => { - // Store current scroll position before state update - if (scrollContainerRef.current) { - scrollPosition.current = scrollContainerRef.current.scrollTop; - } - - const newSelectedValues = selectedRepos.includes(repo) - ? selectedRepos.filter((value) => value !== repo) - : [...selectedRepos, repo]; - onSelectedReposChange(newSelectedValues); - }; - - const handleClear = () => { - onSelectedReposChange([]); - }; - - const handleTogglePopover = () => { - onOpenChanged(!isOpen); - }; - - const sortedRepos = React.useMemo(() => { - return repos - .map((repo) => ({ - repo, - isSelected: selectedRepos.includes(repo) - })) - .sort((a, b) => { - if (a.isSelected && !b.isSelected) return -1; - if (!a.isSelected && b.isSelected) return 1; - return 0; - }) - }, [repos, selectedRepos]); - - // Restore scroll position after re-render - React.useEffect(() => { - if (scrollContainerRef.current && scrollPosition.current > 0) { - scrollContainerRef.current.scrollTop = scrollPosition.current; - } - }, [sortedRepos]); - - return ( - - - - - onOpenChanged(false)} - > - - - - No results found. - - {sortedRepos.map(({ repo, isSelected }) => { - return ( - toggleRepo(repo)} - className="cursor-pointer" - > -
- -
- {repo} -
- ); - })} -
-
- {selectedRepos.length > 0 && ( - <> - - - Clear - - - )} -
-
-
- ); - } -); - -RepoSelector.displayName = "RepoSelector"; \ No newline at end of file diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index 85ffc9ba1..c6f63a8d1 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -23,6 +23,7 @@ import { ErrorBanner } from './errorBanner'; import { useRouter } from 'next/navigation'; import { usePrevious } from '@uidotdev/usehooks'; import { RepositoryQuery } from '@/lib/types'; +import { ContextItem } from '../chatBox/contextSelector'; type ChatHistoryState = { scrollOffset?: number; @@ -34,8 +35,8 @@ interface ChatThreadProps { inputMessage?: CreateUIMessage; languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; - selectedRepos: string[]; - onSelectedReposChange: (repos: string[]) => void; + selectedItems: ContextItem[]; + onSelectedItemsChange: (items: ContextItem[]) => void; isChatReadonly: boolean; } @@ -45,8 +46,8 @@ export const ChatThread = ({ inputMessage, languageModels, repos, - selectedRepos, - onSelectedReposChange, + selectedItems, + onSelectedItemsChange, isChatReadonly, }: ChatThreadProps) => { const domain = useDomain(); @@ -57,7 +58,7 @@ export const ChatThread = ({ const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(false); const { toast } = useToast(); const router = useRouter(); - const [isRepoSelectorOpen, setIsRepoSelectorOpen] = useState(false); + const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); // Initial state is from attachments that exist in in the chat history. const [sources, setSources] = useState( @@ -111,13 +112,18 @@ export const ChatThread = ({ .map((part) => part.data); setSources((prev) => [...prev, ...sources]); + // 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); + _sendMessage(message, { body: { selectedRepos, + selectedContexts, languageModelId: selectedLanguageModel.model, } satisfies AdditionalChatRequestParams, }); - }, [_sendMessage, selectedLanguageModel, selectedRepos, toast]); + }, [_sendMessage, selectedLanguageModel, selectedItems, toast]); const messagePairs = useMessagePairs(messages); @@ -233,13 +239,16 @@ export const ChatThread = ({ const text = slateContentToString(children); const mentions = getAllMentionElements(children); - const message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos); + // Extract repos and contexts from selectedItems for createUIMessage + 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 message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos, selectedContexts); sendMessage(message); setIsAutoScrollEnabled(true); resetEditor(editor); - }, [sendMessage, selectedRepos]); + }, [sendMessage, selectedItems]); return ( <> @@ -316,17 +325,17 @@ export const ChatThread = ({ isGenerating={status === "streaming" || status === "submitted"} onStop={stop} languageModels={languageModels} - selectedRepos={selectedRepos} - onRepoSelectorOpenChanged={setIsRepoSelectorOpen} + selectedItems={selectedItems} + onContextSelectorOpenChanged={setIsContextSelectorOpen} />
diff --git a/packages/web/src/features/chat/types.ts b/packages/web/src/features/chat/types.ts index 2bbea7985..daddb8906 100644 --- a/packages/web/src/features/chat/types.ts +++ b/packages/web/src/features/chat/types.ts @@ -51,6 +51,7 @@ export const sbChatMessageMetadataSchema = z.object({ userId: z.string(), })).optional(), selectedRepos: z.array(z.string()).optional(), + selectedContexts: z.array(z.string()).optional(), traceId: z.string().optional(), }); @@ -139,6 +140,7 @@ export const SET_CHAT_STATE_QUERY_PARAM = 'setChatState'; export type SetChatStatePayload = { inputMessage: CreateUIMessage; selectedRepos: string[]; + selectedContexts?: string[]; } @@ -156,5 +158,6 @@ export type LanguageModelInfo = { export const additionalChatRequestParamsSchema = z.object({ languageModelId: z.string(), selectedRepos: z.array(z.string()), + selectedContexts: z.array(z.string()).optional(), }); 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 155be3375..54aaf14d2 100644 --- a/packages/web/src/features/chat/useCreateNewChatThread.ts +++ b/packages/web/src/features/chat/useCreateNewChatThread.ts @@ -11,6 +11,7 @@ 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"; export const useCreateNewChatThread = () => { const domain = useDomain(); @@ -18,10 +19,15 @@ export const useCreateNewChatThread = () => { const { toast } = useToast(); const router = useRouter(); - const createNewChatThread = useCallback(async (children: Descendant[], selectedRepos: string[]) => { + const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: ContextItem[]) => { const text = slateContentToString(children); const mentions = getAllMentionElements(children); - const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos); + + // 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); setIsLoading(true); const response = await createChat(domain); @@ -37,6 +43,7 @@ export const useCreateNewChatThread = () => { [SET_CHAT_STATE_QUERY_PARAM, JSON.stringify({ inputMessage, selectedRepos, + selectedContexts, } satisfies SetChatStatePayload)], ); diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index ec1385b10..a99c777d1 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -172,7 +172,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[]): CreateUIMessage => { +export const createUIMessage = (text: string, mentions: MentionData[], selectedRepos: string[], selectedContexts?: string[]): CreateUIMessage => { // Converts applicable mentions into sources. const sources: Source[] = mentions .map((mention) => { @@ -206,6 +206,7 @@ export const createUIMessage = (text: string, mentions: MentionData[], selectedR ], metadata: { selectedRepos, + selectedContexts, }, } } From 9bb7865f8eeccb418d3f01a53637d742986a741b Mon Sep 17 00:00:00 2001 From: msukkari Date: Sat, 26 Jul 2025 09:18:21 -0700 Subject: [PATCH 2/7] ui nits --- packages/web/src/features/chat/components/chatBox/chatBox.tsx | 4 ++-- .../src/features/chat/components/chatBox/contextSelector.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/web/src/features/chat/components/chatBox/chatBox.tsx b/packages/web/src/features/chat/components/chatBox/chatBox.tsx index 925f043e4..e26c6bbc4 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBox.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBox.tsx @@ -146,7 +146,7 @@ export const ChatBox = ({ if (isSubmitDisabled) { if (isSubmitDisabledReason === "no-repos-selected") { toast({ - description: "⚠️ One or more repositories or contexts must be selected.", + description: "⚠️ One or more repositories or search contexts must be selected.", variant: "destructive", }); onContextSelectorOpenChanged(true); @@ -323,7 +323,7 @@ export const ChatBox = ({
- One or more repositories must be selected. + One or more repositories or search contexts must be selected.
)} diff --git a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx b/packages/web/src/features/chat/components/chatBox/contextSelector.tsx index d9bca580d..753f6e5c2 100644 --- a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx +++ b/packages/web/src/features/chat/components/chatBox/contextSelector.tsx @@ -4,9 +4,9 @@ import * as React from "react"; import { CheckIcon, ChevronDown, - BookMarkedIcon, FolderIcon, LayersIcon, + LibraryBigIcon, } from "lucide-react"; import Image from "next/image"; @@ -213,7 +213,7 @@ export const ContextSelector = React.forwardRef<
{item.type === 'context' ? ( - + ) : ( // Render code host icon for repos (() => { From 0b80e02f46b847ac525783ff462e093a9589a367 Mon Sep 17 00:00:00 2001 From: msukkari Date: Sat, 26 Jul 2025 14:14:05 -0700 Subject: [PATCH 3/7] move search context fetch to server --- .../chat/[id]/components/chatThreadPanel.tsx | 5 ++++- .../web/src/app/[domain]/chat/[id]/page.tsx | 8 ++++++- .../[domain]/chat/components/newChatPanel.tsx | 5 ++++- packages/web/src/app/[domain]/chat/page.tsx | 8 ++++++- .../components/homepage/agenticSearch.tsx | 5 ++++- .../[domain]/components/homepage/index.tsx | 5 ++++- packages/web/src/app/[domain]/page.tsx | 8 ++++++- .../components/chatBox/chatBoxToolbar.tsx | 22 ++++--------------- .../chat/components/chatThread/chatThread.tsx | 5 ++++- packages/web/src/lib/schemas.ts | 6 +++++ packages/web/src/lib/types.ts | 5 +++-- 11 files changed, 54 insertions(+), 28 deletions(-) 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 468091aac..1e31d37ae 100644 --- a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx @@ -3,7 +3,7 @@ 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 { RepositoryQuery } from '@/lib/types'; +import { RepositoryQuery, SearchContextQuery } from '@/lib/types'; import { CreateUIMessage } from 'ai'; import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -13,6 +13,7 @@ import { ContextItem } from '@/features/chat/components/chatBox/contextSelector' interface ChatThreadPanelProps { languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; + searchContexts: SearchContextQuery[]; order: number; messages: SBChatMessage[]; isChatReadonly: boolean; @@ -21,6 +22,7 @@ interface ChatThreadPanelProps { export const ChatThreadPanel = ({ languageModels, repos, + searchContexts, order, messages, isChatReadonly, @@ -94,6 +96,7 @@ export const ChatThreadPanel = ({ inputMessage={inputMessage} languageModels={languageModels} repos={repos} + searchContexts={searchContexts} selectedItems={selectedItems} onSelectedItemsChange={setSelectedItems} isChatReadonly={isChatReadonly} diff --git a/packages/web/src/app/[domain]/chat/[id]/page.tsx b/packages/web/src/app/[domain]/chat/[id]/page.tsx index 67331eb44..ab7592ba9 100644 --- a/packages/web/src/app/[domain]/chat/[id]/page.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/page.tsx @@ -1,4 +1,4 @@ -import { getRepos } from '@/actions'; +import { getRepos, getSearchContexts } from '@/actions'; import { getUserChatHistory, getConfiguredLanguageModelsInfo, getChatInfo } from '@/features/chat/actions'; import { ServiceErrorException } from '@/lib/serviceError'; import { isServiceError } from '@/lib/utils'; @@ -22,6 +22,7 @@ interface PageProps { export default async function Page({ params }: PageProps) { const languageModels = await getConfiguredLanguageModelsInfo(); const repos = await getRepos(params.domain); + const searchContexts = await getSearchContexts(params.domain); const chatInfo = await getChatInfo({ chatId: params.id }, params.domain); const session = await auth(); const chatHistory = session ? await getUserChatHistory(params.domain) : []; @@ -34,6 +35,10 @@ export default async function Page({ params }: PageProps) { throw new ServiceErrorException(repos); } + if (isServiceError(searchContexts)) { + throw new ServiceErrorException(searchContexts); + } + if (isServiceError(chatInfo)) { if (chatInfo.statusCode === StatusCodes.NOT_FOUND) { return notFound(); @@ -74,6 +79,7 @@ export default async function Page({ params }: PageProps) { { const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); @@ -55,6 +57,7 @@ export const NewChatPanel = ({ repo.indexedAt !== undefined); return ( @@ -48,6 +53,7 @@ export default async function Page({ params }: PageProps) { diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx index 7c3ccc3e8..ddb9638dc 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx @@ -8,7 +8,7 @@ import { LanguageModelInfo } from "@/features/chat/types"; import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; import { resetEditor } from "@/features/chat/utils"; import { useDomain } from "@/hooks/useDomain"; -import { RepositoryQuery } from "@/lib/types"; +import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import { getDisplayTime } from "@/lib/utils"; import { BrainIcon, FileIcon, LucideIcon, SearchIcon } from "lucide-react"; import Link from "next/link"; @@ -110,6 +110,7 @@ interface AgenticSearchProps { searchModeSelectorProps: SearchModeSelectorProps; languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; + searchContexts: SearchContextQuery[]; chatHistory: { id: string; createdAt: Date; @@ -121,6 +122,7 @@ export const AgenticSearch = ({ searchModeSelectorProps, languageModels, repos, + searchContexts, chatHistory, }: AgenticSearchProps) => { const [selectedSuggestionType, _setSelectedSuggestionType] = useState(undefined); @@ -173,6 +175,7 @@ export const AgenticSearch = ({ diff --git a/packages/web/src/app/[domain]/page.tsx b/packages/web/src/app/[domain]/page.tsx index 607bc143c..a4455f340 100644 --- a/packages/web/src/app/[domain]/page.tsx +++ b/packages/web/src/app/[domain]/page.tsx @@ -1,4 +1,4 @@ -import { getRepos } from "@/actions"; +import { getRepos, getSearchContexts } from "@/actions"; import { Footer } from "@/app/components/footer"; import { getOrgFromDomain } from "@/data/org"; import { getConfiguredLanguageModelsInfo, getUserChatHistory } from "@/features/chat/actions"; @@ -22,12 +22,17 @@ export default async function Home({ params: { domain } }: { params: { domain: s const models = await getConfiguredLanguageModelsInfo(); const repos = await getRepos(domain); + const searchContexts = await getSearchContexts(domain); const chatHistory = session ? await getUserChatHistory(domain) : []; if (isServiceError(repos)) { throw new ServiceErrorException(repos); } + if (isServiceError(searchContexts)) { + throw new ServiceErrorException(searchContexts); + } + if (isServiceError(chatHistory)) { throw new ServiceErrorException(chatHistory); } @@ -52,6 +57,7 @@ export default async function Home({ params: { domain } }: { params: { domain: s void; isContextSelectorOpen: boolean; @@ -30,25 +27,14 @@ export interface ChatBoxToolbarProps { export const ChatBoxToolbar = ({ languageModels, repos, + searchContexts, selectedItems, onSelectedItemsChange, isContextSelectorOpen, onContextSelectorOpenChanged, }: ChatBoxToolbarProps) => { const editor = useSlate(); - const domain = useDomain(); - const { data: searchContexts } = useQuery({ - queryKey: ["searchContexts", domain], - queryFn: () => getSearchContexts(domain), - select: (data) => { - if (isServiceError(data)) { - return []; - } - return data; - }, - }); - const onAddContext = useCallback(() => { editor.insertText("@"); ReactEditor.focus(editor); @@ -95,7 +81,7 @@ export const ChatBoxToolbar = ({ ; languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; + searchContexts: SearchContextQuery[]; selectedItems: ContextItem[]; onSelectedItemsChange: (items: ContextItem[]) => void; isChatReadonly: boolean; @@ -46,6 +47,7 @@ export const ChatThread = ({ inputMessage, languageModels, repos, + searchContexts, selectedItems, onSelectedItemsChange, isChatReadonly, @@ -333,6 +335,7 @@ export const ChatThread = ({ ; -export type RepositoryQuery = z.infer; \ No newline at end of file +export type RepositoryQuery = z.infer; +export type SearchContextQuery = z.infer; \ No newline at end of file From f47ffc3da9d793ae167ddfd4d8a6d58426fa7f09 Mon Sep 17 00:00:00 2001 From: msukkari Date: Sat, 26 Jul 2025 14:37:54 -0700 Subject: [PATCH 4/7] feedback --- .../chat/[id]/components/chatThreadPanel.tsx | 26 +++++++++++++--- .../components/chatBox/contextSelector.tsx | 31 ++++++++++--------- .../chat/components/chatThread/chatThread.tsx | 19 ++++++------ 3 files changed, 46 insertions(+), 30 deletions(-) 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 1e31d37ae..9d109c158 100644 --- a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx @@ -46,10 +46,18 @@ export const ChatThreadPanel = ({ type: 'repo' as const, value: repoName, name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName, - codeHostType: repoInfo?.codeHostType + codeHostType: repoInfo?.codeHostType || '' }; }), - ...defaultSelectedContexts.map(context => ({ type: 'context' as const, value: context, name: context })) + ...defaultSelectedContexts.map(contextName => { + const context = searchContexts.find(c => c.name === contextName); + return { + type: 'context' as const, + value: contextName, + name: contextName, + repoCount: context?.repoCount || 0 + }; + }) ]); useEffect(() => { @@ -68,10 +76,18 @@ export const ChatThreadPanel = ({ type: 'repo' as const, value: repoName, name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName, - codeHostType: repoInfo?.codeHostType + codeHostType: repoInfo?.codeHostType || '' }; }), - ...(selectedContexts || []).map(context => ({ type: 'context' as const, value: context, name: context })) + ...(selectedContexts || []).map(contextName => { + const context = searchContexts.find(c => c.name === contextName); + return { + type: 'context' as const, + value: contextName, + name: contextName, + repoCount: context?.repoCount || 0 + }; + }) ]); } catch { console.error('Invalid message in URL'); @@ -81,7 +97,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]); + }, [searchParams, router, repos, searchContexts]); return ( { repos: RepositoryQuery[]; - searchContexts: { name: string; description?: string; repoCount: number }[]; + searchContexts: SearchContextQuery[]; selectedItems: ContextItem[]; onSelectedItemsChange: (items: ContextItem[]) => void; className?: string; @@ -111,7 +118,6 @@ export const ContextSelector = React.forwardRef< type: 'context' as const, value: context.name, name: context.name, - description: context.description, repoCount: context.repoCount })); @@ -236,7 +242,7 @@ export const ContextSelector = React.forwardRef< {item.name} - {item.type === 'context' && item.repoCount !== undefined && ( + {item.type === 'context' && item.repoCount && ( )}
- {item.description && ( - - {item.description} - - )} diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index d0cd34d8d..6b874b1b0 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -12,7 +12,7 @@ import { useChat } from '@ai-sdk/react'; import { CreateUIMessage, DefaultChatTransport } from 'ai'; import { ArrowDownIcon } from 'lucide-react'; import { useNavigationGuard } from 'next-navigation-guard'; -import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Descendant } from 'slate'; import { useMessagePairs } from '../../useMessagePairs'; import { useSelectedLanguageModel } from '../../useSelectedLanguageModel'; @@ -62,6 +62,12 @@ 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) => @@ -114,10 +120,6 @@ export const ChatThread = ({ .map((part) => part.data); setSources((prev) => [...prev, ...sources]); - // 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); - _sendMessage(message, { body: { selectedRepos, @@ -125,7 +127,7 @@ export const ChatThread = ({ languageModelId: selectedLanguageModel.model, } satisfies AdditionalChatRequestParams, }); - }, [_sendMessage, selectedLanguageModel, selectedItems, toast]); + }, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedContexts]); const messagePairs = useMessagePairs(messages); @@ -241,16 +243,13 @@ export const ChatThread = ({ const text = slateContentToString(children); const mentions = getAllMentionElements(children); - // Extract repos and contexts from selectedItems for createUIMessage - 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 message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos, selectedContexts); sendMessage(message); setIsAutoScrollEnabled(true); resetEditor(editor); - }, [sendMessage, selectedItems]); + }, [sendMessage, selectedRepos, selectedContexts]); return ( <> From 4324bde2e46c6ca41e0cc91aed06cd0cd1788dc5 Mon Sep 17 00:00:00 2001 From: msukkari Date: Sat, 26 Jul 2025 15:25:51 -0700 Subject: [PATCH 5/7] search context for chat suggestion, nits --- packages/web/src/actions.ts | 3 ++- .../chat/[id]/components/chatThreadPanel.tsx | 6 +++--- .../[domain]/chat/components/newChatPanel.tsx | 1 + .../components/homepage/agenticSearch.tsx | 1 + .../chat/components/chatBox/chatBox.tsx | 18 +++++++++++++++++- .../components/chatBox/contextSelector.tsx | 2 +- .../chat/components/chatThread/chatThread.tsx | 1 + packages/web/src/features/chat/types.ts | 4 ++-- packages/web/src/lib/schemas.ts | 3 ++- 9 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index fa83d2b9e..430c003ba 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -1868,9 +1868,10 @@ export const getSearchContexts = async (domain: string) => sew(() => }); return searchContexts.map((context) => ({ + id: context.id, name: context.name, description: context.description ?? undefined, - repoCount: context.repos.length, + repoNames: context.repos.map((repo) => repo.name), })); }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true )); 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 9d109c158..48824d762 100644 --- a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx @@ -55,7 +55,7 @@ export const ChatThreadPanel = ({ type: 'context' as const, value: contextName, name: contextName, - repoCount: context?.repoCount || 0 + repoCount: context?.repoNames.length || 0 }; }) ]); @@ -79,13 +79,13 @@ export const ChatThreadPanel = ({ codeHostType: repoInfo?.codeHostType || '' }; }), - ...(selectedContexts || []).map(contextName => { + ...selectedContexts.map(contextName => { const context = searchContexts.find(c => c.name === contextName); return { type: 'context' as const, value: contextName, name: contextName, - repoCount: context?.repoCount || 0 + repoCount: context?.repoNames.length || 0 }; }) ]); diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx index 6926a4bd6..a2addcd41 100644 --- a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx +++ b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx @@ -51,6 +51,7 @@ export const NewChatPanel = ({ isRedirecting={isLoading} languageModels={languageModels} selectedItems={selectedItems} + searchContexts={searchContexts} 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 ddb9638dc..cbd6999b0 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx @@ -167,6 +167,7 @@ export const AgenticSearch = ({ isRedirecting={isLoading} languageModels={languageModels} selectedItems={selectedItems} + searchContexts={searchContexts} onContextSelectorOpenChanged={setIsContextSelectorOpen} /> diff --git a/packages/web/src/features/chat/components/chatBox/chatBox.tsx b/packages/web/src/features/chat/components/chatBox/chatBox.tsx index e26c6bbc4..35bb83016 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBox.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBox.tsx @@ -19,6 +19,7 @@ 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 { onSubmit: (children: Descendant[], editor: CustomEditor) => void; @@ -29,6 +30,7 @@ interface ChatBoxProps { isGenerating?: boolean; languageModels: LanguageModelInfo[]; selectedItems: ContextItem[]; + searchContexts: SearchContextQuery[]; onContextSelectorOpenChanged: (isOpen: boolean) => void; } @@ -41,6 +43,7 @@ export const ChatBox = ({ isGenerating, languageModels, selectedItems, + searchContexts, onContextSelectorOpenChanged, }: ChatBoxProps) => { const suggestionsBoxRef = useRef(null); @@ -50,7 +53,20 @@ export const ChatBox = ({ const { suggestions, isLoading } = useSuggestionsData({ suggestionMode, suggestionQuery, - selectedRepos: selectedItems.filter(item => item.type === 'repo').map(item => item.value), + selectedRepos: selectedItems.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; + } + } + + return []; + }).flat(), }); const { selectedLanguageModel } = useSelectedLanguageModel({ initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined, diff --git a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx b/packages/web/src/features/chat/components/chatBox/contextSelector.tsx index 0ab753207..7c8c058aa 100644 --- a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx +++ b/packages/web/src/features/chat/components/chatBox/contextSelector.tsx @@ -118,7 +118,7 @@ export const ContextSelector = React.forwardRef< type: 'context' as const, value: context.name, name: context.name, - repoCount: context.repoCount + repoCount: context.repoNames.length })); const repoItems: ContextItem[] = repos.map(repo => ({ diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index 6b874b1b0..acbb20234 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -328,6 +328,7 @@ export const ChatThread = ({ onStop={stop} languageModels={languageModels} selectedItems={selectedItems} + searchContexts={searchContexts} onContextSelectorOpenChanged={setIsContextSelectorOpen} />
diff --git a/packages/web/src/features/chat/types.ts b/packages/web/src/features/chat/types.ts index daddb8906..b4c6bc8d5 100644 --- a/packages/web/src/features/chat/types.ts +++ b/packages/web/src/features/chat/types.ts @@ -140,7 +140,7 @@ export const SET_CHAT_STATE_QUERY_PARAM = 'setChatState'; export type SetChatStatePayload = { inputMessage: CreateUIMessage; selectedRepos: string[]; - selectedContexts?: string[]; + selectedContexts: string[]; } @@ -158,6 +158,6 @@ export type LanguageModelInfo = { export const additionalChatRequestParamsSchema = z.object({ languageModelId: z.string(), selectedRepos: z.array(z.string()), - selectedContexts: z.array(z.string()).optional(), + selectedContexts: z.array(z.string()), }); export type AdditionalChatRequestParams = z.infer; \ No newline at end of file diff --git a/packages/web/src/lib/schemas.ts b/packages/web/src/lib/schemas.ts index 6efcbf954..6c66e0fc9 100644 --- a/packages/web/src/lib/schemas.ts +++ b/packages/web/src/lib/schemas.ts @@ -29,9 +29,10 @@ export const repositoryQuerySchema = z.object({ }); export const searchContextQuerySchema = z.object({ + id: z.number(), name: z.string(), description: z.string().optional(), - repoCount: z.number(), + repoNames: z.array(z.string()), }); export const verifyCredentialsRequestSchema = z.object({ From 1c769c9cdcb24fef9a994fe53daae4987dece41e Mon Sep 17 00:00:00 2001 From: msukkari Date: Sat, 26 Jul 2025 15:33:17 -0700 Subject: [PATCH 6/7] type nit --- packages/web/src/features/chat/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index 8e9f47d47..e24956644 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -172,7 +172,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[], selectedRepos: string[], selectedContexts: string[]): CreateUIMessage => { // Converts applicable mentions into sources. const sources: Source[] = mentions .map((mention) => { From d7151f20de470a41f35d7c7c585f7ab3d1796e19 Mon Sep 17 00:00:00 2001 From: msukkari Date: Sat, 26 Jul 2025 15:34:55 -0700 Subject: [PATCH 7/7] fix minor ui nit --- .../src/features/chat/components/chatBox/contextSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx b/packages/web/src/features/chat/components/chatBox/contextSelector.tsx index 7c8c058aa..e2ebe18c1 100644 --- a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx +++ b/packages/web/src/features/chat/components/chatBox/contextSelector.tsx @@ -242,7 +242,7 @@ export const ContextSelector = React.forwardRef< {item.name} - {item.type === 'context' && item.repoCount && ( + {item.type === 'context' && (