EN / field notes OpenHands feature map

OpenHands / F13

Agent profiles

Settings → Agent is a library of named agent profiles. Each profile is either an OpenHands agent (an LLM profile plus sub-agents, the LLM-switch tool, parallel tool calls, and MCP-server and secret scopes) or an ACP external agent (a preset such as Claude Code, Codex or Gemini CLI, its launch command, a model and provider credentials). One profile is active; new conversations started from Home launch from it.

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

How to get to it

  • Sidebar settings gear (backend-selector-settings-link): on desktop /settings redirects to /settings/agents. In the settings navigation, Agent (sidebar-settings-/settings/agents).
  • Direct URL /settings/agents; the retired /settings/agent redirects there.
  • Command menu (Control+k/Meta+k or command-menu-trigger): Agent settings.
  • Getting-started checklist Customize your agent and the composer + → Switch agent profile → Manage agent profiles (both mapped in F09/F05).
  • On Home, the composer + → Switch agent profile list activates a profile like Set as active (F05.agent-profile-switch).
  • All per-profile actions are in the row's ... menu (agent-profile-menu-trigger). The editor has no route: it replaces the list in place.

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) on a fresh run. F13.editor-no-llm must run before control-openhands llm preset deepseek; everything from F13.stale-llm-ref on needs the preset (deepseek-flash active, deepseek-pro).
  • A fresh run has one profile, default, active, whose LLM profile is default (which does not exist). control-openhands api GET /api/agent-profiles shows it.
  • F13.mcp-scope needs two configured MCP servers. Arrange them with control-openhands api POST /api/settings/mcp/qa_mcp_a --data '{"transport":"stdio","command":"qa-no-such-command","enabled":false}' --write and the same for qa_mcp_b.
  • F13.acp-conversation needs the provider's agent CLI and an approved account; F13.cloud-read-only needs a Cloud organization member login. Both are blocked here.
  • Dropdowns are autocompletes. Pick an option with control-openhands browser choose 'testid=<testid>' '<label>' (proven on agent-type-selector, agent-preset-selector, agent-model-selector (Custom), agent-profile-llm-selector, agent-settings-mcp-mode and agent-settings-secrets-mode). The manual opener, control-openhands browser click 'div:has(> div > input[data-testid=<testid>]) >> role=button[name="Show suggestions"]' then browser click 'role=option[name="<label>"]', works for most of them but is flaky right after an editor opens (see Gotchas); clicking the input itself can open an empty, filtered list.

Behavior inventory

20 stable behavior IDs and their expected behavior
  • F13.list under Available Profiles, rows (sorted by name) show the profile name, then its LLM profile (OpenHands) or ACP (external subprocess), a Default badge on the active profile and a ... menu (Edit, Set as active, Delete). Read recipe ↓
  • F13.create Add agent profile opens the editor; Save toasts Profile "<name>" created and the row is listed after a reload. Read recipe ↓
  • F13.name-validation an empty or malformed name marks the field invalid (red rule) and disables Save; a duplicate name disables Save without a message. Read recipe ↓
  • F13.editor-no-llm with no LLM profiles, an OpenHands profile shows Create an LLM profile first to define an OpenHands agent. and Save toasts Select an LLM profile for this agent. without creating anything. Read recipe ↓
  • F13.openhands-options OpenHands profiles offer an LLM profile dropdown, Enable sub-agents (off by default), Let the agent switch LLM profiles (on by default) and Parallel tool calls (default 1, minimum 1); values persist and reopen. Read recipe ↓
  • F13.edit Edit reopens the stored values with Save disabled until something changes; renaming keeps the profile id; Back and Cancel leave without saving. Read recipe ↓
  • F13.stale-llm-ref editing a profile whose LLM profile no longer exists preselects the active LLM profile and enables Save, so saving heals the reference. Read recipe ↓
  • F13.set-active Set as active toasts Switched to profile "<name>", moves the Default badge and is disabled on the active row. Read recipe ↓
  • F13.active-drives-conversation a conversation started from Home after activation launches from that profile (its id and settings are stamped on the conversation). Read recipe ↓
  • F13.llm-pill-precedence when the active profile pins a different LLM profile than the Home LLM pill, the launch uses the pill's model and not the profile's other settings (by design). Read recipe ↓
  • F13.mcp-scope MCP servers offers All servers (every configured server listed, switched on and disabled) or Choose servers (a switch per configured server); the choice persists; a chosen server that was removed stays listed with a red warning, and launching with it is refused. Read recipe ↓
  • F13.secrets-scope Secrets offers No profile restriction (every switch on and disabled) or Choose secrets; on an ACP profile, Choose preselects that provider's secret names; the selection persists. Read recipe ↓
  • F13.acp-form choosing ACP (external subprocess) shows Preset, Command and Model; each preset prefills its command and model, Custom empties the command and disables Save until one is typed, typing a preset's exact command selects that preset, and the Model list's Custom opens a free-text model field. Read recipe ↓
  • F13.acp-credentials built-in presets show a Credentials section (password fields), an auth banner (signed in / configured), a conflict warning for mutually exclusive Claude credentials, and Already saved — leave blank to keep for stored values. Credentials are saved as global secrets. Read recipe ↓
  • F13.acp-preset-credentials the Credentials fields follow the preset: Claude Code CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL; Codex OPENAI_API_KEY, CODEX_AUTH_JSON (multi-line), OPENAI_BASE_URL; Gemini CLI GOOGLE_APPLICATION_CREDENTIALS_JSON (multi-line), GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION, GOOGLE_GENAI_USE_VERTEXAI, GEMINI_API_KEY, GEMINI_BASE_URL; Custom has no Credentials section. With no host login and no stored credential (Gemini here) no auth banner shows. Read recipe ↓
  • F13.menu-keyboard opening the row's ... menu focuses Edit; ArrowDown/ArrowUp move focus between the enabled items (wrapping, and skipping the disabled Set as active on the active row), Enter runs the focused item, and Tab, Escape or a click outside close the menu. Read recipe ↓
  • F13.delete Delete asks for confirmation; Cancel keeps the row, Delete removes it. Deleting the active profile leaves none active; deleting the last profile makes the server re-seed default. Read recipe ↓
  • F13.phone the list, row menu and editor fit a 390 px viewport without horizontal overflow. Read recipe ↓
  • F13.acp-conversation a conversation launched from an active ACP profile runs the external agent. Blocked without an approved provider account. Read recipe ↓
  • F13.cloud-read-only on a Cloud backend, organization members see rows without the ... menu or Add agent profile. Blocked without a Cloud account. 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.

List #

  1. Note
    From / run
  2. Check
    control-openhands browser click 'testid=backend-selector-settings-link' --expect-url '/settings'
  3. Check
    control-openhands browser url

    (/settings/agents) and

  4. Check
    control-openhands browser text 'testid=agent-profile-row'
  5. Note
    : default, its LLM profile default, Default on three lines.
  6. Check
    control-openhands browser count 'testid=agent-profile-active-badge'

    is 1;

  7. Do
    control-openhands browser screenshot --feature F13.list --name list
  8. Note
    Command menu: run
  9. Do
    control-openhands browser goto /

    (the browser is on /settings/agents, where the URL wait would pass at once),

  10. Do
    control-openhands browser press Control+k
  11. Do
    control-openhands browser type 'testid=command-menu >> role=combobox' 'Agent settings'
  12. Do
    control-openhands browser press Enter
  13. Wait
    control-openhands browser wait-url '/settings/agents(\?|$)'
  14. Note
    Legacy URL:
  15. Do
    control-openhands browser goto /settings/agent

    returns .../settings/agents.

No LLM profile #

  1. Note
    Before the preset, run
  2. Do
    control-openhands browser click 'testid=add-agent-profile'
  3. Check
    control-openhands browser text 'testid=agent-profile-no-llm'
  4. Note
    : Create an LLM profile first to define an OpenHands agent.
  5. Check
    control-openhands browser text 'testid=agent-profile-editor-title'

    is Add agent profile and

  6. Check
    control-openhands browser text 'testid=agent-profile-editor-description'

    is Configure the agent below, then click Save to create this profile. (agent copy, not the LLM page's hint, since #18035).

  7. Do
    control-openhands browser fill 'testid=agent-profile-name-input' QA_NoLlm
  8. Note
    Save turns enabled (control-openhands browser enabled 'testid=save-agent-profile-btn').
  9. Note
    Click testid=save-agent-profile-btn and run
  10. Check
    control-openhands browser toasts
  11. Note
    : Select an LLM profile for this agent. api GET /api/agent-profiles lists no QA_NoLlm.
  12. Note
    Leave with
  13. Do
    control-openhands browser click 'testid=cancel-agent-profile-btn'

Name rules #

  1. Note
    In the Add editor, fill testid=agent-profile-name-input with '', default, 'bad name!' and QA_ok in turn; after each run
  2. Check
    control-openhands browser attr 'testid=agent-profile-name-input' aria-invalid
  3. Check
    control-openhands browser enabled 'testid=save-agent-profile-btn'
  4. Note
    Results: '' → true/false; default (duplicate) → false/false, and nothing explains why; 'bad name!' → true/false; QA_ok → false/true.
  5. Expect
    The rule under the field is always shown (grey) and turns red (text-red-400) while the name is invalid; it reads Profile name must start with a letter or digit, and contain only letters, digits, dots, underscores, or hyphens (max 64 characters).
  6. Note
    Take
  7. Do
    control-openhands browser screenshot --feature F13.name-validation --name duplicate
  8. Note
    with default filled.

ACP form #

  1. Note
    In the Add editor run
  2. Do
    control-openhands browser click 'testid=agent-type-selector'
  3. Do
    control-openhands browser click 'role=option[name="ACP (external subprocess)"]'
  4. Check
    control-openhands browser value 'testid=agent-preset-selector'

    is Claude Code, browser value 'testid=agent-command-input' is npx -y --prefer-offline @agentclientprotocol/claude-agent-acp@0.63.0 and browser value 'testid=agent-model-selector' is Claude Opus (1M); agent-profile-no-llm is gone (ACP needs no LLM profile).

  5. Note
    Pick Codex and Gemini CLI from testid=agent-preset-selector: the command becomes npx -y --prefer-offline @agentclientprotocol/codex-acp@1.10.0 / npx -y --prefer-offline @google/gemini-cli@0.46.0 --acp and the model GPT-5.5 / Gemini 2.5 Pro.
  6. Note
    Pick Custom: the command is empty and Save is disabled.
  7. Do
    control-openhands browser fill 'testid=agent-command-input' 'npx -y qa-custom-acp --stdio'
  8. Note
    enables Save.
  9. Note
    Filling the exact Codex command flips the preset to Codex; appending --verbose flips it back to Custom.
  10. Note
    Model override: with a built-in preset, run
  11. Do
    control-openhands browser choose 'testid=agent-model-selector' 'Custom'

    (or open the model list with its toggle button and control-openhands browser click 'role=option[name="Custom"][exact]');

  12. Check
    control-openhands browser count 'testid=agent-model-input'

    is 1 and empty.

  13. Expect
    The Custom preset shows agent-model-input directly (no model list).

Create an ACP profile #

  1. Note
    With preset Custom, fill testid=agent-profile-name-input with QA_acp_custom, testid=agent-command-input with 'npx -y qa-custom-acp --stdio' and testid=agent-model-input with qa-model, click testid=save-agent-profile-btn, then
  2. Wait
    control-openhands browser wait-text 'Profile "QA_acp_custom" created' --timeout 10000
  3. Do
    control-openhands browser reload
  4. Check
    control-openhands browser text 'testid=agent-profile-row >> has-text=QA_acp_custom'
  5. Note
    : QA_acp_custom / ACP (external subprocess).
  6. Check
    control-openhands api GET /api/agent-profiles/QA_acp_custom

    shows acp_server custom, the command and acp_model qa-model.

Edit and rename #

  1. Do
    control-openhands browser click 'testid=agent-profile-row >> has-text=QA_acp_custom >> testid=agent-profile-menu-trigger'
  2. Check
    control-openhands browser snapshot 'testid=agent-profile-actions-menu'

    lists Edit, Set as active, Delete.

  3. Note
    Click testid=agent-profile-edit: the title is Edit agent profile, the description Editing profile "QA_acp_custom" - save to apply changes, the type/preset/command/model values are the stored ones, and Save is disabled.
  4. Note
    Note the id from api GET /api/agent-profiles, fill the name with QA_acp_renamed, click Save,
  5. Wait
    control-openhands browser wait-text 'Profile "QA_acp_renamed" updated' --timeout 10000
  6. Note
    and browser reload: browser count 'testid=agent-profile-row >> has-text=QA_acp_custom' is 0, the renamed row is 1 and api GET /api/agent-profiles shows the same id under the new name.
  7. Note
    Back/Cancel: open any editor, change a field, click testid=back-to-agent-profiles (or testid=cancel-agent-profile-btn); browser count 'testid=add-agent-profile' is 1 and the API is unchanged.

Credentials #

  1. Note
    In a new ACP editor with preset Claude Code,
  2. Check
    control-openhands browser testids --hidden --filter acp

    lists settings-acp-secret-CLAUDE_CODE_OAUTH_TOKEN, -ANTHROPIC_API_KEY, -ANTHROPIC_BASE_URL and one banner (settings-acp-auth-detected when the agent-server machine has a Claude CLI login, as this sandbox does: You're already signed in to Claude Code — you can leave the fields below blank.).

  3. Check
    control-openhands browser attr 'testid=settings-acp-secret-ANTHROPIC_API_KEY' type

    is password.

  4. Note
    Fill settings-acp-secret-CLAUDE_CODE_OAUTH_TOKEN with qa-dummy-oauth and settings-acp-secret-ANTHROPIC_API_KEY with qa-dummy-key;
  5. Check
    control-openhands browser text 'testid=acp-credential-conflict-warning'

    is Setting ANTHROPIC_API_KEY alongside CLAUDE_CODE_OAUTH_TOKEN breaks its authentication — leave one of them blank. (screenshot --feature F13.acp-credentials --name conflict after browser scroll 'testid=acp-credential-conflict-warning').

  6. Note
    Cancel; api GET /api/settings/secrets gains nothing.
  7. Expect
    Saved credential: new profile QA_codex, ACP, preset Codex, fill testid=settings-acp-secret-OPENAI_API_KEY with qa-dummy-openai, Save (Profile "QA_codex" created); api GET /api/settings/secrets now lists OPENAI_API_KEY and the profile has acp_server codex, acp_command null, acp_model gpt-5.5.
  8. Note
    Reopen it:
  9. Check
    control-openhands browser testids --hidden --filter settings-acp-auth

    shows settings-acp-auth-configured (Credentials for Codex are configured — you can leave the fields below blank.) and

  10. Check
    control-openhands browser attr 'testid=settings-acp-secret-OPENAI_API_KEY' placeholder

    is Already saved — leave blank to keep with an empty value.

Secrets scope #

  1. Note
    In the QA_codex editor,
  2. Do
    control-openhands browser eval "[...document.querySelectorAll('[data-testid=agent-settings-secret-list] input')].map(i=>i.closest('li').innerText.split('\n')[0]+'='+i.checked+(i.disabled?'(disabled)':''))"

    shows every name =true(disabled) under No profile restriction.

  3. Note
    Open testid=agent-settings-secrets-mode,
  4. Do
    control-openhands browser click 'role=option[name="Choose secrets"]'
  5. Note
    and rerun the eval: OPENHANDS_AUTOMATION_API_KEY=false, OPENAI_API_KEY=true, CODEX_AUTH_JSON=true, OPENAI_BASE_URL=true.
  6. Note
    Save (Profile "QA_codex" updated); api GET /api/agent-profiles/QA_codex has those three in secret_refs, and after browser reload and Edit the mode and switches are the same.
  7. Note
    On an OpenHands profile, Choose starts with nothing selected; a switch toggles by its name: in the Add editor pick Choose secrets, then
  8. Do
    control-openhands browser click 'testid=agent-settings-secret-list >> text=OPENHANDS_AUTOMATION_API_KEY'
  9. Note
    turns document.querySelector('[data-testid=agent-settings-secret-OPENHANDS_AUTOMATION_API_KEY]').checked from false to true.
The lower agent profile editor shows MCP servers, secret scope, a deepseek-flash LLM profile selector and Save.
Agent profiles can restrict MCP servers and secrets, and select an LLM profile. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

The lower agent profile editor shows MCP servers, secret scope, a deepseek-flash LLM profile selector and Save.

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. Only the current editor and its options were captured. The recipe names some older controls; sub-agent and switch-LLM enable controls were not exercised or asserted.

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 /settings/agents
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=add-agent-profile
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser fill testid=agent-profile-name-input docs-demo-agent
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=agent-settings-screen
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser testids --filter profile
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser testids --hidden --filter profile
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser scroll testid=save-agent-profile-btn
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=agent-settings-screen
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F13.secrets-scope --name agent-profile-scopes

Stale LLM reference #

  1. Note
    After
  2. Arrange
    control-openhands llm preset deepseek
  3. Do
    control-openhands browser reload
  4. Note
    open testid=agent-profile-row >> has-text=default >> testid=agent-profile-menu-trigger → testid=agent-profile-edit,
  5. Wait
    control-openhands browser wait 'testid=agent-profile-llm-selector'
  6. Note
    browser value 'testid=agent-profile-llm-selector' is deepseek-flash (deepseek/deepseek-flash) and Save is enabled although nothing was touched.
  7. Note
    Save (Profile "default" updated), browser reload: the row reads default / deepseek-flash / Default and api GET /api/agent-profiles/default has llm_profile_ref deepseek-flash with the same id.
  8. Note
    Screenshot --feature F13.stale-llm-ref --name fallback before saving.

OpenHands options #

  1. Note
    Click testid=add-agent-profile, fill the name QA_oh.
  2. Note
    Defaults: browser value 'testid=agent-profile-llm-selector' is the active LLM profile,
  3. Do
    control-openhands browser eval "document.querySelector('[data-testid=agent-settings-enable-sub-agents]').checked"

    is false, the same for agent-settings-enable-switch-llm-tool is true, browser value 'testid=sdk-settings-tool_concurrency_limit' is 1.

  4. Note
    Click 'testid=agent-settings-screen >> text=Enable sub-agents' and 'testid=agent-settings-screen >> text=Let the agent switch LLM profiles' (the checks flip).
  5. Note
    Fill testid=sdk-settings-tool_concurrency_limit with 0 and click Save: browser toasts shows Parallel tool calls must be at least 1 and browser eval "document.querySelector('[data-testid=sdk-settings-tool_concurrency_limit]').validationMessage" is Value must be greater than or equal to 1. Fill 2,
  6. Do
    control-openhands browser choose 'testid=agent-profile-llm-selector' 'deepseek-pro (deepseek/deepseek-v4-pro)'
  7. Note
    Save (Profile "QA_oh" created), browser reload: the row reads QA_oh / deepseek-pro, and api GET /api/agent-profiles/QA_oh has llm_profile_ref deepseek-pro, enable_sub_agents true, enable_switch_llm_tool false, tool_concurrency_limit 2.
  8. Note
    Reopening shows the same values with Save disabled.
OpenHands agent profile editor shows docs-demo-agent, OpenHands default system prompt, parallel tool calls and the Standard tools list.
An agent profile combines its agent type, system prompt, parallel tool limit and tool configuration. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

OpenHands agent profile editor shows docs-demo-agent, OpenHands default system prompt, parallel tool calls and the Standard tools list.

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. Only the current editor and its options were captured. The recipe names some older controls; sub-agent and switch-LLM enable controls were not exercised or asserted.

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 /settings/agents
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=add-agent-profile
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser fill testid=agent-profile-name-input docs-demo-agent
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=agent-settings-screen
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F13.openhands-options --name agent-profile-options --full-page

Set as active #

  1. Do
    control-openhands browser click 'testid=agent-profile-row >> has-text=QA_oh >> testid=agent-profile-menu-trigger'
  2. Do
    control-openhands browser click 'testid=agent-profile-set-active'
  3. Wait
    control-openhands browser wait-text 'Switched to profile "QA_oh"' --timeout 10000
  4. Expect
    After browser reload, browser count 'testid=agent-profile-row >> has-text=QA_oh >> testid=agent-profile-active-badge' and browser count 'testid=agent-profile-active-badge' are both 1.
  5. Note
    Reopen the QA_oh menu: browser enabled 'testid=agent-profile-set-active' is false (screenshot --feature F13.set-active --name menu);
  6. Do
    control-openhands browser press Escape
  7. Note
    closes it (browser count 'testid=agent-profile-actions-menu' is 0).

Pill wins over a pinned LLM #

  1. Note
    With QA_oh active (pinned to deepseek-pro) and the Home pill on deepseek-flash (browser text 'testid=chat-input-llm-profile' on /), run
  2. Wait
    control-openhands conversation start --prompt "Reply with only: qa-profile-ok" --wait --timeout 240
  3. Expect
    The output's model is deepseek/deepseek-flash, and
  4. Check
    control-openhands api GET /api/conversations/<id>

    (id from that output) has "launched_agent_profile": null and tool_concurrency_limit 1: the profile was not applied.

Active profile drives the launch #

  1. Do
    control-openhands browser goto /settings/agents

    (the previous conversation start left the browser on the conversation), edit QA_oh, run

  2. Do
    control-openhands browser choose 'testid=agent-profile-llm-selector' 'deepseek-flash (deepseek/deepseek-flash)'

    (the toggle-button opener failed 5 of 5 times here: see Gotchas), check browser value 'testid=agent-profile-llm-selector', Save (Profile "QA_oh" updated).

  3. Note
    Run the same conversation start again; api GET /api/conversations/<id> now has launched_agent_profile.agent_profile_id equal to QA_oh's id from api GET /api/agent-profiles and tool_concurrency_limit 2;
  4. Check
    control-openhands conversation events <id> --kinds MessageEvent
  5. Note
    contains qa-profile-ok.

MCP scope #

  1. Note
    With qa_mcp_a and qa_mcp_b arranged, browser goto /settings/agents, Edit QA_oh: browser value 'testid=agent-settings-mcp-mode' is All servers and the eval below (with +(i.disabled?'(disabled)':'') appended) shows both =true(disabled).
  2. Note
    Pick Choose servers from testid=agent-settings-mcp-mode;
  3. Do
    control-openhands browser eval "[...document.querySelectorAll('[data-testid=agent-settings-mcp-list] input')].map(i=>i.dataset.testid+'='+i.checked)"

    shows both =true.

  4. Do
    control-openhands browser click 'testid=agent-settings-mcp-list >> text=qa_mcp_b'

    (now false), Save; api GET /api/agent-profiles/QA_oh has mcp_server_refs ["qa_mcp_a"].

  5. Note
    Dangling: arrange
  6. Arrange
    control-openhands api DELETE /api/settings/mcp/qa_mcp_a --write
  7. Note
    browser reload, Edit QA_oh:
  8. Check
    control-openhands browser text 'testid=agent-settings-mcp-dangling'

    is qa_mcp_a is no longer configured. Saving with it selected will stop this agent from starting — clear it or re-add the server. (screenshot --feature F13.mcp-scope --name dangling).

  9. Note
    Leave without saving, browser goto /, browser reload,
  10. Do
    control-openhands browser fill 'testid=chat-input' 'Reply with only: qa-dangling'
  11. Check
    control-openhands browser click 'testid=submit-button' --observe 'role=status' --observe-ms 4000
  12. Check
    control-openhands browser toasts --history

    (QA_oh must still be active and pinned to the pill's LLM, as F13.active-drives-conversation leaves it; otherwise the launch skips the profile and nothing is refused): the launch is refused (one POST /api/conversations 422 in browser errors --app-only; earlier failures in the session are listed too): browser toasts --history shows Creating conversation…, then one toast carrying the server's detail.message (MCP server(s) not configured: qa_mcp_a on Agent Server 1.53.0), and browser text 'testid=chat-input' still reads Reply with only: qa-dangling.

  13. Note
    Clear the composer with browser fill 'testid=chat-input' '', then clear the ref:
  14. Do
    control-openhands browser goto /settings/agents
  15. Note
    Edit QA_oh, browser click 'testid=agent-settings-mcp-list >> text=qa_mcp_a' (the warning count drops to 0), pick All servers, Save; mcp_server_refs is null.

Credential fields per preset #

  1. Note
    In the Add editor,
  2. Do
    control-openhands browser choose 'testid=agent-type-selector' 'ACP (external subprocess)'
  3. Note
    then for each of Codex, Gemini CLI and Custom run
  4. Do
    control-openhands browser choose 'testid=agent-preset-selector' '<preset>'
  5. Check
    control-openhands browser testids --hidden --filter settings-acp
  6. Note
    : Codex lists settings-acp-secret-OPENAI_API_KEY (input), -CODEX_AUTH_JSON (textarea), -OPENAI_BASE_URL; Gemini CLI lists -GOOGLE_APPLICATION_CREDENTIALS_JSON (textarea), -GOOGLE_CLOUD_PROJECT, -GOOGLE_CLOUD_LOCATION, -GOOGLE_GENAI_USE_VERTEXAI, -GEMINI_API_KEY, -GEMINI_BASE_URL; Custom lists nothing and browser eval "document.body.innerText.includes('Credentials')" is false.
  7. Note
    For Gemini,
  8. Wait
    control-openhands browser wait 'testid=settings-acp-auth-checking' --state detached --timeout 15000
  9. Note
    then browser testids --hidden --filter settings-acp-auth has count 0 (no login, no stored credential); browser eval "document.querySelector('[data-testid=settings-acp-secret-GEMINI_API_KEY]').type" is password, GOOGLE_CLOUD_PROJECT is text.
  10. Note
    Screenshot --feature F13.acp-preset-credentials --name gemini after browser scroll 'testid=settings-acp-secret-GOOGLE_APPLICATION_CREDENTIALS_JSON'.
  11. Note
    Cancel (Codex shows settings-acp-auth-configured instead while OPENAI_API_KEY from F13.acp-credentials is still saved).

Menu keyboard #

  1. Do
    control-openhands browser goto /settings/agents
  2. Expect
    It needs a non-active row: run
  3. Do
    control-openhands browser click 'testid=add-agent-profile'
  4. Do
    control-openhands browser fill 'testid=agent-profile-name-input' QA_kb
  5. Do
    control-openhands browser click 'testid=save-agent-profile-btn'
  6. Wait
    control-openhands browser wait-text 'Profile "QA_kb" created' --timeout 10000
  7. Note
    Open its menu with
  8. Do
    control-openhands browser click 'testid=agent-profile-row >> has-text=QA_kb >> testid=agent-profile-menu-trigger'
  9. Note
    and right away run
  10. Do
    control-openhands browser eval "document.activeElement?.dataset?.testid"
  11. Do
    control-openhands browser press ArrowDown
  12. Note
    and the same eval.
  13. Note
    Expected: agent-profile-edit, then agent-profile-set-active (opening the menu moves focus to its first item, as the LLM profile menu does in F10.actions-menu).
  14. Note
    Known failure: both evals read agent-profile-menu-trigger; focus stays on the trigger and ArrowDown does nothing (#18060).
  15. Do
    control-openhands browser focus 'testid=agent-profile-edit'
  16. Do
    control-openhands browser press ArrowDown
  17. Note
    followed by that eval four times: agent-profile-set-active, agent-profile-delete, agent-profile-edit, agent-profile-set-active; browser press ArrowUp returns to agent-profile-edit.
  18. Do
    control-openhands browser press Escape
  19. Note
    closes the menu (control-openhands browser count 'testid=agent-profile-actions-menu' is 0); reopen it, run
  20. Do
    control-openhands browser focus 'testid=agent-profile-delete'
  21. Do
    control-openhands browser press Tab
  22. Note
    : closed; reopen it and run
  23. Do
    control-openhands browser mouse-click 900 600
  24. Note
    : closed.
  25. Note
    Active row: open its menu with
  26. Do
    control-openhands browser click '[data-testid=agent-profile-row]:has([data-testid=agent-profile-active-badge]) >> testid=agent-profile-menu-trigger'

    (control-openhands browser snapshot 'testid=agent-profile-actions-menu' shows menuitem "Set as active" [disabled]), run

  27. Do
    control-openhands browser focus 'testid=agent-profile-edit'
  28. Do
    control-openhands browser press ArrowDown
  29. Note
    and the eval, then
  30. Do
    control-openhands browser press ArrowUp
  31. Note
    and the eval.
  32. Note
    Expected: agent-profile-delete (the disabled Set as active is skipped), then agent-profile-edit.
  33. Note
    Known failure: agent-profile-edit (ArrowDown tries to focus the disabled item, which cannot take focus), then agent-profile-delete (ArrowUp wraps) (#18060).
  34. Note
    With the menu still open, run
  35. Do
    control-openhands browser focus 'testid=agent-profile-edit'
  36. Do
    control-openhands browser press Enter
  37. Check
    control-openhands browser text 'testid=agent-profile-editor-title'

    is Edit agent profile.

  38. Note
    Leave the editor with
  39. Do
    control-openhands browser click 'testid=cancel-agent-profile-btn'

    (browser count 'testid=add-agent-profile' is 1).

  40. Note
    Delete QA_kb: reopen its menu,
  41. Do
    control-openhands browser click 'testid=agent-profile-delete'
  42. Do
    control-openhands browser click 'testid=delete-agent-profile-confirm'
  43. Wait
    control-openhands browser wait-text 'Profile "QA_kb" deleted' --timeout 10000

Delete #

  1. Do
    control-openhands browser click 'testid=agent-profile-row >> has-text=QA_acp_renamed >> testid=agent-profile-menu-trigger'
  2. Do
    control-openhands browser click 'testid=agent-profile-delete'
  3. Check
    control-openhands browser text 'role=dialog'

    reads Delete Profile and Are you sure you want to delete the profile "QA_acp_renamed"? This action cannot be undone.

  4. Do
    control-openhands browser click 'role=dialog >> role=button[name="Cancel"]'
  5. Note
    keeps the row (count 1).
  6. Note
    Repeat and
  7. Do
    control-openhands browser click 'testid=delete-agent-profile-confirm'
  8. Note
    browser wait-text 'Profile "QA_acp_renamed" deleted' --timeout 10000, browser reload: count 0.
  9. Note
    Delete the active QA_oh the same way: after browser reload, browser count 'testid=agent-profile-active-badge' is 0 and api GET /api/agent-profiles has active_agent_profile_id null.
  10. Note
    Delete QA_codex and default: the list again shows one row, default / deepseek-flash / Default, with a new id (the server re-seeds it).

Phone #

  1. Do
    control-openhands browser viewport phone
  2. Do
    control-openhands browser goto /settings/agents
  3. Check
    control-openhands browser bbox 'testid=agent-profile-row'

    (insideViewport true, pageHorizontalOverflow false) and browser screenshot --feature F13.phone --name list.

  4. Note
    Open the row menu: browser bbox 'testid=agent-profile-actions-menu' is inside the viewport. browser press Escape, click testid=add-agent-profile: browser bbox 'testid=agent-settings-screen' is 348 px wide at x 16 with no page overflow (insideViewport is false only because the form is taller than the screen).
  5. Note
    Switch to ACP, browser scroll 'testid=settings-acp-secret-ANTHROPIC_API_KEY' and screenshot --name acp-credentials.
  6. Note
    Cancel and
  7. Do
    control-openhands browser viewport desktop

Errors #

  1. Expect
    After the family,
  2. Check
    control-openhands browser errors --app-only

    shows no page errors and no [i18n] Missing translation warnings (the Parallel tool calls label and description are translated since #18035), only the deliberate 422s from the dangling-MCP launch.

Cleanup #

  1. Note
    Delete the OPENAI_API_KEY credential through Secrets:
  2. Do
    control-openhands browser goto /settings/secrets
  3. Do
    control-openhands browser click 'testid=secret-item >> has-text=OPENAI_API_KEY >> testid=delete-secret-button'
  4. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button'
  5. Note
    Remove qa_mcp_b with
  6. Arrange
    control-openhands api DELETE /api/settings/mcp/qa_mcp_b --write
  7. Note
    Leave default active.

Gotchas and known limits

  • The editor is not a route: browser url stays /settings/agents and browser reload drops unsaved edits back to the list. The settings page header (settings-page-subtitle) is hidden while it is open.
  • Autocomplete dropdowns: clicking an input that already shows a value can open an empty listbox (the text filters it), and an option click then times out. The toggle-button opener does not always open the list either: on the Edit editor's agent-profile-llm-selector (bottom of the form) the option click timed out 5 of 5 times right after opening the editor, and the model list's Custom option once detached mid-click after a preset change. browser choose was reliable on every dropdown. A listbox also closes if you spend a command (snapshot, eval) in between, so click the option right after opening.
  • Save is disabled for a duplicate name with no message and no red rule (aria-invalid stays false) (#17938); check the name before suspecting the form.
  • Switch rows are <input>s inside labels (#17900): click the label text ('testid=agent-settings-screen >> text=Enable sub-agents', 'testid=agent-settings-mcp-list >> text=<name>') and read .checked with browser eval. A bare label selector segment is parsed as label=, not CSS.
  • The Default badge, the Set as active item and the Switched to profile toast all refer to the same active pointer; the wording differs from the LLM page (Set as default).
  • The OpenHands row's grey text is the LLM profile name, not the kind. A fresh run's default shows default there although no LLM profile of that name exists until it is edited (F13.stale-llm-ref).
  • Home launches follow the LLM pill: a named profile pinned to another LLM profile launches through the legacy settings path, so its sub-agent, concurrency and MCP settings do not apply (F13.llm-pill-precedence). To prove a profile's settings, pin it to the active LLM profile, and read launched_agent_profile on the conversation.
  • The ACP auth probe runs the provider CLI on the agent-server machine. On this sandbox the host's Claude CLI is logged in, so Claude Code shows "already signed in". Never launch an ACP conversation on that login.
  • ACP credentials are global secrets, not profile fields: cancelling the editor saves nothing, but saving any ACP profile with a typed credential writes it to Settings → Secrets (F14) for every conversation.
  • Choose secrets on an ACP profile also selects provider secret names that are not saved (CODEX_AUTH_JSON, OPENAI_BASE_URL); they show as switches without descriptions.
  • The empty state (agent-profiles-empty) and the load-error state (agent-profiles-load-error) are not reachable on a local backend: deleting the last profile re-seeds default, and with the agent-server stopped the app swaps to Manage backends after a reload, or keeps the cached list without one.
  • The refused-launch toast text comes from the Agent Server and changes between versions (MCP server(s) not configured: <name> on 1.53.0): assert that one toast names the server and the prompt stays, not the exact wording.
  • Row-menu keyboard: on the active row, ArrowDown from Edit keeps focus on Edit because the handler moves focus to the next item without skipping the disabled Set as active, and a disabled button cannot take focus; reach Delete with ArrowUp. Opening the menu also leaves focus on the trigger (F13.menu-keyboard, an accessibility bug). Both are open as #18060. The LLM profile menu on Settings → LLM had the same defects: #18019 fixed only that menu (F10.actions-menu) and closed #17934, so #17934 being closed says nothing about this menu.
  • #18035 (closed #17939) gave the Add editor its own description and translated the Parallel tool calls field. A checkout without it shows the LLM page's Configure your LLM settings below, then click Save to create this profile. and logs [i18n] Missing translation for key "SCHEMA$TOOL_CONCURRENCY_LIMIT$LABEL" / $DESCRIPTION whenever the editor renders.

Source paths: src/routes/agent-profiles-settings.tsx, src/routes/agent-settings.tsx, src/components/features/settings/agent-profiles/, src/components/features/settings/acp-credentials-section.tsx, src/constants/acp-providers.ts, src/hooks/mutation/use-create-conversation.ts.