diff --git a/apps/code/src/renderer/features/sessions/components/session-update/FileMentionChip.tsx b/apps/code/src/renderer/features/sessions/components/session-update/FileMentionChip.tsx index 890aa6068..c6d350bdc 100644 --- a/apps/code/src/renderer/features/sessions/components/session-update/FileMentionChip.tsx +++ b/apps/code/src/renderer/features/sessions/components/session-update/FileMentionChip.tsx @@ -1,6 +1,6 @@ import { FileIcon } from "@components/ui/FileIcon"; import { usePanelLayoutStore } from "@features/panels"; -import { useCwd } from "@features/sidebar/hooks/useCwd"; +import { useDisplayRepoPath } from "@features/sessions/hooks/useDisplayRepoPath"; import { useTaskStore } from "@features/tasks/stores/taskStore"; import { useWorkspace } from "@features/workspace/hooks/useWorkspace"; import { Flex, Text } from "@radix-ui/themes"; @@ -33,7 +33,7 @@ export const FileMentionChip = memo(function FileMentionChip({ filePath, }: FileMentionChipProps) { const taskId = useTaskStore((s) => s.selectedTaskId); - const repoPath = useCwd(taskId ?? ""); + const repoPath = useDisplayRepoPath(taskId ?? undefined); const workspace = useWorkspace(taskId ?? undefined); const openFileInSplit = usePanelLayoutStore((s) => s.openFileInSplit); diff --git a/apps/code/src/renderer/features/sessions/hooks/useDisplayRepoPath.test.ts b/apps/code/src/renderer/features/sessions/hooks/useDisplayRepoPath.test.ts new file mode 100644 index 000000000..48ac87791 --- /dev/null +++ b/apps/code/src/renderer/features/sessions/hooks/useDisplayRepoPath.test.ts @@ -0,0 +1,74 @@ +import { renderHook } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockUseCwd = vi.hoisted(() => vi.fn((): string | undefined => undefined)); +const mockUseTasks = vi.hoisted(() => + vi.fn((): { data: Array<{ id: string; repository?: string | null }> } => ({ + data: [], + })), +); + +vi.mock("@features/sidebar/hooks/useCwd", () => ({ useCwd: mockUseCwd })); +vi.mock("@features/tasks/hooks/useTasks", () => ({ useTasks: mockUseTasks })); + +import { useDisplayRepoPath } from "./useDisplayRepoPath"; + +describe("useDisplayRepoPath", () => { + beforeEach(() => { + mockUseCwd.mockReturnValue(undefined); + mockUseTasks.mockReturnValue({ data: [] }); + }); + + it("returns local cwd when available (local task)", () => { + mockUseCwd.mockReturnValue("/Users/me/code/posthog"); + const { result } = renderHook(() => useDisplayRepoPath("task-1")); + expect(result.current).toBe("/Users/me/code/posthog"); + }); + + it("derives remote workspace path from task.repository when cwd is missing", () => { + mockUseTasks.mockReturnValue({ + data: [{ id: "task-1", repository: "posthog/posthog.com" }], + }); + const { result } = renderHook(() => useDisplayRepoPath("task-1")); + expect(result.current).toBe("/tmp/workspace/repos/posthog/posthog.com"); + }); + + it("prefers local cwd over derived remote path when both could resolve", () => { + mockUseCwd.mockReturnValue("/Users/me/code/posthog"); + mockUseTasks.mockReturnValue({ + data: [{ id: "task-1", repository: "posthog/posthog.com" }], + }); + const { result } = renderHook(() => useDisplayRepoPath("task-1")); + expect(result.current).toBe("/Users/me/code/posthog"); + }); + + it("returns undefined when task has no repository", () => { + mockUseTasks.mockReturnValue({ data: [{ id: "task-1" }] }); + const { result } = renderHook(() => useDisplayRepoPath("task-1")); + expect(result.current).toBeUndefined(); + }); + + it("returns undefined when repository is malformed", () => { + mockUseTasks.mockReturnValue({ + data: [{ id: "task-1", repository: "not-a-valid-repo-string" }], + }); + const { result } = renderHook(() => useDisplayRepoPath("task-1")); + expect(result.current).toBeUndefined(); + }); + + it("returns undefined when task is not found", () => { + mockUseTasks.mockReturnValue({ + data: [{ id: "other-task", repository: "posthog/posthog.com" }], + }); + const { result } = renderHook(() => useDisplayRepoPath("task-1")); + expect(result.current).toBeUndefined(); + }); + + it("returns undefined when taskId is undefined", () => { + mockUseTasks.mockReturnValue({ + data: [{ id: "task-1", repository: "posthog/posthog.com" }], + }); + const { result } = renderHook(() => useDisplayRepoPath(undefined)); + expect(result.current).toBeUndefined(); + }); +}); diff --git a/apps/code/src/renderer/features/sessions/hooks/useDisplayRepoPath.ts b/apps/code/src/renderer/features/sessions/hooks/useDisplayRepoPath.ts new file mode 100644 index 000000000..6bc4a71cc --- /dev/null +++ b/apps/code/src/renderer/features/sessions/hooks/useDisplayRepoPath.ts @@ -0,0 +1,27 @@ +import { useCwd } from "@features/sidebar/hooks/useCwd"; +import { useTasks } from "@features/tasks/hooks/useTasks"; +import { parseRepository } from "@utils/repository"; + +const REMOTE_WORKSPACE_PREFIX = "/tmp/workspace/repos"; + +/** + * Returns the repo root to strip when displaying file paths in tool calls. + * Cloud tasks have no local cwd, so we derive the conventional sandbox + * clone location from `task.repository` — otherwise chips would render + * the full `/tmp/workspace/repos///...` sandbox path. + */ +export function useDisplayRepoPath( + taskId: string | undefined, +): string | undefined { + const localCwd = useCwd(taskId ?? ""); + const { data: tasks = [] } = useTasks(); + + if (localCwd) return localCwd; + if (!taskId) return undefined; + + const task = tasks.find((t) => t.id === taskId); + const parsed = task?.repository ? parseRepository(task.repository) : null; + if (!parsed) return undefined; + + return `${REMOTE_WORKSPACE_PREFIX}/${parsed.organization}/${parsed.repoName}`; +}