EN / field notes OpenHands feature map

OpenHands / F25

Backends, Cloud and sharing

One Canvas can drive several agent servers. The backend selector at the bottom of the sidebar lists every registered backend with a live status dot and switches between them; its footer opens Add Backend (OpenHands Cloud login through an OAuth device flow, or a local/remote agent server by host and API key) and Manage Backends (select, edit, remove, and Cloud "Log back in"). When the active backend is unreachable the whole app is replaced by Manage backends in recovery mode. Cloud backends add organization rows, a Cloud settings link and public sharing of conversations (read-only at /shared/conversations/:id); /oauth/device/verify is the device-approval page. Everything except the Cloud account itself can be driven locally with a second control-openhands stack as the extra backend.

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

How to get to it

  • Sidebar, bottom: the backend selector (backend-selector; click or hover) → options, Add Backend (add-backend-menu-item), Manage Backends (manage-backends-menu-item).
  • Collapsed rail: hover the backend icon (collapsed-backend-selector-link) → the same footer items.
  • Phone: hamburger (sidebar-mobile-menu-toggle) → drawer → backend selector.
  • Manage backends → Add Backend (manage-backends-add) opens the same Add form.
  • Direct URLs: any in-app URL with ?backend=<id>; /oauth/device/verify[?user_code=CODE]; /shared/conversations/<id>.
  • Conversation header name → "..." (conversation-name >> ellipsis-button) for Public Share (Cloud only).
  • Reloading with an unreachable active backend opens the recovery gate.
  • Onboarding's backend step and the public-mode API-key screen also add backends; they belong to F01. The ChatGPT subscription card is F10 (F10.subscription) and the provider balance card is F27 (F27.usage-provider-balance).

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:

  • Baseline state (launched, doctored, onboard --skip done) and control-openhands llm preset deepseek on this run.
  • One conversation on this run: control-openhands conversation start --prompt "Reply with exactly: QA_F25_PONG" --wait --timeout 180; note its <id>. The conversation only has to exist: F25 never reads the reply, so a run that ends in error (for example a key without balance, saved with llm preset deepseek --no-validate) is enough.
  • A second stack to add as a backend, started without a browser and never driven directly: export OH_VERIFY_RUN_2=$(OH_VERIFY_RUN= control-openhands launch --new --build never --no-browser --print-run), then control-openhands status --run "$OH_VERIFY_RUN_2"; its baseUrl is <second-url> below and its key is $OH_VERIFY_RUN_2/private/session-key. It has no conversations.
  • No backend named QA_Second or QA_Renamed exists (the selector lists only Local).
  • Bullets run in order: Add creates QA_Second, Edit renames it to QA_Renamed, Remove deletes it.
  • Blocked here (no OpenHands Cloud account): approving the device flow, F25.cloud-org-rows, F25.cloud-log-back-in, F25.cloud-settings-link, F25.cloud-sandbox-states, F25.cloud-org-suspended (also needs a suspended OpenHands Enterprise organization or membership), F25.share-publicly (toggle), and F25.locked-cloud, which also needs a launcher flag (see Gotchas).

Behavior inventory

29 stable behavior IDs and their expected behavior
  • F25.selector-dropdown clicking (or hovering) the selector opens a listbox of backends, each option named <status> <name> with a status dot, and a footer with Add Backend and Manage Backends. Read recipe ↓
  • F25.add-backend-modal Add Backend opens "Choose how you want to connect" with a description, a Learn more link to the deployment docs and two tabs, OpenHands Cloud (selected) and Agent-server. Escape does not close it; the X does. Read recipe ↓
  • F25.cloud-advanced-host on the Cloud tab, Advanced reveals a Host field (placeholder https://app.all-hands.dev) for a self-hosted Cloud. Read recipe ↓
  • F25.cloud-device-flow Connect to OpenHands opens a popup and shows Starting authentication..., then Waiting for authorization... with the verification link and Cancel; Cancel returns to the idle button and closes the popup. An unreachable host shows Failed to start device flow: ... with Try again. Approving the code (adds a backend named OpenHands Cloud) needs a Cloud account. Read recipe ↓
  • F25.agent-server-guidance the Agent-server tab has a Local / Remote toggle. Local shows a collapsible Before you connect with agent-canvas --backend-only --port 8001 and a local-setup docs link; Remote shows Recommended setup, Connection details and a remote-setup docs link. Read recipe ↓
  • F25.add-agent-server Connect stays disabled until Host Name and a valid Host are filled; the API Key field is optional for Connect on Local and required on Remote, but a key-protected host still rejects an empty key. A dead host, an empty key or a wrong key shows Could not connect to <host> with the reason inline (Disconnected (check URL or network) for a dead host, Invalid API key for an empty or wrong key); a reachable host with the right key, entered on the default Local location, adds the backend, makes it active (BM-001) and redirects a /conversations/<id> page to /conversations without an error toast, also on the backend's first activation, without asking the new backend for the old conversation id. Read recipe ↓
  • F25.switch-backend choosing another option shows a full-screen Switching to <name> overlay for about a second, then the sidebar and pages show that backend's data; on /conversations/<id> the app moves to /conversations (BM-002). Read recipe ↓
  • F25.backend-pinned-url in-app links carry ?backend=<id>; opening such a URL pins the tab to that backend; an unknown id is ignored. Read recipe ↓
  • F25.per-tab-backend the active backend is per browser tab: switching it in one tab leaves the other open tabs where they were, a new tab opened from a plain link starts from the last choice made in any tab, and a sidebar conversation row opened in a new tab (Control-click) lands on the backend that owns the conversation because its link carries ?backend=<owner>. Read recipe ↓
  • F25.manage-backends Manage backends lists every backend with status dot, name, version (v1.50.1), host, status text and a LOCAL/CLOUD pill, plus Edit and Remove; Escape, a backdrop click, X and Done close it. Read recipe ↓
  • F25.manage-add Add Backend in Manage backends opens the same Add form stacked over Manage; its X returns to Manage, and a successful add keeps Manage open with the new row while the new backend becomes active. Read recipe ↓
  • F25.manage-select clicking a healthy row makes that backend active and closes the modal; on a detail page it leaves it for that section's list, like the selector does (BM-002). Read recipe ↓
  • F25.edit-backend the pencil opens "Edit backend" pre-filled (key masked) with a Connected · Local v<version> badge; empty name and invalid host show Name is required and Enter a valid URL (e.g. http://localhost:8080) and disable Save; Save re-tests the connection, keeps the modal open with Could not connect to <host> on failure, and persists on success. Read recipe ↓
  • F25.edit-cancel Cancel and the X in Edit backend close only the Edit modal and discard the draft; Manage backends stays open with the stored values. Read recipe ↓
  • F25.edit-escape Escape in the Edit modal should not discard the draft (the modal disables Escape). Known failure, see Gotchas. Read recipe ↓
  • F25.health-status a backend that stops answering turns its dot red (Disconnected) within one health probe (every 30 s for local and remote agent servers; Cloud backends every 5 min and not on window focus or reconnect, #18128); its Manage row reads Disconnected (check URL or network) with a red detail line and cannot be selected. Read recipe ↓
  • F25.recovery-gate reloading while the active backend is down shows only agent-server-onboarding-screen with Manage backends in recovery mode (no X, no Done, Escape ignored, primary Add Backend); choosing a healthy row restores the app. Read recipe ↓
  • F25.remove-backend the trash icon asks Remove backend "<name>"? If it is active, the app will switch back to Local.; Cancel keeps it, Confirm removes it and an active removed backend falls back to Local (BM-003). Read recipe ↓
  • F25.collapsed-entry with the rail collapsed, the backend icon's popover offers the same Add Backend and Manage Backends items, and both modals open. Read recipe ↓
  • F25.phone at 390 px the drawer's selector opens both modals inside the viewport without horizontal overflow. Read recipe ↓
  • F25.device-verify-page /oauth/device/verify shows a "Device Authorization" code form; with ?user_code= it shows "Device Authorization Request", the code, a Security Notice and Cancel / Authorize Device. On a local stack Authorize and Continue end in the Error card with Try Again. Read recipe ↓
  • F25.shared-conversation-view /shared/conversations/<id> is a standalone read-only page (no app shell); on a local backend it reads Conversation not found. Read recipe ↓
  • F25.share-publicly on a Cloud backend the conversation menu has a Public Share toggle with copy-link and open-link buttons; on a local backend the item is absent. Read recipe ↓
  • F25.cloud-org-rows each Cloud backend appears once per organization as <name> – <org> (Personal Workspace for the user's own), and choosing one scopes lists to that org.
  • F25.cloud-log-back-in a logged-out Cloud row shows Logged out and a Log back in button that reruns the device flow.
  • F25.cloud-sandbox-states Cloud conversations show waiting, archived and error sandbox states in chat.
  • F25.cloud-org-suspended when a Cloud call made for the selected organization (X-Org-Id) fails with 403 and detail Organization is suspended or User membership is suspended, the app is replaced by <org> is suspended. Contact your administrator to restore access. (or Your access to <org> is suspended. Contact your administrator to restore it.), then Switch to another workspace and one button per other organization (Personal Workspace for the user's own). A button selects that organization and the app returns; with no other organization only the message shows. A reload clears the in-memory record. Blocked here: needs an OpenHands Enterprise instance with ENABLE_SUPER_ADMIN where a Super Admin suspended the organization or this member's membership, signed in as a non-Super-Admin member with that organization selected (#17988).
  • F25.locked-cloud a Canvas served with --lock-to-cloud <url> shows only the Cloud login (no close) on first run, and its selector footer reads Reconnect to Cloud with no add, edit or remove.

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.

Open the selector #

  1. Note
    From /conversations run
  2. Do
    control-openhands browser click 'testid=backend-selector'
  3. Check
    control-openhands browser snapshot 'testid=backend-selector'
  4. Expect
    The snapshot shows listbox with option "Connected Local", a separator, button "Add Backend" and button "Manage Backends".
  5. Note
    Screenshot with
  6. Do
    control-openhands browser screenshot --feature F25.selector-dropdown --name open
  7. Note
    Hover opens it too: browser press Escape, move the pointer away with
  8. Do
    control-openhands browser hover 'testid=command-menu-trigger'

    (hovering a selector the pointer is already on does nothing),

  9. Do
    control-openhands browser hover 'testid=backend-selector'
  10. Note
    then the same snapshot shows combobox "Local" [expanded] and the two footer buttons.

Add Backend modal #

  1. Do
    control-openhands browser click 'testid=add-backend-menu-item'
  2. Check
    control-openhands browser snapshot 'testid=add-backend-modal'
  3. Note
    : heading Choose how you want to connect, link Compare Cloud, self-hosted, and Enterprise deployment options (/url: https://docs.openhands.dev/overview/introduction, text Learn more) and tab OpenHands Cloud ... [selected] next to tab Agent-server ....
  4. Do
    control-openhands browser press Escape
  5. Note
    leaves browser count 'testid=add-backend-modal' at 1;
  6. Do
    control-openhands browser click 'testid=add-backend-close'
  7. Note
    makes it 0.

Advanced host #

  1. Note
    Reopen with browser click 'testid=backend-selector' and browser click 'testid=add-backend-menu-item', run
  2. Do
    control-openhands browser click 'testid=add-backend-advanced-toggle'
  3. Check
    control-openhands browser attr 'testid=add-backend-advanced-toggle' aria-expanded

    (true) and

  4. Check
    control-openhands browser snapshot 'testid=add-backend-cloud-panel'

    (textbox Host with placeholder https://app.all-hands.dev).

Device flow error #

  1. Do
    control-openhands browser fill 'testid=add-backend-cloud-host' http://127.0.0.1:9
  2. Do
    control-openhands browser click 'testid=add-backend-login-button' --observe 'testid=add-backend-device-flow'
  3. Wait
    control-openhands browser wait 'testid=add-backend-auth-error' --timeout 15000
  4. Expect
    The observation goes Starting authentication... → Failed to start device flow: Failed to fetch / Try again, and
  5. Check
    control-openhands browser network --last 5

    shows POST /oauth/device/authorize to http://127.0.0.1:9.

  6. Do
    control-openhands browser click 'testid=add-backend-auth-retry'
  7. Note
    starts again and fails the same way.
  8. Note
    Each attempt leaves an about:blank popup:
  9. Do
    control-openhands browser tabs
  10. Do
    control-openhands browser close-tab <i>
  11. Note
    for each extra page and
  12. Do
    control-openhands browser tab 0
  13. Note
    Close the modal with testid=add-backend-close.

Device flow awaiting and Cancel #

  1. Note
    Reopen Add Backend (Cloud tab, default host), run
  2. Do
    control-openhands browser click 'testid=add-backend-login-button' --observe 'testid=add-backend-device-flow' --observe-ms 6000
  3. Check
    control-openhands browser snapshot 'testid=add-backend-device-flow'
  4. Note
    If https://app.all-hands.dev is reachable (control-openhands browser network --external --last 5 lists it), the snapshot shows Waiting for authorization..., Browser opened. Complete sign-in to continue. If browser didn't open, visit:, a link https://app.all-hands.dev/oauth/device/verify?user_code=<code> and button Cancel; browser tabs lists a second page on that URL.
  5. Note
    Screenshot with --feature F25.cloud-device-flow --name awaiting, then
  6. Do
    control-openhands browser click 'testid=add-backend-auth-cancel'
  7. Note
    : the panel is back to Connect to OpenHands / Advanced and browser tabs lists one page.
  8. Note
    Never approve the code.
  9. Note
    If Cloud is unreachable you get the error state of the previous bullet instead.

Agent-server guidance #

  1. Do
    control-openhands browser click 'testid=add-backend-option-agent-server'
  2. Check
    control-openhands browser snapshot 'testid=add-backend-agent-server-panel'
  3. Note
    : radiogroup Agent-server location with Local checked, heading Before you connect, textboxes Host Name, Host, API Key and button "Connect" [disabled].
  4. Do
    control-openhands browser click 'testid=add-backend-local-guidance-toggle'
  5. Note
    and browser snapshot 'testid=add-backend-local-guidance' show the code agent-canvas --backend-only --port 8001 and link Read the local backend setup guide.
  6. Do
    control-openhands browser click 'testid=add-backend-location-option-remote'
  7. Do
    control-openhands browser click 'testid=add-backend-remote-guidance-toggle'
  8. Note
    browser snapshot 'testid=add-backend-remote-guidance' shows Recommended setup, Connection details and Read the remote backend setup guide.

Connect rules and errors #

  1. Note
    Still on Remote:
  2. Do
    control-openhands browser fill 'testid=add-backend-name' QA_Second
  3. Do
    control-openhands browser fill 'testid=add-backend-host' 127.0.0.1:9
  4. Check
    control-openhands browser enabled 'testid=add-backend-submit'

    is false (Remote needs a key).

  5. Do
    control-openhands browser fill 'testid=add-backend-api-key' wrong-key
  6. Note
    makes it true.
  7. Do
    control-openhands browser click 'testid=add-backend-location-option-local'
  8. Note
    keeps the fields.
  9. Do
    control-openhands browser click 'testid=add-backend-submit' --observe 'testid=add-backend-submit'

    shows Checking…, then

  10. Check
    control-openhands browser text 'testid=add-backend-error'
  11. Note
    starts Could not connect to http://127.0.0.1:9 / Disconnected (check URL or network). ....
  12. Note
    Fill add-backend-host with <second-url> and submit again: the error reads Could not connect to <second-url> / Invalid API key.
  13. Note
    On Local the key is optional for Connect but not for a key-protected host:
  14. Do
    control-openhands browser fill 'testid=add-backend-api-key' ''
  15. Note
    leaves
  16. Check
    control-openhands browser enabled 'testid=add-backend-submit'
  17. Note
    at true (browser value 'testid=add-backend-api-key' reports length 0), and
  18. Do
    control-openhands browser click 'testid=add-backend-submit'
  19. Wait
    control-openhands browser wait 'testid=add-backend-error' --timeout 15000

    shows the same Could not connect to <second-url> / Invalid API key (browser screenshot --feature F25.add-agent-server --name local-no-key-error).

  20. Note
    Close with testid=add-backend-close.

Add the second backend #

  1. Do
    control-openhands browser goto /conversations/<id>
  2. Note
    browser click 'testid=backend-selector', browser click 'testid=add-backend-menu-item', browser click 'testid=add-backend-option-agent-server', fill add-backend-name with QA_Second and add-backend-host with <second-url>, then
  3. Check
    control-openhands browser fill 'testid=add-backend-api-key' --value-file "$OH_VERIFY_RUN_2/private/session-key"
  4. Check
    control-openhands browser click 'testid=add-backend-submit' --expect-url '/conversations(\?|$)'
  5. Expect
    The URL is /conversations, browser count 'testid=add-backend-modal' is 0, browser snapshot 'testid=backend-selector' shows combobox "QA_Second", and browser count 'testid=conversation-card' is 0.
  6. Note
    Before anything reloads the page,
  7. Check
    control-openhands browser toasts --history

    lists only Loading... and the new backend's Your LLM isn't set up yet, so conversations won't run. ... (the second stack has no LLM profile; no error toast), and

  8. Check
    control-openhands browser network --external --filter 'ids='

    lists no request: the new backend is never asked for the first stack's conversation id. (--external leaves out this stack's own GET /api/conversations?ids=<id> from the goto /conversations/<id> above, which the request log keeps across page loads; <second-url> is another origin, so a request to it is listed.) The new backend asks its own telemetry consent ("This preference is saved for the local backend “QA_Second”"): run

  9. Do
    control-openhands onboard --skip

    (it leaves the browser on /).

  10. Note
    After
  11. Do
    control-openhands browser reload
  12. Note
    the selector still reads QA_Second.

Switch with the overlay #

  1. Note
    From / run browser click 'testid=backend-selector' and
  2. Do
    control-openhands browser click 'testid=backend-selector >> role=option[name="Connected Local"]' --observe 'testid=environment-switch-overlay' --observe-ms 2500
  3. Expect
    The observation shows Switching to Local, then <absent> about one second later; the selector reads Local and browser count 'testid=conversation-card' is 1.
  4. Check
    control-openhands browser click 'testid=conversation-card' --expect-url '/conversations/[0-9a-f-]+'
  5. Note
    open the selector and
  6. Do
    control-openhands browser click 'testid=backend-selector >> role=option[name="Connected QA_Second"]' --expect-url '/conversations(\?|$)'
  7. Note
    : the URL is /conversations, the selector reads QA_Second and the card count is 0.

Pinned URL #

  1. Note
    With QA_Second active run
  2. Do
    control-openhands browser goto '/conversations/<id>?backend=default-local'
  3. Wait
    control-openhands browser wait 'testid=chat-interface'
  4. Note
    : the selector reads Local and the conversation opens.
  5. Do
    control-openhands browser goto '/conversations?backend=qa-unknown-id'
  6. Note
    keeps Local (card count 1).
  7. Expect
    The sidebar card links themselves end in ?backend=default-local (browser snapshot).

Per-tab backend #

  1. Note
    With Local active and QA_Second registered, run
  2. Do
    control-openhands browser goto /conversations
  3. Do
    control-openhands browser eval "JSON.parse(sessionStorage.getItem('openhands-active-backend')).backendId"

    (default-local); note QA_Second's id with

  4. Do
    control-openhands browser eval "JSON.parse(localStorage.getItem('openhands-backends')).find(b=>b.name==='QA_Second').id"

    (<second-id>, a generated UUID).

  5. Note
    Open a plain second tab, as a user opening the app again:
  6. Do
    control-openhands browser tab new /conversations
  7. Note
    prints index 1 and makes it the daemon's active tab (control-openhands browser tabs lists two pages at /conversations with active 1);
  8. Wait
    control-openhands browser wait-tab '/conversations$' --new --timeout 5000
  9. Note
    then prints index 0, the tab you came from, because --new leaves out the tab the daemon is on and that is now the new one (run from browser tab 0 it prints index 1, the second tab).
  10. Do
    control-openhands browser tab 1
  11. Wait
    control-openhands browser wait 'testid=backend-selector'
  12. Note
    : browser snapshot 'testid=backend-selector' reads combobox "Local" and
  13. Do
    control-openhands browser eval "sessionStorage.getItem('openhands-active-backend')"

    is null (a new tab inherits nothing and starts from the localStorage fallback).

  14. Note
    Switch this tab: browser click 'testid=backend-selector',
  15. Do
    control-openhands browser click 'testid=backend-selector >> role=option[name="Connected QA_Second"]'
  16. Wait
    control-openhands browser wait 'testid=environment-switch-overlay' --state detached --timeout 10000
  17. Note
    the selector reads QA_Second, browser count 'testid=conversation-card' is 0 and
  18. Do
    control-openhands browser eval "JSON.parse(localStorage.getItem('openhands-active-backend')).backendId"

    is <second-id>.

  19. Note
    Back in
  20. Do
    control-openhands browser tab 0
  21. Note
    nothing moved: the selector still reads Local, the card count is 1, the sessionStorage eval is still default-local while the localStorage eval is <second-id> (browser screenshot --feature F25.per-tab-backend --name first-tab-unchanged).
  22. Expect
    A plain new tab starts from the last choice made in any tab: from tab 0 run
  23. Do
    control-openhands browser tab new /conversations

    (index 2, now active), browser wait 'testid=backend-selector': the selector reads QA_Second, the card count is 0, its sessionStorage item is null and the localStorage eval is <second-id> (--name plain-new-tab-last-choice).

  24. Expect
    A conversation row opens on its owner instead:
  25. Do
    control-openhands browser tab 0
  26. Do
    control-openhands browser click 'testid=conversation-card' --modifiers Control

    (add >> nth=0 when the run holds more conversations than the precondition's one: the click is strict),

  27. Wait
    control-openhands browser wait-tab '/conversations/<id>' --timeout 20000
  28. Note
    prints index 3 and the URL /conversations/<id>?backend=default-local;
  29. Do
    control-openhands browser tab 3
  30. Wait
    control-openhands browser wait 'testid=chat-interface' --timeout 30000
  31. Note
    browser url still ends in /conversations/<id>?backend=default-local (no bounce to /conversations), the selector reads Local and browser toasts --history lists only Loading... (no This conversation does not exist…); browser screenshot --feature F25.per-tab-backend --name new-tab-on-owner.
  32. Note
    Close the extra tabs highest first:
  33. Do
    control-openhands browser close-tab 3
  34. Do
    control-openhands browser close-tab 2
  35. Do
    control-openhands browser close-tab 1
  36. Do
    control-openhands browser tab 0
  37. Note
    browser tabs lists one page and the selector reads Local.
  38. Note
    Opening the pinned link also recorded default-local as the last choice (the localStorage eval in tab 3 reads default-local), so run the plain-new-tab check before the row check.

Manage list and closing #

  1. Note
    Run browser click 'testid=backend-selector',
  2. Do
    control-openhands browser click 'testid=manage-backends-menu-item'
  3. Check
    control-openhands browser snapshot 'testid=manage-backends-modal'
  4. Note
    : heading Manage backends, two rows Connected Local v1.50.1 <this run's URL> Connected Local and Connected QA_Second v1.50.1 <second-url> Connected Local, each with Edit and Remove, then Add Backend and Done. browser text 'testid=manage-backends-status-QA_Second' is Connected and browser text 'testid=manage-backends-version-QA_Second' is v1.50.1 (whatever doctor reports).
  5. Note
    Screenshot with --feature F25.manage-backends --name list.
  6. Note
    Close it four ways, reopening between: browser press Escape,
  7. Do
    control-openhands browser mouse-click 20 500

    (backdrop), browser click 'testid=close-manage-backends-modal', browser click 'testid=manage-backends-done'; each time browser count 'testid=manage-backends-modal' is 0.

Manage backends modal listing one connected Local backend and Add Backend.
Manage backends exposes the connected local server and edit, remove and add controls. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Manage backends displays the connected Local server and its version with Edit and Remove.

One local backend only. No second backend, Cloud sign-in, organization, sharing or device authorization tested.

How this screenshot was taken

agent server: 1.53.0 · automation: 1.19.0 (launcher default) · canvas: 1.26.0

control-openhands browser goto /
control-openhands browser click testid=backend-selector
control-openhands browser click testid=manage-backends-menu-item
control-openhands browser screenshot --feature F25.manage-backends --name manage-local

Select from Manage #

  1. Note
    With Local active,
  2. Check
    control-openhands browser click 'testid=conversation-card' --expect-url '/conversations/[0-9a-f-]+'
  3. Note
    open Manage Backends, then
  4. Do
    control-openhands browser click 'testid=manage-backends-row-QA_Second >> role=button >> nth=0' --expect-url '/conversations(\?|$)'
  5. Note
    Like the selector, the app leaves the detail page for /conversations (BM-002): browser count 'testid=manage-backends-modal' is 0, the selector reads QA_Second, browser count 'testid=chat-interface' and browser count 'testid=conversation-card' are 0, and browser screenshot --feature F25.manage-select --name after-select shows QA_Second's Home with its empty conversation list.

Edit validation and errors #

  1. Note
    Open Manage Backends,
  2. Do
    control-openhands browser click 'testid=manage-backends-edit-QA_Second'
  3. Note
    then browser value 'testid=edit-backend-host' (<second-url>), browser attr 'testid=edit-backend-api-key' type (password) and browser text 'testid=edit-backend-status' (Connected, ·, Local, v1.50.1 on separate lines).
  4. Note
    Fill edit-backend-name with an empty string and edit-backend-host with not a host, then browser focus 'testid=edit-backend-api-key' and browser snapshot 'testid=edit-backend-form': alerts Name is required and Enter a valid URL (e.g. http://localhost:8080); browser enabled 'testid=edit-backend-submit' is false.

Escape in Edit #

  1. Note
    In the same Edit modal press
  2. Do
    control-openhands browser press Escape
  3. Note
    Expected: the Edit modal stays (it disables Escape) or only it closes.
  4. Note
    Today browser count 'testid=edit-backend-modal' and browser count 'testid=manage-backends-modal' are both 0: the whole stack closes and the draft is lost.
  5. Note
    Nothing was saved (browser snapshot 'testid=backend-selector' still reads QA_Second).

Edit and save #

  1. Note
    Reopen Manage Backends and the pencil.
  2. Note
    Fill edit-backend-host with http://127.0.0.1:9, browser click 'testid=edit-backend-submit', browser wait 'testid=edit-backend-error' --timeout 10000: the text starts Could not connect to http://127.0.0.1:9 and the modal stays.
  3. Note
    Fill the host back with <second-url> and edit-backend-name with QA_Renamed, click edit-backend-submit, then
  4. Wait
    control-openhands browser wait 'testid=edit-backend-modal' --state detached --timeout 10000
  5. Note
    browser count 'testid=manage-backends-row-QA_Renamed' is 1.
  6. Note
    Click manage-backends-done, browser reload: the selector reads QA_Renamed.

Health status #

  1. Note
    With QA_Renamed the active backend (as the Edit bullet's reload leaves it; the closed selector shows only the active backend's dot), run
  2. Do
    control-openhands service stop agent-server --run "$OH_VERIFY_RUN_2"
  3. Note
    then on the open page
  4. Wait
    control-openhands browser wait 'testid=backend-selector >> role=status[name="Disconnected"]' --timeout 40000
  5. Note
    browser click 'testid=backend-selector' and browser snapshot 'testid=backend-selector' show option "Disconnected QA_Renamed" next to option "Connected Local".
  6. Note
    In Manage Backends browser text 'testid=manage-backends-status-QA_Renamed' is Disconnected (check URL or network), testid=manage-backends-status-detail-QA_Renamed repeats it with Check that the backend URL is correct ..., and browser enabled 'testid=manage-backends-row-QA_Renamed >> role=button >> nth=0' is false.
  7. Expect
    An error toast with the same text also appears.
  8. Note
    Screenshot with --feature F25.health-status --name manage-down.

Recovery gate #

  1. Note
    Close the modal,
  2. Do
    control-openhands browser reload
  3. Wait
    control-openhands browser wait 'testid=manage-backends-modal' --timeout 20000
  4. Note
    browser testids lists agent-server-onboarding-screen and the Manage rows but no sidebar; browser count 'testid=close-manage-backends-modal' and browser count 'testid=manage-backends-done' are 0, and browser press Escape leaves the modal (count 1).
  5. Do
    control-openhands browser click 'testid=manage-backends-row-Local >> role=button >> nth=0'
  6. Note
    and browser wait 'testid=agent-server-onboarding-screen' --state detached --timeout 15000 restore the app: selector Local, card count 1.
  7. Note
    Screenshot the gate with --feature F25.recovery-gate --name gate before choosing.

Remove the active backend #

  1. Do
    control-openhands restart --run "$OH_VERIFY_RUN_2"
  2. Note
    browser click 'testid=backend-selector', browser wait 'testid=backend-selector >> role=option[name="Connected QA_Renamed"]' --timeout 25000 and click that option (selector reads QA_Renamed).
  3. Note
    Open Manage Backends,
  4. Do
    control-openhands browser click 'testid=manage-backends-remove-QA_Renamed'
  5. Note
    browser text 'testid=confirmation-modal' reads Remove backend "QA_Renamed"? If it is active, the app will switch back to Local.
  6. Do
    control-openhands browser click 'testid=confirmation-modal >> role=button[name="Cancel"]'
  7. Note
    keeps the row (count 'testid=manage-backends-row-QA_Renamed' 1).
  8. Note
    Click the trash again, then
  9. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button'
  10. Note
    : the row count is 0.
  11. Note
    Click Done and browser reload: the selector reads Local, browser count 'testid=backend-selector >> role=option' (dropdown open) is 1, and the card count is 1.

Add from Manage, Edit Cancel and X #

  1. Expect
    The second stack is running again after the previous bullet.
  2. Note
    Run browser goto /conversations/<id>, browser wait 'testid=chat-interface', open the selector, click manage-backends-menu-item, then
  3. Do
    control-openhands browser click 'testid=manage-backends-add'
  4. Wait
    control-openhands browser wait 'testid=add-backend-modal' --timeout 5000

    (visible); browser count 'testid=manage-backends-modal' is 1 underneath. browser click 'testid=add-backend-close': the Add count is 0 and the Manage count stays 1.

  5. Note
    Click manage-backends-add again, add-backend-option-agent-server, fill add-backend-name with QA_Second, add-backend-host with <second-url> and add-backend-api-key with --value-file "$OH_VERIFY_RUN_2/private/session-key", then browser click 'testid=add-backend-submit' --expect-url '/conversations(\?|$)': the URL is /conversations, count 'testid=add-backend-modal' is 0, count 'testid=manage-backends-modal' is still 1, count 'testid=manage-backends-row-QA_Second' is 1 and the selector reads QA_Second (screenshot --feature F25.manage-add --name after-add).
  6. Expect
    No consent prompt this time: it is stored on the second backend.
  7. Do
    control-openhands browser click 'testid=manage-backends-edit-QA_Second'
  8. Note
    browser fill 'testid=edit-backend-name' QA_Draft and
  9. Do
    control-openhands browser click 'testid=edit-backend-cancel'
  10. Note
    : count 'testid=edit-backend-modal' 0, count 'testid=manage-backends-modal' 1, row QA_Second 1, row QA_Draft 0.
  11. Note
    Repeat with
  12. Do
    control-openhands browser click 'testid=edit-backend-close'
  13. Note
    : same counts, and reopening the pencil shows browser value 'testid=edit-backend-name' QA_Second; leave with edit-backend-cancel.
  14. Note
    Remove it again: browser click 'testid=manage-backends-remove-QA_Second', browser click 'testid=confirmation-modal >> testid=confirm-button', browser click 'testid=manage-backends-done' and browser reload: the selector reads Local.

Collapsed rail #

  1. Do
    control-openhands browser click 'testid=sidebar-collapse-toggle'

    (attr 'aside[data-collapsed]' data-collapsed is true),

  2. Do
    control-openhands browser hover 'testid=collapsed-backend-selector-link'
  3. Do
    control-openhands browser click 'testid=add-backend-menu-item'
  4. Wait
    control-openhands browser wait 'testid=add-backend-modal' --timeout 5000

    (visible).

  5. Note
    Close it, hover again, click manage-backends-menu-item and browser wait 'testid=manage-backends-modal' --timeout 5000.
  6. Note
    Close with Done and expand with browser click 'testid=sidebar-collapse-toggle'.

Phone #

  1. Do
    control-openhands browser viewport phone
  2. Note
    browser goto /conversations,
  3. Do
    control-openhands browser click 'testid=sidebar-mobile-menu-toggle'
  4. Do
    control-openhands browser click 'testid=sidebar-mobile-drawer >> testid=backend-selector'
  5. Note
    browser click 'testid=add-backend-menu-item', browser click 'testid=add-backend-option-agent-server' and
  6. Check
    control-openhands browser bbox 'testid=add-backend-modal'
  7. Note
    : insideViewport true, pageHorizontalOverflow false.
  8. Note
    Screenshot --feature F25.phone --name add-modal.
  9. Note
    Close it (the drawer stays open), open Manage Backends from the drawer selector the same way and check bbox 'testid=manage-backends-modal' the same.
  10. Note
    Return with browser viewport desktop.

Device verify page #

  1. Do
    control-openhands browser goto /oauth/device/verify
  2. Note
    and browser snapshot: heading Device Authorization, Enter the code displayed on your device:, textbox Device Code: and button Continue, inside the normal app shell.
  3. Do
    control-openhands browser fill 'role=textbox[name="Device Code:"]' QA-0000
  4. Note
    and browser click 'role=button[name="Continue"]' go straight to the result: heading Error, Failed to authorize device. Please try again., button Try Again; browser network --filter verify-authenticated shows POST /oauth/device/verify-authenticated 404.
  5. Do
    control-openhands browser goto '/oauth/device/verify?user_code=QA-0000'
  6. Note
    : heading Device Authorization Request, DEVICE CODE, QA-0000, Security Notice, buttons Cancel and Authorize Device (screenshot --feature F25.device-verify-page --name with-code). browser click 'role=button[name="Cancel"]' does nothing in a tab the page did not open (URL unchanged). browser click 'role=button[name="Authorize Device"]' and browser wait 'role=heading[name="Error"]' show the same Error card; Try Again reloads to the request.

Shared view #

  1. Do
    control-openhands browser goto /shared/conversations/<id>
  2. Wait
    control-openhands browser wait-text 'Conversation not found' --timeout 15000
  3. Expect
    The page has no sidebar; browser network --filter shared shows GET /api/shared-conversations and /api/shared-events/search 404.
  4. Note
    Screenshot --feature F25.shared-conversation-view --name local-not-found.

No Public Share locally #

  1. Note
    Run browser goto /conversations/<id>,
  2. Do
    control-openhands browser click 'testid=conversation-name >> testid=ellipsis-button'
  3. Check
    control-openhands browser count 'testid=share-publicly-button'
  4. Note
    : 0 on a local backend (browser testids --filter button lists rename-button, show-skills-button, show-agent-tools-button, export-transcript-button, download-trajectory-button, display-cost-button and delete-button; show-hooks-button and stop-button appear only while the conversation is active (idle, running, waiting for confirmation or finished), so a paused or errored conversation has neither).
  5. Expect
    The toggle, copy-share-link-button and open-share-link-button need a Cloud backend: blocked.

Cleanup #

  1. Note
    browser press Escape, then
  2. Do
    control-openhands stop --run "$OH_VERIFY_RUN_2"
  3. Note
    and unset OH_VERIFY_RUN_2.
  4. Expect
    The conversation stays with this run's state.

Errors #

  1. Check
    control-openhands browser errors --app-only
  2. Note
    after the family shows no page errors.
  3. Note
    Expected app-origin entries: POST /oauth/device/verify-authenticated 404 (twice), the shared-view 404s, the warning Scripts may close only the windows that were opened by them. from the device-verify Cancel, and a burst of CORS console.errors for <second-url> while the second agent server is stopped or restarting (see Gotchas).

Gotchas and known limits

  • Export the second run as OH_VERIFY_RUN_2 and pass it with --run; never export it as OH_VERIFY_RUN, or the browser commands look for a browser that run does not have. OH_VERIFY_RUN= control-openhands launch --new ... keeps the launch from reusing this run.
  • The second backend asks its own telemetry consent the first time it becomes active, over whatever page you are on; onboard --skip answers it. Consent is stored per backend, so a later restart of the second run does not ask again.
  • browser fill <sel> --value-file FILE takes no positional value; it keeps the key out of the shell history and the evidence. Edit forms show the stored key masked, but browser snapshot of the Edit form prints it in clear: do not save that snapshot as evidence with a real key.
  • Manage-row test ids use the backend name (manage-backends-row-QA_Second), so they change after a rename and are not unique when two backends share a name; the Cloud Log back in id uses the backend id instead (manage-backends-login-<id>-login-button).
  • Selector options are named <status> <name> (Connected Local, Disconnected QA_Renamed): select them with role=option[name="Connected <name>"].
  • After a navigating option click, --expect-url must allow ?backend=...: end regexes with (\?|$).
  • browser wait-tab <regex> matches any open page, including the current one, unless --new leaves out the tab the daemon is on: for a second tab at the same URL (/conversations from New Chat) use wait-tab '/conversations$' --new, or read browser tabs and switch with browser tab <i>. browser tab new /conversations opens a plain second tab (no opener, empty sessionStorage), the state a user reaches by opening the app again. Close extra tabs highest index first, or the indexes shift under you.
  • browser tab new switches the daemon to the tab it opened, so a wait-tab <regex> --new run right after it reports the tab you came from (index 0 with two tabs); browser tab 0 first when you want the new tab's index from wait-tab. A tab new and a click --modifiers Control on New Chat reach the same state (selector from the localStorage fallback, sessionStorage item null).
  • A tab opened with click --modifiers Control starts with an empty sessionStorage, so it boots from the localStorage fallback (the backend last chosen in any tab) unless its URL carries ?backend=. Opening a ?backend= link writes that backend to localStorage too: a per-tab check that relies on "last choice" must run before opening pinned links.
  • The Add Backend Agent-server form has no inline "required" messages; it only disables Connect. The messages exist in the Edit form.
  • Footer clicks from the collapsed popover or the phone drawer open the modals a moment later: browser wait for them instead of count.
  • The device-flow popup survives errors: every failed attempt leaves an about:blank tab (#17953). Cancel closes it; after an error close it with browser close-tab.
  • With the default host the device flow talks to the real app.all-hands.dev (POST /oauth/device/authorize, then token polling). Starting and cancelling is harmless; never approve a code.
  • While the second stack's agent server is stopped or restarting, its ingress answers without CORS headers, so the browser logs Access to fetch at '<second-url>/api/settings' ... blocked by CORS policy (also /server_info, /api/conversations/search, /api/llm/models/verified). That is the dead upstream, not a CORS bug; a healthy second stack sends access-control-allow-origin for this origin.
  • Local and remote backends are probed every 30 s (settings + server_info), Cloud backends every 5 min (#18128), so allow up to 40 s for a local dot to change. While the active backend is down, any reload lands on the recovery gate: drive the disconnected rows on the page that was already loaded.
  • /oauth/device/verify is a Cloud page that also ships in Canvas: on a local stack its POST target does not exist, so Authorize always ends in Error. The "Authentication Required" state is unreachable (useIsAuthed always resolves true).
  • /shared/conversations/<id> on a local backend also raises a raw toast HTTP request failed (404 Not Found): {"detail":"Not Found"} next to Conversation not found (#17953).
  • Known failure (repro candidate, not an exemption): F25.edit-escape closes the Manage modal underneath and drops the Edit draft (Cancel and X close only Edit, F25.edit-cancel) (#17953).
  • The add bullet's network --external --filter 'ids=' check guards an earlier race: on a backend's first activation from /conversations/<id>, about 1 attempt in 5 asked the new backend for the old conversation id. It did not happen in 15 first activations at 4eff80237: each time QA_Second was removed, its consent unset with control-openhands api PATCH /api/settings --run "$OH_VERIFY_RUN_2" --write --data '{"misc_settings_diff":{"app_preferences":{"user_consents_to_analytics":null}}}' (arrange only) and the backend added again from /conversations/<id>. A backend that lacks an id answers [null], which now reads as "not found" (only the This conversation does not exist… toast), not as incompatible data. No merged change was shown to fix the race itself (#18009 only changed how a [null] answer reads; #18024 only moved the redirect into a shared hook, still called after addBackend), so a hit (an ids= request to <second-url>) is a bug to file. At 95115e84f6dd it hit on 1 of 2 first activations from /conversations/<id> (one GET <second-url>/api/conversations?ids=<id> answered 200, with no This conversation does not exist… toast after it). The hit followed a failed Connect in the same modal (an empty key, Invalid API key) before the successful one; the clean retry, which connected once, had none, so a repro should try both sequences. On 2026-10-08 at d2c89252d it did not reproduce in 17 first activations (8 clean, 9 failed-then-clean, some under CPU load and some human-paced): every ids= capture was empty, and no merged change touched the activation logic since the hit, so it stays a watch item rather than a filed defect. Record the Add bullet as fail when the request appears, even though the redirect and the selector look right.
  • Toasts are not page or HTTP errors, so browser errors --app-only misses them: read browser toasts --history after every add or switch, before anything reloads the page (the history starts with each page load).
  • F25.locked-cloud needs scripts/static-server.mjs --lock-to-cloud <url>, which neither bin/agent-canvas.mjs nor control-openhands launch can pass today; even with it, reconnecting needs a Cloud account.
  • The Cloud tab title truncates to OpenHan… at 390 px when the Agent-server tab is selected; the layout still fits.

Source paths: src/components/features/backends/, src/contexts/active-backend-context.tsx, src/api/backend-registry/, src/hooks/query/use-backends-health.ts, src/routes/device-verify.tsx, src/routes/shared-conversation.tsx, src/components/features/conversation/conversation-name-context-menu.tsx, src/api/cloud/, specs/backend-management.md.