Skip to content

[FEATURE]: Plugin UI surface for the web/desktop app, mirroring the TUI plugin API #43132

Open

Description

@davidprokopec

The gap

V2 has a rich client-side plugin surface for the TUI (@opencode-ai/plugin/tui): Plugin.define({ id, setup }) with keymap.layer (slash commands with run(input) that execute before any model dispatch), ui.dialog (show/select/prompt/confirm), ui.slot (e.g. sidebar.content), host-reactive storage, and client. The loader resolves package plugins through the exports["./tui"] subpath.

The web/desktop app (packages/app) has no equivalent: no plugin runtime, no slot system, no client-side command hooks. The Extensions settings tab (packages/app/src/components/settings-v2/extensions.tsx) is a read-only list of server config. So any plugin UX built for the TUI simply cannot exist in the web or desktop clients.

Concrete use case

opencode-waits schedules a prompt for later (/wait 1hour implement this). Its whole point is working while rate limited, so scheduling must not cost a model turn. In the TUI that works: a plugin slash command intercepts before submission, plus a sidebar section with live countdowns and a manager dialog (list/search/add/edit/delete/send-now) built from ui.dialog.show + keymap layers.

In the web/desktop app the only available fallback is a server-side command template that instructs the agent to call a scheduling tool — one model turn per use, and unusable exactly when rate limited, which is the scenario the feature exists for.

The ask

A plugin UI surface for packages/app, even a small subset of the TUI's:

  1. Client-side slash commands with a run(input) executed before prompt submission — this alone unlocks model-free plugin commands.
  2. Dialogs (the select/prompt/confirm primitives).
  3. A sidebar slot.

A natural shape would be an exports["./web"] entrypoint mirroring the ./tui loader convention, with a context exposing the same client/storage/ui capabilities adapted to solid-on-DOM.

I understand the V2 plugin API is beta and moving; happy to adapt the plugin to whatever shape this takes, and to test early iterations.

Activity

  1. github-actions commented on Aug 17, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. slkiser commented on Sep 26, 2026

    @slkiser

    +1 from opencode-quota (~980 stars). In the TUI we show quota in a sidebar slot and toasts. On Web/Desktop plugins get no slot and no toast. Even just the sidebar slot and toasts on the web app would cover us.

  3. iceteaSA commented on Oct 18, 2026

    @iceteaSA

    +1, with a different use case and a checkout-level map of what exists today, in case it helps scope the first cut.

    We're porting a set of plugins to 2.x that have to behave the same in the TUI, the web app and desktop. Mapping 2.0.31 (83c5abbbe3):

    • The only client plugin surface is packages/plugin/src/tui/ (exports["./tui"], packages/plugin/package.json:12-18): slots (context.ts:180-203), dialogs (:327-405), keymap slash commands (:407-433), toasts (:490-542).
    • packages/app has no plugin runtime, slot, dialog or command hook at all. Desktop renders the same app, so it inherits the gap.
    • What every client does render for a plugin: session messages, host-native permission asks, and the question tool. That's the whole cross-client surface right now, and none of it can run plugin code before a model turn or collect input outside the transcript.

    Two things we need that the fallback (a command template plus a tool) can't do:

    1. An operator action that must not be a model turn and must not land in the transcript. Ours is a sudo askpass flow: the plugin rewrites the shell call to sudo -A, the askpass helper asks the operator for the password, the password goes straight to sudo. In the TUI we built the modal ourselves. On web/desktop there is no plugin path to ask the operator anything, so the feature has to refuse instead of prompt.
    2. An operator broadcast to other sessions on the same service (explicit message or a scheduled reminder). Again a plugin command with run(input) plus a small form, nothing a model should be involved in.

    Minimum subset that would cover both, in order: slash commands with run(input) executed before submission (already the top ask here), dialog.prompt and dialog.confirm, a toast. A sidebar slot would be nice but neither of the above needs it. We'd test an early iteration.

  4. magoz commented on Oct 27, 2026

    @magoz

    Desktop GUI plugins shipped in v2.2.0 (#55246, #59469), announced here: https://x.com/LukeParkerDev/status/2115009131535085782. That covers the desktop half of this issue.

    The web app (opencode serve / opencode web) still loads no third-party GUIs: createWebPlatform sets no plugins bridge, so PluginGuiLoader never mounts (packages/app/src/runtime/plugin/root.tsx). The SDK already has ctx.app.platform: "web", so it looks designed for it.

    Is web still planned under this issue? If so, a possible approach:

    • Load GUIs only from the server that served the page (same origin, which already runs the plugins' server parts). Never in the hosted app.
    • Serve each bundle as a same-origin script (e.g. /api/plugin/gui/<id>.js), since the web CSP (script-src 'self') blocks the new Function evaluation desktop uses.
    • Ipc, embeds and menubar stay inactive, as the docs already describe for web.

    Use case: I embed the OpenCode web UI in a dashboard and drive its tabs, keys and composer through internals today. ctx.tabs, Command and composer would replace all of that.

    Happy to open a PR if the approach fits.

  5. Hona commented on Oct 27, 2026

    @Hona
    Member

    Yeah so 'remote servers' are on the list to figure out

    Which sorta means for web too :)

  6. magoz commented on Oct 28, 2026

    @magoz

    Makes sense, thanks! Are you open to contributions? Happy to help :-)

  7. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed write-up and the concrete use case. You're asking for a plugin UI surface in the web/desktop app with client-side slash commands, dialogs and a sidebar slot, so plugins like opencode-waits work there without spending a model turn.

    Much of this already exists for OpenCode Desktop as an experimental GUI plugin API (@opencode/plugin/gui, available since v2.2.0). A package exposes it through a ./gui-experimental export next to ./tui and is listed in opencode.json(c) like any other plugin. The docs are under Build › Plugins › GUI (source: services/www/src/docs/content/build/plugins/gui/index.mdx). It includes:

    • Slash commands: ctx.add(Command, () => ({ id, title, slash: { name: "wait", arguments: true }, run: (input) => ... })). With arguments: true, the text after /wait is passed to run(input).
    • Dialogs: ctx.dialogs.confirm/alert/prompt/select use the app's own dialogs, and ctx.dialogs.open(render) with ctx.ui.Dialog lets you build a custom manager dialog.
    • UI surfaces: Panel tabs in the session side region (a good place for a live countdown list), plus Slots at window.bottom, session.header, session.location.menu and session.panel.end.
    • Data and feedback: ctx.client, stores and toasts.

    What isn't there yet, as far as I can see:

    • A plain browser tab doesn't load third-party GUI plugins; only the desktop app does.
    • There is no dedicated session-sidebar slot like the TUI's sidebar.content.

    The API is experimental, so its shape may still change. It would help if you could try porting opencode-waits to ./gui-experimental on Desktop. Please confirm here whether a slash Command stays fully client-side for your case, and say which remaining gaps block you.

    I've noted this for the maintainers to consider.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions