feat: plugin settings (#1375)
* feat(settings): add opencode plugins page Manage opencode `plugin` array entries (npm, scoped npm, versioned, local paths) and auto-loaded plugin files in `~/.config/opencode/plugins/` and `<project>/.opencode/plugins/`. Mirrors MCP CRUD pattern. - Server: `plugins.js` data layer + `plugin-routes.js` REST routes - UI: PluginsSidebar / PluginsPage / AddPluginDialog - Store: usePluginsStore (cache TTL, in-flight dedup, narrow selectors) - i18n: 41 keys across 7 locales Whitelist /api/config/plugins in JSON body-parser so POST/PATCH bodies parse; opencode plugin specs runtime-resolve OPENCODE_CONFIG dir so parallel test files do not cross-pollute module-frozen consts. * feat(settings/plugins): hook npm registry for update + invalid-version detection Plugins page now consults registry.npmjs.org with a 1h server cache. Sidebar rows show an update badge with the latest version, group headers show how many updates are available, the kebab adds an "Update to latest" action that reuses the existing PATCH+restart flow, and the editor surfaces a banner for update-available / missing-version / missing-package / malformed / missing-path / unreadable-path / offline-registry states. A refresh button in the sidebar header forces a cache bypass. - Server: `npm-registry.js` (cache + in-flight dedup + 5s timeout, 404 cached, network failures NOT cached) + `plugin-spec.js` (parser + exact semver detection) + `GET /api/config/plugins/registry?specs=...&refresh=` - Routes accept up to 100 specs/request, dedup by npm package name before fetching, classify each result by kind, never propagate network failure as 500. - Client: `registryInfo` slice + `loadRegistryInfo` (fire-and-forget after loadPlugins, refreshes on mutations) + `updateToLatest(id)`. - UI: `RegistryBadge` per-row + `RegistryBanner` per-entry editor, both use theme tokens (text-only color, no new bg/border tokens) and the shared Icon sprite. Per-spec subscriptions only. - i18n: 24 new keys (incl. split singular/plural for "N update(s) available" because the runtime does not parse ICU plural format). * fix(settings/plugins): keep registry badge visible for long specs Sidebar entry row used `inline-flex` with `truncate` only on the spec text. With long npm specs the badge could be pushed past the row edge and clipped by the parent overflow. Switch to `flex` with spec `flex-1 min-w-0 truncate` and add `shrink-0` to the badge wrapper so the update indicator stays anchored to the right of the row. * fix(settings/plugins): use code-box icon to distinguish from MCP Plugins nav entry used 'plug' which is visually too close to MCP's 'plug-2' icon. Swap to 'code-box' for clearer differentiation in the Settings nav list. * Update packages/ui/src/components/sections/plugins/PluginsPage.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Quat3rnion <81202811+Quat3rnion@users.noreply.github.com> * Update packages/ui/src/stores/usePluginsStore.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: Quat3rnion <81202811+Quat3rnion@users.noreply.github.com> * fix(settings/plugins): validate registry directory + surface save errors - registry endpoint: return 400 on invalid directory query (was silently falling back to homedir, breaking relative path specs) - save failure toast: prefer result.message over generic 'Reload failed' * fix(settings/plugins): address review follow-ups --------- Signed-off-by: Quat3rnion <81202811+Quat3rnion@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
a25e64099c
commit
2b47d899c6
@@ -13,6 +13,8 @@ export const settingsDict = {
|
||||
"settings.view.home.cards.skillsCatalog.description": "Instalar habilidades de catálogos",
|
||||
"settings.view.home.cards.mcp.title": "MCP",
|
||||
"settings.view.home.cards.mcp.description": "Configure servidores e conexões MCP",
|
||||
'settings.view.home.cards.plugins.title': 'Plugins',
|
||||
'settings.view.home.cards.plugins.description': 'Manage opencode plugins',
|
||||
"settings.view.home.cards.usage.title": "Uso",
|
||||
"settings.view.home.cards.usage.description": "Cota + visibilidade dos gastos",
|
||||
"settings.view.unavailable.title": "Indisponível",
|
||||
@@ -33,6 +35,7 @@ export const settingsDict = {
|
||||
"settings.page.agents.title": "Agentes",
|
||||
"settings.page.commands.title": "Comandos",
|
||||
"settings.page.mcp.title": "MCP",
|
||||
'settings.page.plugins.title': 'Plugins',
|
||||
"settings.page.skills.title": "Habilidades",
|
||||
"settings.page.skillsCatalog.title": "Catálogo de habilidades",
|
||||
"settings.page.git.title": "Git",
|
||||
@@ -766,6 +769,73 @@ export const settingsDict = {
|
||||
"settings.mcp.sidebar.toast.deleteFailed": "Não foi possível excluir o servidor MCP",
|
||||
"settings.mcp.sidebar.toast.serverDeleted": "Servidor MCP \"{name}\" excluído",
|
||||
"settings.mcp.sidebar.toast.refreshListIfStale": "Atualize a lista de MCP se a interface parecer desatualizada.",
|
||||
'settings.plugins.sidebar.title': 'Plugins',
|
||||
'settings.plugins.sidebar.total': 'Total: {count}',
|
||||
'settings.plugins.sidebar.actions.addTitle': 'Adicionar plugin',
|
||||
'settings.plugins.sidebar.actions.deleting': 'Excluindo…',
|
||||
'settings.plugins.sidebar.empty.title': 'Nenhum plugin configurado',
|
||||
'settings.plugins.sidebar.empty.description': 'Use o botão + acima para adicionar um',
|
||||
'settings.plugins.sidebar.group.userEntries': 'Configuração do usuário',
|
||||
'settings.plugins.sidebar.group.userFiles': 'Arquivo de plugin do usuário',
|
||||
'settings.plugins.sidebar.group.projectEntries': 'Configuração do projeto',
|
||||
'settings.plugins.sidebar.group.projectFiles': 'Arquivo de plugin do projeto',
|
||||
'settings.plugins.sidebar.kind.npm': 'pacote npm',
|
||||
'settings.plugins.sidebar.kind.path': 'Caminho local',
|
||||
'settings.plugins.sidebar.kind.file': 'Arquivo de plugin',
|
||||
'settings.plugins.sidebar.deleteDialog.title': 'Excluir plugin',
|
||||
'settings.plugins.sidebar.deleteDialog.description': 'Tem certeza de que deseja excluir "{name}"?',
|
||||
'settings.plugins.sidebar.toast.deleted': 'Plugin "{name}" excluído',
|
||||
'settings.plugins.sidebar.toast.deleteFailed': 'Falha ao excluir plugin',
|
||||
'settings.plugins.page.empty.select': 'Selecione um plugin para ver ou editar',
|
||||
'settings.plugins.page.empty.add': 'Ou clique em + para adicionar um novo plugin',
|
||||
'settings.plugins.page.header.entry': 'Plugin instalado',
|
||||
'settings.plugins.page.header.file': 'Arquivo de plugin',
|
||||
'settings.plugins.page.field.spec': 'Especificação',
|
||||
'settings.plugins.page.field.spec.placeholder': 'pacote-npm@versão ou /caminho/absoluto',
|
||||
'settings.plugins.page.field.options': 'Opções (JSON)',
|
||||
'settings.plugins.page.field.options.invalidJson': 'JSON inválido',
|
||||
'settings.plugins.page.field.fileName': 'Nome do arquivo',
|
||||
'settings.plugins.page.field.content': 'Conteúdo',
|
||||
'settings.plugins.page.field.scope': 'Escopo',
|
||||
'settings.plugins.scope.user': 'Usuário',
|
||||
'settings.plugins.scope.project': 'Projeto',
|
||||
'settings.plugins.page.action.save': 'Salvar',
|
||||
'settings.plugins.page.action.discard': 'Descartar',
|
||||
'settings.plugins.dialog.add.title': 'Adicionar plugin',
|
||||
'settings.plugins.dialog.add.tab.npm': 'Do npm',
|
||||
'settings.plugins.dialog.add.tab.path': 'De caminho local',
|
||||
'settings.plugins.dialog.add.tab.file': 'Novo arquivo',
|
||||
'settings.plugins.dialog.add.action.submit': 'Adicionar',
|
||||
'settings.plugins.dialog.add.action.cancel': 'Cancelar',
|
||||
'settings.plugins.toast.created': 'Plugin adicionado',
|
||||
'settings.plugins.toast.updated': 'Plugin atualizado',
|
||||
'settings.plugins.toast.reloadFailed': 'Falha ao recarregar opencode; reinicialização necessária',
|
||||
'settings.plugins.validation.fileName': 'O nome do arquivo deve estar em minúsculas e terminar em .js / .ts / .mjs / .cjs',
|
||||
'settings.plugins.validation.specRequired': 'A especificação é obrigatória',
|
||||
'settings.plugins.registry.badge.update.label': '↑ {version}',
|
||||
'settings.plugins.registry.badge.update.tooltip': 'Atualização disponível: {current} → {latest}',
|
||||
'settings.plugins.registry.badge.malformed.tooltip': 'A especificação está malformada',
|
||||
'settings.plugins.registry.badge.missingPackage.tooltip': 'Pacote {name} não encontrado no npm',
|
||||
'settings.plugins.registry.badge.missingVersion.tooltip': 'A versão {version} de {name} não foi publicada',
|
||||
'settings.plugins.registry.badge.pathMissing.tooltip': 'Arquivo não encontrado: {path}',
|
||||
'settings.plugins.registry.badge.pathUnreadable.tooltip': 'Arquivo não legível: {path}',
|
||||
'settings.plugins.registry.badge.network.tooltip': 'Não foi possível acessar o registro npm',
|
||||
'settings.plugins.registry.banner.updateAvailable.title': 'Atualização disponível',
|
||||
'settings.plugins.registry.banner.updateAvailable.description': '{current} → {latest}',
|
||||
'settings.plugins.registry.banner.updateAvailable.action': 'Atualizar para {latest}',
|
||||
'settings.plugins.registry.banner.invalid.title': 'Plugin inválido',
|
||||
'settings.plugins.registry.banner.invalid.malformed': 'A sintaxe da especificação está malformada',
|
||||
'settings.plugins.registry.banner.invalid.missingPackage': 'Pacote não encontrado no npm',
|
||||
'settings.plugins.registry.banner.invalid.missingVersion': 'Versão não publicada',
|
||||
'settings.plugins.registry.banner.invalid.pathMissing': 'O arquivo não existe',
|
||||
'settings.plugins.registry.banner.invalid.pathUnreadable': 'O arquivo não é legível',
|
||||
'settings.plugins.sidebar.actions.updateToLatest': 'Atualizar para a versão mais recente',
|
||||
'settings.plugins.sidebar.actions.refresh': 'Verificar atualizações',
|
||||
'settings.plugins.sidebar.group.updatesAvailable_one': '{count} atualização disponível',
|
||||
'settings.plugins.sidebar.group.updatesAvailable_other': '{count} atualizações disponíveis',
|
||||
'settings.plugins.toast.updatedToLatest': 'Plugin atualizado para {version}',
|
||||
'settings.plugins.toast.refreshing': 'Verificando atualizações no npm…',
|
||||
'settings.plugins.toast.refreshFailed': 'Não foi possível consultar o registro npm',
|
||||
"settings.openchamber.keyboardShortcuts.title": "Atalhos de teclado",
|
||||
"settings.openchamber.keyboardShortcuts.actions.resetAll": "Redefinir tudo",
|
||||
"settings.openchamber.keyboardShortcuts.actions.overwrite": "Sobrescrever",
|
||||
|
||||
Reference in New Issue
Block a user