EN / field notes OpenHands feature map

OpenHands / F20

Canvas apps

Canvas apps (Canvas Extensions) are trusted browser modules installed on the active local Agent Server. Under Customize → Apps a user installs an app from a local path or a Git source (it arrives disabled), reviews its card, enables it through a trust confirmation, updates, disables or uninstalls it, and opens the pages it contributes from the main sidebar rail at /extensions/<name>/<path>.

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

How to get to it

  • Main sidebar Customize (sidebar-skills-link, lands on /mcp), then Apps in the Customize navigation (sidebar-extensions-/apps).
  • Command menu (Control+k/Meta+k or command-menu-trigger): search Customize, Enter (lands on /mcp), then Apps. Searching Apps finds nothing.
  • Phone: /customize shows the Customize hub (extensions-mobile-hub) instead of redirecting; tap Apps.
  • Direct URL /apps.
  • App pages are not in the command menu; only the rail (or a URL) reaches them.
  • App pages: the app's entry in the main sidebar rail (phone: the sidebar drawer, sidebar-mobile-menu-toggle), or the direct URL /extensions/<name>/<path>[/<sub-path>].

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) on the launcher's local Agent Server. No LLM is needed.
  • No app is installed: control-openhands api GET /api/canvas-extensions/installed returns "canvas_extensions": [].
  • Run commands from the checkout root: recipes install the bundled, reviewed fixture $PWD/src/fixtures/canvas-extensions/demo-page (name demo-page, display name Demo page, one page hello at /hello). Never install or enable arbitrary third-party code: enabling runs it inside Canvas.
  • Git bullets (tree URL, Ref and Path, busy lock, duplicate): the Agent Server can read https://github.com/OpenHands/OpenHands anonymously (read-only; no account needed), and that repo's main holds the same reviewed fixture at src/fixtures/canvas-extensions/demo-page. Check both before installing, with a read-only shell step: git clone -q --depth 1 --filter=blob:none --sparse https://github.com/OpenHands/OpenHands "$OH_VERIFY_RUN/private/qa-upstream" && git -C "$OH_VERIFY_RUN/private/qa-upstream" sparse-checkout set src/fixtures/canvas-extensions && diff -r "$OH_VERIFY_RUN/private/qa-upstream/src/fixtures/canvas-extensions/demo-page" src/fixtures/canvas-extensions/demo-page && git -C "$OH_VERIFY_RUN/private/qa-upstream" rev-parse HEAD must print no diff and a commit. Enable a Git-installed copy only when the card's Ref equals that commit. If GitHub is unreachable or the diff is not empty, these bullets are blocked (a local smart-HTTP Git server would do, but control-openhands has no verb for one; dumb HTTP such as python3 -m http.server fails the Agent Server's fetch).
  • Desktop viewport unless a bullet says otherwise.

Behavior inventory

24 stable behavior IDs and their expected behavior
  • F20.entry-points the Apps page is reached from the sidebar Customize link (lands on /mcp) then Apps in the Customize navigation, from the command menu's Customize command plus Apps, from the phone Customize hub, and by direct URL /apps; there is no bare /extensions page and no "Apps" command. Read recipe ↓
  • F20.page /apps shows the title Apps for Agent Canvas, a description, a Build an app docs link (new tab), an enabled Add app button, the amber trust notice, the Installed apps heading and, with nothing installed, No apps are installed on this backend. Read recipe ↓
  • F20.add-modal Add app opens a form with App source (required), Ref and Path; Install stays disabled while the source is empty or blank; Close, the X, Escape and a backdrop click close it. Read recipe ↓
  • F20.install installing from a local path shows Installing…, toasts App installed. Review it here, then enable it when you are ready., closes the form and lists the app disabled (after reload too) with no rail entry; a local App source plus Path is joined into one path. Read recipe ↓
  • F20.install-error a failed install toasts the server's reason (Could not read canvas extension source: …) and keeps the form open with the input, for local paths and for unreachable Git sources alike. Read recipe ↓
  • F20.git-tree-url a Git host folder URL (…/<owner>/<repo>/tree/<ref>/<path>) pasted into App source is split into source, ref and path; the card shows the repo URL as source, Ref (resolved commit) and Path. Read recipe ↓
  • F20.git-ref-path the Ref and Path fields apply to a Git source, including the placeholder's github:owner/repo shorthand; the card keeps the source as typed and shows the resolved commit and the path. Enter in a field submits the form. Read recipe ↓
  • F20.install-duplicate installing an app whose name is already installed is refused (409): the form stays open with its input, the toast says the app is already installed and points to Update or Uninstall on its card, and the existing card is unchanged. Read recipe ↓
  • F20.card each card shows the display name, source, a role=switch (Enable/Disable), the description, Disabled/Enabled, v<version> and Pages: N pills, each page title, and Update and Uninstall buttons. Read recipe ↓
  • F20.enable-confirm switching a disabled app on (click or Space) opens a trust confirmation (confirmation-modal) with Cancel and Enable trusted app; Cancel, Escape and a backdrop click leave it disabled, confirming enables it (persists after reload). Read recipe ↓
  • F20.rail-entry an enabled app's page appears in the main sidebar rail as sidebar-canvas-extension-<name>-<pageId> linking to /extensions/<name>/<path>. Read recipe ↓
  • F20.rail-active on the app's page its rail entry is marked current (aria-current="page", highlighted); with the sidebar collapsed the entry is an icon whose tooltip and aria-label carry the label. App pages are not listed in the command menu. Read recipe ↓
  • F20.rail-label the rail entry is labelled with the page's nav_label (Extension demo for the demo fixture), falling back to its title. Read recipe ↓
  • F20.page-render the rail entry and the direct URL mount the app into main named after the page title (demo: h1 Hello from a Canvas Extension); a deeper URL passes the rest of the path to the app (Nested extension path: nested). Read recipe ↓
  • F20.app-backend-view an app page learns from the host whether it has a backend view; the host offers one only when the Agent Server advertises the canvas_app_backend_bridge_v1 capability with an app-backend ingress URL, so on the pinned server the demo's status line reads App backend view unavailable on every path of its page. Read recipe ↓
  • F20.page-unavailable an unknown app, an unknown page of a known app, or a disabled or uninstalled app shows Not available / This app is disabled, missing, or does not provide this page. Read recipe ↓
  • F20.phone at 390 px the Customize hub lists Apps, the Apps page, the Add form and an app page fit without horizontal overflow, and the app's entry is in the sidebar drawer. Read recipe ↓
  • F20.refresh Update re-installs from the recorded source, toasts App updated. and keeps the enabled state; a failed update toasts the error and leaves the card unchanged. Read recipe ↓
  • F20.persist-restart installed apps, their enabled state and rail entries survive a stack restart. Read recipe ↓
  • F20.disable switching an enabled app off happens immediately without confirmation, removes its rail entry and makes its page unavailable. Read recipe ↓
  • F20.uninstall Uninstall asks Uninstall <display name> from this backend?; Escape or Cancel keep the app, Confirm toasts App uninstalled. and removes the card and the rail entry. Read recipe ↓
  • F20.busy-lock while Update, Uninstall or Enable is pending, every card control is inert, the switch included. Read recipe ↓
  • F20.load-error when listing apps fails, the Installed section shows the error and a Retry button. Read recipe ↓
  • F20.unsupported on a Cloud backend, with no backend, or on an Agent Server without the apps API, the Installed section shows Not available with the reason and Add app is disabled; Cloud hides Apps from the Customize navigation. 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.

Sidebar entry and empty page #

  1. Note
    From / run
  2. Check
    control-openhands browser click 'testid=sidebar-skills-link' --expect-url '/mcp(\?|$)'
  3. Check
    control-openhands browser click 'testid=sidebar-extensions-/apps' --expect-url '/apps(\?|$)'
  4. Check
    control-openhands browser snapshot 'testid=canvas-extensions-screen >> main'
  5. Expect
    The snapshot shows heading Apps for Agent Canvas, the description Apps are built with the Canvas Extensions API, link Build an app, button Add app, the trust notice, heading Installed apps and paragraph No apps are installed on this backend.
  6. Check
    control-openhands browser enabled 'testid=canvas-extensions-add-button'

    is true and

  7. Check
    control-openhands browser attr 'role=link[name="Build an app"]' target

    is _blank.

  8. Do
    control-openhands browser screenshot --feature F20.page --name empty
Canvas Apps page with trust notice and no installed apps.
Apps begin with a trust notice and an empty Installed apps section. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Apps page is reachable from Customize and shows no installed apps.

Local backend only; no third-party app is installed.

How this screenshot was taken

agent server: 1.53.0 · automation: 1.19.0 (launcher default) · canvas: 1.26.0

control-openhands browser click testid=sidebar-extensions-/apps --expect-url '/apps(\?|$)'
control-openhands browser snapshot 'testid=canvas-extensions-screen >> main'
control-openhands browser screenshot --feature F20.entry-points --name empty-apps

Command-menu entry #

  1. Note
    From / run
  2. Do
    control-openhands browser press Control+k
  3. Do
    control-openhands browser type 'testid=command-menu >> role=combobox' Apps
  4. Check
    control-openhands browser snapshot 'testid=command-menu'
  5. Note
    : it reads No commands found.
  6. Do
    control-openhands browser fill 'testid=command-menu >> role=combobox' Customize
  7. Do
    control-openhands browser press Enter
  8. Wait
    control-openhands browser wait-url '/mcp(\?|$)'
  9. Check
    control-openhands browser click 'testid=sidebar-extensions-/apps' --expect-url '/apps(\?|$)'
  10. Do
    control-openhands browser goto /extensions
  11. Check
    control-openhands browser snapshot 'testid=not-found-screen'
  12. Note
    show the app's not-found page, like any unknown path: heading Page not found, This address does not match any page. Check the URL, or go back to the home page. and link Home.

Add form and its closers #

  1. Note
    On /apps (control-openhands browser goto /apps; the previous bullet ends on the /extensions not-found page) run
  2. Do
    control-openhands browser click 'testid=canvas-extensions-add-button'
  3. Check
    control-openhands browser snapshot 'testid=add-canvas-extension-modal'
  4. Note
    : heading Add app, intro Install from a Git source or a path on the active Agent Server. The app will be installed disabled., textboxes App source, Ref Optional, Path Optional, buttons Close and Install [disabled].
  5. Note
    After
  6. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' ' '
  7. Check
    control-openhands browser enabled 'testid=add-canvas-extension-submit'

    is still false.

  8. Note
    Each of
  9. Do
    control-openhands browser click 'testid=add-canvas-extension-dismiss'
  10. Do
    control-openhands browser click 'testid=add-canvas-extension-modal-close'
  11. Do
    control-openhands browser press Escape
  12. Do
    control-openhands browser mouse-click 20 500

    (backdrop) closes it:

  13. Check
    control-openhands browser count 'testid=add-canvas-extension-modal'

    is 0 (reopen with canvas-extensions-add-button between them).

Add app dialog with App source, Ref and Path fields and disabled Install button.
The Add app form accepts a Git source or server path and installs apps disabled. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Add app form shows source, optional ref and path, and a disabled Install button.

Only opening the form illustrated; no installation or enablement.

How this screenshot was taken

agent server: 1.53.0 · automation: 1.19.0 (launcher default) · canvas: 1.26.0

control-openhands browser goto /apps
control-openhands browser click testid=canvas-extensions-add-button
control-openhands browser screenshot --feature F20.add-modal --name add-app

Install error, local path #

  1. Note
    Open the form, run
  2. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' /nonexistent/qa-app
  3. Do
    control-openhands browser click 'testid=add-canvas-extension-submit'
  4. Check
    control-openhands browser toasts
  5. Note
    : Could not read canvas extension source: Local extension path does not exist: /nonexistent/qa-app. browser count 'testid=add-canvas-extension-modal' stays 1 and the input keeps its value.
  6. Check
    control-openhands browser errors --app-only

    lists the expected 400 on POST /api/canvas-extensions/install.

Install error, Git source #

  1. Note
    In the same form run
  2. Check
    control-openhands browser toasts --clear
  3. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' 'http://127.0.0.1:9/qa-owner/qa-missing'
  4. Do
    control-openhands browser click 'testid=add-canvas-extension-submit'
  5. Wait
    control-openhands browser wait 'testid=add-canvas-extension-submit >> text=Install' --timeout 60000
  6. Check
    control-openhands browser toasts --history
  7. Do
    control-openhands browser screenshot --feature F20.install-error --name git-server-reason
  8. Expect
    The history holds one toast with the server's 400 detail, Could not read canvas extension source: Failed to fetch extension from http://127.0.0.1:9/qa-owner/qa-missing (browser toasts may still list the local-path toast below it).
  9. Expect
    The form stays open with the URL.

Install the demo #

  1. Note
    In the open form run
  2. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' "$PWD/src/fixtures/canvas-extensions/demo-page"
  3. Do
    control-openhands browser click 'testid=add-canvas-extension-submit' --observe 'testid=add-canvas-extension-submit'
  4. Note
    : the observed states are Install, Installing… [disabled], <absent>.
  5. Check
    control-openhands browser toasts

    shows App installed. Review it here, then enable it when you are ready. Then

  6. Do
    control-openhands browser reload
  7. Check
    control-openhands browser attr 'testid=canvas-extension-card-demo-page >> role=switch' aria-checked

    (false) and

  8. Check
    control-openhands browser count 'testid=sidebar-canvas-extension-demo-page-hello'

    (0).

Card #

  1. Check
    control-openhands browser snapshot 'testid=canvas-extension-card-demo-page'
  2. Note
    : heading Demo page, the source path, switch Enable, A dependency-free fixture for the Canvas Extension page ABI., text Disabled v0.1.0 Pages: 1, list item Hello from an extension, buttons Update and Uninstall.
  3. Do
    control-openhands browser screenshot --feature F20.card --name disabled

Trust confirmation #

  1. Do
    control-openhands browser click 'testid=canvas-extension-card-demo-page >> role=switch'
  2. Check
    control-openhands browser snapshot 'testid=confirmation-modal'
  3. Note
    : the paragraph This app runs trusted JavaScript inside Agent Canvas and can make authenticated requests to the active Agent Server. Review its source and revision before enabling it., buttons Cancel and Enable trusted app.
  4. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=cancel-button'
  5. Note
    closes it and aria-checked stays false.
  6. Note
    Keyboard:
  7. Do
    control-openhands browser focus 'testid=canvas-extension-card-demo-page >> role=switch'
  8. Do
    control-openhands browser press Space
  9. Note
    open it again (browser count 'testid=confirmation-modal' is 1);
  10. Do
    control-openhands browser press Escape
  11. Note
    closes it and api GET /api/canvas-extensions/installed still says "enabled": false.
  12. Note
    Backdrop: click the switch again,
  13. Do
    control-openhands browser mouse-click 20 500
  14. Note
    then browser count 'testid=confirmation-modal' is 0 and the API still says "enabled": false.
  15. Note
    Reopen with the click, then
  16. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button'
  17. Do
    control-openhands browser reload
  18. Note
    and read aria-checked: true;
  19. Check
    control-openhands browser text 'testid=canvas-extension-card-demo-page'
  20. Note
    contains Enabled.

Rail entry #

  1. Expect
    After enabling, run
  2. Wait
    control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --timeout 10000
  3. Check
    control-openhands browser attr 'testid=sidebar-canvas-extension-demo-page-hello' href

    (/extensions/demo-page/hello) and

  4. Check
    control-openhands browser text 'testid=sidebar-canvas-extension-demo-page-hello'
  5. Note
    Expected Extension demo (the fixture's nav_label); today it reads Hello from an extension (see Gotchas).

Render the page #

  1. Check
    control-openhands browser click 'testid=sidebar-canvas-extension-demo-page-hello' --expect-url '/extensions/demo-page/hello(\?|$)'
  2. Check
    control-openhands browser snapshot 'role=main[name="Hello from an extension"]'
  3. Note
    : heading Hello from a Canvas Extension and paragraph Host API 1 on backend default-local.
  4. Note
    Direct URL:
  5. Do
    control-openhands browser goto /extensions/demo-page/hello/nested
  6. Note
    and the same snapshot show the paragraph Nested extension path: nested.
  7. Do
    control-openhands browser screenshot --feature F20.page-render --name rail

App backend view #

  1. Note
    Still on /extensions/demo-page/hello/nested (where Render the page ends) run
  2. Check
    control-openhands browser text 'testid=demo-extension-app-backend-status'
  3. Check
    control-openhands api GET /server_info --pick capabilities
  4. Check
    control-openhands api GET /server_info --pick app_backend_ingress_url
  5. Expect
    The host hands a page appBackendView only when the capabilities list canvas_app_backend_bridge_v1 and the ingress URL is not null: with the pinned server (1.53.0: neither) the line reads App backend view unavailable; a server advertising both would make the demo read App backend view available (not driven here).
  6. Do
    control-openhands browser goto /extensions/demo-page/hello

    shows the same line,

  7. Check
    control-openhands browser snapshot 'role=main[name="Hello from an extension"]'

    lists it as the last paragraph, and

  8. Do
    control-openhands browser screenshot --feature F20.app-backend-view --name unavailable

Active and collapsed rail entry #

  1. Note
    With the demo enabled, run
  2. Do
    control-openhands browser goto /apps
  3. Check
    control-openhands browser attr 'testid=sidebar-canvas-extension-demo-page-hello' aria-current

    is null; after

  4. Check
    control-openhands browser click 'testid=sidebar-canvas-extension-demo-page-hello' --expect-url '/extensions/demo-page/hello(\?|$)'
  5. Note
    it is page.
  6. Do
    control-openhands browser click 'testid=sidebar-collapse-toggle'
  7. Note
    : browser visible 'testid=sidebar-canvas-extension-demo-page-hello' stays true (about 40 px wide), browser attr ... aria-label and
  8. Do
    control-openhands browser tooltip 'testid=sidebar-canvas-extension-demo-page-hello'
  9. Note
    read Hello from an extension;
  10. Do
    control-openhands browser screenshot --feature F20.rail-active --name collapsed

    shows the highlighted icon.

  11. Note
    Expand again with browser click 'testid=sidebar-collapse-toggle' (its aria-label is Collapse sidebar again).
  12. Note
    Command menu: browser goto /, browser press Control+k, browser type 'testid=command-menu >> role=combobox' Hello and browser snapshot 'testid=command-menu' read No commands found; browser press Escape.

Unavailable page #

  1. Do
    control-openhands browser goto /extensions/qa-missing/x
  2. Check
    control-openhands browser snapshot 'role=main'
  3. Note
    : heading Not available and This app is disabled, missing, or does not provide this page.
  4. Do
    control-openhands browser goto /extensions/demo-page/nope

    shows the same card while the demo is enabled.

Phone #

  1. Do
    control-openhands browser viewport phone
  2. Do
    control-openhands browser goto /customize

    (the URL stays /customize),

  3. Do
    control-openhands browser click 'testid=extensions-mobile-hub >> testid=sidebar-extensions-/apps' --expect-url '/apps(\?|$)'
  4. Check
    control-openhands browser bbox 'testid=canvas-extensions-screen'

    (insideViewport true, pageHorizontalOverflow false) and

  5. Do
    control-openhands browser screenshot --feature F20.phone --name apps
  6. Note
    Open the form with canvas-extensions-add-button;
  7. Check
    control-openhands browser bbox 'testid=add-canvas-extension-modal'

    is inside the viewport with no page overflow; close it with add-canvas-extension-dismiss.

  8. Do
    control-openhands browser click 'testid=sidebar-mobile-menu-toggle'
  9. Check
    control-openhands browser visible 'testid=sidebar-mobile-drawer >> testid=sidebar-canvas-extension-demo-page-hello'

    (true),

  10. Do
    control-openhands browser click 'testid=sidebar-mobile-drawer >> testid=sidebar-canvas-extension-demo-page-hello' --expect-url '/extensions/demo-page/hello(\?|$)'
  11. Check
    control-openhands browser bbox 'role=main[name="Hello from an extension"]'

    (inside, no overflow).

  12. Note
    Return with
  13. Do
    control-openhands browser viewport desktop

Update #

  1. Note
    Note installed_at from
  2. Check
    control-openhands api GET /api/canvas-extensions/installed
  3. Note
    then on /apps (control-openhands browser goto /apps; Phone ends on the extension page) run
  4. Do
    control-openhands browser click 'testid=canvas-extension-refresh-demo-page'
  5. Check
    control-openhands browser toasts

    (App updated.).

  6. Expect
    The API shows a newer installed_at and keeps "enabled": true; browser count 'testid=sidebar-canvas-extension-demo-page-hello' stays 1.

Survives a restart #

  1. Do
    control-openhands restart
  2. Check
    control-openhands doctor
  3. Do
    control-openhands browser goto /
  4. Wait
    control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --timeout 15000
  5. Do
    control-openhands browser goto /apps
  6. Note
    and read the switch's aria-checked: true.

Update with the backend down #

  1. Note
    On /apps run
  2. Do
    control-openhands service stop agent-server
  3. Do
    control-openhands browser click 'testid=canvas-extension-refresh-demo-page'
  4. Check
    control-openhands browser toasts
  5. Note
    : one toast, An error occurred (the ingress answers the Update's POST /api/canvas-extensions/install with a 502 that carries no server reason; see Gotchas), and the card still shows Enabled (browser screenshot --feature F20.refresh --name backend-down).
  6. Note
    Bring the stack back with
  7. Do
    control-openhands restart
  8. Check
    control-openhands doctor
  9. Note
    before going on.

Disable #

  1. Note
    On /apps run
  2. Do
    control-openhands browser click 'testid=canvas-extension-card-demo-page >> role=switch'
  3. Check
    control-openhands browser count 'testid=confirmation-modal'

    is 0 and

  4. Wait
    control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --state detached --timeout 10000
  5. Note
    succeeds.
  6. Note
    After
  7. Do
    control-openhands browser reload
  8. Note
    the switch's aria-checked is false;
  9. Do
    control-openhands browser goto /extensions/demo-page/hello
  10. Note
    and browser snapshot 'role=main' show Not available.

Uninstall #

  1. Note
    Re-enable the demo on /apps (control-openhands browser goto /apps, since Disable ends on the extension page; the switch, then testid=confirmation-modal >> testid=confirm-button, then browser wait 'testid=sidebar-canvas-extension-demo-page-hello').
  2. Do
    control-openhands browser click 'testid=canvas-extension-uninstall-demo-page'
  3. Check
    control-openhands browser snapshot 'testid=confirmation-modal'
  4. Note
    : Uninstall Demo page from this backend?, buttons Cancel and Confirm.
  5. Do
    control-openhands browser press Escape
  6. Note
    leaves browser count 'testid=canvas-extension-card-demo-page' at 1; so does clicking Uninstall again and
  7. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=cancel-button'
  8. Note
    Click Uninstall again,
  9. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=confirm-button'
  10. Check
    control-openhands browser toasts

    (App uninstalled.);

  11. Wait
    control-openhands browser wait 'testid=sidebar-canvas-extension-demo-page-hello' --state detached --timeout 10000
  12. Note
    succeeds.
  13. Note
    After
  14. Do
    control-openhands browser reload
  15. Note
    the card count is 0, the page shows No apps are installed on this backend., and /extensions/demo-page/hello shows Not available.

Local source plus Path #

  1. Do
    control-openhands browser goto /apps

    (the Uninstall check ends on /extensions/demo-page/hello),

  2. Do
    control-openhands browser click 'testid=canvas-extensions-add-button'
  3. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' "$PWD/src/fixtures/canvas-extensions"
  4. Do
    control-openhands browser fill 'testid=add-canvas-extension-repo-path-input' demo-page
  5. Do
    control-openhands browser click 'testid=add-canvas-extension-submit'
  6. Wait
    control-openhands browser wait 'testid=add-canvas-extension-modal' --state detached --timeout 30000
  7. Expect
    The toast is App installed. … and api GET /api/canvas-extensions/installed shows "source" ending in /src/fixtures/canvas-extensions/demo-page with "repo_path": null.
  8. Note
    Uninstall it as above (switch untouched, so no rail check).

Git tree URL #

  1. Note
    Needs the Git precondition (upstream reachable, fixture identical).
  2. Do
    control-openhands browser click 'testid=canvas-extensions-add-button'
  3. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' 'https://github.com/OpenHands/OpenHands/tree/main/src/fixtures/canvas-extensions/demo-page'

    (Ref and Path empty),

  4. Do
    control-openhands browser click 'testid=add-canvas-extension-submit' --observe 'testid=add-canvas-extension-submit'

    (Installing… [disabled] for about 0.4 to 2 s, then <absent>) and

  5. Wait
    control-openhands browser wait 'testid=add-canvas-extension-modal' --state detached --timeout 90000
  6. Note
    the toast is App installed. ….
  7. Note
    After
  8. Do
    control-openhands browser reload
  9. Check
    control-openhands browser snapshot 'testid=canvas-extension-card-demo-page'

    shows source https://github.com/OpenHands/OpenHands, switch Enable, term Ref with the 40-character commit from the precondition and term Path with src/fixtures/canvas-extensions/demo-page; api GET /api/canvas-extensions/installed has the same source, resolved_ref and repo_path.

  10. Check
    control-openhands browser screenshot --feature F20.git-tree-url --name card

Duplicate install #

  1. Note
    With the Git-installed demo present, run
  2. Check
    control-openhands browser errors --clear
  3. Do
    control-openhands browser click 'testid=canvas-extensions-add-button'
  4. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' "$PWD/src/fixtures/canvas-extensions/demo-page"
  5. Do
    control-openhands browser click 'testid=add-canvas-extension-submit'
  6. Wait
    control-openhands browser wait 'testid=add-canvas-extension-submit >> text=Install' --timeout 30000
  7. Check
    control-openhands browser toasts

    reads This app is already installed. Use Update or Uninstall on its card. (browser screenshot --feature F20.install-duplicate --name already-installed), browser count 'testid=add-canvas-extension-modal' stays 1, browser count '[data-testid^="canvas-extension-card-"]' stays 1, api GET /api/canvas-extensions/installed still shows the GitHub source, and browser errors --app-only lists the expected 409 on POST /api/canvas-extensions/install.

  8. Note
    Close the form with
  9. Do
    control-openhands browser press Escape

Busy lock #

  1. Note
    Needs the Git-installed demo from the Git tree URL bullet: its Update re-clones in about 0.4 to 2 s, so issue the next commands in the same shell line as the click.
  2. Note
    Disabled app: run
  3. Do
    control-openhands browser click 'testid=canvas-extension-refresh-demo-page'
  4. Note
    then at once
  5. Do
    control-openhands browser press Space --selector 'testid=canvas-extension-card-demo-page >> role=switch'

    (focuses the switch, then presses Space) and

  6. Do
    control-openhands browser eval "[document.querySelector('[data-testid=canvas-extension-uninstall-demo-page]').disabled, document.querySelector('[data-testid=canvas-extension-card-demo-page] [role=switch]').disabled, document.activeElement.tagName].join(' ')"
  7. Note
    which reads true true BODY (Uninstall and the switch are both disabled, and the busy switch takes no focus), then
  8. Check
    control-openhands browser count 'testid=confirmation-modal'

    (0: Space did nothing).

  9. Note
    Read the three values in that one eval: each browser enabled call takes about 150 ms and can land after a short Update.
  10. Note
    If the eval reads false for either control, the Update ended mid-line and Space may have reached an idle switch: close any trust dialog with
  11. Do
    control-openhands browser click 'testid=confirmation-modal >> testid=cancel-button'
  12. Note
    and run the line again.
  13. Note
    Once
  14. Check
    control-openhands browser toasts --history

    shows App updated., the keyboard works again: browser enabled on the switch is true, and browser focus on it, browser press Space and browser count 'testid=confirmation-modal' give 1.

  15. Note
    Close the dialog with testid=confirmation-modal >> testid=cancel-button; api GET /api/canvas-extensions/installed --pick canvas_extensions.0.enabled is still false.
  16. Note
    Enabled app (only when the card's Ref equals the precondition's commit): enable it through the trust dialog and wait for sidebar-canvas-extension-demo-page-hello, then repeat the same line (click Update, Space on the switch, the eval reads true true BODY, no dialog).
  17. Note
    If the eval reads false for either control there and Space reached the idle switch, it turned the app off with no dialog (api GET /api/canvas-extensions/installed --pick canvas_extensions.0.enabled is false): enable it again through the trust dialog, wait for sidebar-canvas-extension-demo-page-hello and repeat the line.
  18. Expect
    A few seconds later api GET /api/canvas-extensions/installed --pick canvas_extensions.0.enabled is still true, and after browser reload the switch's aria-checked is true and browser count 'testid=sidebar-canvas-extension-demo-page-hello' is 1.
  19. Note
    Clean up: uninstall as above and confirm api GET /api/canvas-extensions/installed --pick canvas_extensions is [].

Ref and Path with the github: shorthand #

  1. Note
    Needs the Git precondition.
  2. Do
    control-openhands browser click 'testid=canvas-extensions-add-button'
  3. Do
    control-openhands browser fill 'testid=add-canvas-extension-source-input' 'github:OpenHands/OpenHands'
  4. Do
    control-openhands browser fill 'testid=add-canvas-extension-ref-input' main
  5. Do
    control-openhands browser fill 'testid=add-canvas-extension-repo-path-input' src/fixtures/canvas-extensions/demo-page
  6. Do
    control-openhands browser focus 'testid=add-canvas-extension-repo-path-input'
  7. Do
    control-openhands browser press Enter

    (Enter submits) and

  8. Wait
    control-openhands browser wait 'testid=add-canvas-extension-modal' --state detached --timeout 90000
  9. Note
    the toast is App installed. ….
  10. Note
    After
  11. Do
    control-openhands browser reload
  12. Note
    browser snapshot 'testid=canvas-extension-card-demo-page' shows source github:OpenHands/OpenHands, Ref with the precondition's commit and Path src/fixtures/canvas-extensions/demo-page.
  13. Note
    Uninstall it as above.

List error with Retry · Blocked prerequisite #

  1. Note
    Blocked: it needs an Agent Server whose GET /api/canvas-extensions/installed fails with a non-404 while its health checks pass.
  2. Do
    control-openhands service stop agent-server
  3. Do
    control-openhands browser goto /apps

    shows the app-wide Manage backends gate (Disconnected) instead of the page, and on an already loaded page the cached list stays on screen.

Unsupported backends · Blocked prerequisite #

  1. Note
    Blocked: needs a Cloud backend account (Apps are not available on Cloud backends yet., Apps hidden from Customize), a state with no backend (Add an Agent Server backend to use Apps.), or an Agent Server that passes Canvas's version gate (1.47.0 or newer) but has no /api/canvas-extensions routes (This Agent Server does not support Apps yet. Upgrade the backend and try again.).
  2. Note
    In each case Add app should be disabled.
  3. Note
    Released servers cannot produce the last state: the routes ship from 1.44.1, and an older server such as
  4. Do
    control-openhands launch --new --sdk-ref v1.43.1

    (whose GET /api/canvas-extensions/installed is 404) never reaches /apps, because every page shows the Manage backends gate with Agent Canvas requires agent-server 1.47.0 or newer; this backend is running 1.43.1. (doctor still reports ok).

Gotchas and known limits

  • The card's refresh button (canvas-extension-refresh-<name>) is labelled Update, not "Refresh".
  • The trust confirmation has no title and reuses the shared confirmation-modal; its body repeats the amber notice. Keyboard focus is not moved into it: after Space on the switch, Tab lands on the card's Update button behind the dialog.
  • nav_label is dropped by the Agent Server: GET /api/canvas-extensions/installed returns pages with only id, title and path (Agent Server 1.50.1), so the rail shows the page title (OpenHands/software-agent-sdk#5501). Keep F20.rail-label failing until the backend returns it.
  • Install and update errors show the server's reason as sent, even when it says Failed to fetch extension from <url> (an unreachable Git source). A response with no reason in it, such as the ingress's 502 while the Agent Server is stopped, shows the generic An error occurred. Only a request that gets no response at all shows Disconnected (check URL or network)…; no control-openhands verb takes the ingress offline, so that state is not driven here.
  • The page states (empty, error, unsupported) and the /extensions/... unavailable card have no test ids; assert on text with browser snapshot.
  • The Agent Server fetches Git sources with a shallow clone and appends .git to the repo URL: a test Git server must speak smart HTTP and answer both qa-repo and qa-repo.git.
  • The Git bullets read the public upstream repo, so they depend on its main still holding the identical fixture; the precondition's diff -r guards that. A Git install or Update clones in about 0.4 to 2 s here (Updates measured about 0.4 to 1.5 s on 2026-10-06), long enough for the busy-lock checks when they run in the same shell line as the click and read both controls in one eval; on a slower network raise the wait timeouts, not the expectations.
  • A duplicate install answers 409 with the Agent Server's API hint (Canvas extension already installed. Use force=true to overwrite.); the form has no force option, so the toast replaces it with the Update/Uninstall advice. Plugins still show their 409 hint verbatim (F19 Gotchas).
  • Busy states with a local-path source last about 50 ms: --observe catches Installing…, but checking that controls are disabled during Update needs a Git source (an upstream Update lasts about 0.4 to 2 s).
  • control-openhands launch passes the launcher only a fixed list of environment variables (path, locale, proxy) plus its own flags: OH_AGENT_SERVER_VERSION=<old> in the environment is ignored (the run still gets the pinned server); use --sdk-ref v<version>. Any server older than 1.47.0 is gated by Canvas before /apps renders.
  • Stopping the Agent Server makes the next full page load show the Manage backends gate; always restart and doctor afterwards.
  • A trailing browser errors --app-only sweep shows a 400 per failed install by design; count only other errors.
  • App backend view unavailable on the demo page is the expected text while GET /server_info lists no canvas_app_backend_bridge_v1 capability (Agent Server 1.53.0); it does not mean the app failed to load. Cloud backends and the no-backend state never get a backend view either.

Source paths: src/routes/canvas-extensions.tsx, src/routes/canvas-extension-page.tsx, src/components/features/canvas-extensions/, src/hooks/mutation/use-manage-canvas-extensions.ts, src/api/canvas-extensions-service.ts, src/utils/parse-git-tree-url.ts, src/components/features/sidebar/sidebar-rail-body.tsx, src/fixtures/canvas-extensions/demo-page/.