EN / field notes OpenHands feature map

OpenHands / F06

Agent activity: messages, events and lifecycle

Inside a conversation the user watches the agent work: their own messages appear at once (optimistically), the agent's replies render as markdown, tool calls are grouped into collapsible "N actions completed" blocks whose cards show commands, outputs, diffs and task lists, and a live chip says what the agent is doing right now. Around the transcript sit lifecycle controls and states: the starting pill, stop and resume, the confirmation prompt when confirmation mode is on, error and reconnect banners, the no-LLM banner and the skill-installed banner. History pages in from the server, survives reloads and backend restarts, and any message can be branched into a new conversation.

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

How to get to it

  • Open any conversation: a sidebar conversation card, or the direct URL /conversations/<id>. Leaving one for the home page: the sidebar's New Chat link; coming back: its card again.
  • Start one from the home composer (New Chat, /): type and press Send. control-openhands conversation start drives exactly this path.
  • An empty conversation: the sidebar's create-thread button (conversation-panel-new-thread-picker), then No workspace (or a workspace).
  • Composer status control (right side of the composer): Stop while running, Play while stopped.
  • Hover a bubble: Branch from here and Copy to clipboard icons; hover a tool title for its timestamp.
  • Confirmation mode and the critic are switched on under Settings → Verification (/settings/verification; the page itself belongs to the settings families).

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 (deepseek-flash active). Every prompt below is tiny and stays in the conversation's own workspace.
  • Conversation ids are printed as "id" by control-openhands conversation start, or read from control-openhands browser url (/conversations/<id>). <id> below is always the conversation the step just created or opened.
  • F06.llm-not-configured-banner needs a second, fresh control-openhands launch --new --build never with no llm preset (export its run dir as OH_VERIFY_RUN for those commands, then control-openhands stop it).
  • F06.critic-result is blocked: it needs a model and an OpenHands Cloud critic API key (Settings → Verification → Critic API Key); its bullet says what a scored run shows. The automatic-fetch half of F06.load-older-history (its 16-command conversation) needs a model; the scroll-to-top half is driven without one, on the dummy profile below, with thirty refused user messages. F06.hook-events needs a configured hook; none is set in a fresh run.
  • F06.clock-skew-send and F06.failed-send-persists need no model reply: a conversation that exists is enough (without a key the agent errors at once, and the user rows still land). Without a key, create the family's conversations with a dummy profile (QA_DUMMY_KEY=qa control-openhands llm set --profile qa-zeta --model openai/gpt-4o --api-key-env QA_DUMMY_KEY --no-validate) and control-openhands conversation start --prompt ... without --wait. F06.corrective-nudge is blocked: the nudge needs a model turn with neither text nor a tool call, which a normal prompt to deepseek-flash does not produce. When one happens, control-openhands browser count 'testid=corrective-nudge-message' is 1 and browser count 'testid=user-message >> has-text=Your last response did not include' is 0.
  • The harness browser grants clipboard access: control-openhands browser clipboard reads what a Copy button wrote and browser clipboard --write qa-empty resets it first.
  • A terminal call can come back as "The terminal session was reset..." after this run's agent server restarts; the model reruns it, which turns one action into a group of two (see Gotchas).

Behavior inventory

35 stable behavior IDs and their expected behavior
  • F06.empty-state-suggestions an empty conversation shows "Let's start building!" with four suggestion chips; a chip fills the composer with its prompt. Read recipe ↓
  • F06.user-and-agent-messages user and agent bubbles; a long user message is clipped with a gradient and expands on click ("View More"). Read recipe ↓
  • F06.message-copy hovering a bubble shows Copy to clipboard; it copies the message's markdown source and the label flips to Copied to clipboard for 2 s. Read recipe ↓
  • F06.code-block-copy hovering a fenced code block in an agent reply shows its own Copy button, which copies only the code. Read recipe ↓
  • F06.timestamps hovering a bubble or tool title shows the event's local date and time. Read recipe ↓
  • F06.pending-messages a sent message shows "Sending..." until the server echoes it; a failed send shows Failed to send with Retry and Dismiss; Retry delivers it exactly once. Read recipe ↓
  • F06.clock-skew-send a message sent while the browser clock runs ahead of the server is still confirmed by the server echo: no lingering "Sending..." and no Retry. Read recipe ↓
  • F06.failed-send-persists a failed send keeps its Retry row after the backend returns and after leaving and reopening the conversation from the sidebar; sending the same text again succeeds without clearing it, and Retry then delivers that attempt exactly once. Read recipe ↓
  • F06.markdown-rendering agent replies render markdown: tables inside a horizontal scroller with edge fades, fenced code, inline code. Read recipe ↓
  • F06.thinking model reasoning appears as a collapsed "Thinking" row that expands. Read recipe ↓
  • F06.event-groups consecutive tool calls collapse into "N actions completed" (or "k/N actions completed" with a spinner while one is pending); expanding lists one titled row per action. Read recipe ↓
  • F06.tool-visualizers expanding a tool row shows a rich body: terminal command and output (non-zero exit badge exit N), file-editor path chip and content. Read recipe ↓
  • F06.markdown-file-preview when the agent creates a Markdown file, its row stays expanded and ungrouped and shows a rendered, height-limited preview card with the file name and View, which opens the file in the Files tab. Read recipe ↓
  • F06.task-list task-tracker calls render a "Tasks" card with done / todo icons. Read recipe ↓
  • F06.events-match the stream shows one bubble per message event and one row per action, with no duplicates (compare with conversation events). Read recipe ↓
  • F06.load-older-history only the newest 50 events load first; the previous 50 are fetched when the user scrolls the transcript to its top (a "Fetching older messages…" row shows until they arrive) and automatically when the content is too short to scroll, so the first prompt and all actions appear. Read recipe ↓
  • F06.scroll-to-bottom scrolling up shows a scroll-to-bottom button; clicking it returns to the bottom and hides it. Read recipe ↓
  • F06.live-activity while the agent runs, a chip above the composer names the current action ("Thinking", then the action title) and the composer status reads Running. Read recipe ↓
  • F06.status-indicator while a new conversation starts, a "Starting" pill shows above the composer. Read recipe ↓
  • F06.stop-resume Stop in the composer interrupts the agent, including a running tool call (status Stopped, play button, an Agent error row for the interrupted call); Play resumes it to completion. Read recipe ↓
  • F06.reload-mid-run reloading while the agent runs reattaches to the live run; it finishes without duplicated events. Read recipe ↓
  • F06.reconnect when the Agent Server goes away the composer shows Disconnected; after it returns the page reconnects without a reload. Read recipe ↓
  • F06.error-banner losing the server shows "Unable to connect to server" with Retry, Copy and Close; Retry reconnects once the server is back. Read recipe ↓
  • F06.error-events a conversation error (ConversationErrorEvent) surfaces as a warning banner and the composer status Error. Read recipe ↓
  • F06.confirmation-mode with confirmation mode on, a lock chip sits above the composer and each action waits for "Do you want to continue with this action?" with Cancel (⇧⌘⌫) and Continue (⌘↩). Read recipe ↓
  • F06.confirmation-shortcuts Cmd+Enter continues and Shift+Cmd+Backspace cancels a pending action. Read recipe ↓
  • F06.branch-from-here hovering a message shows Branch from here; on an agent message it forks inclusively; on a user message it forks without that message and pre-fills its text in the new composer, ready to send. The branch is titled "<title> (branch)" and keeps working. Read recipe ↓
  • F06.image-attachments images sent with a message show as thumbnails in the bubble; expand opens a lightbox that closes with X or Escape. Read recipe ↓
  • F06.skill-install-banner after the agent installs a skill into the workspace, a banner offers "Start new conversation with this skill" (same workspace) and Close. Read recipe ↓
  • F06.llm-not-configured-banner with no usable LLM, a conversation shows "Your LLM isn't set up yet..." with Set up LLM; suggestions are hidden and sending is disabled. Read recipe ↓
  • F06.critic-result with the critic enabled, agent messages and the finishing action each carry a "Critic: agent success likelihood" block (stars labelled Score: <N.N>%, one decimal, with the percentage beside them); Expand details lists the issue categories (Potential Issues, Infrastructure, Likely Follow-up). Read recipe ↓
  • F06.hook-events hook executions render as their own rows in the stream.
  • F06.corrective-nudge when the model answers with neither a message nor a tool call, the SDK's nudge (Your last response did not include a function call or a message. …) shows as a muted, italic role=note line with an info icon (corrective-nudge-message), not as a user bubble (#17864).
  • F06.phone at 390 px the transcript fits the viewport; wide tables scroll inside their own container. Read recipe ↓

Readable recipes

Read each script from top to bottom. Code is copied from the map; prose gives the action, expected observation, and conditions. <id>, <run> and similar placeholders stand for values from your own run. Short forms such as browser count continue the same control-openhands invocation; they are kept as documented.

Expected observations describe the recipe’s contract. Captures below selected recipes show representative real states from this snapshot; they do not mark every mapped behavior as passed. Follow cleanup before moving to another family.

One run that exercises the stream #

  1. Wait
    control-openhands conversation start --prompt "Run the terminal command 'echo qa-f06-hello', then create a file qa_f06.txt containing the word hi. Finally reply with a markdown table with columns Step and Result (two rows) followed by a python code block that prints 1." --wait --timeout 300
  2. Check
    control-openhands browser testids 'testid=chat-interface'
  3. Check
    control-openhands browser snapshot 'testid=agent-message'
  4. Expect
    The testids include user-message, event-group ("3 actions completed"), agent-message, markdown-table-scroll with markdown-table-scroll-fade-left/-right; the snapshot shows a table with column headers Step and Result and a code block print(1).
  5. Note
    Compare with
  6. Check
    control-openhands conversation events <id>
  7. Note
    : one user and one agent MessageEvent, three ActionEvents (terminal, file_editor, canvas_ui_control), matching one user-message, one group of three rows and one agent-message.
  8. Expect
    The thinking row (collapsible-thinking) lives inside the group, so it is listed only after the next bullet expands it.
Agent reply renders a two-row Step and Result table and a Python code block containing print(1).
A real deepseek-flash reply renders markdown as a readable table and syntax-highlighted code. This representative capture uses a no-tools prompt. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Agent reply renders a two-row Step and Result table and a Python code block containing print(1).

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. A tiny no-tools prompt was used; the ActionEvent count was zero. Event groups and tool visualizers were not 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 screenshot testid=agent-message --feature F06.markdown-rendering --name markdown-reply

Expand a group and its rows #

  1. Do
    control-openhands browser click 'testid=event-group-toggle'

    (label becomes Collapse actions),

  2. Check
    control-openhands browser text 'testid=event-group'

    (three titles, e.g. "Echo test string in terminal", "Create qa_f06.txt containing hi"; titles are model-written), then

  3. Do
    control-openhands browser click 'testid=event-group-content >> role=button[name="Expand"][exact] >> nth=0'
  4. Note
    twice (each click expands the next collapsed row).
  5. Expect
    The group text now contains echo qa-f06-hello and its output qa-f06-hello, the file path chip (testid=file-path-chip) and the content hi.
  6. Do
    control-openhands browser click 'testid=collapsible-thinking-toggle >> nth=0'
  7. Note
    reveals collapsible-thinking-content.
  8. Note
    Take
  9. Do
    control-openhands browser screenshot 'testid=chat-interface' --feature F06.tool-visualizers --name bash-and-file

Open files from the stream #

  1. Note
    On the same page the agent may already have opened the file; run
  2. Do
    control-openhands browser click 'testid=file-quick-row-close-qa_f06.txt'
  3. Note
    and check
  4. Check
    control-openhands browser count 'testid=file-quick-row-item-qa_f06.txt'

    is 0.

  5. Do
    control-openhands browser click 'testid=agent-message >> testid=markdown-file-path-link'
  6. Note
    : the count is 1 and
  7. Check
    control-openhands browser text 'testid=file-content-viewer-plain'

    is hi.

  8. Note
    Close it again and
  9. Do
    control-openhands browser click 'testid=file-path-chip'

    (inside the expanded file-editor row): the count is 1 again.

Markdown file preview #

  1. Wait
    control-openhands conversation start --prompt "Use the file_editor tool to create the file qa_notes.md containing a heading '# QA Notes' and one bullet '- first'. Do nothing else and reply DONE." --wait --timeout 150
  2. Note
    Without any click,
  3. Check
    control-openhands browser text 'testid=markdown-file-preview'

    reads QA Notes first qa_notes.md View (rendered heading and bullet, then the file name) and count 'testid=event-group' is 0.

  4. Check
    control-openhands browser count 'testid=file-quick-row-item-qa_notes.md'

    is 0;

  5. Do
    control-openhands browser click 'testid=markdown-file-preview-view'
  6. Note
    makes it 1 and
  7. Check
    control-openhands browser text 'testid=file-content-viewer-markdown'

    reads QA Notes first.

  8. Do
    control-openhands browser goto /conversations/<stream id>
  9. Note
    to return to the stream conversation for the next bullets.

Timestamps #

  1. Do
    control-openhands browser tooltip 'testid=user-message'
  2. Do
    control-openhands browser tooltip 'testid=event-group-toggle'
  3. Expect
    Both return the local date and time, e.g. Oct 4, 2026, 11:08 PM.

Copy a message and a code block #

  1. Note
    On the same page run
  2. Check
    control-openhands browser clipboard --write qa-empty
  3. Do
    control-openhands browser hover 'testid=agent-message >> nth=0'
  4. Do
    control-openhands browser click 'testid=agent-message >> nth=0 >> testid=copy-to-clipboard >> nth=0'

    (the bubble's button; the code block has a second, hidden copy-to-clipboard).

  5. Check
    control-openhands browser attr 'testid=agent-message >> nth=0 >> testid=copy-to-clipboard >> nth=0' aria-label

    is Copied to clipboard, and Copy to clipboard again about 2 s later;

  6. Check
    control-openhands browser clipboard
  7. Note
    starts with | Step | Result | and ends with the fenced print(1) block.
  8. Do
    control-openhands browser hover 'testid=agent-message >> pre'
  9. Do
    control-openhands browser click 'testid=agent-message >> pre >> testid=copy-to-clipboard'
  10. Check
    control-openhands browser clipboard

    is exactly print(1).

Non-zero exit badge #

  1. Note
    In an open conversation run
  2. Do
    control-openhands browser fill 'testid=chat-input' "Run exactly this terminal command, verbatim, with nothing appended: ls /qa_missing_dir2 . Then reply OK."
  3. Do
    control-openhands browser click 'testid=submit-button'
  4. Wait
    control-openhands conversation wait <id> --fresh --timeout 150

    (without --fresh it returns the previous run's finished at once).

  5. Note
    If the reply's actions render as a single row, run
  6. Do
    control-openhands browser click 'testid=chat-scroll-container >> role=button[name="Expand"][exact] >> nth=-1'
  7. Note
    if they render as a group (browser count 'testid=event-group' grew, e.g. 2 actions completed after a terminal reset and rerun), run
  8. Do
    control-openhands browser click 'testid=event-group-toggle >> nth=-1'
  9. Do
    control-openhands browser click 'testid=event-group-content >> nth=-1 >> role=button[name="Expand"][exact] >> nth=-1'
  10. Check
    control-openhands browser count 'testid=chat-scroll-container >> text="exit 2"'

    is 1; the badge sits above the No such file or directory output (browser screenshot 'testid=event-group >> nth=-1' --feature F06.tool-visualizers --name exit-code).

  11. Note
    Repeating the check in the same conversation needs a new directory name and a count that grows, because an earlier expanded badge stays on the page.

Task list #

  1. Note
    From / run
  2. Do
    control-openhands browser goto /
  3. Do
    control-openhands browser fill 'testid=chat-input' "Use your task_tracker tool to plan exactly two tasks titled 'qa step one' and 'qa step two'; mark 'qa step one' done and 'qa step two' todo. Do nothing else and reply DONE."
  4. Check
    control-openhands browser click 'testid=submit-button' --expect-url '/conversations/' --observe '[data-testid=chat-status-indicator]' --observe-ms 8000
  5. Note
    observed contains Starting for a moment (the pill).
  6. Wait
    control-openhands conversation wait <id> --timeout 150
  7. Check
    control-openhands browser text 'testid=chat-scroll-container'
  8. Note
    : it contains Tasks, qa step one, qa step two; the screenshot shows a check icon and muted text for the done task, an empty circle for the todo one.

Empty state #

  1. Do
    control-openhands browser click 'testid=conversation-panel-new-thread-picker'
  2. Check
    control-openhands browser click 'testid=launch-no-workspace' --expect-url '/conversations/'
  3. Check
    control-openhands browser text 'testid=chat-suggestions'
  4. Note
    : Let's start building! plus Increase test coverage, Auto-merge PRs, Fix README, Clean dependencies.
  5. Do
    control-openhands browser click 'testid=chat-suggestions >> text=Fix README'
  6. Check
    control-openhands browser text 'testid=chat-input'
  7. Note
    : the composer holds the README-improvement prompt.

Pending bubble and long messages #

  1. Note
    Write a 25-line message to a file (first line Reply with only the word OK. Ignore the filler lines below., then filler line 1…filler line 24), run
  2. Check
    control-openhands browser fill 'testid=chat-input' x --value-file <file>
  3. Do
    control-openhands browser click 'testid=submit-button' --observe '[data-testid=chat-message-sending]' --observe-ms 3000
  4. Note
    observed shows Sending... then <absent> once the server echoes it.
  5. Note
    After
  6. Wait
    control-openhands conversation wait <id> --timeout 120
  7. Check
    control-openhands browser testids 'testid=chat-scroll-container'

    lists chat-message-truncation-gradient and chat-message-expand ("View More");

  8. Do
    control-openhands browser click 'testid=chat-message-expand'
  9. Note
    removes both and the whole text shows. testid=agent-message reads OK.

Browser clock ahead #

  1. Note
    On the same conversation, skew the page's clock before loading it:
  2. Do
    control-openhands browser clock --offset-ms 300000
  3. Do
    control-openhands browser goto /conversations/<id>
  4. Wait
    control-openhands browser wait 'testid=chat-input'
  5. Do
    control-openhands browser eval "new Date().toISOString()"

    is about five minutes ahead of date -u (read-only shell check).

  6. Do
    control-openhands browser fill 'testid=chat-input' 'Reply with only: clock-ok'
  7. Do
    control-openhands browser click 'testid=submit-button' --observe '[data-testid=chat-message-sending],[data-testid=chat-message-error]' --observe-ms 6000
  8. Note
    : observed shows Sending... and then <absent> (the server echo confirmed it), never Failed to send.
  9. Check
    control-openhands browser count 'testid=chat-message-sending'
  10. Check
    control-openhands browser count 'testid=chat-message-retry'

    are 0, and

  11. Check
    control-openhands conversation events <id> --kinds MessageEvent
  12. Note
    gains one user row Reply with only: clock-ok whose ts is the server's time (close to date -u), not the browser's.
  13. Note
    Undo the skew before moving on:
  14. Do
    control-openhands browser clock --offset-ms 0
  15. Do
    control-openhands browser reload

    (with a model, control-openhands conversation wait <id> --fresh --timeout 120 first).

Server lost: banner, failed send, reconnect #

  1. Note
    With a conversation open run
  2. Do
    control-openhands service stop agent-server
  3. Wait
    control-openhands browser wait 'testid=error-message-banner' --timeout 20000
  4. Note
    : the banner reads Unable to connect to server with error-message-banner-retry, -copy and -dismiss; the composer shows Disconnected.
  5. Do
    control-openhands browser fill 'testid=chat-input' 'Reply with only the word PONG.'
  6. Do
    control-openhands browser click 'testid=submit-button' --observe '[data-testid=chat-message-sending],[data-testid=chat-message-error]' --observe-ms 8000
  7. Note
    : Sending... turns into Failed to send Retry Dismiss.
  8. Do
    control-openhands browser click 'testid=chat-message-retry'
  9. Note
    while down fails again;
  10. Do
    control-openhands browser click 'testid=chat-message-dismiss'
  11. Note
    removes the bubble (count 'testid=chat-message-error' is 0).
  12. Note
    Send the same message again so one failed bubble remains,
  13. Do
    control-openhands browser click 'testid=error-message-banner-dismiss'

    (banner count 0), then

  14. Do
    control-openhands restart
  15. Note
    Poll
  16. Check
    control-openhands browser text 'testid=interactive-chat-box'
  17. Note
    every 10 s: within about a minute after restart returns it no longer contains Disconnected (reconnected without reload).
  18. Do
    control-openhands browser click 'testid=chat-message-retry'
  19. Wait
    control-openhands conversation wait <id> --timeout 120
  20. Check
    control-openhands conversation events <id> --kinds MessageEvent
  21. Note
    : Reply with only the word PONG. appears once and the last agent-message is PONG.

Banner Retry #

  1. Do
    control-openhands service stop agent-server
  2. Note
    wait for testid=error-message-banner as above,
  3. Do
    control-openhands restart
  4. Do
    control-openhands browser click 'testid=error-message-banner-retry'
  5. Check
    control-openhands browser count 'testid=error-message-banner'

    is 0 and the composer is no longer Disconnected.

  6. Note
    Take browser screenshot 'testid=chat-interface' --feature F06.error-banner --name disconnected while the banner is up.

Failed send survives leaving #

  1. Note
    With /conversations/<id> open and healthy, run
  2. Do
    control-openhands service stop agent-server
  3. Wait
    control-openhands browser wait 'testid=error-message-banner' --timeout 20000
  4. Do
    control-openhands browser fill 'testid=chat-input' 'Reply with only: retry-ok'
  5. Do
    control-openhands browser click 'testid=submit-button' --observe '[data-testid=chat-message-sending],[data-testid=chat-message-error]' --observe-ms 8000
  6. Note
    : Sending... turns into Failed to send Retry Dismiss and
  7. Check
    control-openhands browser count 'testid=chat-message-retry'

    is 1 (control-openhands browser screenshot 'testid=chat-interface' --feature F06.failed-send-persists --name failed-before-leaving).

  8. Do
    control-openhands restart
  9. Wait
    control-openhands browser wait 'testid=error-message-banner' --state hidden --timeout 90000

    (the page reconnects by itself; testid=error-message-banner-retry hurries it while the banner is still up): the Retry row is still there (count 1).

  10. Note
    Leave through the sidebar and come back:
  11. Do
    control-openhands browser click 'role=link[name="New Chat"]'
  12. Wait
    control-openhands browser wait 'testid=home-chat-launcher'
  13. Do
    control-openhands browser click 'a[href*="/conversations/<id>"] >> nth=0' --expect-url '/conversations/<id>'
  14. Wait
    control-openhands browser wait 'testid=user-message'
  15. Check
    control-openhands browser count 'testid=chat-message-retry'

    is still 1 after the history reloaded (control-openhands browser screenshot 'testid=chat-interface' --feature F06.failed-send-persists --name retry-after-reopen).

  16. Note
    Send the same text again (fill Reply with only: retry-ok, click testid=submit-button with the same --observe): the new bubble goes Sending... and settles while Failed to send Retry Dismiss stays;
  17. Check
    control-openhands browser count 'testid=chat-message-sending'

    is 0,

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

    is 1, and the rows of

  19. Check
    control-openhands conversation events <id> --kinds MessageEvent --grep 'retry-ok'
  20. Note
    whose source is user number exactly one, Reply with only: retry-ok (in a model-free run count is 1; with a model the agent's retry-ok reply matches the grep too, so count the user rows, not count).
  21. Do
    control-openhands browser click 'testid=chat-message-retry'
  22. Wait
    control-openhands browser wait 'testid=chat-message-retry' --state detached --timeout 15000
  23. Check
    control-openhands browser count 'testid=chat-message-sending'

    is 0 and the same --grep now lists exactly two user rows Reply with only: retry-ok (one per attempt, none duplicated).

  24. Note
    With a model,
  25. Wait
    control-openhands conversation wait <id> --fresh --timeout 120
  26. Note
    before the next bullet.

Live chip, stop and resume #

  1. Do
    control-openhands conversation start --prompt "Run exactly one terminal command: sleep 25 && echo qa-slept. Then reply with only its output."

    (no --wait),

  2. Wait
    control-openhands browser wait 'testid=live-activity-chip' --timeout 30000
  3. Check
    control-openhands browser text 'testid=live-activity-chip'
  4. Note
    : first Thinking, then the action title (e.g. Run sleep 25 then echo qa-slept); browser text 'testid=interactive-chat-box' contains Running.
  5. Do
    control-openhands browser click 'testid=stop-button'
  6. Check
    control-openhands conversation status <id>

    is paused, the composer reads Stopped, count 'testid=play-button' is 1 and the chip is gone.

  7. Note
    Stop interrupts the running sleep too:
  8. Check
    control-openhands conversation events <id>

    shows an AgentErrorEvent (Tool call interrupted before completion. The conversation was paused.) followed by an InterruptEvent, and browser text 'testid=chat-scroll-container' shows an Agent error row under the action title.

  9. Do
    control-openhands browser click 'testid=play-button'
  10. Note
    : status running, then
  11. Wait
    control-openhands conversation wait <id> --timeout 120
  12. Note
    ends finished with a new agent MessageEvent (the model reports that the command was interrupted; qa-slept never prints).

Reload mid-run #

  1. Do
    control-openhands conversation start --prompt "Run exactly one terminal command: sleep 15 && echo qa-reloaded. Then reply with only its output."
  2. Note
    wait for testid=live-activity-chip, then
  3. Do
    control-openhands browser reload
  4. Expect
    The composer still reads Running and the chip is back.
  5. Note
    After
  6. Wait
    control-openhands conversation wait <id> --timeout 120
  7. Note
    : testid=agent-message is qa-reloaded, count 'testid=user-message' is 1,
  8. Do
    control-openhands browser eval "document.querySelectorAll('[data-testid=chat-scroll-container] [data-testid=generic-event-message-title]').length"

    is 1.

Older history #

  1. Wait
    control-openhands conversation start --prompt "Run these 16 terminal commands strictly one per tool call, sequentially (never combine them): echo qa1, echo qa2, echo qa3, echo qa4, echo qa5, echo qa6, echo qa7, echo qa8, echo qa9, echo qa10, echo qa11, echo qa12, echo qa13, echo qa14, echo qa15, echo qa16. Then reply DONE." --wait --timeout 400

    (note <history-id>);

  2. Check
    control-openhands conversation events <history-id> --last 500
  3. Note
    reports a count above 50.
  4. Check
    control-openhands browser network --clear
  5. Do
    control-openhands browser reload
  6. Check
    control-openhands browser network --last 300
  7. Note
    : two GET /api/conversations/<history-id>/events/search requests (newest page, then the older page).
  8. Check
    control-openhands browser text 'testid=chat-scroll-container'
  9. Note
    starts with the prompt and shows 16 actions completed and DONE.
  10. Expect
    The other trigger is the user scrolling to the top, driven without a model on the dummy profile from Preconditions (qa-zeta active; in a keyed run create and activate it with that same llm set command for this bullet, then re-run control-openhands llm preset deepseek and remove it with control-openhands api DELETE /api/profiles/qa-zeta --write, so later families still count two profiles; arrange, not proof).
  11. Do
    control-openhands conversation start --prompt "qa older 1"

    (no --wait; note <older-id>), then for N in $(seq 2 30); do control-openhands conversation send <older-id> --prompt "qa older $N"; done: every send is accepted while the agent sits in error, about 1.5 s apiece (42 s for the 29 here), and each refused message adds five events (a MessageEvent, three ConversationStateUpdateEvents, a ConversationErrorEvent), so the conversation ends with about 150 events (152 here).

  12. Check
    control-openhands conversation events <older-id> --last 500
  13. Note
    reports "total": 152, "pages": 2, "more": false (both server pages read, see Gotchas); its first row is the SystemPromptEvent and the first MessageEvent is qa older 1, while --last 25 reports "pages": 1, "more": true and starts at qa older 26.
  14. Check
    control-openhands browser network --clear
  15. Do
    control-openhands browser reload
  16. Wait
    control-openhands browser wait 'testid=chat-input'
  17. Check
    control-openhands browser network --filter 'events/search' --last 10
  18. Note
    : two requests, ?limit=50&sort_order=TIMESTAMP_DESC and one with timestamp__lt=<ts of qa older 21>, because the newest 50 events hold only ten bubbles, shorter than the viewport, so the automatic fetch ran once and then stopped.
  19. Check
    control-openhands browser count 'testid=user-message'

    is 20,

  20. Check
    control-openhands browser text 'testid=chat-scroll-container'
  21. Note
    starts qa older 11 and holds none of qa older 1…qa older 10, and
  22. Check
    control-openhands browser bbox 'testid=chat-scroll-container'

    has scrollHeight 1576 against clientHeight 814: taller than the viewport, so the third page waits for the user.

  23. Do
    control-openhands browser scroll 'testid=chat-scroll-container' --by -100000
  24. Check
    control-openhands browser network --filter 'events/search' --last 5
  25. Note
    gains a request with timestamp__lt=<ts of qa older 11>,
  26. Check
    control-openhands browser count 'testid=user-message'

    is 29 and the text starts qa older 2 (that page held messages 2–10 plus the tail of the first message's events; not seen in two runs: if the first refusal ever logs six events instead of seven, the arithmetic gives count 30, text qa older 1 and a last page of only the SystemPromptEvent), and the view stays on qa older 11:

  27. Do
    control-openhands browser eval "document.querySelector('[data-testid=chat-scroll-container]').scrollTop"

    is 684, the height of the prepended rows.

  28. Note
    Scroll to the top again the same way: a fourth request (timestamp__lt= the oldest loaded event), the count is 30, the text starts qa older 1 and scrollTop is 76 (control-openhands browser screenshot 'testid=chat-interface' --feature F06.load-older-history --name scroll-top-loaded-qa-older-1).
  29. Expect
    A third scroll adds no request: the short page ended the paging.
  30. Check
    control-openhands browser count 'testid=loading-older-events'

    is 0 after each scroll because the Fetching older messages… row lasts a few tens of milliseconds on a local stack; to see it, scroll with

  31. Check
    control-openhands browser scroll 'testid=chat-scroll-container' --by -100000 --observe 'testid=loading-older-events' --observe-ms 1500
  32. Note
    instead (both scrolls were driven this way here, with the same counts), whose observed goes <absent> at 0 ms, Fetching older messages… at about 20 ms and <absent> again within about 40 to 120 ms (116 ms on the first scroll, 39 on the second; observedBy mutation); the timestamp__lt= network rows remain the durable proof.
  33. Note
    Return to the 16-command conversation for the next bullet:
  34. Do
    control-openhands browser goto /conversations/<history-id>
  35. Wait
    control-openhands browser wait 'testid=chat-input'

Scroll to bottom #

  1. Note
    On the 16-command conversation (<history-id>; in a model-free run this bullet is blocked with the automatic-fetch half, prerequisite DEEPSEEK_API_KEY),
  2. Do
    control-openhands browser click 'testid=event-group-toggle'
  3. Note
    makes the transcript taller than the viewport while the view stays where it was.
  4. Do
    control-openhands browser scroll 'testid=chat-scroll-container' --by 2000
  5. Note
    first (the button only reacts to scroll events, so a page that is already at the top never shows it): count 'testid=scroll-to-bottom' is 0.
  6. Do
    control-openhands browser scroll 'testid=chat-scroll-container' --by -2000
  7. Note
    the count is 1.
  8. Do
    control-openhands browser click 'testid=scroll-to-bottom'
  9. Note
    then the count is 0 and
  10. Do
    control-openhands browser eval "(()=>{const e=document.querySelector('[data-testid=chat-scroll-container]');return Math.round(e.scrollHeight-e.scrollTop-e.clientHeight)})()"

    is 0.

Confirmation mode on #

  1. Note
    Arrange through the settings UI, in this order (the Security Analyzer field appears only once Confirmation Mode is on):
  2. Do
    control-openhands browser goto /settings/verification
  3. Do
    control-openhands browser click 'testid=verification-settings-screen >> text=Confirmation Mode'
  4. Do
    control-openhands browser click 'testid=sdk-section-all-toggle'
  5. Do
    control-openhands browser click 'testid=verification-settings-screen >> role=button[name="Show suggestions"]'
  6. Do
    control-openhands browser click 'role=listbox >> role=option[name="None"]'
  7. Check
    control-openhands browser value 'testid=verification-settings-screen >> role=combobox[name="Security Analyzer"]'

    (None),

  8. Do
    control-openhands browser click 'testid=verification-settings-screen >> testid=save-button'
  9. Check
    control-openhands api GET /api/settings

    shows "confirmation_mode": true and "security_analyzer": "none" (None means every action is confirmed; the default LLM analyzer only stops HIGH-risk actions, so echo would run unprompted).

  10. Wait
    control-openhands conversation start --prompt "Run the terminal command 'echo qa-confirm-ok' and reply with its output." --wait --timeout 180
  11. Note
    : it returns "status": "waiting_for_confirmation".
  12. Check
    control-openhands browser text 'testid=chat-scroll-container'
  13. Note
    ends Do you want to continue with this action? Cancel ⇧⌘⌫ Continue ⌘↩,
  14. Do
    control-openhands browser tooltip 'testid=action-confirm-button'

    is Confirm the requested action, and a lock icon sits above the composer (screenshot).

  15. Do
    control-openhands browser click 'testid=action-reject-button'
  16. Note
    : status idle, the buttons are gone and conversation events <id> --last 6 ends with UserRejectObservation.
  17. Note
    Send Run the terminal command 'echo qa-confirm-two' and reply with its output. through testid=chat-input / testid=submit-button,
  18. Wait
    control-openhands browser wait 'testid=action-confirm-button' --timeout 20000
  19. Do
    control-openhands browser click 'testid=action-confirm-button'
  20. Note
    about 15 s later the status is finished and the events contain the observation qa-confirm-two.
  21. Note
    Expected after a Cancel: the rejected row is shown as resolved and the group stops spinning (currently fails, see Gotchas).

Confirmation shortcuts #

  1. Expect
    The previous bullet resolved its action, so arrange a new one in the same conversation: send Run the terminal command 'echo qa-confirm-three' and reply with its output. through testid=chat-input / testid=submit-button and
  2. Wait
    control-openhands browser wait 'testid=action-confirm-button' --timeout 20000
  3. Note
    While it awaits confirmation,
  4. Do
    control-openhands browser press Control+Enter
  5. Note
    does nothing (control-openhands conversation status <id> stays waiting_for_confirmation); see Gotchas.
  6. Do
    control-openhands browser press Meta+Enter
  7. Note
    continues it:
  8. Wait
    control-openhands conversation wait <id> --fresh --timeout 120
  9. Note
    ends finished and
  10. Check
    control-openhands conversation events <id> --kinds ObservationEvent
  11. Note
    contains qa-confirm-three.
  12. Note
    Restore afterwards: browser goto /settings/verification, click testid=sdk-section-all-toggle, Show suggestions, role=listbox >> role=option[name="LLM"], then testid=verification-settings-screen >> text=Confirmation Mode, then save-button; api GET /api/settings shows "confirmation_mode": false, "security_analyzer": "llm".

Branch from here #

  1. Note
    Open a conversation with at least two exchanges (the OK/PONG one above):
  2. Do
    control-openhands browser goto /conversations/<id>
  3. Note
    with its id (the previous bullet left /settings/verification).
  4. Note
    Agent message:
  5. Do
    control-openhands browser hover 'testid=agent-message >> nth=0'
  6. Do
    control-openhands browser click 'testid=agent-message >> nth=0 >> role=button[name="Branch from here"]' --expect-url '/conversations/(?!<id>)'
  7. Note
    after
  8. Wait
    control-openhands browser wait 'testid=user-message'

    (the history mounts a moment after the URL changes, so an immediate count reads 0) the new conversation has count 'testid=user-message' 1, the agent message OK, and browser eval "document.title" contains (branch).

  9. Note
    User message (edit): back on the OK/PONG conversation (control-openhands browser goto /conversations/<id>; the branch above has one user message),
  10. Do
    control-openhands browser hover 'testid=user-message >> nth=1'
  11. Do
    control-openhands browser click 'testid=user-message >> nth=1 >> role=button[name="Branch from here"]' --observe '[data-testid=chat-input]' --observe-ms 4000
  12. Note
    : observed shows the composer change from empty to Reply with only the word PONG. and stay.
  13. Expect
    The new conversation (<branch-id> from browser url) omits that message,
  14. Check
    control-openhands browser text 'testid=chat-input'

    is Reply with only the word PONG., and nothing was sent:

  15. Check
    control-openhands conversation events <branch-id> --kinds MessageEvent

    lists only the copied first exchange (count 2: the long message that starts Reply with only the word OK., then OK; no PONG).

  16. Check
    control-openhands browser enabled 'testid=submit-button'
  17. Note
    should be true with the prefilled text; today it is false until the text is edited or the page is reloaded (see Gotchas).
  18. Note
    Send it with
  19. Do
    control-openhands browser press Enter --selector 'testid=chat-input'

    (Enter sends whatever the Send button's state is) and

  20. Wait
    control-openhands conversation wait <branch-id> --fresh --timeout 120
  21. Note
    : expected finished with a reply; today it ends error (Agent Server bug, see Gotchas).

Images #

  1. Arrange
    control-openhands fixture image --name qa-f06-img

    (prints the PNG path),

  2. Do
    control-openhands browser upload 'testid=upload-image-input' <png path>

    (a thumbnail appears in the composer), fill testid=chat-input with Reply with only the word SEEN. and click testid=submit-button.

  3. Expect
    The user bubble contains image-carousel / image-preview.
  4. Do
    control-openhands browser hover 'testid=image-preview'
  5. Do
    control-openhands browser click 'testid=expand-image-button'

    (count 'testid=image-lightbox' is 1),

  6. Do
    control-openhands browser press Escape

    (0), click expand-image-button again and

  7. Do
    control-openhands browser click 'testid=image-lightbox-close'

    (0).

Error events #

  1. Note
    Any run that ends in a ConversationErrorEvent (today: any message sent in a branched conversation) leaves conversation wait <id> at "status": "error";
  2. Check
    control-openhands browser testids 'testid=chat-interface'

    lists error-message-banner with warning-message-banner-icon and the error's text, and the composer status reads Error.

  3. Note
    Read the code with
  4. Check
    control-openhands api GET "/api/conversations/<id>/events/search?limit=3&sort_order=TIMESTAMP_DESC"

Skill installed banner #

  1. Expect
    The banner keys on the installer's success line in terminal output.
  2. Note
    Put this one line in a prompt file: Run exactly this one terminal command, verbatim, then reply DONE: mkdir -p .agents/skills/qa-skill && printf '# qa-skill\n' > .agents/skills/qa-skill/SKILL.md && echo "✅ Successfully installed 'qa-skill' to $PWD/.agents/skills/qa-skill", then run
  3. Wait
    control-openhands conversation start --prompt "$(cat <prompt file>)" --wait --timeout 150
  4. Check
    control-openhands browser text 'testid=skill-install-restart-banner'

    reads Installed to this workspace: qa-skill. Skills load when a conversation starts, so this conversation can't use them yet. Run

  5. Check
    control-openhands browser click 'testid=skill-install-restart-action' --expect-url '/conversations/(?!<id>)'
  6. Check
    control-openhands api GET /api/conversations/<new id>

    has the same workspace.working_dir as the source.

  7. Note
    Back on the source (control-openhands browser goto /conversations/<id>),
  8. Do
    control-openhands browser click 'testid=skill-install-restart-dismiss'
  9. Note
    hides it (count 0); after browser reload it is back (dismissal is session-only by design).

No LLM #

  1. Note
    On the fresh no-LLM run:
  2. Do
    control-openhands onboard --skip
  3. Do
    control-openhands browser click 'testid=conversation-panel-new-thread-picker'
  4. Check
    control-openhands browser click 'testid=launch-no-workspace' --expect-url '/conversations/'
  5. Check
    control-openhands browser text 'testid=home-llm-not-configured-banner'

    reads Your LLM isn't set up yet, so conversations won't run. Finish setup to get started. Set up LLM; count 'testid=chat-suggestions' is 0; the composer cannot take text (control-openhands browser attr 'testid=chat-input' contenteditable is false, so browser fill fails) and

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

    is false.

  7. Do
    control-openhands browser click 'testid=home-llm-not-configured-action'
  8. Note
    then browser url ends in /settings/llm.

Critic (F06.critic-result, blocked) #

  1. Note
    Toggle the hidden switch through its label on /settings/verification:
  2. Do
    control-openhands browser goto /settings/verification
  3. Do
    control-openhands browser click 'label:has([data-testid="sdk-settings-verification.critic_enabled"])'
  4. Note
    save, api GET /api/settings shows "critic_enabled": true.
  5. Note
    Without a Critic API key a Reply with only the word OK. conversation renders no critic block.
  6. Note
    With a key and a model, run
  7. Wait
    control-openhands conversation start --prompt "Create qa_critic.txt containing hi, then finish." --wait --timeout 180
  8. Note
    : every scored event carries a block, so
  9. Check
    control-openhands browser count 'text=Critic: agent success likelihood'

    is at least 1 and a finishing action (finish tool row) shows its own block next to the agent's message; the stars carry the score as their label, which an ARIA snapshot does not list:

  10. Check
    control-openhands browser count '[aria-label^="Score: "]'

    is at least 2 and

  11. Check
    control-openhands browser attr '[aria-label^="Score: "] >> nth=0' aria-label

    reads Score: <N.N>% with one decimal (Score: 82.0%), the same number as the (82.0%) text beside the stars.

  12. Do
    control-openhands browser click 'role=button[name="Expand details"] >> nth=0'
  13. Note
    opens the categories Potential Issues:, Infrastructure: and Likely Follow-up: with their named issues in
  14. Check
    control-openhands browser text 'testid=chat-scroll-container'
  15. Note
    and the button's label flips to Collapse details.
  16. Note
    Blocked here: DEEPSEEK_API_KEY and a Cloud critic key.
  17. Note
    Turn it off again the same way.

Phone #

  1. Do
    control-openhands browser viewport phone
  2. Note
    open the table conversation (control-openhands browser goto /conversations/<stream id>),
  3. Check
    control-openhands browser bbox 'testid=chat-interface'

    (insideViewport true, pageHorizontalOverflow false) and

  4. Check
    control-openhands browser bbox 'testid=markdown-table-scroll'

    (scrollWidth larger than clientWidth: the table scrolls inside itself).

  5. Do
    control-openhands browser screenshot --feature F06.phone --name conversation
  6. Do
    control-openhands browser viewport desktop

After the family #

  1. Expect
    After each group run
  2. Check
    control-openhands browser errors --app-only
  3. Note
    Outages driven with service stop log connection-refused console errors by design; pageErrors must stay 0.

Gotchas and known limits

  • control-openhands conversation events <id> returns the last 25 events unless you pass --last N; it reads the server's 100-event pages until N rows are in hand (pages says how many it read, more: true that older pages remain), so --last 500 covers a 150-event conversation in two pages (total 152, pages 2, more false on the Older history conversation; --last 25 there reads one page, more true) and --from-start reads from the oldest. --kinds and --grep filter before --last counts, and pages are read until --last matching rows are in hand: --kinds MessageEvent --last 3 is the three newest messages, --kinds MessageEvent --last 500 all thirty, and --grep 'qa older 1' reaches qa older 1 on the oldest page with the default --last (count 11, pages 2). Event texts are cut at about 160 characters.
  • --observe takes the same selector syntax as the verbs: testid= and plain CSS segments (joined with >> ) are watched by an in-page MutationObserver (observedBy mutation; the Older history bullet's --observe 'testid=loading-older-events' is one), while other engines (role=, text=, nth=) are polled every 20 ms instead (not driven in this family).
  • role=button[name="Expand"] also matches "Expand thinking" and "Expand actions"; use [exact] to target tool rows. Each click flips the row to Collapse, so nth=0 walks to the next collapsed row.
  • Tool rows show no success icon; the only indicators are the exit N badge for non-zero exits and a clock icon (status-icon) for timeouts. A single action is not grouped: it renders as a bare row without event-group.
  • The model writes the tool titles and may append to commands (it turned ls /missing into ls ...; echo "EXIT:$?", exit 0). Ask for the command "verbatim, with nothing appended" when the exit code matters.
  • The agent itself may open files with canvas_ui_control (a third action in the echo/file run). Close the file tab before proving that a path link opens it.
  • On Settings → Verification the Security Analyzer field (All tab) is shown only while Confirmation Mode is on: switch Confirmation Mode on first, then pick the analyzer. Check api GET /api/settings after saving.
  • Confirmation mode with the default Security Analyzer LLM uses ConfirmRisky(HIGH): harmless commands run without a prompt. Set the analyzer to None to confirm every action, and restore LLM afterwards. The composer stays editable while waiting (status "User needed"); the buttons read Cancel / Continue.
  • Known failure, Canvas UI: after Cancel the event group keeps a spinner and "1/2 actions completed" forever, even after reload, although the conversation is idle and the server recorded UserRejectObservation (the group only counts ObservationEvents) (#17920). Evidence F06.confirmation-mode/rejected-after-reload.png.
  • Known failure, Canvas UI: the confirm/cancel shortcuts check only metaKey; on Linux/Windows Control+Enter does nothing although the composer and plan shortcuts accept Ctrl (#17928).
  • Known failure, Canvas UI (repro candidate, not filed yet): after a user-message branch the composer holds the prefilled text but Send stays disabled (browser enabled 'testid=submit-button' is false, and a click on it times out with element is not enabled). The prefill is a saved draft that src/hooks/chat/use-draft-persistence.ts writes into the field without an input event, and the composer is reused across the client-side switch, so its Send state keeps the source conversation's empty field. Editing the text, a reload, or Enter in the field (browser press Enter --selector 'testid=chat-input') all work. A draft restored by a sidebar switch behaves the same (F05 Gotchas).
  • Known failure, likely Agent Server: every message in a branched conversation ends with ConversationErrorEvent LLMAuthenticationError ("Your LLM API key appears to be invalid or has expired."), while the source conversation keeps working (OpenHands/software-agent-sdk#5465). Do not use branches as fixtures for other checks.
  • "The terminal session was reset because the underlying tmux server/session disappeared" (clock status-icon on the row) appears after a service stop agent-server / restart cycle. Since launch gives each run its own short TMUX_TMPDIR, other runs no longer cause it. The underlying product bug is still open (OpenHands/OpenHands#17946) for plain agent-canvas instances: bin/agent-canvas.mjs → scripts/dev-with-automation.mjs passes TMUX_TMPDIR=<state>/tmux to the Agent Server but ensureDirectories never creates it (dev-safe.mjs does), so tmux 3.4 falls back to the shared /tmp/tmux-<uid>/openhands socket and another instance's restart or stop kills this run's windows. The model usually reruns the command; expect an extra action.
  • The ConversationErrorEvent banner is not restored after navigating away and back (or a reload); read the error from the events API instead.
  • After restart, reconnection takes about 20 s without a click; click error-message-banner-retry to reconnect at once. The error banner does not come back after a page reload once the stack is healthy.
  • "Sending..." lasts about 100 to 650 ms on a local stack, too short to hover and click chat-message-stop; that stop path is not driven here. The F06.pending-messages failure path needs service stop agent-server.
  • browser clock installs a fake clock that stays with the page across reloads and client-side navigation. Install it before the goto whose page should see it, and put it back with browser clock --offset-ms 0 plus browser reload; compare browser eval "new Date().toISOString()" with date -u when in doubt.
  • The pending-message queue lives in page memory, so a failed bubble survives sidebar navigation and a backend restart but not a browser reload or browser goto of the conversation (src/stores/optimistic-user-message-store.ts). Leave and return through the sidebar when proving F06.failed-send-persists. The sidebar card's title is the first message or model-written: click its link by id, 'a[href*="/conversations/<id>"] >> nth=0'.
  • Both the pending bubble and the echoed message carry testid=user-message, so count 'testid=user-message >> has-text=...' is one too high while a failed bubble is still shown; count attempts with conversation events <id> --kinds MessageEvent (user rows: "source": "user") and the bubbles with chat-message-retry.
  • Local Stop calls the Agent Server's interrupt (Cloud pauses the sandbox instead, see pauseConversation): a command already running in the terminal is cut off, recorded as AgentErrorEvent + InterruptEvent, and after Play the model only reports the interruption.
  • The no-LLM banner reuses the home banner's test ids (home-llm-not-configured-banner). It exists only in a run where no LLM was ever configured: use a separate launch --new, never remove the profile from the shared run.
  • Read the clipboard with control-openhands browser clipboard, never browser eval "navigator.clipboard.readText()". An agent bubble that contains a code block holds two copy-to-clipboard buttons (the code block's is hidden until hovered): scope the bubble's with >> nth=0.
  • Plan-mode previews (plan-preview-*) and the BTW side messages render in this stream but are reached from the composer's mode controls; they are not mapped here.

Source paths: src/components/features/chat/chat-interface.tsx, src/components/conversation-events/chat/, src/components/features/chat/ (messages, banners, tool-visualizers/, task-tracking/), src/components/features/markdown/, src/components/features/images/, src/components/features/suggestions/, src/components/shared/buttons/conversation-confirmation-buttons.tsx, src/components/features/controls/agent-status.tsx, src/hooks/use-load-older-events.ts, src/hooks/mutation/use-fork-conversation.ts.