Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ import {
claimWorkspaceBasenameLookup,
needsWorkspaceBasenameLookup,
pickWorkspaceBasenameMatch,
stripRepeatedWorkspacePrefix,
WORKSPACE_BASENAME_LOOKUP_LIMIT,
} from "../workspaceBasenameLookup";
import {
Expand Down Expand Up @@ -2559,26 +2560,34 @@ function useChatMarkdownState({
);
const findWorkspaceBasenameMatch = useCallback(
async (workspaceRelativePath: string) => {
if (!cwd || environmentId === null || !needsWorkspaceBasenameLookup(workspaceRelativePath)) {
return null;
}
if (!cwd || environmentId === null) return null;
const isBasename = needsWorkspaceBasenameLookup(workspaceRelativePath);
const strippedPath = isBasename
? null
: stripRepeatedWorkspacePrefix(workspaceRelativePath, cwd);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High components/ChatMarkdown.tsx:2567

On case-insensitive Windows workspaces, stripRepeatedWorkspacePrefix fails to match a repo-relative path such as physics\notes\outline.md against C:\school\Physics, so the lookup is skipped and the panel opens the duplicated Physics\physics\notes\outline.md path. Make the prefix comparison case-insensitive for Windows paths so normal casing variations are stripped.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 2567:

On case-insensitive Windows workspaces, `stripRepeatedWorkspacePrefix` fails to match a repo-relative path such as `physics\notes\outline.md` against `C:\school\Physics`, so the lookup is skipped and the panel opens the duplicated `Physics\physics\notes\outline.md` path. Make the prefix comparison case-insensitive for Windows paths so normal casing variations are stripped.

if (!isBasename && strippedPath === null) return null;
const result = await searchProjectEntries({
environmentId,
input: {
cwd,
query: workspaceRelativePath,
query: strippedPath ?? workspaceRelativePath,
limit: WORKSPACE_BASENAME_LOOKUP_LIMIT,
kind: "file",
},
});
return result._tag === "Success"
? pickWorkspaceBasenameMatch(workspaceRelativePath, result.value.entries)
: null;
if (result._tag !== "Success") return null;
if (strippedPath === null) {
return pickWorkspaceBasenameMatch(workspaceRelativePath, result.value.entries);
}
// A real child folder named like the project keeps the literal path.
const paths = new Set(result.value.entries.map((entry) => entry.path));
return !paths.has(workspaceRelativePath) && paths.has(strippedPath) ? strippedPath : null;
},
[cwd, environmentId, searchProjectEntries],
);
// A bare filename resolves to the workspace root, which is rarely where the
// file is, so ask the index before opening. Absolute host paths open as-is.
// file is, and a repo-root-relative path repeats the project folder, so ask
// the index before opening. Absolute host paths open as-is.
const openFileInPanel = useCallback(
(panelPath: string, line: number | undefined) => {
if (!threadRef) return;
Expand All @@ -2587,7 +2596,11 @@ function useChatMarkdownState({
const isLatestLookup = claimWorkspaceBasenameLookup();
const openAt = (path: string) =>
useRightPanelStore.getState().openFile(threadRef, path, line);
if (!cwd || !needsWorkspaceBasenameLookup(panelPath)) {
if (
!cwd ||
(!needsWorkspaceBasenameLookup(panelPath) &&
stripRepeatedWorkspacePrefix(panelPath, cwd) === null)
) {
openAt(panelPath);
return;
}
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/workspaceBasenameLookup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
claimWorkspaceBasenameLookup,
needsWorkspaceBasenameLookup,
pickWorkspaceBasenameMatch,
stripRepeatedWorkspacePrefix,
} from "./workspaceBasenameLookup";

describe("needsWorkspaceBasenameLookup", () => {
Expand Down Expand Up @@ -92,3 +93,20 @@ describe("claimWorkspaceBasenameLookup", () => {
expect(only()).toBe(true);
});
});

describe("stripRepeatedWorkspacePrefix", () => {
it("drops the project folder repeated by a repo-root-relative path", () => {
expect(stripRepeatedWorkspacePrefix("physics/notes/outline.md", "/school/physics")).toBe(
"notes/outline.md",
);
expect(stripRepeatedWorkspacePrefix("courses/physics/a.md", "/school/courses/physics")).toBe(
"a.md",
);
expect(stripRepeatedWorkspacePrefix("physics\\a.md", "C:\\school\\physics")).toBe("a.md");
});

it("returns null when the path does not start with the project folder", () => {
expect(stripRepeatedWorkspacePrefix("notes/outline.md", "/school/physics")).toBeNull();
expect(stripRepeatedWorkspacePrefix("physics", "/school/physics")).toBeNull();
});
});
17 changes: 17 additions & 0 deletions apps/web/src/workspaceBasenameLookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,20 @@ export function pickWorkspaceBasenameMatch(
);
return foldedMatches.length === 1 ? (foldedMatches[0]?.path ?? null) : null;
}

/**
* Agents echo git paths relative to the repository root, so in a project that
* is a subfolder of its repository `physics/notes/a.md` repeats the project's
* own folder. Returns the path with that repeated prefix removed, if any.
*/
export function stripRepeatedWorkspacePrefix(relativePath: string, cwd: string): string | null {
const pathSegments = relativePath.split(/[\\/]+/);
const cwdSegments = cwd.split(/[\\/]+/).filter(Boolean);
for (let count = Math.min(cwdSegments.length, pathSegments.length - 1); count > 0; count--) {
const prefix = cwdSegments.slice(-count);
if (prefix.every((segment, index) => segment === pathSegments[index])) {
return pathSegments.slice(count).join("/");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/workspaceBasenameLookup.ts:67

A Windows path such as physics\notes\a.md is not matched against the workspace index's physics/notes/a.md, so paths.has(workspaceRelativePath) incorrectly reports the literal file as absent and may open notes/a.md instead. The slash normalization here only affects the stripped return value; normalize workspaceRelativePath before the lookup or use the normalized value consistently.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/workspaceBasenameLookup.ts around line 67:

A Windows path such as `physics\notes\a.md` is not matched against the workspace index's `physics/notes/a.md`, so `paths.has(workspaceRelativePath)` incorrectly reports the literal file as absent and may open `notes/a.md` instead. The slash normalization here only affects the stripped return value; normalize `workspaceRelativePath` before the lookup or use the normalized value consistently.

}
}
return null;
}
Loading