Repository navigation
Conversation
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
There was a problem hiding this comment.
The slot works as described. I ran the new component story and the rest of plugin-host.spec.ts (40/40) in headless Chromium, plus the session-ui unit tests (561 pass). Type checks pass on app, session-ui and plugin. With no plugin, rows build nothing; with one, footers show live input, a throwing contribution fails only its own plugin, and the response footer follows the newest step and flips completed when the response finishes.
Public API: additive to the experimental GUI plugin API. It adds the session.message.footer key to SlotMap, with input { screen, session, active, message }, and exports two new types, TimelineUserMessage and TimelineResponse, from @opencode/plugin/gui. In the app, PluginLinks now requires a screen prop (internal). Nothing existing changes, so it is backwards compatible.
One gap worth fixing: the response footer is anchored on the last model-output row only (details inline).
| ) | ||
| } | ||
|
|
||
| // The response's footer renders under its last part row, and moves to each new one while the response streams. |
There was a problem hiding this comment.
The footer only attaches to the last AssistantPart row, because lastAssistantGroupKey only tracks those rows. That leaves two cases the slot docs don't cover:
- A response that fails or is interrupted before writing any output (only an
Errorrow) gets no footer at all. A plugin can't show that response'serroror cost, even though the docs say the slot is under each response and thatcompletedflips on failure. - When parts are followed by an
Errorrow, the footer sits above the error card instead of under the response.
Anchoring on the last row of the turn (Error included), or documenting that only responses with output get a footer, would close this.
Adds a GUI plugin
Slotatsession.message.footer: a line under each user message and each assistant response in the session timeline, for per-message details such as cost, tokens, the model, or a link.screen, the timeline'ssession,active(assession.header), andmessage:{ role: "user", info }or{ role: "assistant", promptID, messages, completed }.infoandmessagesare the server's ownSessionMessageUser/SessionMessageAssistantobjects, so a plugin readstokens,cost,model,content,finish,errorandtimewithout a new data model. Every field is a live getter.completedflips once the model stops (finished, failed or interrupted).footers.enabled()) inside the key memo they already have, and build nothing else: no component, subscription or DOM node. When a plugin fills the slot, the mounted rows mount again with footers; only rows the virtualizer mounts render them; empty content isdisplay: none; height changes reach the row's ResizeObserver. Nothing is reserved.Contribution: a throwing render records a plugin failure and hides only that contribution.session.*because the slot lives on the session screen and gets itsscreen, aspackages/gui-plugins/AGENTS.mdrequires ofsession.*slots. It pairs withsession.message.menufrom the message action row PR../gui-experimental), and nothing changes until a plugin fills the slot.packages/gui-plugins/AGENTS.md("Never add work to timeline rows") and theContributiondoc inrender.tsxnow state this exception exactly. Docs (Slots section with a "Message footers" subsection and reference), README registry row, and full TSDoc (sdk-docs: 0 problems).Sample plugin
A third-party local-folder plugin,
opencode-response-receipt, built with the published@opencode/pluginand its@opencode/plugin/gui/vitepreset (vite build→dist/gui-experimental.cjs, 4.4 kB). It shows the model, tokens in and out, the cache share, the step count and the cost under each response, with a per-step breakdown on hover, and renders nothing under user messages. The screenshots load its built bundle through the app's own bundle evaluation (pluginDefinition→evaluatePluginBundle), from a temporary, uncommitted builtins entry, because local plugins load only in Desktop. Data comes from the e2e mock server.Hovering a receipt:
Sample layout and
src/gui.tsxPerformance
strobe refused this machine (session locked, 56% background CPU load), so this is an alternating A/B instead: the production
SessionTimelineinside the real plugin host andPluginLinks, 300 exchanges (600 message rows, all mounted, no virtualizer: the worst case), headless Chromium. Seven rounds in alternating order (dev, PR, PR with one footer contribution), three warm mounts each; time is the synchronous mount plus a forced layout.devVisual
Addresses #61385 (per-message plugin UI, alongside the message action row) and #53019 (plugins showing their own per-message details without patching the timeline); replaces the timeline DOM injection community plugins do today.
Added by the opencode bot (AI-generated).
Related: #61385, #53019
Public API: adds the
session.message.footerslot and theTimelineUserMessage/TimelineResponsetypes to the experimental GUI plugin API (backwards compatible)