Uh oh!
There was an error while loading. Please reload this page.
fix(tui): render plugin slots and routes with component semantics - #39917
Merged
Conversation
Uh oh!
There was an error while loading. Please reload this page.
github-actionsBot
pushed a commit
to ReStranger/opencode
that referenced
this pull request
Jul 31, 2026
* upstream/v2: fix(tui): render plugin slots and routes with component semantics (anomalyco#39917) refactor(ai): simplify provider options (anomalyco#39924) refactor(core): rename attachments config to media (anomalyco#39927) feat(tui): expand pasted text (anomalyco#39920) feat(ai): expand OpenRouter native support (anomalyco#39907)
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 freeto 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.
What
Plugin slot and route render functions were invoked as bare function calls inside the host's tracked reactive scope. Any reactive read in the returned JSX subscribed the host's resolution memo, so an update re-executed the plugin's entire render body — not a fine-grained update.
Before / After
Before: a plugin slot like
renders
0forever. Instrumented sequence: body runs →tick()read subscribes the host memo → interval fires → memo re-runs the whole body → new signal at 0, old interval cleaned up → repeat. File markers showrender / expr tick=0 / cleanup / render / ...in a loop — plugin-local state resets every update, and each cycle churns a create/cleanup pair. Anything stateful a plugin does in a slot (signals, timers, subscriptions) is silently broken.After: the body runs exactly once and untracked, signals and intervals created inside are stable, and updates flow through the JSX bindings. The invariant is standard Solid component semantics: render bodies execute once; reactivity lives in the returned expressions.
How
packages/tui/src/plugin/context.tsx: invoke plugin renders throughcreateComponentinstead of calling them directly.PluginSlot:createComponent(render, mergeProps(() => props.input))—mergePropskeeps slot input reactive through a getter, so plugins still see prop changes without a remount.PluginRoute:createComponent(render, { data: route.data.data })— the surrounding memo still recreates the component on navigation, matching existing behavior.Found while building the hot-reload demos for #39776, but the bug predates that branch — it has existed since plugin slots were introduced.
Scope
A deeper issue remains and is deliberately not addressed here: child expressions of plugin-returned JSX still resolve inside a host-tracked scope in some paths, so store-driven updates can remount rather than update fine-grained. That needs an OpenTUI-level fix and is tracked separately with full instrumentation notes.
Testing
bun typecheckand fullbun run testinpackages/tui(584 pass).