EN / field notes OpenHands feature map

OpenHands / F16

Application settings

Settings → Application holds the user's app-wide preferences: UI language, colour theme, anonymous usage data, sound notifications, the sidebar Getting Started checklist, which LLM profile writes conversation titles, an optional voice-input transcription endpoint, and the git identity used for commits. The page mixes two save models: language, analytics, sound, title model and git identity wait for Save Changes and are stored on the backend; theme, checklist and voice input apply instantly and live in this browser only.

19 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), then Application in the settings navigation (sidebar-settings-/settings/app). The gear lands on /settings/agents, not on this page.
  • Direct URL /settings/app (also the default settings page for locked-to-Cloud deployments).
  • Command menu (Control+k or Meta+k, or command-menu-trigger in the sidebar): search "Application", choose Application settings ("Change app language, theme, and preferences").
  • Manage LLM profiles link under Conversation titles leaves this page for /settings/llm (F16.manage-profiles-link); the LLM settings row menu is how a user deletes a profile (owned by the LLM profiles family).

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; it stores analytics off, so the analytics switch starts unchecked).
  • F16.title-model, F16.title-model-fallback and the agent half of F16.git-identity need LLM profiles: control-openhands llm preset deepseek --api-key-file <key file> (or --api-key-env DEEPSEEK_API_KEY), which saves deepseek-flash (active) and deepseek-pro.
  • Read the starting values once: control-openhands api GET /api/settings; backend-saved fields are under body.misc_settings.app_preferences (language, user_consents_to_analytics, enable_sound_notifications, title_llm_profile, git_user_name, git_user_email). Keys absent there mean defaults.
  • The three switches are <input hidden> inside a <label>: click the label text scoped to the form, and read state with control-openhands browser eval "document.querySelector('[data-testid=enable-analytics-switch]').checked" (same for enable-sound-notifications-switch, show-getting-started-checklist-switch).
  • Every bullet starts on /settings/app at the desktop viewport unless it says otherwise (control-openhands browser goto /settings/app, then control-openhands browser wait 'testid=submit-button').
  • F16.analytics-cloud is blocked without an OpenHands Cloud backend and account.

Behavior inventory

19 stable behavior IDs and their expected behavior
  • F16.open the page opens from the settings navigation, the command menu and the direct URL; every section renders and Save Changes is disabled while nothing has changed. Read recipe ↓
  • F16.language choosing a language and saving switches the whole UI at once and persists after a reload; <html lang> and <html dir> follow the chosen language (rtl for Arabic). Read recipe ↓
  • F16.color-theme choosing a colour theme applies it immediately without Save, persists after a reload and on other pages, and is never sent to the backend. Read recipe ↓
  • F16.analytics the "Send anonymous usage data" switch saves with the form and sets browser telemetry consent to granted/denied. Read recipe ↓
  • F16.analytics-cloud on a Cloud backend the analytics switch is forced on and disabled. Read recipe ↓
  • F16.sound the "Sound Notifications" switch saves with the form; with it on, a sound plays when the agent finishes or needs input. Read recipe ↓
  • F16.checklist the "Show Getting Started checklist" switch hides or shows the sidebar checklist immediately, without Save, and survives a reload. Read recipe ↓
  • F16.title-model the Title generation model dropdown offers "Automatic (recommended)" plus one name · model option per LLM profile; the choice persists and new conversations still get an automatic title. Read recipe ↓
  • F16.title-model-fallback if the chosen profile is deleted (from LLM settings, or outside the UI), the dropdown shows Automatic and the form stays clean. Read recipe ↓
  • F16.voice-endpoint Voice input Base URL, API Key (masked) and Model save on every keystroke in this browser only, independently of Save; the key lives in session storage. Read recipe ↓
  • F16.git-identity Git Username and Git Email save with the form, persist after a reload, and are what the agent's commits use. Read recipe ↓
  • F16.git-identity-validation an email without @ is refused by native validation; empty fields save the defaults openhands / openhands@all-hands.dev. Read recipe ↓
  • F16.save-states Save is enabled only while a saved field differs from the stored value; while a save is pending the whole form, Save included, is replaced by the app-settings-skeleton (so the "Saving..." label in the source never shows); success toasts "Settings saved", failure toasts the HTTP error and should keep the user's edits for a retry. Read recipe ↓
  • F16.keyboard every control, including the three switches, is reachable with Tab and exposed to assistive technology. Read recipe ↓
  • F16.phone the form fits a 390 px viewport without horizontal overflow and Save is reachable by scrolling. Read recipe ↓
  • F16.title-model-clear the Title generation model combobox has a clear (×) button, shown on hover; it resets the choice to Automatic (recommended). Read recipe ↓
  • F16.dropdown-filter typing into the Language (or any) combobox filters its options; Escape restores the stored value and leaves Save disabled. Read recipe ↓
  • F16.browser-scope backend-saved preferences (language and the rest of the Save group) follow the user to a fresh browser profile, while theme and checklist visibility return to their defaults there. 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.

Open from navigation #

  1. Note
    From / run
  2. Do
    control-openhands browser click 'testid=backend-selector-settings-link'
  3. Do
    control-openhands browser click 'testid=sidebar-settings-/settings/app'
  4. Check
    control-openhands browser url
  5. Check
    control-openhands browser snapshot 'testid=app-settings-screen'
  6. Do
    control-openhands browser screenshot --feature F16.open --name desktop
  7. Expect
    The URL ends in /settings/app; the snapshot shows comboboxes "Language" (English), "Color Theme" (OpenHands-Neutral) and "Title generation model" (Automatic (recommended)), headings "Conversation titles", "Voice input" and "Git Settings", and button "Save Changes" [disabled].
Application settings upper section shows language, color theme, analytics, sounds, checklist, conversation title model and voice input endpoint fields.
The upper Application page groups general preferences, conversation titles and optional voice input configuration. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Application settings upper section shows language, color theme, analytics, sounds, checklist, conversation title model and voice input endpoint fields.

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 /settings/app
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=app-settings-screen
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=app-settings-screen
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F16.open --name application-settings --full-page

Command-menu and URL entries #

  1. Do
    control-openhands browser goto /

    (the previous bullet ends on /settings/app, where the URL check would pass at once),

  2. Do
    control-openhands browser press Control+k
  3. Do
    control-openhands browser type 'testid=command-menu >> role=combobox' Application
  4. Check
    control-openhands browser snapshot 'testid=command-menu'

    (option "Application settings Change app language, theme, and preferences.

  5. Note
    Go" is [selected]),
  6. Do
    control-openhands browser press Enter
  7. Check
    control-openhands browser url
  8. Note
    it ends in /settings/app.
  9. Note
    For the URL entry run
  10. Do
    control-openhands browser goto /settings/app
  11. Check
    control-openhands browser enabled 'testid=submit-button'

    (false) and

  12. Check
    control-openhands browser attr 'testid=sidebar-settings-/settings/app' aria-current

    (page).

Language #

  1. Do
    control-openhands browser click 'testid=language-input'

    (15 options),

  2. Do
    control-openhands browser click 'role=option[name="Français"]'
  3. Check
    control-openhands browser enabled 'testid=submit-button'

    (true),

  4. Do
    control-openhands browser click 'testid=submit-button'
  5. Check
    control-openhands browser text 'testid=submit-button'
  6. Expect
    It reads Enregistrer les modifications without a reload.
  7. Do
    control-openhands browser reload
  8. Check
    control-openhands browser value 'testid=language-input'

    (Français),

  9. Do
    control-openhands browser screenshot --feature F16.language --name french

    (the screenshot, not a scoped snapshot, shows the sidebar's "Nouvelle discussion" and the headings "Titres des conversations", "Saisie vocale") and

  10. Check
    control-openhands api GET /api/settings

    (language is fr).

  11. Note
    Then check the document language with
  12. Do
    control-openhands browser eval "document.documentElement.lang + ' ' + document.documentElement.dir"

    (fr ltr) and the translated sidebar trigger with

  13. Check
    control-openhands browser attr 'testid=command-menu-trigger' aria-label

    (Rechercher des commandes).

  14. Note
    Choose role=option[name="Arabic"] the same way (click the input, the option, Save, reload): the eval reads ar rtl, and
  15. Check
    control-openhands browser bbox 'aside[data-collapsed]'

    has x 1140, so the 300 px sidebar now sits at the right edge of the 1440 px viewport.

  16. Do
    control-openhands browser screenshot --feature F16.language --name arabic

    shows the whole layout flipped right to left: the sidebar on the right, the settings navigation to its left, and the form on the left with its Arabic labels and values aligned right.

  17. Note
    Restore:
  18. Do
    control-openhands browser click 'testid=language-input'
  19. Do
    control-openhands browser click 'role=option[name="English"]'
  20. Do
    control-openhands browser click 'testid=submit-button'
  21. Do
    control-openhands browser reload
  22. Check
    control-openhands browser text 'testid=submit-button'

    reads Save Changes and the eval reads en ltr.

Colour theme #

  1. Do
    control-openhands browser click 'testid=color-theme-input'
  2. Check
    control-openhands browser snapshot 'role=listbox'

    (OpenHands-DeepSea, OpenHands-Neutral [selected], OpenHands-Neo, Light+, Solarized Light),

  3. Do
    control-openhands browser click 'role=option[name="Light+"]'
  4. Do
    control-openhands browser eval "document.documentElement.style.colorScheme + ' ' + localStorage.getItem('openhands-color-theme')"

    (light light-plus) and

  5. Check
    control-openhands browser enabled 'testid=submit-button'

    (false: no Save needed).

  6. Do
    control-openhands browser screenshot --feature F16.color-theme --name light-plus

    shows a white page.

  7. Note
    Second views:
  8. Do
    control-openhands browser reload
  9. Check
    control-openhands browser value 'testid=color-theme-input'

    (Light+);

  10. Do
    control-openhands browser goto /
  11. Do
    control-openhands browser eval "getComputedStyle(document.body).backgroundColor + ' ' + document.documentElement.style.colorScheme"

    (rgb(255, 255, 255) light);

  12. Check
    control-openhands api GET /api/settings
  13. Note
    contains no theme key.
  14. Note
    Restore on /settings/app (control-openhands browser goto /settings/app; the second view left /) by choosing role=option[name="OpenHands-Neutral"] the same way (dark openhands-neutral).

Analytics consent #

  1. Do
    control-openhands browser click 'testid=app-settings-screen >> text=Send anonymous usage data'
  2. Note
    read the switch with the eval from Preconditions (true),
  3. Do
    control-openhands browser click 'testid=submit-button'
  4. Wait
    control-openhands browser wait-text 'Settings saved'
  5. Do
    control-openhands browser reload
  6. Note
    read the switch again (true),
  7. Check
    control-openhands api GET /api/settings

    (user_consents_to_analytics: true) and

  8. Do
    control-openhands browser eval "localStorage.getItem('openhands-telemetry-consent')"

    (granted).

  9. Note
    Restore by clicking the label again and saving; consent reads denied and the API false.

Cloud forces analytics · Blocked prerequisite #

  1. Note
    Blocked: needs a Cloud backend selected in the backend selector.
  2. Note
    Expected:
  3. Check
    control-openhands browser enabled 'testid=enable-analytics-switch'

    is false and the switch reads checked.

Sound notifications #

  1. Do
    control-openhands browser click 'testid=app-settings-screen >> text=Sound Notifications'
  2. Do
    control-openhands browser click 'testid=submit-button'
  3. Wait
    control-openhands browser wait-text 'Settings saved'
  4. Do
    control-openhands browser reload
  5. Note
    read enable-sound-notifications-switch (true) and
  6. Check
    control-openhands api GET /api/settings

    (enable_sound_notifications: true).

  7. Note
    Restore by toggling and saving again (false).
  8. Note
    Audible half: with the switch on and saved,
  9. Wait
    control-openhands conversation start --prompt "Reply with only the word ok. Do not run any tools." --wait --timeout 180
  10. Do
    control-openhands browser media
  11. Note
    : plays lists one /assets/notification-<hash>.mp3 at the moment the conversation finished.
  12. Note
    With the switch off, the same check after a conversation reads plays: [] (the F16.git-identity conversation below doubles as that negative check).
  13. Note
    Turn the switch off and save afterwards (control-openhands browser goto /settings/app first: conversation start left the browser on the conversation).

Getting Started checklist #

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

    (1),

  2. Do
    control-openhands browser click 'testid=app-settings-screen >> text=Show Getting Started checklist'
  3. Note
    then the same count (0, immediately) and
  4. Check
    control-openhands browser enabled 'testid=submit-button'

    (false).

  5. Do
    control-openhands browser screenshot --feature F16.checklist --name hidden

    shows the sidebar without the "Getting started" card.

  6. Do
    control-openhands browser reload
  7. Note
    keeps the count at 0 and the switch unchecked.
  8. Note
    Click the label again to restore; the count returns to 1.

Title generation model #

  1. Do
    control-openhands browser click 'testid=title-llm-profile-input'
  2. Check
    control-openhands browser snapshot 'role=listbox'
  3. Note
    : options Automatic (recommended), deepseek-flash · deepseek/deepseek-flash, deepseek-pro · deepseek/deepseek-v4-pro.
  4. Do
    control-openhands browser click 'role=option >> has-text=deepseek-pro'
  5. Do
    control-openhands browser click 'testid=submit-button'
  6. Wait
    control-openhands browser wait-text 'Settings saved'
  7. Do
    control-openhands browser reload
  8. Check
    control-openhands browser value 'testid=title-llm-profile-input'

    (deepseek-pro · deepseek/deepseek-v4-pro) and

  9. Check
    control-openhands api GET /api/settings

    (title_llm_profile: "deepseek-pro").

  10. Wait
    control-openhands conversation start --prompt "Reply with only the word ok. Do not run any tools." --wait --timeout 180
  11. Note
    and, a few seconds later,
  12. Check
    control-openhands conversation status <id>
  13. Note
    with the id it printed: title is a generated title: an emoji and a short phrase whose wording varies (for example 🔧 Acknowledge with ok only).
  14. Expect
    The start output's own title may already be set; when it is still null, the title is written a few seconds after the run finishes.
  15. Note
    Return with
  16. Do
    control-openhands browser goto /settings/app
  17. Note
    choose role=option[name="Automatic (recommended)"], save; title_llm_profile is then absent from app_preferences.

Manage LLM profiles link #

  1. Do
    control-openhands browser click 'role=link[name="Manage LLM profiles"]'
  2. Check
    control-openhands browser url
  3. Note
    it ends in /settings/llm and
  4. Check
    control-openhands browser count 'testid=profile-row'

    lists the profiles.

Deleted profile falls back #

  1. Note
    Arrange a throwaway profile:
  2. Arrange
    control-openhands llm set --profile qa-title --model deepseek/deepseek-flash --api-key-file <key file> --no-activate --no-validate
  3. Note
    On /settings/app select it (control-openhands browser click 'testid=title-llm-profile-input', control-openhands browser click 'role=option >> has-text=qa-title', save).
  4. Note
    Delete it through the UI:
  5. Do
    control-openhands browser click 'role=link[name="Manage LLM profiles"]'
  6. Do
    control-openhands browser click 'testid=profile-row >> has-text=qa-title >> testid=profile-menu-trigger'
  7. Do
    control-openhands browser click 'testid=profile-actions-menu >> testid=profile-delete'
  8. Do
    control-openhands browser click 'testid=delete-profile-confirm'
  9. Do
    control-openhands browser click 'testid=sidebar-settings-/settings/app'
  10. Check
    control-openhands browser value 'testid=title-llm-profile-input'

    is Automatic (recommended), Save is disabled, and title_llm_profile is absent from the API's app_preferences.

  11. Note
    For a preference that goes stale outside the UI, arrange
  12. Arrange
    control-openhands api PATCH /api/settings --data '{"misc_settings_diff":{"app_preferences":{"title_llm_profile":"qa-missing"}}}' --write
  13. Do
    control-openhands browser reload
  14. Note
    : the dropdown still shows Automatic (recommended) and Save stays disabled, while the API keeps qa-missing until the next Save of this form.
  15. Note
    Any Save writes the form's current preferences, which no longer hold the stale key, so saving an unrelated change removes it: toggle Sound Notifications, save, toggle back, save.

Voice input endpoint #

  1. Check
    control-openhands browser fill 'testid=transcription-base-url-input' http://127.0.0.1:9/v1
  2. Do
    control-openhands browser fill 'testid=transcription-api-key-input' qa-dummy-key
  3. Do
    control-openhands browser fill 'testid=transcription-model-input' qa-whisper
  4. Check
    control-openhands browser enabled 'testid=submit-button'

    (false) and

  5. Check
    control-openhands browser attr 'testid=transcription-api-key-input' type

    (password).

  6. Note
    After
  7. Do
    control-openhands browser reload
  8. Check
    control-openhands browser value 'testid=transcription-model-input'

    is qa-whisper (same for the other two).

  9. Do
    control-openhands browser eval "localStorage.getItem('openhands-transcription-endpoint') + ' | session=' + sessionStorage.getItem('openhands-transcription-api-key')"

    reads {"baseUrl":"http://127.0.0.1:9/v1","model":"qa-whisper"} | session=qa-dummy-key; the API body has no qa-whisper.

  10. Note
    Clear each with
  11. Check
    control-openhands browser fill 'testid=transcription-base-url-input' ''

    (and the other two).

  12. Note
    Using the endpoint from the composer microphone is not driven here (needs microphone capture and a transcription server).

Git identity #

  1. Do
    control-openhands browser fill 'testid=git-user-name-input' 'QA Tester'
  2. Do
    control-openhands browser fill 'testid=git-user-email-input' 'qa-tester@example.com'
  3. Do
    control-openhands browser click 'testid=submit-button'
  4. Wait
    control-openhands browser wait-text 'Settings saved'
  5. Do
    control-openhands browser reload
  6. Check
    control-openhands browser value 'testid=git-user-name-input'

    (QA Tester) and

  7. Check
    control-openhands api GET /api/settings

    (git_user_name, git_user_email set).

  8. Note
    Agent half:
  9. Wait
    control-openhands conversation start --prompt "Run exactly this command and reply with only its output: env | grep -c '^GIT_'; git init -q qa_t && cd qa_t && git commit --allow-empty -qm x; git log -1 --format='%an <%ae>'" --wait --timeout 240
  10. Check
    control-openhands conversation events <id> --kinds ObservationEvent,MessageEvent
  11. Note
    with the id from the start output.
  12. Note
    Expected: QA Tester <qa-tester@example.com>. Known failure: the observation is 0 Author identity unknown ...: the identity is stored only in misc_settings.app_preferences and never reaches the agent.
Application settings lower section shows voice input endpoint fields and Git username and email above Save Changes.
Voice input stores an optional endpoint in this browser; the Git identity fields appear in the saved application settings form. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Application settings lower section shows voice input endpoint fields and Git username and email above Save Changes.

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 /settings/app
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser scroll testid=submit-button
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F16.git-identity --name application-settings-bottom

Git validation and defaults #

  1. Do
    control-openhands browser fill 'testid=git-user-email-input' 'not-an-email'
  2. Do
    control-openhands browser click 'testid=submit-button'
  3. Do
    control-openhands browser eval "document.querySelector('[data-testid=git-user-email-input]').validationMessage"

    (Please include an '@' in the email address. ...); the API email is unchanged.

  4. Do
    control-openhands browser fill 'testid=git-user-email-input' ''
  5. Do
    control-openhands browser fill 'testid=git-user-name-input' ''
  6. Note
    click submit,
  7. Wait
    control-openhands browser wait-text 'Settings saved'
  8. Do
    control-openhands browser reload
  9. Note
    : the inputs read openhands and openhands@all-hands.dev, which also restores the baseline.

Save button states #

  1. Note
    Click the analytics label once:
  2. Check
    control-openhands browser enabled 'testid=submit-button'

    is true; click it again: false (back to the stored value).

  3. Note
    Each successful save above made while the UI is in English shows the toast found by
  4. Wait
    control-openhands browser wait-text 'Settings saved'
  5. Note
    the Arabic save and the English restore in F16.language are made from a French and an Arabic UI and toast Paramètres enregistrés and تم حفظ الإعدادات instead (see Gotchas).
  6. Note
    Pending and failure: run
  7. Do
    control-openhands service stop agent-server

    (the open page keeps its form), click testid=app-settings-screen >> text=Sound Notifications, start a sampler with

  8. Do
    control-openhands browser eval "(() => { window.__seen = []; window.__iv = setInterval(() => { const s = document.querySelector('[data-testid=submit-button]'); const k = (s ? s.textContent : '-') + ' skeleton=' + !!document.querySelector('[data-testid=app-settings-skeleton]'); if (window.__seen.at(-1) !== k) window.__seen.push(k); }, 10); return 'started'; })()"
  9. Note
    click testid=submit-button,
  10. Wait
    control-openhands browser wait-text 'HTTP request failed'

    (the toast starts HTTP request failed (502 Bad Gateway) and names the refused Agent Server port), then

  11. Do
    control-openhands browser eval "(() => { clearInterval(window.__iv); return window.__seen.join(' -> '); })()"
  12. Note
    : it starts with Save Changes skeleton=false -> - skeleton=true, i.e. the form and its Save button are replaced by the skeleton while pending; Saving... never appears.
  13. Note
    By the time the failure toast is read the form has usually come back, adding -> Save Changes skeleton=false; assert the prefix.
  14. Note
    Expected after the failure: the Sound switch stays on so the user can retry. Known failure:
  15. Do
    control-openhands browser eval "document.querySelector('[data-testid=enable-sound-notifications-switch]').checked"

    is false and Save is disabled: the edit is silently discarded.

  16. Note
    Bring the backend back with
  17. Do
    control-openhands restart
  18. Check
    control-openhands doctor
  19. Do
    control-openhands browser goto /settings/app

Keyboard and screen readers #

  1. Do
    control-openhands browser focus 'testid=color-theme-input'
  2. Note
    then repeat
  3. Do
    control-openhands browser press Tab
  4. Do
    control-openhands browser eval "(document.activeElement.getAttribute('data-testid')||'') + ' ' + document.activeElement.tagName"
  5. Note
    Expected (the acceptance criterion): focus visits the three switches before title-llm-profile-input. Known failure, today's result: the first Tab lands on title-llm-profile-input;
  6. Check
    control-openhands browser snapshot 'testid=app-settings-screen' --feature F16.keyboard --name aria

    shows the switches only as text: Send anonymous usage data Sound Notifications Show Getting Started checklist, with no switch or checkbox nodes.

Phone layout #

  1. Do
    control-openhands browser viewport phone
  2. Do
    control-openhands browser goto /settings/app
  3. Check
    control-openhands browser bbox 'testid=app-settings-screen'

    (pageHorizontalOverflow: false, width 348 at x 16),

  4. Do
    control-openhands browser screenshot --feature F16.phone --name top
  5. Do
    control-openhands browser scroll 'testid=submit-button'
  6. Check
    control-openhands browser bbox 'testid=submit-button'

    (insideViewport: true) and

  7. Do
    control-openhands browser screenshot --feature F16.phone --name bottom
  8. Note
    Return with
  9. Do
    control-openhands browser viewport desktop

Clear the title model #

  1. Do
    control-openhands browser click 'testid=title-llm-profile-input'
  2. Do
    control-openhands browser click 'role=option >> has-text=deepseek-flash'

    (Save enables),

  3. Do
    control-openhands browser hover 'testid=title-llm-profile-input'
  4. Do
    control-openhands browser click 'testid=title-llm-profile-input >> xpath=../..//button[not(@aria-label)]'
  5. Check
    control-openhands browser value 'testid=title-llm-profile-input'

    reads Automatic (recommended) and

  6. Check
    control-openhands browser enabled 'testid=submit-button'

    is false.

  7. Note
    Without the hover the click times out (<div ...> intercepts pointer events).

Filter a dropdown by typing #

  1. Do
    control-openhands browser fill 'testid=language-input' Deu
  2. Check
    control-openhands browser snapshot 'role=listbox'
  3. Note
    : the only option is Deutsch.
  4. Note
    Save is briefly true while the text is typed;
  5. Do
    control-openhands browser press Escape
  6. Note
    restores
  7. Check
    control-openhands browser value 'testid=language-input'
  8. Note
    to English and
  9. Check
    control-openhands browser enabled 'testid=submit-button'
  10. Note
    to false.

Browser vs backend scope #

  1. Note
    Choose Deutsch in testid=language-input and save, choose Solarized Light in testid=color-theme-input, and hide the checklist with
  2. Check
    control-openhands browser click 'testid=show-getting-started-checklist-switch >> xpath=ancestor::label'

    (the label text is German now).

  3. Do
    control-openhands browser reset
  4. Do
    control-openhands onboard --skip
  5. Do
    control-openhands browser goto /settings/app
  6. Wait
    control-openhands browser wait 'testid=submit-button'
  7. Check
    control-openhands browser text 'testid=submit-button'

    is Änderungen speichern and

  8. Check
    control-openhands browser value 'testid=language-input'

    is Deutsch (backend), while

  9. Check
    control-openhands browser value 'testid=color-theme-input'

    is OpenHands-Neutral,

  10. Do
    control-openhands browser eval "localStorage.getItem('openhands-color-theme')"

    is null and

  11. Check
    control-openhands browser count 'testid=sidebar-onboarding-checklist'

    is 1 (browser-only, back to defaults).

  12. Note
    Restore English (the option keeps its English name in the German UI):
  13. Do
    control-openhands browser click 'testid=language-input'
  14. Do
    control-openhands browser click 'role=option[name="English"]'
  15. Do
    control-openhands browser click 'testid=submit-button'
  16. Do
    control-openhands browser reload
  17. Wait
    control-openhands browser wait 'testid=submit-button'
  18. Check
    control-openhands browser text 'testid=submit-button'

    reads Save Changes and

  19. Check
    control-openhands api GET /api/settings --pick misc_settings.app_preferences.language

    is en.

After the family #

  1. Expect
    After the family,
  2. Check
    control-openhands browser errors --app-only
  3. Note
    reports appErrors: 0.
  4. Note
    Requests to registry.npmjs.org (update check) and Google Fonts fail through the sandbox proxy; they are not app errors.

Gotchas and known limits

  • Two save models on one form: theme, checklist and voice input apply instantly and ignore Save; the rest is dropped silently if you navigate away before Save (filling git-user-name-input, then clicking sidebar-settings-/settings/secrets and back, shows the stored openhands again; browser dialogs is empty). Assert the instant ones without clicking Save, and never expect Save to enable for them.
  • The success toast uses the language that was active when you clicked Save, not the one you saved. In F16.language the French Save toasts Settings saved, the Arabic Save toasts Paramètres enregistrés, and the English restore toasts تم حفظ الإعدادات, so wait-text 'Settings saved' times out on the last two. Assert with browser text 'testid=submit-button' after a reload, or read the toast with browser toasts.
  • Some chrome strings stay English after a language switch: the sidebar Automate label in every language (the automation interface supplies it, not the translation file), and most of the command menu's own texts (placeholder Search actions, pages, and settings..., the search box's Search commands name, the group names and the item names) in French and 11 other languages. Only the Model Router and Agent Context items are translated (Routeur de modèles and Contexte de l'agent in French), because they reuse the settings navigation's titles. Do not treat the English ones as a language switch failure; the sidebar trigger's Search commands is translated.
  • In Arabic (dir="rtl") a switch that is on draws its thumb outside its track (on show-getting-started-checklist-switch the track spans x 778–818 and the thumb 822–838). Assert switch state with .checked, not from a screenshot.
  • The switches are hidden checkboxes: browser click 'testid=enable-analytics-switch' times out (element is not visible). Click the label text scoped to testid=app-settings-screen, and read .checked through eval.
  • role=option[name^=...] is not supported by the selector parser; use role=option >> has-text=<profile> for profile options whose names include the model.
  • conversation start leaves the browser on the conversation page, which also has a testid=submit-button; goto /settings/app before the next settings step or clicks hit the composer.
  • Which profile actually generated a title is not visible in the UI, the conversation API or the stack logs; F16.title-model proves the preference is stored and a title appears, not which model wrote it.
  • onboard --skip stores analytics off, so a fresh run starts with the switch unchecked even though the code defaults to on when the preference is missing.
  • browser storage lists localStorage, and browser storage --session lists sessionStorage, where the voice-input key lives. With --values that key prints as <redacted>, so the recipe reads its dummy value with eval. The key disappears in a new tab or browser session by design.
  • Deleting a profile through the LLM settings UI clears a matching title preference itself; only a profile removed outside the UI leaves a stale title_llm_profile that this page shows as Automatic.
  • The Title generation model combobox has a clear (×) button with no accessible name (- button in the snapshot, visible as × at phone width). At desktop width it only takes clicks after browser hover on the input (F16.title-model-clear); otherwise use the Automatic option.
  • browser eval wraps its argument as an expression: statements separated by ; fail with SyntaxError: Unexpected token ';'; wrap them in (() => { ...; return x; })().
  • With service stop agent-server, a browser reload of /settings/app shows the backend-unavailable screen (agent-server-onboarding-screen, Manage backends), not the form: stop the service only after the page is loaded, and restart afterwards.
  • Known product bugs (repro candidates): git identity never reaches the agent (#17899); switches are not keyboard reachable or announced (#17900); a failed save discards the user's edits (F16.save-states) (#17927).
  • The harness builds with VITE_DO_NOT_TRACK=1, so PostHog never starts: browser network --filter 'posthog|z\.openhands\.dev' reports total 0, also after analytics consent is granted (#18086). A browser-only Do Not Track setting, and a non-DNT build where consent starts PostHog, cannot be driven with the harness (F16.analytics).
  • The title model's clear button has no test id: the recipe's xpath relies on DOM nesting to skip the unnamed Show suggestions button. If both ever match, Playwright's strict mode fails the click instead of pressing the wrong one; report it as harness drift.

Source paths: src/routes/app-settings.tsx, src/components/features/settings/app-settings/, src/components/features/settings/settings-switch.tsx, src/themes/color-theme/, src/utils/transcription-endpoint-storage.ts, src/api/settings-service/settings-service.api.ts.