Context
#39776 adds hot reloading of local TUI plugins: PluginProvider watches plugin sources and re-runs reconcile() on change. reconcile predates that PR and rebuilds the whole plugin generation imperatively: it deactivates every plugin (builtins included), wipes all registrations/slots/routes, then replays the entry list. Config plugins changes always behaved this way; hot reload just makes it fire on every file save.
Consequences per save:
- every plugin's in-memory state resets, not just the edited one
- all slots unmount/remount (visible churn), all routes re-register
- manual activate/deactivate toggles from the plugins dialog reset
- a save that fails to import drops the plugin entirely until the next successful reconcile
Proposal: fold-then-diff
Replace the imperative replay in packages/tui/src/plugin/context.tsx with:
- Fold the entry list (discovered files + config directives, including
- disables and selectors) into a desired end state: id -> { target, version, options, enabled }. For local files, version is the entrypoint mtime, which freshSpecifier already stats.
- Diff against current registrations:
- same target/version/options: leave untouched (no cleanup, no setup, slots stay mounted)
- version or options changed: deactivate that plugin, re-import, re-activate
- added/removed: activate/deactivate individually
- builtins are static and never churn
- Keep-last-good: when a changed source fails to re-import, keep the previous instance active and mark the state failed, instead of dropping the plugin.
This mirrors the core registry's versioned-generation semantics (packages/core/src/plugin/*: replacing a plugin with the same ID retains order; only the replacement is torn down), and makes a no-change reconcile a near-no-op, so spurious watch events (sibling files in a watched parent directory) cost a readdir + stats instead of a full rebuild.
Edge semantics to decide
- plugin renames its
id on edit: treat as remove old + add new
- options changed in config: reload that plugin
- ordering: end-state fold must preserve current directive semantics (later entries win)
Scope
TUI only. No change to the core plugin supervisor.
Context
#39776 adds hot reloading of local TUI plugins:
PluginProviderwatches plugin sources and re-runsreconcile()on change.reconcilepredates that PR and rebuilds the whole plugin generation imperatively: it deactivates every plugin (builtins included), wipes all registrations/slots/routes, then replays the entry list. Configpluginschanges always behaved this way; hot reload just makes it fire on every file save.Consequences per save:
Proposal: fold-then-diff
Replace the imperative replay in
packages/tui/src/plugin/context.tsxwith:-disables and selectors) into a desired end state:id -> { target, version, options, enabled }. For local files,versionis the entrypoint mtime, whichfreshSpecifieralready stats.This mirrors the core registry's versioned-generation semantics (
packages/core/src/plugin/*: replacing a plugin with the same ID retains order; only the replacement is torn down), and makes a no-change reconcile a near-no-op, so spurious watch events (sibling files in a watched parent directory) cost a readdir + stats instead of a full rebuild.Edge semantics to decide
idon edit: treat as remove old + add newScope
TUI only. No change to the core plugin supervisor.