fix(ask_sb): Various improvements to the references system (#396)

This commit is contained in:
Brendan Kellam 2025-07-25 18:34:33 -07:00 committed by GitHub
parent efc9656b6e
commit 41addb50a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 961 additions and 850 deletions

View file

@ -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",

View file

@ -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: {

View file

@ -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: {

View file

@ -269,6 +269,7 @@ export const ChatThread = ({
return (
<Fragment key={index}>
<ChatThreadListItem
index={index}
chatId={chatId}
userMessage={userMessage}
assistantMessage={assistantMessage}

View file

@ -1,26 +1,19 @@
'use client';
import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle';
import { Card, CardContent } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import { Separator } from '@/components/ui/separator';
import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';
import { Brain, CheckCircle, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, Zap } from 'lucide-react';
import { CheckCircle, Loader2 } from 'lucide-react';
import { CSSProperties, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import { ANSWER_TAG } from '../../constants';
import { Reference, referenceSchema, SBChatMessage, SBChatMessageMetadata, Source } from "../../types";
import { Reference, referenceSchema, SBChatMessage, Source } from "../../types";
import { useExtractReferences } from '../../useExtractReferences';
import { getAnswerPartFromAssistantMessage, groupMessageIntoSteps } from '../../utils';
import { getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from '../../utils';
import { AnswerCard } from './answerCard';
import { DetailsCard } from './detailsCard';
import { MarkdownRenderer, REFERENCE_PAYLOAD_ATTRIBUTE } from './markdownRenderer';
import { ReferencedSourcesListView } from './referencedSourcesListView';
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
import { uiVisiblePartTypes } from '../../constants';
interface ChatThreadListItemProps {
userMessage: SBChatMessage;
@ -28,34 +21,56 @@ interface ChatThreadListItemProps {
isStreaming: boolean;
sources: Source[];
chatId: string;
index: number;
}
export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemProps>(({
userMessage,
assistantMessage,
assistantMessage: _assistantMessage,
isStreaming,
sources,
chatId,
index,
}, ref) => {
const leftPanelRef = useRef<HTMLDivElement>(null);
const [leftPanelHeight, setLeftPanelHeight] = useState<number | null>(null);
const markdownRendererRef = useRef<HTMLDivElement>(null);
const answerRef = useRef<HTMLDivElement>(null);
const [hoveredReference, setHoveredReference] = useState<Reference | undefined>(undefined);
const [selectedReference, setSelectedReference] = useState<Reference | undefined>(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<HTMLDivElement, ChatThreadListItemP
return getAnswerPartFromAssistantMessage(assistantMessage, isStreaming);
}, [assistantMessage, isStreaming]);
const references = useExtractReferences(answerPart);
const thinkingSteps = useMemo(() => {
// 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<HTMLDivElement, ChatThreadListItemP
};
}, [leftPanelHeight]);
// Handles mouse over and click events on reference elements, syncing these events
// with the `hoveredReference` and `selectedReference` state.
useEffect(() => {
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<HTMLDivElement, ChatThreadListItemP
};
}, [answerPart, selectedReference?.id]); // Re-run when answerPart changes to ensure we catch new content
// When the selected reference changes, highlight all associated reference elements
// and scroll to the nearest one, if needed.
useEffect(() => {
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<HTMLDivElement, ChatThreadListItemP
</div>
)}
<Card className="mb-4">
<Collapsible open={isDetailsPanelExpanded} onOpenChange={onExpandDetailsPanel}>
<CollapsibleTrigger asChild>
<CardContent
className={cn("p-3 cursor-pointer hover:bg-muted", {
"rounded-lg": !isDetailsPanelExpanded,
"rounded-t-lg": isDetailsPanelExpanded,
})}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-4">
<DetailsCard
isExpanded={isDetailsPanelExpanded}
onExpandedChanged={onExpandDetailsPanel}
isThinking={isThinking}
isStreaming={isStreaming}
thinkingSteps={uiVisibleThinkingSteps}
metadata={assistantMessage?.metadata}
/>
<p className="flex items-center font-semibold text-muted-foreground text-sm">
{isThinking ? (
<>
<Loader2 className="w-4 h-4 animate-spin mr-1 flex-shrink-0" />
Thinking...
</>
) : (
<>
<InfoIcon className="w-4 h-4 mr-1 flex-shrink-0" />
Details
</>
)}
</p>
{!isStreaming && (
<>
<Separator orientation="vertical" className="h-4" />
{messageMetadata?.modelName && (
<div className="flex items-center text-xs">
<Cpu className="w-3 h-3 mr-1 flex-shrink-0" />
{messageMetadata?.modelName}
</div>
)}
{messageMetadata?.totalTokens && (
<div className="flex items-center text-xs">
<Zap className="w-3 h-3 mr-1 flex-shrink-0" />
{messageMetadata?.totalTokens} tokens
</div>
)}
{messageMetadata?.totalResponseTimeMs && (
<div className="flex items-center text-xs">
<Clock className="w-3 h-3 mr-1 flex-shrink-0" />
{messageMetadata?.totalResponseTimeMs / 1000} seconds
</div>
)}
<div className="flex items-center text-xs">
<Brain className="w-3 h-3 mr-1 flex-shrink-0" />
{`${thinkingSteps.length} step${thinkingSteps.length === 1 ? '' : 's'}`}
</div>
</>
)}
</div>
{isDetailsPanelExpanded ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronRight className="w-4 h-4 text-muted-foreground" />
)}
</div>
</CardContent>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="mt-2 space-y-6">
{thinkingSteps.length === 0 ? (
isStreaming ? (
<Skeleton className="h-24 w-full" />
) : (
<p className="text-sm text-muted-foreground">No thinking steps</p>
)
) : thinkingSteps.map((step, index) => {
return (
<div
key={index}
className="border-l-2 pl-4 relative border-muted"
>
<div
className={`absolute left-[-9px] top-1 w-4 h-4 rounded-full flex items-center justify-center bg-muted`}
>
<span
className={`text-xs font-semibold`}
>
{index + 1}
</span>
</div>
{step.map((part, index) => {
switch (part.type) {
case 'reasoning':
case 'text':
return (
<MarkdownRenderer
key={index}
content={part.text}
className="text-sm"
/>
)
case 'tool-readFiles':
return (
<ReadFilesToolComponent
key={index}
part={part}
/>
)
case 'tool-searchCode':
return (
<SearchCodeToolComponent
key={index}
part={part}
/>
)
case 'tool-findSymbolDefinitions':
return (
<FindSymbolDefinitionsToolComponent
key={index}
part={part}
/>
)
case 'tool-findSymbolReferences':
return (
<FindSymbolReferencesToolComponent
key={index}
part={part}
/>
)
default:
return null;
}
})}
</div>
)
})}
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
{/* Answer section */}
{(answerPart && assistantMessage) ? (
<AnswerCard
ref={markdownRendererRef}
answerText={answerPart.text.replace(ANSWER_TAG, '').trim()}
ref={answerRef}
answerText={answerPart.text}
chatId={chatId}
messageId={assistantMessage.id}
traceId={messageMetadata?.traceId}
traceId={assistantMessage.metadata?.traceId}
/>
) : !isStreaming && (
<p className="text-destructive">Error: No answer response was provided</p>
@ -432,6 +356,7 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
>
{references.length > 0 ? (
<ReferencedSourcesListView
index={index}
references={references}
sources={sources}
hoveredReference={hoveredReference}
@ -459,3 +384,20 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
});
ChatThreadListItem.displayName = 'ChatThreadListItem';
// Finds the nearest reference element to the viewport center.
const getNearestReferenceElement = (referenceElements: Element[]) => {
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;
});
}

View file

@ -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'
}
];

View file

@ -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);
};

View file

@ -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 (
<Card className="mb-4">
<Collapsible open={isExpanded} onOpenChange={onExpandedChanged}>
<CollapsibleTrigger asChild>
<CardContent
className={cn("p-3 cursor-pointer hover:bg-muted", {
"rounded-lg": !isExpanded,
"rounded-t-lg": isExpanded,
})}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-4">
<p className="flex items-center font-semibold text-muted-foreground text-sm">
{isThinking ? (
<>
<Loader2 className="w-4 h-4 animate-spin mr-1 flex-shrink-0" />
Thinking...
</>
) : (
<>
<InfoIcon className="w-4 h-4 mr-1 flex-shrink-0" />
Details
</>
)}
</p>
{!isStreaming && (
<>
<Separator orientation="vertical" className="h-4" />
{metadata?.modelName && (
<div className="flex items-center text-xs">
<Cpu className="w-3 h-3 mr-1 flex-shrink-0" />
{metadata?.modelName}
</div>
)}
{metadata?.totalTokens && (
<div className="flex items-center text-xs">
<Zap className="w-3 h-3 mr-1 flex-shrink-0" />
{metadata?.totalTokens} tokens
</div>
)}
{metadata?.totalResponseTimeMs && (
<div className="flex items-center text-xs">
<Clock className="w-3 h-3 mr-1 flex-shrink-0" />
{metadata?.totalResponseTimeMs / 1000} seconds
</div>
)}
<div className="flex items-center text-xs">
<Brain className="w-3 h-3 mr-1 flex-shrink-0" />
{`${thinkingSteps.length} step${thinkingSteps.length === 1 ? '' : 's'}`}
</div>
</>
)}
</div>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronRight className="w-4 h-4 text-muted-foreground" />
)}
</div>
</CardContent>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="mt-2 space-y-6">
{thinkingSteps.length === 0 ? (
isStreaming ? (
<Skeleton className="h-24 w-full" />
) : (
<p className="text-sm text-muted-foreground">No thinking steps</p>
)
) : thinkingSteps.map((step, index) => {
return (
<div
key={index}
className="border-l-2 pl-4 relative border-muted"
>
<div
className={`absolute left-[-9px] top-1 w-4 h-4 rounded-full flex items-center justify-center bg-muted`}
>
<span
className={`text-xs font-semibold`}
>
{index + 1}
</span>
</div>
{step.map((part, index) => {
switch (part.type) {
case 'reasoning':
case 'text':
return (
<MarkdownRenderer
key={index}
content={part.text}
className="text-sm"
/>
)
case 'tool-readFiles':
return (
<ReadFilesToolComponent
key={index}
part={part}
/>
)
case 'tool-searchCode':
return (
<SearchCodeToolComponent
key={index}
part={part}
/>
)
case 'tool-findSymbolDefinitions':
return (
<FindSymbolDefinitionsToolComponent
key={index}
part={part}
/>
)
case 'tool-findSymbolReferences':
return (
<FindSymbolReferencesToolComponent
key={index}
part={part}
/>
)
default:
return null;
}
})}
</div>
)
})}
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
)
}

View file

@ -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: `<span
role="button"
id="${fileReference.id}"
class="${fileReference.id}"
className="font-mono cursor-pointer text-xs border px-1 py-[1.5px] rounded-md transition-all duration-150 bg-chat-reference"
title="Click to navigate to code"
${REFERENCE_PAYLOAD_ATTRIBUTE}="${encodeURIComponent(JSON.stringify(fileReference))}"

View file

@ -0,0 +1,359 @@
'use client';
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { PathHeader } from "@/app/[domain]/components/pathHeader";
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 { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { cn } from "@/lib/utils";
import { Range } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror';
import { ChevronDown, ChevronRight } from "lucide-react";
import { forwardRef, Ref, useCallback, useImperativeHandle, useMemo, useState } from "react";
import { FileReference } from "../../types";
import { createCodeFoldingExtension } from "./codeFoldingExtension";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { createAuditAction } from "@/ee/features/audit/actions";
import { useDomain } from "@/hooks/useDomain";
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" },
});
interface ReferencedFileSourceListItemProps {
id: string;
code: string;
language: string;
revision: string;
repoName: string;
repoCodeHostType: string;
repoDisplayName?: string;
repoWebUrl?: string;
fileName: string;
references: FileReference[];
selectedReference?: FileReference;
hoveredReference?: FileReference;
onSelectedReferenceChanged: (reference?: FileReference) => 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<ReactCodeMirrorRef>) => {
const theme = useCodeMirrorTheme();
const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(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<DecorationSet>({
create(state) {
const decorations: Range<Decoration>[] = [];
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 (
<div className="relative" id={id}>
{/* Sentinel element to scroll to when collapsing a file */}
<div id={`${id}-start`} />
{/* Sticky header outside the bordered container */}
<div className={cn("sticky top-0 z-10 flex flex-row items-center bg-accent py-1 px-3 gap-1.5 border-l border-r border-t rounded-t-md", {
'rounded-b-md border-b': !isExpanded,
})}>
<ExpandCollapseIcon className={`h-3 w-3 cursor-pointer mt-0.5`} onClick={() => onExpandedChanged(!isExpanded)} />
<PathHeader
path={fileName}
repo={{
name: repoName,
codeHostType: repoCodeHostType,
displayName: repoDisplayName,
webUrl: repoWebUrl,
}}
branchDisplayName={revision === 'HEAD' ? undefined : revision}
repoNameClassName="font-normal text-muted-foreground text-sm"
/>
</div>
{/* Code container */}
{/* @note: don't conditionally render here since we want to maintain state */}
<div className="border-l border-r border-b rounded-b-md overflow-hidden" style={{
height: isExpanded ? 'auto' : '0px',
visibility: isExpanded ? 'visible' : 'hidden',
}}>
<CodeMirror
ref={setEditorRef}
value={code}
extensions={extensions}
readOnly={true}
theme={theme}
basicSetup={{
highlightActiveLine: false,
highlightActiveLineGutter: false,
foldGutter: false,
foldKeymap: false,
}}
>
{editorRef && hasCodeNavEntitlement && (
<SymbolHoverPopup
editorRef={editorRef}
revisionName={revision}
language={language}
onFindReferences={onFindReferences}
onGotoDefinition={onGotoDefinition}
/>
)}
</CodeMirror>
</div>
</div>
)
}
export default forwardRef(ReferencedFileSourceListItem) as (
props: ReferencedFileSourceListItemProps & { ref?: Ref<ReactCodeMirrorRef> },
) => ReturnType<typeof ReferencedFileSourceListItem>;

View file

@ -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;
@ -42,25 +31,10 @@ const resolveFileReference = (reference: FileReference, sources: FileSource[]):
);
}
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<HTMLDivElement>(null);
const editorRefsMap = useRef<Map<string, ReactCodeMirrorRef>>(new Map());
const domain = useDomain();
const [collapsedFileIds, setCollapsedFileIds] = useState<string[]>([]);
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,11 +159,18 @@ 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
// @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',
});
});
}
// Otherwise, fallback to scrolling to the top of the file.
@ -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 (
<CodeMirrorCodeBlockWithRef
<ReferencedFileSourceListItem
key={fileId}
id={fileId}
code={fileData.source}
@ -252,7 +241,8 @@ export const ReferencedSourcesListView = ({
revision={fileSource.revision}
repoName={fileSource.repo}
repoCodeHostType={fileData.repositoryCodeHostType}
repositoryDisplayName={fileData.repositoryDisplayName}
repoDisplayName={fileData.repositoryDisplayName}
repoWebUrl={fileData.repositoryWebUrl}
fileName={fileData.path}
references={referencesInFile}
ref={(ref) => setEditorRef(fileId, ref)}
@ -260,10 +250,18 @@ export const ReferencedSourcesListView = ({
onHoveredReferenceChanged={onHoveredReferenceChanged}
selectedReference={selectedReference}
hoveredReference={hoveredReference}
isExpanded={!collapsedFileIds.includes(fileId)}
onExpandedChanged={(isExpanded) => {
if (isExpanded) {
setCollapsedFileIds(collapsedFileIds.filter((id) => id !== fileId));
} else {
setCollapsedFileIds([...collapsedFileIds, fileId]);
}
// 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={() => {
if (!isExpanded) {
const fileSourceStart = document.getElementById(`${fileId}-start`);
if (!fileSourceStart) {
return;
@ -274,7 +272,9 @@ export const ReferencedSourcesListView = ({
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<ReactCodeMirrorRef>) => {
const theme = useCodeMirrorTheme();
const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(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<DecorationSet>({
create(state) {
const decorations: Range<Decoration>[] = [];
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 (
<div className="relative" id={id}>
{/* Sentinel element to scroll to when collapsing a file */}
<div id={`${id}-start`} />
{/* Sticky header outside the bordered container */}
<div className={cn("sticky top-0 z-10 flex flex-row items-center bg-accent py-1 px-3 gap-1.5 border-l border-r border-t rounded-t-md", {
'rounded-b-md border-b': !isExpanded,
})}>
<ExpandCollapseIcon className={`h-3 w-3 cursor-pointer mt-0.5`} onClick={() => setIsExpanded(!isExpanded)} />
<PathHeader
path={fileName}
repo={{
name: repoName,
codeHostType: repoCodeHostType,
displayName: repositoryDisplayName,
}}
branchDisplayName={revision === 'HEAD' ? undefined : revision}
repoNameClassName="font-normal text-muted-foreground text-sm"
/>
</div>
{/* Code container */}
{/* @note: don't conditionally render here since we want to maintain state */}
<div className="border-l border-r border-b rounded-b-md overflow-hidden" style={{
height: isExpanded ? 'auto' : '0px',
visibility: isExpanded ? 'visible' : 'hidden',
}}>
<CodeMirror
ref={setEditorRef}
value={code}
extensions={extensions}
readOnly={true}
theme={theme}
basicSetup={{
highlightActiveLine: false,
highlightActiveLineGutter: false,
foldGutter: false,
foldKeymap: false,
}}
>
{editorRef && hasCodeNavEntitlement && (
<SymbolHoverPopup
editorRef={editorRef}
revisionName={revision}
language={language}
onFindReferences={onFindReferences}
onGotoDefinition={onGotoDefinition}
/>
)}
</CodeMirror>
</div>
</div>
)
}
export const CodeMirrorCodeBlockWithRef = forwardRef(CodeMirrorCodeBlock) as (
props: CodeMirrorCodeBlockProps & { ref?: Ref<ReactCodeMirrorRef> },
) => ReturnType<typeof CodeMirrorCodeBlock>;

View file

@ -1,3 +1,4 @@
import { SBChatMessagePart } from "./types";
export const FILE_REFERENCE_PREFIX = '@file:';
export const FILE_REFERENCE_REGEX = new RegExp(
@ -14,3 +15,13 @@ export const toolNames = {
findSymbolReferences: 'findSymbolReferences',
findSymbolDefinitions: 'findSymbolDefinitions',
} 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;

View file

@ -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({

View file

@ -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: [
{
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,
}
});
});

View file

@ -1,18 +1,19 @@
'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;
@ -29,11 +30,7 @@ export const useExtractReferences = (message?: SBChatMessage) => {
references.push(fileReference);
}
break;
}
}
});
return references;
}, [message]);
}, [part]);
};

View file

@ -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);
});

View file

@ -239,10 +239,12 @@ 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) => {
return text
.replace(ANSWER_TAG, '')
.replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => {
const displayName = fileName.split('/').pop() || fileName;
let linkText = displayName;
@ -255,7 +257,8 @@ export const convertLLMOutputToPortableMarkdown = (text: string): string => {
}
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;

View file

@ -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;

View file

@ -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(),
});

View file

@ -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: {},
//////////////////////////////////////////////////////////////////

174
yarn.lock
View file

@ -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