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': '카탈로그에서 스킬을 설치합니다',
|
||||
'settings.view.home.cards.mcp.title': 'MCP',
|
||||
'settings.view.home.cards.mcp.description': 'MCP 서버 연결을 설정하세요',
|
||||
'settings.view.home.cards.plugins.title': 'Plugins',
|
||||
'settings.view.home.cards.plugins.description': 'Manage opencode plugins',
|
||||
'settings.view.home.cards.usage.title': '사용량',
|
||||
'settings.view.home.cards.usage.description': '할당량 및 지출 현황',
|
||||
'settings.view.unavailable.title': '사용할 수 없음',
|
||||
@@ -33,6 +35,7 @@ export const settingsDict = {
|
||||
'settings.page.agents.title': '에이전트',
|
||||
'settings.page.commands.title': '명령어',
|
||||
'settings.page.mcp.title': 'MCP',
|
||||
'settings.page.plugins.title': 'Plugins',
|
||||
'settings.page.skills.title': '스킬',
|
||||
'settings.page.skillsCatalog.title': '스킬 카탈로그',
|
||||
'settings.page.git.title': 'Git',
|
||||
@@ -766,6 +769,73 @@ export const settingsDict = {
|
||||
'settings.mcp.sidebar.toast.deleteFailed': 'MCP 서버를 삭제하지 못했습니다',
|
||||
'settings.mcp.sidebar.toast.serverDeleted': 'MCP 서버 "{name}"이(가) 삭제되었습니다',
|
||||
'settings.mcp.sidebar.toast.refreshListIfStale': 'UI가 오래된 것처럼 보이면 MCP 목록을 새로고침하세요.',
|
||||
'settings.plugins.sidebar.title': '플러그인',
|
||||
'settings.plugins.sidebar.total': '총 {count}개',
|
||||
'settings.plugins.sidebar.actions.addTitle': '플러그인 추가',
|
||||
'settings.plugins.sidebar.actions.deleting': '삭제 중…',
|
||||
'settings.plugins.sidebar.empty.title': '설정된 플러그인이 없습니다',
|
||||
'settings.plugins.sidebar.empty.description': '위의 + 버튼을 눌러 추가하세요',
|
||||
'settings.plugins.sidebar.group.userEntries': '사용자 설정',
|
||||
'settings.plugins.sidebar.group.userFiles': '사용자 플러그인 파일',
|
||||
'settings.plugins.sidebar.group.projectEntries': '프로젝트 설정',
|
||||
'settings.plugins.sidebar.group.projectFiles': '프로젝트 플러그인 파일',
|
||||
'settings.plugins.sidebar.kind.npm': 'npm 패키지',
|
||||
'settings.plugins.sidebar.kind.path': '로컬 경로',
|
||||
'settings.plugins.sidebar.kind.file': '플러그인 파일',
|
||||
'settings.plugins.sidebar.deleteDialog.title': '플러그인 삭제',
|
||||
'settings.plugins.sidebar.deleteDialog.description': '"{name}"을(를) 삭제하시겠습니까?',
|
||||
'settings.plugins.sidebar.toast.deleted': '플러그인 "{name}"이(가) 삭제되었습니다',
|
||||
'settings.plugins.sidebar.toast.deleteFailed': '플러그인을 삭제하지 못했습니다',
|
||||
'settings.plugins.page.empty.select': '보거나 편집할 플러그인을 선택하세요',
|
||||
'settings.plugins.page.empty.add': '또는 +를 눌러 새 플러그인을 추가하세요',
|
||||
'settings.plugins.page.header.entry': '설치된 플러그인',
|
||||
'settings.plugins.page.header.file': '플러그인 파일',
|
||||
'settings.plugins.page.field.spec': '명세',
|
||||
'settings.plugins.page.field.spec.placeholder': 'npm-패키지@버전 또는 /절대/경로',
|
||||
'settings.plugins.page.field.options': '옵션(JSON)',
|
||||
'settings.plugins.page.field.options.invalidJson': '잘못된 JSON',
|
||||
'settings.plugins.page.field.fileName': '파일 이름',
|
||||
'settings.plugins.page.field.content': '내용',
|
||||
'settings.plugins.page.field.scope': '범위',
|
||||
'settings.plugins.scope.user': '사용자',
|
||||
'settings.plugins.scope.project': '프로젝트',
|
||||
'settings.plugins.page.action.save': '저장',
|
||||
'settings.plugins.page.action.discard': '취소',
|
||||
'settings.plugins.dialog.add.title': '플러그인 추가',
|
||||
'settings.plugins.dialog.add.tab.npm': 'npm에서',
|
||||
'settings.plugins.dialog.add.tab.path': '로컬 경로에서',
|
||||
'settings.plugins.dialog.add.tab.file': '새 파일',
|
||||
'settings.plugins.dialog.add.action.submit': '추가',
|
||||
'settings.plugins.dialog.add.action.cancel': '취소',
|
||||
'settings.plugins.toast.created': '플러그인이 추가되었습니다',
|
||||
'settings.plugins.toast.updated': '플러그인이 업데이트되었습니다',
|
||||
'settings.plugins.toast.reloadFailed': 'opencode 다시 로드 실패; 재시작이 필요합니다',
|
||||
'settings.plugins.validation.fileName': '파일 이름은 소문자여야 하며 .js / .ts / .mjs / .cjs로 끝나야 합니다',
|
||||
'settings.plugins.validation.specRequired': '명세는 필수입니다',
|
||||
'settings.plugins.registry.badge.update.label': '↑ {version}',
|
||||
'settings.plugins.registry.badge.update.tooltip': '업데이트 가능: {current} → {latest}',
|
||||
'settings.plugins.registry.badge.malformed.tooltip': '명세 형식이 잘못되었습니다',
|
||||
'settings.plugins.registry.badge.missingPackage.tooltip': 'npm에서 패키지 {name}을(를) 찾을 수 없습니다',
|
||||
'settings.plugins.registry.badge.missingVersion.tooltip': '{name}의 {version} 버전이 게시되지 않았습니다',
|
||||
'settings.plugins.registry.badge.pathMissing.tooltip': '파일을 찾을 수 없음: {path}',
|
||||
'settings.plugins.registry.badge.pathUnreadable.tooltip': '파일을 읽을 수 없음: {path}',
|
||||
'settings.plugins.registry.badge.network.tooltip': 'npm 레지스트리에 연결할 수 없습니다',
|
||||
'settings.plugins.registry.banner.updateAvailable.title': '업데이트 가능',
|
||||
'settings.plugins.registry.banner.updateAvailable.description': '{current} → {latest}',
|
||||
'settings.plugins.registry.banner.updateAvailable.action': '{latest}(으)로 업데이트',
|
||||
'settings.plugins.registry.banner.invalid.title': '잘못된 플러그인',
|
||||
'settings.plugins.registry.banner.invalid.malformed': '명세 구문이 잘못되었습니다',
|
||||
'settings.plugins.registry.banner.invalid.missingPackage': 'npm에서 패키지를 찾을 수 없습니다',
|
||||
'settings.plugins.registry.banner.invalid.missingVersion': '게시되지 않은 버전입니다',
|
||||
'settings.plugins.registry.banner.invalid.pathMissing': '파일이 존재하지 않습니다',
|
||||
'settings.plugins.registry.banner.invalid.pathUnreadable': '파일을 읽을 수 없습니다',
|
||||
'settings.plugins.sidebar.actions.updateToLatest': '최신 버전으로 업데이트',
|
||||
'settings.plugins.sidebar.actions.refresh': '업데이트 확인',
|
||||
'settings.plugins.sidebar.group.updatesAvailable_one': '업데이트 {count}개 가능',
|
||||
'settings.plugins.sidebar.group.updatesAvailable_other': '업데이트 {count}개 가능',
|
||||
'settings.plugins.toast.updatedToLatest': '플러그인이 {version}(으)로 업데이트되었습니다',
|
||||
'settings.plugins.toast.refreshing': 'npm에서 업데이트 확인 중…',
|
||||
'settings.plugins.toast.refreshFailed': 'npm 레지스트리를 확인하지 못했습니다',
|
||||
'settings.openchamber.keyboardShortcuts.title': '키보드 단축키',
|
||||
'settings.openchamber.keyboardShortcuts.actions.resetAll': '모두 초기화',
|
||||
'settings.openchamber.keyboardShortcuts.actions.overwrite': '덮어쓰기',
|
||||
|
||||
Reference in New Issue
Block a user