mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-01 18:13:03 +08:00
fix(chat): open file references in editor instead of new browser window
Clicking a file reference in a chat message (e.g. `useShellTerminal.ts`) opened a new browser window because it was rendered as a plain anchor with target="_blank" and an empty/relative href. The markdown link renderer now intercepts file-path links — using the href, or the link text when the href is empty — strips any `:line:col` suffix, and opens the file in the in-app editor side panel while keeping the Chat tab active (matching the inline edit view). - useFileOpenResolver: resolves bare/partial references to real project files via the cached project file tree - PaletteOpsContext: add `openFileInEditor` op that opens the editor without switching tabs
This commit is contained in:
108
src/hooks/useFileOpenResolver.ts
Normal file
108
src/hooks/useFileOpenResolver.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import type { Project } from '../types/app';
|
||||
|
||||
type FileNode = {
|
||||
type: 'file' | 'directory';
|
||||
name: string;
|
||||
path: string;
|
||||
children?: FileNode[];
|
||||
};
|
||||
|
||||
type FlatFile = {
|
||||
name: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
// `diffInfo` is intentionally `any` so this resolver can wrap editor handlers
|
||||
// that expect a concrete diff payload type as well as generic callers.
|
||||
type OnFileOpen = (filePath: string, diffInfo?: any) => void;
|
||||
|
||||
const normalize = (value: string): string => value.replace(/\\/g, '/');
|
||||
|
||||
const flatten = (nodes: FileNode[], out: FlatFile[]): void => {
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'file') {
|
||||
out.push({ name: node.name, path: node.path });
|
||||
} else if (node.children && node.children.length > 0) {
|
||||
flatten(node.children, out);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// References inside chat messages are often bare basenames (`foo.ts`) or partial
|
||||
// paths (`utils/foo.ts`) rather than full paths, so match by path suffix and
|
||||
// fall back to filename equality.
|
||||
const findBestMatch = (files: FlatFile[], ref: string): string | null => {
|
||||
const target = normalize(ref).replace(/^\.\//, '').replace(/^\/+/, '');
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffixMatch = files.find((file) => {
|
||||
const filePath = normalize(file.path);
|
||||
return filePath === target || filePath.endsWith(`/${target}`);
|
||||
});
|
||||
if (suffixMatch) {
|
||||
return suffixMatch.path;
|
||||
}
|
||||
|
||||
const base = target.split('/').pop() || target;
|
||||
return files.find((file) => file.name === base)?.path ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps an `onFileOpen` handler so a possibly bare/partial file reference is
|
||||
* resolved against the project's file tree (cached per project) before the file
|
||||
* is opened in the in-app editor.
|
||||
*/
|
||||
export function useFileOpenResolver(
|
||||
selectedProject: Project | null | undefined,
|
||||
onFileOpen: OnFileOpen,
|
||||
): OnFileOpen {
|
||||
const projectId = selectedProject?.projectId;
|
||||
const cacheRef = useRef<{ projectId?: string; files: Promise<FlatFile[]> | null }>({
|
||||
projectId: undefined,
|
||||
files: null,
|
||||
});
|
||||
|
||||
const loadFiles = useCallback((): Promise<FlatFile[]> => {
|
||||
if (!projectId) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
if (cacheRef.current.projectId === projectId && cacheRef.current.files) {
|
||||
return cacheRef.current.files;
|
||||
}
|
||||
|
||||
const filesPromise = (async () => {
|
||||
try {
|
||||
const response = await api.getFiles(projectId);
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
const data = await response.json();
|
||||
const tree: FileNode[] = Array.isArray(data) ? data : [];
|
||||
const flat: FlatFile[] = [];
|
||||
flatten(tree, flat);
|
||||
return flat;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
cacheRef.current = { projectId, files: filesPromise };
|
||||
return filesPromise;
|
||||
}, [projectId]);
|
||||
|
||||
return useCallback(
|
||||
(filePath: string, diffInfo?: any) => {
|
||||
const ref = normalize(filePath).trim();
|
||||
void loadFiles().then((files) => {
|
||||
const match = findBestMatch(files, ref);
|
||||
onFileOpen(match ?? filePath, diffInfo);
|
||||
});
|
||||
},
|
||||
[loadFiles, onFileOpen],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user