Plugin Manager — add and remove OpenCode plugins from the UI#593
Plugin Manager — add and remove OpenCode plugins from the UI#593ashutoshsinghpr7 wants to merge 1 commit into
Conversation
|
PR builds are available as GitHub Actions artifacts: https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29319171187 Artifacts expire in 7 days. |
84419e8 to
701c968
Compare
@shantur @pascalandr |
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.
701c968 to
da98887
Compare
|
PR builds are available as GitHub Actions artifacts: https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29319744953 Artifacts expire in 7 days.
|
pascalandr
left a comment
There was a problem hiding this comment.
Gatekeeper review of da98887
Blocking findings
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
|
PR builds are available as GitHub Actions artifacts: https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29320980628 Artifacts expire in 7 days.
|
1 similar comment
|
PR builds are available as GitHub Actions artifacts: https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/29320980628 Artifacts expire in 7 days.
|
|
@ashutoshsinghpr7 |
000d8ac to
da98887
Compare
There was a problem hiding this comment.
UX review
@ashutoshsinghpr7 If @shantur think a plugin manager is relevant for codenomad then we 'll have to adress these :
High-priority UX findings
- 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.
- 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.
- 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.
- 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.
- 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
-
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.
-
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.
-
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.
-
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.
-
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.
|
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. |



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
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:
~/.config/opencode/opencode.jsonc— the global OpenCode config fileenvironmentVariables.OPENCODE_CONFIG_CONTENT— what the server reads when launching new workspacesWriting to both means the plugin list survives restarts. The end-to-end flow is:
What I tested
Built a fresh workspace, added
npm:opencode-memthrough the modal, restarted the workspace, and confirmed thememorytool 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:litopencodewhich 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
$schemaandmcpare left untouched.The duplicate
stripJsoncwas extracted into a shared module atpackages/server/src/config/jsonc.ts. Previously it was copy-pasted in bothopencode-plugin.tsandsettings.ts— two identical ~60-line functions doing the same JSONC comment stripping. Keeping the local copy alongside the new import would causeTS2440(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 componentAPI 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).