mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
fix(ask_sb): Various improvements to the references system (#396)
This commit is contained in:
parent
efc9656b6e
commit
41addb50a7
21 changed files with 961 additions and 850 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ export const ChatThread = ({
|
|||
return (
|
||||
<Fragment key={index}>
|
||||
<ChatThreadListItem
|
||||
index={index}
|
||||
chatId={chatId}
|
||||
userMessage={userMessage}
|
||||
assistantMessage={assistantMessage}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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))}"
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
@ -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<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,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 (
|
||||
<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,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<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>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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
174
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue