Repository navigation
WebUI hosting for Mini Apps (ADR 0020) #251
Description
Problem Statement
A person using the WebUI cannot use Mini Apps at all. The Mini App packages installed on the machine
are the generation the desktop client ships, and in the desktop client they list, open, render and
talk to their host. In the WebUI the same installation is invisible: the runtime refuses to start its
Mini App service for this client's runtime owner kind, there is no client entry to list or open one,
no view to render one, no proxy to reach its page and no page bridge for that page to call. A Mini App
someone installs, or builds, simply does not exist here.
That gap matters more than a missing menu item. A Mini App is a first-party plugin page whose whole
value is the page: a dashboard, a work surface, an app the person built for themselves. Half a
capability — a listing that cannot open anything — is worse than none, because it reports that the
thing exists and then refuses.
Solution
The WebUI hosts Mini Apps the way the desktop client does, and an installed Mini App renders and works
here as it does there.
A Mini App's page renders in a tab of the workspace panel, or alone in a browser tab of its own, loaded
from a loopback origin the service proxies. The proxy injects the credential the Mini App's own server
demands and the page bridge the page calls, so the page behaves as its author wrote it. The runtime's
Mini App service is enabled for this client's runtime owner, so the supervisor, the plugin package
reader and the session lifecycle all come alive for the WebUI process.
The plugin catalogue's apps area becomes the place a person sees what is installed: each Mini App with
its runtime state, and the lifecycle actions the rest of the catalogue already has — install,
uninstall, enable, disable — plus stop and open. A launcher tile in the workspace panel opens a picker
for the quick path. Capacity is visible: three Mini Apps run at once, and a fourth open names what is
running and lets the person stop one.
An open Mini App can be moved into its own browser tab, and the client remembers which presentation is
live so the inline view can say it is open elsewhere. A Mini App page can append a draft to the
composer and can run or answer an agent turn, including permissions and questionnaires, so parity with
the desktop client extends past rendering to the work a page does.
User Stories
- As a WebUI user, I want my installed Mini Apps to appear in the plugin catalogue's apps area, so that
I can see what I have without asking the runtime. - As a WebUI user, I want each listed Mini App to show whether it is stopped, starting or running, so
that I know what opening it will cost me. - As a WebUI user, I want to open a Mini App into the workspace panel of the session I am in, so that I
can use it beside the conversation that needs it. - As a WebUI user, I want to open a Mini App from a launcher tile and a picker, so that the common case
does not require going to the plugin catalogue. - As a WebUI user, I want the picker to open the Mini App I chose and nothing else, so that choosing one
does not disturb the tabs I already have. - As a WebUI user, I want to move an open Mini App into its own browser tab, so that it keeps rendering
while I work in another session. - As a WebUI user, I want the inline tab to tell me that the Mini App is open in its own tab, so that I
do not open a second copy by accident. - As a WebUI user, I want closing that browser tab to release it, so that the state does not claim a
presentation that no longer exists. - As a WebUI user, I want a Mini App's page to render as its author built it, so that I do not get a
degraded or restyled version of the desktop experience. - As a WebUI user, I want to stop a running Mini App, so that I can free one of the three slots.
- As a WebUI user, I want the capacity refusal to name the running Mini Apps, so that I know which one
to stop instead of guessing. - As a WebUI user, I want a stopped runtime to clear the page and tell me why, so that I am not looking
at a frozen document. - As a WebUI user, I want the view to reconnect only when I ask, so that closing a laptop lid does not
restart a Mini App behind my back. - As a WebUI user, I want a Mini App opened by a deep link to show only that Mini App, so that its own
browser tab is the app and not the whole client with something in the middle. - As a WebUI user, I want that deep link to survive the session ending, so that the Mini App still
works when the conversation that opened it is done. - As a WebUI user, I want to append a draft from a Mini App page into the composer, so that I can carry
its output into a message without copying and pasting. - As a WebUI user, I want that append to ask me first, so that a page cannot write into my input area
silently. - As a WebUI user, I want a Mini App page to run an agent turn, so that the app can use the agent the
way it does in the desktop client. - As a WebUI user, I want to answer a permission request raised by such a turn, so that the turn can
proceed. - As a WebUI user, I want to answer a questionnaire raised by such a turn, so that the same holds for
the richer prompt. - As a WebUI user, I want the page to show the turn's progress, so that I can tell a slow turn from a
stuck one. - As a WebUI user, I want a Mini App to be able to show a session it created, so that I can watch and
answer that turn. - As a WebUI user, I want that request to bring the session in front of me rather than fail, so that
the turns a Mini App starts are answerable instead of silently denied. - As a WebUI user, I want to keep the page on screen while I answer such a session, so that I can see
what the turn is doing to the app. - As a user with both clients installed, I want the second client to tell me a Mini App is already
running in the other one, so that I am not left with a silent failure or a second process. - As a user with both clients installed, I want that refusal to leave the Mini App usable in the client
that owns it, so that opening one here does not break it there. - As a security-minded user, I want a Mini App's loopback server to reject requests that do not carry
the credential, so that another local process cannot drive it. - As a security-minded user, I want the credential to stay on the server side, so that no page and no
browser tab ever holds it. - As a WebUI user, I want a package written for the newer contract to install and list, so that an app
built for the desktop client is not rejected before it can run. - As a WebUI user, I want a Mini App that cannot be started to fail with a reason I can read, so that I
am not left with a blank tab. - As a WebUI user, I want the composer confirm dialog to keep my unsent draft intact when I decline, so
that a declined append costs me nothing. - As a WebUI maintainer, I want all of this to live in one business, so that Mini App state does not
spread into the plugin catalogue or the workspace panel.
Implementation Decisions
The contract. Take the shipped generation of the Mini App contract, minus its window block: the
window block is the page asking for its own native window, and this client decides presentation itself.
The remainder arrives as one contiguous omission rather than hand-picked sections. The package manifest
reader must accept the three keys the newer generation declares — surface.window (read and ignored,
because its effect was dropped), agentAccess and agentConcurrency (validated here, enforced by the
agent service) — or a package built for the desktop client fails the reader before any of this runs.
The credential guard. The guard the desktop client runs inside the Mini App's own process is part of
this port, not an option: the service is enabled here, so the exposure is new, and the port is derived
from the plugin id, so a local process can compute it rather than scan for it. Readiness stops meaning
"the socket accepts" and starts meaning "anonymous is rejected, credentialed is answered".
The bridge. The page cannot be scripted from its parent, so the proxy rewrites its document and
injects a browser build of the same bridge; the page's calls then travel to the client over
postMessage, with the client validating both the source frame and its origin. The alternative — the
proxy exposing bridge endpoints on the Mini App's own origin — is rejected and recorded, because after
the guard strips the credential those endpoints would carry no identity and any local process could
drive a host-authorised call through them. The bridge implements the message and agent parts; the
window handle is absent because the contract omits it.
Runtime seams. The Mini App service becomes a cell of the runtime owner policy, in the wiring group,
stated as a literal per row — an owner that declared no kind must not silently get the service. The
resolver keeps the explicit-options override it already has. The client's entry is a forwarding facade
over the Mini App application, and only its open and stop are new: the listing already crosses the
management operation's apps action. The application gains a start that opens no page. The presenter is a
host-owned port the service implements; its open means the client should show that Mini App. The cell,
the presenter and the view belong to one change, because flipping the cell alone leaves the agent tool
able to start a Mini App that nothing can display.
The agent capability. It is in scope, because parity is the acceptance standard and a page probes its
bridge when it loads. The shipped runtime's agent service — arenas, queues, turn lineage, session ports
and their projection, and the application that fronts them — is ported with its suites. The connector's
frames move off the child's standard streams onto the IPC channel that already carries lifecycle, told
apart by message type, with the child's stdin closed and stdout and stderr becoming logs. That move is
forced, not chosen: the connector currently shares stdout with whatever the plugin logs, and an
agent-enabled plugin logs. A page's request to show one of its sessions maps to the session rail: the
client selects that session, reports it as shown while it is selected, and reports it closed when the
person leaves, because visibility is what makes that session's permissions answerable.
The proxy. One loopback listener per running Mini App, not one origin with a path prefix: the page
refers to its own origin by absolute path, and rewriting those URLs means intercepting its fetches. Paths
pass through unchanged, the credential is injected outbound, the document is rewritten inbound, and
WebSocket upgrades are forwarded under the same rules. The listener's port is stable per plugin, because
the page's localStorage and IndexedDB are keyed by origin.
The client. One business owns the view, the picker, the apps list, the presentation lease and the
host side of the bridge. The workspace panel gains one tab kind and mounts the published view; the plugin
catalogue mounts the published apps list and holds no Mini App state; the business dispatches the panel's
own tab command rather than reaching into its state. A tab's existence stays in the panel state, and the
runtime and bridge facts about it stay in the Mini App business. The view URL is produced by the service
and returned by the open operation, so no client ever assembles a Mini App origin or carries its
credential.
The standalone presentation. A client route selects a session and a Mini App and renders only the
view; the service maps that path to the same document, so the tab is an ordinary client. The client's own
inline tab carries the action that opens it, since nothing else can produce a link a person would not
type. The service holds the presentation lease: a standalone client declares its plugin, session and
presentation when it connects, the record is released when its socket closes, and the inline view reads it
to say the Mini App is open elsewhere. That record is what gives the contract's detached flag and
presentation revision meaning here.
Ownership across clients. One client owns a Mini App at a time, per plugin. The owner is recorded in
the shared data directory with a liveness proof the operating system releases when the owner dies; the
shipped desktop client writes no such record, so the signal for anything already running is the Mini
App's own derived port answering the way the guard answers. The second client reports the owner and
refuses to start, and accepts that it cannot display a Mini App the desktop client is running.
Glossary. The Mini App bridge no longer describes a window handle, since this client has none; the
Mini App view records that both presentations belong to the client; and the presentation lease is defined.
Testing Decisions
What makes a good test here. Only external behaviour: what a person can do and what the client shows.
A test asserts the observable outcome — a rendered page, a row with a runtime state, a refusal with a
reason — not the presence of a test id or the shape of a reducer's internals. Interaction coverage takes
the repository's two sanctioned shapes: extract the transitions into exported pure functions and test
those, and assert render branches through static markup. What neither can reach belongs in the browser
suite or in the report's untested-boundary section.
One new seam. A synthetic Mini App package fixture, seeded into the isolated temporary data
directory the browser suite already uses. Its package shape follows the scaffold the runtime ships
(page entry, node server, manifest surface path), it is small enough to read, and every surface in this
spec is exercised against it rather than against a mock. Everything else reuses existing seams.
Suites and their prior art.
- The browser suite, one spec per surface, is the acceptance seam for the user-visible paths: inline
render, standalone, composer append, capacity refusal, ownership refusal, and an agent turn with a
permission and a questionnaire. Prior art: the existing browser specs and their shared harness. - The level-three scripted leaf behind the harness port covers the new operations and the states a
browser run cannot induce reliably — capacity busy, ownership refusal, lease release. Prior art: the
loopback harness path suite. - Unit suites cover the pure parts: the panel reducer's new tab kind and its command, the launcher tile
table, the proxy's origin and port derivation, the bridge's request parsing and validation, and the
lease's transitions. Prior art: the existing panel and port unit suites. - The ported runtime suites arrive with their code; they must be registered in the repository's declared
suite inventory, or they never run. The Mini App service carries no tests here today, so adopting them
is cheaper than writing new ones.
What each seam cannot prove. The browser suite proves rendering and the bridge against a real
runtime host and a real child process on this platform; it does not prove the desktop client's behaviour,
external provider execution, or anything about a Mini App running in the other client.
Out of Scope
- The page-side window handle and the native window options it asks for: no pop-out bounds, always-on-top,
click-through, transparency, frameless or skip-taskbar behaviour. - Displaying, in the WebUI, a Mini App the desktop client is running. The credential that reaches it never
leaves that process, and this spec chooses refusal over a cross-process credential channel. - A second conversation surface beside the chat, so that a Mini App's session and the page are in one
window. The rail plus the standalone tab is this spec's answer. - Any new authoring flow: building, publishing or sharing a Mini App beyond the plugin actions that
already exist. - Prewarming a Mini App from the picker before it is opened.
- Agent catalog bridge methods the desktop client exposes that this client's UI has no home for.
- Mobile and narrow-viewport layouts for the view, the picker and the apps list.
Further Notes
The authority is ADR 0020 in its revised form: every section states a decision except its two open
implementation questions — the response headers the proxy needs so the page can be framed at all, and the
bridge's method-level error mapping. Those two are settled during implementation and written back into
that record, not decided here.
Two risks a reviewer should weigh. The connector migration touches the channel every Mini App tool call
uses, so a defect there is not confined to agent features. And the app's page is a foreign document this
client frames without a sandbox attribute, with origin separation as its only isolation — the opposite of
the workspace file preview, and deliberate.
Related records: ADR 0006 (the shared data directory), ADR 0016 and ADR 0017 (business modules, ownership
and delivery), ADR 0019 (the browser panel this view's mounting follows), and the glossary in
CONTEXT.md.
Activity