feat: disable / enable skills + preferences api by neriousy · Pull Request #47595 · anomalyco/opencode · GitHub
Skip to content

feat: disable / enable skills + preferences api - #47595

Open
neriousy wants to merge 5 commits into
v2from
feat/skills-disable-enable
Open

feat: disable / enable skills + preferences api#47595
neriousy wants to merge 5 commits into
v2from
feat/skills-disable-enable

Conversation

@neriousy

@neriousy neriousy commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Related: #43536 (earlier capability-preference approach).

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds persistent, server-wide skill enable/disable preferences, with controls in the TUI and desktop/web app.

  • TUI: /skills opens List skills or Toggle skills. Toggle rows have fuzzy search across IDs, names, and descriptions, Enabled/Disabled labels, Enter to toggle, and Ctrl+R to reset.
  • Desktop/web: switches in Settings → Extensions → Skills and Settings → Projects → select project → Extensions → Skills. Project settings include project and inherited skills. Failed saves retain the saved state and allow retry.
  • Disablement is keyed by skill ID across projects and sessions on that server, regardless of where the skill was defined.

Preferences API

Preferences is a reusable Core service over KV. It stores explicit JSON values and validates writes against a registered schema for each preference kind. skill.activation is the first kind; only its value schema is restricted to enabled / disabled. Other kinds can define boolean, numeric, string, array, object, or nullable values.

const target = { kind: "skill.activation", id: "effect" }

await client.preferences.set({ ...target, value: "disabled" })
await client.preferences.get(target)
// { target: { kind: "skill.activation", id: "effect" }, value: "disabled" }

await client.preferences.list() // All explicit overrides
await client.preferences.set({ ...target, value: "enabled" })
await client.preferences.reset(target)
await client.preferences.get(target) // null: use the domain default
Operation HTTP
List overrides GET /api/preferences
Read one override GET /api/preferences/:kind/:id
Set an override PUT /api/preferences/:kind/:id with { "value": "disabled" }
Reset an override DELETE /api/preferences/:kind/:id

Each target has one KV entry under preferences:values:, containing { target, value }. Explicit choices survive restarts; reset deletes the override. Skills default to enabled. An explicit JSON null value is distinct from a missing override. Unknown kinds and invalid values return HTTP 400.

New kinds register their schemas in packages/core/src/preferences.ts. Domains own defaults and behavior; Preferences owns validation, persistence, reset, and global preferences.updated invalidations.

How skills use it

skill.list() returns all registered definitions, with no activation metadata. Consumers join those definitions with preference overrides:

skill.list() ──────────┐
                      ├─ Toggle/settings views: all definitions + preferences
preferences.list() ───┘
                      └─ skill.available(location): filtered picker view

Preference updates refresh availability across loaded Locations without refetching definitions. The TUI, desktop/web composer, Mini, and ACP filter disabled skills from normal pickers.

Core also excludes disabled skills from model guidance and rejects new tool loads, prompt attachments, and explicit session activation. Session APIs return SkillDisabledError (HTTP 409). Existing history and already-admitted prompt snapshots are preserved. autoinvoke: false remains manual-only behavior, separate from disablement and agent permissions.

How did you verify your code works?

  • Focused Core, Server, Client, TUI, Mini, and ACP tests passed, including persistence, pagination, restart recovery, disabled-load rejection, and cross-Location filtering.
  • TUI renderer tests cover narrow/dark and wide/light layouts, fuzzy search, whitespace, toggle/reset, live updates, and failed saves.
  • Traced the browser CI failures to missing or malformed mock responses for preferences/skills. Nine affected tests passed against the unchanged production bundle after correcting fixtures.
  • Four new production-build browser tests pass for global/project switches, composer filtering, LTR/RTL, failed saves, and external preference updates without reloading definitions.
  • Relevant package typechecks and app/website builds passed. Regenerated Promise/Effect clients, Protocol OpenAPI, and both website OpenAPI copies; the generated-documentation check passes locally.

Screenshots / recordings

Not attached yet. TUI renderer and browser tests cover the interactions.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

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.

1 participant