Skip to content

Plugin Manager — add and remove OpenCode plugins from the UI#593

Draft
ashutoshsinghpr7 wants to merge 1 commit into
NeuralNomadsAI:devfrom
ashutoshsinghpr7:feat/plugin-manager-ui
Draft

Plugin Manager — add and remove OpenCode plugins from the UI#593
ashutoshsinghpr7 wants to merge 1 commit into
NeuralNomadsAI:devfrom
ashutoshsinghpr7:feat/plugin-manager-ui

Conversation

@ashutoshsinghpr7

@ashutoshsinghpr7 ashutoshsinghpr7 commented Jul 14, 2026

Copy link
Copy Markdown

Right now the Status tab shows which plugins are loaded, but there is no way to add or remove them from CodeNomad. You have to go edit opencode.jsonc by hand, then restart the workspace, then hope you got the JSON right.

This adds a Plugin Manager modal that fixes that. It lives behind a "Manage" button next to the Plugins section in the right panel Status tab.

What it does

  • Lists all your configured plugins with their enabled/disabled state
  • Lets you type in a plugin spec (npm:package, file:///path, https://url, /absolute, ./relative) and hit Add
  • Validates specs inline — if you type something that is not a valid plugin spec, a warning appears
  • Asks for confirmation before removing a plugin
  • Handles the tuple format [spec, {enabled: false}] if you have those in your config
  • Everything persists across workspace restarts

How persistence works

The naive approach would be to call client.config.update() from the SDK, but that PATCH does not write changes to disk — they disappear when the workspace restarts.

Instead, this writes to two places:

  1. ~/.config/opencode/opencode.jsonc — the global OpenCode config file
  2. CodeNomad server settings environmentVariables.OPENCODE_CONFIG_CONTENT — what the server reads when launching new workspaces

Writing to both means the plugin list survives restarts. The end-to-end flow is:

PUT /api/opencode-plugin-config
  ├─► writes opencode.jsonc
  └─► updates server environmentVariables
        │
        ▼ (workspace restart)
   CodeNomad reads env vars → passes to OpenCode
        │
        ▼
   Plugin loads, agents/tools appear in the UI

What I tested

Built a fresh workspace, added npm:opencode-mem through the modal, restarted the workspace, and confirmed the memory tool was registered and the agent used it in a conversation. Then removed it and confirmed it was gone after a restart.

Also tested with npm:litopencode which registered 20+ agents (lit-loop, lit-plan, lit-implement, etc.) visible in the agent selector dropdown.

Edge cases covered: empty config file gets created on first add, broken JSON is handled gracefully, whitespace is trimmed, invalid specs are rejected, duplicate adds are skipped, concurrent adds/removes have the button disabled, removing the last plugin cleans the field from config entirely, and existing fields like $schema and mcp are left untouched.

The duplicate stripJsonc was extracted into a shared module at packages/server/src/config/jsonc.ts. Previously it was copy-pasted in both opencode-plugin.ts and settings.ts — two identical ~60-line functions doing the same JSONC comment stripping. Keeping the local copy alongside the new import would cause TS2440 (import conflicts with local declaration), so the local definition was removed. Both files now import from the shared module instead.

New files

  • packages/server/src/config/jsonc.ts — shared JSONC parser (extracted from opencode-plugin.ts, used by both)
  • packages/ui/src/components/provider-auth/plugin-manager-modal.tsx — the modal component

API endpoints added

  • GET /api/opencode-plugin-config — returns { plugins: [{ spec, enabled }] }
  • PUT /api/opencode-plugin-config — accepts { plugins: ["spec1", "spec2"] }

i18n

All 21 new strings are translated in all 9 supported languages (en, es, fr, de, ru, ja, he, ne, zh-Hans).

@ashutoshsinghpr7 ashutoshsinghpr7 requested a review from a team July 14, 2026 08:43
@ashutoshsinghpr7 ashutoshsinghpr7 changed the base branch from main to dev July 14, 2026 08:45
@github-actions

Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29319171187

Artifacts expire in 7 days.
Artifacts: (none found on this run)

@ashutoshsinghpr7 ashutoshsinghpr7 force-pushed the feat/plugin-manager-ui branch from 84419e8 to 701c968 Compare July 14, 2026 08:54
@ashutoshsinghpr7

Copy link
Copy Markdown
Author
image image image

@shantur @pascalandr
Any suggestions or feedback?

CodeNomad shows which plugins are loaded in the Status tab but
there was no way to add or remove them from the UI — you had to
edit opencode.jsonc by hand.

This adds a Plugin Manager modal that lets you:

- See all configured plugins (including tuple [spec, opts] format)
- Add new plugins by spec (npm:, file://, https://, /paths, ./relative)
- Remove plugins with a confirmation step
- Get inline validation feedback for invalid specs
- All changes persist across workspace restarts

How it works:

The modal reads and writes plugins to two places: the global
OpenCode config file (~/.config/opencode/opencode.jsonc) and
CodeNomad server settings (environmentVariables). Writing to
both means plugin changes survive restarts — the server config
is what CodeNomad reads when launching new workspaces.

A "Manage" button sits next to the Plugins heading in the
Status tab right panel. Clicking it opens the modal with an
input field for new plugin specs, a list of installed plugins
with Remove buttons, and a notice that changes need a restart.

The API endpoint at /api/opencode-plugin-config handles the
config file I/O with JSONC support (comments, trailing commas).
The endpoint also syncs to CodeNomad server settings so the
next workspace launch picks up the changed plugin list.

Edge cases covered:

- Empty config file -- creates it on first add
- Broken JSON -- returns empty list gracefully
- Duplicate specs -- silently skipped
- Whitespace -- trimmed before storage
- Invalid specs -- rejected with a message
- Tuple format [spec, {enabled:false}] -- read correctly
- Concurrent add/remove -- button disabled while in flight
- Remove confirmation -- Cancel goes back, Remove confirms
- Empty plugin list -- removes plugin field from config entirely
- Original config preserved -- $schema, mcp, etc. untouched

A shared stripJsonc module was extracted to config/jsonc.ts used
by both the settings routes and the existing opencode-plugin.ts.
@ashutoshsinghpr7 ashutoshsinghpr7 force-pushed the feat/plugin-manager-ui branch from 701c968 to da98887 Compare July 14, 2026 09:11
@github-actions

Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29319744953

Artifacts expire in 7 days.
Artifacts:

  • pr-593-701c968a836cfd81907169782d8f30f20daad0ca-tauri-windows
  • pr-593-701c968a836cfd81907169782d8f30f20daad0ca-electron-macos
  • pr-593-701c968a836cfd81907169782d8f30f20daad0ca-tauri-macos-arm64
  • pr-593-701c968a836cfd81907169782d8f30f20daad0ca-tauri-linux
  • pr-593-701c968a836cfd81907169782d8f30f20daad0ca-electron-linux
  • pr-593-701c968a836cfd81907169782d8f30f20daad0ca-electron-windows

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gatekeeper review of da98887

Blocking findings

  1. P1: every update destroys non-plugin OPENCODE_CONFIG_CONTENT settings.

packages/server/src/server/routes/settings.ts:252-266 replaces the complete inline OpenCode config with a new object containing only plugin. OPENCODE_CONFIG_CONTENT is documented and already supported as a full OpenCode config; it may contain provider, model, MCP, permission, agent, or other runtime settings. Adding one plugin through this modal silently removes all of those settings for subsequent workspaces.

Parse the existing inline config, change only its plugin field, preserve every other key, and cover this with a regression test.

  1. P1: removing the final plugin does not clear the inline override.

At lines 263-266 the code deletes OPENCODE_CONFIG_CONTENT from a copied environmentVariables object, then sends that object through RFC 7396-style merge patch. Omitted keys are preserved, not deleted, so the old OPENCODE_CONFIG_CONTENT remains in server settings. The global file loses its plugin field, but restarted workspaces continue receiving the previous plugin list from the higher-precedence inline config.

Use an explicit null deletion in the patch or replace the environmentVariables owner value, and test the persisted owner state after removing the last plugin.

  1. P1: any add/remove operation silently enables all disabled tuple entries.

GET preserves the enabled state from [spec, { enabled: false }], but plugin-manager-modal.tsx:101-109 and 134-140 sends only spec strings. The PUT schema then reconstructs every entry with enabled: true at settings.ts:249. Adding or removing an unrelated plugin therefore rewrites all disabled plugins as enabled in both persistence locations.

Carry PluginEntry objects through the request, validate them server-side, and preserve tuple options that this manager does not edit.

  1. P1: malformed or temporarily unreadable JSONC is overwritten with a new mostly-empty config.

writeGlobalConfigPlugins uses safeJsonParse(raw) ?? {} and also falls back to an empty object on any read error. A syntax error, partial write, encoding issue, or transient read failure therefore causes the next PUT to erase the user's complete global OpenCode config. GET hides the same condition by returning an empty list, making a destructive update look safe.

Fail closed without writing when an existing file cannot be read or parsed. Write atomically through a temporary file and rename. Preserve a useful error for the UI.

  1. P1: the validator rejects the standard npm plugin syntax documented by OpenCode.

isValidPluginSpec only accepts values beginning with npm:, URL/path prefixes, or a dot. Official OpenCode configuration uses bare package names such as opencode-wakatime and scoped names such as @my-org/custom-plugin. A global config containing either can be displayed by GET, but the first add/remove PUT fails because the existing item is now invalid.

Validate the actual OpenCode plugin schema, including bare and scoped npm package names, and add round-trip tests for existing standard entries.

  1. P1: concurrent updates and partial two-store writes can lose configuration.

The UI only disables the specific action flag, so add, refresh, and other remove actions can overlap. The server performs an unlocked read-modify-write to one file and then a separate settings write. Concurrent requests can overwrite each other, and a failure after the file write leaves the two advertised persistence locations divergent.

Serialize updates server-side, make the file write atomic, prevent overlapping UI mutations, and define rollback or reconciliation behavior when the second store fails.

Important findings

  • The modal says it manages plugins for the active workspace, but the endpoint has no instance identity and updates global config plus global server settings for every future workspace. The UI and API need a consistent global or workspace scope.
  • Server validation errors are returned as English strings and displayed directly by the UI, bypassing the i18n layer.
  • No tests cover either new endpoint, JSONC preservation/failure behavior, disabled tuples, inline-config merging, final removal, or concurrent updates. The existing 14 focused server tests only cover prior plugin injection and speech settings.

What looks correct

  • The JSONC stripping extraction preserves the existing implementation.
  • Existing environment variable keys other than OPENCODE_CONFIG_CONTENT are copied before patching.
  • The modal has loading, validation, confirmation, and error states, and all new UI labels are present across the registered locales.
  • The manual end-to-end evidence demonstrates the happy path for two npm: examples.

Validation

  • 14 existing focused server tests: passed.
  • Server and UI typechecks: passed.
  • UI production build: passed.
  • git diff --check upstream/dev...HEAD: passed.
  • Latest Electron Linux/macOS/Windows and Tauri Linux/macOS ARM64/Windows builds: passed; Tauri macOS is still pending.
  • The artifact-comment job failed on a GitHub API 502, unrelated to product code.

Verdict

Changes requested. The happy path works, but the current persistence implementation can erase unrelated OpenCode configuration and does not reliably remove or preserve plugins.

@github-actions

Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29320980628

Artifacts expire in 7 days.
Artifacts:

  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-macos
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-windows
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-electron-macos
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-macos-arm64
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-linux
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-electron-linux
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-electron-windows

1 similar comment
@github-actions

Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29320980628

Artifacts expire in 7 days.
Artifacts:

  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-macos
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-windows
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-electron-macos
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-macos-arm64
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-tauri-linux
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-electron-linux
  • pr-593-da988878291e399a35ba5e10be2c04a1fac9bec3-electron-windows

@ashutoshsinghpr7 ashutoshsinghpr7 marked this pull request as draft July 14, 2026 09:52
@pascalandr

Copy link
Copy Markdown
Contributor

@ashutoshsinghpr7
I think it's going to be complicated to do it well, a true plugin manager will have many cases to take care of and I'm not sure the trade off worth it as you can just ask your agent to take care of it ?
so I'm not sure CodeNomad is a good fit for such a feature, we would need Shantur's opinion on this before putting too much work in it.

@ashutoshsinghpr7 ashutoshsinghpr7 force-pushed the feat/plugin-manager-ui branch from 000d8ac to da98887 Compare July 14, 2026 09:58

@pascalandr pascalandr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX review

@ashutoshsinghpr7 If @shantur think a plugin manager is relevant for codenomad then we 'll have to adress these :

High-priority UX findings

  1. The scope presented to the user is incorrect and potentially destructive.

The Manage action sits beside the active workspace's runtime Plugins status, and the modal subtitle says it adds or removes plugins for the active workspace. The implementation actually changes global user configuration and global server launch settings for every future workspace.

Before this can ship, CodeNomad must choose and expose one clear scope. If the feature stays global, move it to Settings and label it Global configured plugins. If it is workspace-specific, pass an instance/project identity and persist to that project layer. Scope should remain visible during creation and in each configured item.

  1. Configured plugins and loaded plugins are presented as if they were the same inventory.

The Status panel shows plugins reported by the running OpenCode instance. The modal reads only one global config file. Project plugins, plugin-directory files, inline config entries, disabled entries, the bundled CodeNomad plugin, and failed-to-load entries can make the two lists differ.

Opening Manage beside the runtime list will therefore look inconsistent or broken. CodeNomad needs at least Configured globally, Loaded in this workspace, and Pending restart states, or the manager must be detached from runtime Status. Every item should expose its source, scope, and kind where relevant.

  1. The add flow is too ambiguous for a code-loading feature.

One unlabeled text field accepts npm, URL, absolute path, and relative path forms, but only performs prefix validation. Users cannot tell which syntax is expected, what relative paths are relative to, or whether the package/path actually exists.

A proportionate flow should provide at least Package and Local path modes, contextual fields, a visible label, syntax examples, scope explanation, and server-side package/path validation before asking the user to restart. Missing packages, missing versions, unreadable paths, malformed input, and network failures should have distinct feedback.

  1. Successful completion has no actionable restart state.

After a mutation, the input clears or a row disappears, but there is no success announcement, no persistent Pending restart marker, no identification of affected workspaces, and no Restart workspace action. The always-visible restart note does not tell the user whether a change actually succeeded.

CodeNomad should provide a success toast or status banner plus Restart current workspace when relevant. For global scope, explain that existing workspaces are unchanged and new or restarted ones receive the configuration. If automatic reload is attempted, distinguish success from reload failure.

  1. The feature is already too complex for a small modal inside Status.

The current modal has add, refresh, list, enabled state, confirmation, loading, validation, two error channels, and restart guidance. It still lacks source, scope, type, options, health, update state, and loaded/configured reconciliation. Adding those will make this modal increasingly dense.

If CodeNomad intends a true plugin manager, use a dedicated Settings section with a grouped list and detail view. If the desired feature is intentionally lightweight, narrow it to a clearly named Global plugin entries editor rather than presenting it as a complete manager.

Important UX findings

  1. Disabled is displayed as a status but has no corresponding action. Users can see Enabled or Disabled, but cannot toggle or edit options; their only action is Remove. Either support enable/disable while preserving options, or explain that disabled entries are read-only and must be edited elsewhere.

  2. Error and empty states can appear together. When loading fails with an empty local list, the modal renders the error and No plugins configured, falsely implying a successful empty result. Error, empty, loading, and populated states should be mutually exclusive.

  3. Mutation locking does not match the visual model. During Add, remove and refresh remain available; during Remove, other rows and Add remain available. Even apart from the persistence race, users can initiate overlapping actions with stale data. Use one modal-level mutation state and disable all conflicting controls.

  4. Confirmation lacks item identity and stable layout. Inline replacement of a card footer with Remove this plugin? shifts the row and does not repeat the plugin name. CodeNomad can keep inline confirmation, but it should include the spec, reserve stable space, and keep all destructive actions disabled while the request runs.

  5. Accessibility feedback is incomplete. The add input relies on placeholder text instead of a persistent label; validation and request errors are not announced with alert/status semantics; focus is not moved to the invalid field or result; and successful changes have no live-region feedback.

Recommended product direction

Given the complexity and the existing concern about product fit, the smallest coherent version is:

  • Place it under Settings, not active workspace Status.
  • Call it Global plugin entries, not Plugin Manager.
  • Support only user-level config entries initially.
  • Separate npm package and local path input modes.
  • Show source, type, configured state, and pending-restart state.
  • Validate against npm/path reality before save.
  • Preserve advanced tuple options without exposing incomplete editing, or provide an Advanced JSON options field.
  • Offer a clear restart action and outcome feedback.

A full manager additionally requires user/project scopes, plugin-directory files, options editing, package health and updates, loaded-versus-configured reconciliation, and robust reload behavior. That is a substantially larger feature and should be an explicit product decision rather than incremental additions to this modal.

UX verdict

Not ready as currently framed. The visual implementation is serviceable, but the location, naming, scope, inventory model, validation, and post-save feedback create incorrect expectations. The highest-value fix is to decide whether this is a narrow global config editor or a true multi-source plugin manager before refining the modal.

@ashutoshsinghpr7

Copy link
Copy Markdown
Author

@pascalandr

The inspiration for this was the inconsistency in the Status tab. MCP servers have enable/disable toggles. Providers get a full connect/disconnect modal with OAuth flow support. Even LSP servers show connection status dots. But plugins just bare names listed in a box you can't even tell if a plugin is enabled or disabled without opening the config file. Every other service in that right panel has some degree of interactive management, plugins were the only section that felt purely informational, and it stuck out.

Will wait for @shantur view on this.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants