Skip to content

fix(tui): persist plugin activation toggles - #42410

Closed
kitlangton wants to merge 1 commit into
v2from
plugin-toggle
Closed

kitlangton wants to merge 1 commit into
v2from
plugin-toggle

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

What

Persist activation changes made through /plugins so manually disabled plugins stay disabled across unrelated plugin source reloads and TUI restarts.

Before / After

Before

  1. Disable plugin A through /plugins.
  2. Edit plugin B's source.
  3. Source reconciliation rebuilds desired state from config.
  4. Plugin A is declared normally in config, so it is activated again.

After

  1. Disable plugin A through /plugins.
  2. The TUI writes a trailing -plugin.a directive to the configured plugin list.
  3. Edit plugin B's source or restart the TUI.
  4. Reconciliation applies the persisted directive and plugin A remains inactive.

How

  • packages/tui/src/config/plugin.ts normalizes exact activation directives and appends the selected final state.
  • packages/tui/src/feature-plugins/system/plugins.tsx updates host configuration instead of mutating only the current in-memory registration.
  • Existing reconciliation remains the single lifecycle path that activates or deactivates registrations after the config update.
sequenceDiagram
  participant User
  participant Dialog as /plugins
  participant Config
  participant Reconciler

  User->>Dialog: disable plugin A
  Dialog->>Config: append -plugin.a
  Config->>Reconciler: configuration changed
  Reconciler->>Reconciler: deactivate plugin A
  User->>Reconciler: edit plugin B source
  Reconciler->>Config: read ordered directives
  Reconciler->>Reconciler: reload B; keep A inactive
Loading

Scope

This does not change plugin discovery, source watching, matching semantics, or failure fallback behavior. It only makes dialog activation changes durable through the existing ordered plugin directives.

Testing

  • bun run test test/plugin-hot-reload.test.tsx test/plugin-toggle.test.ts from packages/tui: 9 passed.
  • bun typecheck from packages/tui.
  • Push hook repository typecheck: 34 packages passed.
  • Live V2 TUI: disabled kit.session-recap through /plugins, confirmed -kit.session-recap was persisted, edited pr-tracker, and confirmed kit.session-recap remained inactive. Restored it to active afterward.

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

  • packages/tui/src/feature-plugins/system/plugins.tsx:67-72 — The toggle computes the desired state from the runtime snapshot (!current.active) instead of from the config draft it is about to mutate — If config and runtime ever diverge (external edit to opencode.json, an in-flight reconcile, or a previous toggle whose apply failed), clicking toggle can re-enable a plugin the user explicitly disabled (or vice versa) — the exact class of bug this PR sets out to fix. Suggestion: derive the effective state inside the update callback, e.g. check whether draft.plugins currently contains id or `-${id}` (absence = enabled) and flip that, so the decision is made against the same data being written.
  • packages/tui/src/feature-plugins/system/plugins.tsx:74-79 — The soft-failure signal is gone: the old code showed a "Failed to update plugin …" toast when activate/deactivate returned false, but the new path treats any resolved config.update as success — a validation rejection or silent no-op write leaves the user with no feedback while the option footer keeps showing the stale active/inactive state. Suggestion: verify the resulting config (return value or a follow-up config.get()) contains the expected directive, and fall back to the error toast when it did not land.
  • packages/tui/src/feature-plugins/system/plugins.tsx:70-80 — Persistence succeeding is not the same as the plugin actually toggling, yet finally(() => setLocked(false)) unlocks immediately after the config write — if the runtime reconcile fails afterwards (broken module, missed watcher event), the dialog is interactive again while showing a state that was never applied. Suggestion: await the expected flip in props.plugins.registered()/list() (with a timeout) before releasing the lock, or surface a "change applies on next reload" hint so the UI never claims success prematurely.
  • packages/tui/src/config/plugin.ts:18-20 — When draft.plugins exists but is not an array, it is silently replaced with [directive], discarding whatever malformed value was stored — this hides authoring errors in the user's config instead of surfacing them. Suggestion: only default when the field is undefined/null; otherwise throw so the existing .catch in the caller turns it into a visible toast.
  • packages/tui/test/plugin-toggle.test.ts:6-13 — Good unit coverage of setEnabled, and the hot-reload regression test is well done, but nothing drives the actual dialog toggle() through config.update to assert the written directive matches !active — the two issues above live precisely in that untested seam, so a regression there would ship unnoticed. Suggestion: extend the renderer harness (bootApp already accepts a config object) to trigger the plugins.toggle action and assert the resulting config contents.
  • packages/tui/src/feature-plugins/system/plugins.tsx:75-78 — The error toast now shows the raw error.message with no indication of which plugin the failure belongs to (the old message named the plugin) — for multi-error failures this makes the toast hard to act on. Suggestion: restore the context, e.g. `Failed to update plugin ${current.id}: ${message}`.

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants