Support client-local UI extensions in daemon-backed interactive chats #1462
Replies: 3 comments
|
Supporting this from a concrete use case, and adding one measurement that may help scope it. We ship the same build-orchestration tool as an opencode plugin and as a prime extension. In opencode the plugin The other half is placement, which I have opened separately as One note in support of your Verified on 0.7.2 ( |
|
In short, i'm trying to build voice-mode into this harness. |
|
Same gap, one slot further: extension tool renderers ( What we hit. Our extension (a connector that registers six custom tools) implements Why it happens (traced through 0.8.0):
Net effect: for extension tools the panel header ( Ask: include tool renderers in the client-local (Precedent for the split execution model: #1057 / PR #1071 kept shortcut handler execution in the daemon while proxying the UI trigger — tool renderers are the mirror image, UI-only code that should live entirely client-side.) |
Uh oh!
There was an error while loading. Please reload this page.
Problem
Prime Agent 0.7.2 cannot run custom editors, raw terminal-input handlers, or autocomplete providers from discovered extensions in a normal interactive chat. The active extension runner is in the daemon worker, but these callbacks need the terminal client's live TUI objects.
This is the gap reported in issue #734. The issue was closed during the discussion-first backlog cleanup, not because the behavior was fixed. Release 0.7.2 and current
mainstill contain the no-op bindings.A minimal reproduction is:
session_starthandler that callsctx.ui.setEditorComponent(factory)andctx.ui.onTerminalInput(handler).ctx.ui.getEditorComponent().The context reports
hasUI: true, but the getter returnsundefinedand neither callback runs.dist/modes/daemon/daemon-extension-bindingimplements these APIs as no-ops. Process-localextensionFactoriesmake the real TUI callbacks available, but deliberately select the reduced in-process path instead of preserving daemon-backed agents and attach/resume.There is a related complication. Daemon-client startup prepares local runtime services and imports discovered worker extension modules, even though
InteractiveModenever binds that local runner or emits its lifecycle events. The worker imports them again and owns the active runner. A fix should stop importing worker entry points in the client, not turn this existing double execution into a supported contract.Suggested direction
Add an explicit client-local UI entry point. Keep the current worker entry point for tools, providers, agent events, and session-owned behavior.
One possible package contract is:
{ "pi": { "extensions": ["./dist/worker-extension.js"], "uiExtensions": ["./dist/ui-extension.js"] } }A package could declare either entry point or both. Prime would not infer a UI entry point by executing every worker extension again.
For a first version, I would limit the client API to:
Serializable notifications, status text, widget lines, titles, and dialogs already use daemon UI requests. They do not need a second path.
A UI entry point needs a smaller API than a worker extension. It should not register tools, providers, prompt transforms, or agent-event hooks. A separate
UIExtensionAPItype would make the boundary clear, but it would not be a sandbox: local extension JavaScript still has the client's Node, filesystem, process, and network privileges.UI-local commands must intercept input before
AgentConnection.prompt(). The daemon command list is a catalog; its handlers execute in the worker after submission. The client therefore needs a small, namedAgentConnectionfacade rather than a forwardedExtensionCommandContext, whoseSessionManagercallbacks are worker-only. The first version could reject UI/worker command name collisions rather than add another priority rule.Discovery and trust
I see two viable same-host options:
uiExtensionsfrom local user, project, and package configuration.uiExtensionsresource collection, and the client loads only local files allowed by its trust policy.The existing
AgentConnectionResourceSnapshot.extensionslist contains successfully loaded worker extensions, so it cannot represent a UI-only package. Its artifact references are metadata; they provide neither bytes nor integrity digests.Remote or gateway-selected UI code should be out of scope initially. Automatically executing code chosen by a remote peer creates a remote-code-execution boundary. Remote support would need explicit client trust, authenticated artifact transfer, integrity metadata, and visible provenance. Snapshot membership alone is not authorization.
Prime should not transmit keystrokes or draft text implicitly. A trusted local extension can still make explicit connection calls, as other extension code can.
Lifecycle
On attach or reload, the client would bind the real TUI context and start the active local UI entries. On detach, session replacement, resource change, or terminal shutdown, Prime should await bounded cleanup, dispose host-tracked registrations, restore the stock editor, and drop the runner reference. The extension remains responsible for global timers and other effects that it created outside the host API.
Tree navigation should not reload UI code unless the resource set or working directory changes. Daemon-owned and client-owned UI state also need separate ownership, because current status/widget keys do not carry an extension-source owner.
Costs
This adds package and settings discovery. A daemon-described option also adds protocol surface. Authors may need split worker/UI packages or shared initialization code. Client and worker versions can disagree about capabilities. Prime must also stop client startup from importing worker entry points that only the daemon should execute.
These costs seem preferable to serializing executable callbacks, executing every extension twice, or relying on a version-coupled terminal proxy.
Initial proof
A focused implementation should prove that:
Open questions
Would maintainers accept this separate-entry-point direction for a same-host first version? If so, the next step could be a maintainer-scoped issue that settles these questions before implementation.
All reactions