Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3b0b3a8
Show onboarding before public backend auth gate
openhands-agent Jun 16, 2026
0ebd22a
Make backend setup the first onboarding step
openhands-agent Jun 16, 2026
b41d36a
Restore Cloud backend option in onboarding
openhands-agent Jun 16, 2026
ae212df
Make first-run backend onboarding calmer
openhands-agent Jun 16, 2026
dff8484
fix: update public onboarding e2e expectation
Jun 16, 2026
5e93038
fix: cover onboarding-first public auth e2e
Jun 16, 2026
90149a9
test: keep ProgressEvent polyfill through teardown
neubig Jun 17, 2026
7730ac3
chore: refresh PR checks after QA
neubig Jun 17, 2026
2c7b76f
Add lock-to-cloud backend setup mode
openhands-agent Jun 16, 2026
04f77e0
Hide skip on locked Cloud backend onboarding
openhands-agent Jun 16, 2026
0ae3841
Remove add-backend onboarding subtitle
openhands-agent Jun 16, 2026
608e719
Skip healthy backend onboarding step
openhands-agent Jun 16, 2026
04958f3
fix: support skipped backend step in onboarding e2e
Jun 16, 2026
7ccb256
chore: Remove PR-only artifacts
Jun 17, 2026
bec461d
Merge remote-tracking branch 'origin/main' into fix-public-onboarding
neubig Jun 18, 2026
f874bff
Merge remote-tracking branch 'origin/fix-public-onboarding' into lock…
neubig Jun 18, 2026
ab405ad
Merge branch 'main' into fix-public-onboarding
hieptl Jun 18, 2026
ad85ca0
Merge branch 'main' into lock-to-cloud-backend
hieptl Jun 18, 2026
8390971
fix: address onboarding review nits
neubig Jun 18, 2026
ab9c102
Merge remote-tracking branch 'origin/fix-public-onboarding' into lock…
neubig Jun 18, 2026
f88db64
fix: show onboarding for locked cloud first run
neubig Jun 18, 2026
5bb8049
ci: support stacked mock llm runs
neubig Jun 18, 2026
0da388a
test: assert scoped shell background
neubig Jun 18, 2026
6e7bac6
Merge remote-tracking branch 'origin/fix-public-onboarding' into lock…
neubig Jun 18, 2026
7ff697e
Merge remote-tracking branch 'origin/main' into lock-to-cloud-backend
openhands-agent Jun 18, 2026
4b3118f
fix: resolve merge conflicts with main (fix-public-onboarding stacking)
openhands-agent Jun 18, 2026
addda40
fix: remove unused isLockedToCloud export
openhands-agent Jun 18, 2026
682e853
fix: show onboarding first in locked-cloud mode when a session key is…
Jun 19, 2026
a8fa110
fix: locked-cloud onboarding layout + restore CI test mock
Jun 19, 2026
572f13e
test: add getLockedCloudHost to agent-server-config test mocks
Jun 19, 2026
7d6cb4b
Enhance conversation sidebar with pinned section and grouped organiza…
FraterCCCLXIII Jun 19, 2026
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: 0 additions & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ on:
tags:
- "v*"
pull_request:
branches: [main]
workflow_dispatch:
inputs:
agent_server_image:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mock-llm-docker-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
SHOULD_RUN=false
while IFS= read -r file; do
case "$file" in
src/*|public/*|scripts/*|bin/*|config/*|docker/*|tests/e2e/mock-llm/*|tests/e2e/support/*|package.json|package-lock.json|vite.config.ts|tsconfig.json|react-router.config.ts|playwright.mock-llm.config.ts|playwright.mock-llm-docker.config.ts|tailwind.config.js|hero.ts|.github/workflows/mock-llm-docker-e2e.yml)
src/*|public/*|scripts/*|bin/*|config/*|docker/*|tests/e2e/mock-llm/*|tests/e2e/support/*|package.json|package-lock.json|vite.config.ts|tsconfig.json|react-router.config.ts|playwright.mock-llm.config.ts|playwright.mock-llm-docker.config.ts|tailwind.config.js|hero.ts|.github/workflows/docker.yml|.github/workflows/mock-llm-docker-e2e.yml)
SHOULD_RUN=true
break
;;
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/mock-llm-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ jobs:
needs: detect-pr-changes
if: github.event_name != 'pull_request' || needs.detect-pr-changes.outputs.should_run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
# Full-suite runs can spend several minutes on dependency, browser, uv,
# and frontend setup before Playwright starts. Keep Playwright's own
# 10-minute test deadline below, but give the job enough wall-clock room
# for setup plus reporting so GitHub does not terminate it mid-suite.
timeout-minutes: 30

env:
MOCK_LLM_REPORT_PATH: mock-llm-report.md
Expand Down
42 changes: 42 additions & 0 deletions __tests__/api/agent-server-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getAgentServerFormDefaults,
getAgentServerSessionApiKey,
getAgentServerWorkingDir,
getLockedCloudHost,
isAuthRequired,
isAuthRequiredAndMissing,
} from "#/api/agent-server-config";
Expand All @@ -24,6 +25,8 @@ afterEach(() => {
vi.unstubAllEnvs();
delete (window as unknown as Record<string, unknown>)
.__AGENT_CANVAS_SESSION_API_KEY__;
delete (window as unknown as Record<string, unknown>)
.__AGENT_CANVAS_LOCK_TO_CLOUD__;
Object.defineProperty(window, "location", {
configurable: true,
value: ORIGINAL_LOCATION,
Expand Down Expand Up @@ -75,6 +78,45 @@ describe("agent server config", () => {
});
});

describe("getLockedCloudHost", () => {
function setInjectedCloudHost(value: unknown) {
(
window as unknown as Record<string, unknown>
).__AGENT_CANVAS_LOCK_TO_CLOUD__ = value;
}

it("returns null when no Cloud lock is configured", () => {
expect(getLockedCloudHost()).toBeNull();
});

it("uses VITE_LOCK_TO_CLOUD when it is provided", () => {
vi.stubEnv("VITE_LOCK_TO_CLOUD", "https://cloud.example.com/");
setInjectedCloudHost("https://runtime.example.com");

expect(getLockedCloudHost()).toBe("https://cloud.example.com");
});

it("falls back to the runtime-injected Cloud URL", () => {
setInjectedCloudHost("https://runtime.example.com/");

expect(getLockedCloudHost()).toBe("https://runtime.example.com");
});

it("adds https:// to hostnames without an explicit scheme", () => {
setInjectedCloudHost("cloud.example.com/");

expect(getLockedCloudHost()).toBe("https://cloud.example.com");
});

it("ignores blank and non-string runtime values", () => {
setInjectedCloudHost(" ");
expect(getLockedCloudHost()).toBeNull();

setInjectedCloudHost(12345);
expect(getLockedCloudHost()).toBeNull();
});
});

describe("isAuthRequired", () => {
afterEach(() => {
delete (window as unknown as Record<string, unknown>)
Expand Down
1 change: 1 addition & 0 deletions __tests__/api/agent-server-conversation-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ vi.mock("#/api/agent-server-config", () => ({
getAgentServerHeaders: vi.fn(() => ({ "X-Session-API-Key": "test-api-key" })),
shouldLoadPublicSkills: vi.fn(() => true),
syncBakedSessionApiKey: vi.fn(),
getLockedCloudHost: vi.fn(() => null),
}));

vi.mock("#/api/settings-service/settings-service.api", () => ({
Expand Down
1 change: 1 addition & 0 deletions __tests__/api/use-create-conversation-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ vi.mock("#/api/agent-server-config", () => ({
),
shouldLoadPublicSkills: vi.fn(() => true),
syncBakedSessionApiKey: vi.fn(),
getLockedCloudHost: vi.fn(() => null),
}));

vi.mock("#/api/settings-service/settings-service.api", () => ({
Expand Down
72 changes: 42 additions & 30 deletions __tests__/components/backends/backend-form-modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ beforeEach(() => {

afterEach(() => {
window.localStorage.clear();
vi.unstubAllEnvs();
delete (window as unknown as Record<string, unknown>)
.__AGENT_CANVAS_LOCK_TO_CLOUD__;
__resetActiveStoreForTests();
});

Expand Down Expand Up @@ -128,14 +131,12 @@ describe("BackendFormModal – edit mode (BackendForm entry point)", () => {
renderWithProviders(
<TestSeed
onMount={(ctx) => {
backendId = ctx
.addBackend({
name: "Local Seed",
host: "http://localhost:9000",
apiKey: "sk-old",
kind: "local",
})
.id;
backendId = ctx.addBackend({
name: "Local Seed",
host: "http://localhost:9000",
apiKey: "sk-old",
kind: "local",
}).id;
}}
>
<BackendFormModal
Expand All @@ -153,9 +154,7 @@ describe("BackendFormModal – edit mode (BackendForm entry point)", () => {
);

await waitFor(() => {
expect(screen.getByTestId("edit-backend-name")).toHaveValue(
"Local Seed",
);
expect(screen.getByTestId("edit-backend-name")).toHaveValue("Local Seed");
});

const user = userEvent.setup();
Expand Down Expand Up @@ -187,14 +186,12 @@ describe("BackendFormModal – edit mode (BackendForm entry point)", () => {
renderWithProviders(
<TestSeed
onMount={(ctx) => {
backendId = ctx
.addBackend({
name: "Offline Server",
host: "http://localhost:9999",
apiKey: "sk-key",
kind: "local",
})
.id;
backendId = ctx.addBackend({
name: "Offline Server",
host: "http://localhost:9999",
apiKey: "sk-key",
kind: "local",
}).id;
}}
>
<BackendFormModal
Expand Down Expand Up @@ -227,9 +224,9 @@ describe("BackendFormModal – edit mode (BackendForm entry point)", () => {

await user.click(screen.getByTestId("edit-backend-submit"));

expect(
await screen.findByTestId("edit-backend-error"),
).toHaveTextContent("Connection refused");
expect(await screen.findByTestId("edit-backend-error")).toHaveTextContent(
"Connection refused",
);
});

it("preserves cloud kind when editing a cloud backend", async () => {
Expand All @@ -238,14 +235,12 @@ describe("BackendFormModal – edit mode (BackendForm entry point)", () => {
renderWithProviders(
<TestSeed
onMount={(ctx) => {
backendId = ctx
.addBackend({
name: "OpenHands Cloud",
host: "https://app.all-hands.dev",
apiKey: "sk-cloud",
kind: "cloud",
})
.id;
backendId = ctx.addBackend({
name: "OpenHands Cloud",
host: "https://app.all-hands.dev",
apiKey: "sk-cloud",
kind: "cloud",
}).id;
}}
>
<BackendFormModal
Expand Down Expand Up @@ -290,4 +285,21 @@ describe("BackendFormModal – edit mode (BackendForm entry point)", () => {
kind: "cloud",
});
});

it("locks add mode to Cloud login when VITE_LOCK_TO_CLOUD is set", () => {
vi.stubEnv("VITE_LOCK_TO_CLOUD", "https://cloud.example.com");

renderWithProviders(<BackendFormModal mode="add" onClose={vi.fn()} />);

expect(screen.getByTestId("add-backend-cloud-title")).toBeVisible();
expect(screen.getByTestId("add-backend-login-button")).toBeVisible();
expect(screen.queryByTestId("add-backend-host")).not.toBeInTheDocument();
expect(screen.queryByTestId("add-backend-api-key")).not.toBeInTheDocument();
expect(
screen.queryByTestId("add-backend-advanced-toggle"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("add-backend-cloud-host"),
).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ vi.mock("react-i18next", async () => {
CONVERSATION$UPDATED: "Updated",
COMMON$NO_REPOSITORY: "No repository",
CONVERSATION$ACP_AGENT_GENERIC: "ACP",
CONVERSATION_PANEL$PIN_CONVERSATION: "Pin conversation",
CONVERSATION_PANEL$UNPIN_CONVERSATION: "Unpin conversation",
};
return translations[key] || key;
},
Expand Down Expand Up @@ -799,4 +801,47 @@ describe("ConversationCard", () => {
).not.toBeInTheDocument();
});
});

it("calls onTogglePin when the pin button is clicked", async () => {
const onTogglePin = vi.fn();
const user = userEvent.setup();

renderWithProviders(
<ConversationCard
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
conversationId="conversation-1"
onDelete={vi.fn()}
onTogglePin={onTogglePin}
/>,
);

const card = screen.getByTestId("conversation-card");
await user.hover(card);
await user.click(
screen.getByTestId("conversation-pin-toggle-conversation-1"),
);
expect(onTogglePin).toHaveBeenCalledTimes(1);
});

it("keeps the pin icon visible without hover when alwaysShowPinIcon is set", () => {
renderWithProviders(
<ConversationCard
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
conversationId="conversation-1"
onDelete={vi.fn()}
onTogglePin={vi.fn()}
isPinned
alwaysShowPinIcon
/>,
);

expect(
screen.getByTestId("conversation-pin-toggle-conversation-1"),
).toBeVisible();
expect(screen.getByRole("time")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ function renderFilterMenu(
toggleShowRepoBranchMetadata: vi.fn(),
showLlmProfiles: false,
toggleShowLlmProfiles: vi.fn(),
showHoverMetadata: false,
toggleShowHoverMetadata: vi.fn(),
totalConversationsCount: 5,
onRequestDeleteAll: vi.fn(),
...overrides,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { describe, expect, it } from "vitest";
import {
applyGroupFolderOrder,
getGroupConversationPreview,
groupConversations,
GROUP_CONVERSATIONS_PREVIEW_LIMIT,
parseConversationTimeMs,
moveGroupFolderOrder,
resolvePinnedConversations,
sortConversationsByField,
} from "#/components/features/conversation-panel/conversation-panel-list-helpers";
import type { AppConversation } from "#/api/conversation-service/agent-server-conversation-service.types";
Expand Down Expand Up @@ -252,6 +255,20 @@ describe("conversation-panel-list-helpers", () => {
expect(GROUP_CONVERSATIONS_PREVIEW_LIMIT).toBe(5);
});

it("resolvePinnedConversations preserves pin order and drops missing ids", () => {
const conversations = [
{ ...base, id: "a", title: "A" },
{ ...base, id: "b", title: "B" },
{ ...base, id: "c", title: "C" },
] as AppConversation[];

expect(
resolvePinnedConversations(["c", "missing", "a"], conversations).map(
(conversation) => conversation.id,
),
).toEqual(["c", "a"]);
});

it("groups local conversations by selected_workspace, collapsing per-conversation worktree paths", () => {
// Two worktree-mode conversations launched against the same workspace must
// end up in a single group keyed off the user-selected workspace, not split
Expand Down Expand Up @@ -365,4 +382,36 @@ describe("conversation-panel-list-helpers", () => {
},
});
});

it("applies a persisted folder order and moves folders via drag-and-drop", () => {
const groups = [
{ id: "ws:/workspace/alpha", label: "alpha" },
{ id: "ws:/workspace/beta", label: "beta" },
{ id: "ws:/workspace/gamma", label: "gamma" },
];

expect(
applyGroupFolderOrder(groups, [
"ws:/workspace/gamma",
"ws:/workspace/alpha",
]).map((group) => group.id),
).toEqual([
"ws:/workspace/gamma",
"ws:/workspace/alpha",
"ws:/workspace/beta",
]);

expect(
moveGroupFolderOrder(
["ws:/workspace/gamma", "ws:/workspace/alpha"],
groups.map((group) => group.id),
"ws:/workspace/alpha",
"ws:/workspace/beta",
),
).toEqual([
"ws:/workspace/gamma",
"ws:/workspace/beta",
"ws:/workspace/alpha",
]);
});
});
Loading
Loading