Engel’s notebook

Application settings

F16 · 20 mapped behaviors in the Self-Healing Codebase Project. Follow the prerequisites, actions and expected results; expand the original CLI recipe to replay it.

778 behavior IDs · 28 families · 844 named recipes & support steps
16 October 2026 · PR #18334 at 9a3c3a69
Canvas 1.27.1 · Agent Server 1.55.0 · automation 1.20.0

This is the map at the revision above, including its open bug notes. It does not incorporate later changes on main or claim a fresh verification run. Setup & reading guide · Download the scenario inventory

All scenarios & marker key · Open a script to read Given / Do / Expect. Expand the exact recipe for replayable commands.

F16 Application settings

20 behaviors · 24 scripts & contracts · Maintained source

Entry points & prerequisites

What this family covers

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.

How a user gets here

  • 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).

Given

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 visually hidden native checkboxes inside a <label>: click the label text scoped to the form or focus them with Tab and toggle with Space, 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.
Open from navigationF16.openMapped behavior

Scenario F16--open-from-navigation · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Settings”.
  2. Do
    Choose “App settings”.

Expect

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.

Exact CLI recipe & bug checks

From / run control-openhands browser click 'testid=backend-selector-settings-link', control-openhands browser click 'testid=sidebar-settings-/settings/app', then control-openhands browser url, control-openhands browser snapshot 'testid=app-settings-screen' and control-openhands browser screenshot --feature F16.open --name desktop. 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].

Command-menu and URL entriesF16.openMapped behavior

Scenario F16--command-menu-and-url-entries · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Open /.
  2. Do
    Press Control+k.
  3. Do
    Enter “Application” in “Command menu” / the combobox.
  4. Do
    Press Enter.
  5. Do
    Open /settings/app.

Expect

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.

Exact CLI recipe & bug checks

Run control-openhands browser goto / (the previous bullet ends on /settings/app, where the URL check would pass at once), control-openhands browser press Control+k, control-openhands browser type 'testid=command-menu >> role=combobox' Application, control-openhands browser snapshot 'testid=command-menu' (option "Application settings Change app language, theme, and preferences. Go" is [selected]), control-openhands browser press Enter, then control-openhands browser url; it ends in /settings/app. For the URL entry run control-openhands browser goto /settings/app, control-openhands browser enabled 'testid=submit-button' (false) and control-openhands browser attr 'testid=sidebar-settings-/settings/app' aria-current (page).

LanguageF16.languageMapped behavior

Scenario F16--language · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Language”.
  2. Do
    Choose “Français”.
  3. Do
    Choose “Submit”.
  4. Do
    Reload the page.
  5. Do
    Choose “Language”.
  6. Do
    Choose “English”.
  7. Do
    Choose “Submit”.
  8. Do
    Reload the page.

Expect

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).

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=language-input' (15 options), control-openhands browser click 'role=option[name="Français"]', control-openhands browser enabled 'testid=submit-button' (true), control-openhands browser click 'testid=submit-button', then control-openhands browser text 'testid=submit-button'. It reads Enregistrer les modifications without a reload. Run control-openhands browser reload, control-openhands browser value 'testid=language-input' (Français), 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 control-openhands api GET /api/settings (language is fr). Then check the document language with control-openhands browser eval "document.documentElement.lang + ' ' + document.documentElement.dir" (fr ltr) and the translated sidebar trigger with control-openhands browser attr 'testid=command-menu-trigger' aria-label (Rechercher des commandes). Choose role=option[name="Arabic"] the same way (click the input, the option, Save, reload): the eval reads ar rtl, and 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. 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. Restore: control-openhands browser click 'testid=language-input', control-openhands browser click 'role=option[name="English"]', control-openhands browser click 'testid=submit-button', control-openhands browser reload, control-openhands browser text 'testid=submit-button' reads Save Changes and the eval reads en ltr.

Colour themeF16.color-themeMapped behavior

Scenario F16--colour-theme · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Color theme”.
  2. Do
    Choose “Light+”.
  3. Do
    Reload the page.
  4. Do
    Open /.
  5. Do
    Open /settings/app.

Expect

choosing a colour theme applies it immediately without Save, persists after a reload and on other pages, and is never sent to the backend.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=color-theme-input', control-openhands browser snapshot 'role=listbox' (OpenHands-DeepSea, OpenHands-Neutral [selected], OpenHands-Neo, Light+, Solarized Light), control-openhands browser click 'role=option[name="Light+"]', then control-openhands browser eval "document.documentElement.style.colorScheme + ' ' + localStorage.getItem('openhands-color-theme')" (light light-plus) and control-openhands browser enabled 'testid=submit-button' (false: no Save needed). control-openhands browser screenshot --feature F16.color-theme --name light-plus shows a white page. Second views: control-openhands browser reload then control-openhands browser value 'testid=color-theme-input' (Light+); control-openhands browser goto / then control-openhands browser eval "getComputedStyle(document.body).backgroundColor + ' ' + document.documentElement.style.colorScheme" (rgb(255, 255, 255) light); control-openhands api GET /api/settings contains no theme key. 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).

Cloud forces analyticsF16.analytics-cloudBlocked

Scenario F16--cloud-forces-analytics · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Blocked: needs a Cloud backend selected in the backend selector. Expected: control-openhands browser enabled 'testid=enable-analytics-switch' is false and the switch reads checked.

Expect

on a Cloud backend the analytics switch is forced on and disabled.

Exact CLI recipe & bug checks

Blocked: needs a Cloud backend selected in the backend selector. Expected: control-openhands browser enabled 'testid=enable-analytics-switch' is false and the switch reads checked.

Sound notificationsF16.soundMapped behavior

Scenario F16--sound-notifications · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “App settings screen” / “Sound Notifications”.
  2. Do
    Choose “Submit”.
  3. Wait
    Wait for the element identified in the CLI recipe to appear.
  4. Do
    Reload the page.
  5. Do
    Start a conversation with the prompt “Reply with only the word ok. Do not run any tools.”.
  6. Do
    Open /settings/app.
  7. Do
    Start a conversation as specified in the recipe.

Expect

the "Sound Notifications" switch saves with the form; with it on, a sound plays when the agent finishes or needs input.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=app-settings-screen >> text=Sound Notifications', control-openhands browser click 'testid=submit-button', control-openhands browser wait-text 'Settings saved', control-openhands browser reload, read enable-sound-notifications-switch (true) and control-openhands api GET /api/settings (enable_sound_notifications: true). Restore by toggling and saving again (false). Audible half: with the switch on and saved, control-openhands conversation start --prompt "Reply with only the word ok. Do not run any tools." --wait --timeout 180, then control-openhands browser media: plays lists one /assets/notification-<hash>.mp3 at the moment the conversation finished. With the switch off, the same check after a conversation reads plays: [] (the F16.git-identity conversation below doubles as that negative check). Turn the switch off and save afterwards (control-openhands browser goto /settings/app first: conversation start left the browser on the conversation).

Getting Started checklistF16.checklistMapped behavior

Scenario F16--getting-started-checklist · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “App settings screen” / “Show Getting Started checklist”.
  2. Do
    Reload the page.

Expect

the "Show Getting Started checklist" switch hides or shows the sidebar checklist immediately, without Save, and survives a reload.

Exact CLI recipe & bug checks

Run control-openhands browser count 'testid=sidebar-onboarding-checklist' (1), control-openhands browser click 'testid=app-settings-screen >> text=Show Getting Started checklist', then the same count (0, immediately) and control-openhands browser enabled 'testid=submit-button' (false). control-openhands browser screenshot --feature F16.checklist --name hidden shows the sidebar without the "Getting started" card. control-openhands browser reload keeps the count at 0 and the switch unchecked. Click the label again to restore; the count returns to 1.

Title generation modelF16.title-modelMapped behavior

Scenario F16--title-generation-model · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Title llm profile”.
  2. Do
    Choose the option / containing “deepseek-pro”.
  3. Do
    Choose “Submit”.
  4. Wait
    Wait for the element identified in the CLI recipe to appear.
  5. Do
    Reload the page.
  6. Do
    Start a conversation with the prompt “Reply with only the word ok. Do not run any tools.”.
  7. Do
    Open /settings/app.

Expect

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.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=title-llm-profile-input' and control-openhands browser snapshot 'role=listbox': options Automatic (recommended), deepseek-flash · deepseek/deepseek-flash, deepseek-pro · deepseek/deepseek-v4-pro. Run control-openhands browser click 'role=option >> has-text=deepseek-pro', control-openhands browser click 'testid=submit-button', control-openhands browser wait-text 'Settings saved', control-openhands browser reload, control-openhands browser value 'testid=title-llm-profile-input' (deepseek-pro · deepseek/deepseek-v4-pro) and control-openhands api GET /api/settings (title_llm_profile: "deepseek-pro"). Then control-openhands conversation start --prompt "Reply with only the word ok. Do not run any tools." --wait --timeout 180 and, a few seconds later, control-openhands conversation status <id> 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). 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. Return with control-openhands browser goto /settings/app, choose role=option[name="Automatic (recommended)"], save; title_llm_profile is then absent from app_preferences.

Deleted profile falls backF16.title-model-fallbackMapped behavior

Scenario F16--deleted-profile-falls-back · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Setup
    Configure or inspect the model profiles specified in the recipe.
  2. Do
    Choose “Title llm profile”.
  3. Do
    Choose the option / containing “qa-title”.
  4. Do
    Choose “Manage LLM profiles”.
  5. Do
    Choose “Profile row” / containing “qa-title” / “Profile menu trigger”.
  6. Do
    Choose “Profile actions menu” / “Profile delete”.
  7. Do
    Choose “Delete profile confirm”.
  8. Do
    Choose “App settings”.
  9. Setup
    Arrange the prerequisite backend state through the API.
  10. Do
    Reload the page.

Expect

if the chosen profile is deleted (from LLM settings, or outside the UI), the dropdown shows Automatic and the form stays clean.

Exact CLI recipe & bug checks

Arrange a throwaway profile: control-openhands llm set --profile qa-title --model deepseek/deepseek-flash --api-key-file <key file> --no-activate --no-validate. 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). Delete it through the UI: control-openhands browser click 'role=link[name="Manage LLM profiles"]', control-openhands browser click 'testid=profile-row >> has-text=qa-title >> testid=profile-menu-trigger', control-openhands browser click 'testid=profile-actions-menu >> testid=profile-delete', control-openhands browser click 'testid=delete-profile-confirm', then control-openhands browser click 'testid=sidebar-settings-/settings/app'. 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. For a preference that goes stale outside the UI, arrange control-openhands api PATCH /api/settings --data '{"misc_settings_diff":{"app_preferences":{"title_llm_profile":"qa-missing"}}}' --write, then control-openhands browser reload: the dropdown still shows Automatic (recommended) and Save stays disabled, while the API keeps qa-missing until the next Save of this form. 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 endpointF16.voice-endpointMapped behavior

Scenario F16--voice-input-endpoint · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Enter “http://127.0.0.1:9/v1” in “Transcription base url”.
  2. Do
    Enter “qa-dummy-key” in “Transcription api key”.
  3. Do
    Enter “qa-whisper” in “Transcription model”.
  4. Do
    Reload the page.
  5. Do
    Enter “” in “Transcription base url”.

Expect

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.

Exact CLI recipe & bug checks

Run control-openhands browser fill 'testid=transcription-base-url-input' http://127.0.0.1:9/v1, control-openhands browser fill 'testid=transcription-api-key-input' qa-dummy-key, control-openhands browser fill 'testid=transcription-model-input' qa-whisper, then control-openhands browser enabled 'testid=submit-button' (false) and control-openhands browser attr 'testid=transcription-api-key-input' type (password). After control-openhands browser reload, control-openhands browser value 'testid=transcription-model-input' is qa-whisper (same for the other two). 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. Clear each with control-openhands browser fill 'testid=transcription-base-url-input' '' (and the other two). Using the endpoint from the composer microphone is not driven here (needs microphone capture and a transcription server).

Git identityF16.git-identityKnown failure

Scenario F16--git-identity · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Enter “QA Tester” in “Git user name”.
  2. Do
    Enter “qa-tester@example.com” in “Git user email”.
  3. Do
    Choose “Submit”.
  4. Wait
    Wait for the element identified in the CLI recipe to appear.
  5. Do
    Reload the page.
  6. Do
    Start a conversation with the 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>'”.

Expect

Git Username and Git Email save with the form, persist after a reload, and are what the agent's commits use.

Known bug observations

Known failure:** the observation is 0 Author identity unknown ...: the identity is stored only in misc_settings.app_preferences and never reaches the agent.

Exact CLI recipe & bug checks

Run control-openhands browser fill 'testid=git-user-name-input' 'QA Tester', control-openhands browser fill 'testid=git-user-email-input' 'qa-tester@example.com', control-openhands browser click 'testid=submit-button', control-openhands browser wait-text 'Settings saved', control-openhands browser reload, then control-openhands browser value 'testid=git-user-name-input' (QA Tester) and control-openhands api GET /api/settings (git_user_name, git_user_email set). Agent half: 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, then control-openhands conversation events <id> --kinds ObservationEvent,MessageEvent with the id from the start output. 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.

Git validation and defaultsF16.git-identity-validationMapped behavior

Scenario F16--git-validation-and-defaults · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Enter “not-an-email” in “Git user email”.
  2. Do
    Choose “Submit”.
  3. Do
    Enter “” in “Git user email”.
  4. Do
    Enter “” in “Git user name”.
  5. Wait
    Wait for the element identified in the CLI recipe to appear.
  6. Do
    Reload the page.

Expect

an email without @ is refused by native validation; empty fields save the defaults openhands / openhands@all-hands.dev.

Exact CLI recipe & bug checks

Run control-openhands browser fill 'testid=git-user-email-input' 'not-an-email', control-openhands browser click 'testid=submit-button', then 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. Then control-openhands browser fill 'testid=git-user-email-input' '', control-openhands browser fill 'testid=git-user-name-input' '', click submit, control-openhands browser wait-text 'Settings saved', control-openhands browser reload: the inputs read openhands and openhands@all-hands.dev, which also restores the baseline.

Save button statesF16.save-statesKnown failure

Scenario F16--save-button-states · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Wait
    Wait for the element identified in the CLI recipe to appear.
  2. Setup
    Change the run-owned service state: stop agent-server.
  3. Wait
    Wait for the element identified in the CLI recipe to appear.
  4. Do
    Restart the run-owned stack as specified.
  5. Do
    Open /settings/app.

Expect

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 server's message (An error occurred when it has none) and should keep the user's edits for a retry.

Known bug observations

Known failure:** control-openhands browser eval "document.querySelector('[data-testid=enable-sound-notifications-switch]').checked" is false and Save is disabled: the edit is silently discarded. Bring the backend back with control-openhands restart, control-openhands doctor, control-openhands browser goto /settings/app.

Exact CLI recipe & bug checks

Click the analytics label once: control-openhands browser enabled 'testid=submit-button' is true; click it again: false (back to the stored value). Each successful save above made while the UI is in English shows the toast found by control-openhands browser wait-text 'Settings saved'; 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). Pending and failure: run control-openhands service stop agent-server (the open page keeps its form), click testid=app-settings-screen >> text=Sound Notifications, start a sampler with 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'; })()", click testid=submit-button, control-openhands browser wait-text 'An error occurred' (after three PATCH /api/settings attempts answer 502, browser network --filter /api/settings shows them; the ingress's plain-text 502 body carries no server message, so the toast is the generic one, #18051), then control-openhands browser eval "(() => { clearInterval(window.__iv); return window.__seen.join(' -> '); })()": 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. By the time the failure toast is read the form has usually come back, adding -> Save Changes skeleton=false; assert the prefix. Expected after the failure: the Sound switch stays on so the user can retry. Known failure: control-openhands browser eval "document.querySelector('[data-testid=enable-sound-notifications-switch]').checked" is false and Save is disabled: the edit is silently discarded. Bring the backend back with control-openhands restart, control-openhands doctor, control-openhands browser goto /settings/app.

Keyboard and screen readersF16.keyboardMapped behavior

Scenario F16--keyboard-and-screen-readers · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Use browser focus as specified in the exact recipe.
  2. Do
    Press Tab.

Expect

every control, including the three switches, is reachable with Tab and exposed to assistive technology.

Exact CLI recipe & bug checks

Run control-openhands browser focus 'testid=color-theme-input', then repeat control-openhands browser press Tab and control-openhands browser eval "(document.activeElement.getAttribute('data-testid')||'') + ' ' + document.activeElement.tagName". Expected (the acceptance criterion): focus visits the three switches before title-llm-profile-input. The first Tab lands on enable-analytics-switch, followed by enable-sound-notifications-switch and show-getting-started-checklist-switch; Space toggles the focused checkbox. control-openhands browser snapshot 'testid=app-settings-screen' --feature F16.keyboard --name aria exposes each checkbox label and checked state. The label draws a visible focus outline.

sh

  control-openhands browser viewport desktop
  control-openhands browser goto /settings/app
  control-openhands browser wait 'testid=submit-button'
  control-openhands browser record start --feature F16.keyboard --name keyboard-desktop
  control-openhands browser focus 'testid=color-theme-input'
  control-openhands browser press Tab
  control-openhands browser press Escape
  control-openhands browser screenshot --feature F16.keyboard --name keyboard-desktop
  control-openhands browser record stop --gif
  control-openhands assert --field value --equals enable-analytics-switch -- browser eval "document.activeElement.getAttribute('data-testid')"

sh

  control-openhands browser viewport phone
  control-openhands browser goto /settings/app
  control-openhands browser wait 'testid=submit-button'
  control-openhands browser record start --feature F16.keyboard --name keyboard-phone
  control-openhands browser focus 'testid=color-theme-input'
  control-openhands browser press Tab
  control-openhands browser press Escape
  control-openhands browser screenshot --feature F16.keyboard --name keyboard-phone
  control-openhands browser record stop --gif
  control-openhands assert --field value --equals enable-analytics-switch -- browser eval "document.activeElement.getAttribute('data-testid')"
Phone layoutF16.phoneMapped behavior

Scenario F16--phone-layout · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Setup
    Use the phone viewport.
  2. Do
    Open /settings/app.
  3. Do
    Scroll “Submit” as specified in the recipe.
  4. Setup
    Use the desktop viewport.

Expect

the form fits a 390 px viewport without horizontal overflow and Save is reachable by scrolling.

Exact CLI recipe & bug checks

Run control-openhands browser viewport phone, control-openhands browser goto /settings/app, control-openhands browser bbox 'testid=app-settings-screen' (pageHorizontalOverflow: false, width 348 at x 16), control-openhands browser screenshot --feature F16.phone --name top, control-openhands browser scroll 'testid=submit-button', control-openhands browser bbox 'testid=submit-button' (insideViewport: true) and control-openhands browser screenshot --feature F16.phone --name bottom. Return with control-openhands browser viewport desktop.

Clear the title modelF16.title-model-clearMapped behavior

Scenario F16--clear-the-title-model · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Title llm profile”.
  2. Do
    Choose the option / containing “deepseek-flash”.
  3. Do
    Hover over “Title llm profile”.
  4. Do
    Choose “Title llm profile” / its containing control.

Expect

the Title generation model combobox has a clear (×) button, shown on hover; it resets the choice to Automatic (recommended).

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=title-llm-profile-input', control-openhands browser click 'role=option >> has-text=deepseek-flash' (Save enables), control-openhands browser hover 'testid=title-llm-profile-input', then control-openhands browser click 'testid=title-llm-profile-input >> xpath=../..//button[not(@aria-label)]'. control-openhands browser value 'testid=title-llm-profile-input' reads Automatic (recommended) and control-openhands browser enabled 'testid=submit-button' is false. Without the hover the click times out (<div ...> intercepts pointer events).

Filter a dropdown by typingF16.dropdown-filterMapped behavior

Scenario F16--filter-a-dropdown-by-typing · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Enter “Deu” in “Language”.
  2. Do
    Press Escape.

Expect

typing into the Language (or any) combobox filters its options; Escape restores the stored value and leaves Save disabled.

Exact CLI recipe & bug checks

Run control-openhands browser fill 'testid=language-input' Deu and control-openhands browser snapshot 'role=listbox': the only option is Deutsch. Save is briefly true while the text is typed; control-openhands browser press Escape restores control-openhands browser value 'testid=language-input' to English and control-openhands browser enabled 'testid=submit-button' to false.

Browser vs backend scopeF16.browser-scopeMapped behavior

Scenario F16--browser-vs-backend-scope · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Show getting started checklist switch” / its containing control.
  2. Do
    Start with a fresh browser profile.
  3. Do
    Follow the onboarding setup in the recipe.
  4. Do
    Open /settings/app.
  5. Wait
    Wait for “Submit” to appear.
  6. Do
    Choose “Language”.
  7. Do
    Choose “English”.
  8. Do
    Choose “Submit”.
  9. Do
    Reload the page.
  10. Wait
    Wait for “Submit” to appear.

Expect

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.

Exact CLI recipe & bug checks

Choose Deutsch in testid=language-input and save, choose Solarized Light in testid=color-theme-input, and hide the checklist with control-openhands browser click 'testid=show-getting-started-checklist-switch >> xpath=ancestor::label' (the label text is German now). Then control-openhands browser reset, control-openhands onboard --skip, control-openhands browser goto /settings/app, control-openhands browser wait 'testid=submit-button': control-openhands browser text 'testid=submit-button' is Änderungen speichern and control-openhands browser value 'testid=language-input' is Deutsch (backend), while control-openhands browser value 'testid=color-theme-input' is OpenHands-Neutral, control-openhands browser eval "localStorage.getItem('openhands-color-theme')" is null and control-openhands browser count 'testid=sidebar-onboarding-checklist' is 1 (browser-only, back to defaults). Restore English (the option keeps its English name in the German UI): control-openhands browser click 'testid=language-input', control-openhands browser click 'role=option[name="English"]', control-openhands browser click 'testid=submit-button', control-openhands browser reload, control-openhands browser wait 'testid=submit-button'; control-openhands browser text 'testid=submit-button' reads Save Changes and control-openhands api GET /api/settings --pick misc_settings.app_preferences.language is en.

Backend switch in placeF16.backend-switchMapped behavior

Scenario F16--backend-switch-in-place · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Setup
    Arrange the prerequisite backend state through the API.
  2. Do
    Open /settings/app.
  3. Do
    Reload the page.
  4. Do
    Choose “Backend selector”.
  5. Do
    Choose “Backend selector” / “Connected QA_Second”.
  6. Wait
    Wait for “Environment switch overlay” to disappear.
  7. Do
    Enter “QA Local Edit” in “Git user name”.
  8. Setup
    Use the phone viewport.
  9. Do
    Use browser mouse click as specified in the exact recipe.

Expect

switching the active backend in the backend selector while this page is open shows the new backend's stored preferences (the footer already names it), also when switching back to a backend the page has already loaded; an unsaved edit is dropped on switch.

Exact CLI recipe & bug checks

Needs a second backend with a different Git username: the second stack from F25's Preconditions added as QA_Second (F25 Add bullet), arranged with control-openhands api PATCH /api/settings --run "$OH_VERIFY_RUN_2" --write --data '{"misc_settings_diff":{"app_preferences":{"git_user_name":"QA Second Git"}}}'. With Local active, run control-openhands browser goto /settings/app and control-openhands browser reload; browser value 'testid=git-user-name-input' is openhands. Switch without leaving the page: control-openhands browser click 'testid=backend-selector', control-openhands browser click 'testid=backend-selector >> role=option[name="Connected QA_Second"]', control-openhands browser wait 'testid=environment-switch-overlay' --state detached --timeout 10000, wait a few seconds: the value is QA Second Git and browser text 'testid=backend-synced-settings-badge' names QA_Second (a first visit to a backend loads fresh). Switch back the same way to Connected Local: the value is openhands beside synced from Local backend, and it keeps following the active backend on further switches. The form remounts per backend, so an unsaved edit is dropped: on Local run control-openhands browser fill 'testid=git-user-name-input' 'QA Local Edit' (browser enabled 'testid=submit-button' is true), switch to QA_Second (QA Second Git, Save false) and back (openhands, Save false); control-openhands api GET /api/settings --pick misc_settings.app_preferences.git_user_name is unchanged. At browser viewport phone the selector is in the drawer (testid=sidebar-mobile-menu-toggle, then 'testid=sidebar-mobile-drawer >> testid=backend-selector' and role=option[name="Connected QA_Second"]; close the drawer with browser mouse-click 370 400) and the field follows the same way. Switch back to Local and remove QA_Second afterwards (F25).

After each groupF16 · setup / cleanupSupport step

Scenario F16--after-each-group · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

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

Expect

Complete this support step before continuing to the next scenario.

Exact CLI recipe & bug checks

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

Hard Do Not TrackF16.analyticsMapped behavior

Scenario F16--hard-do-not-track · Permalink · Source

Given

Use F16’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Use browser storage as specified in the exact recipe.

Expect

the "Send anonymous usage data" switch saves with the form and sets browser telemetry consent to granted/denied.

Exact CLI recipe & bug checks

With the harness build's VITE_DO_NOT_TRACK=1, open /settings/app, read browser network --external --filter 'posthog|z.openhands.dev' and browser storage. Grant analytics through the real switch and Save, reload, inspect those again, then revoke and Save to restore. Expected: zero PostHog requests and no PostHog storage key, including after opt-in. This checks the build flag; browser-only DNT and builds without it have separate paths.

Family notes, prerequisites for known issues & 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 visually hidden native checkboxes: Use the label as the pointer target. Click the label text scoped to testid=app-settings-screen, or use Tab/Space, 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); 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 is outside this recipe. Consent reversal requires the explicit non-DNT build and loopback-only analytics configuration described above, served with launch --build never (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.

Before you start

Use an OpenHands checkout at the source revision, with Node 24+, installed npm dependencies, uv / uvx, and a supported Chromium browser. The verification skill is the complete setup contract.

export PATH="$PWD/.agents/skills/verify-openhands/scripts:$PATH"
export OH_VERIFY_RUN=$(control-openhands launch --new --print-run)
control-openhands doctor
control-openhands onboard --skip
# Configure a model only for a scenario that needs one:
control-openhands llm preset deepseek
# When the run is complete:
control-openhands stop

F01 walks onboarding instead of skipping it. No-model checks run before model configuration. Keep OH_VERIFY_RUN set so commands address your own run. Desktop is 1440 × 1000; phone is 390 × 844. Model-backed checks need the configured provider key and budget.

Recipes run in source order within each family and can share fixtures. The page preserves that order; “as above” is a dependency on an earlier case. Setup commands and API writes arrange state. They do not prove a UI path. Follow the family cleanup steps.

A known failure preserves the intended expectation beside the observed defect. Executable known-bug blocks use xfail for a reproduced symptom and xpass when it stops reproducing. Cloud accounts, enterprise capabilities and other unavailable prerequisites remain explicit. “Not run” is different from “blocked”.

The maintained map describes application behavior at its pinned revision. Once a fix is verified in the application version under test, remove the obsolete failure note and keep the normal expectation. Dated evidence belongs in the backfill ledger.

Read the full map index and coverage boundaries, the CLI guide, or the earlier illustrated edition. The verification approach adapts Lauren Tan’s (@poteto) pstack verification-skill generators; attribution and adaptation.