Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ VITE_FRONTEND_PORT="3001" # Port to run the frontend application
VITE_USE_TLS="false" # Use HTTPS/WSS for proxied backend connections
VITE_INSECURE_SKIP_VERIFY="false" # Skip TLS certificate verification for proxied backend requests
# VITE_BASE_PATH="/canvas" # Build/serve the SPA under a subpath. Leave unset for root-local development.
# VITE_DISABLE_ONBOARDING="true" # Skip the first-run onboarding wizard. Runtime equivalent for prebuilt bundles: static-server.mjs --disable-onboarding (or env AGENT_CANVAS_DISABLE_ONBOARDING=true).

# Public PostHog client key compiled into source builds. Precompiled library
# consumers can instead pass apiKey/apiHost/uiHost to AgentServerUIProviders.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- `VITE_WORKING_DIR` for the default workspace path sent when starting conversations.
- `VITE_ENABLE_BROWSER_TOOLS=false` to omit `BrowserToolSet` from new conversation payloads.
- `VITE_BASE_PATH` for serving the SPA under a subpath such as `/canvas`; pair it with `scripts/static-server.mjs --base-path` at runtime.
- `VITE_DISABLE_ONBOARDING=true` to skip the first-run onboarding wizard (users land directly on the home screen; Cloud login is never suppressed); pair it with `scripts/static-server.mjs --disable-onboarding` or env `AGENT_CANVAS_DISABLE_ONBOARDING=true` at runtime for prebuilt bundles.
Comment thread
hieptl marked this conversation as resolved.
- Public skills are loaded from the `@openhands/extensions` npm package at build time via `SKILLS_CATALOG` (exported from `@openhands/extensions/skills`). The frontend's `SkillsService` maps catalog entries to `SkillInfo` objects and merges them with user/project skills fetched from the agent-server (with `load_public: false`). The agent-server no longer clones the extensions repo or uses `EXTENSIONS_REF` for public skills.
- 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`.
- Current Cloud behavior is implemented explicitly through the backend registry, Cloud service layer, and device authorization flow.
Expand Down
34 changes: 34 additions & 0 deletions __tests__/api/agent-server-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isAuthRequired,
isSameCloudHost,
isAuthRequiredAndMissing,
isOnboardingDisabled,
} from "#/api/agent-server-config";

const ORIGINAL_LOCATION = window.location;
Expand Down Expand Up @@ -247,6 +248,39 @@ describe("isAuthRequiredAndMissing", () => {
});
});

describe("isOnboardingDisabled", () => {
afterEach(() => {
Comment thread
hieptl marked this conversation as resolved.
Outdated
delete (window as unknown as Record<string, unknown>)
.__AGENT_CANVAS_DISABLE_ONBOARDING__;
});

it("returns false when neither env var nor window flag is set", () => {
expect(isOnboardingDisabled()).toBe(false);
});

it("returns true when VITE_DISABLE_ONBOARDING is 'true'", () => {
vi.stubEnv("VITE_DISABLE_ONBOARDING", "true");

expect(isOnboardingDisabled()).toBe(true);
});

it("returns true when window.__AGENT_CANVAS_DISABLE_ONBOARDING__ is set", () => {
(
window as unknown as Record<string, unknown>
).__AGENT_CANVAS_DISABLE_ONBOARDING__ = true;

expect(isOnboardingDisabled()).toBe(true);
});

it("returns false when window flag is a non-true value", () => {
(
window as unknown as Record<string, unknown>
).__AGENT_CANVAS_DISABLE_ONBOARDING__ = "true";

expect(isOnboardingDisabled()).toBe(false);
});
});

// Covers the published `agent-canvas` binary path: the prebuilt bundle has
// no VITE_SESSION_API_KEY baked in, but `scripts/static-server.mjs` injects
// the runtime key into `window.__AGENT_CANVAS_SESSION_API_KEY__`. Without
Expand Down
24 changes: 24 additions & 0 deletions __tests__/components/onboarding/onboarding-host.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,28 @@ describe("OnboardingHost", () => {
window.localStorage.getItem(ONBOARDING_COMPLETED_STORAGE_KEY),
).toBeNull();
});

it("does not mount the modal for a fresh Cloud user when onboarding is disabled for the deployment", () => {
vi.stubEnv("VITE_DISABLE_ONBOARDING", "true");
Comment thread
hieptl marked this conversation as resolved.
Outdated
seedCloudBackend();
vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
...DEFAULT_SETTINGS,
llm_api_key_set: true,
agent_settings: {
...DEFAULT_SETTINGS.agent_settings,
llm: { model: "openhands/minimax-m2.7", api_key: "stored" },
},
});

renderHost();

expect(
screen.queryByTestId("onboarding-modal-stub"),
).not.toBeInTheDocument();
// Suppression comes from the deployment flag, not a fake completion
// marker — nothing is written to localStorage.
expect(
window.localStorage.getItem(ONBOARDING_COMPLETED_STORAGE_KEY),
).toBeNull();
});
});
17 changes: 17 additions & 0 deletions __tests__/root.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,23 @@ describe("App root agent-server availability guard", () => {
).not.toBeInTheDocument();
});

it("skips first-run onboarding for a fresh install when onboarding is disabled for the deployment", async () => {
vi.stubEnv("VITE_DISABLE_ONBOARDING", "true");
server.use(
http.get("*/server_info", () =>
HttpResponse.json({ uptime: 0, idle_time: 0, version: "1.28.1" }),
),
);

renderApp(["/"]);

expect(await screen.findByTestId("app-outlet")).toBeInTheDocument();
expect(
screen.queryByTestId("first-run-onboarding-screen"),
).not.toBeInTheDocument();
expect(screen.queryByTestId("onboarding-modal")).not.toBeInTheDocument();
});

it("shows first-run onboarding before the recovery modal when locked to Cloud with no backend", async () => {
vi.stubEnv("VITE_LOCK_TO_CLOUD", "https://app.all-hands.dev");
vi.stubEnv("VITE_SESSION_API_KEY", "");
Expand Down
44 changes: 44 additions & 0 deletions __tests__/scripts/static-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ describe("static-server.mjs", () => {
expect(config.lockToCloud).toBeNull();
});

it("defaults disableOnboarding to false", () => {
const config = parseArgs([]);
expect(config.disableOnboarding).toBe(false);
});

it("parses --disable-onboarding", () => {
const config = parseArgs(["--disable-onboarding"]);
expect(config.disableOnboarding).toBe(true);
});

it("defaults basePath to root", () => {
const config = parseArgs([]);
expect(config.basePath).toBe("/");
Expand Down Expand Up @@ -240,6 +250,40 @@ describe("static-server.mjs", () => {
});
});

describe("disable-onboarding injection", () => {
// Covers the SaaS / prebuilt-bundle path: the published build has no
// VITE_DISABLE_ONBOARDING baked in, so the wizard is suppressed through
// this injected window global (see `isOnboardingDisabled()` in
// src/api/agent-server-config.ts).
it("exposes the flag on window.__AGENT_CANVAS_DISABLE_ONBOARDING__", async () => {
const buildDir = mkdtempSync(path.join(tmpdir(), "agent-canvas-build-"));
tempDirs.push(buildDir);
writeFileSync(
path.join(buildDir, "index.html"),
"<html><head></head><body>app</body></html>",
);

const origin = await startServer(buildDir, { disableOnboarding: true });
const body = await (await fetch(`${origin}/`)).text();

expect(body).toContain("window.__AGENT_CANVAS_DISABLE_ONBOARDING__=true");
});

it("does not inject the flag by default", async () => {
const buildDir = mkdtempSync(path.join(tmpdir(), "agent-canvas-build-"));
tempDirs.push(buildDir);
writeFileSync(
path.join(buildDir, "index.html"),
"<html><head></head><body>app</body></html>",
);

const origin = await startServer(buildDir);
const body = await (await fetch(`${origin}/`)).text();

expect(body).not.toContain("__AGENT_CANVAS_DISABLE_ONBOARDING__");
});
});

describe("session key injection", () => {
async function startServerWithKey(dir: string, sessionApiKey: string) {
const server = await startStaticServer({
Expand Down
1 change: 1 addition & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ You can create a `.env` file in the project directory with these variables based
| `VITE_WORKING_DIR` | Workspace path sent when starting new conversations | `workspace/project` |
| `VITE_ENABLE_BROWSER_TOOLS` | Set to `false` to omit `BrowserToolSet` from new conversation payloads | `true` |
| `VITE_BASE_PATH` | Build/serve the SPA under a subpath such as `/canvas` | `/` |
| `VITE_DISABLE_ONBOARDING` | Set to `true` to skip the first-run onboarding wizard (users land on the home screen) | `false` |
| `VITE_MOCK_API` | Enable/disable API mocking with MSW | `false` |
| `VITE_USE_TLS` | Use HTTPS/WSS for the Vite proxy target | `false` |
| `VITE_FRONTEND_PORT` | Port to run the frontend application | `3001` |
Expand Down
31 changes: 31 additions & 0 deletions scripts/static-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function parseArgs(argv = process.argv.slice(2)) {
authRequired: false,
runtimeServicesInfo: null,
lockToCloud: null,
disableOnboarding: false,
basePath: "/",
};

Expand Down Expand Up @@ -128,6 +129,9 @@ export function parseArgs(argv = process.argv.slice(2)) {
case "--lock-to-cloud":
config.lockToCloud = argv[++i] || null;
break;
case "--disable-onboarding":
config.disableOnboarding = true;
break;
case "--base-path":
config.basePath = normalizeBasePath(argv[++i]);
break;
Expand Down Expand Up @@ -206,6 +210,10 @@ OPTIONS:
--lock-to-cloud <cloud-url> Lock backend setup to a single OpenHands Cloud
URL. Hides manual/local backend setup and the
custom Cloud URL field in the pre-built frontend.
--disable-onboarding Inject a flag into index.html so the pre-built
frontend skips the first-run onboarding wizard.
Also enabled by env
AGENT_CANVAS_DISABLE_ONBOARDING=true.
Comment thread
hieptl marked this conversation as resolved.
--base-path <path> Mount the SPA under <path> (default: /).
For example, --base-path /canvas serves
index.html and assets under /canvas.
Expand Down Expand Up @@ -263,6 +271,11 @@ ROUTING:
* `agent-server-config.ts` so pre-built frontend bundles can hide manual
* backend setup and the custom Cloud URL field at runtime.
*
* - `disableOnboarding`: sets `window.__AGENT_CANVAS_DISABLE_ONBOARDING__ =
* true` so the pre-built frontend skips the first-run onboarding wizard
* (read by `isOnboardingDisabled()` in `agent-server-config.ts`) without
* VITE_DISABLE_ONBOARDING baked in.
*
* - `basePath`: the path prefix the SPA is mounted under, exposed as
* `window.__AGENT_CANVAS_BASE_PATH__` so runtime static assets like locale
* files can resolve through the same subpath as the built bundle.
Expand All @@ -272,6 +285,7 @@ function makeConfigInjectionScript(
authRequired,
runtimeServicesInfo,
lockToCloud,
disableOnboarding,
basePath,
) {
const parts = [];
Expand Down Expand Up @@ -316,6 +330,10 @@ function makeConfigInjectionScript(
);
}

if (disableOnboarding) {
parts.push(`window.__AGENT_CANVAS_DISABLE_ONBOARDING__=true;`);
}

if (basePath && basePath !== "/") {
parts.push(
`window.__AGENT_CANVAS_BASE_PATH__=${JSON.stringify(basePath)};`,
Expand All @@ -340,6 +358,7 @@ async function serveInjectedIndexHtml(
authRequired,
runtimeServicesInfo,
lockToCloud,
disableOnboarding,
basePath,
} = {},
) {
Expand All @@ -355,6 +374,7 @@ async function serveInjectedIndexHtml(
authRequired,
runtimeServicesInfo,
lockToCloud,
disableOnboarding,
basePath,
);
// Inject right before </head> so the key is available before any app code runs.
Expand Down Expand Up @@ -404,6 +424,7 @@ function needsRuntimeInjection(injectionOpts) {
injectionOpts.authRequired ||
injectionOpts.runtimeServicesInfo ||
injectionOpts.lockToCloud ||
injectionOpts.disableOnboarding ||
(injectionOpts.basePath && injectionOpts.basePath !== "/"),
);
}
Expand Down Expand Up @@ -547,6 +568,7 @@ export function startStaticServer(config) {
authRequired: config.authRequired || false,
runtimeServicesInfo: config.runtimeServicesInfo || null,
lockToCloud: config.lockToCloud || null,
disableOnboarding: config.disableOnboarding || false,
basePath: normalizeBasePath(config.basePath),
};
const basePath = injectionOpts.basePath;
Expand Down Expand Up @@ -611,6 +633,9 @@ export function startStaticServer(config) {
if (config.lockToCloud) {
console.log(` Backend setup locked to Cloud: ${config.lockToCloud}`);
}
if (config.disableOnboarding) {
console.log(" First-run onboarding: disabled");
}
console.log(" * (default) -> static files + SPA fallback");
console.log("");
resolveListen(server);
Expand All @@ -628,6 +653,12 @@ const isMainModule =
if (isMainModule) {
try {
const config = parseArgs();
// Env fallback so deployments that can only set container env vars
// (e.g. the Helm chart's generic `env:` passthrough) can enable the
// flag without changing the CLI invocation. Env can only turn it ON.
if (process.env.AGENT_CANVAS_DISABLE_ONBOARDING === "true") {
Comment thread
hieptl marked this conversation as resolved.
Outdated
config.disableOnboarding = true;
}
await startStaticServer(config);
} catch (err) {
console.error(err instanceof Error ? err.message : err);
Expand Down
17 changes: 17 additions & 0 deletions src/api/agent-server-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,20 @@ export function isAuthRequiredAndMissing(): boolean {
if (!isAuthRequired()) return false;
return !getAgentServerSessionApiKey();
}

/**
* True when first-run onboarding should be suppressed for this deployment
* (users land directly on the home screen with default settings). Sourced
* from build-time `VITE_DISABLE_ONBOARDING=true` or the runtime
* `window.__AGENT_CANVAS_DISABLE_ONBOARDING__` global injected by
* `scripts/static-server.mjs --disable-onboarding`. Cloud login is not
* onboarding and is never suppressed by this flag.
Comment thread
hieptl marked this conversation as resolved.
Outdated
*/
export function isOnboardingDisabled(): boolean {
return (
import.meta.env.VITE_DISABLE_ONBOARDING === "true" ||
(typeof window !== "undefined" &&
(window as unknown as Record<string, unknown>)
.__AGENT_CANVAS_DISABLE_ONBOARDING__ === true)
);
}
10 changes: 9 additions & 1 deletion src/components/features/onboarding/use-onboarding-completion.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from "react";

import { isOnboardingDisabled } from "#/api/agent-server-config";

/**
* localStorage key persisting whether the welcome onboarding flow has
* been completed (or skipped). Once present, the modal won't auto-show
Expand Down Expand Up @@ -49,5 +51,11 @@ export function useOnboardingCompletion() {
setIsCompleted(true);
}, []);

return { isCompleted, markCompleted } as const;
// A deployment-level disable flag reports "completed" so both wizard
// gates (root.tsx, onboarding-host.tsx) stay closed; the
// ?previewOnboardingStep harness bypasses isCompleted and keeps working.
return {
isCompleted: isOnboardingDisabled() || isCompleted,
Comment thread
hieptl marked this conversation as resolved.
Outdated
markCompleted,
} as const;
}
Loading