| author | |
| committer | |
| log | 55ec55b1fb2e65f2736ff8420ae0373210c1911e |
| tree | b6f67fca531af54133558764f08ac5b8946cebc4 |
| parent | 916ec94f93da686303f878d506a760efd13506c1 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>10 files changed, 83 insertions(+), 3 deletions(-)
apps/web/src/components/files/FilePreviewPanel.test.ts+22| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | } from "./fileCommentAnnotations"; | 7 | } from "./fileCommentAnnotations"; |
| 8 | import { | 8 | import { |
| 9 | isMarkdownPreviewFile, | 9 | isMarkdownPreviewFile, |
| 10 | resolveFilePreviewPath, | ||
| 10 | setMarkdownTaskChecked, | 11 | setMarkdownTaskChecked, |
| 11 | shouldShowFileExplorer, | 12 | shouldShowFileExplorer, |
| 12 | } from "./filePreviewMode"; | 13 | } from "./filePreviewMode"; |
| ... | @@ -120,3 +121,24 @@ describe("setMarkdownTaskChecked", () => { | ... | @@ -120,3 +121,24 @@ describe("setMarkdownTaskChecked", () => { |
| 120 | expect(setMarkdownTaskChecked(markdown, 200, true)).toBe(markdown); | 121 | expect(setMarkdownTaskChecked(markdown, 200, true)).toBe(markdown); |
| 121 | }); | 122 | }); |
| 122 | }); | 123 | }); |
| 124 | |||
| 125 | describe("resolveFilePreviewPath", () => { | ||
| 126 | it.each([ | ||
| 127 | ["/repo/project", null], | ||
| 128 | ["/repo/project/", null], | ||
| 129 | [".", null], | ||
| 130 | [null, null], | ||
| 131 | ["/repo/project/src", "/repo/project/src"], | ||
| 132 | ["/repo/project/src/main.ts", "/repo/project/src/main.ts"], | ||
| 133 | ["src/main.ts", "src/main.ts"], | ||
| 134 | ["/repo/project-other", "/repo/project-other"], | ||
| 135 | ])("opens %s in the appropriate workspace surface", (path, expected) => { | ||
| 136 | const relativePath = resolveFilePreviewPath(path, "/repo/project"); | ||
| 137 | expect(relativePath).toBe(expected); | ||
| 138 | if (expected === null) { | ||
| 139 | expect( | ||
| 140 | shouldShowFileExplorer({ relativePath, explorerOpen: false, attachmentOpen: false }), | ||
| 141 | ).toBe(true); | ||
| 142 | } | ||
| 143 | }); | ||
| 144 | }); |
apps/web/src/components/files/FilePreviewPanel.tsx+10-2| ... | @@ -81,6 +81,7 @@ import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; | ... | @@ -81,6 +81,7 @@ import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; |
| 81 | import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; | 81 | import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; |
| 82 | import { | 82 | import { |
| 83 | isMarkdownPreviewFile, | 83 | isMarkdownPreviewFile, |
| 84 | resolveFilePreviewPath, | ||
| 84 | setMarkdownTaskChecked, | 85 | setMarkdownTaskChecked, |
| 85 | shouldShowFileExplorer, | 86 | shouldShowFileExplorer, |
| 86 | } from "./filePreviewMode"; | 87 | } from "./filePreviewMode"; |
| ... | @@ -907,7 +908,7 @@ export default function FilePreviewPanel({ | ... | @@ -907,7 +908,7 @@ export default function FilePreviewPanel({ |
| 907 | environmentId, | 908 | environmentId, |
| 908 | cwd, | 909 | cwd, |
| 909 | projectName, | 910 | projectName, |
| 910 | relativePath, | 911 | relativePath: requestedPath, |
| 911 | attachment, | 912 | attachment, |
| 912 | threadRef, | 913 | threadRef, |
| 913 | composerDraftTarget, | 914 | composerDraftTarget, |
| ... | @@ -920,6 +921,8 @@ export default function FilePreviewPanel({ | ... | @@ -920,6 +921,8 @@ export default function FilePreviewPanel({ |
| 920 | selectedFilePending, | 921 | selectedFilePending, |
| 921 | workspaceMutationId, | 922 | workspaceMutationId, |
| 922 | }: FilePreviewPanelProps) { | 923 | }: FilePreviewPanelProps) { |
| 924 | const relativePath = | ||
| 925 | attachment === undefined ? resolveFilePreviewPath(requestedPath, cwd) : requestedPath; | ||
| 923 | const { resolvedTheme } = useTheme(); | 926 | const { resolvedTheme } = useTheme(); |
| 924 | const wordWrap = useClientSettings((settings) => settings.wordWrap); | 927 | const wordWrap = useClientSettings((settings) => settings.wordWrap); |
| 925 | const primaryEnvironmentId = usePrimaryEnvironmentId(); | 928 | const primaryEnvironmentId = usePrimaryEnvironmentId(); |
| ... | @@ -945,7 +948,12 @@ export default function FilePreviewPanel({ | ... | @@ -945,7 +948,12 @@ export default function FilePreviewPanel({ |
| 945 | // shown. The read still runs: a folder named `assets.png` is only knowable as a | 948 | // shown. The read still runs: a folder named `assets.png` is only knowable as a |
| 946 | // folder from the read failure, and the server stats before reading, so a folder | 949 | // folder from the read failure, and the server stats before reading, so a folder |
| 947 | // costs an open and a stat and returns no body. | 950 | // costs an open and a stat and returns no body. |
| 948 | const file = useProjectFileQuery(environmentId, cwd, relativePath, attachment === undefined); | 951 | const file = useProjectFileQuery( |
| 952 | environmentId, | ||
| 953 | cwd, | ||
| 954 | relativePath, | ||
| 955 | attachment === undefined && relativePath !== null, | ||
| 956 | ); | ||
| 949 | // A chat link cannot tell a folder from a file, so a folder arrives here as | 957 | // A chat link cannot tell a folder from a file, so a folder arrives here as |
| 950 | // a file surface and the read fails. Keep the breadcrumbs, drop the preview | 958 | // a file surface and the read fails. Keep the breadcrumbs, drop the preview |
| 951 | // pane, and let the tree fill the surface with the folder revealed. Mutation | 959 | // pane, and let the tree fill the surface with the folder revealed. Mutation |
apps/web/src/components/files/filePreviewMode.ts+7| ... | @@ -1,5 +1,12 @@ | ... | @@ -1,5 +1,12 @@ |
| 1 | import { workspaceRelativeFilePath } from "@t3tools/client-runtime/markdown-links"; | ||
| 1 | import { isAbsolutePath } from "~/terminal-links"; | 2 | import { isAbsolutePath } from "~/terminal-links"; |
| 2 | 3 | ||
| 4 | /** Resolve workspace links before choosing between the explorer and a file preview. */ | ||
| 5 | export function resolveFilePreviewPath(path: string | null, cwd: string): string | null { | ||
| 6 | if (path === null) return null; | ||
| 7 | return path === "." || workspaceRelativeFilePath(path, cwd) === "." ? null : path; | ||
| 8 | } | ||
| 9 | |||
| 3 | export const isMarkdownPreviewFile = (path: string): boolean => /\.(?:md|mdx)$/i.test(path); | 10 | export const isMarkdownPreviewFile = (path: string): boolean => /\.(?:md|mdx)$/i.test(path); |
| 4 | 11 | ||
| 5 | export function shouldShowFileExplorer(input: { | 12 | export function shouldShowFileExplorer(input: { |
apps/web/src/markdown-links.test.ts+8| ... | @@ -522,3 +522,11 @@ describe("directory paths with a trailing separator", () => { | ... | @@ -522,3 +522,11 @@ describe("directory paths with a trailing separator", () => { |
| 522 | expect(meta?.basename).not.toBe(""); | 522 | expect(meta?.basename).not.toBe(""); |
| 523 | }); | 523 | }); |
| 524 | }); | 524 | }); |
| 525 | |||
| 526 | it("routes the project-root code link to the workspace explorer", () => { | ||
| 527 | const cwd = "/Users/saphid/.t3/worktrees/ov2-standalone-20260918"; | ||
| 528 | expect(resolveInlineCodeFileLinkMeta(cwd, cwd)).toMatchObject({ | ||
| 529 | workspaceRelativePath: ".", | ||
| 530 | filePath: cwd, | ||
| 531 | }); | ||
| 532 | }); |
apps/web/src/rightPanelStore.test.ts+18| ... | @@ -447,6 +447,24 @@ describe("rightPanelStore", () => { | ... | @@ -447,6 +447,24 @@ describe("rightPanelStore", () => { |
| 447 | }); | 447 | }); |
| 448 | }); | 448 | }); |
| 449 | 449 | ||
| 450 | it("opens workspace-root links as the singleton files explorer", () => { | ||
| 451 | const store = useRightPanelStore.getState(); | ||
| 452 | store.openFile(refA, "README.md"); | ||
| 453 | store.openFile(refA, "."); | ||
| 454 | store.openFile(refA, "."); | ||
| 455 | const state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); | ||
| 456 | expect(state.activeSurfaceId).toBe("files"); | ||
| 457 | expect(state.surfaces.map((surface) => surface.id)).toEqual(["file:README.md", "files"]); | ||
| 458 | store.closeSurface(refA, "files"); | ||
| 459 | expect( | ||
| 460 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).activeSurfaceId, | ||
| 461 | ).toBe("file:README.md"); | ||
| 462 | store.openFile(refA, "."); | ||
| 463 | expect( | ||
| 464 | selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).activeSurfaceId, | ||
| 465 | ).toBe("files"); | ||
| 466 | }); | ||
| 467 | |||
| 450 | it("replaces the standalone explorer with peer file surfaces", () => { | 468 | it("replaces the standalone explorer with peer file surfaces", () => { |
| 451 | useRightPanelStore.getState().open(refA, "files"); | 469 | useRightPanelStore.getState().open(refA, "files"); |
| 452 | useRightPanelStore.getState().openFile(refA, "src/index.ts"); | 470 | useRightPanelStore.getState().openFile(refA, "src/index.ts"); |
apps/web/src/rightPanelStore.ts+3| ... | @@ -578,6 +578,9 @@ export const useRightPanelStore = create<RightPanelStoreState>()( | ... | @@ -578,6 +578,9 @@ export const useRightPanelStore = create<RightPanelStoreState>()( |
| 578 | openFile: (ref, requestedPath, line) => | 578 | openFile: (ref, requestedPath, line) => |
| 579 | set((state) => | 579 | set((state) => |
| 580 | userAction(state, scopedThreadKey(ref), (current) => { | 580 | userAction(state, scopedThreadKey(ref), (current) => { |
| 581 | if (requestedPath === ".") { | ||
| 582 | return upsertSurface(current, singletonSurface("files")); | ||
| 583 | } | ||
| 581 | // Workspace entry paths use '/', including on Windows. | 584 | // Workspace entry paths use '/', including on Windows. |
| 582 | const relativePath = /^[A-Za-z]:\/+$/.test(requestedPath) | 585 | const relativePath = /^[A-Za-z]:\/+$/.test(requestedPath) |
| 583 | ? requestedPath | 586 | ? requestedPath |
apps/web/src/workspaceBasenameLookup.test.ts+2| ... | @@ -15,6 +15,8 @@ describe("needsWorkspaceBasenameLookup", () => { | ... | @@ -15,6 +15,8 @@ describe("needsWorkspaceBasenameLookup", () => { |
| 15 | it("leaves anything with a directory alone", () => { | 15 | it("leaves anything with a directory alone", () => { |
| 16 | expect(needsWorkspaceBasenameLookup("apps/web/src/components/ChatView.tsx")).toBe(false); | 16 | expect(needsWorkspaceBasenameLookup("apps/web/src/components/ChatView.tsx")).toBe(false); |
| 17 | expect(needsWorkspaceBasenameLookup("apps\\web\\ChatView.tsx")).toBe(false); | 17 | expect(needsWorkspaceBasenameLookup("apps\\web\\ChatView.tsx")).toBe(false); |
| 18 | expect(needsWorkspaceBasenameLookup(".")).toBe(false); | ||
| 19 | expect(needsWorkspaceBasenameLookup("..")).toBe(false); | ||
| 18 | expect(needsWorkspaceBasenameLookup(" ")).toBe(false); | 20 | expect(needsWorkspaceBasenameLookup(" ")).toBe(false); |
| 19 | }); | 21 | }); |
| 20 | }); | 22 | }); |
apps/web/src/workspaceBasenameLookup.ts+7-1| ... | @@ -25,7 +25,13 @@ function basenameOfPath(path: string): string { | ... | @@ -25,7 +25,13 @@ function basenameOfPath(path: string): string { |
| 25 | 25 | ||
| 26 | export function needsWorkspaceBasenameLookup(relativePath: string): boolean { | 26 | export function needsWorkspaceBasenameLookup(relativePath: string): boolean { |
| 27 | const trimmed = relativePath.trim(); | 27 | const trimmed = relativePath.trim(); |
| 28 | return trimmed.length > 0 && !trimmed.includes("/") && !trimmed.includes("\\"); | 28 | return ( |
| 29 | trimmed !== "." && | ||
| 30 | trimmed !== ".." && | ||
| 31 | trimmed.length > 0 && | ||
| 32 | !trimmed.includes("/") && | ||
| 33 | !trimmed.includes("\\") | ||
| 34 | ); | ||
| 29 | } | 35 | } |
| 30 | 36 | ||
| 31 | export function pickWorkspaceBasenameMatch( | 37 | export function pickWorkspaceBasenameMatch( |
packages/client-runtime/src/markdownLinks.test.ts+5| ... | @@ -155,6 +155,11 @@ describe("fileBasename", () => { | ... | @@ -155,6 +155,11 @@ describe("fileBasename", () => { |
| 155 | 155 | ||
| 156 | describe("workspaceRelativeFilePath", () => { | 156 | describe("workspaceRelativeFilePath", () => { |
| 157 | it.each([ | 157 | it.each([ |
| 158 | ["/repo/project", "/repo/project", "."], | ||
| 159 | ["/repo/project/", "/repo/project/", "."], | ||
| 160 | ["/", "/", "."], | ||
| 161 | ["C:/USERS/mike/project", "c:/users/MIKE/project", "."], | ||
| 162 | ["C:/", "c:/", "."], | ||
| 158 | ["/repo/project/src/main.ts", "/repo/project", "src/main.ts"], | 163 | ["/repo/project/src/main.ts", "/repo/project", "src/main.ts"], |
| 159 | ["/repo/project/src/main.ts", "/repo/project/", "src/main.ts"], | 164 | ["/repo/project/src/main.ts", "/repo/project/", "src/main.ts"], |
| 160 | ["C:\\Users\\mike\\t3code\\apps\\web\\a.ts", "C:/Users/mike/t3code", "apps/web/a.ts"], | 165 | ["C:\\Users\\mike\\t3code\\apps\\web\\a.ts", "C:/Users/mike/t3code", "apps/web/a.ts"], |
packages/client-runtime/src/markdownLinks.ts+1| ... | @@ -340,6 +340,7 @@ export function workspaceRelativeFilePath( | ... | @@ -340,6 +340,7 @@ export function workspaceRelativeFilePath( |
| 340 | const caseInsensitive = isWindowsAbsolutePath(stripSlashPrefixedWindowsDrive(workspaceRoot)); | 340 | const caseInsensitive = isWindowsAbsolutePath(stripSlashPrefixedWindowsDrive(workspaceRoot)); |
| 341 | const pathForCompare = caseInsensitive ? normalizedPath.toLowerCase() : normalizedPath; | 341 | const pathForCompare = caseInsensitive ? normalizedPath.toLowerCase() : normalizedPath; |
| 342 | const rootForCompare = caseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot; | 342 | const rootForCompare = caseInsensitive ? normalizedRoot.toLowerCase() : normalizedRoot; |
| 343 | if (pathForCompare.replace(/\/+$/, "") === rootForCompare) return "."; | ||
| 343 | if (!pathForCompare.startsWith(`${rootForCompare}/`)) return null; | 344 | if (!pathForCompare.startsWith(`${rootForCompare}/`)) return null; |
| 344 | return normalizedPath.slice(normalizedRoot.length + 1); | 345 | return normalizedPath.slice(normalizedRoot.length + 1); |
| 345 | } | 346 | } |