diff --git a/packages/web/package.json b/packages/web/package.json index 6ee2ab7d..6d5d518d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -12,16 +12,16 @@ "stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "3.0.0-beta.9", - "@ai-sdk/anthropic": "2.0.0-beta.8", - "@ai-sdk/azure": "2.0.0-beta.11", - "@ai-sdk/deepseek": "1.0.0-beta.8", - "@ai-sdk/google": "2.0.0-beta.14", - "@ai-sdk/google-vertex": "3.0.0-beta.16", - "@ai-sdk/mistral": "2.0.0-beta.6", - "@ai-sdk/openai": "2.0.0-beta.11", - "@ai-sdk/react": "2.0.0-beta.26", - "@ai-sdk/xai": "2.0.0-beta.10", + "@ai-sdk/amazon-bedrock": "3.0.0-beta.10", + "@ai-sdk/anthropic": "2.0.0-beta.9", + "@ai-sdk/azure": "2.0.0-beta.12", + "@ai-sdk/deepseek": "1.0.0-beta.9", + "@ai-sdk/google": "2.0.0-beta.15", + "@ai-sdk/google-vertex": "3.0.0-beta.17", + "@ai-sdk/mistral": "2.0.0-beta.7", + "@ai-sdk/openai": "2.0.0-beta.12", + "@ai-sdk/react": "2.0.0-beta.28", + "@ai-sdk/xai": "2.0.0-beta.11", "@auth/prisma-adapter": "^2.7.4", "@codemirror/commands": "^6.6.0", "@codemirror/lang-cpp": "^6.0.2", @@ -108,7 +108,7 @@ "@vercel/otel": "^1.13.0", "@viz-js/lang-dot": "^1.0.4", "@xiechao/codemirror-lang-handlebars": "^1.0.4", - "ai": "5.0.0-beta.26", + "ai": "5.0.0-beta.28", "ajv": "^8.17.1", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.0", diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureCodePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureCodePreviewPanel.tsx index 49124588..b2398e9b 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureCodePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureCodePreviewPanel.tsx @@ -136,7 +136,9 @@ export const PureCodePreviewPanel = ({ }, [editorRef, highlightRange]); const onFindReferences = useCallback((symbolName: string) => { - captureEvent('wa_browse_find_references_pressed', {}); + captureEvent('wa_find_references_pressed', { + source: 'browse', + }); createAuditAction({ action: "user.performed_find_references", metadata: { @@ -160,7 +162,9 @@ export const PureCodePreviewPanel = ({ // If we resolve multiple matches, instead of navigating to the first match, we should // instead popup the bottom sheet with the list of matches. const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => { - captureEvent('wa_browse_goto_definition_pressed', {}); + captureEvent('wa_goto_definition_pressed', { + source: 'browse', + }); createAuditAction({ action: "user.performed_goto_definition", metadata: { diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx index 8c869f8d..2d2eadbc 100644 --- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx +++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx @@ -119,7 +119,9 @@ export const CodePreview = ({ }, [onSelectedMatchIndexChange]); const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => { - captureEvent('wa_preview_panel_goto_definition_pressed', {}); + captureEvent('wa_goto_definition_pressed', { + source: 'preview', + }); createAuditAction({ action: "user.performed_goto_definition", metadata: { @@ -163,7 +165,9 @@ export const CodePreview = ({ }, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName, domain]); const onFindReferences = useCallback((symbolName: string) => { - captureEvent('wa_preview_panel_find_references_pressed', {}); + captureEvent('wa_find_references_pressed', { + source: 'preview', + }); createAuditAction({ action: "user.performed_find_references", metadata: { diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index 85ffc9ba..1e4cd2d5 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -269,6 +269,7 @@ export const ChatThread = ({ return ( (({ userMessage, - assistantMessage, + assistantMessage: _assistantMessage, isStreaming, sources, chatId, + index, }, ref) => { const leftPanelRef = useRef(null); const [leftPanelHeight, setLeftPanelHeight] = useState(null); - const markdownRendererRef = useRef(null); + const answerRef = useRef(null); const [hoveredReference, setHoveredReference] = useState(undefined); const [selectedReference, setSelectedReference] = useState(undefined); - const references = useExtractReferences(assistantMessage); const [isDetailsPanelExpanded, _setIsDetailsPanelExpanded] = useState(isStreaming); const hasAutoCollapsed = useRef(false); const userHasManuallyExpanded = useRef(false); - const userQuestion = useMemo(() => { return userMessage.parts.length > 0 && userMessage.parts[0].type === 'text' ? userMessage.parts[0].text : ''; }, [userMessage]); - const messageMetadata = useMemo((): SBChatMessageMetadata | undefined => { - return assistantMessage?.metadata; - }, [assistantMessage?.metadata]); + // Take the assistant message and repair any references that are not properly formatted. + // This applies to parts that are text (i.e., text & reasoning). + const assistantMessage = useMemo(() => { + if (!_assistantMessage) { + return undefined; + } + + return { + ..._assistantMessage, + ...(_assistantMessage.parts ? { + parts: _assistantMessage.parts.map(part => { + switch (part.type) { + case 'text': + case 'reasoning': + return { + ...part, + text: repairReferences(part.text), + } + default: + return part; + } + }), + } : {}), + } satisfies SBChatMessage; + }, [_assistantMessage]); const answerPart = useMemo(() => { if (!assistantMessage) { @@ -65,11 +80,33 @@ export const ChatThreadListItem = forwardRef { + // Groups parts into steps that are associated with thinking steps that + // should be visible to the user. By "steps", we mean parts that originated + // from the same LLM invocation. By "visibile", we mean parts that have some + // visual representation in the UI (e.g., text, reasoning, tool calls, etc.). + const uiVisibleThinkingSteps = useMemo(() => { const steps = groupMessageIntoSteps(assistantMessage?.parts ?? []); + // Filter out the answerPart and empty steps - return steps.map(step => step.filter(part => part !== answerPart)).filter(step => step.length > 0); + return steps + .map( + (step) => step + // First, filter out any parts that are not text + .filter((part) => { + if (part.type !== 'text') { + return true; + } + + return part.text !== answerPart?.text; + }) + .filter((part) => { + return uiVisiblePartTypes.includes(part.type); + }) + ) + // Then, filter out any steps that are empty + .filter(step => step.length > 0); }, [answerPart, assistantMessage?.parts]); // "thinking" is when the agent is generating output that is not the answer. @@ -123,13 +160,14 @@ export const ChatThreadListItem = forwardRef { - if (!markdownRendererRef.current) { + if (!answerRef.current) { return; } - const markdownRenderer = markdownRendererRef.current; + const markdownRenderer = answerRef.current; const handleMouseOver = (event: MouseEvent) => { const target = event.target as HTMLElement; @@ -175,44 +213,59 @@ export const ChatThreadListItem = forwardRef { if (!selectedReference) { return; } - const referenceElement = document.getElementById(`user-content-${selectedReference.id}`); - if (!referenceElement) { + // The reference id is attached to the DOM element as a class name. + // @see: markdownRenderer.tsx + const referenceElements = Array.from(answerRef.current?.getElementsByClassName(selectedReference.id) ?? []); + if (referenceElements.length === 0) { return; } - scrollIntoView(referenceElement, { + const nearestReferenceElement = getNearestReferenceElement(referenceElements); + scrollIntoView(nearestReferenceElement, { behavior: 'smooth', scrollMode: 'if-needed', block: 'center', }); - referenceElement.classList.add('chat-reference--selected'); + referenceElements.forEach(element => { + element.classList.add('chat-reference--selected'); + }); return () => { - referenceElement.classList.remove('chat-reference--selected'); + referenceElements.forEach(element => { + element.classList.remove('chat-reference--selected'); + }); }; }, [selectedReference]); + // When the hovered reference changes, highlight all associated reference elements. useEffect(() => { if (!hoveredReference) { return; } - const referenceElement = document.getElementById(`user-content-${hoveredReference.id}`); - if (!referenceElement) { + // The reference id is attached to the DOM element as a class name. + // @see: markdownRenderer.tsx + const referenceElements = Array.from(answerRef.current?.getElementsByClassName(hoveredReference.id) ?? []); + if (referenceElements.length === 0) { return; } - referenceElement.classList.add('chat-reference--hover'); + referenceElements.forEach(element => { + element.classList.add('chat-reference--hover'); + }); return () => { - referenceElement.classList.remove('chat-reference--hover'); + referenceElements.forEach(element => { + element.classList.remove('chat-reference--hover'); + }); }; }, [hoveredReference]); @@ -265,151 +318,22 @@ export const ChatThreadListItem = forwardRef )} - - - - -
-
+ -

- {isThinking ? ( - <> - - Thinking... - - ) : ( - <> - - Details - - )} -

- {!isStreaming && ( - <> - - {messageMetadata?.modelName && ( -
- - {messageMetadata?.modelName} -
- )} - {messageMetadata?.totalTokens && ( -
- - {messageMetadata?.totalTokens} tokens -
- )} - {messageMetadata?.totalResponseTimeMs && ( -
- - {messageMetadata?.totalResponseTimeMs / 1000} seconds -
- )} -
- - {`${thinkingSteps.length} step${thinkingSteps.length === 1 ? '' : 's'}`} -
- - )} -
- - {isDetailsPanelExpanded ? ( - - ) : ( - - )} -
-
-
- - - {thinkingSteps.length === 0 ? ( - isStreaming ? ( - - ) : ( -

No thinking steps

- ) - ) : thinkingSteps.map((step, index) => { - return ( -
-
- - {index + 1} - -
- {step.map((part, index) => { - switch (part.type) { - case 'reasoning': - case 'text': - return ( - - ) - case 'tool-readFiles': - return ( - - ) - case 'tool-searchCode': - return ( - - ) - case 'tool-findSymbolDefinitions': - return ( - - ) - case 'tool-findSymbolReferences': - return ( - - ) - default: - return null; - } - })} -
- ) - })} -
-
-
-
- - - {/* Answer section */} {(answerPart && assistantMessage) ? ( ) : !isStreaming && (

Error: No answer response was provided

@@ -432,6 +356,7 @@ export const ChatThreadListItem = forwardRef {references.length > 0 ? ( { + return referenceElements.reduce((nearest, current) => { + if (!nearest) return current; + + const nearestRect = nearest.getBoundingClientRect(); + const currentRect = current.getBoundingClientRect(); + + // Calculate distance from element center to viewport center + const viewportCenter = window.innerHeight / 2; + const nearestDistance = Math.abs((nearestRect.top + nearestRect.bottom) / 2 - viewportCenter); + const currentDistance = Math.abs((currentRect.top + currentRect.bottom) / 2 - viewportCenter); + + return currentDistance < nearestDistance ? current : nearest; + }); +} \ No newline at end of file diff --git a/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts b/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts index 920cc129..50b73512 100644 --- a/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts +++ b/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts @@ -482,7 +482,8 @@ describe('StateField Integration', () => { path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; diff --git a/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.ts b/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.ts index c49a46be..6dc96d90 100644 --- a/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.ts +++ b/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.ts @@ -342,6 +342,9 @@ const createDecorations = (state: EditorState, foldingState: FoldingState): Deco decorations.push(decoration.range(from, to)); }); + // Sort decorations by their 'from' position to ensure proper ordering + decorations.sort((a, b) => a.from - b.from); + return Decoration.set(decorations); }; diff --git a/packages/web/src/features/chat/components/chatThread/detailsCard.tsx b/packages/web/src/features/chat/components/chatThread/detailsCard.tsx new file mode 100644 index 00000000..7bfe49a0 --- /dev/null +++ b/packages/web/src/features/chat/components/chatThread/detailsCard.tsx @@ -0,0 +1,172 @@ +'use client'; + +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 { cn } from '@/lib/utils'; +import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, 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'; + + +interface DetailsCardProps { + isExpanded: boolean; + onExpandedChanged: (isExpanded: boolean) => void; + isThinking: boolean; + isStreaming: boolean; + thinkingSteps: SBChatMessagePart[][]; + metadata?: SBChatMessageMetadata; +} + +export const DetailsCard = ({ + isExpanded, + onExpandedChanged, + isThinking, + isStreaming, + metadata, + thinkingSteps, +}: DetailsCardProps) => { + + return ( + + + + +
+
+ +

+ {isThinking ? ( + <> + + Thinking... + + ) : ( + <> + + Details + + )} +

+ {!isStreaming && ( + <> + + {metadata?.modelName && ( +
+ + {metadata?.modelName} +
+ )} + {metadata?.totalTokens && ( +
+ + {metadata?.totalTokens} tokens +
+ )} + {metadata?.totalResponseTimeMs && ( +
+ + {metadata?.totalResponseTimeMs / 1000} seconds +
+ )} +
+ + {`${thinkingSteps.length} step${thinkingSteps.length === 1 ? '' : 's'}`} +
+ + )} +
+ + {isExpanded ? ( + + ) : ( + + )} +
+
+
+ + + {thinkingSteps.length === 0 ? ( + isStreaming ? ( + + ) : ( +

No thinking steps

+ ) + ) : thinkingSteps.map((step, index) => { + return ( +
+
+ + {index + 1} + +
+ {step.map((part, index) => { + switch (part.type) { + case 'reasoning': + case 'text': + return ( + + ) + case 'tool-readFiles': + return ( + + ) + case 'tool-searchCode': + return ( + + ) + case 'tool-findSymbolDefinitions': + return ( + + ) + case 'tool-findSymbolReferences': + return ( + + ) + default: + return null; + } + })} +
+ ) + })} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx b/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx index 17a27937..71528f73 100644 --- a/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx +++ b/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx @@ -63,9 +63,12 @@ function remarkReferencesPlugin() { return { type: 'html', // @note: if you add additional attributes to this span, make sure to update the rehypeSanitize plugin to allow them. + // + // @note: we attach the reference id to the DOM element as a class name since there may be multiple reference elements + // with the same id (i.e., referencing the same file & range). value: ` void; + onHoveredReferenceChanged: (reference?: FileReference) => void; + isExpanded: boolean; + onExpandedChanged: (isExpanded: boolean) => void; +} + +const ReferencedFileSourceListItem = ({ + id, + code, + language, + revision, + repoName, + repoCodeHostType, + repoDisplayName, + repoWebUrl, + fileName, + references, + selectedReference, + hoveredReference, + onSelectedReferenceChanged, + onHoveredReferenceChanged, + isExpanded, + onExpandedChanged, +}: ReferencedFileSourceListItemProps, forwardedRef: Ref) => { + const theme = useCodeMirrorTheme(); + const [editorRef, setEditorRef] = useState(null); + const captureEvent = useCaptureEvent(); + const domain = useDomain(); + + useImperativeHandle( + forwardedRef, + () => editorRef as ReactCodeMirrorRef + ); + const keymapExtension = useKeymapExtension(editorRef?.view); + const hasCodeNavEntitlement = useHasEntitlement("code-nav"); + + const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view); + const { navigateToPath } = useBrowseNavigation(); + + const getReferenceAtPos = useCallback((x: number, y: number, view: EditorView): FileReference | undefined => { + const pos = view.posAtCoords({ x, y }); + if (pos === null) return undefined; + + // Check if position is within the main editor content area + const rect = view.contentDOM.getBoundingClientRect(); + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { + return undefined; + } + + const line = view.state.doc.lineAt(pos); + const lineNumber = line.number; + + // Check if this line is part of any highlighted range + const matchingRanges = references.filter(({ range }) => + range && lineNumber >= range.startLine && lineNumber <= range.endLine + ); + + // Sort by the length of the range. + // Shorter ranges are more specific, so we want to prioritize them. + matchingRanges.sort((a, b) => { + const aLength = (a.range!.endLine) - (a.range!.startLine); + const bLength = (b.range!.endLine) - (b.range!.startLine); + return aLength - bLength; + }); + + if (matchingRanges.length > 0) { + return matchingRanges[0]; + } + + return undefined; + }, [references]); + + const codeFoldingExtension = useMemo(() => { + return createCodeFoldingExtension(references, 3); + }, [references]); + + const extensions = useMemo(() => { + return [ + languageExtension, + EditorView.lineWrapping, + keymapExtension, + ...(hasCodeNavEntitlement ? [ + symbolHoverTargetsExtension, + ] : []), + codeFoldingExtension, + StateField.define({ + create(state) { + const decorations: Range[] = []; + + for (const { range, id } of references) { + if (!range) { + continue; + } + + const isHovered = id === hoveredReference?.id; + const isSelected = id === selectedReference?.id; + + for (let line = range.startLine; line <= range.endLine; line++) { + // Skip lines that are outside the document bounds. + if (line > state.doc.lines) { + continue; + } + + if (isSelected) { + decorations.push(selectedLineDecoration.range(state.doc.line(line).from)); + } else { + decorations.push(lineDecoration.range(state.doc.line(line).from)); + if (isHovered) { + decorations.push(hoverLineDecoration.range(state.doc.line(line).from)); + } + } + + } + } + + decorations.sort((a, b) => a.from - b.from); + return Decoration.set(decorations); + }, + update(deco, tr) { + return deco.map(tr.changes); + }, + provide: (field) => EditorView.decorations.from(field), + }), + EditorView.domEventHandlers({ + click: (event, view) => { + const reference = getReferenceAtPos(event.clientX, event.clientY, view); + + if (reference) { + onSelectedReferenceChanged(reference.id === selectedReference?.id ? undefined : reference); + return true; // prevent default handling + } + return false; + }, + mouseover: (event, view) => { + const reference = getReferenceAtPos(event.clientX, event.clientY, view); + if (!reference) { + return false; + } + + if (reference.id === selectedReference?.id || reference.id === hoveredReference?.id) { + return false; + } + + onHoveredReferenceChanged(reference); + return true; + }, + mouseout: (event, view) => { + const reference = getReferenceAtPos(event.clientX, event.clientY, view); + if (reference) { + return false; + } + + onHoveredReferenceChanged(undefined); + return true; + } + }) + ]; + }, [ + languageExtension, + keymapExtension, + hasCodeNavEntitlement, + references, + hoveredReference?.id, + selectedReference?.id, + getReferenceAtPos, + onSelectedReferenceChanged, + onHoveredReferenceChanged, + codeFoldingExtension, + ]); + + const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => { + if (symbolDefinitions.length === 0) { + return; + } + + captureEvent('wa_goto_definition_pressed', { + source: 'chat', + }); + createAuditAction({ + action: "user.performed_goto_definition", + metadata: { + message: symbolName, + }, + }, domain); + + if (symbolDefinitions.length === 1) { + const symbolDefinition = symbolDefinitions[0]; + const { fileName, repoName } = symbolDefinition; + + navigateToPath({ + repoName, + revisionName: revision, + path: fileName, + pathType: 'blob', + highlightRange: symbolDefinition.range, + }) + } else { + navigateToPath({ + repoName, + revisionName: revision, + path: fileName, + pathType: 'blob', + setBrowseState: { + selectedSymbolInfo: { + symbolName, + repoName, + revisionName: revision, + language: language, + }, + activeExploreMenuTab: "definitions", + isBottomPanelCollapsed: false, + } + }); + + } + }, [captureEvent, domain, navigateToPath, revision, repoName, fileName, language]); + + const onFindReferences = useCallback((symbolName: string) => { + captureEvent('wa_find_references_pressed', { + source: 'chat', + }); + createAuditAction({ + action: "user.performed_find_references", + metadata: { + message: symbolName, + }, + }, domain); + + navigateToPath({ + repoName, + revisionName: revision, + path: fileName, + pathType: 'blob', + setBrowseState: { + selectedSymbolInfo: { + symbolName, + repoName, + revisionName: revision, + language: language, + }, + activeExploreMenuTab: "references", + isBottomPanelCollapsed: false, + } + }) + + }, [captureEvent, domain, fileName, language, navigateToPath, repoName, revision]); + + const ExpandCollapseIcon = useMemo(() => { + return isExpanded ? ChevronDown : ChevronRight; + }, [isExpanded]); + + return ( +
+ {/* Sentinel element to scroll to when collapsing a file */} +
+ {/* Sticky header outside the bordered container */} +
+ onExpandedChanged(!isExpanded)} /> + +
+ + {/* Code container */} + {/* @note: don't conditionally render here since we want to maintain state */} +
+ + {editorRef && hasCodeNavEntitlement && ( + + )} + +
+
+ ) +} + +export default forwardRef(ReferencedFileSourceListItem) as ( + props: ReferencedFileSourceListItemProps & { ref?: Ref }, +) => ReturnType; diff --git a/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx b/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx index c7a0c087..d97709bf 100644 --- a/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx +++ b/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx @@ -1,33 +1,22 @@ 'use client'; -import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; -import { PathHeader } from "@/app/[domain]/components/pathHeader"; import { fetchFileSource } from "@/app/api/(client)/client"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Skeleton } from "@/components/ui/skeleton"; -import { SymbolHoverPopup } from '@/ee/features/codeNav/components/symbolHoverPopup'; -import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension"; -import { SymbolDefinition } from '@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo'; -import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement"; -import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension"; -import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme"; import { useDomain } from "@/hooks/useDomain"; -import { useKeymapExtension } from "@/hooks/useKeymapExtension"; -import { cn, isServiceError, unwrapServiceError } from "@/lib/utils"; -import { Range } from "@codemirror/state"; -import { Decoration, DecorationSet, EditorView } from '@codemirror/view'; +import { isServiceError, unwrapServiceError } from "@/lib/utils"; import { useQueries } from "@tanstack/react-query"; -import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror'; -import { ChevronDown, ChevronRight } from "lucide-react"; -import { forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; +import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import scrollIntoView from 'scroll-into-view-if-needed'; import { FileReference, FileSource, Reference, Source } from "../../types"; -import { createCodeFoldingExtension } from "./codeFoldingExtension"; +import ReferencedFileSourceListItem from "./referencedFileSourceListItem"; interface ReferencedSourcesListViewProps { references: FileReference[]; sources: Source[]; + index: number; hoveredReference?: Reference; onHoveredReferenceChanged: (reference?: Reference) => void; selectedReference?: Reference; @@ -38,29 +27,14 @@ interface ReferencedSourcesListViewProps { const resolveFileReference = (reference: FileReference, sources: FileSource[]): FileSource | undefined => { return sources.find( (source) => source.repo.endsWith(reference.repo) && - source.path.endsWith(reference.path) + source.path.endsWith(reference.path) ); } -const getFileId = (fileSource: FileSource) => { - return `file-source-${fileSource.repo}-${fileSource.path}`; -} - -const lineDecoration = Decoration.line({ - attributes: { class: "cm-range-border-radius chat-lineHighlight" }, -}); - -const selectedLineDecoration = Decoration.line({ - attributes: { class: "cm-range-border-radius cm-range-border-shadow chat-lineHighlight-selected" }, -}); - -const hoverLineDecoration = Decoration.line({ - attributes: { class: "chat-lineHighlight-hover" }, -}); - export const ReferencedSourcesListView = ({ references, sources, + index, hoveredReference, selectedReference, style, @@ -70,6 +44,14 @@ export const ReferencedSourcesListView = ({ const scrollAreaRef = useRef(null); const editorRefsMap = useRef>(new Map()); const domain = useDomain(); + const [collapsedFileIds, setCollapsedFileIds] = useState([]); + + const getFileId = useCallback((fileSource: FileSource) => { + // @note: we include the index to ensure that the file id is unique + // across other ReferencedSourcesListView components in the + // same thread. + return `file-source-${fileSource.repo}-${fileSource.path}-${index}`; + }, [index]); const setEditorRef = useCallback((fileKey: string, ref: ReactCodeMirrorRef | null) => { if (ref) { @@ -112,7 +94,7 @@ export const ReferencedSourcesListView = ({ } return groupedReferences; - }, [references, referencedFileSources]); + }, [references, referencedFileSources, getFileId]); const fileSourceQueries = useQueries({ queries: referencedFileSources.map((file) => ({ @@ -177,10 +159,17 @@ export const ReferencedSourcesListView = ({ // Calculate the target scroll position to center the line const targetScrollTop = lineTopRelativeToScrollArea - (scrollAreaHeight / 3); + // Expand the file if it's collapsed. + setCollapsedFileIds((collapsedFileIds) => collapsedFileIds.filter((id) => id !== fileId)); + // Scroll to the calculated position - scrollAreaViewport.scrollTo({ - top: Math.max(0, targetScrollTop), - behavior: 'smooth', + // @NOTE: Using requestAnimationFrame is a bit of a hack to ensure + // that the collapsed file ids state has updated before scrolling. + requestAnimationFrame(() => { + scrollAreaViewport.scrollTo({ + top: Math.max(0, targetScrollTop), + behavior: 'smooth', + }); }); } @@ -192,7 +181,7 @@ export const ReferencedSourcesListView = ({ behavior: 'smooth', }); } - }, [referencedFileSources, selectedReference]); + }, [getFileId, referencedFileSources, selectedReference]); if (referencedFileSources.length === 0) { return ( @@ -244,7 +233,7 @@ export const ReferencedSourcesListView = ({ const referencesInFile = referencesGroupedByFile.get(fileId) || []; return ( - setEditorRef(fileId, ref)} @@ -260,21 +250,31 @@ export const ReferencedSourcesListView = ({ onHoveredReferenceChanged={onHoveredReferenceChanged} selectedReference={selectedReference} hoveredReference={hoveredReference} - // When collapsing a file when you are deep in a scroll, it's a better - // experience to have the scroll automatically restored to the top of the file - // s.t., header is still sticky to the top of the scroll area. - onCollapse={() => { - const fileSourceStart = document.getElementById(`${fileId}-start`); - if (!fileSourceStart) { - return; + isExpanded={!collapsedFileIds.includes(fileId)} + onExpandedChanged={(isExpanded) => { + if (isExpanded) { + setCollapsedFileIds(collapsedFileIds.filter((id) => id !== fileId)); + } else { + setCollapsedFileIds([...collapsedFileIds, fileId]); } - scrollIntoView(fileSourceStart, { - scrollMode: 'if-needed', - block: 'start', - behavior: 'instant', - }); - }} + // When collapsing a file when you are deep in a scroll, it's a better + // experience to have the scroll automatically restored to the top of the file + // s.t., header is still sticky to the top of the scroll area. + if (!isExpanded) { + const fileSourceStart = document.getElementById(`${fileId}-start`); + if (!fileSourceStart) { + return; + } + + scrollIntoView(fileSourceStart, { + scrollMode: 'if-needed', + block: 'start', + behavior: 'instant', + }); + } + } + } /> ); })} @@ -283,308 +283,3 @@ export const ReferencedSourcesListView = ({ ); } - -interface CodeMirrorCodeBlockProps { - id: string; - code: string; - language: string; - revision: string; - repoName: string; - repoCodeHostType: string; - repositoryDisplayName?: string; - fileName: string; - references: FileReference[]; - selectedReference?: FileReference; - hoveredReference?: FileReference; - onSelectedReferenceChanged: (reference?: FileReference) => void; - onHoveredReferenceChanged: (reference?: FileReference) => void; - onCollapse: () => void; -} - -const CodeMirrorCodeBlock = ({ - id, - code, - language, - revision, - repoName, - repoCodeHostType, - repositoryDisplayName, - fileName, - references, - selectedReference, - hoveredReference, - onSelectedReferenceChanged, - onHoveredReferenceChanged, - onCollapse, -}: CodeMirrorCodeBlockProps, forwardedRef: Ref) => { - const theme = useCodeMirrorTheme(); - const [editorRef, setEditorRef] = useState(null); - const [isExpanded, _setIsExpanded] = useState(true); - - const setIsExpanded = useCallback((isExpanded: boolean) => { - _setIsExpanded(isExpanded); - if (!isExpanded) { - onCollapse(); - } - }, [onCollapse]); - - useImperativeHandle( - forwardedRef, - () => editorRef as ReactCodeMirrorRef - ); - const keymapExtension = useKeymapExtension(editorRef?.view); - const hasCodeNavEntitlement = useHasEntitlement("code-nav"); - - const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view); - const { navigateToPath } = useBrowseNavigation(); - - const getReferenceAtPos = useCallback((x: number, y: number, view: EditorView): FileReference | undefined => { - const pos = view.posAtCoords({ x, y }); - if (pos === null) return undefined; - - // Check if position is within the main editor content area - const rect = view.contentDOM.getBoundingClientRect(); - if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { - return undefined; - } - - const line = view.state.doc.lineAt(pos); - const lineNumber = line.number; - - // Check if this line is part of any highlighted range - const matchingRanges = references.filter(({ range }) => - range && lineNumber >= range.startLine && lineNumber <= range.endLine - ); - - // Sort by the length of the range. - // Shorter ranges are more specific, so we want to prioritize them. - matchingRanges.sort((a, b) => { - const aLength = (a.range!.endLine) - (a.range!.startLine); - const bLength = (b.range!.endLine) - (b.range!.startLine); - return aLength - bLength; - }); - - if (matchingRanges.length > 0) { - return matchingRanges[0]; - } - - return undefined; - }, [references]); - - const codeFoldingExtension = useMemo(() => { - return createCodeFoldingExtension(references, 3); - }, [references]); - - const extensions = useMemo(() => { - return [ - languageExtension, - EditorView.lineWrapping, - keymapExtension, - ...(hasCodeNavEntitlement ? [ - symbolHoverTargetsExtension, - ] : []), - codeFoldingExtension, - StateField.define({ - create(state) { - const decorations: Range[] = []; - - for (const { range, id } of references) { - if (!range) { - continue; - } - - const isHovered = id === hoveredReference?.id; - const isSelected = id === selectedReference?.id; - - for (let line = range.startLine; line <= range.endLine; line++) { - // Skip lines that are outside the document bounds. - if (line > state.doc.lines) { - continue; - } - - if (isSelected) { - decorations.push(selectedLineDecoration.range(state.doc.line(line).from)); - } else { - decorations.push(lineDecoration.range(state.doc.line(line).from)); - if (isHovered) { - decorations.push(hoverLineDecoration.range(state.doc.line(line).from)); - } - } - - } - } - - decorations.sort((a, b) => a.from - b.from); - return Decoration.set(decorations); - }, - update(deco, tr) { - return deco.map(tr.changes); - }, - provide: (field) => EditorView.decorations.from(field), - }), - EditorView.domEventHandlers({ - click: (event, view) => { - const reference = getReferenceAtPos(event.clientX, event.clientY, view); - - if (reference) { - onSelectedReferenceChanged(reference.id === selectedReference?.id ? undefined : reference); - return true; // prevent default handling - } - return false; - }, - mouseover: (event, view) => { - const reference = getReferenceAtPos(event.clientX, event.clientY, view); - if (!reference) { - return false; - } - - if (reference.id === selectedReference?.id || reference.id === hoveredReference?.id) { - return false; - } - - onHoveredReferenceChanged(reference); - return true; - }, - mouseout: (event, view) => { - const reference = getReferenceAtPos(event.clientX, event.clientY, view); - if (reference) { - return false; - } - - onHoveredReferenceChanged(undefined); - return true; - } - }) - ]; - }, [ - languageExtension, - keymapExtension, - hasCodeNavEntitlement, - references, - hoveredReference?.id, - selectedReference?.id, - getReferenceAtPos, - onSelectedReferenceChanged, - onHoveredReferenceChanged, - codeFoldingExtension, - ]); - - const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => { - if (symbolDefinitions.length === 0) { - return; - } - - if (symbolDefinitions.length === 1) { - const symbolDefinition = symbolDefinitions[0]; - const { fileName, repoName } = symbolDefinition; - - navigateToPath({ - repoName, - revisionName: revision, - path: fileName, - pathType: 'blob', - highlightRange: symbolDefinition.range, - }) - } else { - navigateToPath({ - repoName, - revisionName: revision, - path: fileName, - pathType: 'blob', - setBrowseState: { - selectedSymbolInfo: { - symbolName, - repoName, - revisionName: revision, - language: language, - }, - activeExploreMenuTab: "definitions", - isBottomPanelCollapsed: false, - } - }); - - } - }, [navigateToPath, revision, repoName, fileName, language]); - - const onFindReferences = useCallback((symbolName: string) => { - navigateToPath({ - repoName, - revisionName: revision, - path: fileName, - pathType: 'blob', - setBrowseState: { - selectedSymbolInfo: { - symbolName, - repoName, - revisionName: revision, - language: language, - }, - activeExploreMenuTab: "references", - isBottomPanelCollapsed: false, - } - }) - - }, [fileName, language, navigateToPath, repoName, revision]); - - const ExpandCollapseIcon = useMemo(() => { - return isExpanded ? ChevronDown : ChevronRight; - }, [isExpanded]); - - return ( -
- {/* Sentinel element to scroll to when collapsing a file */} -
- {/* Sticky header outside the bordered container */} -
- setIsExpanded(!isExpanded)} /> - -
- - {/* Code container */} - {/* @note: don't conditionally render here since we want to maintain state */} -
- - {editorRef && hasCodeNavEntitlement && ( - - )} - -
-
- ) -} - -export const CodeMirrorCodeBlockWithRef = forwardRef(CodeMirrorCodeBlock) as ( - props: CodeMirrorCodeBlockProps & { ref?: Ref }, -) => ReturnType; diff --git a/packages/web/src/features/chat/constants.ts b/packages/web/src/features/chat/constants.ts index 6ba38f09..82d458dd 100644 --- a/packages/web/src/features/chat/constants.ts +++ b/packages/web/src/features/chat/constants.ts @@ -1,3 +1,4 @@ +import { SBChatMessagePart } from "./types"; export const FILE_REFERENCE_PREFIX = '@file:'; export const FILE_REFERENCE_REGEX = new RegExp( @@ -13,4 +14,14 @@ export const toolNames = { readFiles: 'readFiles', findSymbolReferences: 'findSymbolReferences', findSymbolDefinitions: 'findSymbolDefinitions', -} as const; \ No newline at end of file +} as const; + +// These part types are visible in the UI. +export const uiVisiblePartTypes: SBChatMessagePart['type'][] = [ + 'reasoning', + 'text', + 'tool-searchCode', + 'tool-readFiles', + 'tool-findSymbolDefinitions', + 'tool-findSymbolReferences', +] as const; \ No newline at end of file diff --git a/packages/web/src/features/chat/tools.ts b/packages/web/src/features/chat/tools.ts index fe6f8542..4149635a 100644 --- a/packages/web/src/features/chat/tools.ts +++ b/packages/web/src/features/chat/tools.ts @@ -9,6 +9,17 @@ import { FileSourceResponse } from "../search/types"; import { addLineNumbers } from "./utils"; import { toolNames } from "./constants"; +// @NOTE: When adding a new tool, follow these steps: +// 1. Add the tool to the `toolNames` constant in `constants.ts`. +// 2. Add the tool to the `SBChatMessageToolTypes` type in `types.ts`. +// 3. Add the tool to the `tools` prop in `agent.ts`. +// 4. If the tool is meant to be rendered in the UI: +// - Add the tool to the `uiVisiblePartTypes` constant in `constants.ts`. +// - Add the tool's component to the `DetailsCard` switch statement in `detailsCard.tsx`. +// +// - bk, 2025-07-25 + + export const findSymbolReferencesTool = tool({ description: `Finds references to a symbol in the codebase.`, inputSchema: z.object({ diff --git a/packages/web/src/features/chat/useExtractReferences.test.ts b/packages/web/src/features/chat/useExtractReferences.test.ts index b2121e6c..9a8d3e71 100644 --- a/packages/web/src/features/chat/useExtractReferences.test.ts +++ b/packages/web/src/features/chat/useExtractReferences.test.ts @@ -1,22 +1,16 @@ import { expect, test } from 'vitest' -import { SBChatMessage } from './types'; import { renderHook } from '@testing-library/react-hooks'; import { useExtractReferences } from './useExtractReferences'; import { getFileReferenceId } from './utils'; +import { TextUIPart } from 'ai'; test('useExtractReferences extracts file references from text content', () => { - const message: SBChatMessage = { - id: 'msg1', - role: 'assistant', - parts: [ - { - type: 'text', - text: 'The auth flow is implemented in @file:{github.com/sourcebot-dev/sourcebot::auth.ts} and uses sessions @file:{github.com/sourcebot-dev/sourcebot::auth.ts:45-60}.' - } - ] - }; + const part: TextUIPart = { + type: 'text', + text: 'The auth flow is implemented in @file:{github.com/sourcebot-dev/sourcebot::auth.ts} and uses sessions @file:{github.com/sourcebot-dev/sourcebot::auth.ts:45-60}.' + } - const { result } = renderHook(() => useExtractReferences(message)); + const { result } = renderHook(() => useExtractReferences(part)); expect(result.current).toHaveLength(2); expect(result.current[0]).toMatchObject({ @@ -44,121 +38,3 @@ test('useExtractReferences extracts file references from text content', () => { } }); }); - -test('useExtractReferences extracts file references from reasoning content', () => { - const message: SBChatMessage = { - id: 'msg1', - role: 'assistant', - parts: [ - { - type: 'reasoning', - text: 'The auth flow is implemented in @file:{github.com/sourcebot-dev/sourcebot::auth.ts} and uses sessions @file:{github.com/sourcebot-dev/sourcebot::auth.ts:45-60}.' - } - ] - }; - - const { result } = renderHook(() => useExtractReferences(message)); - - expect(result.current).toHaveLength(2); - expect(result.current[0]).toMatchObject({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'auth.ts', - id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }), - type: 'file', - }); - - expect(result.current[1]).toMatchObject({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'auth.ts', - id: getFileReferenceId({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'auth.ts', - range: { - startLine: 45, - endLine: 60, - } - }), - type: 'file', - range: { - startLine: 45, - endLine: 60, - } - }); -}); - -test('useExtractReferences extracts file references from multi-part', () => { - const message: SBChatMessage = { - id: 'msg1', - role: 'assistant', - parts: [ - { - type: 'text', - text: 'The auth flow is implemented in @file:{github.com/sourcebot-dev/sourcebot::auth.ts}.' - }, - { - type: 'reasoning', - text: 'We need to check the session handling in @file:{github.com/sourcebot-dev/sourcebot::session.ts:10-20}.' - }, - { - type: 'text', - text: 'The configuration is stored in @file:{github.com/sourcebot-dev/sourcebot::config.json} and @file:{github.com/sourcebot-dev/sourcebot::utils.ts:5}.' - } - ] - }; - - const { result } = renderHook(() => useExtractReferences(message)); - - expect(result.current).toHaveLength(4); - - // From text part - expect(result.current[0]).toMatchObject({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'auth.ts', - id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }), - type: 'file', - }); - - // From reasoning part - expect(result.current[1]).toMatchObject({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'session.ts', - id: getFileReferenceId({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'session.ts', - range: { - startLine: 10, - endLine: 20, - } - }), - type: 'file', - range: { - startLine: 10, - endLine: 20, - } - }); - - expect(result.current[2]).toMatchObject({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'config.json', - id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'config.json' }), - type: 'file', - }); - - expect(result.current[3]).toMatchObject({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'utils.ts', - id: getFileReferenceId({ - repo: 'github.com/sourcebot-dev/sourcebot', - path: 'utils.ts', - range: { - startLine: 5, - endLine: 5, - } - }), - type: 'file', - range: { - startLine: 5, - endLine: 5, - } - }); -}); diff --git a/packages/web/src/features/chat/useExtractReferences.ts b/packages/web/src/features/chat/useExtractReferences.ts index b3eecf97..45ef173d 100644 --- a/packages/web/src/features/chat/useExtractReferences.ts +++ b/packages/web/src/features/chat/useExtractReferences.ts @@ -1,39 +1,36 @@ 'use client'; import { useMemo } from "react"; -import { SBChatMessage, FileReference } from "./types"; +import { FileReference } from "./types"; import { FILE_REFERENCE_REGEX } from "./constants"; import { createFileReference } from "./utils"; +import { TextUIPart } from "ai"; -export const useExtractReferences = (message?: SBChatMessage) => { +export const useExtractReferences = (part?: TextUIPart) => { return useMemo(() => { + if (!part) { + return []; + } + const references: FileReference[] = []; - message?.parts.forEach((part) => { - switch (part.type) { - case 'text': - case 'reasoning': { - const content = part.text; - FILE_REFERENCE_REGEX.lastIndex = 0; + const content = part.text; + FILE_REFERENCE_REGEX.lastIndex = 0; - let match; - while ((match = FILE_REFERENCE_REGEX.exec(content ?? '')) !== null && match !== null) { - const [_, repo, fileName, startLine, endLine] = match; + let match; + while ((match = FILE_REFERENCE_REGEX.exec(content ?? '')) !== null && match !== null) { + const [_, repo, fileName, startLine, endLine] = match; - const fileReference = createFileReference({ - repo: repo, - path: fileName, - startLine, - endLine, - }); + const fileReference = createFileReference({ + repo: repo, + path: fileName, + startLine, + endLine, + }); - references.push(fileReference); - } - break; - } - } - }); + references.push(fileReference); + } return references; - }, [message]); + }, [part]); }; diff --git a/packages/web/src/features/chat/utils.test.ts b/packages/web/src/features/chat/utils.test.ts index af78fa6d..72390378 100644 --- a/packages/web/src/features/chat/utils.test.ts +++ b/packages/web/src/features/chat/utils.test.ts @@ -1,5 +1,5 @@ import { expect, test, vi } from 'vitest' -import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairCitations } from './utils' +import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from './utils' import { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants'; import { SBChatMessage, SBChatMessagePart } from './types'; @@ -243,87 +243,111 @@ test('getAnswerPartFromAssistantMessage returns undefined when streaming and no expect(result).toBeUndefined(); }); -test('repairCitations fixes missing colon after @file', () => { +test('repairReferences fixes missing colon after @file', () => { const input = 'See the function in @file{github.com/sourcebot-dev/sourcebot::auth.ts} for details.'; const expected = 'See the function in @file:{github.com/sourcebot-dev/sourcebot::auth.ts} for details.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations fixes missing colon with range', () => { +test('repairReferences fixes missing colon with range', () => { const input = 'Check @file{github.com/sourcebot-dev/sourcebot::config.ts:15-20} for the configuration.'; const expected = 'Check @file:{github.com/sourcebot-dev/sourcebot::config.ts:15-20} for the configuration.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations fixes missing braces around filename', () => { +test('repairReferences fixes missing braces around filename', () => { const input = 'The logic is in @file:github.com/sourcebot-dev/sourcebot::utils.js and handles validation.'; const expected = 'The logic is in @file:{github.com/sourcebot-dev/sourcebot::utils.js} and handles validation.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations fixes missing braces with path', () => { +test('repairReferences fixes missing braces with path', () => { const input = 'Look at @file:github.com/sourcebot-dev/sourcebot::src/components/Button.tsx for the component.'; const expected = 'Look at @file:{github.com/sourcebot-dev/sourcebot::src/components/Button.tsx} for the component.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations removes multiple ranges keeping only first', () => { +test('repairReferences removes multiple ranges keeping only first', () => { const input = 'See @file:{github.com/sourcebot-dev/sourcebot::service.ts:10-15,20-25,30-35} for implementation.'; const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::service.ts:10-15} for implementation.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations fixes malformed triple number ranges', () => { +test('repairReferences fixes malformed triple number ranges', () => { const input = 'Check @file:{github.com/sourcebot-dev/sourcebot::handler.ts:5-10-15} for the logic.'; const expected = 'Check @file:{github.com/sourcebot-dev/sourcebot::handler.ts:5-10} for the logic.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations handles multiple citations in same text', () => { +test('repairReferences handles multiple citations in same text', () => { const input = 'See @file{github.com/sourcebot-dev/sourcebot::auth.ts} and @file:github.com/sourcebot-dev/sourcebot::config.js for setup details.'; const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::auth.ts} and @file:{github.com/sourcebot-dev/sourcebot::config.js} for setup details.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations leaves correctly formatted citations unchanged', () => { +test('repairReferences leaves correctly formatted citations unchanged', () => { const input = 'The function @file:{github.com/sourcebot-dev/sourcebot::utils.ts:42-50} handles validation correctly.'; - expect(repairCitations(input)).toBe(input); + expect(repairReferences(input)).toBe(input); }); -test('repairCitations handles edge cases with spaces and punctuation', () => { +test('repairReferences handles edge cases with spaces and punctuation', () => { const input = 'Functions like @file:github.com/sourcebot-dev/sourcebot::helper.ts, @file{github.com/sourcebot-dev/sourcebot::main.js}, and @file:{github.com/sourcebot-dev/sourcebot::app.ts:1-5,10-15} work.'; const expected = 'Functions like @file:{github.com/sourcebot-dev/sourcebot::helper.ts}, @file:{github.com/sourcebot-dev/sourcebot::main.js}, and @file:{github.com/sourcebot-dev/sourcebot::app.ts:1-5} work.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations returns empty string unchanged', () => { - expect(repairCitations('')).toBe(''); +test('repairReferences returns empty string unchanged', () => { + expect(repairReferences('')).toBe(''); }); -test('repairCitations returns text without citations unchanged', () => { +test('repairReferences returns text without citations unchanged', () => { const input = 'This is just regular text without any file references.'; - expect(repairCitations(input)).toBe(input); + expect(repairReferences(input)).toBe(input); }); -test('repairCitations handles complex file paths correctly', () => { +test('repairReferences handles complex file paths correctly', () => { const input = 'Check @file:github.com/sourcebot-dev/sourcebot::src/components/ui/Button/index.tsx for implementation.'; const expected = 'Check @file:{github.com/sourcebot-dev/sourcebot::src/components/ui/Button/index.tsx} for implementation.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations handles files with numbers and special characters', () => { +test('repairReferences handles files with numbers and special characters', () => { const input = 'See @file{github.com/sourcebot-dev/sourcebot::utils-v2.0.1.ts} and @file:github.com/sourcebot-dev/sourcebot::config_2024.json for setup.'; const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::utils-v2.0.1.ts} and @file:{github.com/sourcebot-dev/sourcebot::config_2024.json} for setup.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations handles citation at end of sentence', () => { +test('repairReferences handles citation at end of sentence', () => { const input = 'The implementation is in @file:github.com/sourcebot-dev/sourcebot::helper.ts.'; const expected = 'The implementation is in @file:{github.com/sourcebot-dev/sourcebot::helper.ts}.'; - expect(repairCitations(input)).toBe(expected); + expect(repairReferences(input)).toBe(expected); }); -test('repairCitations preserves already correct citations with ranges', () => { +test('repairReferences preserves already correct citations with ranges', () => { const input = 'The function @file:{github.com/sourcebot-dev/sourcebot::utils.ts:10-20} and variable @file:{github.com/sourcebot-dev/sourcebot::config.js:5} work correctly.'; - expect(repairCitations(input)).toBe(input); + expect(repairReferences(input)).toBe(input); }); + +test('repairReferences handles extra closing parenthesis', () => { + const input = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts:5-6)} for details.'; + const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts:5-6} for details.'; + expect(repairReferences(input)).toBe(expected); +}); + +test('repairReferences handles extra colon at end of range', () => { + const input = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts:5-6:} for details.'; + const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts:5-6} for details.'; + expect(repairReferences(input)).toBe(expected); +}); + +test('repairReferences handles inline code blocks around file references', () => { + const input = 'See `@file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts}` for details.'; + const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts} for details.'; + expect(repairReferences(input)).toBe(expected); +}); + +test('repairReferences handles malformed inline code blocks', () => { + const input = 'See `@file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts`} for details.'; + const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts} for details.'; + expect(repairReferences(input)).toBe(expected); +}); \ No newline at end of file diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index ec1385b1..f9651115 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -239,23 +239,26 @@ export const createFileReference = ({ repo, path, startLine, endLine }: { repo: /** * Converts LLM text that includes references (e.g., @file:...) into a portable * Markdown format. Practically, this means converting references into Markdown - * links. + * links and removing the answer tag. */ export const convertLLMOutputToPortableMarkdown = (text: string): string => { - return text.replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => { - const displayName = fileName.split('/').pop() || fileName; + return text + .replace(ANSWER_TAG, '') + .replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => { + const displayName = fileName.split('/').pop() || fileName; - let linkText = displayName; - if (startLine) { - if (endLine && startLine !== endLine) { - linkText += `:${startLine}-${endLine}`; - } else { - linkText += `:${startLine}`; + let linkText = displayName; + if (startLine) { + if (endLine && startLine !== endLine) { + linkText += `:${startLine}-${endLine}`; + } else { + linkText += `:${startLine}`; + } } - } - return `[${linkText}](${fileName})`; - }); + return `[${linkText}](${fileName})`; + }) + .trim(); } // Groups message parts into groups based on step-start delimiters. @@ -288,7 +291,7 @@ export const groupMessageIntoSteps = (parts: SBChatMessagePart[]) => { } // LLMs like to not follow instructions... this takes care of some common mistakes they tend to make. -export const repairCitations = (text: string): string => { +export const repairReferences = (text: string): string => { return text // Fix missing colon: @file{...} -> @file:{...} .replace(/@file\{([^}]+)\}/g, '@file:{$1}') @@ -297,7 +300,15 @@ export const repairCitations = (text: string): string => { // Fix multiple ranges: keep only first range .replace(/@file:\{(.+?):(\d+-\d+),[\d,-]+\}/g, '@file:{$1:$2}') // Fix malformed ranges - .replace(/@file:\{(.+?):(\d+)-(\d+)-(\d+)\}/g, '@file:{$1:$2-$3}'); + .replace(/@file:\{(.+?):(\d+)-(\d+)-(\d+)\}/g, '@file:{$1:$2-$3}') + // Fix extra closing parenthesis: @file:{...)} -> @file:{...} + .replace(/@file:\{([^}]+)\)\}/g, '@file:{$1}') + // Fix extra colon at end: @file:{...range:} -> @file:{...range} + .replace(/@file:\{(.+?):(\d+(?:-\d+)?):?\}/g, '@file:{$1:$2}') + // Fix inline code blocks around file references: `@file:{...}` -> @file:{...} + .replace(/`(@file:\{[^}]+\})`/g, '$1') + // Fix malformed inline code blocks: `@file:{...`} -> @file:{...} + .replace(/`(@file:\{[^`]+)`\}/g, '$1}'); }; // Attempts to find the part of the assistant's message @@ -307,19 +318,13 @@ export const getAnswerPartFromAssistantMessage = (message: SBChatMessage, isStre .findLast((part) => part.type === 'text') if (lastTextPart?.text.startsWith(ANSWER_TAG)) { - return { - ...lastTextPart, - text: repairCitations(lastTextPart.text), - }; + return lastTextPart; } // If the agent did not include the answer tag, then fallback to using the last text part. // Only do this when we are no longer streaming since the agent may still be thinking. if (!isStreaming && lastTextPart) { - return { - ...lastTextPart, - text: repairCitations(lastTextPart.text), - }; + return lastTextPart; } return undefined; diff --git a/packages/web/src/features/search/fileSourceApi.ts b/packages/web/src/features/search/fileSourceApi.ts index f34703e6..aa3ae07a 100644 --- a/packages/web/src/features/search/fileSourceApi.ts +++ b/packages/web/src/features/search/fileSourceApi.ts @@ -55,6 +55,7 @@ export const getFileSource = async ({ fileName, repository, branch }: FileSource repository, repositoryCodeHostType: repoInfo.codeHostType, repositoryDisplayName: repoInfo.displayName, + repositoryWebUrl: repoInfo.webUrl, branch, webUrl: file.webUrl, } satisfies FileSourceResponse; diff --git a/packages/web/src/features/search/schemas.ts b/packages/web/src/features/search/schemas.ts index ecf198a6..7b84081e 100644 --- a/packages/web/src/features/search/schemas.ts +++ b/packages/web/src/features/search/schemas.ts @@ -118,6 +118,7 @@ export const fileSourceResponseSchema = z.object({ repository: z.string(), repositoryCodeHostType: z.string(), repositoryDisplayName: z.string().optional(), + repositoryWebUrl: z.string().optional(), branch: z.string().optional(), webUrl: z.string().optional(), }); \ No newline at end of file diff --git a/packages/web/src/lib/posthogEvents.ts b/packages/web/src/lib/posthogEvents.ts index be5932ba..6637e51e 100644 --- a/packages/web/src/lib/posthogEvents.ts +++ b/packages/web/src/lib/posthogEvents.ts @@ -268,11 +268,12 @@ export type PosthogEventMap = { wa_api_key_created: {}, wa_api_key_creation_fail: {}, ////////////////////////////////////////////////////////////////// - wa_preview_panel_find_references_pressed: {}, - wa_preview_panel_goto_definition_pressed: {}, - ////////////////////////////////////////////////////////////////// - wa_browse_find_references_pressed: {}, - wa_browse_goto_definition_pressed: {}, + wa_goto_definition_pressed: { + source: 'chat' | 'browse' | 'preview', + }, + wa_find_references_pressed: { + source: 'chat' | 'browse' | 'preview', + }, ////////////////////////////////////////////////////////////////// wa_explore_menu_reference_clicked: {}, ////////////////////////////////////////////////////////////////// diff --git a/yarn.lock b/yarn.lock index 1bd1b559..fca7192d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,137 +5,137 @@ __metadata: version: 8 cacheKey: 10c0 -"@ai-sdk/amazon-bedrock@npm:3.0.0-beta.9": - version: 3.0.0-beta.9 - resolution: "@ai-sdk/amazon-bedrock@npm:3.0.0-beta.9" +"@ai-sdk/amazon-bedrock@npm:3.0.0-beta.10": + version: 3.0.0-beta.10 + resolution: "@ai-sdk/amazon-bedrock@npm:3.0.0-beta.10" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" "@smithy/eventstream-codec": "npm:^4.0.1" "@smithy/util-utf8": "npm:^4.0.0" aws4fetch: "npm:^1.0.20" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/b7033125f7d00eaefeeda3316bf0dc5d4c3b79c29b5123434d315809f7127e5b6142234d1672aed4133cafbf18983a31c64ad17d8bb58b55000a9c7860cdbd19 + checksum: 10c0/1e18b20adddee827337e15939f298c621464547819ea9c5f12746f36e6c4fd2215abc9b2ac3445de63dc58550c7b465375b0377a3a7045cee38c8b6da0ed0d72 languageName: node linkType: hard -"@ai-sdk/anthropic@npm:2.0.0-beta.8": - version: 2.0.0-beta.8 - resolution: "@ai-sdk/anthropic@npm:2.0.0-beta.8" +"@ai-sdk/anthropic@npm:2.0.0-beta.9": + version: 2.0.0-beta.9 + resolution: "@ai-sdk/anthropic@npm:2.0.0-beta.9" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/60a026bf0aaff680d1397bc736e5fe051944146fceba1327aa7e92a45f20050f5c9612b8b90764314b022d9686125e5dc3a3494afe983e8864dbc06c4c6fa2ab + checksum: 10c0/ed7974f9ad399d206629a5bfa88964f9542cb95f820a0710b2b0af9677029e2164a5efa2e2d53cb6592a3eba6a43c8e963a7039fba9ff331ada17b98a2838f66 languageName: node linkType: hard -"@ai-sdk/azure@npm:2.0.0-beta.11": - version: 2.0.0-beta.11 - resolution: "@ai-sdk/azure@npm:2.0.0-beta.11" +"@ai-sdk/azure@npm:2.0.0-beta.12": + version: 2.0.0-beta.12 + resolution: "@ai-sdk/azure@npm:2.0.0-beta.12" dependencies: - "@ai-sdk/openai": "npm:2.0.0-beta.11" + "@ai-sdk/openai": "npm:2.0.0-beta.12" "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/3de3bdfde6f604d6ed7e199e15acaed4844e7b59cadc99a662dbaea8bdd509198ab734b8fe41183b39ca73f24ca886cd90b924991929e6b81e6ec039328539b1 + checksum: 10c0/aaf5704c91a00b2f48b0e6b916c958803c5e3761fafa83e9e617a2f2ba2adbda911a0f8cd221297f17926f62d09dcf9fc0252851ec7455be45bd751dd485b19e languageName: node linkType: hard -"@ai-sdk/deepseek@npm:1.0.0-beta.8": - version: 1.0.0-beta.8 - resolution: "@ai-sdk/deepseek@npm:1.0.0-beta.8" +"@ai-sdk/deepseek@npm:1.0.0-beta.9": + version: 1.0.0-beta.9 + resolution: "@ai-sdk/deepseek@npm:1.0.0-beta.9" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.0-beta.8" + "@ai-sdk/openai-compatible": "npm:1.0.0-beta.9" "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/4ff14a3032dcbf931db0f8b02e992ffdee541a2eca1aa49ffae4d56d9f9a14f5c4cc6fbbee03e1841964d00dbe9f7fa55c78ca7ea2c33865bf681d72ac0cf26b + checksum: 10c0/4dd98316ab91610ab64aea2f44c701d59ea37a5a6480f3a27470cfa3109348e8b1dd0117a9e235150ff1c81a47454cfc26a46e13e8c2896710eae2cd403f84eb languageName: node linkType: hard -"@ai-sdk/gateway@npm:1.0.0-beta.12": - version: 1.0.0-beta.12 - resolution: "@ai-sdk/gateway@npm:1.0.0-beta.12" +"@ai-sdk/gateway@npm:1.0.0-beta.14": + version: 1.0.0-beta.14 + resolution: "@ai-sdk/gateway@npm:1.0.0-beta.14" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/acdb23c8a99dc7c412db32dc55bc1e766b7b65988a312f7622686e2ef986ca64e32b1213d3045a855248334e7f328173267e81461ebb9f557a91a81484d2932f + checksum: 10c0/f3d155bd7c5a842a126dbdf25eb16cadb4f785f516e28c995d7e430f0c1974466b402552fdf9f00e6897584299b927888ecb6319599646c12373b3bf147647f9 languageName: node linkType: hard -"@ai-sdk/google-vertex@npm:3.0.0-beta.16": - version: 3.0.0-beta.16 - resolution: "@ai-sdk/google-vertex@npm:3.0.0-beta.16" +"@ai-sdk/google-vertex@npm:3.0.0-beta.17": + version: 3.0.0-beta.17 + resolution: "@ai-sdk/google-vertex@npm:3.0.0-beta.17" dependencies: - "@ai-sdk/anthropic": "npm:2.0.0-beta.8" - "@ai-sdk/google": "npm:2.0.0-beta.14" + "@ai-sdk/anthropic": "npm:2.0.0-beta.9" + "@ai-sdk/google": "npm:2.0.0-beta.15" "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" google-auth-library: "npm:^9.15.0" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/c8b82da3ec9840b1c05cf8c0bc5ff47b6bc058aa6ea6f2bc006be908afdba9056a15e0cfb1d03395cd9abefa71d58ed11eaf88570b0f5a5ea296a817c00cd676 + checksum: 10c0/95544f7f1fd0b7c2bf67d98c87233738a82a733e86c9c809f22b2c1db8809baa1ba2cea9edab7bc47f7947aa314507fa67ce741e54cb881e06341598c7e7dd33 languageName: node linkType: hard -"@ai-sdk/google@npm:2.0.0-beta.14": - version: 2.0.0-beta.14 - resolution: "@ai-sdk/google@npm:2.0.0-beta.14" +"@ai-sdk/google@npm:2.0.0-beta.15": + version: 2.0.0-beta.15 + resolution: "@ai-sdk/google@npm:2.0.0-beta.15" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/24c356541fedbbccbbade82a470a9ee76779661196ca02e4c78d2081660981d6a0034d6af315b2fb603a9f880ae601bda00bd6ae1495c7e0844c44f0a4fe6d0f + checksum: 10c0/527f16f46b8ab3240a38c39d1f5b09f3e9ca66f10229676647e86b1a0e13901c5bc4739386e4a81036657f01d28cd16b8bc206a3de5a425b2bb67961b5166db7 languageName: node linkType: hard -"@ai-sdk/mistral@npm:2.0.0-beta.6": - version: 2.0.0-beta.6 - resolution: "@ai-sdk/mistral@npm:2.0.0-beta.6" +"@ai-sdk/mistral@npm:2.0.0-beta.7": + version: 2.0.0-beta.7 + resolution: "@ai-sdk/mistral@npm:2.0.0-beta.7" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/075cbfc709b5c9b1af69db05c8f8f9e3edfe0caadaad72f26ceb4ee7022b785b8af62e982cfc6c496dff1386da0dd9742d675c55a14f348aff492bed52a310e5 + checksum: 10c0/69000e13adb306d33199818a97bfbed8d721b8c453f53ff58b25d6b554b3e65ce6c3f5239bfa61fdf15fb9ceb5a4b4f768173fde8c26d059f73b5a66a54df4d8 languageName: node linkType: hard -"@ai-sdk/openai-compatible@npm:1.0.0-beta.8": - version: 1.0.0-beta.8 - resolution: "@ai-sdk/openai-compatible@npm:1.0.0-beta.8" +"@ai-sdk/openai-compatible@npm:1.0.0-beta.9": + version: 1.0.0-beta.9 + resolution: "@ai-sdk/openai-compatible@npm:1.0.0-beta.9" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/047f044bf0da9608e09073957916373bd39760ec00f498ba0c4a597ec70ba9eb4ef31f06b21b363b3c1ba775f64fcc46d41b60a171e0e99250824817ecb19ba8 + checksum: 10c0/bee6d3acef2efd874fcdd83662349b95172011addb9a224187920784cf5fec53a3eb4b4ca2801cb8b745f90c4a2406c4683ef006c48d94d6a91492c68289e636 languageName: node linkType: hard -"@ai-sdk/openai@npm:2.0.0-beta.11": - version: 2.0.0-beta.11 - resolution: "@ai-sdk/openai@npm:2.0.0-beta.11" +"@ai-sdk/openai@npm:2.0.0-beta.12": + version: 2.0.0-beta.12 + resolution: "@ai-sdk/openai@npm:2.0.0-beta.12" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/c48664c651cd50c10db5b18b963e39a964d5d69649c24350bff5cca3f5b02ef4f75531ff93a51e8463db91023b050d7f415c81ea0fd48eeb5a55bb5233b151a6 + checksum: 10c0/a96f918f6264a335f26ff694c8952085dbb9a07df455ef32fcd5e8cc4ed7f7e59f2581e7b962a1c38ffd9d74d19290e78c003ab1c568287e029349652852a5a2 languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:3.0.0-beta.5": - version: 3.0.0-beta.5 - resolution: "@ai-sdk/provider-utils@npm:3.0.0-beta.5" +"@ai-sdk/provider-utils@npm:3.0.0-beta.6": + version: 3.0.0-beta.6 + resolution: "@ai-sdk/provider-utils@npm:3.0.0-beta.6" dependencies: "@ai-sdk/provider": "npm:2.0.0-beta.1" "@standard-schema/spec": "npm:^1.0.0" @@ -143,7 +143,7 @@ __metadata: zod-to-json-schema: "npm:^3.24.1" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/229a53672accc5d9d986da2e18f619dbcfaf64ab269c8cc9e955480c4428d2a87255330c587453d01eb66ac297bb6975f91c24a93f87dd4b84f6428cb60d4211 + checksum: 10c0/d1cc412d637689e9252b7e14c8db03e98df06bfd471aba2b1a1d715dbd1353854d046f3028dca6460b2f3741f9d76b0cf52ad76b4c833e3da87bb27d026a450a languageName: node linkType: hard @@ -156,12 +156,12 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/react@npm:2.0.0-beta.26": - version: 2.0.0-beta.26 - resolution: "@ai-sdk/react@npm:2.0.0-beta.26" +"@ai-sdk/react@npm:2.0.0-beta.28": + version: 2.0.0-beta.28 + resolution: "@ai-sdk/react@npm:2.0.0-beta.28" dependencies: - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" - ai: "npm:5.0.0-beta.26" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" + ai: "npm:5.0.0-beta.28" swr: "npm:^2.2.5" throttleit: "npm:2.1.0" peerDependencies: @@ -170,20 +170,20 @@ __metadata: peerDependenciesMeta: zod: optional: true - checksum: 10c0/75583fec8fdb4ceaac75aa5ff00157532ac69d50d9b88604b8f531a68a7b94a6e6ad02e9c54da039903391d403568f0a49837b75f2d860f1fb885ff2d97c8acd + checksum: 10c0/a3435b49eade4d51bbd608aba10102393fd0555004db4b300642fbf70617022741413230a5941afbadc7baf8a3a6f8a5607e50fae1616992c0b706760fc091b9 languageName: node linkType: hard -"@ai-sdk/xai@npm:2.0.0-beta.10": - version: 2.0.0-beta.10 - resolution: "@ai-sdk/xai@npm:2.0.0-beta.10" +"@ai-sdk/xai@npm:2.0.0-beta.11": + version: 2.0.0-beta.11 + resolution: "@ai-sdk/xai@npm:2.0.0-beta.11" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.0-beta.8" + "@ai-sdk/openai-compatible": "npm:1.0.0-beta.9" "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/8f6251785892db79306c95cdde38cbede40c4c73c354bfbdc78262fe2b6736646d0ce4186028e81d0ea59cdf6e584f53afdc2a3e3299e5df754d59c9ad828688 + checksum: 10c0/1a3d8c4bab61cba471eb4fa2cf010c53d4c0ba56dec464bf2eebf37723049a40369db5ffc0d19a561a11b928f0509e2bc61d9d8f745266f17ad41b34fa850179 languageName: node linkType: hard @@ -6497,16 +6497,16 @@ __metadata: version: 0.0.0-use.local resolution: "@sourcebot/web@workspace:packages/web" dependencies: - "@ai-sdk/amazon-bedrock": "npm:3.0.0-beta.9" - "@ai-sdk/anthropic": "npm:2.0.0-beta.8" - "@ai-sdk/azure": "npm:2.0.0-beta.11" - "@ai-sdk/deepseek": "npm:1.0.0-beta.8" - "@ai-sdk/google": "npm:2.0.0-beta.14" - "@ai-sdk/google-vertex": "npm:3.0.0-beta.16" - "@ai-sdk/mistral": "npm:2.0.0-beta.6" - "@ai-sdk/openai": "npm:2.0.0-beta.11" - "@ai-sdk/react": "npm:2.0.0-beta.26" - "@ai-sdk/xai": "npm:2.0.0-beta.10" + "@ai-sdk/amazon-bedrock": "npm:3.0.0-beta.10" + "@ai-sdk/anthropic": "npm:2.0.0-beta.9" + "@ai-sdk/azure": "npm:2.0.0-beta.12" + "@ai-sdk/deepseek": "npm:1.0.0-beta.9" + "@ai-sdk/google": "npm:2.0.0-beta.15" + "@ai-sdk/google-vertex": "npm:3.0.0-beta.17" + "@ai-sdk/mistral": "npm:2.0.0-beta.7" + "@ai-sdk/openai": "npm:2.0.0-beta.12" + "@ai-sdk/react": "npm:2.0.0-beta.28" + "@ai-sdk/xai": "npm:2.0.0-beta.11" "@auth/prisma-adapter": "npm:^2.7.4" "@codemirror/commands": "npm:^6.6.0" "@codemirror/lang-cpp": "npm:^6.0.2" @@ -6603,7 +6603,7 @@ __metadata: "@vercel/otel": "npm:^1.13.0" "@viz-js/lang-dot": "npm:^1.0.4" "@xiechao/codemirror-lang-handlebars": "npm:^1.0.4" - ai: "npm:5.0.0-beta.26" + ai: "npm:5.0.0-beta.28" ajv: "npm:^8.17.1" bcryptjs: "npm:^3.0.2" class-variance-authority: "npm:^0.7.0" @@ -7991,19 +7991,19 @@ __metadata: languageName: node linkType: hard -"ai@npm:5.0.0-beta.26": - version: 5.0.0-beta.26 - resolution: "ai@npm:5.0.0-beta.26" +"ai@npm:5.0.0-beta.28": + version: 5.0.0-beta.28 + resolution: "ai@npm:5.0.0-beta.28" dependencies: - "@ai-sdk/gateway": "npm:1.0.0-beta.12" + "@ai-sdk/gateway": "npm:1.0.0-beta.14" "@ai-sdk/provider": "npm:2.0.0-beta.1" - "@ai-sdk/provider-utils": "npm:3.0.0-beta.5" + "@ai-sdk/provider-utils": "npm:3.0.0-beta.6" "@opentelemetry/api": "npm:1.9.0" peerDependencies: zod: ^3.25.76 || ^4 bin: ai: dist/bin/ai.min.js - checksum: 10c0/a3161a5bd9f6fa9a362a1c938603efc3b806828a297232207126d5c0b3ec45f03212ee5b046dced9df7ad4e48dec7829ef5ac133d12a296f86b2c33ea71ad515 + checksum: 10c0/58f178923ac885cde420091529cdc347b39f52389c06f7a1186564cb7936b761b4790aafd2d9e32c03b8805336be94c94957d3f0515acad7922a41c1d5239cda languageName: node linkType: hard