EN / field notes OpenHands feature map

OpenHands / F01

First run, onboarding and sign-in

What a user sees before the normal app shell. A browser that has never finished or skipped onboarding gets a full-screen onboarding modal on every route: choose an agent, set up the LLM (or ACP credentials), then say hello, which starts the first conversation. In public mode the flow starts with an "Add a backend" step that asks for the server's API key, and a browser with no stored key gets a full-screen API-key screen instead of the app. A one-time telemetry consent modal appears for each local backend. Unknown routes render an in-app "Page not found" page inside the app shell, and the tab title follows the open conversation.

22 mapped behaviors · 26 recipes and supporting checks · source snapshot 9 October 2026
From upstream main at 8793c111. Read the maintained source.

How to get to it

  • First run: open any URL of the app in a browser profile that has never onboarded. Every control-openhands launch --new starts with a fresh browser profile, and control-openhands browser reset gives the same run a fresh one again. Backend-side state (consent answer, LLM profile, agent kind) stays.
  • Public mode: control-openhands launch --new --public serves the UI without the injected key, so step 0 is "Add a backend". The API-key screen appears after skipping onboarding, after removing the last backend under backend selector → Manage backends (see F25), or when the server's key changes under a stored one.
  • Telemetry consent: appears by itself over onboarding or the app. It is changed later in Settings → Application (see F16).
  • Onboarding preview: direct URL with ?previewOnboardingStep=N, for example /?previewOnboardingStep=3 or /settings/app?previewOnboardingStep=3.
  • Last onboarding step: Close, a recommended-automation card, or the "Skip Getting Started checklist" box below the modal.
  • Not-found page: any unknown direct URL, such as /this-route-does-not-exist. Its Home button returns to /.
  • There is no command-menu entry or keyboard shortcut for onboarding.

Before you start

Start with the common launch and health checks, then follow this family’s preconditions in order. Recipes share the fixtures and state named below.

Preconditions:

  • A fresh run you launched yourself: control-openhands launch --new (local) or control-openhands launch --new --public, then export OH_VERIFY_RUN=<run from the launch JSON> and control-openhands doctor. Do not run control-openhands onboard --skip: it consumes the first-run state this family tests.
  • The model-backed say-hello check needs a DeepSeek key in DEEPSEEK_API_KEY. It is typed into the onboarding form with --value-env, which replaces llm preset deepseek here.
  • Each fresh browser profile walks first run once; control-openhands browser reset starts another first run on the same stack. Local and public mode still need separate runs (launch --new and launch --new --public): the local walk, public run A (backend step walked) and public run B (onboarding skipped). Stop each one with control-openhands stop when done.
  • Desktop viewport unless stated.

Behavior inventory

22 stable behavior IDs and their expected behavior
  • F01.first-run-gate with no openhands-onboarded flag in localStorage, any URL (including /conversations) shows only first-run-onboarding-screen with the onboarding modal. There is no sidebar. Finishing or skipping sets the flag, and the app stays revealed after a reload. Read recipe ↓
  • F01.onboarding-modal a segmented progress bar (3 steps, or 4 with the backend step) over a sliding rail. Neither Escape nor a click on the backdrop dismisses it. "Skip for now" shows on every step except the last, and Back/Next move between steps. Read recipe ↓
  • F01.onboarding-backend-step when no reachable backend exists (public mode), step 0 "Add a backend" prefills the name Local and the page origin as host. Next stays disabled until an API key is typed. A wrong key shows "Could not connect to <host>" / "Invalid API key" inline. The right key saves the backend, and the flow collapses to 3 steps on "Choose your agent" with no Back button. The first paint must not raise an error toast. Read recipe ↓
  • F01.onboarding-choose-agent radio tiles for OpenHands (preselected), Claude Code, Codex and Gemini CLI. Next saves the agent kind with a "Settings saved" toast. Read recipe ↓
  • F01.onboarding-setup-llm the embedded LLM form is prefilled with openai/gpt-5.6-sol. Typing a custom model and key then Next saves them. On a local backend it also creates and activates an LLM profile named after the model (deepseek/deepseek-flash → deepseek-flash). Read recipe ↓
  • F01.onboarding-repeat-endpoint onboarding again (a new browser, the same backend) with the custom Base URL and key that are already saved creates a profile that keeps that Base URL, and the say-hello conversation reaches that endpoint. The All view prefills the saved Base URL. Read recipe ↓
  • F01.onboarding-acp-secrets choosing an ACP agent swaps the LLM step for "Add your API keys" with that provider's fields. When the host CLI is already logged in, a green "already signed in" banner makes them optional. Back returns to the agent step. Read recipe ↓
  • F01.onboarding-say-hello the last step has a prefilled message. An empty message disables send. Enter or the send button creates a conversation, navigates to /conversations/<id> and completes onboarding, and the agent replies. A recommended-automations list sits under an "Or" separator, and a "Skip Getting Started checklist" box sits below the modal. Read recipe ↓
  • F01.onboarding-hello-close the last step has Back and Close instead of "Skip for now". Close dismisses the modal without creating a conversation, sets the openhands-onboarded flag and leaves the app shell on /. Read recipe ↓
  • F01.onboarding-skip-checklist the "Skip Getting Started checklist" box under the last step hides the sidebar Getting started card (F02) once the app is revealed. It is the same per-browser setting as the Settings → Application switch, and it is saved as soon as you tick it, preview mode included. Read recipe ↓
  • F01.onboarding-skip "Skip for now" closes the modal and sets the flag. In public mode, skipping before a key is stored lands on the API-key screen; skipping after the backend step lands in the app. Read recipe ↓
  • F01.onboarding-preview ?previewOnboardingStep=0..3 on any route opens the modal (data-preview="true") on that phase, even after onboarding. Values outside 0-3 are ignored. Skip and Close do nothing, so leave by navigating to a URL without the parameter. Read recipe ↓
  • F01.onboarding-phone every onboarding step fits a 390 px viewport without horizontal overflow. Read recipe ↓
  • F01.api-key-entry in public mode with no usable key (onboarding skipped, the last backend removed, or a stored key the server now rejects), a full-screen "Add a backend" card appears. The host is read-only and set to the origin, and Connect stays disabled until the name and key are filled. A wrong key shows "Invalid API key. Please check the key and try again." The right key opens the app shell, which survives a reload, and the consent modal then names the new backend. Read recipe ↓
  • F01.onboarding-cloud-login the backend step's "OpenHands Cloud" column has "Connect to OpenHands" (device-flow login) and an "Advanced" toggle. The toggle reveals a Cloud Host field (placeholder https://app.all-hands.dev) for self-hosted Cloud deployments. The login itself is blocked: it needs an OpenHands Cloud account. Read recipe ↓
  • F01.route-error-boundary an unknown URL renders a "Page not found" page inside the app shell, with the sidebar and a Home link back to /, and logs no page errors. Read recipe ↓
  • F01.document-title the tab title is OpenHands. On a conversation it is <status emoji> <conversation title> | OpenHands. Read recipe ↓
  • F01.bootstrap-loading a centered spinner card shows while /server_info loads (transient, not-run). Read recipe ↓
  • F01.locked-cloud-first-run on a canvas locked to an OpenHands Cloud host, first run shows only the Cloud login (Add backend) without a progress bar or close button (blocked). Read recipe ↓
  • F01.cookie-auth-redirect OHE cookie-auth deployments probe the main-app session behind a spinner and redirect to its /login when it is missing (blocked). Read recipe ↓

Readable recipes

Read each script from top to bottom. Code is copied from the map; prose gives the action, expected observation, and conditions. <id>, <run> and similar placeholders stand for values from your own run. Short forms such as browser count continue the same control-openhands invocation; they are kept as documented.

Expected observations describe the recipe’s contract. Captures below selected recipes show representative real states from this snapshot; they do not mark every mapped behavior as passed. Follow cleanup before moving to another family.

Local run (launch --new):

First-run gate #

  1. Do
    control-openhands browser goto /conversations
  2. Check
    control-openhands browser testids
  3. Expect
    The list starts with first-run-onboarding-screen, telemetry-consent-form and onboarding-modal; root-layout is absent.
  4. Do
    control-openhands browser screenshot --feature F01.first-run-gate --name fresh
  5. Expect
    The screenshot shows the consent modal over "Choose your agent".

Consent #

  1. Check
    control-openhands browser snapshot 'testid=telemetry-consent-form'
  2. Expect
    It reads This preference is saved for the local backend “Local” at http://127.0.0.1:<port>. and the checkbox is [checked].
  3. Do
    control-openhands browser press Escape
  4. Check
    control-openhands browser count 'testid=telemetry-consent-form'
  5. Note
    the count stays 1.
  6. Do
    control-openhands browser uncheck 'testid=telemetry-consent-form >> role=checkbox'
  7. Do
    control-openhands browser click 'testid=confirm-telemetry-preferences'
  8. Wait
    control-openhands browser wait 'testid=telemetry-consent-form' --state detached
  9. Check
    control-openhands api GET /api/settings
  10. Expect
    It shows "user_consents_to_analytics": false.
  11. Do
    control-openhands browser reload
  12. Check
    control-openhands browser count 'testid=telemetry-consent-form'
  13. Note
    the count is 0.

Modal shell #

  1. Do
    control-openhands browser press Escape
  2. Check
    control-openhands browser count 'testid=onboarding-modal'

    (1) and

  3. Check
    control-openhands browser attr 'testid=onboarding-modal' data-current-step

    (0).

  4. Do
    control-openhands browser mouse-click 40 40
  5. Note
    on the bare backdrop; the count stays 1 and the step is unchanged. onboarding-progress-step-0..2 exist and -3 does not, because the backend is healthy.

Choose agent #

  1. Check
    control-openhands browser attr 'testid=onboarding-agent-option-openhands' aria-checked

    (true) and

  2. Check
    control-openhands browser count 'testid=onboarding-agent-back'

    (0).

  3. Do
    control-openhands browser click 'testid=onboarding-agent-option-codex'
  4. Check
    control-openhands browser attr 'testid=onboarding-agent-option-codex' aria-checked

    (true), and switch back with

  5. Do
    control-openhands browser click 'testid=onboarding-agent-option-openhands'
  6. Do
    control-openhands browser click 'testid=onboarding-agent-next'
  7. Wait
    control-openhands browser wait-text 'Settings saved'
  8. Note
    data-current-step becomes 1.
  9. Do
    control-openhands browser click 'testid=onboarding-llm-back'
  10. Note
    data-current-step is 0.
  11. Note
    Click testid=onboarding-agent-next again.
Onboarding offers OpenHands, Claude Code, Codex and Gemini CLI, with OpenHands selected.
The first-run agent step keeps the app behind a modal until setup is completed or skipped. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Onboarding offers OpenHands, Claude Code, Codex and Gemini CLI, with OpenHands selected.

Representative documentation capture, not a complete run of this recipe or family. Captured on an isolated local backend at desktop viewport using the current main checkout.

How this screenshot was taken

canvas: 1.26.0 · agent server: 1.53.0 · sdk: 1.53.0 · automation: 1.19.0

OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser goto /conversations
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser uncheck 'testid=telemetry-consent-form >> role=checkbox'
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=confirm-telemetry-preferences
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=telemetry-consent-form --state detached
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=onboarding-modal
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F01.onboarding-choose-agent --name choose-agent

LLM step #

  1. Check
    control-openhands browser snapshot 'testid=onboarding-step-setup-llm'
  2. Expect
    It shows Set up your LLM and Basic tab comboboxes OpenAI / gpt-5.6-sol.
  3. Do
    control-openhands browser click 'testid=onboarding-step-setup-llm >> testid=sdk-section-advanced-toggle'
  4. Check
    control-openhands browser value 'testid=onboarding-step-setup-llm >> testid=llm-custom-model-input'

    (openai/gpt-5.6-sol).

  5. Do
    control-openhands browser fill 'testid=onboarding-step-setup-llm >> testid=llm-custom-model-input' deepseek/deepseek-flash
  6. Do
    control-openhands browser fill 'testid=onboarding-step-setup-llm >> testid=llm-api-key-input' --value-env DEEPSEEK_API_KEY
  7. Do
    control-openhands browser click 'testid=onboarding-llm-next'
  8. Note
    Wait with
  9. Wait
    control-openhands browser wait '[data-testid=onboarding-modal][data-current-step="2"]'
  10. Note
    Second view:
  11. Check
    control-openhands llm show

    lists profile deepseek-flash (deepseek/deepseek-flash, api_key_set: true) as active_profile.

Say hello #

  1. Check
    control-openhands browser count 'testid=onboarding-skip'

    (0 on the last step),

  2. Do
    control-openhands browser fill 'testid=onboarding-hello-input' ''
  3. Check
    control-openhands browser enabled 'testid=onboarding-hello-input-form >> testid=submit-button'

    (false).

  4. Do
    control-openhands browser fill 'testid=onboarding-hello-input' 'Reply with only the word hello. Do not run any tools.'

    (enabled turns true),

  5. Do
    control-openhands browser press Enter --selector 'testid=onboarding-hello-input'
  6. Wait
    control-openhands browser wait-url '/conversations/[0-9a-f-]+'
  7. Note
    Take <id> from
  8. Check
    control-openhands browser url
  9. Wait
    control-openhands conversation wait <id> --timeout 180
  10. Check
    control-openhands conversation events <id> --kinds MessageEvent
  11. Expect
    The agent message is hello, and
  12. Check
    control-openhands browser count 'testid=onboarding-modal'

    is 0.

  13. Do
    control-openhands browser eval "localStorage.getItem('openhands-onboarded')"

    returns "1".

  14. Note
    After
  15. Do
    control-openhands browser reload
  16. Do
    control-openhands browser goto /
  17. Note
    testid=first-run-onboarding-screen and testid=onboarding-modal both count 0.

Tab title #

  1. Note
    On / run
  2. Check
    control-openhands browser url
  3. Note
    title is OpenHands.
  4. Note
    On /conversations/<id> (after a reload, once the title is generated) the title is <emoji> <conversation title> | OpenHands, for example ✅ … | OpenHands for a finished conversation.

Preview #

  1. Do
    control-openhands browser goto '/?previewOnboardingStep=3'
  2. Check
    control-openhands browser attr 'testid=onboarding-modal' data-preview

    (true) and

  3. Check
    control-openhands browser count 'testid=onboarding-step-say-hello'

    (1).

  4. Note
    Step 2 shows the LLM slide.
  5. Note
    Steps 0 and 1 both show "Choose your agent" when the backend is healthy. /?previewOnboardingStep=9 renders no modal.
  6. Do
    control-openhands browser goto '/settings/app?previewOnboardingStep=3'
  7. Note
    works too.
  8. Do
    control-openhands browser goto '/?previewOnboardingStep=1'
  9. Do
    control-openhands browser click 'testid=onboarding-skip'
  10. Note
    the modal stays (count 1).
  11. Do
    control-openhands browser goto /

    shows no modal.

Skip checklist box #

  1. Do
    control-openhands browser goto /conversations
  2. Check
    control-openhands browser count 'testid=sidebar-onboarding-checklist'

    (1).

  3. Do
    control-openhands browser goto '/?previewOnboardingStep=3'
  4. Check
    control-openhands browser click 'text=Skip Getting Started checklist'
  5. Do
    control-openhands browser eval "document.querySelector('[data-testid=onboarding-skip-getting-started-checklist]').checked"

    is true.

  6. Do
    control-openhands browser goto /conversations
  7. Do
    control-openhands browser reload
  8. Note
    the checklist count is 0.
  9. Note
    On /settings/app,
  10. Check
    control-openhands browser eval "document.querySelector('[data-testid=show-getting-started-checklist-switch]').checked"

    is false.

  11. Note
    Restore it: go back to /?previewOnboardingStep=3, where the box now reads true, click the same text again, and the checklist count on /conversations is 1.

Phone #

  1. Do
    control-openhands browser viewport phone
  2. Note
    Then for N in 1, 2 and 3 run
  3. Do
    control-openhands browser goto '/?previewOnboardingStep=N'
  4. Check
    control-openhands browser bbox 'testid=onboarding-modal'
  5. Do
    control-openhands browser screenshot --feature F01.onboarding-phone --name step-N
  6. Expect
    The modal is 351 px wide with insideViewport true and pageHorizontalOverflow false, and Back/Next or Back/Close are visible.
  7. Note
    Return with
  8. Do
    control-openhands browser viewport desktop

Error page #

  1. Check
    control-openhands browser errors --clear
  2. Do
    control-openhands browser goto /this-route-does-not-exist
  3. Check
    control-openhands browser snapshot 'testid=not-found-screen'
  4. Expect
    It shows heading "Page not found", the paragraph This address does not match any page. Check the URL, or go back to the home page. and link "Home" (/url: /).
  5. Check
    control-openhands browser count 'aside[data-collapsed]'

    is 1: the sidebar stays.

  6. Do
    control-openhands browser screenshot --feature F01.route-error-boundary --name not-found

    shows the message and the Home button centered beside the sidebar.

  7. Check
    control-openhands browser errors --app-only
  8. Note
    reports pageErrors 0 and appErrors 0.
  9. Check
    control-openhands browser click 'testid=not-found-home-link' --expect-url '/$'
  10. Check
    control-openhands browser count 'testid=home-screen'

    is 1.

Onboarding again with the saved endpoint #

  1. Note
    Two more first runs on this stack, both through the All view, which shows the Base URL field.
  2. Note
    Pass 1 saves a custom endpoint:
  3. Do
    control-openhands browser reset
  4. Do
    control-openhands browser goto /
  5. Do
    control-openhands browser click 'testid=onboarding-agent-next'
  6. Wait
    control-openhands browser wait '[data-testid=onboarding-modal][data-current-step="1"]'
  7. Do
    control-openhands browser click 'testid=onboarding-step-setup-llm >> testid=sdk-section-all-toggle'
  8. Check
    control-openhands browser value 'testid=onboarding-step-setup-llm >> testid=base-url-input'

    (empty).

  9. Do
    control-openhands browser fill 'testid=onboarding-step-setup-llm >> testid=llm-custom-model-input' openai/deepseek-chat
  10. Do
    control-openhands browser fill 'testid=onboarding-step-setup-llm >> testid=base-url-input' https://api.deepseek.com/v1
  11. Do
    control-openhands browser fill 'testid=onboarding-step-setup-llm >> testid=llm-api-key-input' --value-env DEEPSEEK_API_KEY
  12. Do
    control-openhands browser click 'testid=onboarding-llm-next'
  13. Wait
    control-openhands browser wait '[data-testid=onboarding-modal][data-current-step="2"]'
  14. Check
    control-openhands llm show

    lists deepseek-chat (openai/deepseek-chat, base_url https://api.deepseek.com/v1) as active_profile.

  15. Note
    Say hello as above (browser fill 'testid=onboarding-hello-input' 'Reply with only the word hello. Do not run any tools.', browser press Enter --selector 'testid=onboarding-hello-input', browser wait-url '/conversations/[0-9a-f-]+'), then
  16. Wait
    control-openhands conversation wait <id> --timeout 180

    is finished and the agent MessageEvent is hello.

  17. Note
    Pass 2 types the same endpoint again: repeat the steps up to the All view. browser value 'testid=onboarding-step-setup-llm >> testid=base-url-input' is now https://api.deepseek.com/v1, the saved value.
  18. Note
    Fill the model openai/deepseek-v4-flash (a DeepSeek alias, so the new profile gets its own name), the same Base URL and the key, then Next and wait for step 2 (control-openhands browser screenshot 'testid=onboarding-step-setup-llm' --feature F01.onboarding-repeat-endpoint --name second-pass-form before Next).
  19. Note
    Expected:
  20. Check
    control-openhands llm show

    lists deepseek-v4-flash with base_url https://api.deepseek.com/v1, and the say-hello conversation finishes with hello.

  21. Note
    Known failure (reproduced 2026-10-08 at 53c8b4d): the profile is saved with base_url null.
  22. Expect
    The say-hello conversation then ends in error, and
  23. Check
    control-openhands conversation events <id> --last 10

    shows ConversationErrorEvent LLMAuthenticationError: litellm.AuthenticationError: ... OpenAIException - Incorrect API key provided: the DeepSeek key went to OpenAI.

  24. Expect
    The chat shows Your LLM API key appears to be invalid or has expired. (browser screenshot --feature F01.onboarding-repeat-endpoint --name second-pass-error).
  25. Note
    Issue #17884, fix in #17889.
  26. Note
    Restore: run
  27. Arrange
    control-openhands llm preset deepseek
  28. Expect
    It also points the default agent profile back at deepseek-flash: its output has repointed from deepseek-v4-flash to deepseek-flash.
  29. Arrange
    control-openhands api DELETE /api/profiles/deepseek-v4-flash --write
  30. Note
    and .../deepseek-chat --write answer 200.
  31. Note
    Delete the two say-hello conversations with Clean up's steps.

Clean up #

  1. Note
    Delete the hello conversation from its header menu:
  2. Do
    control-openhands browser goto /conversations/<id>

    (the hello conversation's <id> from Say hello; the Error page bullet left /),

  3. Do
    control-openhands browser click 'testid=chat-pane-header >> testid=ellipsis-button'
  4. Do
    control-openhands browser click 'testid=conversation-name-context-menu >> testid=delete-button'
  5. Do
    control-openhands browser click 'role=button[name="Confirm Delete"]'
  6. Do
    control-openhands conversation list
  7. Note
    then shows count 0.

Public run A (launch --new --public), backend step walked:

Backend step #

  1. Do
    control-openhands browser goto /
  2. Note
    sleep 12 (gives a late toast time to appear),
  3. Check
    control-openhands browser toasts --history
  4. Do
    control-openhands browser screenshot --feature F01.onboarding-backend-step --name fresh
  5. Check
    control-openhands browser snapshot 'testid=onboarding-step-check-backend'
  6. Expect
    The toast history is [], and the screenshot shows no toast.
  7. Expect
    The snapshot shows Add a backend, name Local, host http://127.0.0.1:<port>, Type Local checked, button "Next" [disabled], and an "OpenHands Cloud" column with "Connect to OpenHands".
  8. Do
    control-openhands browser fill 'testid=onboarding-backend-api-key' qa-wrong-key
  9. Do
    control-openhands browser click 'testid=onboarding-backend-next'
  10. Check
    control-openhands browser text 'testid=onboarding-backend-error'
  11. Expect
    It reads Could not connect to http://127.0.0.1:<port> / Invalid API key, and data-current-step stays 0.
  12. Check
    control-openhands browser fill 'testid=onboarding-backend-api-key' --value-file "$OH_VERIFY_RUN/private/session-key"
  13. Do
    control-openhands browser click 'testid=onboarding-backend-next'
  14. Wait
    control-openhands browser wait 'testid=telemetry-consent-form'
  15. Expect
    The modal is on "Choose your agent" with data-current-step 0, testid=onboarding-progress-step-3 and testid=onboarding-agent-back both count 0, and the consent form names “Local”.
  16. Note
    Answer it with
  17. Do
    control-openhands browser click 'testid=confirm-telemetry-preferences'

ACP credentials #

  1. Do
    control-openhands browser click 'testid=onboarding-agent-option-claude-code'
  2. Do
    control-openhands browser click 'testid=onboarding-agent-next'
  3. Wait
    control-openhands browser wait-text 'Settings saved'
  4. Check
    control-openhands browser snapshot 'testid=onboarding-step-setup-acp-secrets'
  5. Expect
    It shows Add your API keys with CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL, and
  6. Check
    control-openhands api GET /api/settings

    shows "agent_kind": "acp".

  7. Do
    control-openhands browser click 'testid=onboarding-acp-secrets-back'
  8. Do
    control-openhands browser click 'testid=onboarding-agent-option-openhands'
  9. Do
    control-openhands browser click 'testid=onboarding-agent-next'
  10. Note
    testid=onboarding-step-setup-llm is shown and agent_kind is back to openhands.

Skip into the app #

  1. Do
    control-openhands browser click 'testid=onboarding-skip'
  2. Wait
    control-openhands browser wait 'testid=root-layout'
  3. Do
    control-openhands browser reload
  4. Check
    control-openhands browser count 'testid=onboarding-modal'

    (0) and

  5. Check
    control-openhands browser count 'testid=api-key-entry-screen'

    (0).

Last backend removed #

  1. Do
    control-openhands browser click 'testid=backend-selector'
  2. Do
    control-openhands browser click 'testid=manage-backends-menu-item'
  3. Do
    control-openhands browser click 'testid=manage-backends-remove-Local'
  4. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button'
  5. Wait
    control-openhands browser wait 'testid=api-key-entry-screen'
  6. Note
    succeeds.
  7. Note
    Restore with the Connect recipe below.

Public run B (launch --new --public), onboarding skipped:

Skip to API-key screen #

  1. Do
    control-openhands browser goto /
  2. Do
    control-openhands browser click 'testid=onboarding-skip'
  3. Wait
    control-openhands browser wait 'testid=api-key-entry-screen'
  4. Expect
    After sleep 12,
  5. Check
    control-openhands browser toasts --history

    is []: the screen raises no error toast.

  6. Do
    control-openhands browser reload
  7. Wait
    control-openhands browser wait 'testid=api-key-entry-screen'
  8. Note
    sleep 12 and
  9. Check
    control-openhands browser toasts --history
  10. Note
    again: still [] when the API-key screen is the first paint (the toast these checks guard against reads No backend is configured.; control-openhands browser count 'text=No backend is configured' is 0).
  11. Check
    control-openhands browser value 'testid=api-key-entry-host'

    (the origin; the field is disabled) and

  12. Check
    control-openhands browser enabled 'testid=api-key-entry-submit'

    (false).

  13. Note
    Fill only the key with
  14. Do
    control-openhands browser fill 'testid=api-key-entry-api-key' qa-wrong-key
  15. Note
    enabled is still false because Host Name is required.
  16. Do
    control-openhands browser fill 'testid=api-key-entry-name' 'QA Public'

    (enabled turns true),

  17. Do
    control-openhands browser click 'testid=api-key-entry-submit'
  18. Check
    control-openhands browser text 'testid=api-key-entry-status'
  19. Expect
    It reads Invalid API key. Please check the key and try again., the screen stays, and
  20. Do
    control-openhands browser screenshot --feature F01.api-key-entry --name wrong-key

    shows the red line above Connect.

  21. Note
    While the screen is still up, run
  22. Do
    control-openhands browser viewport phone
  23. Check
    control-openhands browser bbox 'testid=api-key-entry-form'
  24. Expect
    It gives insideViewport true and pageHorizontalOverflow false.
  25. Note
    Return with
  26. Do
    control-openhands browser viewport desktop

Connect #

  1. Do
    control-openhands browser fill 'testid=api-key-entry-name' 'QA Public'
  2. Check
    control-openhands browser fill 'testid=api-key-entry-api-key' --value-file "$OH_VERIFY_RUN/private/session-key"
  3. Do
    control-openhands browser click 'testid=api-key-entry-submit'
  4. Wait
    control-openhands browser wait 'testid=api-key-entry-screen' --state detached
  5. Note
    testid=root-layout is shown, the consent form names “QA Public”, and
  6. Check
    control-openhands browser snapshot 'testid=backend-selector'

    shows combobox "QA Public".

  7. Note
    After
  8. Do
    control-openhands browser reload
  9. Note
    testid=api-key-entry-screen counts 0 and testid=root-layout counts 1.

Stale key re-prompt #

  1. Note
    Answer consent if it is open (control-openhands browser click 'testid=confirm-telemetry-preferences').
  2. Do
    control-openhands restart --rotate-key

    (rotatedKey true), then

  3. Do
    control-openhands browser reload
  4. Wait
    control-openhands browser wait 'testid=api-key-entry-screen'
  5. Note
    testid=root-layout counts 0.
  6. Note
    Connect again as above with the new $OH_VERIFY_RUN/private/session-key.
  7. Expect
    After a reload, testid=root-layout counts 1, and
  8. Do
    control-openhands browser eval "JSON.parse(localStorage.getItem('openhands-backends')).length"

    is 1, so no duplicate backend is added.

Close on the last step #

  1. Do
    control-openhands browser reset
  2. Do
    control-openhands browser goto /
  3. Note
    Walk to the last step:
  4. Check
    control-openhands browser fill 'testid=onboarding-backend-api-key' --value-file "$OH_VERIFY_RUN/private/session-key"
  5. Do
    control-openhands browser click 'testid=onboarding-backend-next'
  6. Wait
    control-openhands browser wait 'testid=onboarding-step-choose-agent'

    (consent does not return: the backend already has an answer),

  7. Do
    control-openhands browser click 'testid=onboarding-agent-next'
  8. Wait
    control-openhands browser wait '[data-testid=onboarding-modal][data-current-step="1"]'
  9. Note
    Then fill the key with
  10. Do
    control-openhands browser fill 'testid=onboarding-step-setup-llm >> testid=llm-api-key-input' --value-env DEEPSEEK_API_KEY
  11. Note
    open testid=sdk-section-advanced-toggle and fill llm-custom-model-input with deepseek/deepseek-flash as in the LLM step.
  12. Do
    control-openhands browser click 'testid=onboarding-llm-next'
  13. Wait
    control-openhands browser wait '[data-testid=onboarding-modal][data-current-step="2"]'
  14. Note
    Check
  15. Do
    control-openhands conversation list

    (count 0), then run

  16. Do
    control-openhands browser click 'testid=onboarding-hello-close'
  17. Wait
    control-openhands browser wait 'testid=onboarding-modal' --state detached
  18. Note
    testid=root-layout counts 1, the URL is / and localStorage.getItem('openhands-onboarded') is "1". conversation list still shows count 0, and after browser reload the modal count is 0.

Recommended automation #

  1. Do
    control-openhands browser reset
  2. Do
    control-openhands browser goto /
  3. Note
    and walk to the last step as above (the LLM step's Next works without retyping the key once the profile is saved).
  4. Check
    control-openhands browser click 'testid=recommended-automation-card-news-digest' --expect-url '/automations/new/news-digest(\?|$)'
  5. Note
    testid=onboarding-modal counts 0, openhands-onboarded is "1", and the "Daily news digest" setup form (testid=setup-dialog) is open.
  6. Note
    Leave without creating anything:
  7. Do
    control-openhands browser click 'testid=setup-dialog-close'
  8. Wait
    control-openhands browser wait 'testid=setup-dialog' --state detached

    (the URL becomes /).

  9. Check
    control-openhands api GET /api/automation/v1

    shows "total": 0.

Cloud login column #

  1. Do
    control-openhands browser reset
  2. Do
    control-openhands browser goto /
  3. Do
    control-openhands browser click 'testid=onboarding-backend-advanced-toggle'
  4. Note
    browser attr 'testid=onboarding-backend-advanced-toggle' aria-expanded is true, and browser attr 'testid=onboarding-backend-cloud-host' placeholder is https://app.all-hands.dev. browser text 'testid=onboarding-backend-advanced-panel' mentions self-hosted Cloud deployments.
  5. Note
    Do not click testid=onboarding-backend-login-button: the device-flow login is blocked without an OpenHands Cloud account.

Not reachable locally:

Locked Cloud first run · Blocked prerequisite #

  1. Note
    Blocked: needs a build locked to a Cloud host (VITE_LOCK_TO_CLOUD or static-server --lock-to-cloud) and an OpenHands Cloud account. launch has no such flag.

Bootstrap spinner · Not run #

  1. Note
    Not-run: it lasts under a second on a healthy stack, and slowing /server_info would mean intercepting requests.

Gotchas and known limits

  • Use a fresh run or browser reset for every first-run check, and never onboard --skip first. The browser profile lives in <run>/private/browser-profile. browser reset clears only the browser, so the server keeps the consent answer (the consent modal does not return), the LLM profile and the agent kind.
  • control-openhands login passes both key prompts: the onboarding backend step, and the API-key screen, where it names the backend Local when Host Name is empty (after Skip and after restart --rotate-key, with no duplicate backend). Use it when the screen is only in the way. To check the screen itself (the disabled Connect, the wrong-key line, a chosen name), use the explicit fills above.
  • Inactive onboarding slides stay mounted and are translated off-screen. Plain browser testids lists only the active slide, but count and wait still find handles on the others. Assert the active step with data-current-step, for example browser wait '[data-testid=onboarding-modal][data-current-step="2"]'. The testid= selector cannot carry attribute filters.
  • Slide indices renumber. After the public backend step succeeds, "Choose your agent" becomes index 0 and has no Back. previewOnboardingStep counts phases with the backend step included, so 0 and 1 look identical on a healthy backend.
  • The consent modal (z-70) sits over the onboarding modal, and clicks at the screen center land on agent tiles once it closes. A forced click on first-run-onboarding-screen selected Codex. Check backdrop non-dismissal with browser mouse-click 40 40, a corner outside the modal.
  • The say-hello default message ("Create a basic webpage…") makes the agent work for a while. Replace it with a tiny prompt to keep model cost down.
  • On a local backend, Next on the LLM step also points the default agent profile at the profile it just created. Activating another LLM profile does not move that pointer, and while it stays, deleting the onboarding profile answers 409 LLM profile is referenced by 1 agent profile(s): default. control-openhands llm preset deepseek moves it to deepseek-flash and reports repointed; llm set does not. In the UI, edit default in Settings → Agents and pick the profile in testid=agent-profile-llm-selector.
  • The LLM step saves only the fields that differ from the saved settings, and the profile it creates is built from that same diff. That is the cause of the F01.onboarding-repeat-endpoint failure (#17884, fix in #17889). It is also why the mock-LLM onboarding e2e failed after earlier specs had saved the mock endpoint. The first pass of that bullet passes on a run whose saved Base URL is still empty.
  • The tab title prefixes a status emoji to the stored title. A generated title that already starts with an emoji therefore shows twice (✅ ✅ Reply hello without tools | OpenHands). Right after launch the title is Conversation <id prefix> until a reload picks up the generated one.
  • Right after Next, the ACP step reads "Checking for an existing Claude Code login…" with Next disabled. Wait for testid=onboarding-acp-auth-detected before reading the banner. The "already signed in" banner depends on the host's CLI login. On a clean CI machine the fields become required, and Next is blocked until they are filled.
  • Choosing an ACP agent saves agent_kind: acp immediately. Re-choose OpenHands before leaving, or later conversations in the run use the ACP agent.
  • In preview mode, Skip and Close are inert. The LLM slide shows gpt-5.6-sol even when another model is saved, so do not read it as data loss.
  • browser toasts --history lists every status or alert text (toasts and alert banners) seen since the page loaded, including success toasts and progress lines such as Checking your backend connection…. Read it on a page that has just loaded (or reloaded) to check that no error toast appeared. A page with a standing banner never reads []: the app shell of a backend with no LLM lists the Your LLM isn't set up yet alert.

Source paths: src/root.tsx, src/components/features/onboarding/, src/components/features/analytics/telemetry-consent-banner.tsx, src/components/features/backends/api-key-entry-screen.tsx, src/routes/root-layout.tsx, src/routes/not-found.tsx, src/hooks/use-app-title.ts.