perf: optimize markdown file link detection

Only mark existing files as openable links
Avoid file-link behavior while messages stream
Limit file existence checks to 4 concurrent requests
This commit is contained in:
Bohdan Triapitsyn
2026-05-05 19:21:44 +03:00
parent fd6635a963
commit a49e2ce1ae
3 changed files with 94 additions and 21 deletions
@@ -992,10 +992,15 @@ interface MarkdownRendererProps {
disableStreamAnimation?: boolean; disableStreamAnimation?: boolean;
variant?: MarkdownVariant; variant?: MarkdownVariant;
onShowPopup?: (content: ToolPopupContent) => void; onShowPopup?: (content: ToolPopupContent) => void;
enableFileReferences?: boolean;
} }
const MERMAID_BLOCK_SELECTOR = '[data-markdown="mermaid-block"]'; const MERMAID_BLOCK_SELECTOR = '[data-markdown="mermaid-block"]';
const FILE_LINK_SELECTOR = '[data-openchamber-file-link="true"]'; const FILE_LINK_SELECTOR = '[data-openchamber-file-link="true"]';
const FILE_REFERENCE_STAT_CONCURRENCY = 4;
const FILE_REFERENCE_STAT_CACHE = new Map<string, Promise<boolean>>();
let activeFileReferenceStatCount = 0;
const pendingFileReferenceStats: Array<() => void> = [];
type ParsedFileReference = { type ParsedFileReference = {
path: string; path: string;
@@ -1262,6 +1267,44 @@ const getResolvedReference = (rawValue: string, effectiveDirectory: string): (Pa
}; };
}; };
const fileReferenceExists = (resolvedPath: string): Promise<boolean> => {
const normalizedPath = normalizePath(resolvedPath);
if (!normalizedPath) {
return Promise.resolve(false);
}
const cached = FILE_REFERENCE_STAT_CACHE.get(normalizedPath);
if (cached) {
return cached;
}
const request = new Promise<boolean>((resolve) => {
const run = () => {
activeFileReferenceStatCount += 1;
void fetch(`/api/fs/stat?path=${encodeURIComponent(normalizedPath)}`, {
method: 'GET',
cache: 'no-store',
})
.then((response) => resolve(response.ok))
.catch(() => resolve(false))
.finally(() => {
activeFileReferenceStatCount = Math.max(0, activeFileReferenceStatCount - 1);
pendingFileReferenceStats.shift()?.();
});
};
if (activeFileReferenceStatCount < FILE_REFERENCE_STAT_CONCURRENCY) {
run();
return;
}
pendingFileReferenceStats.push(run);
});
FILE_REFERENCE_STAT_CACHE.set(normalizedPath, request);
return request;
};
const getContextDirectory = (effectiveDirectory: string, resolvedPath: string): string => { const getContextDirectory = (effectiveDirectory: string, resolvedPath: string): string => {
const normalizedDirectory = normalizePath(effectiveDirectory); const normalizedDirectory = normalizePath(effectiveDirectory);
if (normalizedDirectory) { if (normalizedDirectory) {
@@ -1278,11 +1321,13 @@ const useFileReferenceInteractions = ({
effectiveDirectory, effectiveDirectory,
editor, editor,
preferRuntimeEditor, preferRuntimeEditor,
enabled,
}: { }: {
containerRef: React.RefObject<HTMLDivElement | null>; containerRef: React.RefObject<HTMLDivElement | null>;
effectiveDirectory: string; effectiveDirectory: string;
editor?: EditorAPI; editor?: EditorAPI;
preferRuntimeEditor?: boolean; preferRuntimeEditor?: boolean;
enabled: boolean;
}) => { }) => {
const annotationDebounceRef = React.useRef<number | null>(null); const annotationDebounceRef = React.useRef<number | null>(null);
@@ -1291,14 +1336,9 @@ const useFileReferenceInteractions = ({
if (!container) { if (!container) {
return; return;
} }
let cancelled = false;
const annotateFileLinks = () => { const clearFileLinkAttributes = (candidate: HTMLElement) => {
const candidates = container.querySelectorAll<HTMLElement>('[data-markdown="inline-code"], a');
for (const candidate of Array.from(candidates)) {
const rawCandidate = extractPathCandidateFromElement(candidate);
const resolved = getResolvedReference(rawCandidate, effectiveDirectory);
if (!resolved) {
candidate.removeAttribute('data-openchamber-file-link'); candidate.removeAttribute('data-openchamber-file-link');
candidate.removeAttribute('data-openchamber-file-ref'); candidate.removeAttribute('data-openchamber-file-ref');
candidate.removeAttribute('data-openchamber-file-path'); candidate.removeAttribute('data-openchamber-file-path');
@@ -1309,17 +1349,40 @@ const useFileReferenceInteractions = ({
candidate.removeAttribute('role'); candidate.removeAttribute('role');
candidate.removeAttribute('tabindex'); candidate.removeAttribute('tabindex');
} }
};
const annotateFileLinks = () => {
const candidates = container.querySelectorAll<HTMLElement>('[data-markdown="inline-code"], a');
for (const candidate of Array.from(candidates)) {
const rawCandidate = extractPathCandidateFromElement(candidate);
const resolved = getResolvedReference(rawCandidate, effectiveDirectory);
clearFileLinkAttributes(candidate);
if (!enabled || !resolved) {
continue; continue;
} }
void fileReferenceExists(resolved.resolvedPath).then((exists) => {
if (cancelled || !exists || !container.contains(candidate)) {
return;
}
const latestRawCandidate = extractPathCandidateFromElement(candidate);
const latestResolved = getResolvedReference(latestRawCandidate, effectiveDirectory);
if (!latestResolved || latestResolved.resolvedPath !== resolved.resolvedPath) {
return;
}
candidate.setAttribute('data-openchamber-file-link', 'true'); candidate.setAttribute('data-openchamber-file-link', 'true');
candidate.setAttribute('data-openchamber-file-ref', rawCandidate); candidate.setAttribute('data-openchamber-file-ref', latestRawCandidate);
candidate.setAttribute('data-openchamber-file-path', resolved.resolvedPath); candidate.setAttribute('data-openchamber-file-path', latestResolved.resolvedPath);
candidate.setAttribute('title', 'Open file'); candidate.setAttribute('title', 'Open file');
if (candidate.tagName.toLowerCase() !== 'a') { if (candidate.tagName.toLowerCase() !== 'a') {
candidate.setAttribute('role', 'button'); candidate.setAttribute('role', 'button');
candidate.setAttribute('tabindex', '0'); candidate.setAttribute('tabindex', '0');
} }
});
} }
}; };
@@ -1416,6 +1479,7 @@ const useFileReferenceInteractions = ({
container.addEventListener('keydown', handleKeyDown); container.addEventListener('keydown', handleKeyDown);
return () => { return () => {
cancelled = true;
if (annotationDebounceRef.current !== null && typeof window !== 'undefined') { if (annotationDebounceRef.current !== null && typeof window !== 'undefined') {
window.clearTimeout(annotationDebounceRef.current); window.clearTimeout(annotationDebounceRef.current);
} }
@@ -1424,7 +1488,7 @@ const useFileReferenceInteractions = ({
container.removeEventListener('click', handleClick); container.removeEventListener('click', handleClick);
container.removeEventListener('keydown', handleKeyDown); container.removeEventListener('keydown', handleKeyDown);
}; };
}, [containerRef, editor, effectiveDirectory, preferRuntimeEditor]); }, [containerRef, editor, effectiveDirectory, preferRuntimeEditor, enabled]);
}; };
const useMermaidInlineInteractions = ({ const useMermaidInlineInteractions = ({
@@ -1531,6 +1595,7 @@ const MarkdownRendererImpl: React.FC<MarkdownRendererProps> = ({
disableStreamAnimation = false, disableStreamAnimation = false,
variant = 'assistant', variant = 'assistant',
onShowPopup, onShowPopup,
enableFileReferences = true,
}) => { }) => {
const currentTheme = useCurrentMermaidTheme(); const currentTheme = useCurrentMermaidTheme();
const { editor, runtime } = useRuntimeAPIs(); const { editor, runtime } = useRuntimeAPIs();
@@ -1543,6 +1608,7 @@ const MarkdownRendererImpl: React.FC<MarkdownRendererProps> = ({
effectiveDirectory, effectiveDirectory,
editor, editor,
preferRuntimeEditor: runtime.isVSCode, preferRuntimeEditor: runtime.isVSCode,
enabled: enableFileReferences && !isStreaming,
}); });
useExternalLinkInteractions({ containerRef }); useExternalLinkInteractions({ containerRef });
const openContextPreview = useUIStore((state) => state.openContextPreview); const openContextPreview = useUIStore((state) => state.openContextPreview);
@@ -1615,6 +1681,7 @@ const SimpleMarkdownRendererImpl: React.FC<{
onShowPopup?: (content: ToolPopupContent) => void; onShowPopup?: (content: ToolPopupContent) => void;
mermaidControls?: MermaidControlOptions; mermaidControls?: MermaidControlOptions;
allowMermaidWheelZoom?: boolean; allowMermaidWheelZoom?: boolean;
enableFileReferences?: boolean;
}> = ({ }> = ({
content, content,
className, className,
@@ -1623,6 +1690,7 @@ const SimpleMarkdownRendererImpl: React.FC<{
stripFrontmatter = false, stripFrontmatter = false,
onShowPopup, onShowPopup,
allowMermaidWheelZoom = false, allowMermaidWheelZoom = false,
enableFileReferences = true,
}) => { }) => {
const { editor, runtime } = useRuntimeAPIs(); const { editor, runtime } = useRuntimeAPIs();
const renderedContent = React.useMemo( const renderedContent = React.useMemo(
@@ -1644,6 +1712,7 @@ const SimpleMarkdownRendererImpl: React.FC<{
effectiveDirectory, effectiveDirectory,
editor, editor,
preferRuntimeEditor: runtime.isVSCode, preferRuntimeEditor: runtime.isVSCode,
enabled: enableFileReferences,
}); });
useExternalLinkInteractions({ containerRef, enabled: !disableLinkSafety }); useExternalLinkInteractions({ containerRef, enabled: !disableLinkSafety });
const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]); const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
@@ -1674,5 +1743,6 @@ export const SimpleMarkdownRenderer = React.memo(SimpleMarkdownRendererImpl, (pr
&& prev.disableLinkSafety === next.disableLinkSafety && prev.disableLinkSafety === next.disableLinkSafety
&& prev.stripFrontmatter === next.stripFrontmatter && prev.stripFrontmatter === next.stripFrontmatter
&& prev.onShowPopup === next.onShowPopup && prev.onShowPopup === next.onShowPopup
&& prev.allowMermaidWheelZoom === next.allowMermaidWheelZoom; && prev.allowMermaidWheelZoom === next.allowMermaidWheelZoom
&& prev.enableFileReferences === next.enableFileReferences;
}); });
@@ -84,6 +84,7 @@ const AssistantTextPart: React.FC<AssistantTextPartProps> = ({
isStreaming={isStreaming} isStreaming={isStreaming}
disableStreamAnimation={chatRenderMode === 'sorted'} disableStreamAnimation={chatRenderMode === 'sorted'}
variant={part.type === 'reasoning' ? 'reasoning' : 'assistant'} variant={part.type === 'reasoning' ? 'reasoning' : 'assistant'}
enableFileReferences={isFinalized}
/> />
</div> </div>
); );
+2
View File
@@ -976,6 +976,8 @@ html:not(.dark) .chat-scroll {
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
user-select: text;
-webkit-user-select: text;
} }
.markdown-content [data-openchamber-file-link="true"]:hover { .markdown-content [data-openchamber-file-link="true"]:hover {