Repository navigation
fix(web): file chips find paths written relative to a nested repo or named folder #24948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,35 @@ | ||
| import { buildFileLinkParentSuffixByPath, fileLinkLabel } from "@t3tools/shared/fileLinks"; | ||
| import { | ||
| buildFileLinkParentSuffixByPath, | ||
| fileLinkLabel, | ||
| resolvePathLinkTarget, | ||
| } from "@t3tools/shared/fileLinks"; | ||
| import { | ||
| isWindowsDrivePathHref, | ||
| normalizeMarkdownLinkDestination, | ||
| extractInlineCodeSpans, | ||
| extractMarkdownLinkHrefs, | ||
| inlineCodeFilePathCandidate, | ||
| } from "@t3tools/shared/markdownLinks"; | ||
| import { isHostPath, resolveWorkspaceFilePath } from "@t3tools/shared/path"; | ||
| import { | ||
| isHostPath, | ||
| isNetworkPath, | ||
| normalizeProjectPathForComparison, | ||
| resolveWorkspaceFilePath, | ||
| workspaceRelativeFilePath, | ||
| } from "@t3tools/shared/path"; | ||
| import { usePullRequestLinking } from "~/hooks/usePullRequestLinking"; | ||
| import { | ||
| AuthFilesystemReadScope, | ||
| AuthOrchestrationOperateScope, | ||
| FILESYSTEM_METADATA_BATCH_LIMIT, | ||
| SOURCE_CONTROL_MEDIA_ASSET, | ||
| } from "@t3tools/contracts"; | ||
| import { | ||
| CHAT_MARKDOWN_REMARK_PLUGINS, | ||
| CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS, | ||
| CHAT_MARKDOWN_REHYPE_PLUGINS, | ||
| } from "@t3tools/shared/markdownPipeline"; | ||
| import { useAtomValue } from "@effect/atom-react"; | ||
| import { RegistryContext, useAtomValue } from "@effect/atom-react"; | ||
| import { | ||
| COMPOSER_CONTEXT_CLIPBOARD_MIME, | ||
| encodeComposerContextClipboardHtml, | ||
|
|
@@ -165,11 +176,15 @@ import { filesystemEnvironment } from "../state/filesystem"; | |
| import { | ||
| claimWorkspaceBasenameLookup, | ||
| needsWorkspaceBasenameLookup, | ||
| nestedWorkspaceLookupPath, | ||
| pickNestedWorkspaceMatch, | ||
| pickStrippedWorkspaceMatch, | ||
| pickWorkspaceBasenameMatch, | ||
| stripRepeatedWorkspacePrefix, | ||
| WORKSPACE_BASENAME_LOOKUP_LIMIT, | ||
| } from "@t3tools/client-runtime/workspace-basename-lookup"; | ||
| import { FileMetadataThreadContext } from "../hooks/FileMetadataThreadProvider"; | ||
| import { fileMetadataAtom } from "../hooks/useFileMetadata"; | ||
| import { resolvePullRequestPreviewTarget } from "@t3tools/client-runtime/change-request-links"; | ||
| import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | ||
| import { useOpenChangeRequestLink } from "~/lib/openPullRequestLink"; | ||
|
|
@@ -494,6 +509,8 @@ function useChatMarkdownState({ | |
| reportFailure: false, | ||
| refresh: true, | ||
| }); | ||
| const registry = use(RegistryContext); | ||
| const fileMetadataThread = use(FileMetadataThreadContext); | ||
| const openPreview = useAtomCommand(previewEnvironment.open, { | ||
| reportFailure: false, | ||
| }); | ||
|
|
@@ -736,6 +753,74 @@ function useChatMarkdownState({ | |
| openPreview, | ||
| }); | ||
| }; | ||
| // Acts only on the chip's settled metadata, so a click never waits on the | ||
| // network unless the chip already knows its root path is missing. | ||
| const isMissingNestedPath = (fileLinkMeta: MarkdownFileLinkMeta) => { | ||
| if ( | ||
| environmentId === null || | ||
| fileLinkMeta.workspaceLookupPath === null || | ||
| nestedWorkspaceLookupPath(fileLinkMeta.workspaceLookupPath) === null | ||
| ) { | ||
| return false; | ||
| } | ||
| const atom = fileMetadataAtom( | ||
| environmentId, | ||
| fileLinkMeta.filePath, | ||
| undefined, | ||
| fileMetadataThread, | ||
| readEnvironmentScope(environmentId, AuthFilesystemReadScope), | ||
| ); | ||
| if (atom === null) return false; | ||
| const known = registry.get(atom); | ||
| return known._tag === "Success" && !known.waiting && known.value === null; | ||
| }; | ||
| const findNestedWorkspaceFile = async ( | ||
| fileLinkMeta: MarkdownFileLinkMeta, | ||
| workspaceRoot: string, | ||
| lookupEnvironmentId: EnvironmentId, | ||
| ) => { | ||
| const nestedPath = nestedWorkspaceLookupPath(fileLinkMeta.workspaceLookupPath ?? ""); | ||
| if (nestedPath === null || !isMissingNestedPath(fileLinkMeta)) return null; | ||
| // Keyed like the metadata cache, so one Windows folder spelled two ways stays one candidate. | ||
| const seen = new Set([normalizeProjectPathForComparison(fileLinkMeta.filePath)]); | ||
| const candidates = [ | ||
| ...markdownFileLinkMetaByHref.values(), | ||
| ...inlineCodeFileLinkMetaByText.values(), | ||
| ] | ||
| .filter((meta) => meta.workspaceLookupPath === null) | ||
| .map((meta) => resolvePathLinkTarget(nestedPath, meta.filePath)) | ||
| .filter((path) => { | ||
| const key = normalizeProjectPathForComparison(path); | ||
| if (path.length > 512 || isNetworkPath(path) || seen.has(key)) return false; | ||
| seen.add(key); | ||
| return true; | ||
| }) | ||
| .slice(0, FILESYSTEM_METADATA_BATCH_LIMIT - 1); | ||
| // The chip's answer can predate the file, so the root path is checked again. | ||
| const metadata = await getFilesystemMetadata({ | ||
| environmentId: lookupEnvironmentId, | ||
| input: { paths: [fileLinkMeta.filePath, ...candidates] }, | ||
| }); | ||
| if (metadata._tag !== "Success" || metadata.value.entries[0] !== null) return null; | ||
| const named = candidates.filter( | ||
| (_, index) => metadata.value.entries[index + 1]?.kind === "file", | ||
| ); | ||
| if (named.length > 1) return null; | ||
| if (named[0] !== undefined) | ||
| return workspaceRelativeFilePath(named[0], workspaceRoot) ?? named[0]; | ||
| const result = await searchProjectEntries({ | ||
| environmentId: lookupEnvironmentId, | ||
| input: { | ||
| cwd: workspaceRoot, | ||
| query: nestedPath, | ||
| limit: WORKSPACE_BASENAME_LOOKUP_LIMIT, | ||
| kind: "file", | ||
| }, | ||
| }); | ||
| return result._tag === "Success" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High When the nested-path search is truncated, 🤖 Copy this AI Prompt to have your agent fix this:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked this one and am leaving the code as it is. I replayed every nested lookup from one real multi-repo project (647 threads, real files on disk). For 357 references, the top 25 results held exactly one exact suffix match, and 206 of those searches were truncated. I then fetched every result for each one. None gained a second exact match, and the single match was always ranked first. Rejecting truncated results would drop about 58% of these fixes to guard against a case that never occurred. When several repositories hold the path, all the exact matches rank above the fuzzy ones, so the ambiguity shows up inside the top 25 and
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I'm unable to act on this request because you do not have permissions within this repository. |
||
| ? pickNestedWorkspaceMatch(nestedPath, workspaceRoot, result.value.entries) | ||
| : null; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| }; | ||
| const findWorkspaceFileMatch = async (fileLinkMeta: MarkdownFileLinkMeta) => { | ||
| const workspaceRelativePath = fileLinkMeta.workspaceLookupPath; | ||
| if ( | ||
|
|
@@ -750,7 +835,9 @@ function useChatMarkdownState({ | |
| const strippedPath = isBasename | ||
| ? null | ||
| : stripRepeatedWorkspacePrefix(workspaceRelativePath, cwd); | ||
| if (!isBasename && strippedPath === null) return null; | ||
| if (!isBasename && strippedPath === null) { | ||
| return findNestedWorkspaceFile(fileLinkMeta, cwd, environmentId); | ||
| } | ||
| const result = await searchProjectEntries({ | ||
| environmentId, | ||
| input: { | ||
|
|
@@ -765,7 +852,7 @@ function useChatMarkdownState({ | |
| return pickWorkspaceBasenameMatch(workspaceRelativePath, result.value.entries); | ||
| } | ||
| const match = pickStrippedWorkspaceMatch(strippedPath, cwd, result.value.entries); | ||
| if (match === null) return null; | ||
| if (match === null) return findNestedWorkspaceFile(fileLinkMeta, cwd, environmentId); | ||
| // Search can be truncated or still scanning, so it cannot rule out the | ||
| // literal path. Check the host before redirecting to the stripped match. | ||
| const metadata = await getFilesystemMetadata({ | ||
|
|
@@ -784,7 +871,8 @@ function useChatMarkdownState({ | |
| fileLinkMeta.workspaceLookupPath === null || | ||
| !cwd || | ||
| (!needsWorkspaceBasenameLookup(panelPath) && | ||
| stripRepeatedWorkspacePrefix(panelPath, cwd) === null) | ||
| stripRepeatedWorkspacePrefix(panelPath, cwd) === null && | ||
| !isMissingNestedPath(fileLinkMeta)) | ||
| ) { | ||
| openAt(panelPath); | ||
| return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High
components/ChatMarkdown.tsx:805findNestedWorkspaceFiletreats any non-null metadata entry as a file match, so a matching directory can be selected instead of the actual file and cause panel reads to fail withWorkspacePathNotFileError. Filter candidates tokind === "file"before counting or selecting matches.🤖 Copy this AI Prompt to have your agent fix this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in b578e1d. A named-folder candidate now counts only when its metadata
kindis"file". That matches the workspace index step, which already searches files only. The browser test covers a reply naming two folders where the first holds a directory with that name and the second holds the file: the chip opens the file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm unable to act on this request because you do not have permissions within this repository.