EN / field notes OpenHands feature map

OpenHands / F05

Composer, slash commands and plan mode

The composer is the input box at the bottom of the home page and of every conversation. A user types a message (Enter sends, Shift+Enter adds a line), attaches files and images, picks the LLM profile for the running conversation, watches and compacts the context window, stops and resumes the agent, and types / for built-in commands (/btw, /model, /goal, /plan, /code) and skills. /plan hands the next message to a planning agent whose PLAN.md card can be viewed and built; Ctrl/Cmd+Enter builds it.

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

How to get to it

  • Home page composer (/) and the composer of any conversation (/conversations/<id>, reached from the sidebar conversation list or by URL).
  • + (More actions, chat-plus-button) inside the composer opens the tools menu; its last item Add Files and Images opens the native file picker (upload-image-input).
  • Model pill (chat-input-llm-profile) next to +; at 320 px it moves into the vertical-dots More input actions button. On the home page it names the default profile (the one Settings → LLM marks as default); in a conversation it names the profile that conversation runs on.
  • Right side of the composer: status text with Stop/Play, context ring (context-window-meter), mic (chat-dictation-button), Send (submit-button).
  • Typing / at the start of a word opens the slash menu (slash-command-menu); /plan and /code are the only mode switches on the local backend (the Code/Plan pill is Cloud only).
  • Keyboard: Enter sends, Shift+Enter newline, Ctrl+Enter / Cmd+Enter builds a plan; Escape closes the slash menu.

Before you start

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

Preconditions:

  • Baseline state (launched, doctored, onboard --skip done) and control-openhands llm preset deepseek (profiles deepseek-flash active and deepseek-pro). Every recipe except F05.agent-profile-switch, the home draft, F05.image-only-send and F05.profile-identity needs a model. Without a DeepSeek key, QA_DUMMY_KEY=qa control-openhands llm set --profile qa-zeta --model openai/gpt-4o --api-key-env QA_DUMMY_KEY --no-validate stands in for the active profile: conversations can still be created (the agent errors at once), which is all those two bullets need, and every bullet that reads a reply is blocked.
  • F05.profile-identity needs two profiles on one model: its bullet creates qa-alpha and qa-zeta with llm set --no-validate right before it and deletes them afterwards.
  • A conversation to type into: control-openhands conversation start --prompt "Reply with only: ok" --wait --timeout 240 prints id (<id> below) and leaves the browser on /conversations/<id>.
  • Attachment fixtures: control-openhands fixture image --name qa-image and control-openhands fixture file --name qa-note.txt --content "QA attachment 7731" (both land in $OH_VERIFY_RUN/evidence/_fixtures/).
  • Desktop viewport unless a bullet says otherwise. Microphone dictation is blocked in the sandbox browser (no microphone); + > Git tools needs a conversation on a git workspace (not driven here).

Behavior inventory

25 stable behavior IDs and their expected behavior

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.

Type and send #

  1. Note
    On /conversations/<id> run
  2. Check
    control-openhands browser attr 'testid=submit-button' aria-label

    (Send message),

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

    (false),

  4. Do
    control-openhands browser type 'testid=chat-input' 'Reply with only:'
  5. Check
    control-openhands browser enabled 'testid=submit-button'

    (true),

  6. Do
    control-openhands browser press Shift+Enter
  7. Do
    control-openhands browser type 'testid=chat-input' 'ok2'
  8. Do
    control-openhands browser eval "document.querySelector('[data-testid=chat-input]').innerText"
  9. Note
    : Reply with only:\nok2.
  10. Do
    control-openhands browser press Enter --selector 'testid=chat-input'
  11. Note
    the field is empty and
  12. Check
    control-openhands browser count 'testid=stop-button'

    is 1 while the agent runs.

  13. Wait
    control-openhands conversation wait <id> --fresh --timeout 180

    (a fast model can finish before a separate --until running wait starts, which then times out on finished),

  14. Do
    control-openhands browser reload
  15. Check
    control-openhands browser count 'testid=user-message >> has-text=ok2'
  16. Note
    : 1;
  17. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'

    is ok2.

Draft survives navigation #

  1. Check
    control-openhands browser type 'testid=chat-input' 'QA draft text'
  2. Note
    and wait out the 500 ms save:
  3. Do
    control-openhands browser eval "new Promise(r=>setTimeout(()=>r(JSON.parse(localStorage.getItem('conversation-state-<id>')).draftMessage),1500))"

    returns QA draft text.

  4. Check
    control-openhands browser click 'testid=backend-selector-settings-link' --expect-url settings
  5. Do
    control-openhands browser back
  6. Note
    and the innerText eval above: QA draft text.
  7. Do
    control-openhands browser reload
  8. Note
    restores it again.
  9. Note
    Clear with
  10. Do
    control-openhands browser fill 'testid=chat-input' ''
  11. Note
    and wait a second before navigating.

Home draft #

  1. Do
    control-openhands browser goto /
  2. Do
    control-openhands browser type 'testid=chat-input' 'QA home draft'
  3. Do
    control-openhands browser goto /settings/secrets
  4. Do
    control-openhands browser goto /
  5. Note
    then the innerText eval: QA home draft;
  6. Do
    control-openhands browser storage --session

    lists oh:home-prompt-draft.

  7. Note
    Clear with
  8. Do
    control-openhands browser fill 'testid=chat-input' ''

Auto-resize #

  1. Note
    On /conversations/<id> (control-openhands browser goto /conversations/<id>; Home draft left /) type line, then eight times
  2. Do
    control-openhands browser press Shift+Enter
  3. Do
    control-openhands browser type 'testid=chat-input' 'lineN'

    (line2…line9):

  4. Check
    control-openhands browser bbox 'testid=chat-input'
  5. Note
    grows by 20 px per line, from height 20 to 180; after
  6. Do
    control-openhands browser fill 'testid=chat-input' ''
  7. Note
    it is 20.
  8. Check
    control-openhands browser count '#resize-grip'

    is 1.

  9. Do
    control-openhands browser drag '#resize-grip' --by 0,-200
  10. Note
    makes the empty field 220 high and
  11. Do
    control-openhands browser drag '#resize-grip' --by 0,150
  12. Note
    brings it to 70; after
  13. Do
    control-openhands browser reload
  14. Note
    it is 20 again (the dragged height is not kept).

Tools menu and macros #

  1. Note
    On /conversations/<id> run
  2. Do
    control-openhands browser click 'testid=chat-plus-button'
  3. Check
    control-openhands browser testids 'testid=tools-context-menu'
  4. Note
    : macros-button, show-skills-button, show-hooks-button, show-agent-tools-button, add-files-and-images-button (no git-tools-button outside a repository).
  5. Do
    control-openhands browser hover 'testid=macros-button'

    shows macros-submenu with increase-test-coverage-button, fix-readme-button, auto-merge-prs-button, clean-dependencies-button.

  6. Do
    control-openhands browser click 'testid=fix-readme-button'
  7. Note
    closes the menu and fills the composer with Please look at the README and make the following improvements…; the testid=user-message count is unchanged.
  8. Note
    Clear with
  9. Do
    control-openhands browser fill 'testid=chat-input' ''
  10. Note
    Open the menu again and
  11. Do
    control-openhands browser press Escape
  12. Note
    : browser count 'testid=tools-context-menu' is 0 and
  13. Do
    control-openhands browser eval "document.activeElement.dataset.testid"

    is chat-plus-button.

Composer tools menu offers Macros, Skills, Hooks, Agent tools and Add files and images.
The composer plus menu opens shortcuts, agent information and file attachment actions. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Composer tools menu offers Macros, Skills, Hooks, Agent tools and Add files and images.

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

How this screenshot was taken

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

OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser goto /conversations/<conversation-id>
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=chat-plus-button
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=tools-context-menu
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot testid=tools-context-menu --feature F05.plus-menu --name composer-tools-menu

Show dialogs #

  1. Note
    For each of show-skills-button, show-hooks-button, show-agent-tools-button:
  2. Do
    control-openhands browser click 'testid=chat-plus-button'
  3. Check
    control-openhands browser click 'testid=show-hooks-button'

    (etc.),

  4. Check
    control-openhands browser text 'role=dialog'

    (starts Available Skills, Available Hooks, Agent Tools & Metadata), then

  5. Do
    control-openhands browser press Escape
  6. Check
    control-openhands browser count 'role=dialog'
  7. Note
    : 0.

Agent profile before start #

  1. Do
    control-openhands browser goto /
  2. Do
    control-openhands browser click 'testid=chat-plus-button'
  3. Do
    control-openhands browser hover 'testid=switch-agent-profile-button'
  4. Check
    control-openhands browser text 'testid=agent-profile-submenu'
  5. Note
    : AVAILABLE PROFILES, default (checked; chat-input-agent-profile-option-default), Manage agent profiles.
  6. Do
    control-openhands browser click 'testid=agent-profile-submenu >> text=Manage agent profiles' --expect-url 'settings/agents'
  7. Note
    lands on /settings/agents.
  8. Note
    In a started conversation (control-openhands browser goto /conversations/<id>)
  9. Do
    control-openhands browser click 'testid=chat-plus-button'
  10. Check
    control-openhands browser count 'testid=switch-agent-profile-button'

    is 0.

Attach a file and an image #

  1. Note
    On /conversations/<id> run
  2. Do
    control-openhands browser upload 'testid=upload-image-input' "$OH_VERIFY_RUN/evidence/_fixtures/qa-image.png" "$OH_VERIFY_RUN/evidence/_fixtures/qa-note.txt"
  3. Do
    control-openhands browser screenshot 'testid=interactive-chat-box' --feature F05.attach-files --name attached
  4. Note
    : a qa-note.txt TXT chip and a thumbnail.
  5. Check
    control-openhands browser count 'testid=interactive-chat-box >> alt=qa-image.png'

    is 1.

  6. Do
    control-openhands browser click 'role=button[name="Upload as file"]'
  7. Note
    turns the image toggle into Do not upload as file (control-openhands browser attr 'role=button[name="Do not upload as file"]' aria-pressed is true); click it again to restore.
  8. Expect
    The remove buttons are named:
  9. Check
    control-openhands browser snapshot 'testid=interactive-chat-box'

    lists button "Remove qa-note.txt" and button "Remove image".

  10. Note
    Remove the image with
  11. Do
    control-openhands browser click 'testid=interactive-chat-box >> role=button[name="Remove image"]'
  12. Note
    the alt=qa-image.png count is 0.
  13. Do
    control-openhands browser type 'testid=chat-input' 'Reply with only the content of the attached file.'
  14. Do
    control-openhands browser press Enter --selector 'testid=chat-input'
  15. Note
    wait as in Type and send, and
  16. Check
    control-openhands browser text 'testid=user-message >> nth=-1'
  17. Note
    : it ends NEW FILES ADDED: qa-note.txt.
  18. Check
    control-openhands conversation events <id> --kinds MessageEvent --last 3

    shows the reply QA attachment 7731, and qa-note.txt sits in the conversation workspace printed by

  19. Check
    control-openhands conversation status <id>

Size limit #

  1. Note
    Arrange head -c 4194304 /dev/zero > "$OH_VERIFY_RUN/evidence/_fixtures/qa-big.bin" (and qa-two-a.bin, qa-two-b.bin with head -c 2097152).
  2. Do
    control-openhands browser upload 'testid=upload-image-input' "$OH_VERIFY_RUN/evidence/_fixtures/qa-big.bin"
  3. Check
    control-openhands browser toasts
  4. Note
    : Error: Files exceeding 3MB are not allowed: qa-big.bin.
  5. Note
    Uploading qa-two-a.bin and qa-two-b.bin together toasts Error: Total file size would be 4.0MB, exceeding the 3MB limit. Please select fewer or smaller files.
  6. Check
    control-openhands browser count 'testid=interactive-chat-box >> text=qa-big.bin'

    is 0.

Image alone #

  1. Note
    From the home composer:
  2. Do
    control-openhands browser goto /
  3. Check
    control-openhands browser enabled 'testid=submit-button'

    (false, empty field),

  4. Do
    control-openhands browser click 'testid=chat-plus-button'
  5. Do
    control-openhands browser upload-via 'testid=add-files-and-images-button' "$OH_VERIFY_RUN/evidence/_fixtures/qa-image.png"
  6. Check
    control-openhands browser count 'testid=home-chat-launcher >> alt=qa-image.png'

    (1) and

  7. Check
    control-openhands browser enabled 'testid=submit-button'
  8. Note
    : true while
  9. Do
    control-openhands browser eval "document.querySelector('[data-testid=chat-input]').innerText"

    is still empty (control-openhands browser screenshot 'testid=home-chat-launcher' --feature F05.image-only-send --name image-alone-send-enabled).

  10. Check
    control-openhands browser click 'testid=submit-button' --expect-url '/conversations/'
  11. Note
    and read <image-id> from
  12. Check
    control-openhands browser url
  13. Note
    After
  14. Wait
    control-openhands browser wait 'testid=user-message'
  15. Wait
    control-openhands browser wait 'testid=chat-message-sending' --state detached --timeout 5000

    (the pending bubble carries testid=user-message too, so wait for its Sending... to go, not only for a bubble; the wait returns "state": "detached" at once when the indicator has already gone, the usual case on a local stack),

  16. Check
    control-openhands browser count 'testid=user-message >> testid=image-preview'

    is 1, and

  17. Check
    control-openhands browser count 'testid=chat-message-sending'
  18. Check
    control-openhands browser count 'testid=chat-message-retry'

    are 0.

  19. Note
    Server side,
  20. Check
    control-openhands conversation events <image-id> --kinds MessageEvent --grep 'data:image/png;base64'
  21. Note
    matches the one user row (count 1; its text is a blank " ", the row carries images: 1, and the excerpt shows the "type":"image" block with image_urls), and
  22. Check
    control-openhands conversation events <image-id> --kinds MessageEvent

    (default --last) lists the same row with images: 1 and no excerpt. --kinds filters before --last counts, so --kinds MessageEvent --last 1 is that same row.

  23. Note
    To check that the model is handed the picture, send the image from Home together with Which two colours are the bars in this image? Answer with the colour names only. Do not run any tools. and read
  24. Check
    control-openhands conversation events <id> --kinds MessageEvent
  25. Note
    Expect orange and blue in the agent row and --kinds ActionEvent count 0 (fixture image draws those bars).
  26. Note
    Known failure (reproduced 2026-10-08, agent-server 1.53.0, the pinned version): on a deepseek/* profile the user row still carries images: 1, but the agent answers that no image was attached.
  27. Expect
    The model gets text only.
  28. Expect
    The cause is in the SDK, not Canvas: 1.53.0 string-serializes every model id containing deepseek, which drops image parts.
  29. Note
    OpenHands/software-agent-sdk#5460 and #5467 fixed it on 2026-10-08, after the 1.53.0 release.
  30. Note
    On a stack launched with --sdk-path <software-agent-sdk main> the same send answers Blue and orange. with no tool call.
  31. Expect
    An image-only send there reaches the model too: its first reasoning describes four blue and orange stripes.
  32. Note
    On an affected stack, the image-only send leaves the model with an empty task, and deepseek-flash then explores the disk (see Gotchas).
  33. Note
    Return with
  34. Do
    control-openhands browser goto /conversations/<id>

Paste text #

  1. Check
    control-openhands browser type 'testid=chat-input' 'QA paste text'
  2. Do
    control-openhands browser press Control+a
  3. Do
    control-openhands browser press Control+c
  4. Do
    control-openhands browser press Delete
  5. Do
    control-openhands browser press Control+v
  6. Do
    control-openhands browser eval "document.querySelector('[data-testid=chat-input]').innerHTML"
  7. Note
    : QA paste text, no markup.
  8. Note
    On a cleared field
  9. Do
    control-openhands browser paste 'testid=chat-input' --file "$OH_VERIFY_RUN/evidence/_fixtures/qa-image.png"
  10. Note
    attaches the image (control-openhands browser count 'testid=interactive-chat-box >> alt=qa-image.png' is 1, with an Upload as file toggle) and
  11. Check
    control-openhands browser paste 'testid=chat-input' --text 'QA pasted plain'
  12. Note
    inserts plain text (innerHTML is QA pasted plain).
  13. Do
    control-openhands browser drop-files 'testid=chat-input' "$OH_VERIFY_RUN/evidence/_fixtures/qa-note.txt" --stage over
  14. Note
    makes
  15. Check
    control-openhands browser text 'testid=interactive-chat-box'
  16. Note
    start Drop your files here; the same command without --stage drops it: a qa-note.txt TXT chip appears.
  17. Note
    Remove the chip with
  18. Do
    control-openhands browser click 'testid=interactive-chat-box >> role=button[name="Remove qa-note.txt"]'
  19. Note
    and the image as in Attach; clear the field.

Dictation failure #

  1. Check
    control-openhands browser click 'testid=chat-dictation-button' --observe '[role=status]' --observe-ms 3000
  2. Note
    : the observed toasts include Dictation failed. Check microphone access and your voice input settings. and
  3. Check
    control-openhands browser attr 'testid=chat-dictation-button' aria-label

    is back to Start dictation.

  4. Note
    Real dictation is blocked without a microphone.

Switch LLM profile #

  1. Do
    control-openhands browser click 'testid=chat-input-llm-profile'
  2. Check
    control-openhands browser text 'testid=chat-input-llm-profile-popover'
  3. Note
    : AVAILABLE PROFILES, deepseek-flash / deepseek/deepseek-flash, deepseek-pro / deepseek/deepseek-v4-pro, LLM Profiles.
  4. Do
    control-openhands browser click 'testid=chat-input-llm-profile-option-deepseek-pro'
  5. Wait
    control-openhands browser wait-text 'Switched to profile' --timeout 15000
  6. Check
    control-openhands browser text 'testid=chat-input-llm-profile'

    (deepseek-pro) and

  7. Check
    control-openhands conversation status <id>

    (model is deepseek/deepseek-v4-pro).

  8. Note
    Send Reply with only: pro-ok as in Type and send; the reply is pro-ok.
  9. Note
    After
  10. Do
    control-openhands browser reload
  11. Note
    the pill still reads deepseek-pro.
  12. Note
    Switch back with
  13. Do
    control-openhands browser click 'testid=chat-input-llm-profile'
  14. Do
    control-openhands browser click 'testid=chat-input-llm-profile-option-deepseek-flash'

Profile identity #

  1. Note
    Arrange two profiles on one model right before this bullet: QA_DUMMY_KEY=qa control-openhands llm set --profile qa-alpha --model openai/gpt-4o --api-key-env QA_DUMMY_KEY --no-validate --no-activate and QA_DUMMY_KEY=qa control-openhands llm set --profile qa-zeta --model openai/gpt-4o --api-key-env QA_DUMMY_KEY --no-validate (qa-zeta becomes the default: control-openhands api GET /api/profiles --pick active_profile is qa-zeta).
  2. Do
    control-openhands conversation start --prompt "Reply with only: identity-ok"

    (no --wait; with the dummy key the agent errors at once, which does not matter here) and note its id as <zeta-id>.

  3. Wait
    control-openhands browser wait 'testid=chat-input-llm-profile'
  4. Check
    control-openhands browser text 'testid=chat-input-llm-profile'

    is qa-zeta, not the alphabetically first profile on that model.

  5. Do
    control-openhands browser reload
  6. Note
    the wait and the text again: still qa-zeta.
  7. Note
    With a DeepSeek key, arrange both profiles on deepseek/deepseek-flash with --api-key-env DEEPSEEK_API_KEY and without --no-validate, and start with --prompt "Reply with only: identity-ok. Do not run any tools." --wait --timeout 180.
  8. Note
    Then the reply also survives the reload:
  9. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'

    is identity-ok before and after it (driven 2026-10-08).

  10. Expect
    The default agent profile still names deepseek-flash here, so the launch runs the pill's profile through agent settings (F13.llm-pill-precedence).
  11. Expect
    The name is stamped on the conversation in this browser:
  12. Do
    control-openhands browser eval "JSON.parse(localStorage.getItem('openhands-agent-server-conversation-metadata'))['<zeta-id>'].active_profile"

    is qa-zeta.

  13. Note
    Change the default in Settings:
  14. Do
    control-openhands browser goto /settings/llm
  15. Wait
    control-openhands browser wait 'testid=add-llm-profile'
  16. Do
    control-openhands browser click '[data-testid=profile-row]:has([title="qa-alpha"]) >> testid=profile-menu-trigger'
  17. Do
    control-openhands browser click 'testid=profile-actions-menu >> testid=profile-set-active'
  18. Wait
    control-openhands browser wait-text 'Switched to profile "qa-alpha"'

    (control-openhands api GET /api/profiles --pick active_profile is qa-alpha).

  19. Expect
    The home pill follows the default:
  20. Do
    control-openhands browser goto /
  21. Note
    the wait, and
  22. Check
    control-openhands browser text 'testid=chat-input-llm-profile'

    is qa-alpha; the started conversation keeps its own:

  23. Do
    control-openhands browser goto /conversations/<zeta-id>
  24. Note
    the wait, and the text is qa-zeta (control-openhands browser screenshot 'testid=interactive-chat-box' --feature F05.profile-identity --name conversation-keeps-qa-zeta).
  25. Note
    Restore: make deepseek-flash the default again the same way (its row's profile-menu-trigger, then profile-set-active), then
  26. Arrange
    control-openhands api DELETE /api/profiles/qa-alpha --write
  27. Arrange
    control-openhands api DELETE /api/profiles/qa-zeta --write

    (arrange, not proof) so /model below still counts two profiles.

  28. Note
    In a model-free run make qa-zeta the default instead and delete only qa-alpha.
  29. Note
    Return with
  30. Do
    control-openhands browser goto /conversations/<id>

Overflow menu #

  1. Do
    control-openhands browser viewport narrow
  2. Check
    control-openhands browser snapshot 'testid=interactive-chat-box'
  3. Note
    : the model pill is gone and button "More input actions" appears (at phone and 350x800 the pill stays inline).
  4. Do
    control-openhands browser click 'role=button[name="More input actions"]' --observe '[data-testid=chat-input-overflow-menu]' --observe-ms 1000
  5. Note
    : observed goes from <absent> to Model (the menu appears in the observed sequence; its delay varies from run to run, so do not assert a time) and has no later <absent>.
  6. Check
    control-openhands browser count 'testid=chat-input-overflow-menu'

    is 1 and

  7. Check
    control-openhands browser attr 'role=button[name="More input actions"]' aria-expanded

    is true.

  8. Do
    control-openhands browser click 'testid=overflow-model-button'
  9. Check
    control-openhands browser text 'testid=chat-input-overflow-menu'

    reads Model, AVAILABLE PROFILES, deepseek-flash / deepseek/deepseek-flash, deepseek-pro / deepseek/deepseek-v4-pro, LLM Profiles.

  10. Check
    control-openhands browser bbox 'testid=chat-input-llm-profile-option-deepseek-flash'
  11. Check
    control-openhands browser bbox 'role=link[name="LLM Profiles"]'
  12. Note
    Expected: both insideViewport true.
  13. Note
    Known failure (#18063): the submenu opens to the right of the menu, so the profile row starts at x about 270 with width 210 and the LLM Profiles link sits at y about 725, below the 700 px viewport (both insideViewport false); the profile clicks below still work because the first two rows keep a strip of about 50 px on screen (from x about 270 to the 320 px edge), while a click on the LLM Profiles link times out (browser screenshot --feature F05.overflow-menu --name model-submenu-narrow).
  14. Do
    control-openhands browser click 'testid=chat-input-llm-profile-option-deepseek-pro'
  15. Wait
    control-openhands browser wait-text 'Switched to profile' --timeout 15000
  16. Note
    close the menu (count 0), and
  17. Check
    control-openhands conversation status <id>

    shows deepseek/deepseek-v4-pro.

  18. Note
    Switch back the same way: the trigger, testid=overflow-model-button, then testid=chat-input-llm-profile-option-deepseek-flash (conversation status <id> shows deepseek/deepseek-flash).
  19. Note
    Open the menu once more; a second click on the trigger closes it (aria-expanded false).
  20. Note
    Return with
  21. Do
    control-openhands browser viewport desktop

Context window #

  1. Check
    control-openhands browser attr 'testid=context-window-meter' aria-label

    (Context window usage: 1% used (99% left)),

  2. Do
    control-openhands browser click 'testid=context-window-meter'
  3. Check
    control-openhands browser text 'testid=context-window-meter-popover'

    (Context Window, the percentage, Compact context, tokens such as 11.7k / 1.0M, Usage).

  4. Do
    control-openhands browser click 'testid=context-window-meter-bar-button'

    (the progress bar) closes the popover and opens the Usage tab (control-openhands browser testids --filter usage lists conversation-tab-usage, usage-panel).

  5. Expect
    The popover's Usage row does the same: from another tab (control-openhands browser click 'testid=conversation-tab-files'), reopen the ring,
  6. Do
    control-openhands browser click 'testid=context-window-plan-usage'
  7. Wait
    control-openhands browser wait 'testid=usage-panel'

    (the panel mounts a moment later; an immediate count is 0).

  8. Note
    Reopen the ring, then
  9. Check
    control-openhands browser click 'testid=context-window-compact-button' --observe '[role=status]' --observe-ms 25000
  10. Note
    : Context compaction started, then Context compacted;
  11. Check
    control-openhands conversation events <id> --kinds CondensationRequest,Condensation --last 5

    lists both kinds.

Stop and resume with a queued follow-up #

  1. Note
    Send Run this shell command: sleep 25 && echo slept. Then reply with only: done (type + Enter as above),
  2. Wait
    control-openhands conversation wait <id> --until running --timeout 30
  3. Wait
    control-openhands browser wait 'testid=stop-button'
  4. Check
    control-openhands browser attr 'testid=stop-button' aria-label

    is Stop.

  5. Note
    While it runs,
  6. Check
    control-openhands browser enabled 'testid=submit-button'

    is false with an empty field; type Also reply with: queued-ok (enabled turns true) and press Enter.

  7. Do
    control-openhands browser click 'testid=stop-button'
  8. Wait
    control-openhands browser wait 'testid=play-button' --timeout 30000
  9. Check
    control-openhands browser text 'testid=interactive-chat-box'
  10. Note
    ends Stopped,
  11. Check
    control-openhands browser attr 'testid=play-button' aria-label

    is Resume the agent task and

  12. Check
    control-openhands conversation status <id>

    is paused.

  13. Do
    control-openhands browser click 'testid=play-button'
  14. Note
    wait for running then the terminal state: the last agent message is done queued-ok and testid=user-message >> has-text=queued-ok counts 1.

Slash menu #

  1. Note
    On a cleared composer run
  2. Do
    control-openhands browser type 'testid=chat-input' '/'
  3. Check
    control-openhands browser count 'testid=slash-command-menu >> role=option'

    (37 with the bundled skills) and

  4. Check
    control-openhands browser text 'testid=slash-command-menu'
  5. Note
    : Commands, then /btw, /model, /goal, /plan, /code with descriptions, then skills (/agent-creator, …); no /new on the local backend.
  6. Note
    Type mo: the count drops (descriptions match too).
  7. Check
    control-openhands browser attr 'testid=slash-command-menu >> role=option >> nth=0' aria-selected

    is true; after

  8. Do
    control-openhands browser press ArrowDown
  9. Note
    the selected option is /plan (read it with control-openhands browser text 'testid=slash-command-menu >> [aria-selected=true]').
  10. Do
    control-openhands browser press Escape
  11. Note
    closes the menu and keeps /mo.
  12. Note
    On /btw,
  13. Do
    control-openhands browser press Tab
  14. Note
    leaves /btw in the field; on /model (trailing space) the menu lists /model deepseek-flash and /model deepseek-pro, and
  15. Do
    control-openhands browser press Enter
  16. Note
    inserts /model deepseek-flash instead of sending.
Typing slash in the composer opens the command list, with btw, model, goal and plan visible.
Type / to discover conversation commands and installed skill shortcuts. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Typing slash in the composer opens the command list, with btw, model, goal and plan visible.

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 fixture git-repo
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands conversation start --workspace qa-repo --prompt 'Reply with a markdown table with columns Step and Result and exactly two rows: Setup | ready, Reply | hello. Then a python code block containing print(1). Do not run any tools.' --wait --timeout 180
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands conversation events <conversation-id> --kinds ActionEvent --last 10
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=agent-message
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser type testid=chat-input /
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=slash-command-menu --max-lines 50
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F05.slash-menu --name slash-command-menu

/model #

  1. Note
    Type /model deepseek-pro,
  2. Do
    control-openhands browser press Escape

    (close the menu),

  3. Do
    control-openhands browser press Enter --selector 'testid=chat-input'
  4. Note
    : Switched to profile appears, the pill reads deepseek-pro and conversation status <id> shows deepseek/deepseek-v4-pro.
  5. Note
    Bare /model (Escape, Enter) adds Available profiles (2) to testid=model-messages;
  6. Do
    control-openhands browser click 'testid=model-messages >> role=button[name="Expand"]'
  7. Do
    control-openhands browser click 'role=button[name="Toggle details for deepseek-pro"]'
  8. Note
    show model: deepseek/deepseek-v4-pro, base_url: —, api_key: set. /model qa-nope (Escape, Enter) toasts Profile 'qa-nope' not found (control-openhands browser toasts) and the pill keeps deepseek-pro.
  9. Note
    Restore with /model deepseek-flash.

/btw #

  1. Note
    Note
  2. Check
    control-openhands conversation events <id> --kinds MessageEvent
  3. Note
    count, type /btw Reply with only: side-ok, Escape, Enter.
  4. Check
    control-openhands browser text 'testid=btw-messages'

    is BTW: + the question and testid=btw-spinner counts 1; after the answer the card shows Got it.

  5. Do
    control-openhands browser click 'testid=btw-messages >> role=button[name="Expand"]'
  6. Note
    reveals side-ok;
  7. Do
    control-openhands browser click 'testid=btw-messages >> role=button[name="Got it"]'
  8. Note
    removes the card (count 0).
  9. Expect
    The MessageEvent count is unchanged.
  10. Note
    Bare /btw (Escape, Enter) clears the field and
  11. Check
    control-openhands browser toasts

    shows Please provide a question — e.g. /btw <question>;

  12. Check
    control-openhands browser count 'testid=btw-messages'
  13. Note
    stays 0.

/goal #

  1. Note
    Bare /goal + Enter:
  2. Check
    control-openhands browser toasts

    shows Please provide an objective — e.g. /goal <objective>.

  3. Note
    Send /goal --max 1 Reply with only: goal-ok:
  4. Check
    control-openhands browser text 'testid=goal-status'

    reads Goal: … Round 0/1 running Stop;

  5. Wait
    control-openhands browser wait 'testid=goal-done' --timeout 150000
  6. Note
    then Round 1/1 complete score 100%.
  7. Note
    Send /goal --max 2 Reply with only: goal-two and at once
  8. Check
    control-openhands browser click 'testid=goal-status >> nth=-1 >> testid=goal-stop'
  9. Wait
    control-openhands browser wait 'testid=goal-resume'
  10. Note
    and the banner reads Round 0/2 interrupted Resume.
  11. Do
    control-openhands browser click 'testid=goal-resume'
  12. Note
    sets it running; poll
  13. Check
    control-openhands browser text 'testid=goal-status >> nth=-1'
  14. Note
    until a new row ends Round 1/2 complete score 100%.
  15. Expect
    The old Round 0/2 interrupted row no longer offers Resume:
  16. Check
    control-openhands browser count 'testid=goal-resume'

    is 0.

  17. Note
    After
  18. Do
    control-openhands browser reload
  19. Check
    control-openhands browser count 'testid=goal-status'

    is 3 and the goal-resume count is still 0.

/plan and /code #

  1. Note
    Send bare /plan (Escape, Enter):
  2. Check
    control-openhands browser attr 'testid=chat-input' data-placeholder
  3. Note
    turns Let’s work on a plan;
  4. Do
    control-openhands conversation list
  5. Note
    gains a planner conversation (title null at first, <planner-id>).
  6. Note
    Bare /code turns it back to What do you want to build?.
  7. Check
    control-openhands browser count 'role=button[name="Code"]'

    is 0 on local (the pill is Cloud only). /plan sent while the agent runs should be refused with feedback; today it is dropped silently: the field clears, no toast, the placeholder stays What do you want to build? (fail row; use-plan-mode-interceptor.ts swallows it by design but tells the user nothing).

Plan card #

  1. Note
    In plan mode send Plan creating a file hello.txt containing hi. Keep the plan to three short steps., then
  2. Wait
    control-openhands conversation wait <planner-id> --until running --timeout 60
  3. Wait
    control-openhands conversation wait <planner-id> --timeout 240
  4. Wait
    control-openhands browser wait 'testid=plan-preview-content' --timeout 60000
  5. Check
    control-openhands browser text 'testid=plan-preview-content'
  6. Note
    starts 1. OBJECTIVE and ends Read more;
  7. Check
    control-openhands browser enabled 'testid=plan-preview-build-button'

    is true.

  8. Note
    Use --fresh instead of the --until running wait if the planner is fast.
  9. Do
    control-openhands browser click 'testid=plan-preview-view-button'

    (Read more does the same) opens the Planner tab:

  10. Check
    control-openhands browser testids --filter planner

    lists conversation-tab-planner and planner-tab-build-button (Build ⌘↩).

Build shortcut #

  1. Note
    With the plan card shown and the agent idle,
  2. Do
    control-openhands browser press Control+Enter
  3. Note
    : the placeholder returns to What do you want to build?,
  4. Wait
    control-openhands conversation wait <id> --until running --timeout 60
  5. Note
    then --timeout 240 finishes,
  6. Check
    control-openhands browser text 'testid=user-message >> nth=-1'

    is Execute the plan based on the .agents_tmp/PLAN.md file. (the agent's reply wording varies, e.g. Executed the plan in .agents_tmp/PLAN.md), and hello.txt exists in the workspace from

  7. Check
    control-openhands conversation status <id>

/plan <task> and /code <task> #

  1. Note
    With the agent idle, send bare /plan, then /code Reply with only: code-ok (Escape, Enter): the placeholder turns What do you want to build? at once,
  2. Wait
    control-openhands conversation wait <id> --fresh --timeout 180
  3. Check
    control-openhands browser text 'testid=user-message >> nth=-1'

    is Reply with only: code-ok (no /code prefix) and the last agent message is code-ok.

  4. Note
    Then send /plan Plan writing bye.txt containing bye. One step only.: the placeholder turns Let’s work on a plan,
  5. Wait
    control-openhands conversation wait <planner-id> --fresh --timeout 240
  6. Check
    control-openhands conversation events <planner-id> --kinds MessageEvent --last 100
  7. Note
    ends with the user text Plan writing bye.txt containing bye. One step only. and a planner reply, and
  8. Do
    control-openhands conversation list
  9. Note
    still has only the two conversations (the existing planner is reused).

Build button #

  1. Note
    With the newer plan card from /plan Plan writing bye.txt containing bye. One step only. (above) shown and the agent idle,
  2. Do
    control-openhands browser click 'testid=plan-preview-build-button >> nth=-1'
  3. Note
    does the same as the shortcut: placeholder What do you want to build?,
  4. Wait
    control-openhands conversation wait <id> --fresh --timeout 240
  5. Note
    the same last user message, and bye.txt (content bye) in the workspace.

Phone #

  1. Note
    On /conversations/<id> run
  2. Do
    control-openhands browser viewport phone
  3. Check
    control-openhands browser bbox 'testid=interactive-chat-box'

    (insideViewport true, pageHorizontalOverflow false) and

  4. Do
    control-openhands browser screenshot --feature F05.phone --name composer

    (+, model pill, ring, mic and Send on one row).

  5. Note
    Return with
  6. Do
    control-openhands browser viewport desktop

Gotchas and known limits

  • Always export your own OH_VERIFY_RUN. launch --new repoints the shared current symlink, and another agent relying on current drove and then stopped this family's stack mid-run. If the URL jumps to a page you never opened or fetch failed appears, run control-openhands status before blaming the product.
  • conversation wait <id> right after sending returns at once with the previous finished status. Use conversation wait <id> --fresh; --until running then a terminal wait only works for turns longer than the poll interval (a Reply with only turn on deepseek-flash can finish first, and --until running then times out).
  • The slash menu stays open while you type a command; press Escape before Enter or Enter selects the highlighted option instead of sending.
  • Conversation drafts are saved 500 ms after the last keystroke and not flushed on unmount (the home draft is): navigating sooner restores the older draft (fail row) (#17926). Wait a second before navigating when testing restoration.
  • Escape closes the + tools menu (browser count 'testid=tools-context-menu' is 0) and returns focus to chat-plus-button, whether focus was on the trigger or on a menu item. A menu left open covers attachments in screenshots. Escape should also close the narrow-width "More input actions" overflow menu and return focus to its trigger. Known failure (reproduced 2026-10-08): browser count 'testid=chat-input-overflow-menu' stays 1 and the trigger's aria-expanded stays true, with focus on the trigger or on a menu item (#18172): close it with its trigger or an outside click (browser mouse-click 160 100 at narrow).
  • After a client-side switch between conversations (a sidebar row, or a user-message branch in F06.branch-from-here) Send keeps the previous conversation's state (repro candidate, not filed yet). Switching from an empty composer to one with a saved draft restores the text next to a disabled Send; this is a race (Send came up enabled in 1 of 7 tries), so one enabled reading does not show the bug is gone. Switching from a composer with text to an empty one leaves Send enabled, and clicking it does nothing. Typing or browser reload fixes it, and Enter in the field sends the restored text anyway. Read browser enabled 'testid=submit-button' after a reload, or after typing.
  • Uploaded chips and thumbnails have no test ids. Their remove buttons are named Remove <file name> (file chip) and Remove image (thumbnail), and Send, Stop and Play read Send message, Stop and Resume the agent task (#18033); select by those names, or by text/alt.
  • browser upload feeds the hidden input directly. To drive the real path, + > Add Files and Images, use control-openhands browser upload-via 'testid=add-files-and-images-button' <file>, which answers the native file chooser.
  • The home composer has no interactive-chat-box wrapper: scope its chips and thumbnails with testid=home-chat-launcher.
  • conversation events rows show a message's text and its attachment count (images: 1 for the image-only message, whose text is a blank); --grep 'data:image/png;base64' searches whole events and quotes the image content block in excerpts.
  • A user row with images: 1 proves that the image reached the Agent Server, not the model. Only the reply proves that the model saw it. On agent-server 1.53.0 and older, deepseek/* profiles drop the image before the LLM call (F05.image-only-send). An image sent with no text then leaves deepseek-flash with an empty task. On 2026-10-08 it ran env, read the run directory and other files on the host, and decoded the picture from the event store with PIL to answer. Pause such a conversation (control-openhands conversation pause <id>), and use the question-plus-image send for the model check.
  • The pill resolves the conversation's stamped profile before the user-level default, so profiles that share a model are told apart by name. The stamp lives in this browser's localStorage (openhands-agent-server-conversation-metadata), not on the server (api GET /api/conversations/<id> has no profile name): read it with browser eval, not api GET.
  • The Switched to profile notes are client-side and gone after reload; assert the pill and conversation status.
  • /plan messages go to a separate planner conversation; the browser stays on the parent /conversations/<id>, so wait on the planner id from conversation list.
  • Only the newest goal status offers Resume (#18036): once a resumed goal has posted a newer status, the old interrupted row shows no button, also after a reload. Count testid=goal-resume after the resumed goal ends; while it runs the count is 0 anyway.
  • Opening the context popover logs a 404 for /api/llm/balance on the local backend in browser errors --app-only; it does not break the popover.
  • --observe takes the verbs' selector syntax: testid= and plain CSS segments are watched by an in-page MutationObserver (observedBy mutation), while role=, text= and other engines are polled every 20 ms (poll-20ms), which misses a state shorter than that; prefer a testid= or CSS form for anything brief.
  • At narrow (320 px) the overflow menu's Model submenu opens to the right of the menu and runs off screen: each profile row starts at x about 270 with width 210 (browser bbox 'testid=chat-input-llm-profile-option-deepseek-flash', insideViewport false), so both names read deepse…. Pick profiles by test id: the click lands on the strip of about 50 px that stays on screen. Known issue: #18063 (filed from the follow-up noted in #18015; the submenu also runs past the bottom edge, so the LLM Profiles link cannot be clicked at narrow) (F05.overflow-menu).

Source paths: src/components/features/chat/, src/components/features/controls/tools-context-menu.tsx, src/components/features/controls/agent-status.tsx, src/hooks/chat/, src/utils/constants.ts.