diff --git a/apps/code/src/renderer/features/message-editor/suggestions/getSuggestions.ts b/apps/code/src/renderer/features/message-editor/suggestions/getSuggestions.ts index bbaed3a55..02a158b7b 100644 --- a/apps/code/src/renderer/features/message-editor/suggestions/getSuggestions.ts +++ b/apps/code/src/renderer/features/message-editor/suggestions/getSuggestions.ts @@ -1,7 +1,12 @@ import type { AvailableCommand } from "@agentclientprotocol/sdk"; import { CODE_COMMANDS } from "@features/message-editor/commands"; import { getAvailableCommandsForTask } from "@features/sessions/stores/sessionStore"; -import { fetchRepoFiles, searchFiles } from "@hooks/useRepoFiles"; +import { + fetchRepoFiles, + pathToFileItem, + searchFiles, +} from "@hooks/useRepoFiles"; +import { isAbsolutePath } from "@utils/path"; import Fuse, { type IFuseOptions } from "fuse.js"; import { useDraftStore } from "../stores/draftStore"; import type { CommandSuggestionItem, FileSuggestionItem } from "../types"; @@ -39,26 +44,55 @@ function searchCommands( return results.map((result) => result.item); } +function parentDirLabel(dir: string, name: string): string { + const parent = dir.split("/").filter(Boolean).pop(); + return parent ? `${parent}/${name}` : name; +} + +function getAbsolutePathSuggestion(query: string): FileSuggestionItem | null { + if (!isAbsolutePath(query)) return null; + if (!/\.\w+$/.test(query)) return null; + + const fileItem = pathToFileItem(query); + return { + id: query, + label: parentDirLabel(fileItem.dir, fileItem.name), + description: fileItem.dir || undefined, + filename: fileItem.name, + path: query, + }; +} + export async function getFileSuggestions( sessionId: string, query: string, ): Promise { const repoPath = useDraftStore.getState().contexts[sessionId]?.repoPath; + const absoluteMatch = getAbsolutePathSuggestion(query); if (!repoPath) { - return []; + return absoluteMatch ? [absoluteMatch] : []; } const { files, fzf } = await fetchRepoFiles(repoPath); const matched = searchFiles(fzf, files, query); - return matched.map((file) => ({ + const results: FileSuggestionItem[] = matched.map((file) => ({ id: file.path, - label: file.name, + label: parentDirLabel(file.dir, file.name), description: file.dir || undefined, filename: file.name, path: file.path, })); + + if ( + absoluteMatch && + !results.some((r) => `${repoPath}/${r.id}` === absoluteMatch.id) + ) { + results.unshift(absoluteMatch); + } + + return results; } export function getCommandSuggestions( diff --git a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx index 0a459455d..ae1733913 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx +++ b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx @@ -35,8 +35,9 @@ function DefaultChip({ const isCommand = type === "command"; const prefix = isCommand ? "/" : "@"; + const isFile = type === "file"; - return ( + const chip = ( ); + + if (isFile) { + return {chip}; + } + + return chip; } function PastedTextChip({ diff --git a/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx b/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx index 6079b3ec8..46f693fb7 100644 --- a/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx +++ b/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx @@ -91,12 +91,15 @@ export function parseMentionTags(content: string): ReactNode[] { if (match[1]) { const filePath = match[1]; - const fileName = filePath.split("/").pop() ?? filePath; + const segments = filePath.split("/").filter(Boolean); + const fileName = segments.pop() ?? filePath; + const parentDir = segments.pop(); + const label = parentDir ? `${parentDir}/${fileName}` : fileName; parts.push( } - label={fileName} + label={label} />, ); } else if (match[2]) {