EN / field notes OpenHands feature map

OpenHands / F19

Plugins and plugin launch

Plugins bundle skills, commands and files that the agent loads into a conversation. Under Customize → Plugins a user browses one card grid that merges the marketplace catalog, installed plugins and read-only local plugins; searches and filters it; installs, enables, disables, updates and uninstalls plugins; browses a plugin's bundled skills and files; and starts a conversation with a plugin. Starting goes through /launch, a review screen (also reachable as a deep link) that lists the plugins, lets the user edit plugin parameters and requires a trust checkbox before it creates the conversation.

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

How to get to it

  • Sidebar Customize (sidebar-skills-link, lands on /mcp), then Plugins in the Customize navigation (sidebar-extensions-/plugins).
  • Direct URL /plugins.
  • Command menu (Control+k/Meta+k): "Plugins" finds only Customize ("Browse skills, plugins, and integrations"), which lands on /mcp; then click Plugins. There is no direct Plugins command.
  • Phone width: /customize shows the Customize hub; its Plugins row (extensions-mobile-hub >> sidebar-extensions-/plugins) opens /plugins, and the top bar's Back chevron (sidebar-mobile-back-button) returns to /customize.
  • /launch: from a plugin's detail modal (Start Conversation), or a deep link /launch?plugins=<base64 JSON array>&message=<text> (production format) or /launch?plugin_source=… (dev format).
  • Attaching plugins from the home composer (Plugins pill) is F03 (F03.plugin-picker); the conversation's Show Plugins list is F27 (F27.plugins-modal).

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), local backend, desktop viewport.
  • An active LLM profile (control-openhands llm preset deepseek) for Enabled plugins auto-load and the two Start-a-conversation bullets.
  • No plugin installed: control-openhands api GET /api/plugins/installed returns {"plugins": []}.
  • The catalog comes from the launcher's cached public-skills checkout and needs no network: control-openhands api GET /api/plugins/marketplace lists city-weather, magic-test and others with local-path source values. magic-test answers "alakazam" with The magic worked! Plugin loaded successfully. 🎩✨, which makes plugin loading cheap to prove.
  • Right before the Local plugin bullet (not earlier: it changes card counts and the Local filter), put a local plugin in the Agent Server's ~/.agents/plugins. No CLI verb does this (harness gap); from the checkout run H="$OH_VERIFY_RUN/private/home/.agents/plugins"; mkdir -p "$H" && cp -r "$OH_VERIFY_RUN/private/cache/skills/public-skills/plugins/magic-test" "$H/qa-local-plugin" && sed -i 's/"name": "magic-test"/"name": "qa-local-plugin"/' "$H/qa-local-plugin/.plugin/plugin.json". The rename matters: a local plugin whose name is already in the catalog is hidden.
  • The Add plugin bullet installs from github:OpenHands/extensions, which needs outbound access to GitHub.

Behavior inventory

25 stable behavior IDs and their expected behavior
  • F19.page /plugins shows the Plugins heading, the description "Browse, install, enable, and uninstall plugins. Enabled plugins load automatically into new conversations.", an enabled Add plugin button on a local backend, and the catalog as cards. Read recipe ↓
  • F19.search the search box filters by name, description, source, path and ref; a query matching nothing shows No plugins match your search.; the X clears the query. Read recipe ↓
  • F19.status-filter the All / Installed / Available / Local segments filter the grid; the active one has aria-pressed="true"; the filter resets to All on reload. Read recipe ↓
  • F19.detail-modal clicking a card (or Enter/Space on a focused card) opens a modal with name, source, description, Skills in this plugin bundle, Files and footer actions; X, Close and Escape close it. Read recipe ↓
  • F19.files-browser in the modal's Files tree, expanding a folder and selecting a file shows its highlighted content with line numbers; selecting it again closes the viewer. Read recipe ↓
  • F19.install-card Install on a card installs the plugin: toast Plugin installed., the button becomes an enable switch, a version pill appears, the card sorts first and the install survives a reload. Read recipe ↓
  • F19.install-modal Install in the detail modal does the same for the open plugin; the modal stays open and switches to the installed footer. Read recipe ↓
  • F19.enable-toggle the card switch (named Disable plugin / Enable plugin) and the modal Enabled/Disabled switch turn an installed plugin off and on; the state persists after reload. Read recipe ↓
  • F19.enabled-autoload an installed, enabled plugin loads into every new conversation; disabled, it does not. Read recipe ↓
  • F19.refresh Update in an installed plugin's modal re-fetches it from its recorded source and shows Plugin updated. Read recipe ↓
  • F19.uninstall Uninstall removes the plugin at once (no confirmation), shows Plugin uninstalled., closes the modal and returns the card to Install. Read recipe ↓
  • F19.add-plugin Add plugin opens Add a plugin with Source (required), Ref and Path (optional); Install stays disabled until Source has text; a valid source installs and closes the modal. Read recipe ↓
  • F19.add-plugin-cancel the Add modal's X, Close, Escape and a backdrop click close it without installing; reopening starts with empty fields. Read recipe ↓
  • F19.add-plugin-error an unusable source shows one error toast with the server's reason and keeps the modal open with the typed values. Read recipe ↓
  • F19.installed-coordinates a plugin installed with a source, ref and path shows that source on its card and in the modal header, and modal pills v<version>, Ref <resolved commit> and Path <repo path> next to the Enabled switch. Read recipe ↓
  • F19.local-plugin a plugin found in ~/.agents/plugins shows a Local badge (no Install or switch), is listed under the Local filter, and its modal has a Local pill, Files, no Install/Uninstall and no Start Conversation. Read recipe ↓
  • F19.start-conversation Start Conversation in the detail modal opens /launch?plugins=<base64 JSON> for that plugin. Read recipe ↓
  • F19.launch-review /launch shows a modal titled Launch <plugin> (one plugin) or Launch Plugin (several), the optional message with HTML stripped and capped at 500 characters, and each plugin as <source> / <path> @ <ref>; X and Escape go to /conversations. Read recipe ↓
  • F19.launch-parameters plugins with parameters get an expanded, collapsible section with a text input, number input (blank becomes 0) or checkbox per value; edits are kept. Read recipe ↓
  • F19.launch-trust Start Conversation stays disabled until the trust checkbox (I trust this plugin from <sources> …) is ticked; then it creates the conversation (sending the message, if any), opens /conversations/<id>, and the plugin is loaded there. Read recipe ↓
  • F19.launch-dev-params /launch?plugin_source=…&plugin_ref=…&plugin_repo_path=…&message=… opens the same review for one plugin. Read recipe ↓
  • F19.launch-errors bare /launch or an empty array says No plugins were specified…; bad base64, invalid JSON, a non-array or an item without source says The plugin configuration is invalid…; Go Home opens /conversations. Read recipe ↓
  • F19.launch-creation-failed when conversation creation fails, the screen shows Unable to Launch / Failed to Start Conversation with Go Home and Try Again; Try Again reloads the page, which brings back the review modal with the trust box cleared. Read recipe ↓
  • F19.launch-unicode a deep link whose JSON contains non-ASCII text (UTF-8 base64) shows that text correctly. Read recipe ↓
  • F19.phone at 390 px the toolbar stacks, cards fit, and the detail, Add and launch modals stay inside the viewport. 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.

Open the page #

  1. Do
    control-openhands browser goto /conversations
  2. Check
    control-openhands browser click 'testid=sidebar-skills-link' --expect-url '/mcp(\?|$)'
  3. Check
    control-openhands browser click 'testid=sidebar-extensions-/plugins' --expect-url '/plugins(\?|$)'
  4. Check
    control-openhands browser text 'testid=skills-plugins-screen >> h2'

    (Plugins),

  5. Check
    control-openhands browser text 'testid=plugins-settings-description'

    (the description above),

  6. Check
    control-openhands browser enabled 'testid=plugins-add-plugin-button'

    (true) and

  7. Check
    control-openhands browser count '[data-testid^="plugin-card-"]'

    (9 with today's catalog).

  8. Do
    control-openhands browser screenshot --feature F19.page --name catalog

    shows two columns of cards, each with name, truncated source path, a two-line description and Install.

Command-menu entry #

  1. Note
    From /conversations run
  2. Do
    control-openhands browser press Control+k
  3. Do
    control-openhands browser type 'testid=command-menu >> role=combobox' Plugins
  4. Check
    control-openhands browser snapshot 'testid=command-menu'

    (one option, Customize Browse skills, plugins, and integrations. Go),

  5. Do
    control-openhands browser press Enter
  6. Wait
    control-openhands browser wait-url '/mcp(\?|$)'
  7. Check
    control-openhands browser click 'testid=sidebar-extensions-/plugins' --expect-url '/plugins(\?|$)'

Search #

  1. Do
    control-openhands browser fill 'testid=plugins-search-input' weather
  2. Check
    control-openhands browser count '[data-testid^="plugin-card-"]'

    (1).

  3. Do
    control-openhands browser fill 'testid=plugins-search-input' alakazam
  4. Note
    also leaves 1 (magic-test, matched on its description).
  5. Do
    control-openhands browser fill 'testid=plugins-search-input' zzz-none
  6. Check
    control-openhands browser text 'testid=plugins-no-match'
  7. Note
    : No plugins match your search. Run
  8. Do
    control-openhands browser click 'testid=plugins-toolbar >> role=button[name="Clear search"]'
  9. Check
    control-openhands browser value 'testid=plugins-search-input'

    is empty and the count is back to 9.

Status filter #

  1. Check
    control-openhands browser attr 'testid=plugins-filter-all' aria-pressed

    is true.

  2. Do
    control-openhands browser click 'testid=plugins-filter-installed'
  3. Note
    : its aria-pressed is true, All's is false, and with nothing installed
  4. Check
    control-openhands browser count 'testid=plugins-no-match'

    is 1.

  5. Do
    control-openhands browser click 'testid=plugins-filter-available'

    shows all 9 cards;

  6. Do
    control-openhands browser click 'testid=plugins-filter-local'

    shows plugins-no-match (no local plugin yet).

  7. Note
    After
  8. Do
    control-openhands browser reload
  9. Note
    plugins-filter-all is pressed again.

Detail modal #

  1. Do
    control-openhands browser click 'testid=plugin-card-magic-test'
  2. Check
    control-openhands browser snapshot 'testid=plugin-detail-modal'
  3. Note
    : heading magic-test, the source path, the description, Skills in this plugin bundle with magic-word, Files with .plugin and skills folders, and buttons Install, Start Conversation, Close.
  4. Note
    Close with
  5. Do
    control-openhands browser click 'testid=plugin-detail-modal-close'

    (browser count 'testid=plugin-detail-modal' is 0).

  6. Note
    Keyboard:
  7. Do
    control-openhands browser focus 'testid=plugin-card-city-weather'
  8. Do
    control-openhands browser press Enter
  9. Check
    control-openhands browser attr 'testid=plugin-detail-modal' data-plugin-name

    (city-weather);

  10. Do
    control-openhands browser press Escape
  11. Note
    closes it; browser focus again and
  12. Do
    control-openhands browser press Space
  13. Note
    reopens it;
  14. Do
    control-openhands browser click 'testid=plugin-detail-modal-dismiss'
  15. Note
    closes it.

Files #

  1. Note
    Open the magic-test modal again and run
  2. Do
    control-openhands browser click 'testid=file-tree-dir-.plugin'
  3. Do
    control-openhands browser click 'testid=file-tree-file-.plugin/plugin.json'
  4. Wait
    control-openhands browser wait 'testid=plugin-file-content >> text=magic-test' --timeout 15000
  5. Check
    control-openhands browser text 'testid=plugin-file-content'
  6. Note
    : .plugin/plugin.json followed by numbered JSON lines (2 "name": "magic-test",).
  7. Do
    control-openhands browser screenshot --feature F19.files-browser --name plugin-json

    shows the tree with the file highlighted above a syntax-coloured viewer.

  8. Note
    Click the same file again;
  9. Check
    control-openhands browser count 'testid=plugin-file-content'

    is 0.

  10. Note
    Close with testid=plugin-detail-modal-close.

Install from a card #

  1. Check
    control-openhands browser toasts --clear
  2. Do
    control-openhands browser click 'testid=plugin-install-magic-test'
  3. Wait
    control-openhands browser wait 'testid=plugin-toggle-magic-test' --timeout 60000
  4. Check
    control-openhands browser toasts

    (Plugin installed.).

  5. Check
    control-openhands browser text 'testid=plugin-version-magic-test'

    is v1.0.0 and

  6. Do
    control-openhands browser eval "[...document.querySelectorAll('[data-testid^=plugin-card-]')].map(e=>e.dataset.testid)[0]"

    is plugin-card-magic-test.

  7. Note
    After
  8. Do
    control-openhands browser reload
  9. Do
    control-openhands browser click 'testid=plugins-filter-installed'
  10. Note
    browser count '[data-testid^="plugin-card-"]' is 1, and
  11. Check
    control-openhands api GET /api/plugins/installed

    lists magic-test with "enabled": true.

  12. Note
    Click testid=plugins-filter-all.

Enable switch #

  1. Check
    control-openhands browser attr 'testid=plugin-toggle-magic-test' aria-checked

    (true) and

  2. Check
    control-openhands browser attr 'testid=plugin-toggle-magic-test' aria-label

    (Disable plugin),

  3. Do
    control-openhands browser hover 'testid=plugin-toggle-magic-test'
  4. Do
    control-openhands browser click 'testid=plugin-toggle-magic-test'
  5. Note
    wait three seconds, then browser attr ... aria-checked again (false, with aria-label Enable plugin); after
  6. Do
    control-openhands browser reload
  7. Note
    it is still false and api GET /api/plugins/installed shows "enabled": false.
  8. Note
    Today the disable is often lost: the reload's concurrent reads hit the metadata race (Gotchas) and the API, then the switch, read "enabled": true again (2 of 3 trials; the PATCH itself returns 200).
  9. Note
    If that happens, disable it once more before the modal step, which needs the plugin disabled.
  10. Note
    In the modal:
  11. Do
    control-openhands browser click 'testid=plugin-card-magic-test'
  12. Do
    control-openhands browser click 'testid=plugin-detail-modal >> text="Disabled"'
  13. Do
    control-openhands browser eval "document.querySelector('[data-testid=plugin-modal-toggle-magic-test]').checked"

    (true); the API shows "enabled": true and, after closing the modal, the card switch's aria-checked is true.

Enabled plugins auto-load #

  1. Note
    With magic-test enabled, run
  2. Wait
    control-openhands conversation start --prompt "alakazam" --wait --timeout 240
  3. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'
  4. Note
    : The magic worked! Plugin loaded successfully. 🎩✨.
  5. Do
    control-openhands browser goto /plugins
  6. Note
    disable it with the card switch (hover, click; API "enabled": false) and run the same conversation start: the reply is an ordinary request for a task, without the phrase. browser goto /plugins again; magic-test stays installed and disabled.
  7. Note
    Today the revisit often re-enables it (API "enabled": true, installed_at reset; see Gotchas), so re-check the API before relying on the disabled state.

Update #

  1. Do
    control-openhands browser click 'testid=plugin-card-magic-test'
  2. Check
    control-openhands browser toasts --clear
  3. Do
    control-openhands browser click 'testid=plugin-detail-refresh-magic-test'
  4. Note
    wait a few seconds, then
  5. Check
    control-openhands browser toasts
  6. Note
    Expected Plugin updated. (the toast fades fast: read control-openhands browser toasts --history).
  7. Note
    Whether it works depends on the recorded source (control-openhands browser text 'testid=plugin-source-magic-test'): with the catalog path it does.
  8. Note
    Known failure (reproduced 2026-10-06): a page revisit after a catalog install usually rewrites the source to local (OpenHands/software-agent-sdk#5496, see Gotchas), and Update then answers 500 with one toast carrying the server's reason, Unable to parse extension source: local. Expected formats: 'github:owner/repo', git URL, or local path (browser screenshot --feature F19.refresh --name local-source-error).
  9. Note
    On a plugin whose source survived (the GitHub install in Add plugin from GitHub, browser text 'testid=plugin-source-magic-test' = github:OpenHands/extensions), the same click logs Updating extension magic-test from github:OpenHands/extensions and toasts --history shows Plugin updated. Close the modal.

Start a conversation from an installed plugin #

  1. Do
    control-openhands browser click 'testid=plugin-card-magic-test'
  2. Check
    control-openhands browser click 'testid=plugin-detail-start-conversation-magic-test' --expect-url '/launch\?plugins='
  3. Do
    control-openhands browser eval "JSON.parse(atob(new URLSearchParams(location.search).get('plugins')))"
  4. Note
    Expected: the plugin's real source.
  5. Note
    Today it is [{"source":"local","ref":null,"repo_path":null}] and the modal title is Launch local; after
  6. Do
    control-openhands browser check 'testid=trust-checkbox'
  7. Check
    control-openhands browser click 'testid=start-conversation-button' --expect-url '/conversations/[0-9a-f-]+'
  8. Do
    control-openhands browser type 'testid=chat-input' 'alakazam'
  9. Do
    control-openhands browser press Enter --selector 'testid=chat-input'
  10. Note
    leave the message at Sending... (still after 25 s) with the banner Unable to parse plugin source: local. Expected formats: 'github:owner/repo', git URL, or local path (control-openhands browser screenshot --feature F19.start-conversation --name local-source-conversation).
  11. Note
    When the installed plugin kept its source (the GitHub install below), the link carries it: [{"source":"github:OpenHands/extensions","ref":"<resolved commit>","repo_path":"plugins/magic-test"}] and the title is Launch magic-test.

Uninstall #

  1. Do
    control-openhands browser goto /plugins
  2. Check
    control-openhands browser toasts --clear
  3. Do
    control-openhands browser click 'testid=plugin-card-magic-test'
  4. Do
    control-openhands browser click 'testid=plugin-detail-uninstall-magic-test'
  5. Check
    control-openhands browser count 'testid=plugin-detail-modal'

    (0),

  6. Check
    control-openhands browser count 'testid=confirmation-modal'

    (0: no confirmation),

  7. Check
    control-openhands browser toasts

    (Plugin uninstalled.) and

  8. Check
    control-openhands browser count 'testid=plugin-install-magic-test'

    (1).

  9. Note
    After
  10. Do
    control-openhands browser reload
  11. Note
    the Install button is still there, browser text 'testid=plugin-source-magic-test' is the catalog path again and api GET /api/plugins/installed returns {"plugins": []}.

Install from the modal #

  1. Check
    control-openhands browser toasts --clear
  2. Do
    control-openhands browser click 'testid=plugin-card-city-weather'
  3. Do
    control-openhands browser click 'testid=plugin-detail-install-city-weather'
  4. Wait
    control-openhands browser wait 'testid=plugin-detail-uninstall-city-weather' --timeout 60000
  5. Check
    control-openhands browser toasts

    (Plugin installed.).

  6. Expect
    The modal stays open (browser count 'testid=plugin-detail-modal' is 1),
  7. Do
    control-openhands browser eval "document.querySelector('[data-testid=plugin-modal-toggle-city-weather]').checked"

    is true, and

  8. Check
    control-openhands browser snapshot 'testid=plugin-detail-modal >> footer'

    lists Update, Uninstall, Start Conversation, Close.

  9. Note
    Clean up with
  10. Do
    control-openhands browser click 'testid=plugin-detail-uninstall-city-weather'
  11. Note
    browser count 'testid=plugin-install-city-weather' is 1.

Cancel Add plugin #

  1. Do
    control-openhands browser click 'testid=plugins-add-plugin-button'
  2. Do
    control-openhands browser fill 'testid=add-plugin-source-input' qa-typed
  3. Do
    control-openhands browser click 'testid=add-plugin-modal-close'
  4. Check
    control-openhands browser count 'testid=add-plugin-modal'

    is 0.

  5. Note
    Reopen it:
  6. Check
    control-openhands browser value 'testid=add-plugin-source-input'

    is empty.

  7. Note
    Type again and close with
  8. Do
    control-openhands browser click 'testid=add-plugin-modal-dismiss'

    (count 0); reopen and

  9. Do
    control-openhands browser press Escape

    (count 0); reopen and

  10. Do
    control-openhands browser mouse-click 20 500
  11. Note
    on the backdrop (count 0). api GET /api/plugins/installed still returns {"plugins": []} and browser network shows no POST /api/plugins/install.

Add plugin form and error #

  1. Do
    control-openhands browser click 'testid=plugins-add-plugin-button'
  2. Check
    control-openhands browser snapshot 'testid=add-plugin-modal'
  3. Note
    : heading Add a plugin, Install a plugin from a Git source or local path., textbox Source (placeholder github:owner/repo, a Git URL, or a local path), Ref Optional, Path Optional, Close and a disabled Install.
  4. Do
    control-openhands browser fill 'testid=add-plugin-source-input' ' '
  5. Note
    keeps
  6. Check
    control-openhands browser enabled 'testid=add-plugin-submit'
  7. Note
    at false;
  8. Do
    control-openhands browser fill 'testid=add-plugin-source-input' qa-not-a-source
  9. Note
    turns it true.
  10. Check
    control-openhands browser toasts --clear
  11. Do
    control-openhands browser click 'testid=add-plugin-submit'
  12. Note
    wait a few seconds, then
  13. Check
    control-openhands browser toasts --history
  14. Check
    control-openhands browser count 'testid=add-plugin-modal'

    (1) and

  15. Check
    control-openhands browser value 'testid=add-plugin-source-input'

    (qa-not-a-source).

  16. Expect
    The history holds one toast with the server's reason: the Agent Server answers 400 Failed to fetch plugin source. Check that the source is valid., and the toast shows that text (browser screenshot --feature F19.add-plugin-error --name toast).
Add a plugin form with Source, Ref and Path fields.
Add plugin accepts a Git source or a local path with optional ref and path. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

The Add a plugin form opens with source, ref and path inputs and disabled Install.

Form view only; no installation or error submission. Captured dialog alone to omit private catalog-cache paths.

How this screenshot was taken

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

control-openhands browser goto /plugins
control-openhands browser click testid=plugins-add-plugin-button
control-openhands browser screenshot testid=add-plugin-modal --feature F19.add-plugin --name add-plugin

Add plugin from GitHub #

  1. Note
    In the same modal run
  2. Do
    control-openhands browser fill 'testid=add-plugin-source-input' 'github:OpenHands/extensions'
  3. Do
    control-openhands browser fill 'testid=add-plugin-repo-path-input' 'plugins/magic-test'
  4. Check
    control-openhands browser toasts --clear
  5. Do
    control-openhands browser click 'testid=add-plugin-submit'
  6. Check
    control-openhands browser text 'testid=add-plugin-submit'

    (Installing…), then

  7. Wait
    control-openhands browser wait 'testid=add-plugin-modal' --state hidden --timeout 120000
  8. Check
    control-openhands browser toasts

    (Plugin installed.). api GET /api/plugins/installed lists magic-test with four files (.claude-plugin/plugin.json, .codex-plugin/plugin.json, .plugin/plugin.json, skills/magic-word/SKILL.md) with "source": "github:OpenHands/extensions", "repo_path": "plugins/magic-test" and a commit in resolved_ref; the card's plugin-source-magic-test reads github:OpenHands/extensions.

  9. Expect
    The metadata race (Gotchas) can still turn it into "source": "local" with repo_path: null.
  10. Note
    Without GitHub access expect the error toast of the previous bullet instead.
  11. Note
    Clean up: testid=plugin-card-magic-test → testid=plugin-detail-uninstall-magic-test.

Installed coordinates #

  1. Note
    Needs GitHub access like the previous bullet.
  2. Do
    control-openhands browser click 'testid=plugins-add-plugin-button'
  3. Do
    control-openhands browser fill 'testid=add-plugin-source-input' 'github:OpenHands/extensions'
  4. Do
    control-openhands browser fill 'testid=add-plugin-ref-input' main
  5. Do
    control-openhands browser fill 'testid=add-plugin-repo-path-input' 'plugins/city-weather'
  6. Do
    control-openhands browser click 'testid=add-plugin-submit'
  7. Wait
    control-openhands browser wait 'testid=add-plugin-modal' --state hidden --timeout 120000

    (toasts --history: Plugin installed.).

  8. Check
    control-openhands browser text 'testid=plugin-source-city-weather'

    is github:OpenHands/extensions;

  9. Do
    control-openhands browser click 'testid=plugin-card-city-weather'
  10. Check
    control-openhands browser snapshot 'testid=plugin-detail-modal'
  11. Note
    show the header paragraph github:OpenHands/extensions and the text v1.0.0 Ref <40-hex commit> Path plugins/city-weather Enabled (the API's resolved_ref), and Files lists .claude-plugin and .codex-plugin that the catalog copy lacks.
  12. Note
    Clean up with
  13. Do
    control-openhands browser click 'testid=plugin-detail-uninstall-city-weather'
  14. Note
    the card's source is the catalog path again.

Local plugin #

  1. Expect
    After arranging qa-local-plugin (Preconditions), run
  2. Do
    control-openhands browser reload
  3. Check
    control-openhands browser text 'testid=plugin-local-badge-qa-local-plugin'

    (Local), and

  4. Check
    control-openhands browser count
  5. Note
    on testid=plugin-install-qa-local-plugin, testid=plugin-toggle-qa-local-plugin and testid=plugin-source-qa-local-plugin (all 0).
  6. Do
    control-openhands browser click 'testid=plugins-filter-local'
  7. Note
    leaves one card; under plugins-filter-available browser count 'testid=plugin-card-qa-local-plugin' is 0.
  8. Note
    Back on All,
  9. Do
    control-openhands browser click 'testid=plugin-card-qa-local-plugin'
  10. Check
    control-openhands browser snapshot 'testid=plugin-detail-modal'
  11. Note
    : no source line, pills Local v1.0.0, the bundled skill and Files, and only a Close button in the footer (browser screenshot --feature F19.local-plugin --name modal).
  12. Note
    Close with Escape.
  13. Note
    Clean up: rm -rf "$OH_VERIFY_RUN/private/home/.agents/plugins/qa-local-plugin",
  14. Do
    control-openhands browser reload
  15. Note
    the card count for qa-local-plugin is 0.

Start a conversation from the catalog #

  1. Note
    With magic-test not installed, run
  2. Do
    control-openhands browser click 'testid=plugin-card-magic-test'
  3. Check
    control-openhands browser click 'testid=plugin-detail-start-conversation-magic-test' --expect-url '/launch\?plugins='
  4. Note
    and the atob eval above: one object with the catalog source, ref: null, repo_path: null.
  5. Check
    control-openhands browser snapshot 'testid=plugin-launch-modal'

    shows Launch magic-test, the source path and the checkbox I trust this plugin from <source path> with the agent secrets defined in my account.;

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

    is false.

  7. Do
    control-openhands browser check 'testid=trust-checkbox'

    (enabled turns true) and

  8. Check
    control-openhands browser click 'testid=start-conversation-button' --expect-url '/conversations/[0-9a-f-]+'
  9. Check
    control-openhands browser url
  10. Note
    gives <id>.
  11. Do
    control-openhands browser click 'testid=chat-plus-button'
  12. Check
    control-openhands browser click 'testid=show-plugins-button'
  13. Check
    control-openhands browser text 'testid=plugins-modal'
  14. Note
    list magic-test; close with testid=close-plugins-modal.
  15. Do
    control-openhands browser type 'testid=chat-input' 'alakazam'
  16. Do
    control-openhands browser press Enter --selector 'testid=chat-input'
  17. Wait
    control-openhands conversation wait <id> --timeout 180
  18. Check
    control-openhands browser text 'testid=agent-message >> nth=-1'
  19. Note
    : the magic phrase.

Review screen #

  1. Do
    control-openhands browser goto '/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIifSx7InNvdXJjZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9PcGVuSGFuZHMvZXh0ZW5zaW9ucy5naXQiLCJyZXBvX3BhdGgiOiJwbHVnaW5zL21hZ2ljLXRlc3QiLCJyZWYiOiJtYWluIn1d'

    (two plugins from OpenHands/extensions, one as an HTTPS git URL) and

  2. Check
    control-openhands browser snapshot 'testid=plugin-launch-modal'
  3. Note
    : the heading Launch Plugin (Launch once); under Plugins, the text city-weather OpenHands/extensions/ plugins/city-weather magic-test OpenHands/extensions/ plugins/magic-test@ main (the screenshot renders OpenHands/extensions / plugins/magic-test @ main); and the checkbox I trust this plugin from OpenHands/extensions with the agent secrets defined in my account., which names OpenHands/extensions once (browser screenshot --feature F19.launch-review --name multi).
  4. Note
    Message:
  5. Do
    control-openhands browser goto '/launch?plugin_source=github%3AOpenHands%2Fextensions&message=%3Cb%3EWeather%3C%2Fb%3E%20for%20QA'
  6. Check
    control-openhands browser text 'testid=plugin-launch-modal >> p'
  7. Note
    : Weather for QA (tags stripped).
  8. Note
    With a 600-character message (/launch?plugin_source=github%3AOpenHands%2Fextensions&message=aaa…),
  9. Do
    control-openhands browser eval "document.querySelector('[data-testid=plugin-launch-modal] p').textContent.length"

    is 500.

  10. Note
    Close:
  11. Do
    control-openhands browser click 'testid=plugin-launch-modal >> testid=close-button' --expect-url '/conversations(\?|$)'
  12. Note
    on a fresh /launch?...,
  13. Do
    control-openhands browser press Escape
  14. Note
    also lands on /conversations.

Parameters #

  1. Do
    control-openhands browser goto '/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9jaXR5LXdlYXRoZXIiLCJyZWYiOiJtYWluIiwicGFyYW1ldGVycyI6eyJjaXR5IjoiUGFyaXMiLCJkYXlzIjozLCJtZXRyaWMiOnRydWV9fV0%3D'

    (city-weather with {"city":"Paris","days":3,"metric":true}) and

  2. Check
    control-openhands browser snapshot 'testid=plugin-launch-modal'
  3. Note
    : a city-weather section button, ref: main, path: plugins/city-weather, textbox city (Paris), spinbutton days (3) and a checked checkbox metric.
  4. Do
    control-openhands browser click 'testid=plugin-section-0'
  5. Note
    collapses it (browser count 'testid=plugin-0-param-city' is 0); click again to expand.
  6. Do
    control-openhands browser fill 'testid=plugin-0-param-city' Berlin
  7. Do
    control-openhands browser fill 'testid=plugin-0-param-days' ''

    (then control-openhands browser value 'testid=plugin-0-param-days' is 0) and

  8. Do
    control-openhands browser uncheck 'testid=plugin-0-param-metric'

    are accepted.

  9. Expect
    A link that mixes plugins with and without parameters (/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOk9wZW5IYW5kcy9leHRlbnNpb25zIiwicmVwb19wYXRoIjoicGx1Z2lucy9tYWdpYy10ZXN0In0seyJzb3VyY2UiOiJnaXRodWI6T3BlbkhhbmRzL2V4dGVuc2lvbnMiLCJyZXBvX3BhdGgiOiJwbHVnaW5zL2NpdHktd2VhdGhlciIsInBhcmFtZXRlcnMiOnsiY2l0eSI6IlBhcmlzIn19XQ%3D%3D) lists the parameter-less one under Additional Plugins.
  10. Note
    Leave with Escape; do not start this one (see Gotchas).

Dev format #

  1. Do
    control-openhands browser goto '/launch?plugin_source=github%3AOpenHands%2Fextensions&plugin_ref=main&plugin_repo_path=plugins%2Fmagic-test&message=hi%20QA'
  2. Check
    control-openhands browser snapshot 'testid=plugin-launch-modal'
  3. Note
    : Launch magic-test, paragraph hi QA, text magic-test OpenHands/extensions/ plugins/magic-test@ main.

Error states #

  1. Note
    For each of /launch, /launch?plugins=garbage, /launch?plugins=e30%3D ({}), /launch?plugins=W10%3D ([]) and /launch?plugins=W3sicmVmIjoibWFpbiJ9XQ%3D%3D ([{"ref":"main"}]) run
  2. Check
    control-openhands browser goto '<url>'
  3. Check
    control-openhands browser text 'testid=launch-error'
  4. Note
    Bare and [] read Unable to Launch / No plugins were specified. Please provide at least one plugin to launch.; the other three read The plugin configuration is invalid. Please check the URL and try again. (browser screenshot --feature F19.launch-errors --name invalid-format).
  5. Check
    control-openhands browser count 'testid=try-again-button'

    is 0 in all of them.

  6. Check
    control-openhands browser click 'testid=go-home-button' --expect-url '/conversations(\?|$)'
  7. Note
    leaves.

Creation failure #

  1. Do
    control-openhands browser goto '/launch?plugin_source=github%3AOpenHands%2Fextensions&plugin_repo_path=plugins%2Fmagic-test'
  2. Do
    control-openhands browser check 'testid=trust-checkbox'
  3. Do
    control-openhands service stop agent-server
  4. Do
    control-openhands browser click 'testid=start-conversation-button'
  5. Wait
    control-openhands browser wait 'testid=launch-error' --timeout 60000
  6. Check
    control-openhands browser text 'testid=launch-error'
  7. Note
    : Unable to Launch, Failed to Start Conversation, Go Home, Try Again (browser screenshot --feature F19.launch-creation-failed --name error).
  8. Do
    control-openhands browser click 'testid=try-again-button'
  9. Note
    reloads the page; with the server still down the app shows Manage backends with the Local backend Disconnected.
  10. Note
    Restore with
  11. Do
    control-openhands restart --timeout 240
  12. Do
    control-openhands browser reload
  13. Note
    : the review modal is back and
  14. Do
    control-openhands browser eval "document.querySelector('[data-testid=trust-checkbox]').checked"

    is false.

  15. Check
    control-openhands doctor
  16. Note
    before continuing.

Non-ASCII deep link #

  1. Do
    control-openhands browser goto '/launch?plugins=W3sic291cmNlIjoiZ2l0aHViOnFhL3LDqXBvIn1d'

    (UTF-8 base64 of [{"source":"github:qa/répo"}]) and

  2. Check
    control-openhands browser text 'testid=plugin-launch-modal >> h2'
  3. Note
    Expected Launch qa/répo; today Launch qa/répo.

Phone #

  1. Do
    control-openhands browser viewport phone
  2. Do
    control-openhands browser goto /plugins
  3. Check
    control-openhands browser bbox 'testid=skills-plugins-screen'

    (insideViewport true, pageHorizontalOverflow false) and

  4. Check
    control-openhands browser eval "(()=>{const s=document.querySelector('[data-testid=plugins-search-input]').getBoundingClientRect(), f=document.querySelector('[data-testid=plugins-status-filter]').getBoundingClientRect();return {searchBottom:Math.round(s.bottom), filterTop:Math.round(f.top), filterWidth:Math.round(f.width)}})()"
  5. Note
    : filterTop is below searchBottom (stacked) and filterWidth is 348.
  6. Do
    control-openhands browser screenshot --feature F19.phone --name list

    shows one column of cards.

  7. Note
    Open testid=plugin-card-city-weather and
  8. Check
    control-openhands browser bbox 'testid=plugin-detail-modal'

    (insideViewport true, browser screenshot --feature F19.phone --name detail), Escape;

  9. Do
    control-openhands browser click 'testid=plugins-add-plugin-button'
  10. Check
    control-openhands browser bbox 'testid=add-plugin-modal'

    (insideViewport true), Escape.

  11. Expect
    The parameters link from above gives
  12. Check
    control-openhands browser bbox 'testid=plugin-launch-modal'
  13. Note
    insideViewport true (browser screenshot --feature F19.phone --name launch).
  14. Note
    Hub entry:
  15. Do
    control-openhands browser goto /customize
  16. Do
    control-openhands browser click 'testid=extensions-mobile-hub >> testid=sidebar-extensions-/plugins' --expect-url '/plugins(\?|$)'
  17. Check
    control-openhands browser click 'testid=sidebar-mobile-back-button' --expect-url '/customize(\?|$)'
  18. Note
    Return with
  19. Do
    control-openhands browser viewport desktop

Errors sweep #

  1. Check
    control-openhands browser errors --app-only

    shows no page errors; the only app HTTP errors are the deliberate ones (refresh 500, install 400, 502s while the Agent Server was stopped).

Gotchas and known limits

  • The card switch swaps its icon on hover. A plain browser click that moves the pointer onto it from elsewhere (for example right after clicking a filter segment) is often swallowed (#17941): no PATCH /api/plugins/installed/<name> shows in browser network. Run browser hover on the switch first, then click, and assert aria-checked and the API, not the click result.
  • Installed plugins lose their source and their enabled state, intermittently (OpenHands/software-agent-sdk#5496). The Agent Server rewrites <run>/private/plugins/installed/.installed.json with a plain truncate-and-write on every load (SDK InstallationMetadata.save_to_dir), so a concurrent GET /api/plugins/installed (page loads fire several) can read an empty file: it logs Failed to load installed extension metadata … Invalid JSON: EOF while parsing and Discovered untracked extension, then saves every plugin with "source": "local", "enabled": true and a fresh installed_at. It hit catalog installs in most trials and a disabled plugin on 2 of 3 page revisits; GitHub installs kept their source in this run. Once it hits, the card shows local, Update fails with a 500, Start Conversation builds an unusable /launch link, and a disabled plugin silently auto-loads again. Prove the launch path from a card that is not installed, and re-read the API after every navigation.
  • A failed plugin install, update or uninstall shows one toast with the server's reason (enable uses the same handler; its failure is not driven here). 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; read the cause in the run's private/stack.log.
  • Installing a plugin whose name is already installed should point the user to Update or Uninstall, as Apps do (F20.install-duplicate). Known failure (reproduced 2026-10-06): with the catalog magic-test installed, Add plugin with github:OpenHands/extensions and Path plugins/magic-test answers 409 and toasts the server's Plugin already installed. Use force=true to overwrite., which the form cannot do (browser screenshot --feature F19.add-plugin-error --name duplicate-409) (#18090).
  • Uninstall acts immediately, with no confirmation (MCP server delete and App uninstall confirm).
  • /launch with a message starts the agent as soon as Start Conversation is clicked. Prefer links without a message and send alakazam yourself; a city-weather launch with a message makes the agent call an external weather API (stop it with testid=stop-button).
  • Edited launch parameters cannot be observed after creation: the conversation's client metadata (localStorage['openhands-agent-server-conversation-metadata']) strips them and the Agent Server does not return a conversation's plugins. Proving delivery needs request-body capture (harness gap).
  • conversation start and launches leave the browser on a conversation page, which has its own chat-input; browser goto /plugins before the next card step.
  • Build deep links with URL-encoded base64 (%2B, %2F, %3D): a raw + decodes to a space and gives the invalid-format error. Non-ASCII text must not be used in payloads (atob decodes bytes as Latin-1).
  • The page loads three queries (marketplace, installed, local) behind skeleton rows; wait for testid=plugins-toolbar before counting cards. With no plugins at all the page shows No plugins found. (plugins-empty), which the bundled catalog makes unreachable; on a Cloud backend Add plugin and card actions are disabled (not driven: no Cloud account).
  • Local plugins under ~/.agents/plugins appear on the next page load without a restart; the Agent Server's HOME is <run>/private/home.

Source paths: src/routes/skills-plugins.tsx, src/routes/launch.tsx, src/components/features/plugins/, src/components/features/launch/, src/utils/plugin-launch-url.ts, src/hooks/mutation/use-install-plugin.ts, src/hooks/mutation/use-set-plugin-enabled.ts, src/hooks/mutation/use-uninstall-plugin.ts, src/hooks/mutation/use-refresh-plugin.ts.