diff --git a/frontend/__tests__/utils/get-git-path.test.ts b/frontend/__tests__/utils/get-git-path.test.ts index 2adfc232d4..a1f3512862 100644 --- a/frontend/__tests__/utils/get-git-path.test.ts +++ b/frontend/__tests__/utils/get-git-path.test.ts @@ -4,27 +4,96 @@ import { getGitPath } from "#/utils/get-git-path"; describe("getGitPath", () => { const conversationId = "abc123"; - it("should return /workspace/project/{conversationId} when no repository is selected", () => { - expect(getGitPath(conversationId, null)).toBe(`/workspace/project/${conversationId}`); - expect(getGitPath(conversationId, undefined)).toBe(`/workspace/project/${conversationId}`); + describe("without sandbox grouping (NO_GROUPING)", () => { + it("should return /workspace/project when no repository is selected", () => { + expect(getGitPath(conversationId, null, false)).toBe("/workspace/project"); + expect(getGitPath(conversationId, undefined, false)).toBe( + "/workspace/project", + ); + }); + + it("should handle standard owner/repo format (GitHub)", () => { + expect(getGitPath(conversationId, "OpenHands/OpenHands", false)).toBe( + "/workspace/project/OpenHands", + ); + expect(getGitPath(conversationId, "facebook/react", false)).toBe( + "/workspace/project/react", + ); + }); + + it("should handle nested group paths (GitLab)", () => { + expect( + getGitPath(conversationId, "modernhealth/frontend-guild/pan", false), + ).toBe("/workspace/project/pan"); + expect(getGitPath(conversationId, "group/subgroup/repo", false)).toBe( + "/workspace/project/repo", + ); + expect(getGitPath(conversationId, "a/b/c/d/repo", false)).toBe( + "/workspace/project/repo", + ); + }); + + it("should handle single segment paths", () => { + expect(getGitPath(conversationId, "repo", false)).toBe( + "/workspace/project/repo", + ); + }); + + it("should handle empty string", () => { + expect(getGitPath(conversationId, "", false)).toBe("/workspace/project"); + }); }); - it("should handle standard owner/repo format (GitHub)", () => { - expect(getGitPath(conversationId, "OpenHands/OpenHands")).toBe(`/workspace/project/${conversationId}/OpenHands`); - expect(getGitPath(conversationId, "facebook/react")).toBe(`/workspace/project/${conversationId}/react`); + describe("with sandbox grouping enabled", () => { + it("should return /workspace/project/{conversationId} when no repository is selected", () => { + expect(getGitPath(conversationId, null, true)).toBe( + `/workspace/project/${conversationId}`, + ); + expect(getGitPath(conversationId, undefined, true)).toBe( + `/workspace/project/${conversationId}`, + ); + }); + + it("should handle standard owner/repo format (GitHub)", () => { + expect(getGitPath(conversationId, "OpenHands/OpenHands", true)).toBe( + `/workspace/project/${conversationId}/OpenHands`, + ); + expect(getGitPath(conversationId, "facebook/react", true)).toBe( + `/workspace/project/${conversationId}/react`, + ); + }); + + it("should handle nested group paths (GitLab)", () => { + expect( + getGitPath(conversationId, "modernhealth/frontend-guild/pan", true), + ).toBe(`/workspace/project/${conversationId}/pan`); + expect(getGitPath(conversationId, "group/subgroup/repo", true)).toBe( + `/workspace/project/${conversationId}/repo`, + ); + expect(getGitPath(conversationId, "a/b/c/d/repo", true)).toBe( + `/workspace/project/${conversationId}/repo`, + ); + }); + + it("should handle single segment paths", () => { + expect(getGitPath(conversationId, "repo", true)).toBe( + `/workspace/project/${conversationId}/repo`, + ); + }); + + it("should handle empty string", () => { + expect(getGitPath(conversationId, "", true)).toBe( + `/workspace/project/${conversationId}`, + ); + }); }); - it("should handle nested group paths (GitLab)", () => { - expect(getGitPath(conversationId, "modernhealth/frontend-guild/pan")).toBe(`/workspace/project/${conversationId}/pan`); - expect(getGitPath(conversationId, "group/subgroup/repo")).toBe(`/workspace/project/${conversationId}/repo`); - expect(getGitPath(conversationId, "a/b/c/d/repo")).toBe(`/workspace/project/${conversationId}/repo`); - }); - - it("should handle single segment paths", () => { - expect(getGitPath(conversationId, "repo")).toBe(`/workspace/project/${conversationId}/repo`); - }); - - it("should handle empty string", () => { - expect(getGitPath(conversationId, "")).toBe(`/workspace/project/${conversationId}`); + describe("default behavior (useSandboxGrouping defaults to false)", () => { + it("should default to no sandbox grouping", () => { + expect(getGitPath(conversationId, null)).toBe("/workspace/project"); + expect(getGitPath(conversationId, "owner/repo")).toBe( + "/workspace/project/repo", + ); + }); }); }); diff --git a/frontend/src/hooks/query/use-unified-get-git-changes.ts b/frontend/src/hooks/query/use-unified-get-git-changes.ts index a1de3852f9..616665a07f 100644 --- a/frontend/src/hooks/query/use-unified-get-git-changes.ts +++ b/frontend/src/hooks/query/use-unified-get-git-changes.ts @@ -5,6 +5,7 @@ import V1GitService from "#/api/git-service/v1-git-service.api"; import { useConversationId } from "#/hooks/use-conversation-id"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready"; +import { useSettings } from "#/hooks/query/use-settings"; import { getGitPath } from "#/utils/get-git-path"; import type { GitChange } from "#/api/open-hands.types"; @@ -16,6 +17,7 @@ import type { GitChange } from "#/api/open-hands.types"; export const useUnifiedGetGitChanges = () => { const { conversationId } = useConversationId(); const { data: conversation } = useActiveConversation(); + const { data: settings } = useSettings(); const [orderedChanges, setOrderedChanges] = React.useState([]); const previousDataRef = React.useRef(null); const runtimeIsReady = useRuntimeIsReady(); @@ -25,10 +27,15 @@ export const useUnifiedGetGitChanges = () => { const sessionApiKey = conversation?.session_api_key; const selectedRepository = conversation?.selected_repository; - // Calculate git path based on selected repository + // Sandbox grouping is enabled when strategy is not NO_GROUPING + const useSandboxGrouping = + settings?.sandbox_grouping_strategy !== "NO_GROUPING" && + settings?.sandbox_grouping_strategy !== undefined; + + // Calculate git path based on selected repository and sandbox grouping strategy const gitPath = React.useMemo( - () => getGitPath(conversationId, selectedRepository), - [selectedRepository], + () => getGitPath(conversationId, selectedRepository, useSandboxGrouping), + [conversationId, selectedRepository, useSandboxGrouping], ); const result = useQuery({ diff --git a/frontend/src/hooks/query/use-unified-git-diff.ts b/frontend/src/hooks/query/use-unified-git-diff.ts index 26bca16fce..8705a70c76 100644 --- a/frontend/src/hooks/query/use-unified-git-diff.ts +++ b/frontend/src/hooks/query/use-unified-git-diff.ts @@ -4,6 +4,7 @@ import GitService from "#/api/git-service/git-service.api"; import V1GitService from "#/api/git-service/v1-git-service.api"; import { useConversationId } from "#/hooks/use-conversation-id"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; +import { useSettings } from "#/hooks/query/use-settings"; import { getGitPath } from "#/utils/get-git-path"; import type { GitChangeStatus } from "#/api/open-hands.types"; @@ -21,20 +22,36 @@ type UseUnifiedGitDiffConfig = { export const useUnifiedGitDiff = (config: UseUnifiedGitDiffConfig) => { const { conversationId } = useConversationId(); const { data: conversation } = useActiveConversation(); + const { data: settings } = useSettings(); const isV1Conversation = conversation?.conversation_version === "V1"; const conversationUrl = conversation?.url; const sessionApiKey = conversation?.session_api_key; const selectedRepository = conversation?.selected_repository; + // Sandbox grouping is enabled when strategy is not NO_GROUPING + const useSandboxGrouping = + settings?.sandbox_grouping_strategy !== "NO_GROUPING" && + settings?.sandbox_grouping_strategy !== undefined; + // For V1, we need to convert the relative file path to an absolute path // The diff endpoint expects: /workspace/project/RepoName/relative/path const absoluteFilePath = React.useMemo(() => { if (!isV1Conversation) return config.filePath; - const gitPath = getGitPath(conversationId, selectedRepository); + const gitPath = getGitPath( + conversationId, + selectedRepository, + useSandboxGrouping, + ); return `${gitPath}/${config.filePath}`; - }, [isV1Conversation, selectedRepository, config.filePath]); + }, [ + isV1Conversation, + conversationId, + selectedRepository, + useSandboxGrouping, + config.filePath, + ]); return useQuery({ queryKey: [ diff --git a/frontend/src/utils/get-git-path.ts b/frontend/src/utils/get-git-path.ts index 39292b819f..e55b0bb989 100644 --- a/frontend/src/utils/get-git-path.ts +++ b/frontend/src/utils/get-git-path.ts @@ -1,17 +1,29 @@ /** * Get the git repository path for a conversation - * If a repository is selected, returns /workspace/project/{repo-name} - * Otherwise, returns /workspace/project * + * When sandbox grouping is enabled (strategy != NO_GROUPING), each conversation + * gets its own subdirectory: /workspace/project/{conversationId}[/{repoName}] + * + * When sandbox grouping is disabled (NO_GROUPING), the path is simply: + * /workspace/project[/{repoName}] + * + * @param conversationId The conversation ID * @param selectedRepository The selected repository (e.g., "OpenHands/OpenHands", "owner/repo", or "group/subgroup/repo") + * @param useSandboxGrouping Whether sandbox grouping is enabled (strategy != NO_GROUPING) * @returns The git path to use */ export function getGitPath( conversationId: string, selectedRepository: string | null | undefined, + useSandboxGrouping: boolean = false, ): string { + // Base path depends on sandbox grouping strategy + const basePath = useSandboxGrouping + ? `/workspace/project/${conversationId}` + : "/workspace/project"; + if (!selectedRepository) { - return `/workspace/project/${conversationId}`; + return basePath; } // Extract the repository name from the path @@ -19,5 +31,5 @@ export function getGitPath( const parts = selectedRepository.split("/"); const repoName = parts[parts.length - 1]; - return `/workspace/project/${conversationId}/${repoName}`; + return `${basePath}/${repoName}`; }