From c44ff9306c76a1d4dd8f60970dc0dfce237cd694 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 1 May 2026 05:51:26 +0000 Subject: [PATCH] Default working dir to workspace/project Co-authored-by: openhands --- AGENTS.md | 1 + DEVELOPMENT.md | 2 +- __tests__/api/agent-server-config.test.ts | 6 +++ .../api/mock-conversation-handlers.test.ts | 4 +- __tests__/api/v1-conversation-service.test.ts | 4 +- __tests__/utils/get-git-path.test.ts | 47 ++++++++++--------- src/api/agent-server-config.ts | 3 +- .../v1-conversation-service.api.ts | 3 +- src/types/v1/core/base/action.ts | 2 +- src/utils/get-git-path.ts | 10 ++-- 10 files changed, 48 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 40a446d..387d999 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,7 @@ - `VITE_WORKING_DIR` for the default workspace path sent when starting conversations. - `VITE_WORKER_URLS` as a comma-separated list of browser worker URLs if you want the Browser tab to probe exposed app hosts. - `VITE_ENABLE_BROWSER_TOOLS=false` to omit `BrowserToolSet` from new conversation payloads. +- Default working-dir fallback is now the relative path `workspace/project` (exported as `DEFAULT_WORKING_DIR` from `src/api/agent-server-config.ts`); git-path heuristics and the default PLAN preview path should reuse that constant instead of hardcoding `/workspace/project`. - The UI keeps most OpenHands routes/layout intact, but SaaS/org/billing/integration behavior is intentionally hidden or stubbed via the fabricated OSS config because there is no separate app backend. - Verification command: `npm run typecheck && npm run build`. - GitHub automation now includes `.github/workflows/ci.yml` for `npm ci`, `npm test`, and `npm run build`, plus `.github/dependabot.yml` with weekly npm/github-actions updates gated by a 7-day cooldown. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1b589ad..baa9910 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -76,7 +76,7 @@ You can create a `.env` file in the project directory with these variables based | `VITE_BACKEND_BASE_URL` | Full base URL for the agent server used by direct browser requests | current browser origin | | `VITE_BACKEND_HOST` | Backend host used by the Vite dev proxy | `127.0.0.1:8000` | | `VITE_SESSION_API_KEY` | Optional `X-Session-API-Key` header value for authenticated agent_server instances | - | -| `VITE_WORKING_DIR` | Workspace path sent when starting new conversations | `/workspace/project` | +| `VITE_WORKING_DIR` | Workspace path sent when starting new conversations | `workspace/project` | | `VITE_WORKER_URLS` | Optional comma-separated worker/app URLs for the Browser tab | - | | `VITE_ENABLE_BROWSER_TOOLS` | Set to `false` to omit `BrowserToolSet` from new conversation payloads | `true` | | `VITE_MOCK_API` | Enable/disable API mocking with MSW | `false` | diff --git a/__tests__/api/agent-server-config.test.ts b/__tests__/api/agent-server-config.test.ts index 471c942..b582175 100644 --- a/__tests__/api/agent-server-config.test.ts +++ b/__tests__/api/agent-server-config.test.ts @@ -1,9 +1,11 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { AGENT_SERVER_CONFIG_STORAGE_KEY, + DEFAULT_WORKING_DIR, getAgentServerBaseUrl, getAgentServerFormDefaults, getAgentServerSessionApiKey, + getAgentServerWorkingDir, saveAgentServerConfig, } from "#/api/agent-server-config"; @@ -57,6 +59,10 @@ describe("agent server config", () => { expect(getAgentServerSessionApiKey()).toBe("env-session-key"); }); + it("defaults the working dir to the relative workspace path", () => { + expect(getAgentServerWorkingDir()).toBe(DEFAULT_WORKING_DIR); + }); + it("lets saved interface settings override environment defaults", () => { vi.stubEnv("VITE_BACKEND_BASE_URL", "https://env-agent.example.com"); vi.stubEnv("VITE_SESSION_API_KEY", "env-session-key"); diff --git a/__tests__/api/mock-conversation-handlers.test.ts b/__tests__/api/mock-conversation-handlers.test.ts index 0439ac4..ae29355 100644 --- a/__tests__/api/mock-conversation-handlers.test.ts +++ b/__tests__/api/mock-conversation-handlers.test.ts @@ -11,7 +11,7 @@ describe("mock conversation handlers", () => { expect(conversation?.id).toBe("1"); expect(conversation?.title).toBe("My New Project"); expect(conversation?.conversation_url).toContain("/api/conversations/1"); - expect(conversation?.workspace?.working_dir).toBe("/workspace/project"); + expect(conversation?.workspace?.working_dir).toBe("workspace/project"); }); it("returns adapted conversation pages for search", async () => { @@ -26,7 +26,7 @@ describe("mock conversation handlers", () => { const changes = await V1GitService.getGitChanges( "http://localhost:3000/api/conversations/1", null, - "/workspace/project", + "workspace/project", ); expect(changes).toEqual([]); diff --git a/__tests__/api/v1-conversation-service.test.ts b/__tests__/api/v1-conversation-service.test.ts index 045c928..0a66e1b 100644 --- a/__tests__/api/v1-conversation-service.test.ts +++ b/__tests__/api/v1-conversation-service.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; +import { DEFAULT_WORKING_DIR } from "#/api/agent-server-config"; import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api"; const { @@ -22,6 +23,7 @@ vi.mock("#/api/typescript-client", () => ({ })); vi.mock("#/api/agent-server-config", () => ({ + DEFAULT_WORKING_DIR: "workspace/project", getAgentServerBaseUrl: vi.fn(() => "http://localhost:54928"), getAgentServerSessionApiKey: vi.fn(() => "test-api-key"), getAgentServerWorkingDir: vi.fn(() => "/workspace/project/agent-server-gui"), @@ -58,7 +60,7 @@ describe("V1ConversationService", () => { expect(mockHttpGet).toHaveBeenCalledWith( "/api/file/download", expect.objectContaining({ - params: { path: "/workspace/project/.agents_tmp/PLAN.md" }, + params: { path: `${DEFAULT_WORKING_DIR}/.agents_tmp/PLAN.md` }, responseType: "arrayBuffer", }), ); diff --git a/__tests__/utils/get-git-path.test.ts b/__tests__/utils/get-git-path.test.ts index 2410ec6..8f9f92f 100644 --- a/__tests__/utils/get-git-path.test.ts +++ b/__tests__/utils/get-git-path.test.ts @@ -1,98 +1,99 @@ import { describe, it, expect } from "vitest"; +import { DEFAULT_WORKING_DIR } from "#/api/agent-server-config"; import { getGitPath } from "#/utils/get-git-path"; describe("getGitPath", () => { const conversationId = "abc123"; describe("without sandbox grouping (NO_GROUPING)", () => { - it("should return /workspace/project when no repository is selected", () => { - expect(getGitPath(conversationId, null, false)).toBe("/workspace/project"); + it("should return the default working dir when no repository is selected", () => { + expect(getGitPath(conversationId, null, false)).toBe(DEFAULT_WORKING_DIR); expect(getGitPath(conversationId, undefined, false)).toBe( - "/workspace/project", + DEFAULT_WORKING_DIR, ); }); it("should handle standard owner/repo format (GitHub)", () => { expect(getGitPath(conversationId, "OpenHands/OpenHands", false)).toBe( - "/workspace/project/OpenHands", + `${DEFAULT_WORKING_DIR}/OpenHands`, ); expect(getGitPath(conversationId, "facebook/react", false)).toBe( - "/workspace/project/react", + `${DEFAULT_WORKING_DIR}/react`, ); }); it("should handle nested group paths (GitLab)", () => { expect( getGitPath(conversationId, "modernhealth/frontend-guild/pan", false), - ).toBe("/workspace/project/pan"); + ).toBe(`${DEFAULT_WORKING_DIR}/pan`); expect(getGitPath(conversationId, "group/subgroup/repo", false)).toBe( - "/workspace/project/repo", + `${DEFAULT_WORKING_DIR}/repo`, ); expect(getGitPath(conversationId, "a/b/c/d/repo", false)).toBe( - "/workspace/project/repo", + `${DEFAULT_WORKING_DIR}/repo`, ); }); it("should handle single segment paths", () => { expect(getGitPath(conversationId, "repo", false)).toBe( - "/workspace/project/repo", + `${DEFAULT_WORKING_DIR}/repo`, ); }); it("should handle empty string", () => { - expect(getGitPath(conversationId, "", false)).toBe("/workspace/project"); + expect(getGitPath(conversationId, "", false)).toBe(DEFAULT_WORKING_DIR); }); }); describe("with sandbox grouping enabled", () => { - it("should return /workspace/project/{conversationId} when no repository is selected", () => { + it("should return the grouped default working dir when no repository is selected", () => { expect(getGitPath(conversationId, null, true)).toBe( - `/workspace/project/${conversationId}`, + `${DEFAULT_WORKING_DIR}/${conversationId}`, ); expect(getGitPath(conversationId, undefined, true)).toBe( - `/workspace/project/${conversationId}`, + `${DEFAULT_WORKING_DIR}/${conversationId}`, ); }); it("should handle standard owner/repo format (GitHub)", () => { expect(getGitPath(conversationId, "OpenHands/OpenHands", true)).toBe( - `/workspace/project/${conversationId}/OpenHands`, + `${DEFAULT_WORKING_DIR}/${conversationId}/OpenHands`, ); expect(getGitPath(conversationId, "facebook/react", true)).toBe( - `/workspace/project/${conversationId}/react`, + `${DEFAULT_WORKING_DIR}/${conversationId}/react`, ); }); it("should handle nested group paths (GitLab)", () => { expect( getGitPath(conversationId, "modernhealth/frontend-guild/pan", true), - ).toBe(`/workspace/project/${conversationId}/pan`); + ).toBe(`${DEFAULT_WORKING_DIR}/${conversationId}/pan`); expect(getGitPath(conversationId, "group/subgroup/repo", true)).toBe( - `/workspace/project/${conversationId}/repo`, + `${DEFAULT_WORKING_DIR}/${conversationId}/repo`, ); expect(getGitPath(conversationId, "a/b/c/d/repo", true)).toBe( - `/workspace/project/${conversationId}/repo`, + `${DEFAULT_WORKING_DIR}/${conversationId}/repo`, ); }); it("should handle single segment paths", () => { expect(getGitPath(conversationId, "repo", true)).toBe( - `/workspace/project/${conversationId}/repo`, + `${DEFAULT_WORKING_DIR}/${conversationId}/repo`, ); }); it("should handle empty string", () => { expect(getGitPath(conversationId, "", true)).toBe( - `/workspace/project/${conversationId}`, + `${DEFAULT_WORKING_DIR}/${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, null)).toBe(DEFAULT_WORKING_DIR); expect(getGitPath(conversationId, "owner/repo")).toBe( - "/workspace/project/repo", + `${DEFAULT_WORKING_DIR}/repo`, ); }); }); @@ -111,7 +112,7 @@ describe("getGitPath", () => { it("ignores blank workspace paths and falls back to heuristics", () => { expect(getGitPath(conversationId, "OpenHands/software-agent-sdk", true, " ")).toBe( - `/workspace/project/${conversationId}/software-agent-sdk`, + `${DEFAULT_WORKING_DIR}/${conversationId}/software-agent-sdk`, ); }); }); diff --git a/src/api/agent-server-config.ts b/src/api/agent-server-config.ts index 2afc40c..a35b476 100644 --- a/src/api/agent-server-config.ts +++ b/src/api/agent-server-config.ts @@ -1,4 +1,5 @@ export const AGENT_SERVER_CONFIG_STORAGE_KEY = "openhands-agent-server-config"; +export const DEFAULT_WORKING_DIR = "workspace/project"; interface StoredAgentServerConfig { baseUrl?: string | null; @@ -153,7 +154,7 @@ export function getAgentServerWorkingDir(): string { const storedDir = readStoredConfig().workingDir?.trim(); if (storedDir) return storedDir; - return "/workspace/project"; + return DEFAULT_WORKING_DIR; } export function getConfiguredWorkerUrls(): string[] { diff --git a/src/api/conversation-service/v1-conversation-service.api.ts b/src/api/conversation-service/v1-conversation-service.api.ts index 976e8db..4f4b3d9 100644 --- a/src/api/conversation-service/v1-conversation-service.api.ts +++ b/src/api/conversation-service/v1-conversation-service.api.ts @@ -1,6 +1,7 @@ import { Provider } from "#/types/settings"; import { SuggestedTask } from "#/utils/types"; import { + DEFAULT_WORKING_DIR, getAgentServerBaseUrl, getAgentServerWorkingDir, } from "../agent-server-config"; @@ -208,7 +209,7 @@ class V1ConversationService { static async readConversationFile( _conversationId: string, - filePath: string = "/workspace/project/.agents_tmp/PLAN.md", + filePath: string = `${DEFAULT_WORKING_DIR}/.agents_tmp/PLAN.md`, ): Promise { return downloadTextFile(filePath); } diff --git a/src/types/v1/core/base/action.ts b/src/types/v1/core/base/action.ts index d2e0b9a..98c92e3 100644 --- a/src/types/v1/core/base/action.ts +++ b/src/types/v1/core/base/action.ts @@ -219,7 +219,7 @@ export interface PlanningFileEditorAction extends ActionBase<"PlanningFileEditor */ command: "view" | "create" | "str_replace" | "insert" | "undo_edit"; /** - * Absolute path to file (typically /workspace/project/PLAN.md). + * File path (typically workspace/project/PLAN.md). */ path: string; /** diff --git a/src/utils/get-git-path.ts b/src/utils/get-git-path.ts index 21876b1..3d02e76 100644 --- a/src/utils/get-git-path.ts +++ b/src/utils/get-git-path.ts @@ -1,3 +1,5 @@ +import { DEFAULT_WORKING_DIR } from "#/api/agent-server-config"; + /** * Get the git repository path for a conversation. * @@ -5,10 +7,10 @@ * prefer that over frontend heuristics. * * Otherwise, when sandbox grouping is enabled (strategy != NO_GROUPING), each - * conversation gets its own subdirectory: /workspace/project/{conversationId}[/{repoName}] + * conversation gets its own subdirectory: workspace/project/{conversationId}[/{repoName}] * * When sandbox grouping is disabled (NO_GROUPING), the path is simply: - * /workspace/project[/{repoName}] + * workspace/project[/{repoName}] */ export function getGitPath( conversationId: string, @@ -22,8 +24,8 @@ export function getGitPath( } const basePath = useSandboxGrouping - ? `/workspace/project/${conversationId}` - : "/workspace/project"; + ? `${DEFAULT_WORKING_DIR}/${conversationId}` + : DEFAULT_WORKING_DIR; if (!selectedRepository) { return basePath;