Compare commits

...

2 Commits

Author SHA1 Message Date
Mohamed Boudra
603b380321 fix(app): preserve workspace-root link tooltips
Workspace file resolution deliberately rejects the root as an openable file, so tooltip formatting preserves its display-only dot form separately.
2026-07-11 09:21:12 +00:00
Mohamed Boudra
ac45b2f2ae fix(app): show Windows file links relative to workspace
Tooltip formatting only handled matching POSIX separators, so Windows paths could remain absolute. Reuse the workspace file-path resolver for normalized relative paths.
2026-07-10 18:49:06 +00:00
3 changed files with 78 additions and 33 deletions

View File

@@ -18,6 +18,7 @@ import { useStableEvent } from "@/hooks/use-stable-event";
import { CODE_SURFACE_DATASET } from "@/styles/code-surface";
import { useAssistantFileLinkResolverContext } from "./provider";
import type { AssistantFileLinkSource } from "./resolver";
import { formatFileLinkTooltipPath } from "./tooltip-path";
import { useFileLink } from "./use-file-link";
interface AssistantMarkdownLinkProps {
@@ -38,7 +39,7 @@ export function AssistantMarkdownLink({
const { configRef } = useAssistantFileLinkResolverContext();
const workspaceRoot = configRef.current.workspaceRoot;
const tooltipPath = useMemo(
() => (target ? formatInlinePathTargetForTooltip(target, workspaceRoot) : null),
() => (target ? formatFileLinkTooltipPath({ target, workspaceRoot }) : null),
[target, workspaceRoot],
);
const handleAnchorClickCapture = useStableEvent((event: MouseEvent<HTMLAnchorElement>) => {
@@ -147,38 +148,6 @@ export function AssistantMarkdownCodeLink({
);
}
function formatInlinePathTargetForTooltip(
target: { path: string; lineStart?: number; lineEnd?: number },
workspaceRoot: string | undefined,
): string {
let result = relativizePathToWorkspace(target.path, workspaceRoot);
if (target.lineStart) {
result += `:${target.lineStart}`;
if (target.lineEnd && target.lineEnd !== target.lineStart) {
result += `-${target.lineEnd}`;
}
}
return result;
}
function relativizePathToWorkspace(filePath: string, workspaceRoot: string | undefined): string {
if (!workspaceRoot) {
return filePath;
}
const root = workspaceRoot.replace(/\/+$/, "");
if (!root) {
return filePath;
}
if (filePath === root) {
return ".";
}
const prefix = `${root}/`;
if (filePath.startsWith(prefix)) {
return filePath.slice(prefix.length);
}
return filePath;
}
interface AssistantInlineCodePathLinkProps {
content: string;
inheritedStyles: TextStyle;

View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { formatFileLinkTooltipPath } from "./tooltip-path";
describe("formatFileLinkTooltipPath", () => {
it("shows a Windows file path relative to its workspace regardless of separators", () => {
expect(
formatFileLinkTooltipPath({
target: {
path: "C:/Users/me/repo/src/app.ts",
lineStart: 12,
lineEnd: 20,
},
workspaceRoot: "C:\\Users\\me\\repo",
}),
).toBe("src/app.ts:12-20");
});
it("shows the workspace root as a dot", () => {
expect(
formatFileLinkTooltipPath({
target: { path: "/Users/me/repo" },
workspaceRoot: "/Users/me/repo",
}),
).toBe(".");
});
it("keeps an absolute path outside the workspace", () => {
expect(
formatFileLinkTooltipPath({
target: { path: "/Users/me/notes.md" },
workspaceRoot: "/Users/me/repo",
}),
).toBe("/Users/me/notes.md");
});
it("keeps the target path when the workspace root is unavailable", () => {
expect(formatFileLinkTooltipPath({ target: { path: "src/app.ts", lineStart: 12 } })).toBe(
"src/app.ts:12",
);
});
});

View File

@@ -0,0 +1,35 @@
import { resolveWorkspaceFilePaths, type WorkspaceFileLocation } from "@/workspace/file-open";
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
interface FormatFileLinkTooltipPathInput {
target: WorkspaceFileLocation;
workspaceRoot?: string;
}
export function formatFileLinkTooltipPath({
target,
workspaceRoot,
}: FormatFileLinkTooltipPathInput): string {
const normalizedTargetPath = normalizeWorkspacePath(target.path);
const normalizedWorkspaceRoot = normalizeWorkspacePath(workspaceRoot);
let isWorkspaceRoot = false;
if (normalizedTargetPath && normalizedWorkspaceRoot) {
isWorkspaceRoot = normalizedTargetPath === normalizedWorkspaceRoot;
if (/^[A-Za-z]:\//.test(normalizedTargetPath)) {
isWorkspaceRoot =
normalizedTargetPath.toLowerCase() === normalizedWorkspaceRoot.toLowerCase();
}
}
const resolvedPaths = workspaceRoot
? resolveWorkspaceFilePaths({ path: target.path, workspaceRoot })
: null;
let result = isWorkspaceRoot ? "." : (resolvedPaths?.relativePath ?? target.path);
if (target.lineStart) {
result += `:${target.lineStart}`;
if (target.lineEnd && target.lineEnd !== target.lineStart) {
result += `-${target.lineEnd}`;
}
}
return result;
}