EN / field notes OpenHands feature map

OpenHands / F08

Workspace drawer: files and changes

Next to the chat, a conversation has a right-hand drawer with tabs. This family covers the drawer itself (toggle, tab bar, overflow menu with pinning, the phone panel page) and its two workspace tabs. Files is a read-only browser of the conversation's working directory: a path row with copy, a strip of open files, a collapsible tree, and a Rich/Plain viewer for code, Markdown, HTML, images and binaries. Commits lists an "Uncommitted" row and the git history, newest first. Each expands into per-file Monaco diffs with old/diff/new modes. The Terminal, Browser, Planner, Task list and Usage tab bodies belong to F27. The Files tab has no upload, download or edit.

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

How to get to it

  • Chat header: the panel toggle (right-panel-toggle, block-drawer icon, right of the overview i button). At desktop width it shows or hides the drawer; at 1024 px or narrower it navigates to the panel page.
  • Drawer tab bar (tabs-pane-header): conversation-tab-files, conversation-tab-commits. The ellipsis (tabs-pane-header >> testid=ellipsis-button) opens the menu with conversation-tabs-menu-open-<tab> and conversation-tabs-menu-pin-<tab>.
  • Direct URL /conversations/<id>/panel (the phone panel page at 1024 px or narrower; wider, it redirects to /conversations/<id> with the drawer open).
  • Overview (conversation-overview-toggle) → Changes row (conversation-overview-diffs) → Commits tab with Uncommitted expanded.
  • Workspace paths in agent messages written as inline code (markdown-file-path-link), plus path chips in file-editor tool cards (file-path-chip) → Files tab with that file selected.
  • There is no command-menu entry or keyboard shortcut for the drawer or these tabs.

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 (three short model turns, deepseek-flash).
  • A git fixture: control-openhands fixture git-repo (creates <run>/workspace/qa-repo with README.md, src/calc.py, src/test_calc.py and one commit by QA Fixture).
  • The conversation runs in that repo. From / run control-openhands browser click 'testid=open-workspace-button', control-openhands browser click 'testid=workspace-dropdown', control-openhands browser click 'role=button[name="+ Add Workspace"]'. The folder browser opens at <run>/private/home, so then run control-openhands browser click 'testid=folder-browser-up' twice, control-openhands browser dblclick 'testid=folder-browser-entry-workspace', control-openhands browser dblclick 'testid=folder-browser-entry-qa-repo', control-openhands browser click 'testid=folder-browser-use' and control-openhands browser click 'testid=workspace-launch-button'. The composer now shows a qa-repo chip. Start with --stay, which keeps that chip; a plain conversation start navigates home and loses it.
  • Seed changes with a prompt file, because the command contains quotes. Write qa-f08-1.txt (anywhere outside the workspace) containing: In the current workspace directory run exactly this one bash command and then reply only "done": printf '# QA notes\n\nHello **world**\n' > qa-notes.md && printf '<h1>QA page</h1>\n' > qa-page.html && printf 'def sub(a, b):\n return a - b\n' >> src/calc.py && rm src/test_calc.py && printf 'PK\003\004' > qa-deck.pptx && head -c 64 /dev/urandom > qa-blob.bin && git status --short. Then run control-openhands conversation start --stay --prompt "$(cat qa-f08-1.txt)" --wait --timeout 240. Read <id> from its id (or control-openhands browser url); the browser stays on /conversations/<id>.
  • Viewport desktop (1440×1000) unless a bullet says phone.

Behavior inventory

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

Open the drawer #

  1. Check
    control-openhands browser attr 'testid=right-panel-toggle' aria-pressed

    (false) and

  2. Do
    control-openhands browser tooltip 'testid=right-panel-toggle'

    (Show panel).

  3. Do
    control-openhands browser click 'testid=right-panel-toggle'
  4. Note
    followed by
  5. Check
    control-openhands browser attr 'testid=right-panel-toggle' aria-label
  6. Check
    control-openhands browser visible 'testid=files-tab'
  7. Expect
    The label is Hide panel, aria-pressed is true, and Files is visible with the workspace path and tree (browser screenshot --feature F08.panel-toggle --name drawer-open-files).

Switch and close by tab #

  1. Do
    control-openhands browser click 'testid=conversation-tab-commits'
  2. Wait
    control-openhands browser wait 'testid=commit-list' --state visible --timeout 10000

    (the first open shows Loading changes... for a moment, so an immediate browser visible can read false).

  3. Note
    Click testid=conversation-tab-commits again. right-panel-toggle aria-pressed becomes false and
  4. Check
    control-openhands browser visible 'testid=tabs-pane-header'

    is false.

  5. Note
    Reopen with right-panel-toggle.

Reload #

  1. Note
    With Commits selected, run
  2. Do
    control-openhands browser reload
  3. Check
    control-openhands browser attr 'testid=right-panel-toggle' aria-pressed

    (false; the drawer is closed).

  4. Note
    Click testid=right-panel-toggle;
  5. Check
    control-openhands browser visible 'testid=commit-list'

    is true.

Overflow menu #

  1. Do
    control-openhands browser click 'testid=tabs-pane-header >> testid=ellipsis-button'
  2. Check
    control-openhands browser testids --filter conversation-tabs-menu
  3. Note
    there is one open row and one pin row per tab.
  4. Do
    control-openhands browser click 'testid=conversation-tabs-menu-open-usage'
  5. Check
    control-openhands browser count 'testid=conversation-tabs-menu-open-usage'

    is 0 (menu closed) and the bar's active label is Usage.

  6. Note
    Reopen the menu and run
  7. Do
    control-openhands browser mouse-click 560 600

    (over the chat); the menu count is 0.

Pin/unpin #

  1. Note
    With Usage active, open the menu and run
  2. Do
    control-openhands browser click 'testid=conversation-tabs-menu-pin-usage'
  3. Check
    control-openhands browser attr 'testid=conversation-tabs-menu-pin-usage' aria-label

    (Pin tab to bar; aria-pressed is false).

  4. Note
    Close the menu with browser mouse-click 560 600, then run
  5. Check
    control-openhands browser count 'testid=tabs-pane-header >> testid=conversation-tab-usage'
  6. Expect
    The count is 0 and Planner is now active.
  7. Do
    control-openhands browser reload
  8. Note
    and reopen the drawer; the count is still 0.
  9. Note
    To restore, open the menu, click testid=conversation-tabs-menu-pin-usage (label Unpin tab from bar), then
  10. Do
    control-openhands browser click 'testid=conversation-tabs-menu-open-files'
  11. Expect
    The usage tab count is 1 again.

Resize #

  1. Expect
    The divider has no test id; its grip is the first .cursor-ew-resize on the page (the second is the Files tree divider).
  2. Note
    With the drawer open,
  3. Check
    control-openhands browser bbox 'testid=chat-interface'
  4. Check
    control-openhands browser bbox 'testid=tabs-pane-header'

    are about 570 px wide each and

  5. Do
    control-openhands browser eval "localStorage.getItem('desktop-layout-panel-width')"

    is "50".

  6. Do
    control-openhands browser drag '.cursor-ew-resize >> nth=0' --by 200,0
  7. Note
    : the chat is 770 px, the drawer about 369 px and the key about 67.5. --by 900,0 clamps at "80", --by -1200,0 at "30" (chat 342 px), and after browser reload plus the toggle the chat is still 342 px.
  8. Note
    Restore with
  9. Do
    control-openhands browser drag '.cursor-ew-resize >> nth=0' --by 228,0

    (chat 570 px, key "50").

VS Code link #

  1. Note
    Blocked in a runtime without a running VS Code server:
  2. Check
    control-openhands browser count 'testid=drawer-vscode-link'

    is 0 (hidden, as designed), and

  3. Check
    control-openhands api GET /api/vscode/status
  4. Note
    explains why (enabled: false, or running: false when the agent-server has no editor binary, as in these stacks; see F26 Gotchas).
  5. Expect
    It needs a runtime with enable_vscode and a running editor, on an origin that serves /vscode/.

Archived #

  1. Note
    Blocked locally: archived status comes from a Cloud sandbox (sandbox_status).
  2. Note
    On such a conversation, browser enabled 'testid=right-panel-toggle' should be false and browser tooltip 'testid=right-panel-toggle' should read Unavailable for Archives.

Workspace path #

  1. Check
    control-openhands browser attr 'testid=files-tab-workspace-path-value' title

    (ends in /workspace/qa-repo).

  2. Note
    Click testid=files-tab-workspace-path >> testid=copy-to-clipboard.
  3. Note
    Immediately afterwards,
  4. Check
    control-openhands browser attr 'testid=files-tab-workspace-path >> testid=copy-to-clipboard' aria-label

    is Copied to clipboard and browser enabled on the same button is false.

  5. Check
    control-openhands browser clipboard

    returns the full path.

  6. Note
    About 3 s later the label is Copy to clipboard again.

Tree #

  1. Do
    control-openhands browser click 'testid=files-tab-refresh'
  2. Note
    first (see Gotchas: files the agent made with bash are missing until a refresh).
  3. Check
    control-openhands browser attr 'testid=file-tree-dir-src' aria-expanded

    (false),

  4. Do
    control-openhands browser click 'testid=file-tree-dir-src'

    (now true) and

  5. Do
    control-openhands browser click 'testid=file-tree-file-src/calc.py'
  6. Check
    control-openhands browser text 'testid=file-content-viewer-highlighted'

    shows the 4 lines def add…return a - b, and testid=file-quick-row-item-src/calc.py exists.

Files tree with src expanded and calc.py selected beside a highlighted Python code viewer.
Expand a folder and select a file to read it in the workspace panel. This capture uses a real CLI-created git fixture. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Files tree with src expanded and calc.py selected beside a highlighted Python code viewer.

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. Files came from control-openhands fixture git-repo. No agent edits or uncommitted-change flow was exercised. Doctor passed and pageErrors were zero. Error history contains GET /api/llm/balance 404 responses on this local backend; this is not a clean-errors claim.

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 press Escape
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser fill testid=chat-input ''
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click 'testid=chat-pane-header >> testid=ellipsis-button'
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=conversation-name-context-menu
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click 'testid=conversation-name-context-menu >> testid=display-cost-button'
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=usage-panel
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=conversation-tab-files
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=files-tab
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=file-tree-dir-src
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=file-tree-file-src/calc.py
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser snapshot testid=files-tab
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser testids testid=files-tab
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser eval '(() => { let n=document.querySelector('"'"'[data-testid=files-tab-tree]'"'"'); const a=[]; for(let i=0;n&&i<4;i++,n=n.parentElement)a.push({tag:n.tagName,testid:n.dataset.testid,children:Array.from(n.children).map(c=>({tag:c.tagName,testid:c.dataset.testid}))}); return a; })()'
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot 'testid=files-tab-tree >> xpath=..' --feature F08.files-tree --name workspace-file-tree

Tree toggle #

  1. Check
    control-openhands browser bbox 'testid=files-tab-tree'

    (width 224).

  2. Do
    control-openhands browser click 'testid=file-quick-row-tree-toggle'
  3. Check
    control-openhands browser attr 'testid=file-quick-row-tree-toggle' aria-label

    (Show file tree) and

  4. Check
    control-openhands browser count 'testid=files-tab-tree'

    (0).

  5. Note
    Reload and reopen the drawer: the count is still 0.
  6. Note
    Click the toggle again to restore (count 1).

Tree resize #

  1. Do
    control-openhands browser drag 'testid=files-tab-tree-resize-handle >> .cursor-ew-resize' --by 100,0
  2. Note
    browser bbox 'testid=files-tab-tree' is 324 wide and
  3. Do
    control-openhands browser eval "localStorage.getItem('files-tab-tree-width')"

    is "324". --by 500,0 clamps at 480, --by -600,0 at 160, and the width survives browser reload plus the toggle.

  4. Note
    Restore with --by 64,0 (224).

Rich/Plain #

  1. Note
    Click testid=file-tree-file-qa-notes.md; testid=file-content-viewer-markdown exists.
  2. Note
    Click testid=file-tree-file-qa-page.html, then run
  3. Check
    control-openhands browser attr 'testid=file-content-viewer-iframe' sandbox

    (allow-same-origin) and

  4. Do
    control-openhands browser eval "document.querySelector('[data-testid=file-content-viewer-iframe]').contentDocument.body.innerText"

    (QA page).

  5. Do
    control-openhands browser click 'testid=files-tab-content-mode-toggle-option-plain'
  6. Check
    control-openhands browser text 'testid=files-tab-content >> testid=file-content-viewer-highlighted'

    reads 1<h1>QA page</h1>.

  7. Expect
    After browser reload and reopening,
  8. Check
    control-openhands browser attr 'testid=files-tab-content-mode-toggle-option-plain' aria-checked

    is true.

  9. Note
    Switch back with testid=files-tab-content-mode-toggle-option-rich.

Open in new window #

  1. Note
    With qa-page.html selected, run
  2. Do
    control-openhands browser click 'testid=files-tab-open-in-new-window'
  3. Do
    control-openhands browser tabs
  4. Note
    Page 1's URL is /api/conversations/<id>/workspace/qa-page.html?v=<n>.
  5. Do
    control-openhands browser tab 1
  6. Check
    control-openhands browser text 'body'

    (QA page) and

  7. Do
    control-openhands browser close-tab 1

Open-file strip #

  1. Note
    With several files opened, run
  2. Check
    control-openhands browser attr 'testid=file-quick-row-item-qa-page.html' title

    (the path).

  3. Note
    Select qa-notes.md with
  4. Do
    control-openhands browser click 'testid=file-quick-row-item-qa-notes.md'
  5. Note
    then close it with
  6. Do
    control-openhands browser click 'testid=file-quick-row-close-qa-notes.md'
  7. Do
    control-openhands browser eval "[...document.querySelectorAll('[data-testid^=file-quick-row-item-]')].filter(e=>e.getAttribute('aria-selected')==='true').map(e=>e.dataset.testid)"
  8. Note
    names the neighbour.
  9. Note
    Run browser reload, reopen the drawer and
  10. Check
    control-openhands browser text 'testid=file-quick-row'
  11. Note
    : the same files remain.
  12. Note
    Opening a different conversation (see Commits empty state) shows no quick-row items: browser text 'testid=file-quick-row' is empty. browser testids omits strip tabs scrolled out of view; count them with browser eval "[...document.querySelectorAll('[data-testid^=file-quick-row-item-]')].length".

No selection #

  1. Note
    Close every open file with its file-quick-row-close-<path> button.
  2. Check
    control-openhands browser text 'testid=files-tab-content'

    reads Select a file to view its contents, and

  3. Check
    control-openhands browser count 'testid=files-tab-content-mode-toggle'

    is 0.

Second agent turn: commit, binaries, auto-refresh #

  1. Note
    Keep Files open with qa-notes.md selected in Rich mode.
  2. Note
    Write qa-f08-2.txt with: In the current workspace directory run exactly this one bash command and then reply only "done": printf 'BIN\000\001' > qa-nul.bin && printf 'PK\003\004\000\000' > qa-real.docx && python3 -c "import base64;open('qa-dot.png','wb').write(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='))" && printf '\n- edited by agent\n' >> qa-notes.md && git add qa-notes.md && git -c user.name=QA-Agent -c user.email=qa@example.invalid commit -qm 'QA agent commit' && git log --oneline.
  3. Wait
    control-openhands conversation start --stay --prompt "$(cat qa-f08-2.txt)" --wait --timeout 240
  4. Check
    control-openhands browser testids 'testid=files-tab-tree'
  5. Check
    control-openhands browser text 'testid=file-content-viewer-markdown'
  6. Note
    Expected: qa-nul.bin, qa-real.docx and qa-dot.png are listed, and the preview ends with edited by agent.
  7. Note
    Known failure (reproduced 2026-10-06): neither changes until Refresh, because the auto-refresh hook skips the file queries for shell commands on purpose (see Gotchas).
  8. Note
    Repro candidate, not yet filed; a report would ask to revisit that choice.
  9. Note
    Click testid=files-tab-refresh once: the list gains the three files and the open preview ends with edited by agent.

Fallbacks #

  1. Expect
    After a refresh, click testid=file-tree-file-qa-nul.bin (testid=file-content-viewer-binary-fallback, text Binary file – preview not available).
  2. Note
    Then click testid=file-tree-file-qa-real.docx (testid=file-content-viewer-unsupported-document, Preview isn't available for Word files.).
  3. Note
    Then click testid=file-tree-file-qa-dot.png and run
  4. Do
    control-openhands browser eval "document.querySelector('[data-testid=file-content-viewer-image] img').naturalWidth"

    (1).

  5. Note
    In Plain mode the PNG shows the binary fallback. qa-deck.pptx and qa-blob.bin from the first prompt have no NUL byte and render as text (see Gotchas).

File-editor edits auto-refresh; deleted file errors #

  1. Note
    Keep Files open with testid=file-tree-file-qa-blob.bin selected.
  2. Wait
    control-openhands conversation start --stay --prompt 'Do two steps, then reply only "done". Step 1: run the bash command: rm qa-blob.bin. Step 2: use your file editor tool (not bash) to create the file qa-editor.txt in the current workspace directory with the single line: created by file editor' --wait --timeout 240
  3. Note
    Without clicking Refresh,
  4. Check
    control-openhands browser count 'testid=file-tree-file-qa-editor.txt'

    is 1 and browser count 'testid=file-tree-file-qa-blob.bin' is 0 (the editor observation refetches the whole list). qa-blob.bin stays in the open-file strip, and the same observation re-reads the open file:

  5. Check
    control-openhands browser text 'testid=file-content-viewer-error'

    reads Failed to read qa-blob.bin: 404 and

  6. Check
    control-openhands browser network --filter qa-blob

    shows the 404 (browser screenshot --feature F08.files-load-error --name deleted-open-file).

Open from chat #

  1. Check
    control-openhands conversation start --stay --prompt 'Without running any tool, reply with exactly this text and nothing else: See
  2. Note
    src/calc.py for the code.' --wait --timeout 180.
  3. Note
    Close the drawer, then run
  4. Do
    control-openhands browser click 'testid=agent-message >> nth=-1 >> testid=markdown-file-path-link'
  5. Note
    right-panel-toggle aria-pressed turns true, testid=files-tab is visible and file-quick-row-item-src/calc.py is the selected tab.
  6. Expect
    The same path in the prompt bubble works too: close the drawer and click 'testid=user-message >> testid=markdown-file-path-link'.
  7. Note
    Phone: select another file (control-openhands browser click 'testid=file-quick-row-item-qa-notes.md'), run
  8. Do
    control-openhands browser viewport phone
  9. Note
    then the same agent-message click with --expect-url '/panel$': the URL becomes /conversations/<id>/panel, browser visible 'testid=files-tab' is true and
  10. Check
    control-openhands browser attr 'testid=file-quick-row-item-src/calc.py' aria-selected

    is true (browser screenshot --feature F08.files-open-from-chat --name phone-after-click).

  11. Note
    Return with
  12. Do
    control-openhands browser viewport desktop
  13. Note
    : the app replaces /panel with /conversations/<id> (control-openhands browser wait-url '/conversations/[^/]+$') and shows the drawer beside the chat.

Tool-card path chip #

  1. Expect
    After the file-editor turn above, close the drawer and expand its tool group and card:
  2. Check
    control-openhands browser click 'text=2 actions completed'
  3. Do
    control-openhands browser click 'role=region[name="Collapse actions"] >> role=button[name="Expand"] >> nth=-1'

    (the Create qa-editor.txt card).

  4. Check
    control-openhands browser text 'testid=file-path-chip'

    is the absolute <run>/workspace/qa-repo/qa-editor.txt.

  5. Do
    control-openhands browser click 'testid=file-path-chip'
  6. Note
    : right-panel-toggle aria-pressed turns true, the selected strip tab is file-quick-row-item-qa-editor.txt (relative) and browser text 'testid=files-tab-content' ends with created by file editor.

Commits list #

  1. Note
    Click testid=conversation-tab-commits and run
  2. Check
    control-openhands browser text 'testid=commit-list'
  3. Expect
    After the first prompt it reads -, Uncommitted, 6 files, then 4bf8272-style SHA, Initial fixture commit and … ago; after the second prompt QA agent commit appears above it, with author names QA-Agent and QA Fixture shown because there are now two authors.
  4. Do
    control-openhands browser click 'testid=uncommitted-changes-row-toggle'
  5. Check
    control-openhands browser text 'testid=uncommitted-changes-row-content'

    (the changed paths; aria-expanded is true).

  6. Arrange
    control-openhands browser click 'testid=commit-row >> has-text=Initial fixture commit >> testid=commit-row-toggle'
  7. Note
    Uncommitted's aria-expanded turns false, and
  8. Check
    control-openhands browser text 'testid=commit-row-content'

    lists README.md, src/calc.py, src/test_calc.py.

  9. Expect
    After the third prompt the Uncommitted count is 8 files.

Diff modes #

  1. Note
    In the expanded Uncommitted row, run
  2. Do
    control-openhands browser click 'testid=uncommitted-changes-row-content >> testid=file-diff-viewer-outer >> has-text=src/calc.py >> testid=collapse'
  3. Check
    control-openhands browser text 'testid=uncommitted-changes-row-content >> testid=file-diff-viewer-outer >> has-text=src/calc.py >> testid=editor-container'

    shows the added def sub lines, and view-mode-diff has aria-pressed true.

  4. Expect
    The row's buttons are named:
  5. Check
    control-openhands browser snapshot 'testid=uncommitted-changes-row-content >> testid=file-diff-viewer-outer >> has-text=src/calc.py'

    lists button "Old version", button "Diff" [pressed], button "New version" and button "Collapse" (the same collapse button reads Expand while the row is collapsed), so … >> role=button[name="Old version"] works as well as the test id.

  6. Note
    Click … >> testid=view-mode-old (2 lines), then … >> testid=view-mode-new (4 lines), then view-mode-diff.
  7. Check
    control-openhands browser errors --app-only
  8. Note
    before and after: each collapse and each diff→old switch adds TextModel got disposed before DiffEditorWidget model got reset (fail, #17567).

Markdown preview #

  1. Note
    In this order qa-notes.md is already committed, so check it in the QA agent commit row:
  2. Do
    control-openhands browser click 'testid=commit-row >> has-text=QA agent commit >> testid=commit-row-toggle'
  3. Note
    then with F='testid=commit-row >> has-text=QA agent commit >> testid=file-diff-viewer-outer >> has-text=qa-notes.md' click "$F >> testid=collapse" and "$F >> testid=view-mode-new".
  4. Check
    control-openhands browser count "$F >> testid=markdown-preview"

    is 1, rendering <h1>QA notes</h1>, world in bold and the edited by agent list item. (Before the second prompt the same check works under testid=uncommitted-changes-row-content.) Re-expand Uncommitted with uncommitted-changes-row-toggle for the next bullet.

Deleted file #

  1. Note
    Expand src/test_calc.py in Uncommitted.
  2. Check
    control-openhands browser text 'testid=uncommitted-changes-row-content >> testid=file-diff-viewer-outer >> has-text=src/test_calc.py >> testid=file-deleted-message'

    reads This file was deleted. Its previous contents are no longer available for preview.

From the overview #

  1. Note
    With the drawer closed, run
  2. Do
    control-openhands browser click 'testid=conversation-overview-toggle'
  3. Do
    control-openhands browser click 'testid=conversation-overview-diffs'

    (its siblings conversation-overview-diffs-additions/-deletions show +N/-N; deleted files are not counted). right-panel-toggle aria-pressed is true, testid=commit-list is visible, uncommitted-changes-row-toggle has aria-expanded true and testid=conversation-overview-panel is hidden.

  4. Expect
    After browser reload and the toggle, Commits is still selected.

Empty states #

  1. Arrange
    control-openhands fixture folder
  2. Note
    which makes <run>/workspace/qa-folder, a plain folder (no .git) with notes.md; by the time the conversation has run it holds an empty .git (seen live; not created by the prompt), so it ends up a repo with no commits.
  3. Do
    control-openhands browser goto /

    (the browser is on the qa-repo conversation) and pick it with the workspace picker as in Preconditions (folder-browser-entry-qa-folder).

  4. Wait
    control-openhands conversation start --stay --prompt 'Run exactly this bash command, then reply only "done": rm notes.md && git status --short' --wait --timeout 180
  5. Note
    Open the drawer and click testid=conversation-tab-commits;
  6. Check
    control-openhands browser count 'text="No commits yet"'

    is 1 (browser screenshot --feature F08.commits-states --name no-commits).

  7. Note
    On Files, click testid=files-tab-refresh;
  8. Check
    control-openhands browser text 'testid=files-tab-tree'

    reads No files in workspace. Loading changes... (testid=commits-tab-status) shows briefly on the first Commits open after a load: on /conversations/<id> with Files selected, browser reload, open the drawer, then

  9. Check
    control-openhands browser click 'testid=conversation-tab-commits' --observe 'testid=commits-tab-status' --observe-ms 4000
  10. Note
    records Loading changes... for a few ms. Waiting for runtime to start... (agent state init/loading/error) was not reached: service stop agent-server and restart both left No commits yet on screen.

Commit cap #

  1. Note
    Arrange more than 50 commits in the fixture (read-only for the UI): for i in $(seq 1 50); do git -C "$OH_VERIFY_RUN/workspace/qa-repo" -c user.name=QA-Bulk -c user.email=qa@example.invalid commit -q --allow-empty -m "QA bulk $i"; done.
  2. Note
    On the qa-repo conversation (control-openhands browser goto /conversations/<id>; Empty states left the qa-folder conversation) run browser reload, open the drawer on Commits and wait for testid=commit-list.
  3. Check
    control-openhands browser text 'testid=commit-list-cap-notice'

    reads Showing the latest 50 commits and browser count 'testid=commit-row' is 50.

  4. Note
    Do this last: it changes the commit list the other bullets read.

Phone panel page #

  1. Do
    control-openhands browser viewport phone
  2. Note
    on /conversations/<id>, then
  3. Check
    control-openhands browser click 'testid=right-panel-toggle' --expect-url '/panel$'
  4. Check
    control-openhands browser bbox 'testid=files-tab'
  5. Note
    gives insideViewport true and pageHorizontalOverflow false, with Commits diffs usable at 390 px (browser screenshot --feature F08.phone-panel-page --name panel).
  6. Check
    control-openhands browser attr 'testid=conversation-mobile-panel-back' aria-label

    is Back.

  7. Check
    control-openhands browser click 'testid=conversation-mobile-panel-back' --expect-url '/conversations/[^/]+$'
  8. Note
    the chat is visible.
  9. Note
    Return with
  10. Do
    control-openhands browser viewport desktop

Direct panel URL #

  1. Note
    At desktop run
  2. Do
    control-openhands browser goto /conversations/<id>/panel
  3. Expect
    The app replaces the URL with /conversations/<id> (the goto result already shows it; control-openhands browser url confirms),
  4. Check
    control-openhands browser count 'testid=conversation-mobile-panel-back'

    is 0, browser visible 'testid=chat-interface' is true and right-panel-toggle aria-pressed is true (the drawer opens beside the chat; browser screenshot --feature F08.panel-direct-url --name desktop-redirect).

  5. Do
    control-openhands browser viewport phone
  6. Note
    and the same goto: the URL keeps /panel, the conversation-mobile-panel-back count is 1 and chat-interface is not visible.
  7. Check
    control-openhands browser click 'testid=conversation-mobile-panel-back' --expect-url '/conversations/[^/]+$'

    returns to the chat.

  8. Note
    Return with
  9. Do
    control-openhands browser viewport desktop

Errors #

  1. Expect
    After each group run
  2. Check
    control-openhands browser errors --app-only
  3. Expect
    The Monaco disposal error above belongs to this family. 404s on /api/llm/balance and .agents_tmp/ downloads come from the Usage and Planner tabs (F27).

Gotchas and known limits

  • When the drawer is closed, its tabs stay mounted at width 0. browser count 'testid=files-tab' is 1 even then, so assert with browser visible or right-panel-toggle aria-pressed.
  • The tab row is a tablist "Conversation panels" of role=tab controls, each named after its tab even when inactive (#17612), so role=tab[name="Commits"] finds the Commits tab whether or not it is active; role=button[name="Commits"] finds nothing. aria-selected marks the selected tab. The conversation-tab-* test ids still work.
  • The tab row ends with the ellipsis menu (Customize tabs), then the VSCode button (drawer-vscode-link, named Open in VS Code) when the runtime reports a running editor. #17660 removed that button and #18048 restored it, so on a checkout between the two the drawer has no editor control at all.
  • The overflow menu is not a role=menu. Escape closes it and returns focus to the tab bar's ellipsis-button, both in the desktop drawer and on the phone /panel page; choosing a row or an outside click (browser mouse-click) closes it too.
  • The Files list and the open preview do not follow bash edits: use-auto-refresh-files-on-edit.ts refreshes the file queries only for file-editor observations, and its comment says shell observations skip them on purpose (so that every shell command does not reload canvas iframes). This map still expects files the agent writes to appear (F08.files-auto-refresh), so the bash case stays a fail row. Click files-tab-refresh once before asserting on files the agent made with the shell. Since #18032 that one click also re-reads the open file (the content fetch revalidates instead of using the browser HTTP cache, and the rich preview gets a new ?v=), and a page reload shows the current body without a click.
  • The chat also fetches the file list (to linkify paths). A conversation page left open while the agent works can therefore show a stale tree the moment the drawer first opens.
  • Binary detection is a NUL-byte sniff of the first 8 KB, and the Office message also needs that sniff to trip. A 4-byte PK\003\004 "pptx" or random bytes without a NUL render as text in the plain viewer. Fixtures must contain a NUL.
  • control-openhands fixture folder writes a plain folder with notes.md, but by the time the conversation has run the folder holds an empty .git, so a conversation that leaves notes.md in place shows Uncommitted 1 file rather than the empty state. Delete it first (as in the recipe).
  • Diff views are Monaco. testid=file-diff-viewer and file-single-viewer are not on the DOM; read text through testid=editor-container. The narrow drawer falls back to an inline (not side-by-side) diff. The view-mode buttons are named Old version, Diff and New version, and collapse is named Expand or Collapse after its state (#18033).
  • browser eval "navigator.clipboard.readText()" hangs for about 2 minutes (no clipboard permission). Use control-openhands browser clipboard to read what Copy wrote.
  • browser click ... --observe accepts the usual selector syntax (testid=...).
  • Two .cursor-ew-resize grips sit in the conversation page (chat/drawer, then the Files tree); scope the tree one with testid=files-tab-tree-resize-handle.
  • At phone width, chat path links (and the agent's canvas_ui_control tab and file requests, through src/services/canvas-ui.ts) navigate to /conversations/<id>/panel (#18045); at desktop width they open the side drawer and the URL does not change. A /panel URL above 1024 px redirects to /conversations/<id>, so a bullet that leaves the phone viewport on /panel ends on the chat route.
  • If the agent's terminal resets mid-command ("terminal session was reset", for example after a restart), the agent reruns the command. Check the workspace with git status through a second prompt rather than trusting the first observation. Runs no longer share a tmux server (OpenHands/OpenHands#17946).
  • Known issue: #17567 (Monaco disposed-model page error on diff unmount; it also fires on collapse and on a diff→old switch).

Source paths: src/components/features/conversation/conversation-tabs/, src/components/features/conversation/right-panel-toggle.tsx, src/components/features/conversation/conversation-main/, src/routes/conversation.tsx, src/hooks/use-conversation-panel-route.ts, src/routes/conversation-panel.tsx, src/routes/files-tab.tsx, src/components/features/files-tab/, src/hooks/query/use-workspace-files.ts, src/hooks/query/use-workspace-file-content.ts, src/hooks/use-auto-refresh-files-on-edit.ts, src/routes/commits-tab.tsx, src/components/features/diff-viewer/, src/services/canvas-ui.ts.