Repository navigation
feat(plugins): offer one line of composer suggestion from a plugin - #1785
Merged
Merged
Conversation
added 2 commits
October 20, 2026 18:02
A plugin that reacts to a finished turn often knows what the user plausibly types next, but every surface it has today interrupts: a toast, a notification, or a rewrite of a draft the user already wrote. Add `pi.composer.suggest` and `pi.composer.clearSuggestion`, gated by the new `composer.suggest` permission, which put one dismissible line into the placeholder layer of the Composer for the session on screen. The decision is split along the ownership the host already has. The main process refuses a session that is not the one on screen and a turn that a newer turn overtook, because it owns both facts; the renderer refuses a draft that is not empty, because only it owns the draft, and answers the waiting caller. A plugin therefore learns `shown: false` with a reason instead of guessing, which is what lets it stop spending completions on a user who is not looking. Main never writes renderer state: it sends a message, like `pi.ui.revealView` does. Nothing a plugin offers becomes draft text on its own. `Tab` writes the line into the draft with the caret at the end, `Esc` drops it, and typing, sending, a session switch, a new turn, a plugin stop, a reload or a window close all end it; the `@` / `/` completion list keeps `Tab` and the first `Escape`. See ADR 0332.
The suggestion bridge kept the composer handle inside its live-draft value, but it already reaches the newest handle through its own registry. Drop the duplicate so the value describes only what the decision needs: the session, the text and how many references the draft holds.
React requires a third snapshot when a component is rendered to a string, and the app-shell snapshot fixtures render the Composer that way. Without it React threw "Missing getServerSnapshot" and the three shell-render tests failed on CI even though the browser path was fine. A suggestion is a live-window fact, so the server snapshot is simply "none".
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a plugin-side channel for one short suggestion in the Composer, so a
plugin can propose the user's next message where the user is already looking,
without a toast, a notification, a bubble, or a panel.
What a plugin gets
Permission:
composer.suggest, risk medium (confirm at install). It canput text in front of a user who may accept it with one key, but it cannot read
the draft, cannot send, and cannot outlive an empty draft — the same trust shape
as
composer.transform.A place that will not show the line is not an error: the call resolves
{ shown: false, reason }so a plugin can log the miss. Only malformed inputthrows (
INVALID_PARAMS), and a missing grant throwsPERMISSION_DENIED.How the decision is split
sessionIdthat is not the sessionon screen, and a
turnIdthatonTurnStartedhas since overtaken. It thenrelays the offer and waits (2 s) for the renderer's answer.
text or an attachment — and answers over
pluginComposerSuggestionResult, sothe plugin learns the real outcome.
Main never writes renderer state; the offer is a message, exactly like
pi.ui.revealView.Lifecycle
Shown only while: the named session is on screen, the draft is empty (no text,
no attachment), the named turn is still the session's newest, and a composer
takes input. One suggestion per session; a newer offer replaces the older one.
Cleared by: any draft change (typing, pasting, an attachment,
composer.insertText,composer.replaceDraft), a send, a session switch, a newturn starting, the plugin unloading/being disabled,
clearSuggestion, arenderer reload, a window close, and the suggestion's own TTL.
Keys:
Tabaccepts (draft = text, caret at end, input focused, stagedattachment kept),
Escdismisses, and the@//completion list keepsTaband the first
Escape.Visual: the existing
.composer-placeholderlayer, brighter (62% primary ink),one line with an ellipsis, plus a localized
Tab/Eschint and one politesr-onlyannouncement.aria-placeholderis unchanged.Compatibility
Additive: new permission, new SDK namespace, two new IPC channels, one i18n key.
A plugin that does not declare
composer.suggestbehaves exactly as before. Nomanifest schema version, host-core schema, or persisted-format change.
Verification
apps/desktop/test/composer-suggestion-state.test.mjs— renderer statemachine (empty draft, session match, attachments, accept/dismiss, typing,
session switch, TTL expiry, host clears, key arbitration with completions).
apps/desktop/test/composer-suggestion-relay.test.mjs— main relay (sessionvisibility, stale turn, replacement, timeout, turn start, plugin stop, quit).
apps/desktop/test/plugin-composer-suggestion.test.mjs— real plugin processcalling
pi.composer.suggest/clearSuggestion: text shaping, TTL clamp,scope refusal,
PERMISSION_DENIEDwithout a grant,INVALID_PARAMS.apps/desktoptypecheck;packages/{shared,plugin-sdk,i18n}builds andvitest suites;
plugin-runtime-risk(permission matrix parity),main-wiring-contract, composer/plugin suites; biome lint; style-token guard.See
docs/adr/0332-plugin-composer-suggestion.md.