From de0055923e9a88bce32790e605f2b82bfadce9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 17 Oct 2026 13:11:13 -0300 Subject: [PATCH 1/3] fix: ~/ file links open in the file panel for projects outside the home folder A ~/ chip only reached the file panel when the client could infer the home folder from the project path. For a project on another drive or under /srv, the chip had no panel target and fell back to the editor, which got the raw ~/ string. The server now expands ~ when reading a file, as getMetadata already does. The client treats ~/ paths as host paths, so they open read-only with breadcrumbs that start at ~. Fixes #20935 Co-Authored-By: Claude Opus 5.5 --- .../src/workspace/WorkspaceFileSystem.test.ts | 20 +++++++++++++++++++ .../src/workspace/WorkspaceFileSystem.ts | 13 ++++++------ apps/web/src/components/ChatMarkdown.tsx | 4 ++-- .../src/components/files/FileBreadcrumbs.tsx | 4 ++-- .../components/files/FilePreviewPanel.test.ts | 7 +++++++ .../src/components/files/FilePreviewPanel.tsx | 4 ++-- .../web/src/components/files/filePath.test.ts | 9 +++++++++ apps/web/src/components/files/filePath.ts | 12 +++++++---- .../src/components/files/filePreviewMode.ts | 4 ++-- packages/shared/src/path.test.ts | 10 ++++++++++ packages/shared/src/path.ts | 7 ++++++- 11 files changed, 75 insertions(+), 19 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/WorkspaceFileSystem.test.ts index ae956e69d0cb..4ec082f843ca 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.test.ts @@ -201,6 +201,26 @@ it.layer(layerTest, { excludeTestServices: true })("WorkspaceFileSystemLive", (i }), ); + it.effect("reads host files in the home folder by ~/ path", () => + Effect.gen(function* () { + const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; + const cwd = yield* makeTempDir; + const home = yield* makeTempDir; + yield* writeTextFile(home, ".claude/CLAUDE.md", "# Notes\n"); + + const result = yield* workspaceFileSystem + .readFile({ cwd, relativePath: "~/.claude/CLAUDE.md" }) + .pipe(Effect.provideService(HostProcess.HomeDirectory, home)); + + expect(result).toEqual({ + relativePath: "~/.claude/CLAUDE.md", + contents: "# Notes\n", + byteLength: 8, + truncated: false, + }); + }), + ); + it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( "rejects a FIFO without blocking on open", () => diff --git a/apps/server/src/workspace/WorkspaceFileSystem.ts b/apps/server/src/workspace/WorkspaceFileSystem.ts index 96fe5a0d234f..f8ee8aba3c8c 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.ts @@ -221,22 +221,23 @@ export const make = Effect.gen(function* () { /** * Workspace-relative paths must stay inside the root, symlinks included. An - * absolute path reads a host file in place, such as a report an agent wrote to a - * temp directory; it gets no root check. + * absolute or `~/` path reads a host file in place, such as a report an agent + * wrote to a temp directory; it gets no root check. */ const resolveReadTarget = Effect.fn("WorkspaceFileSystem.resolveReadTarget")(function* ( input: ProjectReadFileInput, ) { const requestedPath = input.relativePath.trim(); - if (path.isAbsolute(requestedPath)) { + const hostPath = expandHomePath(requestedPath, yield* HostProcess.HomeDirectory); + if (path.isAbsolute(hostPath)) { const realTargetPath = yield* Effect.tryPromise({ - try: () => NodeFSP.realpath(requestedPath), + try: () => NodeFSP.realpath(hostPath), catch: (cause) => new WorkspaceFileSystemOperationError({ workspaceRoot: input.cwd, relativePath: input.relativePath, - resolvedPath: requestedPath, - operationPath: requestedPath, + resolvedPath: hostPath, + operationPath: hostPath, operation: "realpath-target", cause, }), diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 3adc9d9f9f3c..667d6ea1a456 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -11,7 +11,7 @@ import { extractMarkdownLinkHrefs, inlineCodeFilePathCandidate, } from "@t3tools/shared/markdownLinks"; -import { isAbsolutePath } from "@t3tools/shared/path"; +import { isHostPath } from "@t3tools/shared/path"; import { usePullRequestLinking } from "~/hooks/usePullRequestLinking"; import { AuthFilesystemReadScope, AuthOrchestrationOperateScope } from "@t3tools/contracts"; import { @@ -2579,7 +2579,7 @@ function useChatMarkdownState({ ) !== null; const panelPath = fileLinkMeta.workspaceRelativePath ?? - (!canPreviewMedia && isAbsolutePath(fileLinkMeta.filePath) ? fileLinkMeta.filePath : null); + (!canPreviewMedia && isHostPath(fileLinkMeta.filePath) ? fileLinkMeta.filePath : null); return ( crumb.path === currentPath); diff --git a/apps/web/src/components/files/FilePreviewPanel.test.ts b/apps/web/src/components/files/FilePreviewPanel.test.ts index 45304cb58639..95ab6387b3d8 100644 --- a/apps/web/src/components/files/FilePreviewPanel.test.ts +++ b/apps/web/src/components/files/FilePreviewPanel.test.ts @@ -129,6 +129,13 @@ describe("shouldShowFileExplorer", () => { attachmentOpen: false, }), ).toBe(false); + expect( + shouldShowFileExplorer({ + relativePath: "~/.claude/CLAUDE.md", + explorerOpen: true, + attachmentOpen: false, + }), + ).toBe(false); expect( shouldShowFileExplorer({ relativePath: "report.pdf", diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 6a81ef7c4fee..f4d30515a1d2 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -57,7 +57,7 @@ import { resolveDiffThemeName } from "~/lib/diffRendering"; import { PREFERRED_HIGHLIGHTER } from "~/lib/syntaxHighlighting"; import { cn } from "~/lib/utils"; import type { ChatFileAttachment } from "~/types"; -import { isAbsolutePath } from "@t3tools/shared/path"; +import { isHostPath } from "@t3tools/shared/path"; import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; import { ScrollArea } from "~/components/ui/scroll-area"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; @@ -1006,7 +1006,7 @@ export default function FilePreviewPanel({ const isPdf = relativePath !== null && isPdfPreviewFile(relativePath); const isHtml = relativePath !== null && !isPdf && isBrowserPreviewFile(relativePath); const isHostFile = - attachment !== undefined || (relativePath !== null && isAbsolutePath(relativePath)); + attachment !== undefined || (relativePath !== null && isHostPath(relativePath)); const fileAccess = useFilesystemReadAccess(environmentId); const { canReadFiles } = fileAccess; const canWriteFiles = useEnvironmentScope(environmentId, AuthFilesystemWriteScope); diff --git a/apps/web/src/components/files/filePath.test.ts b/apps/web/src/components/files/filePath.test.ts index f9dc52658c79..f54a2a1ad2fc 100644 --- a/apps/web/src/components/files/filePath.test.ts +++ b/apps/web/src/components/files/filePath.test.ts @@ -47,6 +47,14 @@ describe("fileBreadcrumbs", () => { "\\\\server\\share\\report.md", ]); }); + + it("starts home-relative host paths at the home folder", () => { + expect(fileBreadcrumbs("t3code", "~/.claude/CLAUDE.md")).toEqual([ + { label: "~", path: "~", kind: "directory" }, + { label: ".claude", path: "~/.claude", kind: "directory" }, + { label: "CLAUDE.md", path: "~/.claude/CLAUDE.md", kind: "file" }, + ]); + }); }); describe("breadcrumbPathOf", () => { @@ -173,6 +181,7 @@ describe("isListableHostFolder", () => { ["/Users", true], ["C:\\", true], ["\\\\nas\\share", true], + ["~/.claude", true], // A UNC server is not a directory the server can list. ["\\\\nas", false], ["apps/web", false], diff --git a/apps/web/src/components/files/filePath.ts b/apps/web/src/components/files/filePath.ts index 96d6fc5e3d1c..041d1cdc5662 100644 --- a/apps/web/src/components/files/filePath.ts +++ b/apps/web/src/components/files/filePath.ts @@ -1,5 +1,5 @@ import type { ProjectEntry } from "@t3tools/contracts"; -import { isWindowsAbsolutePath, isAbsolutePath } from "@t3tools/shared/path"; +import { isWindowsAbsolutePath, isHostPath } from "@t3tools/shared/path"; export interface FileBreadcrumb { label: string; @@ -12,10 +12,14 @@ export interface FileBreadcrumbChild extends ProjectEntry { } export function fileBreadcrumbs(projectName: string, relativePath: string): FileBreadcrumb[] { - const hostPath = isAbsolutePath(relativePath); + const hostPath = isHostPath(relativePath); const separator = isWindowsAbsolutePath(relativePath) ? "\\" : "/"; const parts = relativePath.split(/[\\/]/).filter(Boolean); - const root = relativePath.startsWith("\\\\") ? "\\\\" : hostPath && separator === "/" ? "/" : ""; + const root = relativePath.startsWith("\\\\") + ? "\\\\" + : relativePath.startsWith("/") && separator === "/" + ? "/" + : ""; return [ ...(hostPath ? [] : [{ label: projectName, path: "", kind: "project" as const }]), ...parts.map((part, index) => { @@ -43,7 +47,7 @@ export function breadcrumbPathOf(relativePath: string): string { * server (`\\server`) is not one; its shares (`\\server\share`) are. */ export function isListableHostFolder(path: string): boolean { - return isAbsolutePath(path) && !/^\\\\[^\\/]+$/.test(path); + return isHostPath(path) && !/^\\\\[^\\/]+$/.test(path); } /** Index of `path`'s crumb among `trail`'s crumbs; -1 when the trail does not pass through it. */ diff --git a/apps/web/src/components/files/filePreviewMode.ts b/apps/web/src/components/files/filePreviewMode.ts index 3906ff553a5d..db39d06d2ee4 100644 --- a/apps/web/src/components/files/filePreviewMode.ts +++ b/apps/web/src/components/files/filePreviewMode.ts @@ -1,5 +1,5 @@ import type { AssetResource, ProjectReadFileError, ScopedThreadRef } from "@t3tools/contracts"; -import { isAbsolutePath, workspaceRelativeFilePath } from "@t3tools/shared/path"; +import { isHostPath, workspaceRelativeFilePath } from "@t3tools/shared/path"; export function resolveFilePreviewPath(path: string | null, cwd: string): string | null { if (path === null) return null; @@ -32,7 +32,7 @@ export function shouldShowFileExplorer(input: { readonly explorerOpen: boolean; readonly attachmentOpen: boolean; }): boolean { - if (input.attachmentOpen || (input.relativePath && isAbsolutePath(input.relativePath))) { + if (input.attachmentOpen || (input.relativePath && isHostPath(input.relativePath))) { return false; } return input.explorerOpen || input.relativePath === null; diff --git a/packages/shared/src/path.test.ts b/packages/shared/src/path.test.ts index f020e16770dd..4e0d82122b02 100644 --- a/packages/shared/src/path.test.ts +++ b/packages/shared/src/path.test.ts @@ -3,6 +3,7 @@ import { fileBasename, workspaceRelativeFilePath, isExplicitRelativePath, + isHostPath, isUncPath, isWindowsAbsolutePath, isWindowsDrivePath, @@ -42,6 +43,15 @@ describe("path helpers", () => { expect(isWindowsAbsolutePath("./repo")).toBe(false); }); + it("treats absolute and home-relative paths as host paths", () => { + expect(isHostPath("/home/me/notes.md")).toBe(true); + expect(isHostPath("C:\\Users\\me\\notes.md")).toBe(true); + expect(isHostPath("~/.claude/CLAUDE.md")).toBe(true); + expect(isHostPath("~\\.claude\\CLAUDE.md")).toBe(true); + expect(isHostPath("docs/notes.md")).toBe(false); + expect(isHostPath("~notes.md")).toBe(false); + }); + it("detects explicit relative paths", () => { expect(isExplicitRelativePath(".")).toBe(true); expect(isExplicitRelativePath("..")).toBe(true); diff --git a/packages/shared/src/path.ts b/packages/shared/src/path.ts index d887262c08ba..14d51ca0d5b4 100644 --- a/packages/shared/src/path.ts +++ b/packages/shared/src/path.ts @@ -105,8 +105,13 @@ export function isAbsolutePath(value: string): boolean { return value.startsWith("/") || isWindowsAbsolutePath(value); } +/** The server reads these in place, expanding `~` to its own home folder. */ +export function isHostPath(value: string): boolean { + return isAbsolutePath(value) || value.startsWith("~/") || value.startsWith("~\\"); +} + export function resolveWorkspaceFilePath(path: string, workspaceRoot: string): string { - if (isAbsolutePath(path) || path.startsWith("~/") || path.startsWith("~\\")) return path; + if (isHostPath(path)) return path; const base = workspaceRoot.replace(/[/\\]+$/, ""); return isWindowsAbsolutePath(workspaceRoot) ? `${base}\\${path.replaceAll("/", "\\")}` From 327d60a25ee91fc55c5df4e3a3c37487d2a0c48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 17 Oct 2026 13:58:12 -0300 Subject: [PATCH 2/3] fix: ~/ draft media resolves from the home folder, and ~\ stays a workspace path A ~/ media file opened from a host folder in a draft thread asked the server for /~/..., because draft-workspace-file only treated absolute paths as host files. It now expands ~ first, as media-file already does. isHostPath no longer accepts ~\. The client's link resolver only expands ~/, so a ~\ path classified as a host file reached the media previews unexpanded. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/assets/AssetAccess.test.ts | 30 ++++++++++++++++++++++ apps/server/src/assets/AssetAccess.ts | 5 ++-- packages/shared/src/path.test.ts | 2 +- packages/shared/src/path.ts | 9 ++++--- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/server/src/assets/AssetAccess.test.ts b/apps/server/src/assets/AssetAccess.test.ts index a327121fd619..bff0fc1a5c98 100644 --- a/apps/server/src/assets/AssetAccess.test.ts +++ b/apps/server/src/assets/AssetAccess.test.ts @@ -780,6 +780,36 @@ describe("AssetAccess", () => { }).pipe(Effect.provide(layerTest)), ); + it.effect("serves home-relative draft media files from the home folder", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-asset-draft-root-", + }); + const home = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-asset-draft-home-", + }); + const clipPath = path.join(home, "Downloads", "clip.mp4"); + yield* fileSystem.makeDirectory(path.dirname(clipPath), { recursive: true }); + yield* fileSystem.writeFileString(clipPath, "video"); + const canonicalClipPath = yield* fileSystem.realPath(clipPath); + + const result = yield* issueAssetUrl({ + resource: { _tag: "draft-workspace-file", cwd: root, path: "~/Downloads/clip.mp4" }, + workspaceRoot: root, + }).pipe(Effect.provideService(HostProcess.HomeDirectory, home)); + const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); + const token = suffix.slice(0, suffix.indexOf("/")); + + expect(yield* resolveAsset(token, "clip.mp4")).toMatchObject({ + kind: "file", + path: canonicalClipPath, + mimeType: "video/mp4", + }); + }).pipe(Effect.provide(layerTest)), + ); + it.effect("serves absolute draft media files exactly, wherever they live", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/assets/AssetAccess.ts b/apps/server/src/assets/AssetAccess.ts index 24224bdd7a77..0b67937b29fa 100644 --- a/apps/server/src/assets/AssetAccess.ts +++ b/apps/server/src/assets/AssetAccess.ts @@ -553,9 +553,10 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i } case "draft-workspace-file": { const draftWorkspaceRoot = input.workspaceRoot ?? input.resource.cwd; - if (path.isAbsolute(input.resource.path)) { + const hostPath = expandHomePath(input.resource.path, yield* HostProcess.HomeDirectory); + if (path.isAbsolute(hostPath)) { const finalized = yield* finalizeAbsoluteMediaFileAsset({ - requestedPath: input.resource.path, + requestedPath: hostPath, resource: input.resource, expiresAt, }); diff --git a/packages/shared/src/path.test.ts b/packages/shared/src/path.test.ts index 4e0d82122b02..15a67f3d71b5 100644 --- a/packages/shared/src/path.test.ts +++ b/packages/shared/src/path.test.ts @@ -47,7 +47,7 @@ describe("path helpers", () => { expect(isHostPath("/home/me/notes.md")).toBe(true); expect(isHostPath("C:\\Users\\me\\notes.md")).toBe(true); expect(isHostPath("~/.claude/CLAUDE.md")).toBe(true); - expect(isHostPath("~\\.claude\\CLAUDE.md")).toBe(true); + expect(isHostPath("~\\.claude\\CLAUDE.md")).toBe(false); expect(isHostPath("docs/notes.md")).toBe(false); expect(isHostPath("~notes.md")).toBe(false); }); diff --git a/packages/shared/src/path.ts b/packages/shared/src/path.ts index 14d51ca0d5b4..5a805f3e3422 100644 --- a/packages/shared/src/path.ts +++ b/packages/shared/src/path.ts @@ -105,13 +105,16 @@ export function isAbsolutePath(value: string): boolean { return value.startsWith("/") || isWindowsAbsolutePath(value); } -/** The server reads these in place, expanding `~` to its own home folder. */ +/** + * The server reads these in place, expanding `~` to its own home folder. Only + * `~/` counts: the client's link resolver does not expand `~\`. + */ export function isHostPath(value: string): boolean { - return isAbsolutePath(value) || value.startsWith("~/") || value.startsWith("~\\"); + return isAbsolutePath(value) || value.startsWith("~/"); } export function resolveWorkspaceFilePath(path: string, workspaceRoot: string): string { - if (isHostPath(path)) return path; + if (isAbsolutePath(path) || path.startsWith("~/") || path.startsWith("~\\")) return path; const base = workspaceRoot.replace(/[/\\]+$/, ""); return isWindowsAbsolutePath(workspaceRoot) ? `${base}\\${path.replaceAll("/", "\\")}` From 33a637b25b54613c8122f555347e12a78fb459dd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 06:57:05 +0000 Subject: [PATCH 3/3] fix(files): keep home-relative previews on the destination host --- apps/server/src/assets/AssetAccess.ts | 4 +++- apps/server/src/workspace/WorkspaceFileSystem.test.ts | 3 ++- apps/server/src/workspace/WorkspaceFileSystem.ts | 4 +++- apps/web/src/components/files/filePath.test.ts | 4 ++-- apps/web/src/components/files/filePath.ts | 6 +----- packages/shared/src/path.test.ts | 10 ---------- packages/shared/src/path.ts | 5 +---- 7 files changed, 12 insertions(+), 24 deletions(-) diff --git a/apps/server/src/assets/AssetAccess.ts b/apps/server/src/assets/AssetAccess.ts index 0b67937b29fa..3d8152d9ad41 100644 --- a/apps/server/src/assets/AssetAccess.ts +++ b/apps/server/src/assets/AssetAccess.ts @@ -553,7 +553,9 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i } case "draft-workspace-file": { const draftWorkspaceRoot = input.workspaceRoot ?? input.resource.cwd; - const hostPath = expandHomePath(input.resource.path, yield* HostProcess.HomeDirectory); + const hostPath = input.resource.path.startsWith("~/") + ? expandHomePath(input.resource.path, yield* HostProcess.HomeDirectory) + : input.resource.path; if (path.isAbsolute(hostPath)) { const finalized = yield* finalizeAbsoluteMediaFileAsset({ requestedPath: hostPath, diff --git a/apps/server/src/workspace/WorkspaceFileSystem.test.ts b/apps/server/src/workspace/WorkspaceFileSystem.test.ts index 4ec082f843ca..6b2c989e57b6 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.test.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.test.ts @@ -201,12 +201,13 @@ it.layer(layerTest, { excludeTestServices: true })("WorkspaceFileSystemLive", (i }), ); - it.effect("reads host files in the home folder by ~/ path", () => + it.effect("reads ~/ paths from the server home outside the workspace", () => Effect.gen(function* () { const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; const cwd = yield* makeTempDir; const home = yield* makeTempDir; yield* writeTextFile(home, ".claude/CLAUDE.md", "# Notes\n"); + yield* writeTextFile(cwd, "~/.claude/CLAUDE.md", "Workspace decoy\n"); const result = yield* workspaceFileSystem .readFile({ cwd, relativePath: "~/.claude/CLAUDE.md" }) diff --git a/apps/server/src/workspace/WorkspaceFileSystem.ts b/apps/server/src/workspace/WorkspaceFileSystem.ts index f8ee8aba3c8c..d5ae5dcf6dde 100644 --- a/apps/server/src/workspace/WorkspaceFileSystem.ts +++ b/apps/server/src/workspace/WorkspaceFileSystem.ts @@ -228,7 +228,9 @@ export const make = Effect.gen(function* () { input: ProjectReadFileInput, ) { const requestedPath = input.relativePath.trim(); - const hostPath = expandHomePath(requestedPath, yield* HostProcess.HomeDirectory); + const hostPath = requestedPath.startsWith("~/") + ? expandHomePath(requestedPath, yield* HostProcess.HomeDirectory) + : requestedPath; if (path.isAbsolute(hostPath)) { const realTargetPath = yield* Effect.tryPromise({ try: () => NodeFSP.realpath(hostPath), diff --git a/apps/web/src/components/files/filePath.test.ts b/apps/web/src/components/files/filePath.test.ts index f54a2a1ad2fc..9dc20199aab1 100644 --- a/apps/web/src/components/files/filePath.test.ts +++ b/apps/web/src/components/files/filePath.test.ts @@ -48,12 +48,13 @@ describe("fileBreadcrumbs", () => { ]); }); - it("starts home-relative host paths at the home folder", () => { + it("builds home-relative breadcrumbs with navigable child folders", () => { expect(fileBreadcrumbs("t3code", "~/.claude/CLAUDE.md")).toEqual([ { label: "~", path: "~", kind: "directory" }, { label: ".claude", path: "~/.claude", kind: "directory" }, { label: "CLAUDE.md", path: "~/.claude/CLAUDE.md", kind: "file" }, ]); + expect(isListableHostFolder("~/.claude")).toBe(true); }); }); @@ -181,7 +182,6 @@ describe("isListableHostFolder", () => { ["/Users", true], ["C:\\", true], ["\\\\nas\\share", true], - ["~/.claude", true], // A UNC server is not a directory the server can list. ["\\\\nas", false], ["apps/web", false], diff --git a/apps/web/src/components/files/filePath.ts b/apps/web/src/components/files/filePath.ts index 041d1cdc5662..9891cef28fab 100644 --- a/apps/web/src/components/files/filePath.ts +++ b/apps/web/src/components/files/filePath.ts @@ -15,11 +15,7 @@ export function fileBreadcrumbs(projectName: string, relativePath: string): File const hostPath = isHostPath(relativePath); const separator = isWindowsAbsolutePath(relativePath) ? "\\" : "/"; const parts = relativePath.split(/[\\/]/).filter(Boolean); - const root = relativePath.startsWith("\\\\") - ? "\\\\" - : relativePath.startsWith("/") && separator === "/" - ? "/" - : ""; + const root = relativePath.startsWith("\\\\") ? "\\\\" : relativePath.startsWith("/") ? "/" : ""; return [ ...(hostPath ? [] : [{ label: projectName, path: "", kind: "project" as const }]), ...parts.map((part, index) => { diff --git a/packages/shared/src/path.test.ts b/packages/shared/src/path.test.ts index 15a67f3d71b5..f020e16770dd 100644 --- a/packages/shared/src/path.test.ts +++ b/packages/shared/src/path.test.ts @@ -3,7 +3,6 @@ import { fileBasename, workspaceRelativeFilePath, isExplicitRelativePath, - isHostPath, isUncPath, isWindowsAbsolutePath, isWindowsDrivePath, @@ -43,15 +42,6 @@ describe("path helpers", () => { expect(isWindowsAbsolutePath("./repo")).toBe(false); }); - it("treats absolute and home-relative paths as host paths", () => { - expect(isHostPath("/home/me/notes.md")).toBe(true); - expect(isHostPath("C:\\Users\\me\\notes.md")).toBe(true); - expect(isHostPath("~/.claude/CLAUDE.md")).toBe(true); - expect(isHostPath("~\\.claude\\CLAUDE.md")).toBe(false); - expect(isHostPath("docs/notes.md")).toBe(false); - expect(isHostPath("~notes.md")).toBe(false); - }); - it("detects explicit relative paths", () => { expect(isExplicitRelativePath(".")).toBe(true); expect(isExplicitRelativePath("..")).toBe(true); diff --git a/packages/shared/src/path.ts b/packages/shared/src/path.ts index 5a805f3e3422..62dd11d91296 100644 --- a/packages/shared/src/path.ts +++ b/packages/shared/src/path.ts @@ -105,10 +105,7 @@ export function isAbsolutePath(value: string): boolean { return value.startsWith("/") || isWindowsAbsolutePath(value); } -/** - * The server reads these in place, expanding `~` to its own home folder. Only - * `~/` counts: the client's link resolver does not expand `~\`. - */ +/** Home-relative host paths are expanded by the destination server. */ export function isHostPath(value: string): boolean { return isAbsolutePath(value) || value.startsWith("~/"); }