Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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` |
Expand Down
6 changes: 6 additions & 0 deletions __tests__/api/agent-server-config.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions __tests__/api/mock-conversation-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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([]);
Expand Down
4 changes: 3 additions & 1 deletion __tests__/api/v1-conversation-service.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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"),
Expand Down Expand Up @@ -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",
}),
);
Expand Down
47 changes: 24 additions & 23 deletions __tests__/utils/get-git-path.test.ts
Original file line number Diff line number Diff line change
@@ -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`,
);
});
});
Expand All @@ -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`,
);
});
});
Expand Down
3 changes: 2 additions & 1 deletion src/api/agent-server-config.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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[] {
Expand Down
3 changes: 2 additions & 1 deletion src/api/conversation-service/v1-conversation-service.api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Provider } from "#/types/settings";
import { SuggestedTask } from "#/utils/types";
import {
DEFAULT_WORKING_DIR,
getAgentServerBaseUrl,
getAgentServerWorkingDir,
} from "../agent-server-config";
Expand Down Expand Up @@ -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<string> {
return downloadTextFile(filePath);
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/v1/core/base/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
10 changes: 6 additions & 4 deletions src/utils/get-git-path.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { DEFAULT_WORKING_DIR } from "#/api/agent-server-config";

/**
* Get the git repository path for a conversation.
*
* If the backend provides an explicit workspace path for the conversation,
* 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,
Expand All @@ -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;
Expand Down
Loading