mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +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"
|
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "3.0.0-beta.9",
|
"@ai-sdk/amazon-bedrock": "3.0.0-beta.10",
|
||||||
"@ai-sdk/anthropic": "2.0.0-beta.8",
|
"@ai-sdk/anthropic": "2.0.0-beta.9",
|
||||||
"@ai-sdk/azure": "2.0.0-beta.11",
|
"@ai-sdk/azure": "2.0.0-beta.12",
|
||||||
"@ai-sdk/deepseek": "1.0.0-beta.8",
|
"@ai-sdk/deepseek": "1.0.0-beta.9",
|
||||||
"@ai-sdk/google": "2.0.0-beta.14",
|
"@ai-sdk/google": "2.0.0-beta.15",
|
||||||
"@ai-sdk/google-vertex": "3.0.0-beta.16",
|
"@ai-sdk/google-vertex": "3.0.0-beta.17",
|
||||||
"@ai-sdk/mistral": "2.0.0-beta.6",
|
"@ai-sdk/mistral": "2.0.0-beta.7",
|
||||||
"@ai-sdk/openai": "2.0.0-beta.11",
|
"@ai-sdk/openai": "2.0.0-beta.12",
|
||||||
"@ai-sdk/react": "2.0.0-beta.26",
|
"@ai-sdk/react": "2.0.0-beta.28",
|
||||||
"@ai-sdk/xai": "2.0.0-beta.10",
|
"@ai-sdk/xai": "2.0.0-beta.11",
|
||||||
"@auth/prisma-adapter": "^2.7.4",
|
"@auth/prisma-adapter": "^2.7.4",
|
||||||
"@codemirror/commands": "^6.6.0",
|
"@codemirror/commands": "^6.6.0",
|
||||||
"@codemirror/lang-cpp": "^6.0.2",
|
"@codemirror/lang-cpp": "^6.0.2",
|
||||||
|
|
@ -108,7 +108,7 @@
|
||||||
"@vercel/otel": "^1.13.0",
|
"@vercel/otel": "^1.13.0",
|
||||||
"@viz-js/lang-dot": "^1.0.4",
|
"@viz-js/lang-dot": "^1.0.4",
|
||||||
"@xiechao/codemirror-lang-handlebars": "^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",
|
"ajv": "^8.17.1",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,9 @@ export const PureCodePreviewPanel = ({
|
||||||
}, [editorRef, highlightRange]);
|
}, [editorRef, highlightRange]);
|
||||||
|
|
||||||
const onFindReferences = useCallback((symbolName: string) => {
|
const onFindReferences = useCallback((symbolName: string) => {
|
||||||
captureEvent('wa_browse_find_references_pressed', {});
|
captureEvent('wa_find_references_pressed', {
|
||||||
|
source: 'browse',
|
||||||
|
});
|
||||||
createAuditAction({
|
createAuditAction({
|
||||||
action: "user.performed_find_references",
|
action: "user.performed_find_references",
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|
@ -160,7 +162,9 @@ export const PureCodePreviewPanel = ({
|
||||||
// If we resolve multiple matches, instead of navigating to the first match, we should
|
// If we resolve multiple matches, instead of navigating to the first match, we should
|
||||||
// instead popup the bottom sheet with the list of matches.
|
// instead popup the bottom sheet with the list of matches.
|
||||||
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
||||||
captureEvent('wa_browse_goto_definition_pressed', {});
|
captureEvent('wa_goto_definition_pressed', {
|
||||||
|
source: 'browse',
|
||||||
|
});
|
||||||
createAuditAction({
|
createAuditAction({
|
||||||
action: "user.performed_goto_definition",
|
action: "user.performed_goto_definition",
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,9 @@ export const CodePreview = ({
|
||||||
}, [onSelectedMatchIndexChange]);
|
}, [onSelectedMatchIndexChange]);
|
||||||
|
|
||||||
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
||||||
captureEvent('wa_preview_panel_goto_definition_pressed', {});
|
captureEvent('wa_goto_definition_pressed', {
|
||||||
|
source: 'preview',
|
||||||
|
});
|
||||||
createAuditAction({
|
createAuditAction({
|
||||||
action: "user.performed_goto_definition",
|
action: "user.performed_goto_definition",
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|
@ -163,7 +165,9 @@ export const CodePreview = ({
|
||||||
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName, domain]);
|
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName, domain]);
|
||||||
|
|
||||||
const onFindReferences = useCallback((symbolName: string) => {
|
const onFindReferences = useCallback((symbolName: string) => {
|
||||||
captureEvent('wa_preview_panel_find_references_pressed', {});
|
captureEvent('wa_find_references_pressed', {
|
||||||
|
source: 'preview',
|
||||||
|
});
|
||||||
createAuditAction({
|
createAuditAction({
|
||||||
action: "user.performed_find_references",
|
action: "user.performed_find_references",
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,7 @@ export const ChatThread = ({
|
||||||
return (
|
return (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<ChatThreadListItem
|
<ChatThreadListItem
|
||||||
|
index={index}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
userMessage={userMessage}
|
userMessage={userMessage}
|
||||||
assistantMessage={assistantMessage}
|
assistantMessage={assistantMessage}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,19 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle';
|
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 { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { cn } from '@/lib/utils';
|
import { CheckCircle, Loader2 } from 'lucide-react';
|
||||||
import { Brain, CheckCircle, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, Zap } from 'lucide-react';
|
|
||||||
import { CSSProperties, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { CSSProperties, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||||
import { ANSWER_TAG } from '../../constants';
|
import { Reference, referenceSchema, SBChatMessage, Source } from "../../types";
|
||||||
import { Reference, referenceSchema, SBChatMessage, SBChatMessageMetadata, Source } from "../../types";
|
|
||||||
import { useExtractReferences } from '../../useExtractReferences';
|
import { useExtractReferences } from '../../useExtractReferences';
|
||||||
import { getAnswerPartFromAssistantMessage, groupMessageIntoSteps } from '../../utils';
|
import { getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from '../../utils';
|
||||||
import { AnswerCard } from './answerCard';
|
import { AnswerCard } from './answerCard';
|
||||||
|
import { DetailsCard } from './detailsCard';
|
||||||
import { MarkdownRenderer, REFERENCE_PAYLOAD_ATTRIBUTE } from './markdownRenderer';
|
import { MarkdownRenderer, REFERENCE_PAYLOAD_ATTRIBUTE } from './markdownRenderer';
|
||||||
import { ReferencedSourcesListView } from './referencedSourcesListView';
|
import { ReferencedSourcesListView } from './referencedSourcesListView';
|
||||||
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
|
import { uiVisiblePartTypes } from '../../constants';
|
||||||
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
|
|
||||||
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
|
|
||||||
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
|
|
||||||
|
|
||||||
interface ChatThreadListItemProps {
|
interface ChatThreadListItemProps {
|
||||||
userMessage: SBChatMessage;
|
userMessage: SBChatMessage;
|
||||||
|
|
@ -28,34 +21,56 @@ interface ChatThreadListItemProps {
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
sources: Source[];
|
sources: Source[];
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemProps>(({
|
export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemProps>(({
|
||||||
userMessage,
|
userMessage,
|
||||||
assistantMessage,
|
assistantMessage: _assistantMessage,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
sources,
|
sources,
|
||||||
chatId,
|
chatId,
|
||||||
|
index,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const leftPanelRef = useRef<HTMLDivElement>(null);
|
const leftPanelRef = useRef<HTMLDivElement>(null);
|
||||||
const [leftPanelHeight, setLeftPanelHeight] = useState<number | null>(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 [hoveredReference, setHoveredReference] = useState<Reference | undefined>(undefined);
|
||||||
const [selectedReference, setSelectedReference] = useState<Reference | undefined>(undefined);
|
const [selectedReference, setSelectedReference] = useState<Reference | undefined>(undefined);
|
||||||
const references = useExtractReferences(assistantMessage);
|
|
||||||
const [isDetailsPanelExpanded, _setIsDetailsPanelExpanded] = useState(isStreaming);
|
const [isDetailsPanelExpanded, _setIsDetailsPanelExpanded] = useState(isStreaming);
|
||||||
const hasAutoCollapsed = useRef(false);
|
const hasAutoCollapsed = useRef(false);
|
||||||
const userHasManuallyExpanded = useRef(false);
|
const userHasManuallyExpanded = useRef(false);
|
||||||
|
|
||||||
|
|
||||||
const userQuestion = useMemo(() => {
|
const userQuestion = useMemo(() => {
|
||||||
return userMessage.parts.length > 0 && userMessage.parts[0].type === 'text' ? userMessage.parts[0].text : '';
|
return userMessage.parts.length > 0 && userMessage.parts[0].type === 'text' ? userMessage.parts[0].text : '';
|
||||||
}, [userMessage]);
|
}, [userMessage]);
|
||||||
|
|
||||||
const messageMetadata = useMemo((): SBChatMessageMetadata | undefined => {
|
// Take the assistant message and repair any references that are not properly formatted.
|
||||||
return assistantMessage?.metadata;
|
// This applies to parts that are text (i.e., text & reasoning).
|
||||||
}, [assistantMessage?.metadata]);
|
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(() => {
|
const answerPart = useMemo(() => {
|
||||||
if (!assistantMessage) {
|
if (!assistantMessage) {
|
||||||
|
|
@ -65,11 +80,33 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
|
||||||
return getAnswerPartFromAssistantMessage(assistantMessage, isStreaming);
|
return getAnswerPartFromAssistantMessage(assistantMessage, isStreaming);
|
||||||
}, [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 ?? []);
|
const steps = groupMessageIntoSteps(assistantMessage?.parts ?? []);
|
||||||
|
|
||||||
// Filter out the answerPart and empty steps
|
// 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]);
|
}, [answerPart, assistantMessage?.parts]);
|
||||||
|
|
||||||
// "thinking" is when the agent is generating output that is not the answer.
|
// "thinking" is when the agent is generating output that is not the answer.
|
||||||
|
|
@ -123,13 +160,14 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
|
||||||
};
|
};
|
||||||
}, [leftPanelHeight]);
|
}, [leftPanelHeight]);
|
||||||
|
|
||||||
|
// Handles mouse over and click events on reference elements, syncing these events
|
||||||
|
// with the `hoveredReference` and `selectedReference` state.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!markdownRendererRef.current) {
|
if (!answerRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdownRenderer = markdownRendererRef.current;
|
const markdownRenderer = answerRef.current;
|
||||||
|
|
||||||
const handleMouseOver = (event: MouseEvent) => {
|
const handleMouseOver = (event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement;
|
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
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!selectedReference) {
|
if (!selectedReference) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceElement = document.getElementById(`user-content-${selectedReference.id}`);
|
// The reference id is attached to the DOM element as a class name.
|
||||||
if (!referenceElement) {
|
// @see: markdownRenderer.tsx
|
||||||
|
const referenceElements = Array.from(answerRef.current?.getElementsByClassName(selectedReference.id) ?? []);
|
||||||
|
if (referenceElements.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollIntoView(referenceElement, {
|
const nearestReferenceElement = getNearestReferenceElement(referenceElements);
|
||||||
|
scrollIntoView(nearestReferenceElement, {
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
scrollMode: 'if-needed',
|
scrollMode: 'if-needed',
|
||||||
block: 'center',
|
block: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
referenceElement.classList.add('chat-reference--selected');
|
referenceElements.forEach(element => {
|
||||||
|
element.classList.add('chat-reference--selected');
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
referenceElement.classList.remove('chat-reference--selected');
|
referenceElements.forEach(element => {
|
||||||
|
element.classList.remove('chat-reference--selected');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, [selectedReference]);
|
}, [selectedReference]);
|
||||||
|
|
||||||
|
// When the hovered reference changes, highlight all associated reference elements.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hoveredReference) {
|
if (!hoveredReference) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceElement = document.getElementById(`user-content-${hoveredReference.id}`);
|
// The reference id is attached to the DOM element as a class name.
|
||||||
if (!referenceElement) {
|
// @see: markdownRenderer.tsx
|
||||||
|
const referenceElements = Array.from(answerRef.current?.getElementsByClassName(hoveredReference.id) ?? []);
|
||||||
|
if (referenceElements.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
referenceElement.classList.add('chat-reference--hover');
|
referenceElements.forEach(element => {
|
||||||
|
element.classList.add('chat-reference--hover');
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
referenceElement.classList.remove('chat-reference--hover');
|
referenceElements.forEach(element => {
|
||||||
|
element.classList.remove('chat-reference--hover');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, [hoveredReference]);
|
}, [hoveredReference]);
|
||||||
|
|
||||||
|
|
@ -265,151 +318,22 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="mb-4">
|
<DetailsCard
|
||||||
<Collapsible open={isDetailsPanelExpanded} onOpenChange={onExpandDetailsPanel}>
|
isExpanded={isDetailsPanelExpanded}
|
||||||
<CollapsibleTrigger asChild>
|
onExpandedChanged={onExpandDetailsPanel}
|
||||||
<CardContent
|
isThinking={isThinking}
|
||||||
className={cn("p-3 cursor-pointer hover:bg-muted", {
|
isStreaming={isStreaming}
|
||||||
"rounded-lg": !isDetailsPanelExpanded,
|
thinkingSteps={uiVisibleThinkingSteps}
|
||||||
"rounded-t-lg": isDetailsPanelExpanded,
|
metadata={assistantMessage?.metadata}
|
||||||
})}
|
/>
|
||||||
>
|
|
||||||
<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" />
|
|
||||||
{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) ? (
|
{(answerPart && assistantMessage) ? (
|
||||||
<AnswerCard
|
<AnswerCard
|
||||||
ref={markdownRendererRef}
|
ref={answerRef}
|
||||||
answerText={answerPart.text.replace(ANSWER_TAG, '').trim()}
|
answerText={answerPart.text}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
messageId={assistantMessage.id}
|
messageId={assistantMessage.id}
|
||||||
traceId={messageMetadata?.traceId}
|
traceId={assistantMessage.metadata?.traceId}
|
||||||
/>
|
/>
|
||||||
) : !isStreaming && (
|
) : !isStreaming && (
|
||||||
<p className="text-destructive">Error: No answer response was provided</p>
|
<p className="text-destructive">Error: No answer response was provided</p>
|
||||||
|
|
@ -432,6 +356,7 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
|
||||||
>
|
>
|
||||||
{references.length > 0 ? (
|
{references.length > 0 ? (
|
||||||
<ReferencedSourcesListView
|
<ReferencedSourcesListView
|
||||||
|
index={index}
|
||||||
references={references}
|
references={references}
|
||||||
sources={sources}
|
sources={sources}
|
||||||
hoveredReference={hoveredReference}
|
hoveredReference={hoveredReference}
|
||||||
|
|
@ -459,3 +384,20 @@ export const ChatThreadListItem = forwardRef<HTMLDivElement, ChatThreadListItemP
|
||||||
});
|
});
|
||||||
|
|
||||||
ChatThreadListItem.displayName = 'ChatThreadListItem';
|
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',
|
path: 'test.ts',
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'file',
|
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));
|
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);
|
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 {
|
return {
|
||||||
type: 'html',
|
type: 'html',
|
||||||
// @note: if you add additional attributes to this span, make sure to update the rehypeSanitize plugin to allow them.
|
// @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
|
value: `<span
|
||||||
role="button"
|
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"
|
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"
|
title="Click to navigate to code"
|
||||||
${REFERENCE_PAYLOAD_ATTRIBUTE}="${encodeURIComponent(JSON.stringify(fileReference))}"
|
${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';
|
'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 { fetchFileSource } from "@/app/api/(client)/client";
|
||||||
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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 { useDomain } from "@/hooks/useDomain";
|
||||||
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
import { isServiceError, unwrapServiceError } from "@/lib/utils";
|
||||||
import { cn, isServiceError, unwrapServiceError } from "@/lib/utils";
|
|
||||||
import { Range } from "@codemirror/state";
|
|
||||||
import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
|
|
||||||
import { useQueries } from "@tanstack/react-query";
|
import { useQueries } from "@tanstack/react-query";
|
||||||
import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror';
|
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
||||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||||
import { FileReference, FileSource, Reference, Source } from "../../types";
|
import { FileReference, FileSource, Reference, Source } from "../../types";
|
||||||
import { createCodeFoldingExtension } from "./codeFoldingExtension";
|
import ReferencedFileSourceListItem from "./referencedFileSourceListItem";
|
||||||
|
|
||||||
interface ReferencedSourcesListViewProps {
|
interface ReferencedSourcesListViewProps {
|
||||||
references: FileReference[];
|
references: FileReference[];
|
||||||
sources: Source[];
|
sources: Source[];
|
||||||
|
index: number;
|
||||||
hoveredReference?: Reference;
|
hoveredReference?: Reference;
|
||||||
onHoveredReferenceChanged: (reference?: Reference) => void;
|
onHoveredReferenceChanged: (reference?: Reference) => void;
|
||||||
selectedReference?: Reference;
|
selectedReference?: Reference;
|
||||||
|
|
@ -38,29 +27,14 @@ interface ReferencedSourcesListViewProps {
|
||||||
const resolveFileReference = (reference: FileReference, sources: FileSource[]): FileSource | undefined => {
|
const resolveFileReference = (reference: FileReference, sources: FileSource[]): FileSource | undefined => {
|
||||||
return sources.find(
|
return sources.find(
|
||||||
(source) => source.repo.endsWith(reference.repo) &&
|
(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 = ({
|
export const ReferencedSourcesListView = ({
|
||||||
references,
|
references,
|
||||||
sources,
|
sources,
|
||||||
|
index,
|
||||||
hoveredReference,
|
hoveredReference,
|
||||||
selectedReference,
|
selectedReference,
|
||||||
style,
|
style,
|
||||||
|
|
@ -70,6 +44,14 @@ export const ReferencedSourcesListView = ({
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
const editorRefsMap = useRef<Map<string, ReactCodeMirrorRef>>(new Map());
|
const editorRefsMap = useRef<Map<string, ReactCodeMirrorRef>>(new Map());
|
||||||
const domain = useDomain();
|
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) => {
|
const setEditorRef = useCallback((fileKey: string, ref: ReactCodeMirrorRef | null) => {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
|
|
@ -112,7 +94,7 @@ export const ReferencedSourcesListView = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupedReferences;
|
return groupedReferences;
|
||||||
}, [references, referencedFileSources]);
|
}, [references, referencedFileSources, getFileId]);
|
||||||
|
|
||||||
const fileSourceQueries = useQueries({
|
const fileSourceQueries = useQueries({
|
||||||
queries: referencedFileSources.map((file) => ({
|
queries: referencedFileSources.map((file) => ({
|
||||||
|
|
@ -177,10 +159,17 @@ export const ReferencedSourcesListView = ({
|
||||||
// Calculate the target scroll position to center the line
|
// Calculate the target scroll position to center the line
|
||||||
const targetScrollTop = lineTopRelativeToScrollArea - (scrollAreaHeight / 3);
|
const targetScrollTop = lineTopRelativeToScrollArea - (scrollAreaHeight / 3);
|
||||||
|
|
||||||
|
// Expand the file if it's collapsed.
|
||||||
|
setCollapsedFileIds((collapsedFileIds) => collapsedFileIds.filter((id) => id !== fileId));
|
||||||
|
|
||||||
// Scroll to the calculated position
|
// Scroll to the calculated position
|
||||||
scrollAreaViewport.scrollTo({
|
// @NOTE: Using requestAnimationFrame is a bit of a hack to ensure
|
||||||
top: Math.max(0, targetScrollTop),
|
// that the collapsed file ids state has updated before scrolling.
|
||||||
behavior: 'smooth',
|
requestAnimationFrame(() => {
|
||||||
|
scrollAreaViewport.scrollTo({
|
||||||
|
top: Math.max(0, targetScrollTop),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +181,7 @@ export const ReferencedSourcesListView = ({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [referencedFileSources, selectedReference]);
|
}, [getFileId, referencedFileSources, selectedReference]);
|
||||||
|
|
||||||
if (referencedFileSources.length === 0) {
|
if (referencedFileSources.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -244,7 +233,7 @@ export const ReferencedSourcesListView = ({
|
||||||
const referencesInFile = referencesGroupedByFile.get(fileId) || [];
|
const referencesInFile = referencesGroupedByFile.get(fileId) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeMirrorCodeBlockWithRef
|
<ReferencedFileSourceListItem
|
||||||
key={fileId}
|
key={fileId}
|
||||||
id={fileId}
|
id={fileId}
|
||||||
code={fileData.source}
|
code={fileData.source}
|
||||||
|
|
@ -252,7 +241,8 @@ export const ReferencedSourcesListView = ({
|
||||||
revision={fileSource.revision}
|
revision={fileSource.revision}
|
||||||
repoName={fileSource.repo}
|
repoName={fileSource.repo}
|
||||||
repoCodeHostType={fileData.repositoryCodeHostType}
|
repoCodeHostType={fileData.repositoryCodeHostType}
|
||||||
repositoryDisplayName={fileData.repositoryDisplayName}
|
repoDisplayName={fileData.repositoryDisplayName}
|
||||||
|
repoWebUrl={fileData.repositoryWebUrl}
|
||||||
fileName={fileData.path}
|
fileName={fileData.path}
|
||||||
references={referencesInFile}
|
references={referencesInFile}
|
||||||
ref={(ref) => setEditorRef(fileId, ref)}
|
ref={(ref) => setEditorRef(fileId, ref)}
|
||||||
|
|
@ -260,21 +250,31 @@ export const ReferencedSourcesListView = ({
|
||||||
onHoveredReferenceChanged={onHoveredReferenceChanged}
|
onHoveredReferenceChanged={onHoveredReferenceChanged}
|
||||||
selectedReference={selectedReference}
|
selectedReference={selectedReference}
|
||||||
hoveredReference={hoveredReference}
|
hoveredReference={hoveredReference}
|
||||||
// When collapsing a file when you are deep in a scroll, it's a better
|
isExpanded={!collapsedFileIds.includes(fileId)}
|
||||||
// experience to have the scroll automatically restored to the top of the file
|
onExpandedChanged={(isExpanded) => {
|
||||||
// s.t., header is still sticky to the top of the scroll area.
|
if (isExpanded) {
|
||||||
onCollapse={() => {
|
setCollapsedFileIds(collapsedFileIds.filter((id) => id !== fileId));
|
||||||
const fileSourceStart = document.getElementById(`${fileId}-start`);
|
} else {
|
||||||
if (!fileSourceStart) {
|
setCollapsedFileIds([...collapsedFileIds, fileId]);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollIntoView(fileSourceStart, {
|
// When collapsing a file when you are deep in a scroll, it's a better
|
||||||
scrollMode: 'if-needed',
|
// experience to have the scroll automatically restored to the top of the file
|
||||||
block: 'start',
|
// s.t., header is still sticky to the top of the scroll area.
|
||||||
behavior: 'instant',
|
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_PREFIX = '@file:';
|
||||||
export const FILE_REFERENCE_REGEX = new RegExp(
|
export const FILE_REFERENCE_REGEX = new RegExp(
|
||||||
|
|
@ -14,3 +15,13 @@ export const toolNames = {
|
||||||
findSymbolReferences: 'findSymbolReferences',
|
findSymbolReferences: 'findSymbolReferences',
|
||||||
findSymbolDefinitions: 'findSymbolDefinitions',
|
findSymbolDefinitions: 'findSymbolDefinitions',
|
||||||
} as const;
|
} 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 { addLineNumbers } from "./utils";
|
||||||
import { toolNames } from "./constants";
|
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({
|
export const findSymbolReferencesTool = tool({
|
||||||
description: `Finds references to a symbol in the codebase.`,
|
description: `Finds references to a symbol in the codebase.`,
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,16 @@
|
||||||
import { expect, test } from 'vitest'
|
import { expect, test } from 'vitest'
|
||||||
import { SBChatMessage } from './types';
|
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
import { useExtractReferences } from './useExtractReferences';
|
import { useExtractReferences } from './useExtractReferences';
|
||||||
import { getFileReferenceId } from './utils';
|
import { getFileReferenceId } from './utils';
|
||||||
|
import { TextUIPart } from 'ai';
|
||||||
|
|
||||||
test('useExtractReferences extracts file references from text content', () => {
|
test('useExtractReferences extracts file references from text content', () => {
|
||||||
const message: SBChatMessage = {
|
const part: TextUIPart = {
|
||||||
id: 'msg1',
|
type: 'text',
|
||||||
role: 'assistant',
|
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}.'
|
||||||
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 { result } = renderHook(() => useExtractReferences(message));
|
const { result } = renderHook(() => useExtractReferences(part));
|
||||||
|
|
||||||
expect(result.current).toHaveLength(2);
|
expect(result.current).toHaveLength(2);
|
||||||
expect(result.current[0]).toMatchObject({
|
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';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { SBChatMessage, FileReference } from "./types";
|
import { FileReference } from "./types";
|
||||||
import { FILE_REFERENCE_REGEX } from "./constants";
|
import { FILE_REFERENCE_REGEX } from "./constants";
|
||||||
import { createFileReference } from "./utils";
|
import { createFileReference } from "./utils";
|
||||||
|
import { TextUIPart } from "ai";
|
||||||
|
|
||||||
export const useExtractReferences = (message?: SBChatMessage) => {
|
export const useExtractReferences = (part?: TextUIPart) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
if (!part) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const references: FileReference[] = [];
|
const references: FileReference[] = [];
|
||||||
|
|
||||||
message?.parts.forEach((part) => {
|
const content = part.text;
|
||||||
switch (part.type) {
|
FILE_REFERENCE_REGEX.lastIndex = 0;
|
||||||
case 'text':
|
|
||||||
case 'reasoning': {
|
|
||||||
const content = part.text;
|
|
||||||
FILE_REFERENCE_REGEX.lastIndex = 0;
|
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
while ((match = FILE_REFERENCE_REGEX.exec(content ?? '')) !== null && match !== null) {
|
while ((match = FILE_REFERENCE_REGEX.exec(content ?? '')) !== null && match !== null) {
|
||||||
const [_, repo, fileName, startLine, endLine] = match;
|
const [_, repo, fileName, startLine, endLine] = match;
|
||||||
|
|
||||||
const fileReference = createFileReference({
|
const fileReference = createFileReference({
|
||||||
repo: repo,
|
repo: repo,
|
||||||
path: fileName,
|
path: fileName,
|
||||||
startLine,
|
startLine,
|
||||||
endLine,
|
endLine,
|
||||||
});
|
});
|
||||||
|
|
||||||
references.push(fileReference);
|
references.push(fileReference);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return references;
|
return references;
|
||||||
}, [message]);
|
}, [part]);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect, test, vi } from 'vitest'
|
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 { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants';
|
||||||
import { SBChatMessage, SBChatMessagePart } from './types';
|
import { SBChatMessage, SBChatMessagePart } from './types';
|
||||||
|
|
||||||
|
|
@ -243,87 +243,111 @@ test('getAnswerPartFromAssistantMessage returns undefined when streaming and no
|
||||||
expect(result).toBeUndefined();
|
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 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.';
|
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 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.';
|
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 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.';
|
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 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.';
|
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 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.';
|
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 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.';
|
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 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.';
|
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.';
|
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 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.';
|
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', () => {
|
test('repairReferences returns empty string unchanged', () => {
|
||||||
expect(repairCitations('')).toBe('');
|
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.';
|
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 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.';
|
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 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.';
|
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 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}.';
|
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.';
|
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
|
* Converts LLM text that includes references (e.g., @file:...) into a portable
|
||||||
* Markdown format. Practically, this means converting references into Markdown
|
* Markdown format. Practically, this means converting references into Markdown
|
||||||
* links.
|
* links and removing the answer tag.
|
||||||
*/
|
*/
|
||||||
export const convertLLMOutputToPortableMarkdown = (text: string): string => {
|
export const convertLLMOutputToPortableMarkdown = (text: string): string => {
|
||||||
return text.replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => {
|
return text
|
||||||
const displayName = fileName.split('/').pop() || fileName;
|
.replace(ANSWER_TAG, '')
|
||||||
|
.replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => {
|
||||||
|
const displayName = fileName.split('/').pop() || fileName;
|
||||||
|
|
||||||
let linkText = displayName;
|
let linkText = displayName;
|
||||||
if (startLine) {
|
if (startLine) {
|
||||||
if (endLine && startLine !== endLine) {
|
if (endLine && startLine !== endLine) {
|
||||||
linkText += `:${startLine}-${endLine}`;
|
linkText += `:${startLine}-${endLine}`;
|
||||||
} else {
|
} else {
|
||||||
linkText += `:${startLine}`;
|
linkText += `:${startLine}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return `[${linkText}](${fileName})`;
|
return `[${linkText}](${fileName})`;
|
||||||
});
|
})
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups message parts into groups based on step-start delimiters.
|
// 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.
|
// 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
|
return text
|
||||||
// Fix missing colon: @file{...} -> @file:{...}
|
// Fix missing colon: @file{...} -> @file:{...}
|
||||||
.replace(/@file\{([^}]+)\}/g, '@file:{$1}')
|
.replace(/@file\{([^}]+)\}/g, '@file:{$1}')
|
||||||
|
|
@ -297,7 +300,15 @@ export const repairCitations = (text: string): string => {
|
||||||
// Fix multiple ranges: keep only first range
|
// Fix multiple ranges: keep only first range
|
||||||
.replace(/@file:\{(.+?):(\d+-\d+),[\d,-]+\}/g, '@file:{$1:$2}')
|
.replace(/@file:\{(.+?):(\d+-\d+),[\d,-]+\}/g, '@file:{$1:$2}')
|
||||||
// Fix malformed ranges
|
// 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
|
// 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')
|
.findLast((part) => part.type === 'text')
|
||||||
|
|
||||||
if (lastTextPart?.text.startsWith(ANSWER_TAG)) {
|
if (lastTextPart?.text.startsWith(ANSWER_TAG)) {
|
||||||
return {
|
return lastTextPart;
|
||||||
...lastTextPart,
|
|
||||||
text: repairCitations(lastTextPart.text),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the agent did not include the answer tag, then fallback to using the last text part.
|
// 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.
|
// Only do this when we are no longer streaming since the agent may still be thinking.
|
||||||
if (!isStreaming && lastTextPart) {
|
if (!isStreaming && lastTextPart) {
|
||||||
return {
|
return lastTextPart;
|
||||||
...lastTextPart,
|
|
||||||
text: repairCitations(lastTextPart.text),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export const getFileSource = async ({ fileName, repository, branch }: FileSource
|
||||||
repository,
|
repository,
|
||||||
repositoryCodeHostType: repoInfo.codeHostType,
|
repositoryCodeHostType: repoInfo.codeHostType,
|
||||||
repositoryDisplayName: repoInfo.displayName,
|
repositoryDisplayName: repoInfo.displayName,
|
||||||
|
repositoryWebUrl: repoInfo.webUrl,
|
||||||
branch,
|
branch,
|
||||||
webUrl: file.webUrl,
|
webUrl: file.webUrl,
|
||||||
} satisfies FileSourceResponse;
|
} satisfies FileSourceResponse;
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ export const fileSourceResponseSchema = z.object({
|
||||||
repository: z.string(),
|
repository: z.string(),
|
||||||
repositoryCodeHostType: z.string(),
|
repositoryCodeHostType: z.string(),
|
||||||
repositoryDisplayName: z.string().optional(),
|
repositoryDisplayName: z.string().optional(),
|
||||||
|
repositoryWebUrl: z.string().optional(),
|
||||||
branch: z.string().optional(),
|
branch: z.string().optional(),
|
||||||
webUrl: z.string().optional(),
|
webUrl: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
@ -268,11 +268,12 @@ export type PosthogEventMap = {
|
||||||
wa_api_key_created: {},
|
wa_api_key_created: {},
|
||||||
wa_api_key_creation_fail: {},
|
wa_api_key_creation_fail: {},
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
wa_preview_panel_find_references_pressed: {},
|
wa_goto_definition_pressed: {
|
||||||
wa_preview_panel_goto_definition_pressed: {},
|
source: 'chat' | 'browse' | 'preview',
|
||||||
//////////////////////////////////////////////////////////////////
|
},
|
||||||
wa_browse_find_references_pressed: {},
|
wa_find_references_pressed: {
|
||||||
wa_browse_goto_definition_pressed: {},
|
source: 'chat' | 'browse' | 'preview',
|
||||||
|
},
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
wa_explore_menu_reference_clicked: {},
|
wa_explore_menu_reference_clicked: {},
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
|
|
|
||||||
174
yarn.lock
174
yarn.lock
|
|
@ -5,137 +5,137 @@ __metadata:
|
||||||
version: 8
|
version: 8
|
||||||
cacheKey: 10c0
|
cacheKey: 10c0
|
||||||
|
|
||||||
"@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.9
|
version: 3.0.0-beta.10
|
||||||
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.0-beta.9"
|
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.0-beta.10"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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/eventstream-codec": "npm:^4.0.1"
|
||||||
"@smithy/util-utf8": "npm:^4.0.0"
|
"@smithy/util-utf8": "npm:^4.0.0"
|
||||||
aws4fetch: "npm:^1.0.20"
|
aws4fetch: "npm:^1.0.20"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/b7033125f7d00eaefeeda3316bf0dc5d4c3b79c29b5123434d315809f7127e5b6142234d1672aed4133cafbf18983a31c64ad17d8bb58b55000a9c7860cdbd19
|
checksum: 10c0/1e18b20adddee827337e15939f298c621464547819ea9c5f12746f36e6c4fd2215abc9b2ac3445de63dc58550c7b465375b0377a3a7045cee38c8b6da0ed0d72
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/anthropic@npm:2.0.0-beta.8":
|
"@ai-sdk/anthropic@npm:2.0.0-beta.9":
|
||||||
version: 2.0.0-beta.8
|
version: 2.0.0-beta.9
|
||||||
resolution: "@ai-sdk/anthropic@npm:2.0.0-beta.8"
|
resolution: "@ai-sdk/anthropic@npm:2.0.0-beta.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/60a026bf0aaff680d1397bc736e5fe051944146fceba1327aa7e92a45f20050f5c9612b8b90764314b022d9686125e5dc3a3494afe983e8864dbc06c4c6fa2ab
|
checksum: 10c0/ed7974f9ad399d206629a5bfa88964f9542cb95f820a0710b2b0af9677029e2164a5efa2e2d53cb6592a3eba6a43c8e963a7039fba9ff331ada17b98a2838f66
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/azure@npm:2.0.0-beta.11":
|
"@ai-sdk/azure@npm:2.0.0-beta.12":
|
||||||
version: 2.0.0-beta.11
|
version: 2.0.0-beta.12
|
||||||
resolution: "@ai-sdk/azure@npm:2.0.0-beta.11"
|
resolution: "@ai-sdk/azure@npm:2.0.0-beta.12"
|
||||||
dependencies:
|
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": "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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/3de3bdfde6f604d6ed7e199e15acaed4844e7b59cadc99a662dbaea8bdd509198ab734b8fe41183b39ca73f24ca886cd90b924991929e6b81e6ec039328539b1
|
checksum: 10c0/aaf5704c91a00b2f48b0e6b916c958803c5e3761fafa83e9e617a2f2ba2adbda911a0f8cd221297f17926f62d09dcf9fc0252851ec7455be45bd751dd485b19e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/deepseek@npm:1.0.0-beta.8":
|
"@ai-sdk/deepseek@npm:1.0.0-beta.9":
|
||||||
version: 1.0.0-beta.8
|
version: 1.0.0-beta.9
|
||||||
resolution: "@ai-sdk/deepseek@npm:1.0.0-beta.8"
|
resolution: "@ai-sdk/deepseek@npm:1.0.0-beta.9"
|
||||||
dependencies:
|
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": "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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/4ff14a3032dcbf931db0f8b02e992ffdee541a2eca1aa49ffae4d56d9f9a14f5c4cc6fbbee03e1841964d00dbe9f7fa55c78ca7ea2c33865bf681d72ac0cf26b
|
checksum: 10c0/4dd98316ab91610ab64aea2f44c701d59ea37a5a6480f3a27470cfa3109348e8b1dd0117a9e235150ff1c81a47454cfc26a46e13e8c2896710eae2cd403f84eb
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/gateway@npm:1.0.0-beta.12":
|
"@ai-sdk/gateway@npm:1.0.0-beta.14":
|
||||||
version: 1.0.0-beta.12
|
version: 1.0.0-beta.14
|
||||||
resolution: "@ai-sdk/gateway@npm:1.0.0-beta.12"
|
resolution: "@ai-sdk/gateway@npm:1.0.0-beta.14"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/acdb23c8a99dc7c412db32dc55bc1e766b7b65988a312f7622686e2ef986ca64e32b1213d3045a855248334e7f328173267e81461ebb9f557a91a81484d2932f
|
checksum: 10c0/f3d155bd7c5a842a126dbdf25eb16cadb4f785f516e28c995d7e430f0c1974466b402552fdf9f00e6897584299b927888ecb6319599646c12373b3bf147647f9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@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.16
|
version: 3.0.0-beta.17
|
||||||
resolution: "@ai-sdk/google-vertex@npm:3.0.0-beta.16"
|
resolution: "@ai-sdk/google-vertex@npm:3.0.0-beta.17"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/anthropic": "npm:2.0.0-beta.8"
|
"@ai-sdk/anthropic": "npm:2.0.0-beta.9"
|
||||||
"@ai-sdk/google": "npm:2.0.0-beta.14"
|
"@ai-sdk/google": "npm:2.0.0-beta.15"
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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"
|
google-auth-library: "npm:^9.15.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/c8b82da3ec9840b1c05cf8c0bc5ff47b6bc058aa6ea6f2bc006be908afdba9056a15e0cfb1d03395cd9abefa71d58ed11eaf88570b0f5a5ea296a817c00cd676
|
checksum: 10c0/95544f7f1fd0b7c2bf67d98c87233738a82a733e86c9c809f22b2c1db8809baa1ba2cea9edab7bc47f7947aa314507fa67ce741e54cb881e06341598c7e7dd33
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/google@npm:2.0.0-beta.14":
|
"@ai-sdk/google@npm:2.0.0-beta.15":
|
||||||
version: 2.0.0-beta.14
|
version: 2.0.0-beta.15
|
||||||
resolution: "@ai-sdk/google@npm:2.0.0-beta.14"
|
resolution: "@ai-sdk/google@npm:2.0.0-beta.15"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/24c356541fedbbccbbade82a470a9ee76779661196ca02e4c78d2081660981d6a0034d6af315b2fb603a9f880ae601bda00bd6ae1495c7e0844c44f0a4fe6d0f
|
checksum: 10c0/527f16f46b8ab3240a38c39d1f5b09f3e9ca66f10229676647e86b1a0e13901c5bc4739386e4a81036657f01d28cd16b8bc206a3de5a425b2bb67961b5166db7
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/mistral@npm:2.0.0-beta.6":
|
"@ai-sdk/mistral@npm:2.0.0-beta.7":
|
||||||
version: 2.0.0-beta.6
|
version: 2.0.0-beta.7
|
||||||
resolution: "@ai-sdk/mistral@npm:2.0.0-beta.6"
|
resolution: "@ai-sdk/mistral@npm:2.0.0-beta.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/075cbfc709b5c9b1af69db05c8f8f9e3edfe0caadaad72f26ceb4ee7022b785b8af62e982cfc6c496dff1386da0dd9742d675c55a14f348aff492bed52a310e5
|
checksum: 10c0/69000e13adb306d33199818a97bfbed8d721b8c453f53ff58b25d6b554b3e65ce6c3f5239bfa61fdf15fb9ceb5a4b4f768173fde8c26d059f73b5a66a54df4d8
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@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.8
|
version: 1.0.0-beta.9
|
||||||
resolution: "@ai-sdk/openai-compatible@npm:1.0.0-beta.8"
|
resolution: "@ai-sdk/openai-compatible@npm:1.0.0-beta.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/047f044bf0da9608e09073957916373bd39760ec00f498ba0c4a597ec70ba9eb4ef31f06b21b363b3c1ba775f64fcc46d41b60a171e0e99250824817ecb19ba8
|
checksum: 10c0/bee6d3acef2efd874fcdd83662349b95172011addb9a224187920784cf5fec53a3eb4b4ca2801cb8b745f90c4a2406c4683ef006c48d94d6a91492c68289e636
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/openai@npm:2.0.0-beta.11":
|
"@ai-sdk/openai@npm:2.0.0-beta.12":
|
||||||
version: 2.0.0-beta.11
|
version: 2.0.0-beta.12
|
||||||
resolution: "@ai-sdk/openai@npm:2.0.0-beta.11"
|
resolution: "@ai-sdk/openai@npm:2.0.0-beta.12"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/c48664c651cd50c10db5b18b963e39a964d5d69649c24350bff5cca3f5b02ef4f75531ff93a51e8463db91023b050d7f415c81ea0fd48eeb5a55bb5233b151a6
|
checksum: 10c0/a96f918f6264a335f26ff694c8952085dbb9a07df455ef32fcd5e8cc4ed7f7e59f2581e7b962a1c38ffd9d74d19290e78c003ab1c568287e029349652852a5a2
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@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.5
|
version: 3.0.0-beta.6
|
||||||
resolution: "@ai-sdk/provider-utils@npm:3.0.0-beta.5"
|
resolution: "@ai-sdk/provider-utils@npm:3.0.0-beta.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
"@ai-sdk/provider": "npm:2.0.0-beta.1"
|
||||||
"@standard-schema/spec": "npm:^1.0.0"
|
"@standard-schema/spec": "npm:^1.0.0"
|
||||||
|
|
@ -143,7 +143,7 @@ __metadata:
|
||||||
zod-to-json-schema: "npm:^3.24.1"
|
zod-to-json-schema: "npm:^3.24.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/229a53672accc5d9d986da2e18f619dbcfaf64ab269c8cc9e955480c4428d2a87255330c587453d01eb66ac297bb6975f91c24a93f87dd4b84f6428cb60d4211
|
checksum: 10c0/d1cc412d637689e9252b7e14c8db03e98df06bfd471aba2b1a1d715dbd1353854d046f3028dca6460b2f3741f9d76b0cf52ad76b4c833e3da87bb27d026a450a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -156,12 +156,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/react@npm:2.0.0-beta.26":
|
"@ai-sdk/react@npm:2.0.0-beta.28":
|
||||||
version: 2.0.0-beta.26
|
version: 2.0.0-beta.28
|
||||||
resolution: "@ai-sdk/react@npm:2.0.0-beta.26"
|
resolution: "@ai-sdk/react@npm:2.0.0-beta.28"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/provider-utils": "npm:3.0.0-beta.5"
|
"@ai-sdk/provider-utils": "npm:3.0.0-beta.6"
|
||||||
ai: "npm:5.0.0-beta.26"
|
ai: "npm:5.0.0-beta.28"
|
||||||
swr: "npm:^2.2.5"
|
swr: "npm:^2.2.5"
|
||||||
throttleit: "npm:2.1.0"
|
throttleit: "npm:2.1.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -170,20 +170,20 @@ __metadata:
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
zod:
|
zod:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10c0/75583fec8fdb4ceaac75aa5ff00157532ac69d50d9b88604b8f531a68a7b94a6e6ad02e9c54da039903391d403568f0a49837b75f2d860f1fb885ff2d97c8acd
|
checksum: 10c0/a3435b49eade4d51bbd608aba10102393fd0555004db4b300642fbf70617022741413230a5941afbadc7baf8a3a6f8a5607e50fae1616992c0b706760fc091b9
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/xai@npm:2.0.0-beta.10":
|
"@ai-sdk/xai@npm:2.0.0-beta.11":
|
||||||
version: 2.0.0-beta.10
|
version: 2.0.0-beta.11
|
||||||
resolution: "@ai-sdk/xai@npm:2.0.0-beta.10"
|
resolution: "@ai-sdk/xai@npm:2.0.0-beta.11"
|
||||||
dependencies:
|
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": "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:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
checksum: 10c0/8f6251785892db79306c95cdde38cbede40c4c73c354bfbdc78262fe2b6736646d0ce4186028e81d0ea59cdf6e584f53afdc2a3e3299e5df754d59c9ad828688
|
checksum: 10c0/1a3d8c4bab61cba471eb4fa2cf010c53d4c0ba56dec464bf2eebf37723049a40369db5ffc0d19a561a11b928f0509e2bc61d9d8f745266f17ad41b34fa850179
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -6497,16 +6497,16 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@sourcebot/web@workspace:packages/web"
|
resolution: "@sourcebot/web@workspace:packages/web"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/amazon-bedrock": "npm:3.0.0-beta.9"
|
"@ai-sdk/amazon-bedrock": "npm:3.0.0-beta.10"
|
||||||
"@ai-sdk/anthropic": "npm:2.0.0-beta.8"
|
"@ai-sdk/anthropic": "npm:2.0.0-beta.9"
|
||||||
"@ai-sdk/azure": "npm:2.0.0-beta.11"
|
"@ai-sdk/azure": "npm:2.0.0-beta.12"
|
||||||
"@ai-sdk/deepseek": "npm:1.0.0-beta.8"
|
"@ai-sdk/deepseek": "npm:1.0.0-beta.9"
|
||||||
"@ai-sdk/google": "npm:2.0.0-beta.14"
|
"@ai-sdk/google": "npm:2.0.0-beta.15"
|
||||||
"@ai-sdk/google-vertex": "npm:3.0.0-beta.16"
|
"@ai-sdk/google-vertex": "npm:3.0.0-beta.17"
|
||||||
"@ai-sdk/mistral": "npm:2.0.0-beta.6"
|
"@ai-sdk/mistral": "npm:2.0.0-beta.7"
|
||||||
"@ai-sdk/openai": "npm:2.0.0-beta.11"
|
"@ai-sdk/openai": "npm:2.0.0-beta.12"
|
||||||
"@ai-sdk/react": "npm:2.0.0-beta.26"
|
"@ai-sdk/react": "npm:2.0.0-beta.28"
|
||||||
"@ai-sdk/xai": "npm:2.0.0-beta.10"
|
"@ai-sdk/xai": "npm:2.0.0-beta.11"
|
||||||
"@auth/prisma-adapter": "npm:^2.7.4"
|
"@auth/prisma-adapter": "npm:^2.7.4"
|
||||||
"@codemirror/commands": "npm:^6.6.0"
|
"@codemirror/commands": "npm:^6.6.0"
|
||||||
"@codemirror/lang-cpp": "npm:^6.0.2"
|
"@codemirror/lang-cpp": "npm:^6.0.2"
|
||||||
|
|
@ -6603,7 +6603,7 @@ __metadata:
|
||||||
"@vercel/otel": "npm:^1.13.0"
|
"@vercel/otel": "npm:^1.13.0"
|
||||||
"@viz-js/lang-dot": "npm:^1.0.4"
|
"@viz-js/lang-dot": "npm:^1.0.4"
|
||||||
"@xiechao/codemirror-lang-handlebars": "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"
|
ajv: "npm:^8.17.1"
|
||||||
bcryptjs: "npm:^3.0.2"
|
bcryptjs: "npm:^3.0.2"
|
||||||
class-variance-authority: "npm:^0.7.0"
|
class-variance-authority: "npm:^0.7.0"
|
||||||
|
|
@ -7991,19 +7991,19 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"ai@npm:5.0.0-beta.26":
|
"ai@npm:5.0.0-beta.28":
|
||||||
version: 5.0.0-beta.26
|
version: 5.0.0-beta.28
|
||||||
resolution: "ai@npm:5.0.0-beta.26"
|
resolution: "ai@npm:5.0.0-beta.28"
|
||||||
dependencies:
|
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": "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"
|
"@opentelemetry/api": "npm:1.9.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.25.76 || ^4
|
zod: ^3.25.76 || ^4
|
||||||
bin:
|
bin:
|
||||||
ai: dist/bin/ai.min.js
|
ai: dist/bin/ai.min.js
|
||||||
checksum: 10c0/a3161a5bd9f6fa9a362a1c938603efc3b806828a297232207126d5c0b3ec45f03212ee5b046dced9df7ad4e48dec7829ef5ac133d12a296f86b2c33ea71ad515
|
checksum: 10c0/58f178923ac885cde420091529cdc347b39f52389c06f7a1186564cb7936b761b4790aafd2d9e32c03b8805336be94c94957d3f0515acad7922a41c1d5239cda
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue