From f1b98bfe9c2c83994768cf35c0f03d6e5e492c61 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Thu, 30 Jul 2026 14:02:16 -0700 Subject: [PATCH 1/6] feat(plugins): add marketplace and enforce runtime integrations - Add bundled plugin catalog, settings UI, localized metadata, and skills - Apply plugin MCP and skill policies across thread launches and subagents - Preserve launch configuration through recovery, restart, and remote sync - Expand integration smoke coverage and runtime tests --- .../scripts/poracode-integration-smoke.mjs | 149 ++++++ .../scripts/smoke-scenarios.mjs | 11 + resources/skills/browser-control/SKILL.md | 18 + resources/skills/chrome-control/SKILL.md | 18 + resources/skills/computer-use/SKILL.md | 18 + resources/skills/subagent-delegation/SKILL.md | 18 + src/main/main.ts | 6 +- src/main/orchestratorThreadBridge.test.ts | 29 ++ src/main/orchestratorThreadBridge.ts | 13 + src/main/sharedSettingsFile.test.ts | 2 + src/main/supervisor/SupervisorClient.test.ts | 34 +- src/main/supervisor/SupervisorClient.ts | 9 +- src/renderer/app.tsx | 3 +- .../components/common/ToggleSwitch.tsx | 6 +- .../components/composer/composerMcpServers.ts | 9 +- .../components/mcp/McpServersManager.test.tsx | 30 ++ .../components/mcp/McpServersManager.tsx | 20 +- .../components/plugins/PluginDetail.test.tsx | 71 +++ .../components/plugins/PluginDetail.tsx | 252 +++++++++ .../components/plugins/PluginIcon.tsx | 17 + .../plugins/PluginMarketplace.test.tsx | 97 ++++ .../components/plugins/PluginMarketplace.tsx | 184 +++++++ src/renderer/components/plugins/pluginCopy.ts | 128 +++++ .../components/skills/SkillViewModal.tsx | 3 +- .../components/skills/SkillsManager.test.tsx | 52 ++ .../components/skills/SkillsManager.tsx | 51 +- .../components/skills/useSkills.test.ts | 152 +++++- src/renderer/components/skills/useSkills.ts | 71 ++- .../components/thread/ThreadCommandPanel.tsx | 14 + .../thread/ThreadComposerSection.test.tsx | 138 ++++- .../thread/ThreadComposerSection.tsx | 37 +- .../thread/ThreadDraftComposerArea.tsx | 140 +++-- .../thread/ThreadDraftView.test.tsx | 124 +++++ .../components/thread/ThreadDraftView.tsx | 15 +- .../thread/ThreadSlashCommands.test.tsx | 32 ++ .../components/thread/ThreadView.test.tsx | 40 +- src/renderer/components/thread/ThreadView.tsx | 16 +- src/renderer/hooks/useAppHydration.ts | 3 +- src/renderer/locales/de/messages.po | 222 +++++++- src/renderer/locales/en/messages.po | 222 +++++++- src/renderer/locales/es/messages.po | 222 +++++++- src/renderer/locales/fr/messages.po | 222 +++++++- src/renderer/locales/ja/messages.po | 222 +++++++- src/renderer/locales/ko/messages.po | 222 +++++++- src/renderer/locales/pl/messages.po | 222 +++++++- src/renderer/locales/pt-BR/messages.po | 222 +++++++- src/renderer/locales/ru/messages.po | 222 +++++++- src/renderer/locales/tr/messages.po | 222 +++++++- src/renderer/locales/uk/messages.po | 222 +++++++- src/renderer/locales/vi/messages.po | 222 +++++++- src/renderer/locales/zh-CN/messages.po | 222 +++++++- src/renderer/state/appStore.test.ts | 47 ++ src/renderer/state/remote/sync.ts | 5 +- .../state/sharedSettingsStore.test.ts | 77 +++ src/renderer/state/sharedSettingsStore.ts | 107 ++++ src/renderer/state/slices/threadSlice.ts | 88 +++- .../views/SettingsOverlay/SettingsOverlay.tsx | 2 + .../parts/McpServersSettings.tsx | 16 + .../parts/PluginsSettings.test.tsx | 51 ++ .../SettingsOverlay/parts/PluginsSettings.tsx | 48 ++ .../SettingsOverlay/parts/SettingsSidebar.tsx | 12 + .../parts/settingsSearchIndex.ts | 10 + .../views/SettingsOverlay/parts/types.ts | 1 + src/server/createHeadlessRemoteHost.test.ts | 96 +++- src/server/createHeadlessRemoteHost.ts | 12 + src/shared/contracts.ts | 1 + src/shared/contracts/agent.ts | 11 + src/shared/contracts/plugin.ts | 70 +++ src/shared/contracts/skill.ts | 5 +- src/shared/contracts/thread.ts | 4 + src/shared/ipc/events.ts | 2 + src/shared/plugins/catalog.test.ts | 319 ++++++++++++ src/shared/plugins/catalog.ts | 475 +++++++++++++++++ src/shared/settings.ts | 4 + src/supervisor/runtime.test.ts | 35 ++ .../runtime/cliHookEventChain.perf.test.ts | 1 + src/supervisor/runtime/sessionTypes.ts | 8 + .../runtime/supervisorSharedSettings.test.ts | 52 ++ .../runtime/supervisorSharedSettings.ts | 6 + .../runtime/threadOutputPipeline.test.ts | 95 +++- .../runtime/threadOutputPipeline.ts | 47 +- .../invalidSessionRecovery.test.ts | 43 +- .../threadSession/invalidSessionRecovery.ts | 94 ++-- .../runtime/threadSession/managerOptions.ts | 30 ++ .../sessionRuntimeLifecycle.test.ts | 41 ++ .../threadSession/sessionRuntimeLifecycle.ts | 26 +- .../threadSession/spawnPipeline.test.ts | 102 +++- .../runtime/threadSession/spawnPipeline.ts | 384 ++++++++++++-- .../runtime/threadSession/steerCoordinator.ts | 19 +- .../threadSession/structuredTurnQueue.ts | 17 +- ...readSessionManager.restartTerminal.test.ts | 261 +++++++++- .../threadSessionManager.stageInput.test.ts | 36 +- ...hreadSessionManager.staleInterrupt.test.ts | 193 ++++++- .../threadSessionManager.startClose.test.ts | 175 ++++++- .../runtime/threadSessionManager.ts | 238 ++++++--- .../runtime/userInterruptRecovery.test.ts | 1 + src/supervisor/skills/SkillsService.test.ts | 478 +++++++++++++++++- src/supervisor/skills/SkillsService.ts | 65 ++- src/supervisor/skills/pluginSkillPolicy.ts | 302 +++++++++++ .../OrchestratorThreadManager.test.ts | 2 + .../subagentMcp/OrchestratorThreadManager.ts | 4 + .../subagentMcp/SubagentRunManager.test.ts | 107 +++- .../subagentMcp/SubagentRunManager.ts | 52 +- src/supervisor/subagentMcp/types.ts | 16 +- src/supervisor/supervisorRuntime.ts | 34 +- 105 files changed, 8977 insertions(+), 366 deletions(-) create mode 100644 resources/skills/browser-control/SKILL.md create mode 100644 resources/skills/chrome-control/SKILL.md create mode 100644 resources/skills/computer-use/SKILL.md create mode 100644 resources/skills/subagent-delegation/SKILL.md create mode 100644 src/main/orchestratorThreadBridge.test.ts create mode 100644 src/renderer/components/plugins/PluginDetail.test.tsx create mode 100644 src/renderer/components/plugins/PluginDetail.tsx create mode 100644 src/renderer/components/plugins/PluginIcon.tsx create mode 100644 src/renderer/components/plugins/PluginMarketplace.test.tsx create mode 100644 src/renderer/components/plugins/PluginMarketplace.tsx create mode 100644 src/renderer/components/plugins/pluginCopy.ts create mode 100644 src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx create mode 100644 src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx create mode 100644 src/shared/contracts/plugin.ts create mode 100644 src/shared/plugins/catalog.test.ts create mode 100644 src/shared/plugins/catalog.ts create mode 100644 src/supervisor/runtime/supervisorSharedSettings.test.ts create mode 100644 src/supervisor/skills/pluginSkillPolicy.ts diff --git a/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs b/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs index 4976f26eb..96dd4c7ba 100644 --- a/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs +++ b/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs @@ -384,6 +384,7 @@ async function settingsScenario(client) { "agentsGeneral", "skills", "mcpServers", + "plugins", "browser", "usage", "archived", @@ -393,6 +394,7 @@ async function settingsScenario(client) { let mcpListScreenshotPath; let mcpScreenshotPath; let mcpImportScreenshotPath; + let pluginsScreenshotPath; let skillsScreenshotPath; let skillsImportScreenshotPath; let skillsImportDestinationsScreenshotPath; @@ -445,6 +447,9 @@ async function settingsScenario(client) { ({ mcpListScreenshotPath, mcpScreenshotPath, mcpImportScreenshotPath } = await mcpServersSectionDeepDive(client, mcpFixture)); } + if (section === "plugins") { + ({ pluginsScreenshotPath } = await pluginsSectionDeepDive(client)); + } } const screenshotPath = join(outDir, "smoke-02-settings.png"); await screenshot(client, screenshotPath); @@ -457,6 +462,7 @@ async function settingsScenario(client) { ...(mcpListScreenshotPath ? { mcpListScreenshotPath } : {}), ...(mcpScreenshotPath ? { mcpScreenshotPath } : {}), ...(mcpImportScreenshotPath ? { mcpImportScreenshotPath } : {}), + ...(pluginsScreenshotPath ? { pluginsScreenshotPath } : {}), ...(skillsScreenshotPath ? { skillsScreenshotPath } : {}), ...(skillsImportScreenshotPath ? { skillsImportScreenshotPath } : {}), ...(skillsImportDestinationsScreenshotPath ? { skillsImportDestinationsScreenshotPath } : {}), @@ -465,6 +471,149 @@ async function settingsScenario(client) { }; } +async function pluginsSectionDeepDive(client) { + const pluginId = "browser-tools"; + const marketplaceState = await waitForValue( + () => + evaluate( + client, + `(() => { + const search = document.querySelector('[aria-label="Search plugins"]'); + const action = document.querySelector("#plugin-browser-tools-action"); + return { + visible: Boolean(search && !search.closest("[hidden]")), + pluginCount: document.querySelectorAll("[data-plugin-id]").length, + action: action?.textContent?.trim(), + initialInstalled: window.__poracodeDev.stores.sharedSettings.getState().installedPlugins["browser-tools"] !== undefined, + }; + })()`, + ), + (state) => state.visible && state.pluginCount > 0 && Boolean(state.action), + "plugins marketplace", + ); + assert( + marketplaceState.action === (marketplaceState.initialInstalled ? "Manage" : "Install"), + `Browser Tools marketplace action did not match install state: ${JSON.stringify(marketplaceState)}`, + ); + + let detailOpened = false; + try { + const opened = await evaluate( + client, + `(() => { + const action = document.querySelector("#plugin-browser-tools-action")?.closest("button"); + if (!(action instanceof HTMLButtonElement)) return false; + action.click(); + return true; + })()`, + ); + assert(opened, "Browser Tools marketplace action was unavailable"); + detailOpened = true; + + const detailState = await waitForValue( + () => + evaluate( + client, + `(() => { + const buttonText = [...document.querySelectorAll("button")].map((button) => button.textContent?.trim()); + const headings = [...document.querySelectorAll("h2")].map((heading) => heading.textContent?.trim()); + const switchNames = [...document.querySelectorAll('[role="switch"]')].map((control) => + (control.getAttribute("aria-labelledby") ?? "") + .split(/\\s+/u) + .map((id) => document.getElementById(id)?.textContent?.trim() ?? "") + .filter(Boolean) + .join(" "), + ); + return { + installed: window.__poracodeDev.stores.sharedSettings.getState().installedPlugins["browser-tools"] !== undefined, + back: buttonText.includes("Back to plugins"), + uninstall: buttonText.includes("Uninstall"), + apps: headings.includes("Apps") && document.body.innerText.includes("Browser"), + skills: headings.includes("Skills") && document.body.innerText.includes("Browser Control"), + appSwitch: switchNames.includes("Browser MCP"), + skillSwitch: switchNames.includes("Browser Control Skill"), + }; + })()`, + ), + (state) => + state.installed && + state.back && + state.uninstall && + state.apps && + state.skills && + state.appSwitch && + state.skillSwitch, + "Browser Tools plugin detail", + ); + assert(detailState.apps && detailState.skills, "Browser Tools contributions did not render"); + assert( + detailState.appSwitch && detailState.skillSwitch, + "Browser Tools contribution controls did not render", + ); + + const pluginsScreenshotPath = join(outDir, "smoke-02-plugins.png"); + await screenshot(client, pluginsScreenshotPath); + + if (!marketplaceState.initialInstalled) { + const uninstalled = await evaluate( + client, + `(() => { + const button = [...document.querySelectorAll("button")].find( + (candidate) => candidate.textContent?.trim() === "Uninstall", + ); + if (!(button instanceof HTMLButtonElement)) return false; + button.click(); + return true; + })()`, + ); + assert(uninstalled, "Browser Tools uninstall action was unavailable"); + await waitForValue( + () => + evaluate( + client, + `window.__poracodeDev.stores.sharedSettings.getState().installedPlugins["browser-tools"] === undefined`, + ), + Boolean, + "Browser Tools install-state restoration", + ); + } + + const restored = await evaluate( + client, + `window.__poracodeDev.stores.sharedSettings.getState().installedPlugins[${JSON.stringify(pluginId)}] !== undefined`, + ); + assert( + restored === marketplaceState.initialInstalled, + "Browser Tools install state was not restored", + ); + return { pluginsScreenshotPath }; + } finally { + await evaluate( + client, + `(() => { + const store = window.__poracodeDev.stores.sharedSettings.getState(); + const installed = store.installedPlugins[${JSON.stringify(pluginId)}] !== undefined; + if (${JSON.stringify(marketplaceState.initialInstalled)} && !installed) { + store.installPlugin(${JSON.stringify(pluginId)}); + } else if (!${JSON.stringify(marketplaceState.initialInstalled)} && installed) { + store.uninstallPlugin(${JSON.stringify(pluginId)}); + } + })()`, + ); + if (detailOpened) { + await evaluate( + client, + `(() => { + const button = [...document.querySelectorAll("button")].find( + (candidate) => candidate.textContent?.trim() === "Back to plugins", + ); + if (button instanceof HTMLButtonElement) button.click(); + })()`, + ); + } + } +} + async function skillsSectionDeepDive(client) { const toolbarState = await evaluate( client, diff --git a/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs b/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs index 308dd040b..0b5017b33 100644 --- a/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs +++ b/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs @@ -87,6 +87,17 @@ export const functionalAreas = [ automated: ["baseline", "settings"], manual: [], }, + { + id: "plugins-marketplace", + title: "Plugin marketplace, installation, and contributed apps and skills", + patterns: [ + /components\/plugins\//i, + /shared\/(?:contracts\/plugin|plugins\/)/i, + /PluginsSettings/i, + ], + automated: ["settings"], + manual: [], + }, { id: "updates-auth-usage", title: "Updates, authentication, usage, notifications, and diagnostics", diff --git a/resources/skills/browser-control/SKILL.md b/resources/skills/browser-control/SKILL.md new file mode 100644 index 000000000..d8d57fe44 --- /dev/null +++ b/resources/skills/browser-control/SKILL.md @@ -0,0 +1,18 @@ +--- +name: browser-control +description: Navigate, inspect, and test pages with Poracode's isolated in-app browser. +--- + +# Browser Control + +Use Poracode's Browser MCP when the task depends on a website, rendered page, or local web app. + +## Workflow + +1. List the available browser tabs and reuse the relevant tab when possible. +2. Inspect the current URL and page snapshot before interacting. +3. Prefer semantic queries and targeted reads over coordinate-based actions. +4. After navigation or a state-changing action, wait for the expected page state and verify it. +5. Use screenshots when visual layout is part of the requirement. + +The in-app browser is isolated from the user's personal Chrome profile. Do not assume it contains the user's existing logins or extensions. diff --git a/resources/skills/chrome-control/SKILL.md b/resources/skills/chrome-control/SKILL.md new file mode 100644 index 000000000..387e546ac --- /dev/null +++ b/resources/skills/chrome-control/SKILL.md @@ -0,0 +1,18 @@ +--- +name: chrome-control +description: Work safely with pages and signed-in sessions already open in the user's Chrome browser. +--- + +# Chrome Control + +Use Poracode's Chrome MCP when a task depends on the user's current Chrome tabs, authenticated sessions, or installed extensions. + +## Workflow + +1. List tabs and attach to the relevant existing tab instead of opening duplicates. +2. Inspect the page before typing, clicking, or evaluating scripts. +3. Keep actions scoped to the requested site and task. +4. Verify the resulting URL or visible state after each meaningful action. +5. Pause before irreversible submissions, purchases, deletions, or messages unless the user explicitly authorized them. + +Treat cookies, page storage, and signed-in content as sensitive user data. diff --git a/resources/skills/computer-use/SKILL.md b/resources/skills/computer-use/SKILL.md new file mode 100644 index 000000000..d510c5cc6 --- /dev/null +++ b/resources/skills/computer-use/SKILL.md @@ -0,0 +1,18 @@ +--- +name: computer-use +description: Operate supported desktop apps through Poracode's desktop-control tools. +--- + +# Computer Use + +Use Poracode's Computer Use MCP for tasks that require interacting with desktop applications or native windows. + +## Workflow + +1. List applications and windows, then select the exact target. +2. Activate and inspect the target window before interacting. +3. Prefer named controls and keyboard shortcuts when they are reliable. +4. Use small, verifiable interaction steps and check the window state after each one. +5. Do not type secrets into an application unless the user explicitly supplied them for that purpose. + +Ask before destructive actions or actions that communicate externally when authorization is unclear. diff --git a/resources/skills/subagent-delegation/SKILL.md b/resources/skills/subagent-delegation/SKILL.md new file mode 100644 index 000000000..deb8b24a4 --- /dev/null +++ b/resources/skills/subagent-delegation/SKILL.md @@ -0,0 +1,18 @@ +--- +name: subagent-delegation +description: Choose, brief, and coordinate Poracode subagents for parallel work. +--- + +# Subagent Delegation + +Use Poracode's Subagents MCP when independent, bounded work can run in parallel or when a specialist agent is a better fit. + +## Workflow + +1. Split work into concrete subtasks with clear deliverables and non-overlapping edit scope. +2. Choose an installed agent whose capabilities match each subtask. +3. Include the relevant context, constraints, and verification requirements in every brief. +4. Track active agents and resolve shared-worktree conflicts before accepting results. +5. Consolidate and verify all returned work against the original request. + +Do not delegate a task merely to avoid understanding it. The coordinating agent remains responsible for the final result. diff --git a/src/main/main.ts b/src/main/main.ts index 81152f4d3..a8f6ff6c6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -24,7 +24,10 @@ import { initDatabase, } from "./db"; import { cleanupOrphanedAttachments, preparePoracodeDataRoot } from "./poracodeData"; -import { handleOrchestratorThreadCreated } from "./orchestratorThreadBridge"; +import { + enforcePersistedThreadLaunchInvariants, + handleOrchestratorThreadCreated, +} from "./orchestratorThreadBridge"; import { createLocalIpcHandlers, showAddFilesDialog } from "./ipc/localHandlers"; import { registerIpcHandlers } from "./ipc/registerHandlers"; import { createSleepInhibitor } from "./sleepInhibitor"; @@ -642,6 +645,7 @@ if (!hasSingleInstanceLock) { wslHelpersDir, bundledSkillsDir, secretStorageKey, + prepareStartThread: enforcePersistedThreadLaunchInvariants, resolveExtraEnv: () => { const env: Record = {}; const browserInfo = browserMcpIngress?.getInfo(); diff --git a/src/main/orchestratorThreadBridge.test.ts b/src/main/orchestratorThreadBridge.test.ts new file mode 100644 index 000000000..2d6122907 --- /dev/null +++ b/src/main/orchestratorThreadBridge.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import type { StartThreadPayload } from "@/shared/contracts"; +import { enforcePersistedThreadLaunchInvariants } from "./orchestratorThreadBridge"; + +const payload: StartThreadPayload = { + threadId: "child-thread", + projectLocation: { kind: "windows", path: "C:\\repo" }, + agentKind: "codex", + config: { model: "test" }, + prompt: "Inspect this.", + initialSize: { cols: 120, rows: 40 }, +}; + +describe("enforcePersistedThreadLaunchInvariants", () => { + it("forces subagent recursion off for a persisted child thread", () => { + expect( + enforcePersistedThreadLaunchInvariants( + { ...payload, invariantDisabledBuiltInMcpServerIds: ["browser"] }, + () => ({ parentThreadId: "parent-thread" }), + ), + ).toMatchObject({ + invariantDisabledBuiltInMcpServerIds: ["browser", "subagents"], + }); + }); + + it("leaves non-child launches unchanged", () => { + expect(enforcePersistedThreadLaunchInvariants(payload, () => ({}))).toBe(payload); + }); +}); diff --git a/src/main/orchestratorThreadBridge.ts b/src/main/orchestratorThreadBridge.ts index 016fe0e48..0a444fb04 100644 --- a/src/main/orchestratorThreadBridge.ts +++ b/src/main/orchestratorThreadBridge.ts @@ -14,6 +14,19 @@ export interface OrchestratorThreadBridgeDeps { sendThreadCommand(command: RemoteThreadCommand): boolean; } +export function enforcePersistedThreadLaunchInvariants( + payload: StartThreadPayload, + getThread: (threadId: string) => Pick | null = dbGetThread, +): StartThreadPayload { + if (!payload.threadId || !getThread(payload.threadId)?.parentThreadId) return payload; + return { + ...payload, + invariantDisabledBuiltInMcpServerIds: [ + ...new Set([...(payload.invariantDisabledBuiltInMcpServerIds ?? []), "subagents" as const]), + ], + }; +} + /** * Main-process half of the subagents MCP orchestrator lane's `create_thread`. * Mirrors the proven remote (mobile) start ordering exactly: diff --git a/src/main/sharedSettingsFile.test.ts b/src/main/sharedSettingsFile.test.ts index 8a12c9950..a19875497 100644 --- a/src/main/sharedSettingsFile.test.ts +++ b/src/main/sharedSettingsFile.test.ts @@ -125,6 +125,7 @@ describe("sharedSettingsFile", () => { mcpServers: [], disabledBuiltInMcpServers: {}, disabledBuiltInMcpTools: {}, + installedPlugins: {}, browser: { allowEval: false, allowDataAccess: false, @@ -240,6 +241,7 @@ describe("sharedSettingsFile", () => { mcpServers: [], disabledBuiltInMcpServers: {}, disabledBuiltInMcpTools: {}, + installedPlugins: {}, browser: { allowEval: false, allowDataAccess: false, diff --git a/src/main/supervisor/SupervisorClient.test.ts b/src/main/supervisor/SupervisorClient.test.ts index 6a6ac20c5..2aa05eb3e 100644 --- a/src/main/supervisor/SupervisorClient.test.ts +++ b/src/main/supervisor/SupervisorClient.test.ts @@ -13,7 +13,7 @@ vi.mock("@/shared/processTree", () => ({ terminateChildProcessTree: vi.fn<() => void>(), })); -import { SupervisorClient } from "./SupervisorClient"; +import { SupervisorClient, type SupervisorClientOptions } from "./SupervisorClient"; type SendCallback = (error?: Error | null) => void; @@ -36,7 +36,7 @@ function makeFakeChild(): FakeChild { return child; } -function makeClient() { +function makeClient(options: Pick = {}) { const child = makeFakeChild(); forkMock.mockReturnValue(child); const client = new SupervisorClient({ @@ -47,6 +47,7 @@ function makeClient() { secretStorageKey: "key", onEvent: vi.fn<(event: SupervisorEvent) => void>(), onReset: vi.fn<() => void>(), + ...options, }); client.start("/base"); return { client, child }; @@ -109,6 +110,35 @@ describe("SupervisorClient.call", () => { await expect(promise).resolves.toBe("result-value"); }); + it("applies main-process start invariants before sending the request", async () => { + const { client, child } = makeClient({ + prepareStartThread: (payload) => ({ + ...payload, + invariantDisabledBuiltInMcpServerIds: ["subagents"], + }), + }); + let request: { id: string; payload: unknown } | undefined; + child.send.mockImplementation((message, callback) => { + request = message as { id: string; payload: unknown }; + callback?.(); + return true; + }); + const promise = client.call("startThread", { + threadId: "child-thread", + projectLocation: { kind: "windows", path: "C:\\repo" }, + agentKind: "codex", + config: { model: "test" }, + prompt: "Inspect this.", + initialSize: { cols: 120, rows: 40 }, + }); + await vi.waitFor(() => expect(request).toBeDefined()); + expect(request?.payload).toMatchObject({ + invariantDisabledBuiltInMcpServerIds: ["subagents"], + }); + child.emit("message", { replyTo: request!.id, ok: true, data: { threadId: "child-thread" } }); + await expect(promise).resolves.toEqual({ threadId: "child-thread" }); + }); + it("rejects when the reply reports failure", async () => { const { client, child } = makeClient(); const getId = captureSentId(child); diff --git a/src/main/supervisor/SupervisorClient.ts b/src/main/supervisor/SupervisorClient.ts index 64124b6f7..5c7a2552e 100644 --- a/src/main/supervisor/SupervisorClient.ts +++ b/src/main/supervisor/SupervisorClient.ts @@ -3,6 +3,7 @@ import type { Readable } from "node:stream"; import { randomUUID } from "node:crypto"; import type { PoracodeDiagnosticTags } from "@/shared/diagnostics/sentryPrivacy"; import { terminateChildProcessTree } from "@/shared/processTree"; +import type { StartThreadPayload } from "@/shared/contracts"; import type { IpcProcedurePayload, IpcProcedureResult, @@ -67,6 +68,8 @@ export interface SupervisorClientOptions { * to inject `PORACODE_BROWSER_MCP_*` per-launch. */ resolveExtraEnv?: () => Record; + /** Apply main-process launch invariants before any start reaches the supervisor. */ + prepareStartThread?(payload: StartThreadPayload): StartThreadPayload; assignPid?(pid: number): Promise; reportError?(error: unknown, tags?: PoracodeDiagnosticTags): void; onEvent(event: SupervisorEvent): void; @@ -208,10 +211,14 @@ export class SupervisorClient { } const id = randomUUID(); + const requestPayload = + type === "startThread" && this.options.prepareStartThread + ? this.options.prepareStartThread(payload as StartThreadPayload) + : payload; const request: SupervisorRequest = { id, type, - payload, + payload: requestPayload, } as SupervisorRequest; return new Promise>((resolve, reject) => { diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 80812d195..98e15193f 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -18,6 +18,7 @@ import { } from "./notifications"; import { useAppStore } from "./state/appStore"; +import { normalizeRuntimeSnapshotLaunchConfig } from "./state/slices/threadSlice"; import { archiveThread, deleteThread, @@ -213,7 +214,7 @@ function handleSupervisorEvent(event: SupervisorEvent): void { const oldThread = shouldCheckNotifications ? appStore.threads.find((t) => t.id === event.threadId) : undefined; - appStore.updateThreadRuntime(event.threadId, event); + appStore.updateThreadRuntime(event.threadId, normalizeRuntimeSnapshotLaunchConfig(event)); if (shouldCheckNotifications) { const newThread = useAppStore.getState().threads.find((t) => t.id === event.threadId); handleThreadStateNotification(event, oldThread, newThread); diff --git a/src/renderer/components/common/ToggleSwitch.tsx b/src/renderer/components/common/ToggleSwitch.tsx index 70c159a2d..7dbc290e6 100644 --- a/src/renderer/components/common/ToggleSwitch.tsx +++ b/src/renderer/components/common/ToggleSwitch.tsx @@ -7,7 +7,11 @@ type ToggleSwitchBaseProps = Omit< >; export type ToggleSwitchProps = ToggleSwitchBaseProps & - ({ "aria-label": string; children?: never } | { "aria-label"?: string; children: ReactNode }); + ( + | { "aria-label": string; "aria-labelledby"?: never; children?: never } + | { "aria-label"?: never; "aria-labelledby": string; children?: never } + | { "aria-label"?: string; "aria-labelledby"?: string; children: ReactNode } + ); /** * App switch with the interactive HeroUI anatomy included. diff --git a/src/renderer/components/composer/composerMcpServers.ts b/src/renderer/components/composer/composerMcpServers.ts index bb27e540a..996e5c6a2 100644 --- a/src/renderer/components/composer/composerMcpServers.ts +++ b/src/renderer/components/composer/composerMcpServers.ts @@ -1,10 +1,10 @@ import { msg } from "@lingui/core/macro"; import type { MessageDescriptor } from "@lingui/core"; import { AppWindow, Globe, Users, type LucideIcon } from "lucide-react"; +import { resolveComposerMcpScope } from "@/shared/contracts"; import type { AgentCapability, ComposerMcpScope, - ComposerMcpScopes, ProjectLocation, ThreadConfig, ThreadPresentationMode, @@ -32,13 +32,10 @@ export type ComposerMcpConfigKey = "browserMcp" | "subagentMcp" | "chromeMcp"; * terminal TUIs have no per-thread gating point ("none"). */ export function resolveMcpScope( - scopes: ComposerMcpScopes | undefined, + scopes: AgentCapability["browserMcpScope"], presentationMode: ThreadPresentationMode, ): ComposerMcpScope { - if (presentationMode === "gui") { - return scopes?.gui ?? "launch"; - } - return scopes?.terminal ?? "none"; + return resolveComposerMcpScope(scopes, presentationMode); } export interface ComposerMcpServerDescriptor { diff --git a/src/renderer/components/mcp/McpServersManager.test.tsx b/src/renderer/components/mcp/McpServersManager.test.tsx index f6b9b8998..67d944b20 100644 --- a/src/renderer/components/mcp/McpServersManager.test.tsx +++ b/src/renderer/components/mcp/McpServersManager.test.tsx @@ -45,6 +45,7 @@ function managerElement(options: { additionalProjects?: McpImportProjectTarget[]; disabledBuiltIns?: Record; disabledBuiltInTools?: Record; + managedBuiltIns?: Partial>; onBuiltInDisabledChange?: (id: string, disabled: boolean) => void; onBuiltInToolEnabledChange?: (id: BuiltInMcpServerId, tool: string, enabled: boolean) => void; includeSubagentsSettings?: boolean; @@ -59,6 +60,7 @@ function managerElement(options: { additionalProjects = [], disabledBuiltIns, disabledBuiltInTools, + managedBuiltIns, onBuiltInDisabledChange, onBuiltInToolEnabledChange, includeSubagentsSettings, @@ -97,6 +99,7 @@ function managerElement(options: { defaultScope={defaultScope} {...(disabledBuiltIns ? { disabledBuiltIns } : {})} {...(disabledBuiltInTools ? { disabledBuiltInTools } : {})} + {...(managedBuiltIns ? { managedBuiltIns } : {})} {...(onBuiltInDisabledChange ? { onBuiltInDisabledChange } : {})} {...(onBuiltInToolEnabledChange ? { onBuiltInToolEnabledChange } : {})} {...(includeSubagentsSettings @@ -141,6 +144,33 @@ describe("McpServersManager", () => { expect(screen.getByRole("button", { name: "44 tools" })).toBeInTheDocument(); }); + it("identifies plugin-managed built-ins without exposing edit or delete controls", () => { + render( + managerElement({ + disabledBuiltIns: {}, + managedBuiltIns: { browser: "Browser Tools" }, + onBuiltInDisabledChange: () => undefined, + }), + ); + + const row = document.querySelector('[data-built-in-mcp-server="browser"]'); + expect(row).not.toBeNull(); + expect(within(row as HTMLElement).getByText("Managed by Browser Tools")).toBeInTheDocument(); + expect(within(row as HTMLElement).queryByText("Built-in")).not.toBeInTheDocument(); + expect( + within(row as HTMLElement).queryByRole("button", { name: "Edit Browser" }), + ).not.toBeInTheDocument(); + expect( + within(row as HTMLElement).queryByRole("button", { name: "Delete Browser" }), + ).not.toBeInTheDocument(); + + fireEvent.change(screen.getByRole("textbox", { name: "Search MCP servers" }), { + target: { value: "Browser Tools" }, + }); + expect(document.querySelector('[data-built-in-mcp-server="browser"]')).not.toBeNull(); + expect(document.querySelector('[data-built-in-mcp-server="chrome"]')).toBeNull(); + }); + it("shows the built-in tool list from its tool count", () => { const onBuiltInToolEnabledChange = vi.fn<(id: BuiltInMcpServerId, tool: string, enabled: boolean) => void>(); diff --git a/src/renderer/components/mcp/McpServersManager.tsx b/src/renderer/components/mcp/McpServersManager.tsx index 72fcc139e..87489d664 100644 --- a/src/renderer/components/mcp/McpServersManager.tsx +++ b/src/renderer/components/mcp/McpServersManager.tsx @@ -95,6 +95,7 @@ export function McpServersManager(props: { onBuiltInDisabledChange?: (id: BuiltInMcpServerId, disabled: boolean) => void; onBuiltInToolEnabledChange?: (id: BuiltInMcpServerId, tool: string, enabled: boolean) => void; builtInSettings?: Partial>; + managedBuiltIns?: Partial>; }) { const { t } = useLingui(); const [query, setQuery] = useState(""); @@ -221,7 +222,14 @@ export function McpServersManager(props: { }, ]; const visibleBuiltIns = builtIns.filter((server) => - [server.name, server.label, server.description, server.settingsLabel, server.tools.join(" ")] + [ + server.name, + server.label, + server.description, + server.settingsLabel, + props.managedBuiltIns?.[server.id], + server.tools.join(" "), + ] .join(" ") .toLowerCase() .includes(normalizedQuery), @@ -554,6 +562,9 @@ export function McpServersManager(props: { key={server.id} server={server} disabled={props.disabledBuiltIns?.[server.id] === true} + {...(props.managedBuiltIns?.[server.id] + ? { managedByPlugin: props.managedBuiltIns[server.id] } + : {})} onToggle={(enabled) => props.onBuiltInDisabledChange?.(server.id, !enabled)} onViewTools={() => setToolList({ @@ -833,6 +844,7 @@ function BuiltInServerRow(props: { onToggle: (enabled: boolean) => void; onViewTools: () => void; onSettings?: () => void; + managedByPlugin?: string; }) { const { t } = useLingui(); const enabled = !props.disabled; @@ -852,7 +864,11 @@ function BuiltInServerRow(props: { {props.server.label} {props.server.name} - {t`Built-in`} + {props.managedByPlugin ? ( + {t`Managed by ${props.managedByPlugin}`} + ) : ( + {t`Built-in`} + )}

{props.server.description}

diff --git a/src/renderer/components/plugins/PluginDetail.test.tsx b/src/renderer/components/plugins/PluginDetail.test.tsx new file mode 100644 index 000000000..f40dcde88 --- /dev/null +++ b/src/renderer/components/plugins/PluginDetail.test.tsx @@ -0,0 +1,71 @@ +import { fireEvent, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithI18n as render } from "@/renderer/testUtils/i18n"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { PluginDetail } from "./PluginDetail"; +import { useLocalizedPluginCatalog } from "./pluginCopy"; + +function BrowserPluginDetail(props: { onBack?: () => void }) { + const plugin = useLocalizedPluginCatalog().find( + (candidate) => candidate.manifest.id === "browser-tools", + )!; + return ( + undefined)} /> + ); +} + +function ComputerUsePluginDetail() { + const plugin = useLocalizedPluginCatalog().find( + (candidate) => candidate.manifest.id === "computer-use", + )!; + return undefined} />; +} + +describe("PluginDetail", () => { + beforeEach(() => { + localStorage.clear(); + useSharedSettings.setState({ installedPlugins: {} }); + }); + + it("updates plugin, app, and skill toggles and uninstalls the bundle", () => { + useSharedSettings.getState().installPlugin("browser-tools"); + render(); + + fireEvent.click(screen.getByRole("switch", { name: "Browser MCP" })); + expect(useSharedSettings.getState().installedPlugins["browser-tools"]?.disabledAppIds).toEqual([ + "browser", + ]); + expect(screen.getByRole("switch", { name: "Browser MCP" })).not.toBeChecked(); + + fireEvent.click(screen.getByRole("switch", { name: "Browser Control Skill" })); + expect( + useSharedSettings.getState().installedPlugins["browser-tools"]?.disabledSkillIds, + ).toEqual(["browser-control"]); + expect(screen.getByRole("switch", { name: "Browser Control Skill" })).not.toBeChecked(); + + fireEvent.click(screen.getByRole("switch", { name: "Browser Tools Enable plugin" })); + expect(useSharedSettings.getState().installedPlugins["browser-tools"]?.enabled).toBe(false); + expect(screen.getByRole("switch", { name: "Browser MCP" })).toBeDisabled(); + expect(screen.getByRole("switch", { name: "Browser Control Skill" })).toBeDisabled(); + + fireEvent.click(screen.getByRole("button", { name: "Uninstall" })); + expect(useSharedSettings.getState().installedPlugins["browser-tools"]).toBeUndefined(); + expect(screen.getByRole("button", { name: "Install" })).toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "Enable plugin" })).not.toBeInTheDocument(); + }); + + it("returns to the marketplace", () => { + const onBack = vi.fn<() => void>(); + render(); + + fireEvent.click(screen.getByRole("button", { name: "Back to plugins" })); + expect(onBack).toHaveBeenCalledOnce(); + }); + + it("disables installation when the plugin is unavailable on this device", () => { + render(); + + expect(screen.getByRole("button", { name: "Unavailable on this device" })).toBeDisabled(); + expect(screen.getByText("Unavailable on this device", { selector: "p" })).toBeInTheDocument(); + }); +}); diff --git a/src/renderer/components/plugins/PluginDetail.tsx b/src/renderer/components/plugins/PluginDetail.tsx new file mode 100644 index 000000000..77c99b738 --- /dev/null +++ b/src/renderer/components/plugins/PluginDetail.tsx @@ -0,0 +1,252 @@ +import { ArrowLeft, Box, Cable } from "lucide-react"; +import { Trans, useLingui } from "@lingui/react/macro"; +import { useEffect, useId, useRef, type ReactNode } from "react"; +import { Button, ToggleSwitch } from "@/renderer/components/common"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { + isPluginAppEnabled, + isPluginSkillEnabled, + isPluginSupportedOnHost, +} from "@/shared/plugins/catalog"; +import { PluginIcon } from "./PluginIcon"; +import type { LocalizedPlugin } from "./pluginCopy"; + +export function PluginDetail(props: { + plugin: LocalizedPlugin; + hostPlatform: NodeJS.Platform; + onBack: () => void; +}) { + const { t } = useLingui(); + const state = useSharedSettings( + (settings) => settings.installedPlugins[props.plugin.manifest.id], + ); + const installPlugin = useSharedSettings((settings) => settings.installPlugin); + const uninstallPlugin = useSharedSettings((settings) => settings.uninstallPlugin); + const setPluginEnabled = useSharedSettings((settings) => settings.setPluginEnabled); + const setPluginSkillEnabled = useSharedSettings((settings) => settings.setPluginSkillEnabled); + const setPluginAppEnabled = useSharedSettings((settings) => settings.setPluginAppEnabled); + const manifest = props.plugin.manifest; + const supported = isPluginSupportedOnHost(manifest, props.hostPlatform); + const backButtonRef = useRef(null); + const titleId = useId(); + const pluginToggleLabelId = useId(); + + useEffect(() => { + backButtonRef.current?.focus(); + }, []); + + return ( +
+ + +
+
+ +
+
+
+
+

+ {props.plugin.name} +

+

{props.plugin.description}

+
+ {state ? ( + + ) : ( + + )} +
+
+ {manifest.publisher} + + {props.plugin.category} + + v{manifest.version} +
+ {!supported ? ( +

+ Unavailable on this device +

+ ) : null} +
+
+ + {state ? ( +
+
+

+ Enable plugin +

+

+ Enable this plugin's active skills and apps for new threads. +

+
+ setPluginEnabled(manifest.id, enabled)} + /> +
+ ) : null} + + } + title={t`Apps`} + description={t`MCP-powered tools contributed by this plugin.`} + > + {manifest.apps.map((app, index) => { + const copy = props.plugin.apps.find((candidate) => candidate.id === app.id)!; + const enabled = state ? isPluginAppEnabled(manifest, state, app.id) : app.defaultEnabled; + const labelId = `${titleId}-app-${app.id}`; + const badgeId = `${labelId}-kind`; + return ( + setPluginAppEnabled(manifest.id, app.id, next)} + /> + ) : undefined + } + /> + ); + })} + + + } + title={t`Skills`} + description={t`Reusable guidance delivered across supported agents.`} + > + {manifest.skills.map((skill, index) => { + const copy = props.plugin.skills.find((candidate) => candidate.id === skill.id)!; + const enabled = state + ? isPluginSkillEnabled(manifest, state, skill.id) + : skill.defaultEnabled; + const labelId = `${titleId}-skill-${skill.id}`; + const badgeId = `${labelId}-kind`; + return ( + setPluginSkillEnabled(manifest.id, skill.id, next)} + /> + ) : undefined + } + /> + ); + })} + + +
+

+ Information +

+
+
+ Publisher +
+
{manifest.publisher}
+
+ Category +
+
{props.plugin.category}
+
+ Version +
+
{manifest.version}
+
+
+
+ ); +} + +function ContributionSection(props: { + icon: ReactNode; + title: string; + description: string; + children: ReactNode; +}) { + return ( +
+
+ {props.icon} +
+

{props.title}

+

{props.description}

+
+
+
+ {props.children} +
+
+ ); +} + +function ContributionRow(props: { + labelId: string; + badgeId: string; + name: string; + description: string; + badge: string; + control?: ReactNode; + last: boolean; +}) { + return ( +
+
+
+ + {props.name} + + + {props.badge} + +
+

{props.description}

+
+ {props.control} +
+ ); +} diff --git a/src/renderer/components/plugins/PluginIcon.tsx b/src/renderer/components/plugins/PluginIcon.tsx new file mode 100644 index 000000000..793cd91f9 --- /dev/null +++ b/src/renderer/components/plugins/PluginIcon.tsx @@ -0,0 +1,17 @@ +import { AppWindow, Globe, Monitor, Network, Puzzle } from "lucide-react"; + +export function PluginIcon(props: { pluginId: string; className?: string }) { + const className = props.className ?? "size-5"; + switch (props.pluginId) { + case "browser-tools": + return ; + case "chrome-tools": + return ; + case "computer-use": + return ; + case "subagent-delegation": + return ; + default: + return ; + } +} diff --git a/src/renderer/components/plugins/PluginMarketplace.test.tsx b/src/renderer/components/plugins/PluginMarketplace.test.tsx new file mode 100644 index 000000000..a40038080 --- /dev/null +++ b/src/renderer/components/plugins/PluginMarketplace.test.tsx @@ -0,0 +1,97 @@ +import { fireEvent, screen, within } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithI18n as render } from "@/renderer/testUtils/i18n"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { useLocalizedPluginCatalog } from "./pluginCopy"; +import { PluginMarketplace } from "./PluginMarketplace"; + +function Marketplace(props: { onOpen: (pluginId: string) => void }) { + const plugins = useLocalizedPluginCatalog(); + return ; +} + +describe("PluginMarketplace", () => { + beforeEach(() => { + localStorage.clear(); + useSharedSettings.setState({ installedPlugins: {} }); + }); + + it("discovers plugins by contribution text, installs one, and exposes management", () => { + const onOpen = vi.fn<(pluginId: string) => void>(); + render(); + + expect(screen.getByRole("tab", { name: "Discover" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByText("Browser Tools")).toBeInTheDocument(); + expect(screen.getByText("Chrome Tools")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Browser Tools Install" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Chrome Tools Install" })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Browser Tools" })); + expect(onOpen).toHaveBeenCalledWith("browser-tools"); + onOpen.mockClear(); + + fireEvent.change(screen.getByRole("textbox", { name: "Search plugins" }), { + target: { value: "navigate" }, + }); + + expect(screen.getByText("Browser Tools")).toBeInTheDocument(); + expect(screen.queryByText("Chrome Tools")).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Browser Tools Install" })); + + expect(useSharedSettings.getState().installedPlugins["browser-tools"]).toMatchObject({ + version: "1.0.0", + enabled: true, + }); + expect(onOpen).toHaveBeenLastCalledWith("browser-tools"); + + fireEvent.click(screen.getByRole("button", { name: "Browser Tools Manage" })); + expect(onOpen).toHaveBeenCalledTimes(2); + expect(onOpen).toHaveBeenLastCalledWith("browser-tools"); + }); + + it("shows only installed plugins and links an empty installation list back to Discover", () => { + useSharedSettings.getState().installPlugin("chrome-tools"); + const onOpen = vi.fn<(pluginId: string) => void>(); + const { unmount } = render(); + + fireEvent.click(screen.getByRole("tab", { name: /^Installed/u })); + + expect(screen.getByText("Chrome Tools")).toBeInTheDocument(); + expect(screen.queryByText("Browser Tools")).not.toBeInTheDocument(); + const installedTab = screen.getByRole("tab", { name: /^Installed/u }); + expect(installedTab).toHaveAttribute("aria-selected", "true"); + fireEvent.click(screen.getByRole("button", { name: "Chrome Tools Manage" })); + expect(onOpen).toHaveBeenCalledWith("chrome-tools"); + + unmount(); + useSharedSettings.setState({ installedPlugins: {} }); + render(); + fireEvent.click(screen.getByRole("tab", { name: /^Installed/u })); + + expect(screen.getByText("No plugins installed yet")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Discover plugins" })); + expect(screen.getByRole("tab", { name: "Discover" })).toHaveAttribute("aria-selected", "true"); + expect(screen.queryByText("No plugins installed yet")).not.toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "Featured" })).toBeInTheDocument(); + }); + + it("does not install a plugin that is unavailable on this host", () => { + const onOpen = vi.fn<(pluginId: string) => void>(); + + function LinuxMarketplace() { + const plugins = useLocalizedPluginCatalog(); + return ; + } + + render(); + + const computerUseCard = screen + .getByText("Computer Use") + .closest("[class*='min-h-40']")!; + expect( + within(computerUseCard).getByRole("button", { + name: "Computer Use Unavailable on this device", + }), + ).toBeDisabled(); + expect(useSharedSettings.getState().installedPlugins["computer-use"]).toBeUndefined(); + }); +}); diff --git a/src/renderer/components/plugins/PluginMarketplace.tsx b/src/renderer/components/plugins/PluginMarketplace.tsx new file mode 100644 index 000000000..3b004aa88 --- /dev/null +++ b/src/renderer/components/plugins/PluginMarketplace.tsx @@ -0,0 +1,184 @@ +import { Card, Input } from "@heroui/react"; +import { Plural, Trans, useLingui } from "@lingui/react/macro"; +import { Search } from "lucide-react"; +import { useState } from "react"; +import { Button, LightballTabs } from "@/renderer/components/common"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { isPluginSupportedOnHost } from "@/shared/plugins/catalog"; +import { PluginIcon } from "./PluginIcon"; +import type { LocalizedPlugin } from "./pluginCopy"; + +type MarketplaceTab = "installed" | "discover"; + +export function PluginMarketplace(props: { + plugins: readonly LocalizedPlugin[]; + hostPlatform: NodeJS.Platform; + onOpen: (pluginId: string) => void; +}) { + const { t } = useLingui(); + const [tab, setTab] = useState("discover"); + const [query, setQuery] = useState(""); + const installedPlugins = useSharedSettings((state) => state.installedPlugins); + const installPlugin = useSharedSettings((state) => state.installPlugin); + const normalizedQuery = query.trim().toLowerCase(); + const visiblePlugins = props.plugins.filter((plugin) => { + if (tab === "installed" && !installedPlugins[plugin.manifest.id]) return false; + return [ + plugin.name, + plugin.description, + plugin.category, + ...plugin.skills.flatMap((skill) => [skill.name, skill.description]), + ...plugin.apps.flatMap((app) => [app.name, app.description]), + ] + .join(" ") + .toLowerCase() + .includes(normalizedQuery); + }); + + const tabs = [ + { + id: "installed" as const, + label: t`Installed`, + trailing: props.plugins.filter((plugin) => installedPlugins[plugin.manifest.id]).length, + }, + { id: "discover" as const, label: t`Discover` }, + ]; + + return ( +
+
+

+ Plugins +

+ +
+

+ + Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent. + +

+
+ + setQuery(event.target.value)} + /> +
+ + {visiblePlugins.length > 0 ? ( +
+
+

+ {tab === "installed" ? Installed : Featured} +

+ {visiblePlugins.length} +
+
+ {visiblePlugins.map((plugin) => { + const installed = installedPlugins[plugin.manifest.id] !== undefined; + const supported = isPluginSupportedOnHost(plugin.manifest, props.hostPlatform); + const titleId = `plugin-${plugin.manifest.id}-title`; + const actionLabelId = `plugin-${plugin.manifest.id}-action`; + return ( + +
+
+ +
+ + + + + + {plugin.description} + + +
+ + + + {" · "} + + + {installed ? ( + + ) : ( + + )} + +
+ ); + })} +
+
+ ) : ( +
+

+ {tab === "installed" && !normalizedQuery ? ( + No plugins installed yet + ) : ( + No plugins match your search. + )} +

+ {tab === "installed" && !normalizedQuery ? ( + + ) : null} +
+ )} +
+ ); +} diff --git a/src/renderer/components/plugins/pluginCopy.ts b/src/renderer/components/plugins/pluginCopy.ts new file mode 100644 index 000000000..ff590d93b --- /dev/null +++ b/src/renderer/components/plugins/pluginCopy.ts @@ -0,0 +1,128 @@ +import { useLingui } from "@lingui/react/macro"; +import type { SkillEntry } from "@/shared/contracts"; +import { BUILT_IN_PLUGIN_MANIFESTS } from "@/shared/plugins/catalog"; + +export interface LocalizedPluginContribution { + id: string; + name: string; + description: string; +} + +export function useLocalizedPluginCatalog() { + const { t } = useLingui(); + + return BUILT_IN_PLUGIN_MANIFESTS.map((manifest) => { + let name: string; + let description: string; + switch (manifest.id) { + case "browser-tools": + name = t`Browser Tools`; + description = t`Browse, inspect, and test websites in Poracode's isolated in-app browser.`; + break; + case "chrome-tools": + name = t`Chrome Tools`; + description = t`Work with the pages and signed-in sessions already open in Chrome.`; + break; + case "computer-use": + name = t`Computer Use`; + description = t`Control desktop apps and complete visual workflows.`; + break; + case "subagent-delegation": + name = t`Subagent Delegation`; + description = t`Delegate focused work to other installed agents and coordinate the results.`; + break; + default: + name = manifest.name; + description = manifest.description; + } + + const skills = manifest.skills.map((skill): LocalizedPluginContribution => { + switch (skill.id) { + case "browser-control": + return { + id: skill.id, + name: t`Browser Control`, + description: t`Navigate, inspect, and test pages with the in-app Browser MCP.`, + }; + case "chrome-control": + return { + id: skill.id, + name: t`Chrome Control`, + description: t`Use Chrome safely when a task needs an existing browser session.`, + }; + case "computer-use": + return { + id: skill.id, + name: t`Computer Use`, + description: t`Operate desktop apps through Poracode's desktop-control tools.`, + }; + case "subagent-delegation": + return { + id: skill.id, + name: t`Subagent Delegation`, + description: t`Choose, brief, and coordinate subagents for parallel work.`, + }; + default: + return { id: skill.id, name: skill.name, description: skill.description }; + } + }); + + const apps = manifest.apps.map((app): LocalizedPluginContribution => { + switch (app.id) { + case "browser": + return { + id: app.id, + name: t`Browser`, + description: t`Control Poracode's isolated in-app browser.`, + }; + case "chrome": + return { + id: app.id, + name: t`Chrome`, + description: t`Control the user's Chrome browser through Poracode.`, + }; + case "computer-use": + return { + id: app.id, + name: t`Computer Use`, + description: t`Control supported desktop apps and windows.`, + }; + case "subagents": + return { + id: app.id, + name: t`Subagents`, + description: t`Create and coordinate Poracode agent threads.`, + }; + default: + return { id: app.id, name: app.name, description: app.description }; + } + }); + + const category = + manifest.category === "developer-tools" + ? t`Developer tools` + : manifest.category === "automation" + ? t`Automation` + : t`Productivity`; + + return { manifest, name, description, category, skills, apps }; + }); +} + +export type LocalizedPlugin = ReturnType[number]; + +export function resolveLocalizedPluginSkill( + catalog: readonly LocalizedPlugin[], + skill: Pick, +) { + const localizedPlugin = skill.pluginId + ? catalog.find((plugin) => plugin.manifest.id === skill.pluginId) + : undefined; + const pluginSkill = localizedPlugin?.manifest.skills.find( + (contribution) => contribution.folder === skill.folderName, + ); + const localizedSkill = localizedPlugin?.skills.find( + (contribution) => contribution.id === pluginSkill?.id, + ); + return { localizedPlugin, pluginSkill, localizedSkill }; +} diff --git a/src/renderer/components/skills/SkillViewModal.tsx b/src/renderer/components/skills/SkillViewModal.tsx index 03e5103fa..15071cec0 100644 --- a/src/renderer/components/skills/SkillViewModal.tsx +++ b/src/renderer/components/skills/SkillViewModal.tsx @@ -13,6 +13,7 @@ function skillMarkdownBody(content: string): string { export function SkillViewModal(props: { skill: SkillEntry; + displayName: string; projectLocation?: ProjectLocation; wslDistro?: string; onClose: () => void; @@ -69,7 +70,7 @@ export function SkillViewModal(props: { - {props.skill.name} + {props.displayName}

{props.skill.skillFilePath}

diff --git a/src/renderer/components/skills/SkillsManager.test.tsx b/src/renderer/components/skills/SkillsManager.test.tsx index 58954a4bd..ac37aa100 100644 --- a/src/renderer/components/skills/SkillsManager.test.tsx +++ b/src/renderer/components/skills/SkillsManager.test.tsx @@ -235,6 +235,58 @@ describe("SkillsManager", () => { ).not.toHaveTextContent("Enabled"); }); + it("identifies plugin-managed skills without exposing lifecycle controls", async () => { + useSkillsMock.mockReturnValue({ + scan: scan([ + skill({ + id: "global:plugin:browser-control:on", + name: "browser-control", + description: "Navigate, inspect, and test pages", + folderName: "browser-control", + absolutePath: "C:\\Users\\me\\.poracode\\plugins\\browser-tools\\browser-control", + skillFilePath: + "C:\\Users\\me\\.poracode\\plugins\\browser-tools\\browser-control\\SKILL.md", + rootPath: "C:\\Users\\me\\.poracode\\plugins\\browser-tools", + providerId: "plugin:browser-tools", + providerLabel: "Browser Tools", + providerGroupId: "plugin:browser-tools", + pluginId: "browser-tools", + pluginName: "Browser Tools", + origin: "plugin", + mutable: false, + enabled: false, + }), + ]), + loading: false, + error: undefined, + reload, + }); + + renderManager(); + + expect(screen.getByRole("heading", { name: "Browser Tools" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "View Browser Control" })).toBeInTheDocument(); + expect( + screen.getByText("Navigate, inspect, and test pages with the in-app Browser MCP."), + ).toBeInTheDocument(); + expect(screen.getByText("Plugin")).toBeInTheDocument(); + expect(screen.getByText("Managed by Browser Tools")).toBeInTheDocument(); + expect(screen.getByText("Disabled", { selector: "span" })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Delete browser-control" }), + ).not.toBeInTheDocument(); + expect(screen.queryByRole("switch", { name: /browser-control/iu })).not.toBeInTheDocument(); + + fireEvent.change(screen.getByRole("textbox", { name: "Search skills" }), { + target: { value: "in-app Browser MCP" }, + }); + expect(screen.getByRole("button", { name: "View Browser Control" })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "View Browser Control" })); + expect(screen.getByRole("heading", { name: "Browser Control" })).toBeInTheDocument(); + await waitFor(() => expect(bridge.readExternalFile).toHaveBeenCalled()); + }); + it("does not label linked skills as already imported", () => { useSkillsMock.mockReturnValue({ scan: scan([ diff --git a/src/renderer/components/skills/SkillsManager.tsx b/src/renderer/components/skills/SkillsManager.tsx index 2f42f7aaa..f6922051f 100644 --- a/src/renderer/components/skills/SkillsManager.tsx +++ b/src/renderer/components/skills/SkillsManager.tsx @@ -25,6 +25,11 @@ import { SkillViewModal } from "./SkillViewModal"; import { groupSkills } from "./skillGrouping"; import { hostGlobalScopeLabel, resolveSkillTarget, skillTargetRequest } from "./skillTargets"; import { useSkills } from "./useSkills"; +import { + resolveLocalizedPluginSkill, + useLocalizedPluginCatalog, + type LocalizedPlugin, +} from "@/renderer/components/plugins/pluginCopy"; type StatusFilter = "all" | "enabled" | "disabled"; @@ -33,6 +38,7 @@ export function SkillsManager(props: { defaultDestinationId?: string; }) { const { t } = useLingui(); + const localizedPlugins = useLocalizedPluginCatalog(); const [destinationId, setDestinationId] = useState( props.defaultDestinationId ?? GLOBAL_MCP_DESTINATION_ID, ); @@ -78,12 +84,19 @@ export function SkillsManager(props: { const visibleSkills = targetSkills.filter((skill) => { if (statusFilter === "enabled" && !skill.enabled) return false; if (statusFilter === "disabled" && skill.enabled) return false; + const { localizedPlugin, localizedSkill } = resolveLocalizedPluginSkill( + localizedPlugins, + skill, + ); return [ skill.name, skill.description, skill.providerLabel, skill.scopeLabel, skill.absolutePath, + localizedPlugin?.name, + localizedSkill?.name, + localizedSkill?.description, ] .join(" ") .toLowerCase() @@ -106,9 +119,12 @@ export function SkillsManager(props: { const roots = new Set(skills.map((skill) => skill.rootPath)); return { key, - title: - skills.find((skill) => skill.providerGroupLabel)?.providerGroupLabel ?? - first.providerLabel, + title: first.pluginId + ? (localizedPlugins.find((plugin) => plugin.manifest.id === first.pluginId)?.name ?? + first.pluginName ?? + first.providerLabel) + : (skills.find((skill) => skill.providerGroupLabel)?.providerGroupLabel ?? + first.providerLabel), ...(roots.size === 1 ? { subtitle: first.rootPath } : {}), skills, order: Math.min(...skills.map((skill) => skill.providerGroupOrder ?? 0)), @@ -123,6 +139,10 @@ export function SkillsManager(props: { const externalCount = targetSkills.filter( (skill) => skill.origin === "external" && skill.valid && skill.portable !== false, ).length; + const viewingSkillDisplayName = viewingSkill + ? (resolveLocalizedPluginSkill(localizedPlugins, viewingSkill).localizedSkill?.name ?? + viewingSkill.name) + : undefined; const runMutation = async (skill: SkillEntry, action: () => Promise) => { setPending((current) => new Set(current).add(skill.id)); @@ -222,6 +242,7 @@ export function SkillsManager(props: { {viewingSkill ? ( setViewingSkill(undefined)} @@ -415,6 +436,7 @@ export function SkillsManager(props: { title={section.title} {...("subtitle" in section ? { subtitle: section.subtitle } : {})} skills={section.skills} + localizedPlugins={localizedPlugins} pending={pending} onEnabledChange={setEnabled} onView={setViewingSkill} @@ -450,6 +472,7 @@ function SkillSection(props: { title: string; subtitle?: string; skills: SkillEntry[]; + localizedPlugins: readonly LocalizedPlugin[]; pending: ReadonlySet; onEnabledChange: (skill: SkillEntry, enabled: boolean) => Promise; onView: (skill: SkillEntry) => void; @@ -473,6 +496,7 @@ function SkillSection(props: { Promise; onView: (skill: SkillEntry) => void; @@ -493,6 +518,13 @@ function SkillRow(props: { }) { const { t } = useLingui(); const skill = props.skill; + const { localizedPlugin, localizedSkill: pluginSkillCopy } = resolveLocalizedPluginSkill( + props.localizedPlugins, + skill, + ); + const pluginName = localizedPlugin?.name ?? skill.pluginName; + const displayName = pluginSkillCopy?.name ?? skill.name; + const displayDescription = pluginSkillCopy?.description ?? skill.description; const providerOwnedLabel = skill.origin === "built-in" ? ( Built-in @@ -530,10 +562,10 @@ function SkillRow(props: { size="sm" variant="ghost" className="!h-auto min-w-0 max-w-full justify-start !p-0 text-sm font-medium text-foreground hover:underline" - aria-label={t`View ${skill.name}`} + aria-label={t`View ${displayName}`} onPress={() => props.onView(skill)} > - {skill.name} + {displayName} {providerOwnedLabel ? ( @@ -553,9 +585,14 @@ function SkillRow(props: { {importLabel ? ( {importLabel} ) : null} + {!skill.mutable && !skill.enabled ? ( + + Disabled + + ) : null}

- {(invalidReason ?? skill.description) || t`No description`} + {(invalidReason ?? displayDescription) || t`No description`}

{skill.absolutePath}

@@ -588,7 +625,7 @@ function SkillRow(props: { ) : ( - Managed by provider + {pluginName ? Managed by {pluginName} : Managed by provider} )} diff --git a/src/renderer/components/skills/useSkills.test.ts b/src/renderer/components/skills/useSkills.test.ts index a00d25681..e331bc7f1 100644 --- a/src/renderer/components/skills/useSkills.test.ts +++ b/src/renderer/components/skills/useSkills.test.ts @@ -1,7 +1,11 @@ +import { createElement, type PropsWithChildren } from "react"; import { act, renderHook, waitFor } from "@testing-library/react"; +import { I18nProvider } from "@lingui/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SkillScanResult } from "@/shared/contracts"; -import { buildSkillSlashCommands, useSkills } from "./useSkills"; +import { dynamicActivate, i18n } from "@/renderer/i18n/i18n"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { buildSkillSlashCommands, useSkills, useSkillSlashCommandState } from "./useSkills"; const { scanSkillsMock } = vi.hoisted(() => ({ scanSkillsMock: vi.fn<() => Promise>(), @@ -33,9 +37,46 @@ function emptyScan(): SkillScanResult { }; } +function pluginSkillScan(): SkillScanResult { + const id = "project:plugin:browser-tools:browser-control"; + return { + skills: [ + { + id, + name: "browser-control", + description: "Navigate, inspect, and test pages with the in-app Browser MCP.", + folderName: "browser-control", + absolutePath: "C:\\project\\.poracode\\skills\\browser-control", + skillFilePath: "C:\\project\\.poracode\\skills\\browser-control\\SKILL.md", + rootPath: "C:\\project\\.poracode\\skills", + providerId: "plugin:browser-tools", + providerLabel: "Browser Tools", + scope: "project", + scopeLabel: "Project", + origin: "plugin", + pluginId: "browser-tools", + pluginName: "Browser Tools", + enabled: true, + mutable: false, + valid: true, + linked: false, + }, + ], + effectiveSkillIds: [id], + invocation: "dollar", + issues: [], + canLinkToGlobal: true, + }; +} + +function I18nWrapper(props: PropsWithChildren) { + return createElement(I18nProvider, { i18n }, props.children); +} + describe("useSkills", () => { beforeEach(() => { scanSkillsMock.mockReset(); + useSharedSettings.setState({ installedPlugins: {} }); }); it("shows the cached result immediately while refreshing a remounted scope", async () => { @@ -59,6 +100,115 @@ describe("useSkills", () => { act(() => resolveRefresh(refreshed)); await waitFor(() => expect(second.result.current.scan).toBe(refreshed)); }); + + it("invalidates mounted composer skills immediately when plugin state changes", async () => { + useSharedSettings.getState().installPlugin("browser-tools"); + const initial = pluginSkillScan(); + scanSkillsMock.mockResolvedValueOnce(initial); + const hook = renderHook( + () => + useSkillSlashCommandState({ kind: "windows", path: "C:\\PluginStateCacheTest" }, "codex"), + { wrapper: I18nWrapper }, + ); + await waitFor(() => expect(hook.result.current.commands).toHaveLength(1)); + + let resolveRefresh!: (result: SkillScanResult) => void; + scanSkillsMock.mockReturnValueOnce( + new Promise((resolve) => { + resolveRefresh = resolve; + }), + ); + act(() => useSharedSettings.getState().setPluginEnabled("browser-tools", false)); + + expect(hook.result.current.commands).toEqual([]); + await waitFor(() => expect(scanSkillsMock).toHaveBeenCalledTimes(2)); + act(() => resolveRefresh(emptyScan())); + await waitFor(() => expect(hook.result.current.resolved).toBe(true)); + }); + + it("scopes composer scans to the active presentation", async () => { + scanSkillsMock.mockResolvedValueOnce(emptyScan()); + const projectLocation = { kind: "windows" as const, path: "C:\\PresentationSkillTest" }; + const hook = renderHook( + () => useSkillSlashCommandState(projectLocation, "claude", "terminal"), + { wrapper: I18nWrapper }, + ); + + await waitFor(() => expect(hook.result.current.resolved).toBe(true)); + expect(scanSkillsMock).toHaveBeenCalledWith({ + projectLocation, + agentKind: "claude", + presentationMode: "terminal", + }); + }); + + it("shows a plugin skill only when its required App is effective for the launch", async () => { + useSharedSettings.getState().installPlugin("browser-tools"); + scanSkillsMock.mockResolvedValueOnce(pluginSkillScan()); + const projectLocation = { kind: "windows" as const, path: "C:\\RequiredAppSkillTest" }; + const hook = renderHook( + ({ browserMcp }: { browserMcp: boolean }) => + useSkillSlashCommandState(projectLocation, "codex", "terminal", { + model: "codex-test", + browserMcp, + }), + { initialProps: { browserMcp: false }, wrapper: I18nWrapper }, + ); + + await waitFor(() => expect(hook.result.current.resolved).toBe(true)); + expect(hook.result.current.commands).toEqual([]); + + hook.rerender({ browserMcp: true }); + expect(hook.result.current.commands).toHaveLength(1); + }); + + it("does not rescan skill files when only a plugin App toggle changes", async () => { + useSharedSettings.getState().installPlugin("browser-tools"); + scanSkillsMock.mockResolvedValueOnce(pluginSkillScan()); + const hook = renderHook( + () => + useSkillSlashCommandState( + { kind: "windows", path: "C:\\PluginAppToggleScanTest" }, + "codex", + ), + { wrapper: I18nWrapper }, + ); + + await waitFor(() => expect(hook.result.current.resolved).toBe(true)); + act(() => useSharedSettings.getState().setPluginAppEnabled("browser-tools", "browser", false)); + + expect(scanSkillsMock).toHaveBeenCalledTimes(1); + }); + + it("localizes plugin command display metadata without changing its invocation identity", async () => { + await dynamicActivate("es"); + useSharedSettings.getState().installPlugin("browser-tools"); + scanSkillsMock.mockResolvedValueOnce(pluginSkillScan()); + const hook = renderHook( + () => + useSkillSlashCommandState( + { kind: "windows", path: "C:\\LocalizedPluginCommandTest" }, + "codex", + ), + { wrapper: I18nWrapper }, + ); + + try { + await waitFor(() => expect(hook.result.current.commands).toHaveLength(1)); + expect(hook.result.current.commands[0]).toMatchObject({ + id: "browser-control", + label: + "Control del navegador — Navega, inspecciona y prueba páginas con el MCP del navegador integrado.", + description: "Navega, inspecciona y prueba páginas con el MCP del navegador integrado.", + skillName: "browser-control", + skillInvocation: "$browser-control", + skillProvider: "Herramientas del navegador", + }); + } finally { + hook.unmount(); + await dynamicActivate("en"); + } + }); }); describe("buildSkillSlashCommands", () => { diff --git a/src/renderer/components/skills/useSkills.ts b/src/renderer/components/skills/useSkills.ts index 3e8f9de3d..13979be8a 100644 --- a/src/renderer/components/skills/useSkills.ts +++ b/src/renderer/components/skills/useSkills.ts @@ -1,16 +1,34 @@ import { useEffect, useRef, useState } from "react"; import type { AgentSlashCommand, + InstalledPlugins, ProjectLocation, ScanSkillsPayload, SkillScanResult, + ThreadConfig, + ThreadPresentationMode, } from "@/shared/contracts"; +import { arePluginSkillRequiredAppsEnabled } from "@/shared/plugins/catalog"; import { readBridge } from "@/renderer/bridge"; +import { + resolveLocalizedPluginSkill, + useLocalizedPluginCatalog, + type LocalizedPlugin, +} from "@/renderer/components/plugins/pluginCopy"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; const scanCache = new Map(); const pendingScans = new Map>(); const scanVersions = new Map(); +function pluginSkillScanKey(installedPlugins: InstalledPlugins): string { + return JSON.stringify( + Object.entries(installedPlugins) + .toSorted(([left], [right]) => left.localeCompare(right)) + .map(([id, state]) => [id, state.version, state.enabled, state.disabledSkillIds.toSorted()]), + ); +} + function requestSkillScan( requestKey: string, payload: ScanSkillsPayload, @@ -39,8 +57,10 @@ export function useSkills( projectLocation?: ProjectLocation, agentKind?: string, wslDistro?: string, + presentationMode?: ThreadPresentationMode, ) { - const requestKey = `${agentKind ?? ""}\0${wslDistro ?? ""}\0${projectLocation ? JSON.stringify(projectLocation) : ""}`; + const installedPlugins = useSharedSettings((state) => state.installedPlugins); + const requestKey = `${agentKind ?? ""}\0${wslDistro ?? ""}\0${presentationMode ?? ""}\0${projectLocation ? JSON.stringify(projectLocation) : ""}\0${pluginSkillScanKey(installedPlugins)}`; const cachedScan = scanCache.get(requestKey); const [scanState, setScanState] = useState< | { @@ -64,6 +84,7 @@ export function useSkills( ...(projectLocation ? { projectLocation } : {}), ...(wslDistro ? { wslDistro } : {}), ...(agentKind ? { agentKind } : {}), + ...(presentationMode ? { presentationMode } : {}), }, reusePending, ); @@ -96,23 +117,55 @@ export function useSkills( export function useSkillSlashCommands( projectLocation: ProjectLocation, agentKind: string, + presentationMode?: ThreadPresentationMode, + launchConfig?: ThreadConfig, ): AgentSlashCommand[] { - return useSkillSlashCommandState(projectLocation, agentKind).commands; + return useSkillSlashCommandState(projectLocation, agentKind, presentationMode, launchConfig) + .commands; } -export function useSkillSlashCommandState(projectLocation: ProjectLocation, agentKind: string) { - const { scan, loading, error } = useSkills(projectLocation, agentKind); +export function useSkillSlashCommandState( + projectLocation: ProjectLocation, + agentKind: string, + presentationMode?: ThreadPresentationMode, + launchConfig?: ThreadConfig, +) { + const { scan, loading, error } = useSkills( + projectLocation, + agentKind, + undefined, + presentationMode, + ); + const localizedPlugins = useLocalizedPluginCatalog(); return { - commands: buildSkillSlashCommands(scan), + commands: buildSkillSlashCommands(scan, localizedPlugins, launchConfig), resolved: !loading && (scan !== null || error !== undefined), }; } -export function buildSkillSlashCommands(scan: SkillScanResult | null): AgentSlashCommand[] { +export function buildSkillSlashCommands( + scan: SkillScanResult | null, + localizedPlugins: readonly LocalizedPlugin[] = [], + launchConfig?: ThreadConfig, +): AgentSlashCommand[] { if (!scan?.invocation) return []; const effective = new Set(scan.effectiveSkillIds); return scan.skills.flatMap((skill) => { if (!effective.has(skill.id)) return []; + const { localizedPlugin, pluginSkill, localizedSkill } = resolveLocalizedPluginSkill( + localizedPlugins, + skill, + ); + if ( + launchConfig && + localizedPlugin && + pluginSkill && + !arePluginSkillRequiredAppsEnabled(localizedPlugin.manifest, pluginSkill, launchConfig) + ) { + return []; + } + const displayName = localizedSkill?.name ?? skill.name; + const description = localizedSkill?.description ?? skill.description; const invocation = scan.invocation === "dollar" ? `$${skill.name}` @@ -122,13 +175,13 @@ export function buildSkillSlashCommands(scan: SkillScanResult | null): AgentSlas return [ { id: skill.name, - label: skill.description ? `${skill.name} — ${skill.description}` : skill.name, - ...(skill.description ? { description: skill.description } : {}), + label: description ? `${displayName} — ${description}` : displayName, + ...(description ? { description } : {}), section: "skills" as const, skillName: skill.name, skillPath: skill.skillFilePath, skillInvocation: invocation, - skillProvider: skill.providerLabel, + skillProvider: localizedPlugin?.name ?? skill.providerLabel, skillScope: skill.scope, }, ]; diff --git a/src/renderer/components/thread/ThreadCommandPanel.tsx b/src/renderer/components/thread/ThreadCommandPanel.tsx index 1151a479b..e4f10ecbc 100644 --- a/src/renderer/components/thread/ThreadCommandPanel.tsx +++ b/src/renderer/components/thread/ThreadCommandPanel.tsx @@ -12,6 +12,14 @@ interface ThreadCommandPanelProps { listId: string; } +function commandDisplayName(command: AgentSlashCommand): string { + if (command.section !== "skills") return command.id; + const descriptionSuffix = command.description ? ` — ${command.description}` : ""; + return descriptionSuffix && command.label.endsWith(descriptionSuffix) + ? command.label.slice(0, -descriptionSuffix.length) + : command.label; +} + export function ThreadCommandPanel(props: ThreadCommandPanelProps) { const { commands, activeIndex, onSelect } = props; const { t } = useLingui(); @@ -66,6 +74,7 @@ export function ThreadCommandPanel(props: ThreadCommandPanelProps) { const isActive = index === activeIndex; const key = cmd.section === "skills" ? `skill:${cmd.skillPath ?? cmd.id}` : cmd.id; + const displayName = commandDisplayName(cmd); return (
props.onActiveIndexChange(index)}> - ) : ( - - )} +
+ {examplePrompt ? ( + + ) : null} + {state ? ( + + ) : ( + + )} +
- {manifest.publisher} + {author ?? plugin.name} + {plugin.poracode.communityMaintained ? ( + + Community + + ) : null} {props.plugin.category} - - v{manifest.version} + {plugin.manifest.version ? ( + <> + + v{plugin.manifest.version} + + ) : null}
{!supported ? (

@@ -85,6 +124,45 @@ export function PluginDetail(props: { + {examplePrompt ? ( + + ) : null} + + {problems.length > 0 ? ( +

+
+ +

+ Some contributions could not be loaded +

+
+
    + {problems.map((diagnostic, index) => ( +
  • {diagnostic.message}
  • + ))} +
+
+ ) : null} + + {plugin.poracode.communityMaintained ? ( +

+ + The server this plugin launches is maintained by a third party, not by the service it + connects to. Review the source before enabling it. + +

+ ) : null} + {state ? (
@@ -99,80 +177,126 @@ export function PluginDetail(props: { aria-labelledby={`${titleId} ${pluginToggleLabelId}`} isSelected={state.enabled} isDisabled={!supported} - onChange={(enabled) => setPluginEnabled(manifest.id, enabled)} + onChange={(enabled) => setPluginEnabled(plugin, enabled)} />
) : null} - } - title={t`Apps`} - description={t`MCP-powered tools contributed by this plugin.`} - > - {manifest.apps.map((app, index) => { - const copy = props.plugin.apps.find((candidate) => candidate.id === app.id)!; - const enabled = state ? isPluginAppEnabled(manifest, state, app.id) : app.defaultEnabled; - const labelId = `${titleId}-app-${app.id}`; - const badgeId = `${labelId}-kind`; - return ( - setPluginAppEnabled(manifest.id, app.id, next)} - /> - ) : undefined - } - /> - ); - })} - - - } - title={t`Skills`} - description={t`Reusable guidance delivered across supported agents.`} - > - {manifest.skills.map((skill, index) => { - const copy = props.plugin.skills.find((candidate) => candidate.id === skill.id)!; - const enabled = state - ? isPluginSkillEnabled(manifest, state, skill.id) - : skill.defaultEnabled; - const labelId = `${titleId}-skill-${skill.id}`; - const badgeId = `${labelId}-kind`; - return ( - setPluginSkillEnabled(manifest.id, skill.id, next)} - /> - ) : undefined - } - /> - ); - })} - + {props.plugin.apps.length > 0 ? ( + } + title={t`Apps`} + description={t`MCP-powered tools contributed by this plugin.`} + > + {props.plugin.apps.map((app, index) => { + const enabled = state ? isPluginAppEnabled(plugin, state, app.id) : true; + const labelId = `${titleId}-app-${app.id}`; + const badgeId = `${labelId}-kind`; + return ( + setPluginAppEnabled(plugin, app.id, next)} + /> + ) : undefined + } + /> + ); + })} + + ) : null} + + {props.plugin.mcpServers.length > 0 ? ( + } + title={t`MCP servers`} + description={t`Servers this plugin declares in mcp.json. Poracode passes them to every supported agent.`} + > + {props.plugin.mcpServers.map((server, index) => { + const enabled = state ? isPluginMcpServerEnabled(plugin, state, server.id) : true; + const labelId = `${titleId}-server-${server.id}`; + const badgeId = `${labelId}-kind`; + return ( + + {oauth.isRemoteServer(server.id) ? ( + void oauth.connect(server.id)} + onDisconnect={() => void oauth.disconnect(server.id)} + /> + ) : null} + setPluginMcpServerEnabled(plugin.name, server.id, next)} + /> + + ) : undefined + } + /> + ); + })} + + ) : null} + + {oauth.error ?

{oauth.error}

: null} + + {props.plugin.skills.length > 0 ? ( + } + title={t`Skills`} + description={t`Reusable guidance delivered across supported agents.`} + > + {props.plugin.skills.map((skill, index) => { + const enabled = state ? isPluginSkillEnabled(plugin, state, skill.id) : true; + const labelId = `${titleId}-skill-${skill.id}`; + const badgeId = `${labelId}-kind`; + return ( + setPluginSkillEnabled(plugin.name, skill.id, next)} + /> + ) : undefined + } + /> + ); + })} + + ) : null}

@@ -180,23 +304,90 @@ export function PluginDetail(props: {

- Publisher + Identifier
-
{manifest.publisher}
+
{plugin.name}
+ {author ? ( + <> +
+ Author +
+
{author}
+ + ) : null}
Category
{props.plugin.category}
+ {plugin.manifest.version ? ( + <> +
+ Version +
+
{plugin.manifest.version}
+ + ) : null} + {plugin.manifest.license ? ( + <> +
+ License +
+
{plugin.manifest.license}
+ + ) : null} + {plugin.manifest.homepage ? ( + <> +
+ Homepage +
+
{plugin.manifest.homepage}
+ + ) : null} + {plugin.manifest.repository ? ( + <> +
+ Repository +
+
{plugin.manifest.repository}
+ + ) : null}
- Version + Location
-
{manifest.version}
+
{plugin.root}
); } +function ConnectControl(props: { + state: "unknown" | "connected" | "disconnected" | "connecting"; + onConnect: () => void; + onDisconnect: () => void; +}) { + if (props.state === "connecting") { + return ( + + Connecting... + + ); + } + if (props.state === "connected") { + return ( + + ); + } + return ( + + ); +} + function ContributionSection(props: { icon: ReactNode; title: string; @@ -223,7 +414,7 @@ function ContributionRow(props: { labelId: string; badgeId: string; name: string; - description: string; + description?: string; badge: string; control?: ReactNode; last: boolean; @@ -244,7 +435,9 @@ function ContributionRow(props: { {props.badge} -

{props.description}

+ {props.description ? ( +

{props.description}

+ ) : null} {props.control} diff --git a/src/renderer/components/plugins/PluginIcon.tsx b/src/renderer/components/plugins/PluginIcon.tsx index 793cd91f9..b3bad264a 100644 --- a/src/renderer/components/plugins/PluginIcon.tsx +++ b/src/renderer/components/plugins/PluginIcon.tsx @@ -1,4 +1,4 @@ -import { AppWindow, Globe, Monitor, Network, Puzzle } from "lucide-react"; +import { AppWindow, GitPullRequest, Globe, Mail, Monitor, Network, Puzzle } from "lucide-react"; export function PluginIcon(props: { pluginId: string; className?: string }) { const className = props.className ?? "size-5"; @@ -11,6 +11,10 @@ export function PluginIcon(props: { pluginId: string; className?: string }) { return ; case "subagent-delegation": return ; + case "github": + return ; + case "outlook": + return ; default: return ; } diff --git a/src/renderer/components/plugins/PluginMarketplace.test.tsx b/src/renderer/components/plugins/PluginMarketplace.test.tsx index a40038080..1687c768b 100644 --- a/src/renderer/components/plugins/PluginMarketplace.test.tsx +++ b/src/renderer/components/plugins/PluginMarketplace.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, screen, within } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithI18n as render } from "@/renderer/testUtils/i18n"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { pluginFixture, seedBuiltInPlugins } from "@/renderer/testUtils/plugins"; import { useLocalizedPluginCatalog } from "./pluginCopy"; import { PluginMarketplace } from "./PluginMarketplace"; @@ -13,67 +14,73 @@ function Marketplace(props: { onOpen: (pluginId: string) => void }) { describe("PluginMarketplace", () => { beforeEach(() => { localStorage.clear(); + seedBuiltInPlugins(); useSharedSettings.setState({ installedPlugins: {} }); }); - it("discovers plugins by contribution text, installs one, and exposes management", () => { + it("browses plugins by contribution text, installs one, and exposes management", () => { const onOpen = vi.fn<(pluginId: string) => void>(); render(); - expect(screen.getByRole("tab", { name: "Discover" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("heading", { name: "Featured" })).toBeInTheDocument(); expect(screen.getByText("Browser Tools")).toBeInTheDocument(); expect(screen.getByText("Chrome Tools")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Browser Tools Install" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Chrome Tools Install" })).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Browser Tools" })); expect(onOpen).toHaveBeenCalledWith("browser-tools"); onOpen.mockClear(); fireEvent.change(screen.getByRole("textbox", { name: "Search plugins" }), { - target: { value: "navigate" }, + target: { value: "chrome" }, }); - expect(screen.getByText("Browser Tools")).toBeInTheDocument(); - expect(screen.queryByText("Chrome Tools")).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Browser Tools Install" })); + expect(screen.getByText("Chrome Tools")).toBeInTheDocument(); + expect(screen.queryByText("Browser Tools")).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Chrome Tools Install" })); - expect(useSharedSettings.getState().installedPlugins["browser-tools"]).toMatchObject({ + expect(useSharedSettings.getState().installedPlugins["chrome-tools"]).toMatchObject({ version: "1.0.0", enabled: true, }); - expect(onOpen).toHaveBeenLastCalledWith("browser-tools"); + expect(onOpen).toHaveBeenLastCalledWith("chrome-tools"); - fireEvent.click(screen.getByRole("button", { name: "Browser Tools Manage" })); + fireEvent.click(screen.getByRole("button", { name: "Chrome Tools Manage" })); expect(onOpen).toHaveBeenCalledTimes(2); - expect(onOpen).toHaveBeenLastCalledWith("browser-tools"); + expect(onOpen).toHaveBeenLastCalledWith("chrome-tools"); }); - it("shows only installed plugins and links an empty installation list back to Discover", () => { - useSharedSettings.getState().installPlugin("chrome-tools"); + it("surfaces installed plugins in the installed strip", () => { + useSharedSettings.getState().installPlugin(pluginFixture("chrome-tools")); const onOpen = vi.fn<(pluginId: string) => void>(); - const { unmount } = render(); + render(); - fireEvent.click(screen.getByRole("tab", { name: /^Installed/u })); + const strip = screen.getByRole("heading", { name: "Installed" }).closest("section")!; + expect(within(strip).getByRole("button", { name: "Chrome Tools" })).toBeInTheDocument(); + expect(within(strip).queryByRole("button", { name: "Browser Tools" })).not.toBeInTheDocument(); - expect(screen.getByText("Chrome Tools")).toBeInTheDocument(); - expect(screen.queryByText("Browser Tools")).not.toBeInTheDocument(); - const installedTab = screen.getByRole("tab", { name: /^Installed/u }); - expect(installedTab).toHaveAttribute("aria-selected", "true"); - fireEvent.click(screen.getByRole("button", { name: "Chrome Tools Manage" })); + fireEvent.click(within(strip).getByRole("button", { name: "Chrome Tools" })); expect(onOpen).toHaveBeenCalledWith("chrome-tools"); + }); - unmount(); - useSharedSettings.setState({ installedPlugins: {} }); - render(); - fireEvent.click(screen.getByRole("tab", { name: /^Installed/u })); + it("groups non-featured plugins under their category", () => { + render( void>()} />); - expect(screen.getByText("No plugins installed yet")).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Discover plugins" })); - expect(screen.getByRole("tab", { name: "Discover" })).toHaveAttribute("aria-selected", "true"); - expect(screen.queryByText("No plugins installed yet")).not.toBeInTheDocument(); + // Every shipped package is featured, so a category heading only appears for + // one that is not — the section list is derived, never hardcoded. + expect(screen.queryByRole("heading", { name: "Communication" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Featured" })).toBeInTheDocument(); }); + it("reports when nothing matches the search", () => { + render( void>()} />); + + fireEvent.change(screen.getByRole("textbox", { name: "Search plugins" }), { + target: { value: "nothing matches this" }, + }); + + expect(screen.getByText("No plugins match your search.")).toBeInTheDocument(); + }); + it("does not install a plugin that is unavailable on this host", () => { const onOpen = vi.fn<(pluginId: string) => void>(); diff --git a/src/renderer/components/plugins/PluginMarketplace.tsx b/src/renderer/components/plugins/PluginMarketplace.tsx index 3b004aa88..e7d404599 100644 --- a/src/renderer/components/plugins/PluginMarketplace.tsx +++ b/src/renderer/components/plugins/PluginMarketplace.tsx @@ -1,14 +1,23 @@ import { Card, Input } from "@heroui/react"; import { Plural, Trans, useLingui } from "@lingui/react/macro"; -import { Search } from "lucide-react"; +import { FolderOpen, Search } from "lucide-react"; import { useState } from "react"; -import { Button, LightballTabs } from "@/renderer/components/common"; +import { Button } from "@/renderer/components/common"; +import { readBridge } from "@/renderer/bridge"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; import { isPluginSupportedOnHost } from "@/shared/plugins/catalog"; +import type { PluginCategory } from "@/shared/contracts"; import { PluginIcon } from "./PluginIcon"; +import { PluginTag } from "./PluginTag"; import type { LocalizedPlugin } from "./pluginCopy"; -type MarketplaceTab = "installed" | "discover"; +/** Section order for the browse view. Featured is derived, not a category. */ +const CATEGORY_ORDER: PluginCategory[] = [ + "developer-tools", + "communication", + "automation", + "productivity", +]; export function PluginMarketplace(props: { plugins: readonly LocalizedPlugin[]; @@ -16,169 +25,221 @@ export function PluginMarketplace(props: { onOpen: (pluginId: string) => void; }) { const { t } = useLingui(); - const [tab, setTab] = useState("discover"); const [query, setQuery] = useState(""); const installedPlugins = useSharedSettings((state) => state.installedPlugins); - const installPlugin = useSharedSettings((state) => state.installPlugin); const normalizedQuery = query.trim().toLowerCase(); - const visiblePlugins = props.plugins.filter((plugin) => { - if (tab === "installed" && !installedPlugins[plugin.manifest.id]) return false; - return [ - plugin.name, - plugin.description, - plugin.category, - ...plugin.skills.flatMap((skill) => [skill.name, skill.description]), - ...plugin.apps.flatMap((app) => [app.name, app.description]), + + const matches = props.plugins.filter((entry) => + [ + entry.name, + entry.description, + entry.category, + ...(entry.plugin.manifest.keywords ?? []), + ...entry.skills.map((skill) => skill.name), + ...entry.apps.map((app) => app.name), + ...entry.mcpServers.map((server) => server.name), ] .join(" ") .toLowerCase() - .includes(normalizedQuery); - }); + .includes(normalizedQuery), + ); - const tabs = [ - { - id: "installed" as const, - label: t`Installed`, - trailing: props.plugins.filter((plugin) => installedPlugins[plugin.manifest.id]).length, - }, - { id: "discover" as const, label: t`Discover` }, - ]; + const installed = props.plugins.filter((entry) => installedPlugins[entry.plugin.name]); + const featured = matches.filter((entry) => entry.plugin.poracode.featured); + const sections = CATEGORY_ORDER.flatMap((category) => { + const entries = matches.filter( + (entry) => entry.plugin.poracode.category === category && !entry.plugin.poracode.featured, + ); + return entries.length > 0 ? [{ category, entries }] : []; + }); return (
-
-

- Plugins -

- -
-

+

+ Plugins +

+

- Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent. + Bundles of skills and MCP servers that work across every supported agent. Poracode loads + any package built for the Agent Plugins specification.

-
- - setQuery(event.target.value)} - /> + +
+
+ + setQuery(event.target.value)} + /> +
+
- {visiblePlugins.length > 0 ? ( -
-
-

- {tab === "installed" ? Installed : Featured} -

- {visiblePlugins.length} -
-
- {visiblePlugins.map((plugin) => { - const installed = installedPlugins[plugin.manifest.id] !== undefined; - const supported = isPluginSupportedOnHost(plugin.manifest, props.hostPlatform); - const titleId = `plugin-${plugin.manifest.id}-title`; - const actionLabelId = `plugin-${plugin.manifest.id}-action`; - return ( - -
-
- -
- - - - - - {plugin.description} - - -
- - - - {" · "} - - - {installed ? ( - - ) : ( - - )} - -
- ); - })} + {installed.length > 0 && !normalizedQuery ? ( +
+

+ Installed +

+
+ {installed.map((entry) => ( + + ))}
- ) : ( + ) : null} + + {matches.length === 0 ? (

- {tab === "installed" && !normalizedQuery ? ( - No plugins installed yet - ) : ( - No plugins match your search. - )} + No plugins match your search.

- {tab === "installed" && !normalizedQuery ? ( - - ) : null}
- )} + ) : null} + + {featured.length > 0 ? ( + + {featured.map((entry) => ( + + ))} + + ) : null} + + {sections.map(({ category, entries }) => ( + + {entries.map((entry) => ( + + ))} + + ))}
); } + +function PluginSection(props: { title: string; count: number; children: React.ReactNode }) { + return ( +
+
+

{props.title}

+ {props.count} +
+
{props.children}
+
+ ); +} + +function PluginCard(props: { + entry: LocalizedPlugin; + hostPlatform: NodeJS.Platform; + onOpen: (pluginId: string) => void; +}) { + const installedPlugins = useSharedSettings((state) => state.installedPlugins); + const installPlugin = useSharedSettings((state) => state.installPlugin); + const plugin = props.entry.plugin; + const installed = installedPlugins[plugin.name] !== undefined; + const supported = isPluginSupportedOnHost(plugin, props.hostPlatform); + const titleId = `plugin-${plugin.name}-title`; + const actionLabelId = `plugin-${plugin.name}-action`; + const serverCount = props.entry.apps.length + props.entry.mcpServers.length; + + return ( + +
+
+ +
+ + + + {plugin.source === "user" ? ( + + External + + ) : null} + {plugin.poracode.communityMaintained ? ( + + Community + + ) : null} + + + {props.entry.description} + + +
+ + + + {" · "} + + + {installed ? ( + + ) : ( + + )} + +
+ ); +} diff --git a/src/renderer/components/plugins/PluginTag.tsx b/src/renderer/components/plugins/PluginTag.tsx new file mode 100644 index 000000000..8eb068051 --- /dev/null +++ b/src/renderer/components/plugins/PluginTag.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from "react"; + +/** + * Inline label chip. Matches the local `Badge` treatment used in + * `components/mcp/McpServersManager.tsx` — HeroUI's `Badge` is an overlay badge + * and renders outside its container here. + */ +export function PluginTag(props: { children: ReactNode }) { + return ( + + {props.children} + + ); +} diff --git a/src/renderer/components/plugins/PluginsManager.tsx b/src/renderer/components/plugins/PluginsManager.tsx new file mode 100644 index 000000000..a61d20e46 --- /dev/null +++ b/src/renderer/components/plugins/PluginsManager.tsx @@ -0,0 +1,241 @@ +import { Input } from "@heroui/react"; +import { Trans, useLingui } from "@lingui/react/macro"; +import { Search } from "lucide-react"; +import { useState, type ReactNode } from "react"; +import { LightballTabs, ToggleSwitch } from "@/renderer/components/common"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { + isPluginAppEnabled, + isPluginMcpServerEnabled, + isPluginSkillEnabled, + isPluginSupportedOnHost, +} from "@/shared/plugins/catalog"; +import { PluginIcon } from "./PluginIcon"; +import { PluginTag } from "./PluginTag"; +import type { LocalizedPlugin, LocalizedPluginContribution } from "./pluginCopy"; + +/** + * Flat management view over installed packages, broken out by contribution type. + * + * The marketplace answers "what can I add"; this answers "what is currently + * active and where did it come from". + */ + +type ManageTab = "plugins" | "apps" | "mcps" | "skills"; + +interface ContributionRow { + key: string; + plugin: LocalizedPlugin; + contribution: LocalizedPluginContribution; + enabled: boolean; + setEnabled: (enabled: boolean) => void; +} + +export function PluginsManager(props: { + plugins: readonly LocalizedPlugin[]; + hostPlatform: NodeJS.Platform; + onOpen: (pluginId: string) => void; +}) { + const { t } = useLingui(); + const [tab, setTab] = useState("plugins"); + const [query, setQuery] = useState(""); + const installedPlugins = useSharedSettings((state) => state.installedPlugins); + const setPluginEnabled = useSharedSettings((state) => state.setPluginEnabled); + const setPluginAppEnabled = useSharedSettings((state) => state.setPluginAppEnabled); + const setPluginSkillEnabled = useSharedSettings((state) => state.setPluginSkillEnabled); + const setPluginMcpServerEnabled = useSharedSettings((state) => state.setPluginMcpServerEnabled); + const normalizedQuery = query.trim().toLowerCase(); + + const installedEntries = props.plugins.filter((entry) => installedPlugins[entry.plugin.name]); + + const contributionRows = (kind: Exclude): ContributionRow[] => + installedEntries.flatMap((entry) => { + const name = entry.plugin.name; + const state = installedPlugins[name]!; + const list = kind === "apps" ? entry.apps : kind === "mcps" ? entry.mcpServers : entry.skills; + return list.map((contribution) => ({ + key: `${name}:${contribution.id}`, + plugin: entry, + contribution, + enabled: + kind === "apps" + ? isPluginAppEnabled(entry.plugin, state, contribution.id) + : kind === "mcps" + ? isPluginMcpServerEnabled(entry.plugin, state, contribution.id) + : isPluginSkillEnabled(entry.plugin, state, contribution.id), + setEnabled: (enabled: boolean) => { + if (kind === "apps") setPluginAppEnabled(entry.plugin, contribution.id, enabled); + else if (kind === "mcps") setPluginMcpServerEnabled(name, contribution.id, enabled); + else setPluginSkillEnabled(name, contribution.id, enabled); + }, + })); + }); + + const appRows = contributionRows("apps"); + const mcpRows = contributionRows("mcps"); + const skillRows = contributionRows("skills"); + + const tabs = [ + { id: "plugins" as const, label: t`Plugins`, trailing: props.plugins.length }, + { id: "apps" as const, label: t`Apps`, trailing: appRows.length }, + { id: "mcps" as const, label: t`MCPs`, trailing: mcpRows.length }, + { id: "skills" as const, label: t`Skills`, trailing: skillRows.length }, + ]; + + const matchesQuery = (...values: (string | undefined)[]) => + values.filter(Boolean).join(" ").toLowerCase().includes(normalizedQuery); + + const visibleRows = (rows: ContributionRow[]) => + rows.filter((row) => + matchesQuery(row.contribution.name, row.contribution.description, row.plugin.name), + ); + + const rows = tab === "apps" ? appRows : tab === "mcps" ? mcpRows : skillRows; + + return ( +
+

+ Plugins +

+

+ Manage installed plugins and the skills and servers they contribute. +

+ +
+ +
+ + setQuery(event.target.value)} + /> +
+
+ + {tab === "plugins" ? ( + + {props.plugins + .filter((entry) => matchesQuery(entry.name, entry.description)) + .map((entry) => { + const name = entry.plugin.name; + const state = installedPlugins[name]; + const supported = isPluginSupportedOnHost(entry.plugin, props.hostPlatform); + return ( + } + name={entry.name} + description={entry.description} + onOpen={() => props.onOpen(name)} + tags={ + <> + {entry.plugin.source === "user" ? ( + + External + + ) : null} + {entry.plugin.poracode.communityMaintained ? ( + + Community + + ) : null} + + } + control={ + state ? ( + setPluginEnabled(entry.plugin, enabled)} + /> + ) : ( + + Not installed + + ) + } + /> + ); + })} + + ) : ( + + {visibleRows(rows).map((row) => ( + } + name={row.contribution.name} + {...(row.contribution.description + ? { description: row.contribution.description } + : {})} + onOpen={() => props.onOpen(row.plugin.plugin.name)} + tags={{row.plugin.name}} + control={ + + } + /> + ))} + + )} + + {tab !== "plugins" && visibleRows(rows).length === 0 ? ( +
+

+ Nothing here yet. Install a plugin to add contributions. +

+
+ ) : null} +
+ ); +} + +function ManageList(props: { children: ReactNode }) { + return
{props.children}
; +} + +function ManageRow(props: { + icon: ReactNode; + name: string; + description?: string; + tags?: ReactNode; + control: ReactNode; + onOpen: () => void; +}) { + return ( +
+
+ {props.icon} +
+
+
+ + {props.tags} +
+ {props.description ? ( +

{props.description}

+ ) : null} +
+ {props.control} +
+ ); +} diff --git a/src/renderer/components/plugins/pluginCopy.ts b/src/renderer/components/plugins/pluginCopy.ts index ff590d93b..8709953b0 100644 --- a/src/renderer/components/plugins/pluginCopy.ts +++ b/src/renderer/components/plugins/pluginCopy.ts @@ -1,20 +1,47 @@ import { useLingui } from "@lingui/react/macro"; -import type { SkillEntry } from "@/shared/contracts"; -import { BUILT_IN_PLUGIN_MANIFESTS } from "@/shared/plugins/catalog"; +import type { LoadedPlugin, SkillEntry } from "@/shared/contracts"; +import { usePlugins } from "@/renderer/state/pluginsStore"; + +/** + * Display copy for loaded Agent Plugins packages. + * + * Poracode's own packages ship English text in `plugin.json`, so their names and + * descriptions are overridden here with translated strings. Third-party packages + * carry author-written metadata that cannot live in our catalogs, so their + * manifest text is shown as authored — that is the correct behavior for a + * general plugin client, not a missing translation. + */ export interface LocalizedPluginContribution { id: string; name: string; + /** + * Absent when we have no copy of our own for this contribution. Callers that + * have the scanned SKILL.md fall back to its description; an empty string here + * would shadow it, because `??` does not treat "" as missing. + */ + description?: string; +} + +export interface LocalizedPlugin { + plugin: LoadedPlugin; + name: string; description: string; + category: string; + skills: LocalizedPluginContribution[]; + apps: LocalizedPluginContribution[]; + mcpServers: LocalizedPluginContribution[]; } -export function useLocalizedPluginCatalog() { +export function useLocalizedPluginCatalog(): LocalizedPlugin[] { const { t } = useLingui(); + const plugins = usePlugins((state) => state.plugins); - return BUILT_IN_PLUGIN_MANIFESTS.map((manifest) => { + return plugins.map((plugin): LocalizedPlugin => { + const fallbackName = plugin.poracode.title ?? plugin.name; let name: string; let description: string; - switch (manifest.id) { + switch (plugin.name) { case "browser-tools": name = t`Browser Tools`; description = t`Browse, inspect, and test websites in Poracode's isolated in-app browser.`; @@ -31,43 +58,56 @@ export function useLocalizedPluginCatalog() { name = t`Subagent Delegation`; description = t`Delegate focused work to other installed agents and coordinate the results.`; break; + case "github": + name = t`GitHub`; + description = t`Triage PRs, issues, CI, and publish flows.`; + break; + case "outlook": + name = t`Outlook`; + description = t`Triage Microsoft Outlook mail and manage your calendar.`; + break; default: - name = manifest.name; - description = manifest.description; + name = fallbackName; + description = plugin.manifest.description ?? ""; } - const skills = manifest.skills.map((skill): LocalizedPluginContribution => { - switch (skill.id) { + const skills = plugin.skills.map((skill): LocalizedPluginContribution => { + const policy = plugin.poracode.skills[skill.folder]; + switch (skill.folder) { case "browser-control": return { - id: skill.id, + id: skill.folder, name: t`Browser Control`, description: t`Navigate, inspect, and test pages with the in-app Browser MCP.`, }; case "chrome-control": return { - id: skill.id, + id: skill.folder, name: t`Chrome Control`, description: t`Use Chrome safely when a task needs an existing browser session.`, }; case "computer-use": return { - id: skill.id, + id: skill.folder, name: t`Computer Use`, description: t`Operate desktop apps through Poracode's desktop-control tools.`, }; case "subagent-delegation": return { - id: skill.id, + id: skill.folder, name: t`Subagent Delegation`, description: t`Choose, brief, and coordinate subagents for parallel work.`, }; default: - return { id: skill.id, name: skill.name, description: skill.description }; + return { + id: skill.folder, + name: policy?.name ?? skill.folder, + ...(policy?.description ? { description: policy.description } : {}), + }; } }); - const apps = manifest.apps.map((app): LocalizedPluginContribution => { + const apps = plugin.poracode.apps.map((app): LocalizedPluginContribution => { switch (app.id) { case "browser": return { @@ -98,31 +138,42 @@ export function useLocalizedPluginCatalog() { } }); + // Server transport detail is author-supplied and identifies the endpoint, so + // it is shown verbatim rather than translated. + const mcpServers = plugin.mcpServers.map((server): LocalizedPluginContribution => { + const entry = server.entry; + return { + id: server.name, + name: server.name, + description: entry.type === "stdio" ? entry.command : entry.url, + }; + }); + const category = - manifest.category === "developer-tools" + plugin.poracode.category === "developer-tools" ? t`Developer tools` - : manifest.category === "automation" + : plugin.poracode.category === "automation" ? t`Automation` - : t`Productivity`; + : plugin.poracode.category === "communication" + ? t`Communication` + : t`Productivity`; - return { manifest, name, description, category, skills, apps }; + return { plugin, name, description, category, skills, apps, mcpServers }; }); } -export type LocalizedPlugin = ReturnType[number]; - export function resolveLocalizedPluginSkill( catalog: readonly LocalizedPlugin[], skill: Pick, ) { const localizedPlugin = skill.pluginId - ? catalog.find((plugin) => plugin.manifest.id === skill.pluginId) + ? catalog.find((entry) => entry.plugin.name === skill.pluginId) : undefined; - const pluginSkill = localizedPlugin?.manifest.skills.find( + const pluginSkill = localizedPlugin?.plugin.skills.find( (contribution) => contribution.folder === skill.folderName, ); const localizedSkill = localizedPlugin?.skills.find( - (contribution) => contribution.id === pluginSkill?.id, + (contribution) => contribution.id === pluginSkill?.folder, ); return { localizedPlugin, pluginSkill, localizedSkill }; } diff --git a/src/renderer/components/plugins/usePluginOauth.ts b/src/renderer/components/plugins/usePluginOauth.ts new file mode 100644 index 000000000..e5a2a3b6c --- /dev/null +++ b/src/renderer/components/plugins/usePluginOauth.ts @@ -0,0 +1,117 @@ +import { useCallback, useEffect, useState } from "react"; +import type { LoadedPlugin, McpServer } from "@/shared/contracts"; +import { DEFAULT_MCP_SERVER_TIMEOUT_MS } from "@/shared/contracts"; +import { readBridge } from "@/renderer/bridge"; + +/** + * Connection state for a plugin's remote MCP servers. + * + * Remote servers a package declares in `mcp.json` are authorized through the + * same OAuth 2.1 flow already used for user-configured MCP servers + * (`src/supervisor/mcp/McpOAuthService.ts`). The supervisor owns the loopback + * redirect listener and the sealed token store; the renderer only ever sees the + * authorization URL and a connected flag. + */ + +type ConnectionState = "unknown" | "connected" | "disconnected" | "connecting"; + +function remoteServerUrl(entry: LoadedPlugin["mcpServers"][number]["entry"]): string | undefined { + return entry.type === "stdio" ? undefined : entry.url; +} + +/** Mirrors the shape `pluginMcpRuntime` builds, so the supervisor authorizes the same server. */ +function toMcpServer(plugin: LoadedPlugin, serverName: string, url: string): McpServer { + return { + id: `plugin:${plugin.name}:${serverName}`, + name: `${plugin.name}.${serverName}`, + description: plugin.manifest.description ?? "", + enabled: true, + timeoutMs: DEFAULT_MCP_SERVER_TIMEOUT_MS, + transport: { type: "http", url, headers: {} }, + }; +} + +export function usePluginOauth(plugin: LoadedPlugin) { + const [authorizedUrls, setAuthorizedUrls] = useState(); + const [pending, setPending] = useState(); + const [error, setError] = useState(); + + const refresh = useCallback(async () => { + try { + const status = await readBridge().getMcpOauthStatus(); + setAuthorizedUrls(status.authenticatedUrls); + } catch { + // Leave the state unknown rather than claiming a server is disconnected. + setAuthorizedUrls(undefined); + } + }, []); + + const hasRemoteServer = plugin.mcpServers.some((server) => remoteServerUrl(server.entry)); + + useEffect(() => { + if (hasRemoteServer) void refresh(); + }, [hasRemoteServer, refresh]); + + const stateFor = (serverName: string): ConnectionState => { + const server = plugin.mcpServers.find((candidate) => candidate.name === serverName); + const url = server ? remoteServerUrl(server.entry) : undefined; + if (!url) return "unknown"; + if (pending === serverName) return "connecting"; + if (!authorizedUrls) return "unknown"; + return authorizedUrls.includes(url) ? "connected" : "disconnected"; + }; + + const connect = async (serverName: string) => { + const server = plugin.mcpServers.find((candidate) => candidate.name === serverName); + const url = server ? remoteServerUrl(server.entry) : undefined; + if (!url) return; + setPending(serverName); + setError(undefined); + try { + const bridge = readBridge(); + const begin = await bridge.beginMcpServerOauth({ + server: toMcpServer(plugin, serverName, url), + }); + if (begin.status === "error") { + setError(begin.message); + return; + } + if (begin.status === "redirect") { + await bridge.openExternal(begin.authorizationUrl); + const result = await bridge.waitMcpServerOauth({ flowId: begin.flowId }); + if (result.status === "error") { + setError(result.message); + return; + } + } + await refresh(); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setPending(undefined); + } + }; + + const disconnect = async (serverName: string) => { + const server = plugin.mcpServers.find((candidate) => candidate.name === serverName); + const url = server ? remoteServerUrl(server.entry) : undefined; + if (!url) return; + setError(undefined); + try { + await readBridge().clearMcpServerOauth({ url }); + await refresh(); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + } + }; + + /** True for servers reached over the network, which are the ones that can be authorized. */ + const isRemoteServer = (serverName: string): boolean => { + const server = plugin.mcpServers.find((candidate) => candidate.name === serverName); + return server ? remoteServerUrl(server.entry) !== undefined : false; + }; + + return { stateFor, isRemoteServer, connect, disconnect, error }; +} + +export type PluginOauthConnectionState = ConnectionState; diff --git a/src/renderer/components/skills/SkillsManager.test.tsx b/src/renderer/components/skills/SkillsManager.test.tsx index ac37aa100..9f997d9b1 100644 --- a/src/renderer/components/skills/SkillsManager.test.tsx +++ b/src/renderer/components/skills/SkillsManager.test.tsx @@ -7,6 +7,7 @@ import type { SkillScanResult, } from "@/shared/contracts"; import { AppProvider } from "@/renderer/components/ui/provider"; +import { seedBuiltInPlugins } from "@/renderer/testUtils/plugins"; import { SkillsManager } from "./SkillsManager"; const { @@ -109,6 +110,7 @@ function renderManager( describe("SkillsManager", () => { beforeEach(() => { + seedBuiltInPlugins(); vi.clearAllMocks(); bridge.listWslDistros.mockReturnValue(new Promise(() => undefined)); ensureHomeScopeProjectMock.mockResolvedValue({ id: "home" }); diff --git a/src/renderer/components/skills/SkillsManager.tsx b/src/renderer/components/skills/SkillsManager.tsx index f6922051f..75ac4db48 100644 --- a/src/renderer/components/skills/SkillsManager.tsx +++ b/src/renderer/components/skills/SkillsManager.tsx @@ -120,7 +120,7 @@ export function SkillsManager(props: { return { key, title: first.pluginId - ? (localizedPlugins.find((plugin) => plugin.manifest.id === first.pluginId)?.name ?? + ? (localizedPlugins.find((entry) => entry.plugin.name === first.pluginId)?.name ?? first.pluginName ?? first.providerLabel) : (skills.find((skill) => skill.providerGroupLabel)?.providerGroupLabel ?? diff --git a/src/renderer/components/skills/useSkills.test.ts b/src/renderer/components/skills/useSkills.test.ts index e331bc7f1..5084e7978 100644 --- a/src/renderer/components/skills/useSkills.test.ts +++ b/src/renderer/components/skills/useSkills.test.ts @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SkillScanResult } from "@/shared/contracts"; import { dynamicActivate, i18n } from "@/renderer/i18n/i18n"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { pluginFixture, seedBuiltInPlugins } from "@/renderer/testUtils/plugins"; import { buildSkillSlashCommands, useSkills, useSkillSlashCommandState } from "./useSkills"; const { scanSkillsMock } = vi.hoisted(() => ({ @@ -76,6 +77,7 @@ function I18nWrapper(props: PropsWithChildren) { describe("useSkills", () => { beforeEach(() => { scanSkillsMock.mockReset(); + seedBuiltInPlugins(); useSharedSettings.setState({ installedPlugins: {} }); }); @@ -102,7 +104,7 @@ describe("useSkills", () => { }); it("invalidates mounted composer skills immediately when plugin state changes", async () => { - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); const initial = pluginSkillScan(); scanSkillsMock.mockResolvedValueOnce(initial); const hook = renderHook( @@ -118,7 +120,7 @@ describe("useSkills", () => { resolveRefresh = resolve; }), ); - act(() => useSharedSettings.getState().setPluginEnabled("browser-tools", false)); + act(() => useSharedSettings.getState().setPluginEnabled(pluginFixture("browser-tools"), false)); expect(hook.result.current.commands).toEqual([]); await waitFor(() => expect(scanSkillsMock).toHaveBeenCalledTimes(2)); @@ -143,7 +145,7 @@ describe("useSkills", () => { }); it("shows a plugin skill only when its required App is effective for the launch", async () => { - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); scanSkillsMock.mockResolvedValueOnce(pluginSkillScan()); const projectLocation = { kind: "windows" as const, path: "C:\\RequiredAppSkillTest" }; const hook = renderHook( @@ -163,7 +165,7 @@ describe("useSkills", () => { }); it("does not rescan skill files when only a plugin App toggle changes", async () => { - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); scanSkillsMock.mockResolvedValueOnce(pluginSkillScan()); const hook = renderHook( () => @@ -175,14 +177,18 @@ describe("useSkills", () => { ); await waitFor(() => expect(hook.result.current.resolved).toBe(true)); - act(() => useSharedSettings.getState().setPluginAppEnabled("browser-tools", "browser", false)); + act(() => + useSharedSettings + .getState() + .setPluginAppEnabled(pluginFixture("browser-tools"), "browser", false), + ); expect(scanSkillsMock).toHaveBeenCalledTimes(1); }); it("localizes plugin command display metadata without changing its invocation identity", async () => { await dynamicActivate("es"); - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); scanSkillsMock.mockResolvedValueOnce(pluginSkillScan()); const hook = renderHook( () => diff --git a/src/renderer/components/skills/useSkills.ts b/src/renderer/components/skills/useSkills.ts index 13979be8a..8b84c8e65 100644 --- a/src/renderer/components/skills/useSkills.ts +++ b/src/renderer/components/skills/useSkills.ts @@ -160,7 +160,7 @@ export function buildSkillSlashCommands( launchConfig && localizedPlugin && pluginSkill && - !arePluginSkillRequiredAppsEnabled(localizedPlugin.manifest, pluginSkill, launchConfig) + !arePluginSkillRequiredAppsEnabled(localizedPlugin.plugin, pluginSkill.folder, launchConfig) ) { return []; } diff --git a/src/renderer/components/thread/ThreadComposerSection.test.tsx b/src/renderer/components/thread/ThreadComposerSection.test.tsx index 681d7c7bf..01ddf882d 100644 --- a/src/renderer/components/thread/ThreadComposerSection.test.tsx +++ b/src/renderer/components/thread/ThreadComposerSection.test.tsx @@ -366,6 +366,7 @@ describe("ThreadComposerSection", () => { enabled: true, disabledSkillIds: [], disabledAppIds: ["browser"], + disabledMcpServerNames: [], }, }, }); diff --git a/src/renderer/components/thread/ThreadComposerSection.tsx b/src/renderer/components/thread/ThreadComposerSection.tsx index 1180a7ded..d1008c0cd 100644 --- a/src/renderer/components/thread/ThreadComposerSection.tsx +++ b/src/renderer/components/thread/ThreadComposerSection.tsx @@ -40,6 +40,7 @@ import { useBrowserAttachInbox } from "@/renderer/state/browserAttachInbox"; import { useComposerUiStore } from "@/renderer/state/composerUiStore"; import { useGitStore } from "@/renderer/state/gitStore"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { usePlugins } from "@/renderer/state/pluginsStore"; import { isDraftContentNonEmpty } from "@/renderer/state/slices/types"; import { selectActiveSubAgentParentItemIds } from "@/renderer/state/subAgentSelectors"; import { useThread } from "@/renderer/state/useThread"; @@ -127,6 +128,7 @@ function ThreadComposerSectionInner(props: ThreadComposerSectionProps & { thread thread.presentationMode ?? agentStatus?.capabilities.presentationMode ?? "terminal"; const runtimeLaunchConfig = useAppStore((s) => s.runtimeLaunchConfigByThreadId[thread.id]); const installedPlugins = useSharedSettings((s) => s.installedPlugins); + const plugins = usePlugins((state) => state.plugins); const disabledBuiltInMcpServers = useSharedSettings((s) => s.disabledBuiltInMcpServers); const agentSettings = useSharedSettings((s) => s.agentSettings[thread.agentKind]); let anticipatedLaunchConfig = thread.config; @@ -137,7 +139,7 @@ function ThreadComposerSectionInner(props: ThreadComposerSectionProps & { thread projectLocation, hostPlatform: readBridge()?.platform ?? ("linux" as const), }; - anticipatedLaunchConfig = resolvePluginLaunchPreview(thread.config, installedPlugins, { + anticipatedLaunchConfig = resolvePluginLaunchPreview(thread.config, plugins, installedPlugins, { ...launchContext, disabledBuiltInMcpServers, agentSettings, diff --git a/src/renderer/components/thread/ThreadDraftComposerArea.tsx b/src/renderer/components/thread/ThreadDraftComposerArea.tsx index bdd3dc026..f9a716cf0 100644 --- a/src/renderer/components/thread/ThreadDraftComposerArea.tsx +++ b/src/renderer/components/thread/ThreadDraftComposerArea.tsx @@ -59,6 +59,7 @@ import { PixelLoader } from "@/renderer/components/common/PixelLoader"; import { useAppStore } from "@/renderer/state/appStore"; import { useGitStore } from "@/renderer/state/gitStore"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { usePlugins } from "@/renderer/state/pluginsStore"; import { isDraftContentNonEmpty } from "@/renderer/state/slices/types"; import { ThreadCommandPanel } from "./ThreadCommandPanel"; import { useSkillSlashCommandState } from "@/renderer/components/skills/useSkills"; @@ -279,6 +280,7 @@ export function ThreadDraftComposerArea(props: { // Persistent (standing-default) composer MCP enablement, keyed by MCP id. const persistentMcpServers = useSharedSettings((s) => s.enabledMcpServers); const installedPlugins = useSharedSettings((s) => s.installedPlugins); + const plugins = usePlugins((state) => state.plugins); const disabledBuiltInMcpServers = useSharedSettings((s) => s.disabledBuiltInMcpServers); const agentSettings = useSharedSettings((s) => s.agentSettings[props.selectedAgent.kind]); const setMcpServerEnabled = useSharedSettings((s) => s.setMcpServerEnabled); @@ -336,19 +338,20 @@ export function ThreadDraftComposerArea(props: { // id — not the per-thread config flag. A new MCP server means adding one // descriptor to the registry. const isPluginMcpAvailable = (id: BuiltInMcpServerId) => - isBuiltInMcpServerAvailableByPlugin(installedPlugins, id); + isBuiltInMcpServerAvailableByPlugin(plugins, installedPlugins, id); const availableComposerMcpServers = composerMcpServers.filter( (descriptor) => disabledBuiltInMcpServers[descriptor.id] !== true && isPluginMcpAvailable(descriptor.id), ); const isPersistentlyEnabled = (id: BuiltInMcpServerId) => - persistentMcpServers[id] === true || isBuiltInMcpServerEnabledByPlugin(installedPlugins, id); + persistentMcpServers[id] === true || + isBuiltInMcpServerEnabledByPlugin(plugins, installedPlugins, id); const setPersistentEnabled = (id: BuiltInMcpServerId, enabled: boolean) => { - const manifest = getInstalledPluginForMcpServer(installedPlugins, id); - const state = manifest ? installedPlugins[manifest.id] : undefined; - const app = manifest?.apps.find((candidate) => candidate.builtInMcpServerId === id); - if (manifest && state && app) { - setPluginAppEnabled(manifest.id, app.id, enabled); + const owner = getInstalledPluginForMcpServer(plugins, installedPlugins, id); + const state = owner ? installedPlugins[owner.name] : undefined; + const app = owner?.poracode.apps.find((candidate) => candidate.builtInMcpServerId === id); + if (owner && state && app) { + setPluginAppEnabled(owner, app.id, enabled); if (!enabled && persistentMcpServers[id] === true) setMcpServerEnabled(id, false); return; } @@ -464,6 +467,7 @@ export function ThreadDraftComposerArea(props: { }; const draftLaunchConfig = resolvePluginLaunchPreview( props.config, + plugins, installedPlugins, launchContext, ); diff --git a/src/renderer/components/thread/ThreadDraftView.test.tsx b/src/renderer/components/thread/ThreadDraftView.test.tsx index 0343d7e45..a9e6bf613 100644 --- a/src/renderer/components/thread/ThreadDraftView.test.tsx +++ b/src/renderer/components/thread/ThreadDraftView.test.tsx @@ -7,6 +7,7 @@ import { HOME_PROJECT_ID, HOME_PROJECT_NAME } from "@/shared/homeScope"; import { useAgentStatusesStore } from "@/renderer/state/agentStatusesStore"; import { useGitStore } from "@/renderer/state/gitStore"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { seedBuiltInPlugins } from "@/renderer/testUtils/plugins"; const { composerSpy } = vi.hoisted(() => ({ composerSpy: vi.fn<(props: unknown) => void>(), @@ -375,6 +376,7 @@ const singleEffortMultiContextCursorStatus: AgentStatus = { describe("ThreadDraftView", () => { beforeEach(() => { + seedBuiltInPlugins(); composerSpy.mockClear(); delete (window as unknown as { poracode?: unknown }).poracode; useAgentStatusesStore.setState({ @@ -664,6 +666,7 @@ describe("ThreadDraftView", () => { enabled: true, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }, }); @@ -692,6 +695,7 @@ describe("ThreadDraftView", () => { enabled: true, disabledSkillIds: [], disabledAppIds: ["browser"], + disabledMcpServerNames: [], }, }, }); @@ -739,6 +743,7 @@ describe("ThreadDraftView", () => { enabled: false, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }, }); diff --git a/src/renderer/components/thread/ThreadDraftView.tsx b/src/renderer/components/thread/ThreadDraftView.tsx index 99b53c9bd..6872baeca 100644 --- a/src/renderer/components/thread/ThreadDraftView.tsx +++ b/src/renderer/components/thread/ThreadDraftView.tsx @@ -23,6 +23,7 @@ import { useGitStore } from "@/renderer/state/gitStore"; import { PixelLoader } from "@/renderer/components/common/PixelLoader"; import { modelVisibilityKey } from "@/renderer/components/common/ProviderModelMenu/parts/providerIdentity"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { usePlugins } from "@/renderer/state/pluginsStore"; import { useAppStore } from "@/renderer/state/appStore"; import { capabilitiesForPresentation, filterHiddenModels } from "@/shared/agentSelection"; import { isBuiltInMcpServerAvailableByPlugin } from "@/shared/plugins/catalog"; @@ -243,6 +244,7 @@ export function ThreadDraftView(props: { const enabledMcpServers = useSharedSettings((s) => s.enabledMcpServers); const disabledBuiltInMcpServers = useSharedSettings((s) => s.disabledBuiltInMcpServers); const installedPlugins = useSharedSettings((s) => s.installedPlugins); + const plugins = usePlugins((state) => state.plugins); const supportedPresentationModes = selectedAgent ? (selectedAgent.capabilities.presentationModes ?? [ selectedAgent.capabilities.presentationMode, @@ -940,7 +942,7 @@ export function ThreadDraftView(props: { const effectiveMcp = (id: BuiltInMcpServerId, mention: boolean, scope: string) => { return ( disabledBuiltInMcpServers[id] !== true && - isBuiltInMcpServerAvailableByPlugin(installedPlugins, id) && + isBuiltInMcpServerAvailableByPlugin(plugins, installedPlugins, id) && (mention || (enabledMcpServers[id] === true && scope !== "none")) ); }; diff --git a/src/renderer/devBridge.ts b/src/renderer/devBridge.ts index f0cfe9c09..d531eb779 100644 --- a/src/renderer/devBridge.ts +++ b/src/renderer/devBridge.ts @@ -14,6 +14,7 @@ import { useAppStore } from "@/renderer/state/appStore"; import { useAgentStatusesStore } from "@/renderer/state/agentStatusesStore"; import { usePanelStore } from "@/renderer/state/panelStore"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { usePlugins } from "./state/pluginsStore"; import { useSidebarUiStore } from "@/renderer/state/sidebarUiStore"; import { useUpdateStore } from "@/renderer/state/updateStore"; @@ -34,6 +35,7 @@ export function installDevBridge(): void { panel: usePanelStore, sidebarUi: useSidebarUiStore, sharedSettings: useSharedSettings, + plugins: usePlugins, }, /** Open Settings; pass a section id (e.g. "about", "usage", "appearance") to deep-link. */ openSettings: (section?: string) => { diff --git a/src/renderer/hooks/useAppHydration.ts b/src/renderer/hooks/useAppHydration.ts index b35ae99a8..ff653d28c 100644 --- a/src/renderer/hooks/useAppHydration.ts +++ b/src/renderer/hooks/useAppHydration.ts @@ -4,6 +4,7 @@ import { readBridge } from "@/renderer/bridge"; import { captureRendererException } from "@/renderer/diagnostics/sentry"; import { useAppStore } from "@/renderer/state/appStore"; import { hydrateThreadRuntimeItems } from "@/renderer/state/chatRuntimePersister"; +import { usePlugins } from "@/renderer/state/pluginsStore"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; import { normalizeRuntimeSnapshotLaunchConfig } from "@/renderer/state/slices/threadSlice"; import { startDeferredFeaturePrewarm } from "@/renderer/deferredFeatures"; @@ -97,6 +98,10 @@ export function useAppHydration(options: { runtimeOwner?: boolean } = {}) { } }); + // Composer MCP toggles and skill lists both depend on the loaded plugin + // list, so it has to be there before the first thread renders. + void usePlugins.getState().load(); + void readBridge() .getThreadSnapshots() .then((snapshots) => { diff --git a/src/renderer/locales/de/messages.po b/src/renderer/locales/de/messages.po index e1a4eeda9..98629fb21 100644 --- a/src/renderer/locales/de/messages.po +++ b/src/renderer/locales/de/messages.po @@ -32,8 +32,8 @@ msgstr "(keine Nachricht)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# App} other {# Apps}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# App} other {# Apps}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# Projekt} other {# Projekte}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# Anbieter bereit} other {# Anbieter bereit}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# Server} other {# Server}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# Ergebnis} other {# Ergebnisse}}" msgid "{seconds}s ago" msgstr "vor {seconds}s" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# Server} other {# Server}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} von {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Genehmigt" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Apps" @@ -1132,6 +1142,10 @@ msgstr "Authentifizierung erforderlich" msgid "Authentication setup cannot be imported" msgstr "Die Authentifizierungskonfiguration kann nicht importiert werden" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Autor" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Erstellt" @@ -1466,6 +1480,10 @@ msgstr "Integrierte Server werden von Poracode verwaltet. Sie können global dea msgid "Built-in shortcut — can't be changed" msgstr "Integriertes Tastenkürzel — kann nicht geändert werden" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Bündel aus Skills und MCP-Servern, die mit jedem unterstützten Agenten funktionieren. Poracode lädt jedes Paket, das nach der Agent-Plugins-Spezifikation gebaut wurde." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "von <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Commit-, PR- und Review-Composer" msgid "Commits" msgstr "Commits" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Kommunikation" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Community" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Kontext verdichten" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Konflikt mit einem verwalteten Skill" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Verbinde hier die KI-Anbieter, die OpenCode nutzen kann, und melde dich #~ msgstr "Mit einem anderen Poracode-Desktop oder einem Headless-Server verbinden, um dessen Projekte hier zu durchsuchen und zu verwalten. Endpunkt und Kopplungstoken findest du unter Einstellungen → Remotezugriff auf diesem Gerät." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Verbunden" @@ -2345,6 +2375,10 @@ msgstr "Verbunden" msgid "Connecting to the desktop browser…" msgstr "Verbindung zum Desktop-Browser wird hergestellt…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Verbinden..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Server trennen" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Entdecken" +#~ msgid "Discover" +#~ msgstr "Entdecken" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Plugins entdecken" +#~ msgid "Discover plugins" +#~ msgstr "Plugins entdecken" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Stufen" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "{0} aktivieren" @@ -3641,6 +3678,8 @@ msgstr "Erweitern Sie das Aufgaben-Dock" msgid "Export MCP servers" msgstr "MCP-Server exportieren" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Extern" @@ -4090,6 +4129,10 @@ msgstr "Git-Status für {0}" msgid "Git status for {0}: not a Git repository" msgstr "Git-Status für {0}: kein Git-Repository" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Home-Ordner" msgid "Home scope" msgstr "Home-Bereich" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Homepage" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, Alias oder benutzer@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Bezeichner" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Identität und Nutzungsstatistiken" @@ -4527,7 +4578,6 @@ msgstr "Unter Windows installieren" msgid "Install packages" msgstr "Pakete installieren" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Von Poracode verwaltete Pakete aus Skills und MCP-gestützten Apps für alle unterstützten Agenten installieren." @@ -4689,6 +4739,7 @@ msgstr "Ermöglicht Agenten den Aufruf von <0>eval, um beliebiges JavaScript #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Lizenz" @@ -4836,6 +4887,7 @@ msgstr "Lokale Änderungen müssen vor dem Pull von {branch} gestasht werden" msgid "localhost:{0} on desktop" msgstr "localhost:{0} auf dem Desktop" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Die Seitenleiste durchscheinend machen — das Unschärfe-Material des Systems unter Windows 11, sonst eine durchscheinende Tönung." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Verwalten" @@ -4913,6 +4966,10 @@ msgstr "Verwalten" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Globale und projektbezogene Agent-Skills verwalten, einschließlich der von anderen Anbietern importierten Skills." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Installierte Plugins und die von ihnen bereitgestellten Skills und Server verwalten." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Remote-Umgebungen verwalten" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Als erledigt markieren" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Marketplace" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "MCP-Server-URL" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "MCP-Server" @@ -5123,6 +5182,10 @@ msgstr "MCP-Transporttyp" msgid "MCP-powered tools contributed by this plugin." msgstr "Von diesem Plugin bereitgestellte MCP-gestützte Tools." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCPs" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Zusammenführen und entfernen" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Noch keine Phasen." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Noch keine Plugins installiert" +#~ msgid "No plugins installed yet" +#~ msgstr "Noch keine Plugins installiert" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Kein Git-Repository" msgid "Not found" msgstr "Nicht gefunden" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Nicht unterstützt" msgid "Notes" msgstr "Notizen" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Hier ist noch nichts. Installiere ein Plugin, um Beiträge hinzuzufügen." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Noch nichts erfasst. Es erscheint hier, sobald Sie es nutzen." @@ -6408,6 +6476,10 @@ msgstr "Andere" msgid "Outcome" msgstr "Ergebnis" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Zu schnelles Tempo – läuft früh aus" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Ansichten des Plugin-Marktplatzes" +msgid "Plugin folder" +msgstr "Plugin-Ordner" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Plugin-Verwaltungsansichten" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Ansichten des Plugin-Marktplatzes" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Plugin-Einstellungsansichten" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "Öffentliche URL aktualisiert." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Herausgeber" +#~ msgid "Publisher" +#~ msgstr "Herausgeber" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Melden Sie ein Problem" msgid "repository" msgstr "Repository" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Repository" @@ -7926,6 +8012,10 @@ msgstr "Branches durchsuchen..." msgid "Search files" msgstr "Dateien durchsuchen" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Installierte Plugins durchsuchen" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Marketplace-Skills durchsuchen" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Plugins durchsuchen" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Plugins, Skills und Apps durchsuchen..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Plugins, Skills und Apps durchsuchen..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Plugins, Skills und Server durchsuchen..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Threads durchsuchen" msgid "Search Threads" msgstr "Threads durchsuchen" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Suchen..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Suchen…" @@ -8153,6 +8251,10 @@ msgstr "Auswahl an Terminal gesendet." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Code-Konten nach Konfigurationsverzeichnis trennen oder ein Profil auf einen externen Anbieter (z.ai, …) ausrichten. Öffnen Sie ein Profil, um dessen Umgebungsvariablen, Modelle und Reasoning-Stufe zu konfigurieren." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Server, die dieses Plugin in mcp.json deklariert. Poracode übergibt sie an jeden unterstützten Agenten." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Gruppenfarbe festlegen" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md ist zu groß." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Slash-Befehle" msgid "Some checks are failing or pending." msgstr "Einige Prüfungen schlagen fehl oder stehen noch aus." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Einige Beiträge konnten nicht geladen werden" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Einige Anbieterordner konnten nicht gescannt werden. Erfolgreich geladene Skills werden weiterhin angezeigt." @@ -9196,6 +9303,10 @@ msgstr "Die Serverkonfiguration ist ungültig." msgid "The server returned an invalid MCP response." msgstr "Der Server hat eine ungültige MCP-Antwort zurückgegeben." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "Der Server, den dieses Plugin startet, wird von Dritten gepflegt, nicht vom verbundenen Dienst. Prüfe den Quellcode, bevor du ihn aktivierst." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "Die Skill-Beschreibung fehlt." @@ -9568,6 +9679,14 @@ msgstr "Durchscheinende Seitenleiste" msgid "Trending now" msgstr "Aktuell im Trend" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Microsoft Outlook-Mail sichten und den Kalender verwalten." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "PRs, Issues, CI und Veröffentlichungsabläufe bearbeiten." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Vertrauen und verbinden" @@ -9576,6 +9695,10 @@ msgstr "Vertrauen und verbinden" msgid "Try a different search or project filter." msgstr "Versuche eine andere Suche oder einen anderen Projektfilter." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Jetzt testen" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Remotezugriff aktivieren, um einen Kopplungscode anzuzeigen." diff --git a/src/renderer/locales/en/messages.po b/src/renderer/locales/en/messages.po index 05752be0d..2b56373fd 100644 --- a/src/renderer/locales/en/messages.po +++ b/src/renderer/locales/en/messages.po @@ -32,8 +32,8 @@ msgstr "(no message)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# app} other {# apps}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# app} other {# apps}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# project} other {# projects}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# provider ready} other {# providers ready}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# server} other {# servers}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# result} other {# results}}" msgid "{seconds}s ago" msgstr "{seconds}s ago" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# server} other {# servers}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} from {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Approved" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Apps" @@ -1132,6 +1142,10 @@ msgstr "Authentication required" msgid "Authentication setup cannot be imported" msgstr "Authentication setup cannot be imported" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Author" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Authored" @@ -1466,6 +1480,10 @@ msgstr "Built-in servers are managed by Poracode. They can be disabled globally msgid "Built-in shortcut — can't be changed" msgstr "Built-in shortcut — can't be changed" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "by <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Commit, PR, and review composers" msgid "Commits" msgstr "Commits" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Communication" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Community" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Compacting context" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Conflicts with a managed skill" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Connect the AI providers OpenCode can use and sign out of them here." #~ msgstr "Connect to another Poracode desktop or a headless server to browse and manage its projects from here. Get its endpoint and pairing token from Settings → Remote Access on that machine." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Connected" @@ -2345,6 +2375,10 @@ msgstr "Connected" msgid "Connecting to the desktop browser…" msgstr "Connecting to the desktop browser…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Connecting..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Disconnect server" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Discover" +#~ msgid "Discover" +#~ msgstr "Discover" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Discover plugins" +#~ msgid "Discover plugins" +#~ msgstr "Discover plugins" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Effort levels" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Enable {0}" @@ -3641,6 +3678,8 @@ msgstr "Expand todo dock" msgid "Export MCP servers" msgstr "Export MCP servers" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "External" @@ -4090,6 +4129,10 @@ msgstr "Git status for {0}" msgid "Git status for {0}: not a Git repository" msgstr "Git status for {0}: not a Git repository" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Home folder" msgid "Home scope" msgstr "Home scope" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Homepage" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, alias, or user@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Identifier" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Identity and usage stats" @@ -4527,7 +4578,6 @@ msgstr "Install on Windows" msgid "Install packages" msgstr "Install packages" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." @@ -4689,6 +4739,7 @@ msgstr "Lets agents call <0>eval to run arbitrary JavaScript inside the embe #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "License" @@ -4836,6 +4887,7 @@ msgstr "Local changes need to be stashed before pulling from {branch}" msgid "localhost:{0} on desktop" msgstr "localhost:{0} on desktop" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Make the sidebar translucent — the system blur material on Windows 11, a translucent tint elsewhere." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Manage" @@ -4913,6 +4966,10 @@ msgstr "Manage" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Manage global and project agent skills, including skills imported from other providers." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Manage installed plugins and the skills and servers they contribute." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Manage remote environments" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Mark Done" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Marketplace" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "MCP server URL" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "MCP servers" @@ -5123,6 +5182,10 @@ msgstr "MCP transport type" msgid "MCP-powered tools contributed by this plugin." msgstr "MCP-powered tools contributed by this plugin." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCPs" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Merge & Remove" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "No phases yet." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "No plugins installed yet" +#~ msgid "No plugins installed yet" +#~ msgstr "No plugins installed yet" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Not a Git repository" msgid "Not found" msgstr "Not found" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Not supported" msgid "Notes" msgstr "Notes" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Nothing here yet. Install a plugin to add contributions." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Nothing tracked yet. It'll appear here as you use it." @@ -6408,6 +6476,10 @@ msgstr "Other" msgid "Outcome" msgstr "Outcome" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Over pace — runs out early" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Plugin marketplace views" +msgid "Plugin folder" +msgstr "Plugin folder" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Plugin management views" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Plugin marketplace views" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Plugin settings views" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "Public URL updated." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Publisher" +#~ msgid "Publisher" +#~ msgstr "Publisher" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Report an Issue" msgid "repository" msgstr "repository" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Repository" @@ -7926,6 +8012,10 @@ msgstr "Search branches..." msgid "Search files" msgstr "Search files" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Search installed plugins" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Search marketplace skills" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Search plugins" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Search plugins, skills, and apps..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Search plugins, skills, and apps..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Search plugins, skills, and servers..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Search threads" msgid "Search Threads" msgstr "Search Threads" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Search..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Search…" @@ -8153,6 +8251,10 @@ msgstr "Sent selection to terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Set group color" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md is too large." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Slash commands" msgid "Some checks are failing or pending." msgstr "Some checks are failing or pending." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Some contributions could not be loaded" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." @@ -9196,6 +9303,10 @@ msgstr "The server configuration is invalid." msgid "The server returned an invalid MCP response." msgstr "The server returned an invalid MCP response." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "The skill description is missing." @@ -9568,6 +9679,14 @@ msgstr "Translucent sidebar" msgid "Trending now" msgstr "Trending now" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Triage Microsoft Outlook mail and manage your calendar." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Triage PRs, issues, CI, and publish flows." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Trust and connect" @@ -9576,6 +9695,10 @@ msgstr "Trust and connect" msgid "Try a different search or project filter." msgstr "Try a different search or project filter." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Try now" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Turn on remote access to show a pairing code." diff --git a/src/renderer/locales/es/messages.po b/src/renderer/locales/es/messages.po index 96dc88f4a..f12785f6e 100644 --- a/src/renderer/locales/es/messages.po +++ b/src/renderer/locales/es/messages.po @@ -32,8 +32,8 @@ msgstr "(sin mensaje)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# aplicación} other {# aplicaciones}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# aplicación} other {# aplicaciones}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# proyecto} other {# proyectos}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# proveedor listo} other {# proveedores listos}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# servidor} other {# servidores}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# resultado} other {# resultados}}" msgid "{seconds}s ago" msgstr "hace {seconds}s" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# servidor} other {# servidores}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} de {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Aprobado" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Aplicaciones" @@ -1132,6 +1142,10 @@ msgstr "Autenticación requerida" msgid "Authentication setup cannot be imported" msgstr "La configuración de autenticación no se puede importar" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Autor" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Creadas" @@ -1466,6 +1480,10 @@ msgstr "Poracode administra los servidores integrados. Se pueden deshabilitar gl msgid "Built-in shortcut — can't be changed" msgstr "Atajo integrado: no se puede cambiar" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Paquetes de skills y servidores MCP que funcionan con todos los agentes compatibles. Poracode carga cualquier paquete creado según la especificación Agent Plugins." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "por <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Redactores de commit, PR y revisión" msgid "Commits" msgstr "Commits" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Comunicación" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Comunidad" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Compactando el contexto" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Entra en conflicto con un skill gestionado" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Conecta los proveedores de IA que OpenCode puede usar y cierra sesión e #~ msgstr "Conéctate a otro escritorio de Poracode o a un servidor headless para explorar y gestionar sus proyectos desde aquí. Consigue su endpoint y token de emparejamiento en Ajustes → Acceso remoto en esa máquina." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Conectado" @@ -2345,6 +2375,10 @@ msgstr "Conectado" msgid "Connecting to the desktop browser…" msgstr "Conectando al navegador del escritorio…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Conectando..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Desconectar servidor" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Descubrir" +#~ msgid "Discover" +#~ msgstr "Descubrir" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Descubrir plugins" +#~ msgid "Discover plugins" +#~ msgstr "Descubrir plugins" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Niveles de esfuerzo" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Activar {0}" @@ -3641,6 +3678,8 @@ msgstr "Expandir panel de tareas" msgid "Export MCP servers" msgstr "Exportar servidores MCP" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Externo" @@ -4090,6 +4129,10 @@ msgstr "Estado de Git para {0}" msgid "Git status for {0}: not a Git repository" msgstr "Estado de Git para {0}: no es un repositorio Git" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Carpeta de inicio" msgid "Home scope" msgstr "Ámbito de inicio" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Página principal" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, alias o usuario@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Identificador" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Identidad y estadísticas de uso" @@ -4527,7 +4578,6 @@ msgstr "Instalar en Windows" msgid "Install packages" msgstr "Instalar paquetes" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Instala paquetes gestionados por Poracode de Skills y aplicaciones con tecnología MCP para todos los agentes compatibles." @@ -4689,6 +4739,7 @@ msgstr "Permite que los agentes llamen a <0>eval para ejecutar JavaScript ar #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Licencia" @@ -4836,6 +4887,7 @@ msgstr "Es necesario guardar los cambios locales en stash antes de hacer pull de msgid "localhost:{0} on desktop" msgstr "localhost:{0} en el escritorio" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Hace translúcido el panel lateral: el efecto de desenfoque del sistema en Windows 11 y un tinte translúcido en otros casos." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Gestionar" @@ -4913,6 +4966,10 @@ msgstr "Gestionar" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Gestiona los skills globales y de proyecto de los agentes, incluidos los importados de otros proveedores." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Gestiona los plugins instalados y las skills y servidores que aportan." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Administrar entornos remotos" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Marcar como hecho" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Marketplace" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "URL del servidor MCP" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "Servidores MCP" @@ -5123,6 +5182,10 @@ msgstr "Tipo de transporte MCP" msgid "MCP-powered tools contributed by this plugin." msgstr "Herramientas con tecnología MCP aportadas por este plugin." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCPs" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Fusionar y eliminar" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Aún no hay fases." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Aún no hay plugins instalados" +#~ msgid "No plugins installed yet" +#~ msgstr "Aún no hay plugins instalados" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "No es un repositorio Git" msgid "Not found" msgstr "No encontrado" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "No compatible" msgid "Notes" msgstr "Notas" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Aquí no hay nada todavía. Instala un plugin para añadir contribuciones." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Aún no hay nada registrado. Aparecerá aquí a medida que lo uses." @@ -6408,6 +6476,10 @@ msgstr "Otras" msgid "Outcome" msgstr "Resultado" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Por encima del ritmo — se agotará antes" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Vistas del marketplace de plugins" +msgid "Plugin folder" +msgstr "Carpeta de plugins" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Vistas de gestión de plugins" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Vistas del marketplace de plugins" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Vistas de ajustes de plugins" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "URL pública actualizada." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Editor" +#~ msgid "Publisher" +#~ msgstr "Editor" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Informar de un problema" msgid "repository" msgstr "repositorio" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Repositorio" @@ -7926,6 +8012,10 @@ msgstr "Buscar ramas..." msgid "Search files" msgstr "Buscar archivos" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Buscar plugins instalados" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Buscar skills en el marketplace" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Buscar plugins" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Buscar plugins, Skills y aplicaciones..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Buscar plugins, Skills y aplicaciones..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Buscar plugins, skills y servidores..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Buscar hilos" msgid "Search Threads" msgstr "Buscar hilos" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Buscar..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Buscar…" @@ -8153,6 +8251,10 @@ msgstr "Selección enviada al terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Separa las cuentas de Claude Code por directorio de configuración, o apunta un perfil a un proveedor externo (z.ai, …). Abre un perfil para configurar sus variables de entorno, modelos y esfuerzo." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Servidores que este plugin declara en mcp.json. Poracode los pasa a todos los agentes compatibles." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Establecer color del grupo" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md es demasiado grande." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Comandos con barra" msgid "Some checks are failing or pending." msgstr "Algunas comprobaciones están fallando o pendientes." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "No se pudieron cargar algunas contribuciones" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "No se pudieron analizar algunas carpetas de proveedores. Los skills cargados correctamente se siguen mostrando." @@ -9196,6 +9303,10 @@ msgstr "La configuración del servidor no es válida." msgid "The server returned an invalid MCP response." msgstr "El servidor devolvió una respuesta MCP no válida." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "El servidor que lanza este plugin lo mantiene un tercero, no el servicio al que se conecta. Revisa el código fuente antes de activarlo." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "Falta la descripción del skill." @@ -9568,6 +9679,14 @@ msgstr "Panel lateral translúcido" msgid "Trending now" msgstr "Tendencias actuales" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Clasifica el correo de Microsoft Outlook y gestiona tu calendario." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Gestiona PRs, incidencias, CI y flujos de publicación." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Confiar y conectar" @@ -9576,6 +9695,10 @@ msgstr "Confiar y conectar" msgid "Try a different search or project filter." msgstr "Prueba con otra búsqueda o filtro de proyecto." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Probar ahora" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Activa el acceso remoto para mostrar un código de emparejamiento." diff --git a/src/renderer/locales/fr/messages.po b/src/renderer/locales/fr/messages.po index e545e6bfb..1cd3a8d6c 100644 --- a/src/renderer/locales/fr/messages.po +++ b/src/renderer/locales/fr/messages.po @@ -32,8 +32,8 @@ msgstr "(pas de message)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# application} other {# applications}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# application} other {# applications}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# projet} other {# projets}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# fournisseur prêt} other {# fournisseurs prêts}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# serveur} other {# serveurs}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# résultat} other {# résultats}}" msgid "{seconds}s ago" msgstr "il y a {seconds} s" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# serveur} other {# serveurs}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} de {providerLabel} : {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Approuvé" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Applications" @@ -1132,6 +1142,10 @@ msgstr "Authentification requise" msgid "Authentication setup cannot be imported" msgstr "La configuration de l’authentification ne peut pas être importée" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Auteur" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Créées" @@ -1466,6 +1480,10 @@ msgstr "Les serveurs intégrés sont gérés par Poracode. Ils peuvent être dé msgid "Built-in shortcut — can't be changed" msgstr "Raccourci intégré — non modifiable" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Ensembles de compétences et de serveurs MCP qui fonctionnent avec tous les agents pris en charge. Poracode charge tout paquet conçu selon la spécification Agent Plugins." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "par <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Compositeurs de commit, PR et revue" msgid "Commits" msgstr "Commits" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Communication" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Communauté" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Compactage du contexte…" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Entre en conflit avec une compétence gérée" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Connectez ici les fournisseurs d'IA qu'OpenCode peut utiliser et déconn #~ msgstr "Connectez-vous à un autre poste Poracode ou à un serveur sans interface pour parcourir et gérer ses projets ici. Récupérez son point d'accès et son jeton d'appairage dans Paramètres → Accès distant sur cette machine." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Connecté" @@ -2345,6 +2375,10 @@ msgstr "Connecté" msgid "Connecting to the desktop browser…" msgstr "Connexion au navigateur de l'ordinateur…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Connexion..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Déconnecter le serveur" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Découvrir" +#~ msgid "Discover" +#~ msgstr "Découvrir" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Découvrir des plugins" +#~ msgid "Discover plugins" +#~ msgstr "Découvrir des plugins" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Niveaux d'effort" msgid "Electron" msgstr "Électron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Activer {0}" @@ -3641,6 +3678,8 @@ msgstr "Développer le dock de tâches" msgid "Export MCP servers" msgstr "Exporter les serveurs MCP" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Externe" @@ -4090,6 +4129,10 @@ msgstr "Statut Git pour {0}" msgid "Git status for {0}: not a Git repository" msgstr "Statut Git pour {0} : pas un dépôt Git" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Dossier personnel" msgid "Home scope" msgstr "Portée utilisateur" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Page d’accueil" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, alias ou utilisateur@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "Identifiant : {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Identifiant" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Identité et statistiques d'utilisation" @@ -4527,7 +4578,6 @@ msgstr "Installer sous Windows" msgid "Install packages" msgstr "Installer des packages" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Installez des ensembles gérés par Poracode, composés de compétences et d’applications optimisées par MCP, pour chaque agent pris en charge." @@ -4689,6 +4739,7 @@ msgstr "Permet aux agents d'appeler <0>eval pour exécuter du JavaScript arb #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Licence" @@ -4836,6 +4887,7 @@ msgstr "Les modifications locales doivent être mises de côté (stash) avant de msgid "localhost:{0} on desktop" msgstr "localhost:{0} sur le bureau" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Rend la barre latérale translucide — le flou système sur Windows 11, une teinte translucide ailleurs." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Gérer" @@ -4913,6 +4966,10 @@ msgstr "Gérer" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Gérez les compétences globales et de projet des agents, y compris celles importées depuis d’autres fournisseurs." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Gérez les plugins installés ainsi que les compétences et serveurs qu’ils apportent." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Gérer les environnements distants" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Marquer terminé" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Place de marché" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "URL du serveur MCP" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "Serveurs MCP" @@ -5123,6 +5182,10 @@ msgstr "Type de transport MCP" msgid "MCP-powered tools contributed by this plugin." msgstr "Outils optimisés par MCP fournis par ce plugin." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Fusionner et supprimer" @@ -5856,8 +5919,8 @@ msgid "No phases yet." msgstr "Aucune phase pour l'instant." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Aucun plugin installé pour le moment" +#~ msgid "No plugins installed yet" +#~ msgstr "Aucun plugin installé pour le moment" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6007,6 +6070,7 @@ msgstr "Pas un dépôt Git" msgid "Not found" msgstr "Pas trouvé" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6030,6 +6094,10 @@ msgstr "Non pris en charge" msgid "Notes" msgstr "Remarques" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Rien ici pour l’instant. Installez un plugin pour ajouter des contributions." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Rien de suivi pour l'instant. Cela apparaîtra ici au fil de votre utilisation." @@ -6407,6 +6475,10 @@ msgstr "Autres" msgid "Outcome" msgstr "Résultat" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Rythme trop rapide — épuisé trop tôt" @@ -6658,10 +6730,23 @@ msgid "Plugin" msgstr "Plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Vues de la place de marché des plugins" +msgid "Plugin folder" +msgstr "Dossier des plugins" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Vues de gestion des plugins" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Vues de la place de marché des plugins" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Vues des réglages de plugins" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6946,8 +7031,8 @@ msgid "Public URL updated." msgstr "URL publique mise à jour." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Éditeur" +#~ msgid "Publisher" +#~ msgstr "Éditeur" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7431,6 +7516,7 @@ msgstr "Signaler un problème" msgid "repository" msgstr "dépôt" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Dépôt" @@ -7925,6 +8011,10 @@ msgstr "Rechercher des branches..." msgid "Search files" msgstr "Rechercher des fichiers" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Rechercher parmi les plugins installés" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Rechercher des compétences sur la place de marché" @@ -7958,8 +8048,12 @@ msgid "Search plugins" msgstr "Rechercher des plugins" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Rechercher des plugins, des compétences et des applications..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Rechercher des plugins, des compétences et des applications..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Rechercher des plugins, compétences et serveurs..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8015,6 +8109,10 @@ msgstr "Rechercher des fils" msgid "Search Threads" msgstr "Rechercher des fils" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Rechercher..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Rechercher…" @@ -8152,6 +8250,10 @@ msgstr "Sélection envoyée au terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Séparez les comptes Claude Code par dossier de configuration, ou dirigez un profil vers un fournisseur externe (z.ai, …). Ouvrez un profil pour configurer ses variables d'environnement, ses modèles et son effort." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Serveurs déclarés par ce plugin dans mcp.json. Poracode les transmet à tous les agents pris en charge." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Définir la couleur du groupe" @@ -8456,6 +8558,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md est trop volumineux." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8500,6 +8603,10 @@ msgstr "Commandes slash" msgid "Some checks are failing or pending." msgstr "Certaines vérifications échouent ou sont en attente." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Certaines contributions n’ont pas pu être chargées" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Certains dossiers de fournisseurs n’ont pas pu être analysés. Les compétences chargées avec succès restent affichées." @@ -9195,6 +9302,10 @@ msgstr "La configuration du serveur n’est pas valide." msgid "The server returned an invalid MCP response." msgstr "Le serveur a renvoyé une réponse MCP non valide." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "Le serveur lancé par ce plugin est maintenu par un tiers, et non par le service auquel il se connecte. Examinez le code source avant de l’activer." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "La description de la compétence est manquante." @@ -9567,6 +9678,14 @@ msgstr "Barre latérale translucide" msgid "Trending now" msgstr "Tendances actuelles" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Triez le courrier Microsoft Outlook et gérez votre agenda." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Traitez les PR, les tickets, la CI et les flux de publication." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Faire confiance et se connecter" @@ -9575,6 +9694,10 @@ msgstr "Faire confiance et se connecter" msgid "Try a different search or project filter." msgstr "Essayez une autre recherche ou un autre filtre de projet." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Essayer" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Activez l’accès distant pour afficher un code d’association." diff --git a/src/renderer/locales/ja/messages.po b/src/renderer/locales/ja/messages.po index 2d0c25da4..a28bdfe74 100644 --- a/src/renderer/locales/ja/messages.po +++ b/src/renderer/locales/ja/messages.po @@ -32,8 +32,8 @@ msgstr "(メッセージはありません)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# 個のアプリ} other {# 個のアプリ}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# 個のアプリ} other {# 個のアプリ}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {#件のプロジェクト} other {#件のプロジェ msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {#プロバイダーの準備ができています} other {#プロバイダーの準備ができています}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# 個のサーバー} other {# 個のサーバー}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -440,6 +445,10 @@ msgstr "{resultCount, plural, one {#結果} other {#結果}}" msgid "{seconds}s ago" msgstr "{seconds}秒前" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# 個のサーバー} other {# 個のサーバー}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{providerLabel} の {serverName}: {disabledLabel}" @@ -1001,6 +1010,7 @@ msgid "Approved" msgstr "承認済み" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "アプリ" @@ -1131,6 +1141,10 @@ msgstr "認証が必要です" msgid "Authentication setup cannot be imported" msgstr "認証設定はインポートできません" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "作成者" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "自分が作成" @@ -1465,6 +1479,10 @@ msgstr "組み込みサーバーはPoracodeによって管理されます。グ msgid "Built-in shortcut — can't be changed" msgstr "組み込みのショートカット — 変更できません" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "サポートされているすべてのエージェントで動作するスキルとMCPサーバーのバンドルです。Poracode は Agent Plugins 仕様に基づくパッケージをすべて読み込みます。" + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "<0>{author} によって" @@ -2185,6 +2203,16 @@ msgstr "コミット、PR、レビューのコンポーザー" msgid "Commits" msgstr "コミット" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "コミュニケーション" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "コミュニティ" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "コンテキストの圧縮" @@ -2299,6 +2327,7 @@ msgid "Conflicts with a managed skill" msgstr "管理対象スキルと競合しています" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2337,6 +2366,7 @@ msgstr "OpenCode が使用できる AI プロバイダーをここで接続し #~ msgstr "別の Poracode デスクトップまたはヘッドレスサーバーに接続すると、そのプロジェクトをここから参照・管理できます。エンドポイントとペアリングトークンは、その端末の「設定 → リモートアクセス」から取得してください。" #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "接続済み" @@ -2344,6 +2374,10 @@ msgstr "接続済み" msgid "Connecting to the desktop browser…" msgstr "デスクトップのブラウザに接続しています…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "接続中..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3193,12 +3227,12 @@ msgid "Disconnect server" msgstr "サーバーを切断" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "探す" +#~ msgid "Discover" +#~ msgstr "探す" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "プラグインを探す" +#~ msgid "Discover plugins" +#~ msgstr "プラグインを探す" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3390,7 +3424,10 @@ msgstr "推論レベル" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "{0} を有効化" @@ -3640,6 +3677,8 @@ msgstr "Todo ドックを展開する" msgid "Export MCP servers" msgstr "MCPサーバーをエクスポート" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "外部" @@ -4089,6 +4128,10 @@ msgstr "{0}の Git ステータス" msgid "Git status for {0}: not a Git repository" msgstr "{0}の Git ステータス: Git リポジトリではありません" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4296,6 +4339,10 @@ msgstr "ホームフォルダー" msgid "Home scope" msgstr "ホームスコープ" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "ホームページ" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com、エイリアス、または user@host.com" @@ -4317,6 +4364,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID:{0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "識別子" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "アイデンティティと利用統計" @@ -4526,7 +4577,6 @@ msgstr "Windows にインストールする" msgid "Install packages" msgstr "パッケージをインストールする" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Poracode が管理するスキルと MCP 対応アプリのバンドルを、対応するすべてのエージェント向けにインストールします。" @@ -4688,6 +4738,7 @@ msgstr "エージェントが <0>eval を呼び出して、埋め込みペ #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "ライセンス" @@ -4835,6 +4886,7 @@ msgstr "ローカルの変更は{branch}からプルする前に隠しておく msgid "localhost:{0} on desktop" msgstr "デスクトップの localhost:{0}" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4905,6 +4957,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "サイドバーを半透明にします。Windows 11ではシステムのぼかし素材を、それ以外では半透明の色付けを使用します。" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "管理" @@ -4912,6 +4965,10 @@ msgstr "管理" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "他のプロバイダーからインポートしたスキルを含む、グローバルおよびプロジェクトのエージェントスキルを管理します。" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "インストール済みのプラグインと、それが提供するスキルやサーバーを管理します。" + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "リモート環境を管理" @@ -5007,6 +5064,7 @@ msgid "Mark Done" msgstr "完了マークを付ける" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "マーケットプレイス" @@ -5098,6 +5156,7 @@ msgid "MCP server URL" msgstr "MCPサーバーのURL" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "MCPサーバー" @@ -5122,6 +5181,10 @@ msgstr "MCPトランスポートタイプ" msgid "MCP-powered tools contributed by this plugin." msgstr "このプラグインが提供する MCP 対応ツール。" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "マージして削除" @@ -5855,8 +5918,8 @@ msgid "No phases yet." msgstr "まだフェーズはありません。" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "まだプラグインがインストールされていません" +#~ msgid "No plugins installed yet" +#~ msgstr "まだプラグインがインストールされていません" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6006,6 +6069,7 @@ msgstr "Git リポジトリではありません" msgid "Not found" msgstr "見つかりません" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6029,6 +6093,10 @@ msgstr "サポートされていません" msgid "Notes" msgstr "注意事項" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "まだ何もありません。プラグインをインストールしてコントリビューションを追加してください。" + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "まだ何も記録されていません。使用するとここに表示されます。" @@ -6406,6 +6474,10 @@ msgstr "その他" msgid "Outcome" msgstr "結果" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "オーバーペース — 早めに力尽きてしまう" @@ -6657,10 +6729,23 @@ msgid "Plugin" msgstr "プラグイン" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "プラグインマーケットプレイスの表示" +msgid "Plugin folder" +msgstr "プラグインフォルダ" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "プラグイン管理ビュー" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "プラグインマーケットプレイスの表示" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "プラグイン設定ビュー" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6945,8 +7030,8 @@ msgid "Public URL updated." msgstr "公開URLを更新しました。" #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "提供元" +#~ msgid "Publisher" +#~ msgstr "提供元" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7430,6 +7515,7 @@ msgstr "問題を報告する" msgid "repository" msgstr "リポジトリ" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "リポジトリ" @@ -7924,6 +8010,10 @@ msgstr "ブランチを検索..." msgid "Search files" msgstr "ファイルの検索" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "インストール済みプラグインを検索" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "マーケットプレイスのスキルを検索" @@ -7957,8 +8047,12 @@ msgid "Search plugins" msgstr "プラグインを検索" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "プラグイン、スキル、アプリを検索..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "プラグイン、スキル、アプリを検索..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "プラグイン・スキル・サーバーを検索..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8014,6 +8108,10 @@ msgstr "スレッドを検索する" msgid "Search Threads" msgstr "スレッドの検索" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "検索..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "検索…" @@ -8151,6 +8249,10 @@ msgstr "選択内容を端末に送信しました。" msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Codeのアカウントを設定ディレクトリごとに分離するか、プロファイルを外部プロバイダー(z.aiなど)に向けます。プロファイルを開くと、その環境変数・モデル・推論レベルを設定できます。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "このプラグインが mcp.json で宣言しているサーバーです。Poracode はサポートされているすべてのエージェントに渡します。" + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "グループの色を設定" @@ -8455,6 +8557,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md が大きすぎます。" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8499,6 +8602,10 @@ msgstr "スラッシュコマンド" msgid "Some checks are failing or pending." msgstr "一部のチェックが失敗または保留中です。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "一部のコントリビューションを読み込めませんでした" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "一部のプロバイダーフォルダーをスキャンできませんでした。正常に読み込まれたスキルは引き続き表示されます。" @@ -9194,6 +9301,10 @@ msgstr "サーバー構成が無効です。" msgid "The server returned an invalid MCP response." msgstr "サーバーから無効なMCPレスポンスが返されました。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "このプラグインが起動するサーバーは、接続先のサービスではなく第三者によって保守されています。有効にする前にソースを確認してください。" + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "スキルの説明がありません。" @@ -9566,6 +9677,14 @@ msgstr "半透明のサイドバー" msgid "Trending now" msgstr "現在のトレンド" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Microsoft Outlook のメールを整理し、カレンダーを管理します。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "PR、Issue、CI、公開フローを処理します。" + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "信頼して接続" @@ -9574,6 +9693,10 @@ msgstr "信頼して接続" msgid "Try a different search or project filter." msgstr "別の検索語句またはプロジェクトフィルターを試してください。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "試してみる" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "リモートアクセスをオンにすると、ペアリングコードが表示されます。" diff --git a/src/renderer/locales/ko/messages.po b/src/renderer/locales/ko/messages.po index b4536e820..30f547819 100644 --- a/src/renderer/locales/ko/messages.po +++ b/src/renderer/locales/ko/messages.po @@ -32,8 +32,8 @@ msgstr "(메시지 없음)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {#개 앱} other {#개 앱}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {#개 앱} other {#개 앱}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {#개 프로젝트} other {#개 프로젝트}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# 공급자 준비됨} other {# 공급자 준비됨}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {서버 #개} other {서버 #개}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# 결과} other {# 결과}}" msgid "{seconds}s ago" msgstr "{seconds}초 전" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {서버 #개} other {서버 #개}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{providerLabel}의 {serverName}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "승인됨" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "앱" @@ -1132,6 +1142,10 @@ msgstr "인증 필요" msgid "Authentication setup cannot be imported" msgstr "인증 설정은 가져올 수 없습니다" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "작성자" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "내가 작성" @@ -1466,6 +1480,10 @@ msgstr "기본 제공 서버는 Poracode에서 관리합니다. 전역으로 비 msgid "Built-in shortcut — can't be changed" msgstr "기본 제공 단축키 — 변경할 수 없습니다" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "지원되는 모든 에이전트에서 작동하는 스킬과 MCP 서버 번들입니다. Poracode는 Agent Plugins 사양으로 만든 패키지를 모두 로드합니다." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "작성자: <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "커밋, PR, 리뷰 작성기" msgid "Commits" msgstr "커밋" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "커뮤니케이션" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "커뮤니티" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "컨텍스트 압축" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "관리 스킬과 충돌함" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "OpenCode가 사용할 수 있는 AI 공급자를 여기에서 연결하 #~ msgstr "다른 Poracode 데스크톱이나 헤드리스 서버에 연결하면 여기에서 해당 프로젝트를 탐색하고 관리할 수 있습니다. 해당 기기의 설정 → 원격 액세스에서 엔드포인트와 페어링 토큰을 확인하세요." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "연결됨" @@ -2345,6 +2375,10 @@ msgstr "연결됨" msgid "Connecting to the desktop browser…" msgstr "데스크톱 브라우저에 연결 중…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "연결 중..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "서버 연결 해제" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "둘러보기" +#~ msgid "Discover" +#~ msgstr "둘러보기" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "플러그인 둘러보기" +#~ msgid "Discover plugins" +#~ msgstr "플러그인 둘러보기" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "추론 강도 단계" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "{0} 활성화" @@ -3641,6 +3678,8 @@ msgstr "할 일 도크 확장" msgid "Export MCP servers" msgstr "MCP 서버 내보내기" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "외부" @@ -4090,6 +4129,10 @@ msgstr "{0}의 Git 상태" msgid "Git status for {0}: not a Git repository" msgstr "{0}에 대한 Git 상태: Git 저장소가 아닙니다." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "홈 폴더" msgid "Home scope" msgstr "홈 범위" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "홈페이지" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, 별칭 또는 user@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID:{0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "식별자" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "신원 정보 및 사용량 통계" @@ -4527,7 +4578,6 @@ msgstr "Windows에 설치" msgid "Install packages" msgstr "패키지 설치" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "지원되는 모든 에이전트에 Poracode가 관리하는 스킬 및 MCP 기반 앱 번들을 설치합니다." @@ -4689,6 +4739,7 @@ msgstr "에이전트가 <0>eval을 호출하여 삽입된 페이지 내에 #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "라이센스" @@ -4836,6 +4887,7 @@ msgstr "{branch}에서 가져오기 전에 로컬 변경 사항을 숨겨야 합 msgid "localhost:{0} on desktop" msgstr "데스크톱의 localhost:{0}" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "사이드바를 반투명하게 만듭니다 — Windows 11에서는 시스템 블러 효과, 그 외 환경에서는 반투명 색조를 사용합니다." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "관리" @@ -4913,6 +4966,10 @@ msgstr "관리" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "다른 제공자에서 가져온 스킬을 포함하여 전역 및 프로젝트 에이전트 스킬을 관리합니다." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "설치된 플러그인과 해당 플러그인이 제공하는 스킬 및 서버를 관리합니다." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "원격 환경 관리" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "완료로 표시" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "마켓플레이스" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "MCP 서버 URL" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "MCP 서버" @@ -5123,6 +5182,10 @@ msgstr "MCP 전송 유형" msgid "MCP-powered tools contributed by this plugin." msgstr "이 플러그인이 제공하는 MCP 기반 도구입니다." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "병합 및 제거" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "아직 단계가 없습니다." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "아직 설치된 플러그인이 없습니다" +#~ msgid "No plugins installed yet" +#~ msgstr "아직 설치된 플러그인이 없습니다" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Git 저장소가 아님" msgid "Not found" msgstr "찾을 수 없음" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "지원되지 않음" msgid "Notes" msgstr "메모" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "아직 아무것도 없습니다. 플러그인을 설치해 기여 항목을 추가하세요." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "아직 추적된 항목이 없습니다. 사용하면 여기에 표시됩니다." @@ -6408,6 +6476,10 @@ msgstr "기타" msgid "Outcome" msgstr "결과" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "사용 속도 초과 — 조기 소진" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "플러그인" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "플러그인 마켓플레이스 보기" +msgid "Plugin folder" +msgstr "플러그인 폴더" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "플러그인 관리 보기" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "플러그인 마켓플레이스 보기" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "플러그인 설정 보기" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "공개 URL이 업데이트되었습니다." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "게시자" +#~ msgid "Publisher" +#~ msgstr "게시자" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "문제 신고" msgid "repository" msgstr "저장소" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "저장소" @@ -7926,6 +8012,10 @@ msgstr "브랜치 검색..." msgid "Search files" msgstr "파일 검색" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "설치된 플러그인 검색" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "마켓플레이스 스킬 검색" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "플러그인 검색" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "플러그인, 스킬 및 앱 검색..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "플러그인, 스킬 및 앱 검색..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "플러그인, 스킬, 서버 검색..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "스레드 검색" msgid "Search Threads" msgstr "스레드 검색" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "검색..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "검색…" @@ -8153,6 +8251,10 @@ msgstr "선택 항목을 터미널로 보냈습니다." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Code 계정을 설정 디렉터리별로 분리하거나, 프로필을 외부 제공자(z.ai 등)로 지정합니다. 프로필을 열어 환경 변수, 모델, 추론 강도를 구성하세요." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "이 플러그인이 mcp.json에 선언한 서버입니다. Poracode가 지원되는 모든 에이전트에 전달합니다." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "그룹 색상 설정" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md가 너무 큽니다." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "슬래시 명령" msgid "Some checks are failing or pending." msgstr "일부 검사가 실패했거나 보류 중입니다." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "일부 기여를 불러오지 못했습니다" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "일부 제공자 폴더를 검사하지 못했습니다. 성공적으로 불러온 스킬은 계속 표시됩니다." @@ -9196,6 +9303,10 @@ msgstr "서버 구성이 잘못되었습니다." msgid "The server returned an invalid MCP response." msgstr "서버가 잘못된 MCP 응답을 반환했습니다." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "이 플러그인이 실행하는 서버는 연결 대상 서비스가 아니라 제3자가 유지 관리합니다. 활성화하기 전에 소스를 확인하세요." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "스킬 설명이 없습니다." @@ -9568,6 +9679,14 @@ msgstr "반투명 사이드바" msgid "Trending now" msgstr "현재 인기" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Microsoft Outlook 메일을 분류하고 일정을 관리합니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "PR, 이슈, CI, 배포 흐름을 처리합니다." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "신뢰하고 연결" @@ -9576,6 +9695,10 @@ msgstr "신뢰하고 연결" msgid "Try a different search or project filter." msgstr "다른 검색어 또는 프로젝트 필터를 사용해 보세요." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "지금 사용해보기" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "페어링 코드를 표시하려면 원격 액세스를 켜세요." diff --git a/src/renderer/locales/pl/messages.po b/src/renderer/locales/pl/messages.po index b3b185b10..e451b7eb0 100644 --- a/src/renderer/locales/pl/messages.po +++ b/src/renderer/locales/pl/messages.po @@ -32,8 +32,8 @@ msgstr "(brak wiadomości)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# aplikacja} other {# aplikacji}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# aplikacja} other {# aplikacji}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# projekt} few {# projekty} many {# projektów} other { msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# dostawca gotowy} few {# dostawcy gotowi} many {# dostawców gotowych} other {# dostawcy gotowych}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# serwer} other {# serwerów}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# wynik} few {# wyniki} many {# wyników} oth msgid "{seconds}s ago" msgstr "{seconds} s temu" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# serwer} other {# serwerów}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} z {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Zatwierdzone" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Aplikacje" @@ -1132,6 +1142,10 @@ msgstr "Wymagane uwierzytelnienie" msgid "Authentication setup cannot be imported" msgstr "Nie można zaimportować konfiguracji uwierzytelniania" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Autor" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Utworzone przeze mnie" @@ -1466,6 +1480,10 @@ msgstr "Wbudowane serwery są zarządzane przez Poracode. Można je globalnie wy msgid "Built-in shortcut — can't be changed" msgstr "Skrót wbudowany — nie można go zmienić" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Pakiety umiejętności i serwerów MCP działające z każdym obsługiwanym agentem. Poracode ładuje każdy pakiet zbudowany zgodnie ze specyfikacją Agent Plugins." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "przez <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Kompozytory commita, PR i recenzji" msgid "Commits" msgstr "Commity" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Komunikacja" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Społeczność" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Kompaktowanie kontekstu" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Powoduje konflikt z zarządzaną umiejętnością" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Połącz dostawców AI, których może używać OpenCode, i wyloguj się #~ msgstr "Połącz się z innym komputerem z Poracode lub serwerem bez interfejsu, aby przeglądać i zarządzać jego projektami tutaj. Adres i token parowania znajdziesz w Ustawienia → Dostęp zdalny na tamtym urządzeniu." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Połączono" @@ -2345,6 +2375,10 @@ msgstr "Połączono" msgid "Connecting to the desktop browser…" msgstr "Łączenie z przeglądarką desktopa…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Łączenie..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Rozłącz serwer" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Odkrywaj" +#~ msgid "Discover" +#~ msgstr "Odkrywaj" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Odkrywaj wtyczki" +#~ msgid "Discover plugins" +#~ msgstr "Odkrywaj wtyczki" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Poziomy rozumowania" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Włącz {0}" @@ -3641,6 +3678,8 @@ msgstr "Rozwiń dok zadań do wykonania" msgid "Export MCP servers" msgstr "Eksportuj serwery MCP" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Zewnętrzny" @@ -4090,6 +4129,10 @@ msgstr "Status Git dla {0}" msgid "Git status for {0}: not a Git repository" msgstr "Status Git dla {0}: to nie jest repozytorium Git" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Folder domowy" msgid "Home scope" msgstr "Zakres domowy" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Strona domowa" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, alias lub użytkownik@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "Identyfikator: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Identyfikator" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Tożsamość i statystyki użycia" @@ -4527,7 +4578,6 @@ msgstr "Zainstaluj w systemie Windows" msgid "Install packages" msgstr "Zainstaluj pakiety" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Instaluj zarządzane przez Poracode pakiety umiejętności i aplikacji opartych na MCP dla każdego obsługiwanego agenta." @@ -4689,6 +4739,7 @@ msgstr "Umożliwia agentom wywoływanie <0>eval w celu uruchomienia dowolneg #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Licencja" @@ -4836,6 +4887,7 @@ msgstr "Lokalne zmiany muszą zostać schowane przed pobraniem z {branch}" msgid "localhost:{0} on desktop" msgstr "localhost:{0} na komputerze" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Uczyń pasek boczny półprzezroczystym — systemowy efekt rozmycia w Windows 11, półprzezroczysty odcień w pozostałych przypadkach." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Zarządzaj" @@ -4913,6 +4966,10 @@ msgstr "Zarządzaj" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Zarządzaj globalnymi umiejętnościami agentów i umiejętnościami projektu, w tym zaimportowanymi od innych dostawców." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Zarządzaj zainstalowanymi wtyczkami oraz umiejętnościami i serwerami, które wnoszą." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Zarządzaj środowiskami zdalnymi" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Oznacz gotowe" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Marketplace" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "Adres URL serwera MCP" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "Serwery MCP" @@ -5123,6 +5182,10 @@ msgstr "Typ transportu MCP" msgid "MCP-powered tools contributed by this plugin." msgstr "Narzędzia oparte na MCP udostępniane przez tę wtyczkę." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Scal i usuń" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Nie ma jeszcze faz." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Nie zainstalowano jeszcze żadnych wtyczek" +#~ msgid "No plugins installed yet" +#~ msgstr "Nie zainstalowano jeszcze żadnych wtyczek" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "To nie jest repozytorium Git" msgid "Not found" msgstr "Nie znaleziono" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Nieobsługiwane" msgid "Notes" msgstr "Notatki" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Jeszcze nic tu nie ma. Zainstaluj wtyczkę, aby dodać wkłady." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Nic jeszcze nie zarejestrowano. Pojawi się tutaj w miarę korzystania." @@ -6408,6 +6476,10 @@ msgstr "Inne" msgid "Outcome" msgstr "Wynik" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Nadmierne tempo — kończy się wcześnie" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Wtyczka" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Widoki rynku wtyczek" +msgid "Plugin folder" +msgstr "Folder wtyczek" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Widoki zarządzania wtyczkami" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Widoki rynku wtyczek" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Widoki ustawień wtyczek" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "Zaktualizowano publiczny URL." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Wydawca" +#~ msgid "Publisher" +#~ msgstr "Wydawca" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Zgłoś problem" msgid "repository" msgstr "repozytorium" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Repozytorium" @@ -7926,6 +8012,10 @@ msgstr "Szukaj gałęzi..." msgid "Search files" msgstr "Wyszukaj pliki" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Szukaj zainstalowanych wtyczek" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Przeszukaj umiejętności w marketplace" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Szukaj wtyczek" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Szukaj wtyczek, umiejętności i aplikacji..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Szukaj wtyczek, umiejętności i aplikacji..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Szukaj wtyczek, umiejętności i serwerów..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Przeszukaj wątki" msgid "Search Threads" msgstr "Przeszukaj wątki" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Szukaj..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Szukaj…" @@ -8153,6 +8251,10 @@ msgstr "Wysłano zaznaczenie do terminala." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Oddziel konta Claude Code według katalogu konfiguracji lub skieruj profil na zewnętrznego dostawcę (z.ai, …). Otwórz profil, aby skonfigurować jego zmienne środowiskowe, modele i poziom rozumowania." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Serwery deklarowane przez tę wtyczkę w mcp.json. Poracode przekazuje je każdemu obsługiwanemu agentowi." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Ustaw kolor grupy" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "Plik SKILL.md jest za duży." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Polecenia z ukośnikiem" msgid "Some checks are failing or pending." msgstr "Niektóre kontrole kończą się niepowodzeniem lub oczekują na realizację." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Nie udało się wczytać części wkładów" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Nie udało się przeskanować niektórych folderów dostawców. Pomyślnie wczytane umiejętności są nadal wyświetlane." @@ -9196,6 +9303,10 @@ msgstr "Konfiguracja serwera jest nieprawidłowa." msgid "The server returned an invalid MCP response." msgstr "Serwer zwrócił nieprawidłową odpowiedź MCP." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "Serwer uruchamiany przez tę wtyczkę jest utrzymywany przez podmiot zewnętrzny, a nie przez usługę, z którą się łączy. Sprawdź kod źródłowy przed włączeniem." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "Brakuje opisu umiejętności." @@ -9568,6 +9679,14 @@ msgstr "Półprzezroczysty pasek boczny" msgid "Trending now" msgstr "Popularne teraz" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Segreguj pocztę Microsoft Outlook i zarządzaj kalendarzem." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Obsługuj PR-y, zgłoszenia, CI i procesy publikacji." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Zaufaj i połącz" @@ -9576,6 +9695,10 @@ msgstr "Zaufaj i połącz" msgid "Try a different search or project filter." msgstr "Spróbuj innego wyszukiwania lub filtra projektu." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Wypróbuj" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Włącz dostęp zdalny, aby wyświetlić kod parowania." diff --git a/src/renderer/locales/pt-BR/messages.po b/src/renderer/locales/pt-BR/messages.po index 89a314fd7..34a275c3d 100644 --- a/src/renderer/locales/pt-BR/messages.po +++ b/src/renderer/locales/pt-BR/messages.po @@ -32,8 +32,8 @@ msgstr "(sem mensagem)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# aplicativo} other {# aplicativos}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# aplicativo} other {# aplicativos}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# projeto} other {# projetos}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# provedor pronto} other {# provedores prontos}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# servidor} other {# servidores}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# resultado} other {# resultados}}" msgid "{seconds}s ago" msgstr "há {seconds}s" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# servidor} other {# servidores}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} de {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Aprovado" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Aplicativos" @@ -1132,6 +1142,10 @@ msgstr "Autenticação necessária" msgid "Authentication setup cannot be imported" msgstr "A configuração de autenticação não pode ser importada" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Autor" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "De sua autoria" @@ -1466,6 +1480,10 @@ msgstr "Os servidores integrados são gerenciados pelo Poracode. Eles podem ser msgid "Built-in shortcut — can't be changed" msgstr "Atalho integrado — não pode ser alterado" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Pacotes de skills e servidores MCP que funcionam com todos os agentes compatíveis. O Poracode carrega qualquer pacote criado conforme a especificação Agent Plugins." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "por <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Composers de commit, PR e revisão" msgid "Commits" msgstr "Commits" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Comunicação" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Comunidade" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Compactando contexto" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Conflita com uma skill gerenciada" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Conecte os provedores de IA que o OpenCode pode usar e saia deles aqui." #~ msgstr "Conecte-se a outro desktop do Poracode ou a um servidor headless para navegar e gerenciar seus projetos aqui. Obtenha o endpoint e o token de pareamento em Configurações → Acesso remoto naquela máquina." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Conectado" @@ -2345,6 +2375,10 @@ msgstr "Conectado" msgid "Connecting to the desktop browser…" msgstr "Conectando ao navegador do desktop…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Conectando..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Desconectar servidor" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Descobrir" +#~ msgid "Discover" +#~ msgstr "Descobrir" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Descobrir plugins" +#~ msgid "Discover plugins" +#~ msgstr "Descobrir plugins" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Níveis de esforço" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Ativar {0}" @@ -3641,6 +3678,8 @@ msgstr "Expandir dock de tarefas" msgid "Export MCP servers" msgstr "Exportar servidores MCP" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Externo" @@ -4090,6 +4129,10 @@ msgstr "Status do Git para {0}" msgid "Git status for {0}: not a Git repository" msgstr "Status do Git para {0}: não é um repositório Git" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Pasta inicial" msgid "Home scope" msgstr "Escopo inicial" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Página inicial" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, alias ou usuário@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Identificador" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Identidade e estatísticas de uso" @@ -4527,7 +4578,6 @@ msgstr "Instalar no Windows" msgid "Install packages" msgstr "Instalar pacotes" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Instale pacotes gerenciados pelo Poracode com Skills e aplicativos baseados em MCP para todos os agentes compatíveis." @@ -4689,6 +4739,7 @@ msgstr "Permite que os agentes chamem <0>eval para executar JavaScript arbit #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Licença" @@ -4836,6 +4887,7 @@ msgstr "As alterações locais precisam ser armazenadas (stash) antes de fazer p msgid "localhost:{0} on desktop" msgstr "localhost:{0} no desktop" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Torna a barra lateral translúcida — o material de desfoque do sistema no Windows 11, um tom translúcido nas demais plataformas." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Gerenciar" @@ -4913,6 +4966,10 @@ msgstr "Gerenciar" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Gerencie skills globais e de projeto dos agentes, inclusive skills importadas de outros provedores." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Gerencie os plugins instalados e as skills e servidores que eles fornecem." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Gerenciar ambientes remotos" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Marcar como concluído" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Marketplace" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "URL do servidor MCP" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "Servidores MCP" @@ -5123,6 +5182,10 @@ msgstr "Tipo de transporte MCP" msgid "MCP-powered tools contributed by this plugin." msgstr "Ferramentas baseadas em MCP fornecidas por este plugin." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCPs" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Mesclar e remover" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Ainda não há fases." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Nenhum plugin instalado ainda" +#~ msgid "No plugins installed yet" +#~ msgstr "Nenhum plugin instalado ainda" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Não é um repositório Git" msgid "Not found" msgstr "Não encontrado" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Não compatível" msgid "Notes" msgstr "Notas" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Nada aqui ainda. Instale um plugin para adicionar contribuições." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Nada registrado ainda. Aparecerá aqui conforme você usar." @@ -6408,6 +6476,10 @@ msgstr "Outros" msgid "Outcome" msgstr "Resultado" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Acima do ritmo — acaba cedo" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Exibições do marketplace de plugins" +msgid "Plugin folder" +msgstr "Pasta de plugins" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Visualizações de gerenciamento de plugins" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Exibições do marketplace de plugins" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Visualizações de configurações de plugins" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "URL pública atualizada." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Publicador" +#~ msgid "Publisher" +#~ msgstr "Publicador" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Informar um problema" msgid "repository" msgstr "repositório" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Repositório" @@ -7926,6 +8012,10 @@ msgstr "Pesquisar branches..." msgid "Search files" msgstr "Pesquisar arquivos" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Buscar plugins instalados" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Pesquisar skills no marketplace" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Pesquisar plugins" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Pesquisar plugins, Skills e aplicativos..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Pesquisar plugins, Skills e aplicativos..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Buscar plugins, skills e servidores..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Pesquisar tópicos" msgid "Search Threads" msgstr "Pesquisar tópicos" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Buscar..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Pesquisar…" @@ -8153,6 +8251,10 @@ msgstr "Seleção enviada para o terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Separe contas do Claude Code por diretório de configuração ou aponte um perfil para um provedor externo (z.ai, …). Abra um perfil para configurar suas variáveis de ambiente, modelos e esforço." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Servidores que este plugin declara em mcp.json. O Poracode os repassa a todos os agentes compatíveis." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Definir cor do grupo" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "O SKILL.md é grande demais." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Comandos de barra" msgid "Some checks are failing or pending." msgstr "Algumas verificações estão falhando ou pendentes." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Não foi possível carregar algumas contribuições" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Não foi possível verificar algumas pastas de provedores. As skills carregadas com sucesso continuam sendo exibidas." @@ -9196,6 +9303,10 @@ msgstr "A configuração do servidor é inválida." msgid "The server returned an invalid MCP response." msgstr "O servidor retornou uma resposta MCP inválida." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "O servidor que este plugin inicia é mantido por terceiros, não pelo serviço ao qual se conecta. Revise o código-fonte antes de ativá-lo." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "A descrição da skill está ausente." @@ -9568,6 +9679,14 @@ msgstr "Barra lateral translúcida" msgid "Trending now" msgstr "Em alta agora" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Faça a triagem do e-mail do Microsoft Outlook e gerencie sua agenda." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Cuide de PRs, issues, CI e fluxos de publicação." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Confiar e conectar" @@ -9576,6 +9695,10 @@ msgstr "Confiar e conectar" msgid "Try a different search or project filter." msgstr "Tente outra pesquisa ou filtro de projeto." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Testar agora" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Ative o acesso remoto para mostrar um código de pareamento." diff --git a/src/renderer/locales/ru/messages.po b/src/renderer/locales/ru/messages.po index 25a890db1..cb3a95b8f 100644 --- a/src/renderer/locales/ru/messages.po +++ b/src/renderer/locales/ru/messages.po @@ -32,8 +32,8 @@ msgstr "(нет сообщения)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# приложение} other {# приложений}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# приложение} other {# приложений}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# проект} few {# проекта} many {# прое msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# провайдер готов} few {# провайдера готовы} many {# провайдеров готовы} other {# провайдера готовы}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# сервер} other {# серверов}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# результат} few {# результа msgid "{seconds}s ago" msgstr "{seconds}с назад" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# сервер} other {# серверов}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} из {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Одобрено" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Приложения" @@ -1132,6 +1142,10 @@ msgstr "Требуется аутентификация" msgid "Authentication setup cannot be imported" msgstr "Настройку аутентификации нельзя импортировать" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Автор" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Созданные мной" @@ -1466,6 +1480,10 @@ msgstr "Встроенные серверы управляются Poracode. И msgid "Built-in shortcut — can't be changed" msgstr "Встроенное сочетание клавиш — изменить нельзя" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Наборы навыков и MCP-серверов, работающие с любым поддерживаемым агентом. Poracode загружает любой пакет, созданный по спецификации Agent Plugins." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "автор: <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Поля ввода коммита, PR и проверки" msgid "Commits" msgstr "Коммиты" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Коммуникации" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Сообщество" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Сжатие контекста" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Конфликтует с управляемым навыком" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Подключайте провайдеров ИИ, которые мо #~ msgstr "Подключитесь к другому компьютеру с Poracode или к серверу без интерфейса, чтобы просматривать проекты и управлять ими отсюда. Адрес и код сопряжения можно получить в разделе «Настройки → Удалённый доступ» на той машине." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Подключено" @@ -2345,6 +2375,10 @@ msgstr "Подключено" msgid "Connecting to the desktop browser…" msgstr "Подключение к браузеру на десктопе…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Подключение..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Отключить сервер" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Обзор" +#~ msgid "Discover" +#~ msgstr "Обзор" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Найти плагины" +#~ msgid "Discover plugins" +#~ msgstr "Найти плагины" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Уровни рассуждений" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Включить {0}" @@ -3641,6 +3678,8 @@ msgstr "Развернуть панель задач" msgid "Export MCP servers" msgstr "Экспортировать серверы MCP" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Внешний" @@ -4090,6 +4129,10 @@ msgstr "Состояние Git для {0}" msgid "Git status for {0}: not a Git repository" msgstr "Состояние Git для {0}: не репозиторий Git" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Домашняя папка" msgid "Home scope" msgstr "Домашняя область" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Домашняя страница" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, псевдоним или пользователь@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Идентификатор" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Личные данные и статистика использования" @@ -4527,7 +4578,6 @@ msgstr "Установить в Windows" msgid "Install packages" msgstr "Установить пакеты" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Установите управляемые Poracode наборы навыков и приложений на базе MCP для каждого поддерживаемого агента." @@ -4689,6 +4739,7 @@ msgstr "Позволяет агентам вызывать <0>eval для в #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Лицензия" @@ -4836,6 +4887,7 @@ msgstr "Перед выполнением pull из {branch} локальные msgid "localhost:{0} on desktop" msgstr "localhost:{0} на десктопе" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Сделать боковую панель полупрозрачной — системное размытие в Windows 11, полупрозрачный оттенок в остальных системах." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Управление" @@ -4913,6 +4966,10 @@ msgstr "Управление" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Управляйте глобальными навыками агентов и навыками проекта, включая импортированные из других провайдеров." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Управляйте установленными плагинами, а также навыками и серверами, которые они добавляют." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Управление удалёнными средами" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Отметить выполненным" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Маркетплейс" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "URL сервера MCP" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "Серверы MCP" @@ -5123,6 +5182,10 @@ msgstr "Тип транспорта MCP" msgid "MCP-powered tools contributed by this plugin." msgstr "Инструменты на базе MCP, предоставляемые этим плагином." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Слить и удалить" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Фаз пока нет." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Плагины пока не установлены" +#~ msgid "No plugins installed yet" +#~ msgstr "Плагины пока не установлены" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Не репозиторий Git" msgid "Not found" msgstr "Не найдено" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Не поддерживается" msgid "Notes" msgstr "Заметки" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Здесь пока пусто. Установите плагин, чтобы добавить компоненты." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Пока ничего не отслежено. Данные появятся здесь по мере использования." @@ -6408,6 +6476,10 @@ msgstr "Прочие" msgid "Outcome" msgstr "Результат" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Темп превышен — закончится раньше" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Плагин" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Представления магазина плагинов" +msgid "Plugin folder" +msgstr "Папка плагинов" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Режимы управления плагинами" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Представления магазина плагинов" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Режимы настроек плагинов" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "Публичный URL обновлён." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Издатель" +#~ msgid "Publisher" +#~ msgstr "Издатель" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Сообщить о проблеме" msgid "repository" msgstr "репозиторий" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Репозиторий" @@ -7926,6 +8012,10 @@ msgstr "Поиск веток..." msgid "Search files" msgstr "Поиск файлов" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Поиск установленных плагинов" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Поиск навыков в маркетплейсе" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Поиск плагинов" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Поиск плагинов, навыков и приложений..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Поиск плагинов, навыков и приложений..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Поиск плагинов, навыков и серверов..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Поиск тредов" msgid "Search Threads" msgstr "Поиск тредов" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Поиск..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Поиск…" @@ -8153,6 +8251,10 @@ msgstr "Выбор отправлен в терминал." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Разделяйте аккаунты Claude Code по каталогу конфигурации или направляйте профиль на внешнего провайдера (z.ai, …). Откройте профиль, чтобы настроить его переменные окружения, модели и уровни рассуждений." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Серверы, объявленные этим плагином в mcp.json. Poracode передаёт их всем поддерживаемым агентам." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Задать цвет группы" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "Файл SKILL.md слишком большой." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Slash-команды" msgid "Some checks are failing or pending." msgstr "Некоторые проверки не пройдены или ожидают выполнения." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Некоторые компоненты не удалось загрузить" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Некоторые папки провайдеров не удалось просканировать. Успешно загруженные навыки по-прежнему отображаются." @@ -9196,6 +9303,10 @@ msgstr "Конфигурация сервера некорректна." msgid "The server returned an invalid MCP response." msgstr "Сервер вернул некорректный ответ MCP." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "Сервер, который запускает этот плагин, поддерживается сторонними разработчиками, а не сервисом, к которому он подключается. Проверьте исходный код перед включением." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "Отсутствует описание навыка." @@ -9568,6 +9679,14 @@ msgstr "Полупрозрачная боковая панель" msgid "Trending now" msgstr "Сейчас в тренде" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Разбирайте почту Microsoft Outlook и управляйте календарём." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Работайте с PR, задачами, CI и публикацией изменений." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Доверять и подключиться" @@ -9576,6 +9695,10 @@ msgstr "Доверять и подключиться" msgid "Try a different search or project filter." msgstr "Попробуйте другой поиск или фильтр проекта." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Попробовать" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Включите удаленный доступ, чтобы показать код сопряжения." diff --git a/src/renderer/locales/tr/messages.po b/src/renderer/locales/tr/messages.po index 07bc7877f..a056aaf5b 100644 --- a/src/renderer/locales/tr/messages.po +++ b/src/renderer/locales/tr/messages.po @@ -32,8 +32,8 @@ msgstr "(mesaj yok)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# uygulama} other {# uygulama}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# uygulama} other {# uygulama}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# proje} other {# proje}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# sağlayıcı hazır} other {# sağlayıcı hazır}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# sunucu} other {# sunucu}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# sonuç} other {# sonuç}}" msgid "{seconds}s ago" msgstr "{seconds} sn önce" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# sunucu} other {# sunucu}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{providerLabel} kaynağından {serverName}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Onaylandı" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Uygulamalar" @@ -1132,6 +1142,10 @@ msgstr "Kimlik doğrulama gerekli" msgid "Authentication setup cannot be imported" msgstr "Kimlik doğrulama ayarları içe aktarılamaz" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Yazar" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Oluşturduklarım" @@ -1466,6 +1480,10 @@ msgstr "Yerleşik sunucular Poracode tarafından yönetilir. Genel olarak devre msgid "Built-in shortcut — can't be changed" msgstr "Yerleşik kısayol — değiştirilemez" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Desteklenen her ajanla çalışan beceri ve MCP sunucusu paketleri. Poracode, Agent Plugins şartnamesine göre hazırlanmış her paketi yükler." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "<0>{author} tarafından" @@ -2186,6 +2204,16 @@ msgstr "Commit, PR ve inceleme composer'ları" msgid "Commits" msgstr "Commit'ler" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "İletişim" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Topluluk" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Bağlam sıkıştırılıyor" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Yönetilen bir beceriyle çakışıyor" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "OpenCode'un kullanabileceği yapay zeka sağlayıcılarını buradan ba #~ msgstr "Projelerini buradan görüntülemek ve yönetmek için başka bir Poracode masaüstüne ya da arayüzsüz bir sunucuya bağlan. Uç noktasını ve eşleştirme jetonunu o makinede Ayarlar → Uzaktan Erişim'den al." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Bağlı" @@ -2345,6 +2375,10 @@ msgstr "Bağlı" msgid "Connecting to the desktop browser…" msgstr "Masaüstü tarayıcısına bağlanılıyor…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Bağlanıyor..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Sunucu bağlantısını kes" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Keşfet" +#~ msgid "Discover" +#~ msgstr "Keşfet" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Eklentileri keşfet" +#~ msgid "Discover plugins" +#~ msgstr "Eklentileri keşfet" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Efor düzeyleri" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "{0} öğesini etkinleştir" @@ -3641,6 +3678,8 @@ msgstr "Yapılacaklar iskelesini genişlet" msgid "Export MCP servers" msgstr "MCP sunucularını dışa aktar" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Harici" @@ -4090,6 +4129,10 @@ msgstr "{0} için Git durumu" msgid "Git status for {0}: not a Git repository" msgstr "{0} için Git durumu: Git deposu değil" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Ana klasör" msgid "Home scope" msgstr "Ana kapsam" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Ana sayfa" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, takma ad veya kullanıcı@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "Kimlik: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Tanımlayıcı" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Kimlik ve kullanım istatistikleri" @@ -4527,7 +4578,6 @@ msgstr "Windows'a yükleyin" msgid "Install packages" msgstr "Paketleri yükle" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Desteklenen her aracı için Poracode tarafından yönetilen beceri ve MCP destekli uygulama paketlerini yükleyin." @@ -4689,6 +4739,7 @@ msgstr "Aracıların, gömülü sayfa içinde rastgele JavaScript çalıştırma #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Lisans" @@ -4836,6 +4887,7 @@ msgstr "{branch}'dan çekilmeden önce yerel değişikliklerin saklanması gerek msgid "localhost:{0} on desktop" msgstr "localhost:{0} masaüstünde" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Kenar çubuğunu yarı saydam yapın — Windows 11'de sistem bulanıklık materyali, diğerlerinde yarı saydam bir ton." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Yönet" @@ -4913,6 +4966,10 @@ msgstr "Yönet" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Diğer sağlayıcılardan içe aktarılanlar dahil olmak üzere genel ve proje aracı becerilerini yönetin." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Kurulu eklentileri ve sundukları becerileri ve sunucuları yönetin." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Uzak ortamları yönet" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Tamamlandı Olarak İşaretle" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Marketplace" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "MCP sunucusu URL'si" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "MCP sunucuları" @@ -5123,6 +5182,10 @@ msgstr "MCP aktarım türü" msgid "MCP-powered tools contributed by this plugin." msgstr "Bu eklentinin sağladığı MCP destekli araçlar." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Birleştir ve Kaldır" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Henüz aşama yok." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Henüz eklenti yüklenmedi" +#~ msgid "No plugins installed yet" +#~ msgstr "Henüz eklenti yüklenmedi" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Git deposu değil" msgid "Not found" msgstr "Bulunamadı" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Desteklenmiyor" msgid "Notes" msgstr "Notlar" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Burada henüz bir şey yok. Katkı eklemek için bir eklenti kurun." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Henüz hiçbir şey izlenmedi. Kullandıkça burada görünecek." @@ -6408,6 +6476,10 @@ msgstr "Diğer" msgid "Outcome" msgstr "Sonuç" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Aşırı tempo — erken biter" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Eklenti" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Eklenti mağazası görünümleri" +msgid "Plugin folder" +msgstr "Eklenti klasörü" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Eklenti yönetim görünümleri" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Eklenti mağazası görünümleri" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Eklenti ayar görünümleri" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "Genel URL güncellendi." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Yayıncı" +#~ msgid "Publisher" +#~ msgstr "Yayıncı" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Sorun Bildir" msgid "repository" msgstr "depo" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Depo" @@ -7926,6 +8012,10 @@ msgstr "Şube ara..." msgid "Search files" msgstr "Dosyaları arayın" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Kurulu eklentilerde ara" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Marketplace becerilerinde ara" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Eklentilerde ara" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Eklentilerde, becerilerde ve uygulamalarda ara..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Eklentilerde, becerilerde ve uygulamalarda ara..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Eklenti, beceri ve sunucu ara..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Konuları ara" msgid "Search Threads" msgstr "Konuları Ara" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Ara..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Ara…" @@ -8153,6 +8251,10 @@ msgstr "Seçim terminale gönderildi." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Code hesaplarını yapılandırma dizinine göre ayırın ya da bir profili harici bir sağlayıcıya (z.ai, …) yönlendirin. Ortam değişkenlerini, modellerini ve efor düzeyini yapılandırmak için bir profili açın." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Bu eklentinin mcp.json içinde tanımladığı sunucular. Poracode bunları desteklenen her ajana iletir." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Grup rengini ayarla" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md çok büyük." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Eğik çizgi komutları" msgid "Some checks are failing or pending." msgstr "Bazı kontroller başarısız oluyor veya beklemede." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Bazı katkılar yüklenemedi" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Bazı sağlayıcı klasörleri taranamadı. Başarıyla yüklenen beceriler gösterilmeye devam ediyor." @@ -9196,6 +9303,10 @@ msgstr "Sunucu yapılandırması geçersiz." msgid "The server returned an invalid MCP response." msgstr "Sunucu geçersiz bir MCP yanıtı döndürdü." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "Bu eklentinin başlattığı sunucu, bağlandığı hizmet tarafından değil üçüncü bir tarafça sürdürülüyor. Etkinleştirmeden önce kaynak kodu inceleyin." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "Beceri açıklaması eksik." @@ -9568,6 +9679,14 @@ msgstr "Yarı saydam kenar çubuğu" msgid "Trending now" msgstr "Şu anda trend" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Microsoft Outlook postalarını ayıklayın ve takviminizi yönetin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "PR'leri, sorunları, CI'yi ve yayınlama akışlarını yönetin." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Güven ve bağlan" @@ -9576,6 +9695,10 @@ msgstr "Güven ve bağlan" msgid "Try a different search or project filter." msgstr "Farklı bir arama veya proje filtresi deneyin." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Şimdi dene" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Eşleştirme kodunu göstermek için uzaktan erişimi açın." diff --git a/src/renderer/locales/uk/messages.po b/src/renderer/locales/uk/messages.po index d002b2d9d..84e928adf 100644 --- a/src/renderer/locales/uk/messages.po +++ b/src/renderer/locales/uk/messages.po @@ -32,8 +32,8 @@ msgstr "(немає повідомлення)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# застосунок} other {# застосунків}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# застосунок} other {# застосунків}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# проєкт} few {# проєкти} many {# проє msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# провайдер готовий} few {# провайдери готові} many {# провайдерів готові} other {# провайдера готові}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# сервер} other {# серверів}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# результат} few {# результа msgid "{seconds}s ago" msgstr "{seconds}с тому" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# сервер} other {# серверів}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} з {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Схвалено" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Застосунки" @@ -1132,6 +1142,10 @@ msgstr "Потрібна автентифікація" msgid "Authentication setup cannot be imported" msgstr "Налаштування автентифікації неможливо імпортувати" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Автор" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Створені мною" @@ -1466,6 +1480,10 @@ msgstr "Вбудовані сервери керуються Poracode. Їх мо msgid "Built-in shortcut — can't be changed" msgstr "Вбудована комбінація клавіш — не можна змінити" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Набори навичок і MCP-серверів, які працюють із будь-яким підтримуваним агентом. Poracode завантажує будь-який пакет, створений за специфікацією Agent Plugins." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "автор: <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Поля вводу коміту, PR і рецензії" msgid "Commits" msgstr "Коміти" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Комунікації" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Спільнота" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Стиснення контексту" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Конфліктує з керованою навичкою" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Підключайте постачальників AI, які може #~ msgstr "Підключіться до іншого комп'ютера з Poracode або до сервера без інтерфейсу, щоб переглядати проєкти та керувати ними звідси. Адресу й код сполучення отримайте в розділі «Налаштування → Віддалений доступ» на тій машині." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Підключено" @@ -2345,6 +2375,10 @@ msgstr "Підключено" msgid "Connecting to the desktop browser…" msgstr "Підключення до браузера на десктопі…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Підключення..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Відключити сервер" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Огляд" +#~ msgid "Discover" +#~ msgstr "Огляд" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Знайти плагіни" +#~ msgid "Discover plugins" +#~ msgstr "Знайти плагіни" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Рівні зусиль" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Увімкнути {0}" @@ -3641,6 +3678,8 @@ msgstr "Розгорнути панель завдань" msgid "Export MCP servers" msgstr "Експортувати сервери MCP" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Зовнішній" @@ -4090,6 +4129,10 @@ msgstr "Стан Git для {0}" msgid "Git status for {0}: not a Git repository" msgstr "Стан Git для {0}: не репозиторій Git" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Домашня папка" msgid "Home scope" msgstr "Домашня область" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Домашня сторінка" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, псевдонім або користувач@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "ID: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Ідентифікатор" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Особисті дані та статистика використання" @@ -4527,7 +4578,6 @@ msgstr "Встановити у Windows" msgid "Install packages" msgstr "Встановити пакети" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Установіть керовані Poracode пакети навичок і застосунків на базі MCP для кожного підтримуваного агента." @@ -4689,6 +4739,7 @@ msgstr "Дозволяє агентам викликати <0>eval для в #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Ліцензія" @@ -4836,6 +4887,7 @@ msgstr "Перед виконанням pull з {branch} локальні змі msgid "localhost:{0} on desktop" msgstr "localhost:{0} на комп'ютері" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Зробіть бічну панель напівпрозорою — системне розмиття у Windows 11 і напівпрозоре тонування в інших системах." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Керування" @@ -4913,6 +4966,10 @@ msgstr "Керування" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Керуйте глобальними навичками агентів і навичками проєкту, зокрема імпортованими від інших провайдерів." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Керуйте встановленими плагінами та навичками й серверами, які вони додають." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Керування віддаленими середовищами" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Позначити виконаним" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Маркетплейс" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "URL сервера MCP" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "Сервери MCP" @@ -5123,6 +5182,10 @@ msgstr "Тип транспорту MCP" msgid "MCP-powered tools contributed by this plugin." msgstr "Інструменти на базі MCP, надані цим плагіном." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Злити та видалити" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Фаз поки немає." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Плагіни ще не встановлено" +#~ msgid "No plugins installed yet" +#~ msgstr "Плагіни ще не встановлено" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Не репозиторій Git" msgid "Not found" msgstr "Не знайдено" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Не підтримується" msgid "Notes" msgstr "Нотатки" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Тут поки що порожньо. Встановіть плагін, щоб додати складові." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Ще нічого не відстежено. З'явиться тут у міру використання." @@ -6408,6 +6476,10 @@ msgstr "Інші" msgid "Outcome" msgstr "Результат" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Темп перевищено — закінчиться раніше" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Плагін" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Подання магазину плагінів" +msgid "Plugin folder" +msgstr "Тека плагінів" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Режими керування плагінами" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Подання магазину плагінів" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Режими налаштувань плагінів" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "Публічний URL оновлено." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Видавець" +#~ msgid "Publisher" +#~ msgstr "Видавець" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Повідомити про проблему" msgid "repository" msgstr "репозиторій" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Репозиторій" @@ -7926,6 +8012,10 @@ msgstr "Пошук гілок..." msgid "Search files" msgstr "Пошук файлів" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Пошук встановлених плагінів" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Пошук навичок у маркетплейсі" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Пошук плагінів" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Пошук плагінів, навичок і застосунків..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Пошук плагінів, навичок і застосунків..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Пошук плагінів, навичок і серверів..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Пошук тредів" msgid "Search Threads" msgstr "Пошук тредів" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Пошук..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Пошук…" @@ -8153,6 +8251,10 @@ msgstr "Вибір надіслано в термінал." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Розділяйте облікові записи Claude Code за каталогом конфігурації або спрямовуйте профіль на зовнішнього провайдера (z.ai, …). Відкрийте профіль, щоб налаштувати його змінні середовища, моделі та рівень зусиль." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Сервери, які цей плагін оголошує в mcp.json. Poracode передає їх кожному підтримуваному агенту." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Задати колір групи" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "Файл SKILL.md завеликий." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Slash-команди" msgid "Some checks are failing or pending." msgstr "Деякі перевірки не пройдені або очікують виконання." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Не вдалося завантажити деякі складові" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Деякі папки провайдерів не вдалося просканувати. Успішно завантажені навички все одно відображаються." @@ -9196,6 +9303,10 @@ msgstr "Конфігурація сервера некоректна." msgid "The server returned an invalid MCP response." msgstr "Сервер повернув некоректну відповідь MCP." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "Сервер, який запускає цей плагін, підтримується сторонніми розробниками, а не сервісом, до якого він підключається. Перевірте вихідний код перед увімкненням." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "Відсутній опис навички." @@ -9568,6 +9679,14 @@ msgstr "Напівпрозора бічна панель" msgid "Trending now" msgstr "Зараз у тренді" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Розбирайте пошту Microsoft Outlook і керуйте календарем." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Працюйте з PR, задачами, CI та публікацією змін." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Довіряти та підключитися" @@ -9576,6 +9695,10 @@ msgstr "Довіряти та підключитися" msgid "Try a different search or project filter." msgstr "Спробуйте інший пошук або фільтр проєкту." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Спробувати" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Увімкніть віддалений доступ, щоб показати код сполучення." diff --git a/src/renderer/locales/vi/messages.po b/src/renderer/locales/vi/messages.po index 574979a7f..848a73b8b 100644 --- a/src/renderer/locales/vi/messages.po +++ b/src/renderer/locales/vi/messages.po @@ -32,8 +32,8 @@ msgstr "(không có tin nhắn)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# ứng dụng} other {# ứng dụng}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# ứng dụng} other {# ứng dụng}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# dự án} other {# dự án}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {# nhà cung cấp đã sẵn sàng} other {# nhà cung cấp đã sẵn sàng}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# máy chủ} other {# máy chủ}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {# kết quả} other {# kết quả}}" msgid "{seconds}s ago" msgstr "{seconds} giây trước" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# máy chủ} other {# máy chủ}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{serverName} từ {providerLabel}: {disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "Đã được phê duyệt" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "Ứng dụng" @@ -1132,6 +1142,10 @@ msgstr "Yêu cầu xác thực" msgid "Authentication setup cannot be imported" msgstr "Không thể nhập thiết lập xác thực" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "Tác giả" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "Do bạn tạo" @@ -1466,6 +1480,10 @@ msgstr "Các máy chủ tích hợp sẵn do Poracode quản lý. Bạn có th msgid "Built-in shortcut — can't be changed" msgstr "Phím tắt tích hợp sẵn — không thể thay đổi" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "Các gói skill và máy chủ MCP hoạt động với mọi agent được hỗ trợ. Poracode tải mọi gói được xây dựng theo đặc tả Agent Plugins." + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "bởi <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "Trình soạn thảo commit, PR và đánh giá" msgid "Commits" msgstr "Cam kết" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "Giao tiếp" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "Cộng đồng" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "Đang nén ngữ cảnh" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "Xung đột với một skill được quản lý" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "Kết nối các nhà cung cấp AI mà OpenCode có thể dùng và đ #~ msgstr "Kết nối tới một máy Poracode khác hoặc máy chủ không giao diện để duyệt và quản lý các dự án của nó tại đây. Lấy endpoint và mã ghép nối trong Cài đặt → Truy cập từ xa trên máy đó." #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "Đã kết nối" @@ -2345,6 +2375,10 @@ msgstr "Đã kết nối" msgid "Connecting to the desktop browser…" msgstr "Đang kết nối tới trình duyệt trên desktop…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "Đang kết nối..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "Ngắt kết nối máy chủ" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "Khám phá" +#~ msgid "Discover" +#~ msgstr "Khám phá" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "Khám phá plugin" +#~ msgid "Discover plugins" +#~ msgstr "Khám phá plugin" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "Mức nỗ lực" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "Bật {0}" @@ -3641,6 +3678,8 @@ msgstr "Mở rộng dock việc cần làm" msgid "Export MCP servers" msgstr "Xuất máy chủ MCP" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "Bên ngoài" @@ -4090,6 +4129,10 @@ msgstr "Trạng thái Git cho {0}" msgid "Git status for {0}: not a Git repository" msgstr "Trạng thái Git cho {0}: không phải kho lưu trữ Git" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "Thư mục chính" msgid "Home scope" msgstr "phạm vi thư mục chính" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "Trang chủ" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com, bí danh hoặc người_dùng@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "Mã số: {0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "Định danh" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "Danh tính và số liệu sử dụng" @@ -4527,7 +4578,6 @@ msgstr "Cài đặt trên Windows" msgid "Install packages" msgstr "Cài đặt gói" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "Cài đặt các gói Skill và ứng dụng hỗ trợ MCP do Poracode quản lý cho mọi tác nhân được hỗ trợ." @@ -4689,6 +4739,7 @@ msgstr "Cho phép các tác nhân gọi <0>eval để chạy JavaScript tùy #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "Giấy phép" @@ -4836,6 +4887,7 @@ msgstr "Các thay đổi cục bộ cần được lưu tạm trước khi kéo msgid "localhost:{0} on desktop" msgstr "localhost:{0} trên máy tính" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "Làm thanh bên trong suốt — hiệu ứng làm mờ của hệ thống trên Windows 11, lớp màu mờ trong suốt ở nơi khác." #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "Quản lý" @@ -4913,6 +4966,10 @@ msgstr "Quản lý" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "Quản lý skill tác tử toàn cục và theo dự án, bao gồm cả skill được nhập từ nhà cung cấp khác." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "Quản lý các plugin đã cài cùng skill và máy chủ mà chúng cung cấp." + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "Quản lý môi trường từ xa" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "Đánh dấu xong" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "Marketplace" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "URL của máy chủ MCP" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "Máy chủ MCP" @@ -5123,6 +5182,10 @@ msgstr "Loại phương thức truyền MCP" msgid "MCP-powered tools contributed by this plugin." msgstr "Các công cụ hỗ trợ MCP do plugin này cung cấp." +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "Hợp nhất & Xóa" @@ -5857,8 +5920,8 @@ msgid "No phases yet." msgstr "Chưa có giai đoạn nào." #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "Chưa cài đặt plugin nào" +#~ msgid "No plugins installed yet" +#~ msgstr "Chưa cài đặt plugin nào" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6008,6 +6071,7 @@ msgstr "Không phải kho lưu trữ Git" msgid "Not found" msgstr "Không tìm thấy" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6031,6 +6095,10 @@ msgstr "Không được hỗ trợ" msgid "Notes" msgstr "Ghi chú" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "Chưa có gì ở đây. Cài một plugin để thêm đóng góp." + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "Chưa ghi nhận gì. Nội dung sẽ xuất hiện ở đây khi bạn sử dụng." @@ -6408,6 +6476,10 @@ msgstr "Khác" msgid "Outcome" msgstr "Kết quả" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "Vượt nhịp độ — hết sớm" @@ -6659,10 +6731,23 @@ msgid "Plugin" msgstr "Plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "Chế độ xem chợ plugin" +msgid "Plugin folder" +msgstr "Thư mục plugin" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "Chế độ xem quản lý plugin" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "Chế độ xem chợ plugin" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "Chế độ xem cài đặt plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6947,8 +7032,8 @@ msgid "Public URL updated." msgstr "Đã cập nhật URL công khai." #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "Nhà phát hành" +#~ msgid "Publisher" +#~ msgstr "Nhà phát hành" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7432,6 +7517,7 @@ msgstr "Báo cáo sự cố" msgid "repository" msgstr "kho lưu trữ" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "Kho lưu trữ" @@ -7926,6 +8012,10 @@ msgstr "Tìm kiếm nhánh..." msgid "Search files" msgstr "Tìm kiếm tập tin" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "Tìm plugin đã cài" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "Tìm kiếm skill trên marketplace" @@ -7959,8 +8049,12 @@ msgid "Search plugins" msgstr "Tìm kiếm plugin" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "Tìm kiếm plugin, Skill và ứng dụng..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "Tìm kiếm plugin, Skill và ứng dụng..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "Tìm plugin, skill và máy chủ..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8016,6 +8110,10 @@ msgstr "Tìm kiếm luồng" msgid "Search Threads" msgstr "Tìm kiếm luồng" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "Tìm kiếm..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "Tìm kiếm…" @@ -8153,6 +8251,10 @@ msgstr "Đã gửi lựa chọn đến thiết bị đầu cuối." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Tách các tài khoản Claude Code theo thư mục cấu hình, hoặc trỏ một hồ sơ tới nhà cung cấp bên ngoài (z.ai, …). Mở một hồ sơ để cấu hình biến môi trường, mô hình và mức nỗ lực của nó." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "Các máy chủ mà plugin này khai báo trong mcp.json. Poracode chuyển chúng tới mọi agent được hỗ trợ." + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "Đặt màu nhóm" @@ -8457,6 +8559,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md quá lớn." #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8501,6 +8604,10 @@ msgstr "Lệnh gạch chéo" msgid "Some checks are failing or pending." msgstr "Một số kiểm tra không thành công hoặc đang chờ xử lý." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "Không thể tải một số đóng góp" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "Không thể quét một số thư mục của nhà cung cấp. Các skill đã tải thành công vẫn được hiển thị." @@ -9196,6 +9303,10 @@ msgstr "Cấu hình máy chủ không hợp lệ." msgid "The server returned an invalid MCP response." msgstr "Máy chủ trả về phản hồi MCP không hợp lệ." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "Máy chủ mà plugin này khởi chạy do bên thứ ba duy trì, không phải dịch vụ mà nó kết nối tới. Hãy xem mã nguồn trước khi bật." + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "Thiếu phần mô tả skill." @@ -9568,6 +9679,14 @@ msgstr "Thanh bên trong suốt" msgid "Trending now" msgstr "Đang thịnh hành" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "Phân loại thư Microsoft Outlook và quản lý lịch của bạn." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "Xử lý PR, issue, CI và luồng phát hành." + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "Tin cậy và kết nối" @@ -9576,6 +9695,10 @@ msgstr "Tin cậy và kết nối" msgid "Try a different search or project filter." msgstr "Thử cụm tìm kiếm hoặc bộ lọc dự án khác." +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "Dùng thử" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "Bật quyền truy cập từ xa để hiển thị mã ghép nối." diff --git a/src/renderer/locales/zh-CN/messages.po b/src/renderer/locales/zh-CN/messages.po index 45cc99594..e4e1d048b 100644 --- a/src/renderer/locales/zh-CN/messages.po +++ b/src/renderer/locales/zh-CN/messages.po @@ -32,8 +32,8 @@ msgstr "(没有消息)" #. placeholder {0}: plugin.apps.length #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "{0, plural, one {# app} other {# apps}}" -msgstr "{0, plural, one {# 个应用} other {# 个应用}}" +#~ msgid "{0, plural, one {# app} other {# apps}}" +#~ msgstr "{0, plural, one {# 个应用} other {# 个应用}}" #. placeholder {0}: details.changedFiles #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx @@ -65,7 +65,12 @@ msgstr "{0, plural, one {# 个项目} other {# 个项目}}" msgid "{0, plural, one {# provider ready} other {# providers ready}}" msgstr "{0, plural, one {#提供商准备就绪} other {#提供商准备就绪}}" -#. placeholder {0}: plugin.skills.length +#. placeholder {0}: entry.apps.length + entry.mcpServers.length +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "{0, plural, one {# server} other {# servers}}" +#~ msgstr "{0, plural, one {# 个服务器} other {# 个服务器}}" + +#. placeholder {0}: props.entry.skills.length #. placeholder {0}: props.skills.length #. placeholder {0}: skills.length #: src/renderer/components/plugins/PluginMarketplace.tsx @@ -441,6 +446,10 @@ msgstr "{resultCount, plural, one {#结果} other {#结果}}" msgid "{seconds}s ago" msgstr "{seconds} 秒前" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "{serverCount, plural, one {# server} other {# servers}}" +msgstr "{serverCount, plural, one {# 个服务器} other {# 个服务器}}" + #: src/renderer/components/mcp/McpExternalImportModal.tsx msgid "{serverName} from {providerLabel}: {disabledLabel}" msgstr "{providerLabel} 的 {serverName}:{disabledLabel}" @@ -1002,6 +1011,7 @@ msgid "Approved" msgstr "已批准" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx msgid "Apps" msgstr "应用" @@ -1132,6 +1142,10 @@ msgstr "需要身份验证" msgid "Authentication setup cannot be imported" msgstr "无法导入身份验证设置" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Author" +msgstr "作者" + #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Authored" msgstr "我创建的" @@ -1466,6 +1480,10 @@ msgstr "内置服务器由 Poracode 管理。可以全局禁用,但不能编 msgid "Built-in shortcut — can't be changed" msgstr "内置快捷键 — 无法更改" +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Bundles of skills and MCP servers that work across every supported agent. Poracode loads any package built for the Agent Plugins specification." +msgstr "适用于所有受支持代理的技能与 MCP 服务器组合包。Poracode 可加载任何按 Agent Plugins 规范构建的软件包。" + #: src/renderer/views/PrReviewOverlay/parts/PrMetaRow.tsx msgid "by <0>{author}" msgstr "由 <0>{author}" @@ -2186,6 +2204,16 @@ msgstr "提交、PR 和审查 Composer" msgid "Commits" msgstr "提交" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Communication" +msgstr "沟通协作" + +#: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Community" +msgstr "社区" + #: src/renderer/components/thread/ChatPane/parts/items/ContextCompaction.tsx msgid "Compacting context" msgstr "压缩上下文" @@ -2300,6 +2328,7 @@ msgid "Conflicts with a managed skill" msgstr "与托管技能冲突" #: src/mobile/setupEmptyState.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "Connect" @@ -2338,6 +2367,7 @@ msgstr "在此连接 OpenCode 可使用的 AI 提供商,并退出登录。" #~ msgstr "连接到另一台 Poracode 桌面端或无界面服务器,即可在此浏览和管理其项目。请在对方设备的“设置 → 远程访问”中获取其端点和配对令牌。" #: src/renderer/components/mcp/McpServersManager.tsx +#: src/renderer/components/plugins/PluginDetail.tsx msgid "Connected" msgstr "已连接" @@ -2345,6 +2375,10 @@ msgstr "已连接" msgid "Connecting to the desktop browser…" msgstr "正在连接桌面浏览器…" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Connecting..." +msgstr "连接中..." + #: src/mobile/views/DesktopsView.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteServersSettings.tsx msgid "Connecting…" @@ -3194,12 +3228,12 @@ msgid "Disconnect server" msgstr "断开服务器连接" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover" -msgstr "发现" +#~ msgid "Discover" +#~ msgstr "发现" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Discover plugins" -msgstr "发现插件" +#~ msgid "Discover plugins" +#~ msgstr "发现插件" #: src/renderer/components/thread/AgentDiscoveryScreen.tsx msgid "Discovering coding agents…" @@ -3391,7 +3425,10 @@ msgstr "思考级别" msgid "Electron" msgstr "Electron" +#. placeholder {0}: entry.name +#. placeholder {0}: row.contribution.name #. placeholder {0}: skill.name +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillsManager.tsx msgid "Enable {0}" msgstr "启用 {0}" @@ -3641,6 +3678,8 @@ msgstr "展开待办事项停靠栏" msgid "Export MCP servers" msgstr "导出 MCP 服务器" +#: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "External" msgstr "外部" @@ -4090,6 +4129,10 @@ msgstr "{0}的 Git 状态" msgid "Git status for {0}: not a Git repository" msgstr "{0}的 Git 状态:不是 Git 存储库" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "GitHub" +msgstr "GitHub" + #: src/renderer/components/providers/copilot/manifest.ts msgid "GitHub Copilot" msgstr "GitHub Copilot" @@ -4297,6 +4340,10 @@ msgstr "主目录" msgid "Home scope" msgstr "主页范围" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Homepage" +msgstr "主页" + #: src/renderer/views/SettingsOverlay/parts/SshConnectionForm.tsx msgid "host.com, alias, or user@host.com" msgstr "host.com、别名或 user@host.com" @@ -4318,6 +4365,10 @@ msgstr "https://example.com/mcp" msgid "ID: {0}" msgstr "编号:{0}" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Identifier" +msgstr "标识符" + #: src/mobile/settingsSections.ts msgid "Identity and usage stats" msgstr "身份和使用统计" @@ -4527,7 +4578,6 @@ msgstr "在 Windows 上安装" msgid "Install packages" msgstr "安装包" -#: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts msgid "Install Poracode-managed bundles of skills and MCP-powered apps for every supported agent." msgstr "为每个受支持的代理安装由 Poracode 管理的技能和 MCP 驱动应用套件。" @@ -4689,6 +4739,7 @@ msgstr "让代理调用 <0>eval 在嵌入页面内运行任意 JavaScript。 #. About page label: software license row #. Link to the license file +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AboutSettings.tsx msgid "License" msgstr "许可证" @@ -4836,6 +4887,7 @@ msgstr "从 {branch} 拉取之前需要暂存(stash)本地更改" msgid "localhost:{0} on desktop" msgstr "桌面上的 localhost:{0}" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/MainView/parts/CreateProject/CloneProjectModal.tsx #: src/renderer/views/MainView/parts/CreateProject/CreateProjectModal.tsx msgid "Location" @@ -4906,6 +4958,7 @@ msgid "Make the sidebar translucent — the system blur material on Windows 11, msgstr "将侧边栏设为半透明——在Windows 11上使用系统模糊材质,在其他平台上使用半透明着色。" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Manage" msgstr "管理" @@ -4913,6 +4966,10 @@ msgstr "管理" #~ msgid "Manage global and project agent skills, including skills imported from other providers." #~ msgstr "管理全局和项目智能体技能,包括从其他提供商导入的技能。" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Manage installed plugins and the skills and servers they contribute." +msgstr "管理已安装的插件及其提供的技能和服务器。" + #: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx msgid "Manage remote environments" msgstr "管理远程环境" @@ -5008,6 +5065,7 @@ msgid "Mark Done" msgstr "标记完成" #: src/renderer/components/skills/SkillsManager.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Marketplace" msgstr "市场" @@ -5099,6 +5157,7 @@ msgid "MCP server URL" msgstr "MCP 服务器 URL" #: src/renderer/components/composer/ComposerAddMenu.tsx +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/ProfileSettings.tsx msgid "MCP servers" msgstr "MCP服务器" @@ -5123,6 +5182,10 @@ msgstr "MCP 传输类型" msgid "MCP-powered tools contributed by this plugin." msgstr "此插件提供的 MCP 驱动工具。" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "MCPs" +msgstr "MCP" + #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Merge & Remove" msgstr "合并和删除" @@ -5856,8 +5919,8 @@ msgid "No phases yet." msgstr "还没有阶段。" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "No plugins installed yet" -msgstr "尚未安装插件" +#~ msgid "No plugins installed yet" +#~ msgstr "尚未安装插件" #: src/renderer/components/plugins/PluginMarketplace.tsx msgid "No plugins match your search." @@ -6007,6 +6070,7 @@ msgstr "不是 Git 存储库" msgid "Not found" msgstr "未找到" +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/AgentEnvironmentRow.tsx #: src/renderer/views/SettingsOverlay/parts/SingleAgentSettings/parts/HookPluginSettings.tsx msgid "Not installed" @@ -6030,6 +6094,10 @@ msgstr "不支持" msgid "Notes" msgstr "注释" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Nothing here yet. Install a plugin to add contributions." +msgstr "这里还没有内容。安装插件即可添加贡献项。" + #: src/renderer/views/ProfileOverlay/parts/PluginUsage.tsx msgid "Nothing tracked yet. It'll appear here as you use it." msgstr "暂无记录。使用过程中将在此显示。" @@ -6407,6 +6475,10 @@ msgstr "其他" msgid "Outcome" msgstr "结果" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Outlook" +msgstr "Outlook" + #: src/renderer/components/providers/usageFormat.ts msgid "Over pace — runs out early" msgstr "节奏过快 — 提前用完" @@ -6658,10 +6730,23 @@ msgid "Plugin" msgstr "插件" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Plugin marketplace views" -msgstr "插件市场视图" +msgid "Plugin folder" +msgstr "插件文件夹" + +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Plugin management views" +msgstr "插件管理视图" + +#: src/renderer/components/plugins/PluginMarketplace.tsx +#~ msgid "Plugin marketplace views" +#~ msgstr "插件市场视图" + +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Plugin settings views" +msgstr "插件设置视图" #: src/renderer/components/plugins/PluginMarketplace.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx msgid "Plugins" @@ -6946,8 +7031,8 @@ msgid "Public URL updated." msgstr "公共 URL 已更新。" #: src/renderer/components/plugins/PluginDetail.tsx -msgid "Publisher" -msgstr "发布者" +#~ msgid "Publisher" +#~ msgstr "发布者" #: src/renderer/views/MainView/parts/Sidebar/parts/useWorktreeActions.ts msgid "Pull" @@ -7431,6 +7516,7 @@ msgstr "报告问题" msgid "repository" msgstr "仓库" +#: src/renderer/components/plugins/PluginDetail.tsx #: src/renderer/views/SettingsOverlay/parts/AcpRegistrySettings.tsx msgid "Repository" msgstr "仓库" @@ -7925,6 +8011,10 @@ msgstr "搜索分支..." msgid "Search files" msgstr "搜索文件" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search installed plugins" +msgstr "搜索已安装的插件" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx msgid "Search marketplace skills" msgstr "搜索市场技能" @@ -7958,8 +8048,12 @@ msgid "Search plugins" msgstr "搜索插件" #: src/renderer/components/plugins/PluginMarketplace.tsx -msgid "Search plugins, skills, and apps..." -msgstr "搜索插件、技能和应用..." +#~ msgid "Search plugins, skills, and apps..." +#~ msgstr "搜索插件、技能和应用..." + +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Search plugins, skills, and servers..." +msgstr "搜索插件、技能和服务器..." #: src/renderer/views/PullRequestsView/PullRequestsView.tsx msgid "Search pull requests" @@ -8015,6 +8109,10 @@ msgstr "搜索线程" msgid "Search Threads" msgstr "搜索线程" +#: src/renderer/components/plugins/PluginsManager.tsx +msgid "Search..." +msgstr "搜索..." + #: src/renderer/views/ThreadSearchOverlay/ThreadSearchOverlay.tsx msgid "Search…" msgstr "搜索..." @@ -8152,6 +8250,10 @@ msgstr "将选择发送到终端。" msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "按配置目录区分Claude Code账户,或将配置指向外部服务商(z.ai,…)。打开某个配置以设置其环境变量、模型和思考级别。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." +msgstr "此插件在 mcp.json 中声明的服务器。Poracode 会将它们传递给所有受支持的代理。" + #: src/renderer/views/MainView/parts/RightPanel/parts/BrowserPanel/parts/BrowserTabGroupMenu.tsx msgid "Set group color" msgstr "设置分组颜色" @@ -8456,6 +8558,7 @@ msgid "SKILL.md is too large." msgstr "SKILL.md 过大。" #: src/renderer/components/plugins/PluginDetail.tsx +#: src/renderer/components/plugins/PluginsManager.tsx #: src/renderer/components/skills/SkillMarketplaceModal.tsx #: src/renderer/components/thread/ThreadCommandPanel.tsx #: src/renderer/views/ProjectSettingsOverlay/parts/SettingsSidebar.tsx @@ -8500,6 +8603,10 @@ msgstr "斜杠命令" msgid "Some checks are failing or pending." msgstr "一些检查失败或待处理。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Some contributions could not be loaded" +msgstr "部分贡献项无法加载" + #: src/renderer/components/skills/SkillsManager.tsx msgid "Some provider folders couldn't be scanned. Successfully loaded skills are still shown." msgstr "无法扫描某些提供商文件夹。已成功加载的技能仍会显示。" @@ -9195,6 +9302,10 @@ msgstr "服务器配置无效。" msgid "The server returned an invalid MCP response." msgstr "服务器返回了无效的 MCP 响应。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "The server this plugin launches is maintained by a third party, not by the service it connects to. Review the source before enabling it." +msgstr "此插件启动的服务器由第三方维护,而非其所连接的服务方。启用前请先查看源代码。" + #: src/renderer/components/skills/SkillsManager.tsx msgid "The skill description is missing." msgstr "缺少技能描述。" @@ -9567,6 +9678,14 @@ msgstr "半透明侧边栏" msgid "Trending now" msgstr "当前热门" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage Microsoft Outlook mail and manage your calendar." +msgstr "整理 Microsoft Outlook 邮件并管理日历。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Triage PRs, issues, CI, and publish flows." +msgstr "处理 PR、议题、CI 和发布流程。" + #: src/mobile/views/DesktopsView.tsx msgid "Trust and connect" msgstr "信任并连接" @@ -9575,6 +9694,10 @@ msgstr "信任并连接" msgid "Try a different search or project filter." msgstr "请尝试其他搜索或项目筛选器。" +#: src/renderer/components/plugins/PluginDetail.tsx +msgid "Try now" +msgstr "立即试用" + #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Turn on remote access to show a pairing code." msgstr "开启远程访问以显示配对码。" diff --git a/src/renderer/state/pluginsStore.ts b/src/renderer/state/pluginsStore.ts new file mode 100644 index 000000000..a8460f33c --- /dev/null +++ b/src/renderer/state/pluginsStore.ts @@ -0,0 +1,51 @@ +import { create } from "zustand"; +import type { LoadedPlugin } from "@/shared/contracts"; +import { readBridge } from "@/renderer/bridge"; + +/** + * Agent Plugins packages loaded by the supervisor. + * + * Packages live on disk, so unlike the old hardcoded catalog this list is read + * over IPC and can change while the app is running — a user can drop a package + * into the plugin folder and refresh. + */ + +interface PluginsState { + plugins: LoadedPlugin[]; + userPluginsDir: string; + loaded: boolean; + loading: boolean; + error: unknown; + load: (rescan?: boolean) => Promise; +} + +export const usePlugins = create()((set, get) => ({ + plugins: [], + userPluginsDir: "", + loaded: false, + loading: false, + error: undefined, + load: async (rescan = false) => { + if (get().loading) return; + set({ loading: true, error: undefined }); + try { + const bridge = readBridge(); + const result = rescan ? await bridge.refreshPlugins() : await bridge.listPlugins(); + set({ + plugins: result.plugins, + userPluginsDir: result.userPluginsDir, + loaded: true, + loading: false, + }); + } catch (error) { + set({ error, loading: false, loaded: true }); + } + }, +})); + +export function findPlugin( + plugins: readonly LoadedPlugin[], + name: string, +): LoadedPlugin | undefined { + return plugins.find((plugin) => plugin.name === name); +} diff --git a/src/renderer/state/sharedSettingsStore.test.ts b/src/renderer/state/sharedSettingsStore.test.ts index 9e9793c84..523900822 100644 --- a/src/renderer/state/sharedSettingsStore.test.ts +++ b/src/renderer/state/sharedSettingsStore.test.ts @@ -1,9 +1,11 @@ import { beforeEach, describe, expect, it } from "vitest"; +import { pluginFixture, seedBuiltInPlugins } from "@/renderer/testUtils/plugins"; import { useSharedSettings } from "./sharedSettingsStore"; describe("sharedSettingsStore", () => { beforeEach(() => { localStorage.clear(); + seedBuiltInPlugins(); useSharedSettings.setState({ themeMode: "dark", staleThreadUnloadMinutes: 20, @@ -95,27 +97,31 @@ describe("sharedSettingsStore", () => { const persistedPlugins = () => JSON.parse(localStorage.getItem("poracode-shared-settings") ?? "null").installedPlugins; - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); expect(persistedPlugins()).toEqual({ "browser-tools": { version: "1.0.0", enabled: true, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }); - useSharedSettings.getState().setPluginEnabled("browser-tools", false); + useSharedSettings.getState().setPluginEnabled(pluginFixture("browser-tools"), false); useSharedSettings.getState().setPluginSkillEnabled("browser-tools", "browser-control", false); - useSharedSettings.getState().setPluginAppEnabled("browser-tools", "browser", false); + useSharedSettings + .getState() + .setPluginAppEnabled(pluginFixture("browser-tools"), "browser", false); expect(persistedPlugins()["browser-tools"]).toEqual({ version: "1.0.0", enabled: false, disabledSkillIds: ["browser-control"], disabledAppIds: ["browser"], + disabledMcpServerNames: [], }); - useSharedSettings.getState().uninstallPlugin("browser-tools"); + useSharedSettings.getState().uninstallPlugin(pluginFixture("browser-tools")); expect(useSharedSettings.getState().installedPlugins).toEqual({}); expect(persistedPlugins()).toEqual({}); }); @@ -125,7 +131,7 @@ describe("sharedSettingsStore", () => { enabledMcpServers: { browser: true, subagents: true }, }); - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); expect(useSharedSettings.getState().enabledMcpServers).toEqual({ subagents: true }); expect( @@ -134,34 +140,36 @@ describe("sharedSettingsStore", () => { }); it("clears the legacy MCP setting when a plugin is disabled", () => { - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); useSharedSettings.setState({ enabledMcpServers: { browser: true, subagents: true }, }); - useSharedSettings.getState().setPluginEnabled("browser-tools", false); + useSharedSettings.getState().setPluginEnabled(pluginFixture("browser-tools"), false); expect(useSharedSettings.getState().enabledMcpServers).toEqual({ subagents: true }); }); it("clears the legacy MCP setting when a plugin app is disabled", () => { - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); useSharedSettings.setState({ enabledMcpServers: { browser: true, subagents: true }, }); - useSharedSettings.getState().setPluginAppEnabled("browser-tools", "browser", false); + useSharedSettings + .getState() + .setPluginAppEnabled(pluginFixture("browser-tools"), "browser", false); expect(useSharedSettings.getState().enabledMcpServers).toEqual({ subagents: true }); }); it("clears the legacy MCP setting when a plugin is uninstalled", () => { - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); useSharedSettings.setState({ enabledMcpServers: { browser: true, subagents: true }, }); - useSharedSettings.getState().uninstallPlugin("browser-tools"); + useSharedSettings.getState().uninstallPlugin(pluginFixture("browser-tools")); expect(useSharedSettings.getState().enabledMcpServers).toEqual({ subagents: true }); }); diff --git a/src/renderer/state/sharedSettingsStore.ts b/src/renderer/state/sharedSettingsStore.ts index 5f3d70c53..83a55fb47 100644 --- a/src/renderer/state/sharedSettingsStore.ts +++ b/src/renderer/state/sharedSettingsStore.ts @@ -24,14 +24,15 @@ import type { WorktreeStorageMode, BuiltInMcpServerId, McpServer, + LoadedPlugin, } from "@/shared/contracts"; import { - getBuiltInPluginManifest, - installBuiltInPlugin, + installPlugin as addInstalledPlugin, setInstalledPluginEnabled as updateInstalledPluginEnabled, setPluginAppEnabled as updatePluginAppEnabled, + setPluginMcpServerEnabled as updatePluginMcpServerEnabled, setPluginSkillEnabled as updatePluginSkillEnabled, - uninstallBuiltInPlugin, + uninstallPlugin as removeInstalledPlugin, } from "@/shared/plugins/catalog"; const STORAGE_KEY = "poracode-shared-settings"; @@ -104,11 +105,12 @@ interface SharedSettingsState extends SharedSettings { setMcpServers: (servers: McpServer[]) => void; setBuiltInMcpServerDisabled: (id: BuiltInMcpServerId, disabled: boolean) => void; setBuiltInMcpToolEnabled: (id: BuiltInMcpServerId, tool: string, enabled: boolean) => void; - installPlugin: (pluginId: string) => void; - uninstallPlugin: (pluginId: string) => void; - setPluginEnabled: (pluginId: string, enabled: boolean) => void; - setPluginSkillEnabled: (pluginId: string, skillId: string, enabled: boolean) => void; - setPluginAppEnabled: (pluginId: string, appId: string, enabled: boolean) => void; + installPlugin: (plugin: LoadedPlugin) => void; + uninstallPlugin: (plugin: LoadedPlugin) => void; + setPluginEnabled: (plugin: LoadedPlugin, enabled: boolean) => void; + setPluginSkillEnabled: (pluginName: string, folder: string, enabled: boolean) => void; + setPluginAppEnabled: (plugin: LoadedPlugin, appId: string, enabled: boolean) => void; + setPluginMcpServerEnabled: (pluginName: string, serverName: string, enabled: boolean) => void; setBrowserSetting: ( key: K, value: SharedSettings["browser"][K], @@ -229,16 +231,18 @@ function providerDraftConfigEqual( const initialSettings = loadFallbackSettings(); +/** + * A plugin now owns its built-in servers, so any pre-plugin per-server toggle + * for them is stale and would otherwise fight the plugin's own state. + */ function clearPluginLegacyMcpSettings( enabledMcpServers: SharedSettings["enabledMcpServers"], - pluginId: string, + plugin: LoadedPlugin, appId?: string, ): SharedSettings["enabledMcpServers"] { - const manifest = getBuiltInPluginManifest(pluginId); - const serverIds = - manifest?.apps - .filter((app) => appId === undefined || app.id === appId) - .map((app) => app.builtInMcpServerId) ?? []; + const serverIds = plugin.poracode.apps + .filter((app) => appId === undefined || app.id === appId) + .map((app) => app.builtInMcpServerId); if (!serverIds.some((serverId) => serverId in enabledMcpServers)) return enabledMcpServers; const next = { ...enabledMcpServers }; @@ -527,10 +531,10 @@ export const useSharedSettings = create()((set, get) => ({ set({ disabledBuiltInMcpTools: next }); persistSettings(selectSharedSettings(get())); }, - installPlugin: (pluginId) => { + installPlugin: (plugin) => { const current = get(); - const installedPlugins = installBuiltInPlugin(current.installedPlugins, pluginId); - const enabledMcpServers = clearPluginLegacyMcpSettings(current.enabledMcpServers, pluginId); + const installedPlugins = addInstalledPlugin(current.installedPlugins, plugin); + const enabledMcpServers = clearPluginLegacyMcpSettings(current.enabledMcpServers, plugin); if ( installedPlugins === current.installedPlugins && enabledMcpServers === current.enabledMcpServers @@ -540,10 +544,10 @@ export const useSharedSettings = create()((set, get) => ({ set({ installedPlugins, enabledMcpServers }); persistSettings(selectSharedSettings(get())); }, - uninstallPlugin: (pluginId) => { + uninstallPlugin: (plugin) => { const current = get(); - const installedPlugins = uninstallBuiltInPlugin(current.installedPlugins, pluginId); - const enabledMcpServers = clearPluginLegacyMcpSettings(current.enabledMcpServers, pluginId); + const installedPlugins = removeInstalledPlugin(current.installedPlugins, plugin.name); + const enabledMcpServers = clearPluginLegacyMcpSettings(current.enabledMcpServers, plugin); if ( installedPlugins === current.installedPlugins && enabledMcpServers === current.enabledMcpServers @@ -553,16 +557,16 @@ export const useSharedSettings = create()((set, get) => ({ set({ installedPlugins, enabledMcpServers }); persistSettings(selectSharedSettings(get())); }, - setPluginEnabled: (pluginId, enabled) => { + setPluginEnabled: (plugin, enabled) => { const current = get(); const installedPlugins = updateInstalledPluginEnabled( current.installedPlugins, - pluginId, + plugin.name, enabled, ); const enabledMcpServers = enabled ? current.enabledMcpServers - : clearPluginLegacyMcpSettings(current.enabledMcpServers, pluginId); + : clearPluginLegacyMcpSettings(current.enabledMcpServers, plugin); if ( installedPlugins === current.installedPlugins && enabledMcpServers === current.enabledMcpServers @@ -572,28 +576,28 @@ export const useSharedSettings = create()((set, get) => ({ set({ installedPlugins, enabledMcpServers }); persistSettings(selectSharedSettings(get())); }, - setPluginSkillEnabled: (pluginId, skillId, enabled) => { + setPluginSkillEnabled: (pluginName, folder, enabled) => { const installedPlugins = updatePluginSkillEnabled( get().installedPlugins, - pluginId, - skillId, + pluginName, + folder, enabled, ); if (installedPlugins === get().installedPlugins) return; set({ installedPlugins }); persistSettings(selectSharedSettings(get())); }, - setPluginAppEnabled: (pluginId, appId, enabled) => { + setPluginAppEnabled: (plugin, appId, enabled) => { const current = get(); const installedPlugins = updatePluginAppEnabled( current.installedPlugins, - pluginId, + plugin.name, appId, enabled, ); const enabledMcpServers = enabled ? current.enabledMcpServers - : clearPluginLegacyMcpSettings(current.enabledMcpServers, pluginId, appId); + : clearPluginLegacyMcpSettings(current.enabledMcpServers, plugin, appId); if ( installedPlugins === current.installedPlugins && enabledMcpServers === current.enabledMcpServers @@ -603,6 +607,17 @@ export const useSharedSettings = create()((set, get) => ({ set({ installedPlugins, enabledMcpServers }); persistSettings(selectSharedSettings(get())); }, + setPluginMcpServerEnabled: (pluginName, serverName, enabled) => { + const installedPlugins = updatePluginMcpServerEnabled( + get().installedPlugins, + pluginName, + serverName, + enabled, + ); + if (installedPlugins === get().installedPlugins) return; + set({ installedPlugins }); + persistSettings(selectSharedSettings(get())); + }, setBrowserSetting: (key, value) => { const current = get().browser; if (current[key] === value) return; diff --git a/src/renderer/testUtils/plugins.ts b/src/renderer/testUtils/plugins.ts new file mode 100644 index 000000000..90a0a414f --- /dev/null +++ b/src/renderer/testUtils/plugins.ts @@ -0,0 +1,67 @@ +import type { LoadedPlugin } from "@/shared/contracts"; +import { parsePluginManifest, parsePoracodeExtension } from "@/shared/plugins/spec"; +import { usePlugins } from "@/renderer/state/pluginsStore"; +import browserTools from "../../../resources/plugins/browser-tools/plugin.json"; +import chromeTools from "../../../resources/plugins/chrome-tools/plugin.json"; +import computerUse from "../../../resources/plugins/computer-use/plugin.json"; +import subagentDelegation from "../../../resources/plugins/subagent-delegation/plugin.json"; + +/** + * Seeds the renderer plugin store from the real shipped manifests. + * + * The manifest JSON is imported directly rather than loaded through the + * supervisor's `PluginLoader` — the renderer must not reach into supervisor + * code, and only the manifest matters here. Skill folders are taken from the + * manifest's own extension block, so a package that changes its contributions + * changes these fixtures with it. Loader behavior itself is covered by + * `src/supervisor/plugins/conformance.test.ts`. + */ + +const SHIPPED_MANIFESTS = [browserTools, chromeTools, computerUse, subagentDelegation]; + +function toLoadedPlugin(raw: unknown): LoadedPlugin { + const parsed = parsePluginManifest(raw); + if (!parsed.manifest) { + throw new Error( + `shipped plugin manifest is invalid: ${parsed.diagnostics.map((d) => d.message).join("; ")}`, + ); + } + const manifest = parsed.manifest; + const { extension } = parsePoracodeExtension(manifest); + const root = `/resources/plugins/${manifest.name}`; + return { + name: manifest.name, + source: "bundled", + root, + manifest, + poracode: extension, + skills: Object.keys(extension.skills).map((folder) => ({ + folder, + path: `${root}/skills/${folder}`, + })), + mcpServers: [], + diagnostics: [], + }; +} + +export function loadBuiltInPluginFixtures(): LoadedPlugin[] { + return SHIPPED_MANIFESTS.map(toLoadedPlugin); +} + +export function seedBuiltInPlugins(): LoadedPlugin[] { + const plugins = loadBuiltInPluginFixtures(); + usePlugins.setState({ + plugins, + userPluginsDir: "/home/test/.poracode/plugins", + loaded: true, + loading: false, + error: undefined, + }); + return plugins; +} + +export function pluginFixture(name: string): LoadedPlugin { + const plugin = usePlugins.getState().plugins.find((candidate) => candidate.name === name); + if (!plugin) throw new Error(`plugin fixture '${name}' is not seeded`); + return plugin; +} diff --git a/src/renderer/views/SettingsOverlay/parts/McpServersSettings.tsx b/src/renderer/views/SettingsOverlay/parts/McpServersSettings.tsx index 47b70f955..3ac6b3072 100644 --- a/src/renderer/views/SettingsOverlay/parts/McpServersSettings.tsx +++ b/src/renderer/views/SettingsOverlay/parts/McpServersSettings.tsx @@ -8,12 +8,14 @@ import { isHomeProject } from "@/shared/homeScope"; import { SettingsPage } from "./SettingsForm"; import { SubagentRoutingSection } from "./SubagentRoutingSection"; import { useLocalizedPluginCatalog } from "@/renderer/components/plugins/pluginCopy"; +import { usePlugins } from "@/renderer/state/pluginsStore"; import { getInstalledPluginForMcpServer } from "@/shared/plugins/catalog"; import { BUILT_IN_MCP_SERVER_IDS } from "@/shared/contracts"; export function McpServersSettings() { const { t } = useLingui(); const localizedPlugins = useLocalizedPluginCatalog(); + const plugins = usePlugins((state) => state.plugins); const servers = useSharedSettings((state) => state.mcpServers); const disabledBuiltIns = useSharedSettings((state) => state.disabledBuiltInMcpServers); const disabledBuiltInTools = useSharedSettings((state) => state.disabledBuiltInMcpTools); @@ -39,11 +41,12 @@ export function McpServersSettings() { })); const managedBuiltIns = Object.fromEntries( BUILT_IN_MCP_SERVER_IDS.flatMap((serverId) => { - const manifest = getInstalledPluginForMcpServer(installedPlugins, serverId); - if (!manifest) return []; + const owner = getInstalledPluginForMcpServer(plugins, installedPlugins, serverId); + if (!owner) return []; const label = - localizedPlugins.find((plugin) => plugin.manifest.id === manifest.id)?.name ?? - manifest.name; + localizedPlugins.find((entry) => entry.plugin.name === owner.name)?.name ?? + owner.poracode.title ?? + owner.name; return [[serverId, label]]; }), ); diff --git a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx index cb0ebd1c8..684d2fd0a 100644 --- a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx +++ b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx @@ -1,7 +1,8 @@ -import { fireEvent, screen } from "@testing-library/react"; +import { fireEvent, screen, within } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithI18n as render } from "@/renderer/testUtils/i18n"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { pluginFixture, seedBuiltInPlugins } from "@/renderer/testUtils/plugins"; import { PluginsSettings } from "./PluginsSettings"; vi.mock("@/renderer/bridge", () => ({ @@ -11,41 +12,75 @@ vi.mock("@/renderer/bridge", () => ({ describe("PluginsSettings", () => { beforeEach(() => { localStorage.clear(); + seedBuiltInPlugins(); useSharedSettings.setState({ installedPlugins: {} }); }); it("moves focus into plugin detail and restores it to the marketplace card", () => { - useSharedSettings.getState().installPlugin("browser-tools"); + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); render(); - fireEvent.click(screen.getByRole("tab", { name: /^Installed/u })); fireEvent.change(screen.getByRole("textbox", { name: "Search plugins" }), { - target: { value: "browser" }, + target: { value: "browser tools" }, }); - const pluginButton = screen.getByRole("button", { name: "Browser Tools" }); - fireEvent.click(pluginButton); + const card = screen.getByText("Browser Tools").closest("[class*='min-h-40']")!; + fireEvent.click(within(card).getByRole("button", { name: "Browser Tools" })); expect(screen.getByRole("button", { name: "Back to plugins" })).toHaveFocus(); fireEvent.click(screen.getByRole("button", { name: "Back to plugins" })); - expect(screen.getByRole("button", { name: "Browser Tools" })).toHaveFocus(); - expect(screen.getByRole("tab", { name: /^Installed/u })).toHaveAttribute( - "aria-selected", - "true", - ); - expect(screen.getByRole("textbox", { name: "Search plugins" })).toHaveValue("browser"); + expect( + within( + screen.getByText("Browser Tools").closest("[class*='min-h-40']")!, + ).getByRole("button", { name: "Browser Tools" }), + ).toHaveFocus(); + expect(screen.getByRole("textbox", { name: "Search plugins" })).toHaveValue("browser tools"); }); - it("restores focus to the active tab when the installed card was removed", () => { - useSharedSettings.getState().installPlugin("browser-tools"); + it("keeps focus on the card after uninstalling from the detail page", () => { + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); render(); - const installedTab = screen.getByRole("tab", { name: /^Installed/u }); - fireEvent.click(installedTab); - fireEvent.click(screen.getByRole("button", { name: "Browser Tools" })); + const card = screen.getByText("Browser Tools").closest("[class*='min-h-40']")!; + fireEvent.click(within(card).getByRole("button", { name: "Browser Tools" })); fireEvent.click(screen.getByRole("button", { name: "Uninstall" })); fireEvent.click(screen.getByRole("button", { name: "Back to plugins" })); - expect(installedTab).toHaveFocus(); + // The card stays in the marketplace after uninstalling; only its action flips. + const restored = screen.getByText("Browser Tools").closest("[class*='min-h-40']")!; + expect(within(restored).getByRole("button", { name: "Browser Tools" })).toHaveFocus(); + expect(within(restored).getByRole("button", { name: "Browser Tools Install" })).toBeVisible(); + }); + + it("switches to the manage tab and lists contributions by type", () => { + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); + render(); + + fireEvent.click(screen.getByRole("tab", { name: "Manage" })); + + expect(screen.getByRole("tab", { name: /^Plugins/u })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("switch", { name: "Enable Browser Tools" })).toBeChecked(); + + fireEvent.click(screen.getByRole("tab", { name: /^Skills/u })); + expect(screen.getByRole("switch", { name: "Enable Browser Control" })).toBeChecked(); + + fireEvent.click(screen.getByRole("switch", { name: "Enable Browser Control" })); + expect( + useSharedSettings.getState().installedPlugins["browser-tools"]?.disabledSkillIds, + ).toEqual(["browser-control"]); + + fireEvent.click(screen.getByRole("tab", { name: /^Apps/u })); + expect(screen.getByRole("switch", { name: "Enable Browser" })).toBeChecked(); + }); + + it("reports an empty contribution list when nothing is installed", () => { + render(); + + fireEvent.click(screen.getByRole("tab", { name: "Manage" })); + fireEvent.click(screen.getByRole("tab", { name: /^Skills/u })); + + expect( + screen.getByText("Nothing here yet. Install a plugin to add contributions."), + ).toBeInTheDocument(); }); }); diff --git a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx index c75643f06..c1141e996 100644 --- a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +++ b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx @@ -1,16 +1,31 @@ +import { useLingui } from "@lingui/react/macro"; import { useEffect, useRef, useState } from "react"; +import { LightballTabs } from "@/renderer/components/common"; import { PluginDetail } from "@/renderer/components/plugins/PluginDetail"; import { PluginMarketplace } from "@/renderer/components/plugins/PluginMarketplace"; +import { PluginsManager } from "@/renderer/components/plugins/PluginsManager"; import { useLocalizedPluginCatalog } from "@/renderer/components/plugins/pluginCopy"; +import { usePlugins } from "@/renderer/state/pluginsStore"; import { readBridge } from "@/renderer/bridge"; +type PluginsTab = "marketplace" | "manage"; + export function PluginsSettings() { + const { t } = useLingui(); const plugins = useLocalizedPluginCatalog(); + const loadPlugins = usePlugins((state) => state.load); + const [tab, setTab] = useState("marketplace"); const [selectedPluginId, setSelectedPluginId] = useState(); const returnFocusPluginId = useRef(undefined); - const selectedPlugin = plugins.find((plugin) => plugin.manifest.id === selectedPluginId); + const selectedPlugin = plugins.find((entry) => entry.plugin.name === selectedPluginId); const hostPlatform = readBridge().platform; + // Packages live on disk and can be added while the app runs, so rescan every + // time the marketplace opens rather than trusting the first load. + useEffect(() => { + void loadPlugins(true); + }, [loadPlugins]); + useEffect(() => { const pluginId = returnFocusPluginId.current; if (selectedPluginId !== undefined || pluginId === undefined) return; @@ -31,10 +46,27 @@ export function PluginsSettings() { setSelectedPluginId(pluginId); }; + const tabs = [ + { id: "marketplace" as const, label: t`Marketplace` }, + { id: "manage" as const, label: t`Manage` }, + ]; + return (
{selectedPlugin ? ( /../../resources/plugins. + return join(__dirname, "..", "..", "resources", "plugins"); +} + const LOCK_FILE = "server.lock"; /** Release handle returned by {@link acquireDataDirLock}; unlinks the lockfile. */ @@ -156,6 +163,7 @@ async function serve(): Promise { supervisorPath: join(__dirname, "supervisor.cjs"), wslHelpersDir: resolveWslHelpersDir(), bundledSkillsDir: resolveBundledSkillsDir(), + bundledPluginsDir: resolveBundledPluginsDir(), secretStorageKey, ...(relayUrl ? { relayUrl } : {}), ...(relaySecret ? { relaySecret } : {}), diff --git a/src/server/createHeadlessRemoteHost.ts b/src/server/createHeadlessRemoteHost.ts index 491785cdd..dad04a971 100644 --- a/src/server/createHeadlessRemoteHost.ts +++ b/src/server/createHeadlessRemoteHost.ts @@ -69,6 +69,7 @@ export interface HeadlessRemoteHostOptions { readonly wslHelpersDir: string; /** Directory of app-bundled read-only skills; forwarded to the supervisor. */ readonly bundledSkillsDir?: string; + readonly bundledPluginsDir?: string; /** base64 32-byte AES key shared with the supervisor for secret sealing. */ readonly secretStorageKey: string; /** Data dir; defaults to the standard Poracode base dir for the channel. */ @@ -180,6 +181,7 @@ export async function createHeadlessRemoteHost( supervisorPath: options.supervisorPath, wslHelpersDir: options.wslHelpersDir, ...(options.bundledSkillsDir ? { bundledSkillsDir: options.bundledSkillsDir } : {}), + ...(options.bundledPluginsDir ? { bundledPluginsDir: options.bundledPluginsDir } : {}), secretStorageKey: options.secretStorageKey, prepareStartThread: enforcePersistedThreadLaunchInvariants, resolveExtraEnv: () => { diff --git a/src/shared/contracts/plugin.ts b/src/shared/contracts/plugin.ts index 0675566da..6ed4ffe95 100644 --- a/src/shared/contracts/plugin.ts +++ b/src/shared/contracts/plugin.ts @@ -1,70 +1,96 @@ import { z } from "zod"; -import { BUILT_IN_MCP_SERVER_IDS } from "./mcpServer"; +import { + agentPluginManifestSchema, + pluginMcpEntrySchema, + poracodePluginExtensionSchema, +} from "../plugins/spec"; -export const pluginCategorySchema = z.enum(["automation", "developer-tools", "productivity"]); -export type PluginCategory = z.infer; -export const pluginPlatformSchema = z.enum(["win32", "darwin", "linux"]); -export type PluginPlatform = z.infer; -export const pluginProjectKindSchema = z.enum(["windows", "posix", "wsl"]); -export type PluginProjectKind = z.infer; +/** + * Contracts for Agent Plugins packages after loading. + * + * The manifest itself is defined by the specification (`src/shared/plugins/spec`). + * This module covers what Poracode adds on top: where a package was found, what + * the loader resolved out of it, and the per-plugin state the user controls. + */ -export const pluginSkillContributionSchema = z +export const pluginSourceSchema = z.enum(["bundled", "user"]); +export type PluginSource = z.infer; + +export const pluginDiagnosticSchema = z .object({ - id: z.string().min(1), - name: z.string().min(1), - description: z.string().min(1), - folder: z - .string() - .min(1) - .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u), - requiredAppIds: z.array(z.string().min(1)).default([]), - defaultEnabled: z.literal(true).default(true), + severity: z.enum(["error", "warning"]), + scope: z.enum(["plugin", "component-type", "skill", "mcp-server"]), + code: z.string().min(1), + message: z.string().min(1), + target: z.string().min(1).optional(), + }) + .strict(); + +/** A `skills//SKILL.md` discovered inside the package boundary. */ +export const pluginSkillRefSchema = z + .object({ + folder: z.string().min(1), + /** Absolute host path to the skill directory. */ + path: z.string().min(1), }) .strict(); -export type PluginSkillContribution = z.infer; +export type PluginSkillRef = z.infer; -export const pluginAppContributionSchema = z +export const pluginMcpServerRefSchema = z .object({ - id: z.string().min(1), + /** Key as authored in `mcp.json`. */ name: z.string().min(1), - description: z.string().min(1), - builtInMcpServerId: z.enum(BUILT_IN_MCP_SERVER_IDS), - defaultEnabled: z.literal(true).default(true), + entry: pluginMcpEntrySchema, }) .strict(); -export type PluginAppContribution = z.infer; +export type PluginMcpServerRef = z.infer; -/** Provider-neutral Poracode plugin manifest. Runtime-owned apps are referenced by stable id. */ -export const pluginManifestSchema = z +/** A package that passed the loader's plugin-level checks. */ +export const loadedPluginSchema = z .object({ - manifestVersion: z.literal(1), - id: z - .string() - .min(1) - .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u), name: z.string().min(1), - description: z.string().min(1), - version: z.string().min(1), - publisher: z.string().min(1), - category: pluginCategorySchema, - platforms: z.array(pluginPlatformSchema).optional(), - projectKinds: z.array(pluginProjectKindSchema).optional(), - featured: z.boolean().default(false), - skills: z.array(pluginSkillContributionSchema), - apps: z.array(pluginAppContributionSchema), + source: pluginSourceSchema, + /** Filesystem-resolved package boundary. */ + root: z.string().min(1), + manifest: agentPluginManifestSchema, + poracode: poracodePluginExtensionSchema, + skills: z.array(pluginSkillRefSchema), + mcpServers: z.array(pluginMcpServerRefSchema), + diagnostics: z.array(pluginDiagnosticSchema), }) .strict(); -export type PluginManifest = z.infer; +export type LoadedPlugin = z.infer; +export const listPluginsResultSchema = z + .object({ + plugins: z.array(loadedPluginSchema), + /** Absolute path of the user plugin directory, for "open folder". */ + userPluginsDir: z.string().min(1), + }) + .strict(); +export type ListPluginsResult = z.infer; + +/** + * Per-plugin user state, keyed by the manifest `name`. Contribution ids are the + * skill folder name, the extension app id, and the `mcp.json` server key. + */ export const installedPluginStateSchema = z .object({ - version: z.string().min(1), + version: z.string().min(1).default("0.0.0"), enabled: z.boolean().default(true), disabledSkillIds: z.array(z.string().min(1)).default([]), disabledAppIds: z.array(z.string().min(1)).default([]), + disabledMcpServerNames: z.array(z.string().min(1)).default([]), }) .strict(); export type InstalledPluginState = z.infer; export const installedPluginsSchema = z.record(z.string(), installedPluginStateSchema).default({}); export type InstalledPlugins = z.infer; + +export type { + PluginCategory, + PluginPlatform, + PluginProjectKind, + PluginAppContribution, +} from "../plugins/spec"; diff --git a/src/shared/ipc/procedureMap.ts b/src/shared/ipc/procedureMap.ts index 824312b29..3faaa9b52 100644 --- a/src/shared/ipc/procedureMap.ts +++ b/src/shared/ipc/procedureMap.ts @@ -5,6 +5,7 @@ import { githubProcedures } from "./procedures/github"; import { gitProcedures } from "./procedures/git"; import { lspProcedures } from "./procedures/lsp"; import { mcpProcedures } from "./procedures/mcp"; +import { pluginProcedures } from "./procedures/plugins"; import { profileProcedures } from "./procedures/profile"; import { scheduleProcedures } from "./procedures/schedules"; import { skillProcedures } from "./procedures/skills"; @@ -32,6 +33,7 @@ export const groupedIpcProcedures = { profile: profileProcedures, schedules: scheduleProcedures, skills: skillProcedures, + plugins: pluginProcedures, } as const; export const ipcProcedureMap = { @@ -51,6 +53,7 @@ export const ipcProcedureMap = { ...profileProcedures, ...scheduleProcedures, ...skillProcedures, + ...pluginProcedures, } as const; export type IpcProcedureMap = typeof ipcProcedureMap; @@ -173,6 +176,7 @@ export const MAIN_LOCAL_PROCEDURE_NAMES = [ "deleteSchedule", "runScheduleNow", "getScheduleRuns", + "openPluginsFolder", ] as const satisfies readonly IpcProcedureName[]; export type MainLocalProcedureName = (typeof MAIN_LOCAL_PROCEDURE_NAMES)[number]; diff --git a/src/shared/ipc/procedures/plugins.ts b/src/shared/ipc/procedures/plugins.ts new file mode 100644 index 000000000..29e5c15dc --- /dev/null +++ b/src/shared/ipc/procedures/plugins.ts @@ -0,0 +1,17 @@ +import type { ListPluginsResult } from "../../contracts"; +import { defineNoArgProcedure } from "../core"; + +/** + * Agent Plugins packages are discovered on disk by the supervisor, so the + * renderer reads them over IPC rather than importing a static catalog. + */ +export const pluginProcedures = { + listPlugins: defineNoArgProcedure("listPlugins", "supervisor"), + /** Rescans the plugin roots, picking up packages added since the last read. */ + refreshPlugins: defineNoArgProcedure( + "refreshPlugins", + "supervisor", + ), + /** Opens the writable plugin directory so the user can drop a package in. */ + openPluginsFolder: defineNoArgProcedure("openPluginsFolder", "main-local"), +} as const; diff --git a/src/shared/plugins/catalog.test.ts b/src/shared/plugins/catalog.test.ts index dd741b7f0..013065d4d 100644 --- a/src/shared/plugins/catalog.test.ts +++ b/src/shared/plugins/catalog.test.ts @@ -1,12 +1,16 @@ import { describe, expect, it } from "vitest"; -import type { AgentCapability, ThreadConfig } from "../contracts"; -import { installedPluginsSchema, pluginManifestSchema } from "../contracts/plugin"; +import type { AgentCapability, LoadedPlugin, ThreadConfig } from "../contracts"; +import { installedPluginsSchema } from "../contracts/plugin"; +import { + AGENT_PLUGINS_MANIFEST_SCHEMA_URL, + type PluginAppContribution, + type PluginSkillPolicyEntry, +} from "./spec"; import { arePluginSkillRequiredAppsEnabled, - getBuiltInPluginManifest, getGloballyDisabledPluginConfigKeys, getInstalledPluginForMcpServer, - installBuiltInPlugin, + installPlugin, isBrowserMcpEnabledByAgentSettings, isBrowserMcpEnabledByConfigOrAgentSettings, isBuiltInMcpServerEnabledByPlugin, @@ -18,7 +22,7 @@ import { setInstalledPluginEnabled, setPluginAppEnabled, setPluginSkillEnabled, - uninstallBuiltInPlugin, + uninstallPlugin, } from "./catalog"; const SUPPORTED_TERMINAL_CONTEXT = { @@ -33,47 +37,62 @@ const SUPPORTED_TERMINAL_CONTEXT = { hostPlatform: "win32", } satisfies PluginAppLaunchContext; -describe("plugin contracts", () => { - it("parses provider-neutral manifests and defaults contribution enablement", () => { - const manifest = pluginManifestSchema.parse({ - manifestVersion: 1, - id: "test-tools", - name: "Test Tools", - description: "Tools used by tests.", - version: "1.0.0", - publisher: "Poracode", +function makePlugin( + name: string, + poracode: { + apps?: PluginAppContribution[]; + skills?: Record; + platforms?: ("win32" | "darwin" | "linux")[]; + projectKinds?: ("windows" | "posix" | "wsl")[]; + } = {}, +): LoadedPlugin { + return { + name, + source: "bundled", + root: `/plugins/${name}`, + manifest: { $schema: AGENT_PLUGINS_MANIFEST_SCHEMA_URL, name, version: "1.0.0" }, + poracode: { category: "developer-tools", - skills: [ - { - id: "test-skill", - name: "Test Skill", - description: "Run a test workflow.", - folder: "test-skill", - }, - ], - apps: [ - { - id: "browser", - name: "Browser", - description: "Control the browser.", - builtInMcpServerId: "browser", - }, - ], - }); - - expect(manifest).toMatchObject({ featured: false, - skills: [{ requiredAppIds: [], defaultEnabled: true }], - apps: [{ defaultEnabled: true }], - }); - expect(() => - pluginManifestSchema.parse({ - ...manifest, - skills: [{ ...manifest.skills[0], defaultEnabled: false }], - }), - ).toThrow(/expected true/u); - }); + communityMaintained: false, + apps: poracode.apps ?? [], + skills: poracode.skills ?? {}, + ...(poracode.platforms ? { platforms: poracode.platforms } : {}), + ...(poracode.projectKinds ? { projectKinds: poracode.projectKinds } : {}), + }, + skills: Object.keys(poracode.skills ?? {}).map((folder) => ({ + folder, + path: `/plugins/${name}/skills/${folder}`, + })), + mcpServers: [], + diagnostics: [], + }; +} +const app = (id: string, builtInMcpServerId: PluginAppContribution["builtInMcpServerId"]) => ({ + id, + name: id, + description: id, + builtInMcpServerId, +}); + +const BROWSER_TOOLS = makePlugin("browser-tools", { + apps: [app("browser", "browser")], + skills: { "browser-control": { requiredAppIds: ["browser"] } }, +}); +const SUBAGENTS = makePlugin("subagent-delegation", { apps: [app("subagents", "subagents")] }); +const CHROME_TOOLS = makePlugin("chrome-tools", { + apps: [app("chrome", "chrome")], + projectKinds: ["windows", "posix"], +}); +const COMPUTER_USE = makePlugin("computer-use", { + apps: [app("computer-use", "computer-use")], + platforms: ["win32", "darwin"], + projectKinds: ["windows", "posix"], +}); +const ALL_PLUGINS = [BROWSER_TOOLS, SUBAGENTS, CHROME_TOOLS, COMPUTER_USE]; + +describe("plugin contracts", () => { it("defaults persisted plugin state fields", () => { expect(installedPluginsSchema.parse({ "test-tools": { version: "1.0.0" } })).toEqual({ "test-tools": { @@ -81,12 +100,17 @@ describe("plugin contracts", () => { enabled: true, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }); }); + + it("defaults the version when a manifest omits it", () => { + expect(installedPluginsSchema.parse({ "test-tools": {} })["test-tools"]?.version).toBe("0.0.0"); + }); }); -describe("built-in plugin catalog", () => { +describe("plugin catalog", () => { it("resolves Browser MCP from either thread config or provider settings", () => { expect(isBrowserMcpEnabledByAgentSettings({ browserMcp: true })).toBe(true); expect(isBrowserMcpEnabledByConfigOrAgentSettings({ browserMcp: true }, undefined)).toBe(true); @@ -98,7 +122,7 @@ describe("built-in plugin catalog", () => { it("previews provider Browser settings while preserving hard disables", () => { const installed = setPluginAppEnabled( - installBuiltInPlugin({}, "browser-tools"), + installPlugin({}, BROWSER_TOOLS), "browser-tools", "browser", false, @@ -110,11 +134,13 @@ describe("built-in plugin catalog", () => { hostPlatform: "win32", disabledBuiltInMcpServers: {}, agentSettings: { browserMcp: true }, - } satisfies Parameters[2]; + } satisfies Parameters[3]; - expect(resolvePluginLaunchPreview({ model: "test" }, installed, context).browserMcp).toBe(true); expect( - resolvePluginLaunchPreview({ model: "test" }, installed, { + resolvePluginLaunchPreview({ model: "test" }, ALL_PLUGINS, installed, context).browserMcp, + ).toBe(true); + expect( + resolvePluginLaunchPreview({ model: "test" }, ALL_PLUGINS, installed, { ...context, disabledBuiltInMcpServers: { browser: true }, }).browserMcp, @@ -122,14 +148,15 @@ describe("built-in plugin catalog", () => { expect( resolvePluginLaunchPreview( { model: "test" }, + ALL_PLUGINS, setInstalledPluginEnabled(installed, "browser-tools", false), context, ).browserMcp, ).toBe(false); }); - it("installs and uninstalls a catalog plugin", () => { - const installed = installBuiltInPlugin({}, "browser-tools"); + it("installs and uninstalls a plugin", () => { + const installed = installPlugin({}, BROWSER_TOOLS); expect(installed).toEqual({ "browser-tools": { @@ -137,15 +164,18 @@ describe("built-in plugin catalog", () => { enabled: true, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }); - expect(getInstalledPluginForMcpServer(installed, "browser")?.id).toBe("browser-tools"); - expect(installBuiltInPlugin(installed, "browser-tools")).toBe(installed); - expect(uninstallBuiltInPlugin(installed, "browser-tools")).toEqual({}); + expect(getInstalledPluginForMcpServer(ALL_PLUGINS, installed, "browser")?.name).toBe( + "browser-tools", + ); + expect(installPlugin(installed, BROWSER_TOOLS)).toBe(installed); + expect(uninstallPlugin(installed, "browser-tools")).toEqual({}); }); it("toggles the plugin and its skill and app contributions independently", () => { - const installed = installBuiltInPlugin({}, "browser-tools"); + const installed = installPlugin({}, BROWSER_TOOLS); const disabledSkill = setPluginSkillEnabled( installed, "browser-tools", @@ -160,6 +190,7 @@ describe("built-in plugin catalog", () => { enabled: false, disabledSkillIds: ["browser-control"], disabledAppIds: ["browser"], + disabledMcpServerNames: [], }); expect( setPluginAppEnabled( @@ -172,10 +203,8 @@ describe("built-in plugin catalog", () => { }); it("requires a skill's companion apps to be launch-compatible", () => { - const manifest = getBuiltInPluginManifest("browser-tools")!; - const skill = manifest.skills[0]!; expect( - isPluginSkillSupportedForLaunch(manifest, skill, { + isPluginSkillSupportedForLaunch(BROWSER_TOOLS, "browser-control", { hostPlatform: "win32", projectLocation: SUPPORTED_TERMINAL_CONTEXT.projectLocation, capabilities: { @@ -186,7 +215,7 @@ describe("built-in plugin catalog", () => { }), ).toBe(true); expect( - isPluginSkillSupportedForLaunch(manifest, skill, { + isPluginSkillSupportedForLaunch(BROWSER_TOOLS, "browser-control", { hostPlatform: "win32", projectLocation: SUPPORTED_TERMINAL_CONTEXT.projectLocation, capabilities: { presentationMode: "terminal" } as AgentCapability, @@ -196,17 +225,14 @@ describe("built-in plugin catalog", () => { }); it("requires a skill's companion Apps in the effective launch config", () => { - const manifest = getBuiltInPluginManifest("browser-tools")!; - const skill = manifest.skills[0]!; - expect( - arePluginSkillRequiredAppsEnabled(manifest, skill, { + arePluginSkillRequiredAppsEnabled(BROWSER_TOOLS, "browser-control", { model: "test-model", browserMcp: true, }), ).toBe(true); expect( - arePluginSkillRequiredAppsEnabled(manifest, skill, { + arePluginSkillRequiredAppsEnabled(BROWSER_TOOLS, "browser-control", { model: "test-model", browserMcp: false, }), @@ -215,15 +241,11 @@ describe("built-in plugin catalog", () => { it("applies enabled app contributions to thread config", () => { const config: ThreadConfig = { model: "test-model" }; - const installed = [ - "browser-tools", - "subagent-delegation", - "chrome-tools", - "computer-use", - ].reduce(installBuiltInPlugin, {}); + const installed = ALL_PLUGINS.reduce(installPlugin, {}); expect( - resolvePluginAppsForThreadConfig(config, installed, SUPPORTED_TERMINAL_CONTEXT).config, + resolvePluginAppsForThreadConfig(config, ALL_PLUGINS, installed, SUPPORTED_TERMINAL_CONTEXT) + .config, ).toEqual({ model: "test-model", browserMcp: true, @@ -231,29 +253,42 @@ describe("built-in plugin catalog", () => { chromeMcp: true, computerUse: true, }); - expect(isBuiltInMcpServerEnabledByPlugin(installed, "browser")).toBe(true); + expect(isBuiltInMcpServerEnabledByPlugin(ALL_PLUGINS, installed, "browser")).toBe(true); const disabled = setPluginAppEnabled(installed, "browser-tools", "browser", false); expect( - resolvePluginAppsForThreadConfig(config, disabled, SUPPORTED_TERMINAL_CONTEXT).config, + resolvePluginAppsForThreadConfig(config, ALL_PLUGINS, disabled, SUPPORTED_TERMINAL_CONTEXT) + .config, ).toEqual({ model: "test-model", subagentMcp: true, chromeMcp: true, computerUse: true, }); - expect(isBuiltInMcpServerEnabledByPlugin(disabled, "browser")).toBe(false); - expect(resolvePluginAppsForThreadConfig(config, {}, SUPPORTED_TERMINAL_CONTEXT).config).toBe( - config, - ); + expect(isBuiltInMcpServerEnabledByPlugin(ALL_PLUGINS, disabled, "browser")).toBe(false); + expect( + resolvePluginAppsForThreadConfig(config, ALL_PLUGINS, {}, SUPPORTED_TERMINAL_CONTEXT).config, + ).toBe(config); const disabledPlugin = setInstalledPluginEnabled(installed, "browser-tools", false); - expect(getGloballyDisabledPluginConfigKeys(disabledPlugin)).toEqual(["browserMcp"]); + expect(getGloballyDisabledPluginConfigKeys(ALL_PLUGINS, disabledPlugin)).toEqual([ + "browserMcp", + ]); expect( - resolvePluginAppsForThreadConfig(config, disabledPlugin, SUPPORTED_TERMINAL_CONTEXT), + resolvePluginAppsForThreadConfig( + config, + ALL_PLUGINS, + disabledPlugin, + SUPPORTED_TERMINAL_CONTEXT, + ), ).toMatchObject({ disabledConfigKeys: ["browserMcp"] }); expect( - resolvePluginAppsForThreadConfig(config, disabledPlugin, SUPPORTED_TERMINAL_CONTEXT).config, + resolvePluginAppsForThreadConfig( + config, + ALL_PLUGINS, + disabledPlugin, + SUPPORTED_TERMINAL_CONTEXT, + ).config, ).toEqual({ model: "test-model", browserMcp: false, @@ -264,6 +299,7 @@ describe("built-in plugin catalog", () => { expect( resolvePluginAppsForThreadConfig( { ...config, browserMcp: true }, + ALL_PLUGINS, disabledPlugin, SUPPORTED_TERMINAL_CONTEXT, ).config, @@ -278,21 +314,16 @@ describe("built-in plugin catalog", () => { it("does not apply plugin apps outside the provider or host launch scope", () => { const config: ThreadConfig = { model: "test-model" }; - const installed = [ - "browser-tools", - "subagent-delegation", - "chrome-tools", - "computer-use", - ].reduce(installBuiltInPlugin, {}); + const installed = ALL_PLUGINS.reduce(installPlugin, {}); expect( - resolvePluginAppsForThreadConfig(config, installed, { + resolvePluginAppsForThreadConfig(config, ALL_PLUGINS, installed, { ...SUPPORTED_TERMINAL_CONTEXT, capabilities: {}, }).config, ).toBe(config); expect( - resolvePluginAppsForThreadConfig(config, installed, { + resolvePluginAppsForThreadConfig(config, ALL_PLUGINS, installed, { ...SUPPORTED_TERMINAL_CONTEXT, projectLocation: { kind: "wsl", @@ -309,11 +340,7 @@ describe("built-in plugin catalog", () => { linuxPath: "/repo", uncPath: "\\\\wsl.localhost\\Ubuntu\\repo", } as const; - expect( - isPluginSupportedForProject(getBuiltInPluginManifest("chrome-tools")!, "win32", wslProject), - ).toBe(false); - expect( - isPluginSupportedForProject(getBuiltInPluginManifest("browser-tools")!, "win32", wslProject), - ).toBe(true); + expect(isPluginSupportedForProject(CHROME_TOOLS, "win32", wslProject)).toBe(false); + expect(isPluginSupportedForProject(BROWSER_TOOLS, "win32", wslProject)).toBe(true); }); }); diff --git a/src/shared/plugins/catalog.ts b/src/shared/plugins/catalog.ts index df1ab1341..79029d512 100644 --- a/src/shared/plugins/catalog.ts +++ b/src/shared/plugins/catalog.ts @@ -7,195 +7,74 @@ import { type ThreadConfig, type ThreadPresentationMode, } from "../contracts"; -import { - pluginManifestSchema, - type InstalledPluginState, - type InstalledPlugins, - type PluginManifest, - type PluginSkillContribution, +import type { + InstalledPluginState, + InstalledPlugins, + LoadedPlugin, + PluginSkillRef, } from "../contracts/plugin"; +import type { PluginSkillPolicyEntry } from "./spec"; -const manifests = [ - { - manifestVersion: 1, - id: "browser-tools", - name: "Browser Tools", - description: "Browse, inspect, and test websites in Poracode's isolated in-app browser.", - version: "1.0.0", - publisher: "Poracode", - category: "developer-tools", - featured: true, - skills: [ - { - id: "browser-control", - name: "Browser Control", - description: "Navigate, inspect, and test pages with the in-app Browser MCP.", - folder: "browser-control", - requiredAppIds: ["browser"], - defaultEnabled: true, - }, - ], - apps: [ - { - id: "browser", - name: "Browser", - description: "Control Poracode's isolated in-app browser.", - builtInMcpServerId: "browser", - defaultEnabled: true, - }, - ], - }, - { - manifestVersion: 1, - id: "chrome-tools", - name: "Chrome Tools", - description: "Work with the pages and signed-in sessions already open in Chrome.", - version: "1.0.0", - publisher: "Poracode", - category: "automation", - projectKinds: ["windows", "posix"], - featured: true, - skills: [ - { - id: "chrome-control", - name: "Chrome Control", - description: "Use Chrome safely when a task needs an existing browser session.", - folder: "chrome-control", - requiredAppIds: ["chrome"], - defaultEnabled: true, - }, - ], - apps: [ - { - id: "chrome", - name: "Chrome", - description: "Control the user's Chrome browser through Poracode.", - builtInMcpServerId: "chrome", - defaultEnabled: true, - }, - ], - }, - { - manifestVersion: 1, - id: "computer-use", - name: "Computer Use", - description: "Control desktop apps and complete visual workflows.", - version: "1.0.0", - publisher: "Poracode", - category: "automation", - platforms: ["win32", "darwin"], - projectKinds: ["windows", "posix"], - featured: true, - skills: [ - { - id: "computer-use", - name: "Computer Use", - description: "Operate desktop apps through Poracode's desktop-control tools.", - folder: "computer-use", - requiredAppIds: ["computer-use"], - defaultEnabled: true, - }, - ], - apps: [ - { - id: "computer-use", - name: "Computer Use", - description: "Control supported desktop apps and windows.", - builtInMcpServerId: "computer-use", - defaultEnabled: true, - }, - ], - }, - { - manifestVersion: 1, - id: "subagent-delegation", - name: "Subagent Delegation", - description: "Delegate focused work to other installed agents and coordinate the results.", - version: "1.0.0", - publisher: "Poracode", - category: "productivity", - featured: true, - skills: [ - { - id: "subagent-delegation", - name: "Subagent Delegation", - description: "Choose, brief, and coordinate subagents for parallel work.", - folder: "subagent-delegation", - requiredAppIds: ["subagents"], - defaultEnabled: true, - }, - ], - apps: [ - { - id: "subagents", - name: "Subagents", - description: "Create and coordinate Poracode agent threads.", - builtInMcpServerId: "subagents", - defaultEnabled: true, - }, - ], - }, -] satisfies PluginManifest[]; - -export const BUILT_IN_PLUGIN_MANIFESTS = manifests.map((manifest) => - pluginManifestSchema.parse(manifest), -); - -const manifestsById = new Map(BUILT_IN_PLUGIN_MANIFESTS.map((manifest) => [manifest.id, manifest])); -const bundledSkillsByFolder = new Map( - BUILT_IN_PLUGIN_MANIFESTS.flatMap((manifest) => - manifest.skills.map( - (contribution) => [contribution.folder, { manifest, contribution }] as const, - ), - ), -); - -export function getBuiltInPluginManifest(pluginId: string): PluginManifest | undefined { - return manifestsById.get(pluginId); -} - -export function getBundledPluginSkill( - folder: string, -): { manifest: PluginManifest; contribution: PluginSkillContribution } | undefined { - return bundledSkillsByFolder.get(folder); -} +/** + * Policy over loaded Agent Plugins packages. + * + * Packages themselves are discovered on disk by the supervisor + * (`src/supervisor/plugins`); this module holds the provider-agnostic rules that + * both the supervisor and the renderer apply to them — host/project support, + * contribution enablement, and how a plugin's runtime-owned apps map onto thread + * launch config. + */ export function isPluginSupportedOnHost( - manifest: PluginManifest, + plugin: LoadedPlugin, hostPlatform: NodeJS.Platform, ): boolean { - return ( - !manifest.platforms || manifest.platforms.includes(hostPlatform as "win32" | "darwin" | "linux") - ); + const platforms = plugin.poracode.platforms; + return !platforms || platforms.includes(hostPlatform as "win32" | "darwin" | "linux"); } export function isPluginSupportedForProject( - manifest: PluginManifest, + plugin: LoadedPlugin, hostPlatform: NodeJS.Platform, projectLocation: ProjectLocation | undefined, ): boolean { + const projectKinds = plugin.poracode.projectKinds; return ( - isPluginSupportedOnHost(manifest, hostPlatform) && - (!projectLocation || - !manifest.projectKinds || - manifest.projectKinds.includes(projectLocation.kind)) + isPluginSupportedOnHost(plugin, hostPlatform) && + (!projectLocation || !projectKinds || projectKinds.includes(projectLocation.kind)) ); } +export function getPluginSkill(plugin: LoadedPlugin, folder: string): PluginSkillRef | undefined { + return plugin.skills.find((skill) => skill.folder === folder); +} + +/** Poracode-specific policy for a skill, defaulted when the plugin declares none. */ +export function getPluginSkillPolicy(plugin: LoadedPlugin, folder: string): PluginSkillPolicyEntry { + return plugin.poracode.skills[folder] ?? { requiredAppIds: [] }; +} + +export interface PluginSkillLaunchContext { + hostPlatform: NodeJS.Platform; + projectLocation?: ProjectLocation; + capabilities?: AgentCapability; + presentationMode?: ThreadPresentationMode; +} + +/** + * True when a plugin skill can be offered for a launch: the plugin supports the + * host and project, and every app the skill requires can actually run there. + */ export function isPluginSkillSupportedForLaunch( - manifest: PluginManifest, - skill: PluginSkillContribution, - context: { - hostPlatform: NodeJS.Platform; - projectLocation?: ProjectLocation; - capabilities?: AgentCapability; - presentationMode?: ThreadPresentationMode; - }, + plugin: LoadedPlugin, + folder: string, + context: PluginSkillLaunchContext, ): boolean { - if (!isPluginSupportedForProject(manifest, context.hostPlatform, context.projectLocation)) { + if (!isPluginSupportedForProject(plugin, context.hostPlatform, context.projectLocation)) { return false; } - if (skill.requiredAppIds.length === 0 || !context.capabilities || !context.projectLocation) { + const requiredAppIds = getPluginSkillPolicy(plugin, folder).requiredAppIds; + if (requiredAppIds.length === 0 || !context.capabilities || !context.projectLocation) { return true; } const capabilities = context.capabilities; @@ -203,8 +82,8 @@ export function isPluginSkillSupportedForLaunch( const modes = context.presentationMode ? [context.presentationMode] : (capabilities.presentationModes ?? [capabilities.presentationMode ?? "terminal"]); - return skill.requiredAppIds.every((appId) => { - const app = manifest.apps.find((candidate) => candidate.id === appId); + return requiredAppIds.every((appId) => { + const app = plugin.poracode.apps.find((candidate) => candidate.id === appId); return Boolean( app && modes.some((presentationMode) => @@ -219,62 +98,39 @@ export function isPluginSkillSupportedForLaunch( }); } -export function getInstalledPluginForMcpServer( - installedPlugins: InstalledPlugins, - serverId: BuiltInMcpServerId, -): PluginManifest | undefined { - return BUILT_IN_PLUGIN_MANIFESTS.find( - (manifest) => - installedPlugins[manifest.id] !== undefined && - manifest.apps.some((app) => app.builtInMcpServerId === serverId), - ); -} - export function isPluginSkillEnabled( - manifest: PluginManifest, + plugin: LoadedPlugin, state: InstalledPluginState, - skillId: string, + folder: string, ): boolean { - const skill = manifest.skills.find((candidate) => candidate.id === skillId); - return Boolean(state.enabled && skill && !state.disabledSkillIds.includes(skill.id)); + return Boolean( + state.enabled && getPluginSkill(plugin, folder) && !state.disabledSkillIds.includes(folder), + ); } export function isPluginAppEnabled( - manifest: PluginManifest, + plugin: LoadedPlugin, state: InstalledPluginState, appId: string, ): boolean { - const app = manifest.apps.find((candidate) => candidate.id === appId); + const app = plugin.poracode.apps.find((candidate) => candidate.id === appId); return Boolean(state.enabled && app && !state.disabledAppIds.includes(app.id)); } -export function isBuiltInMcpServerEnabledByPlugin( - installedPlugins: InstalledPlugins, - serverId: BuiltInMcpServerId, +export function isPluginMcpServerEnabled( + plugin: LoadedPlugin, + state: InstalledPluginState, + serverName: string, ): boolean { - return BUILT_IN_PLUGIN_MANIFESTS.some((manifest) => { - const state = installedPlugins[manifest.id]; - if (!state) return false; - const app = manifest.apps.find((candidate) => candidate.builtInMcpServerId === serverId); - return app ? isPluginAppEnabled(manifest, state, app.id) : false; - }); -} - -export interface PluginAppLaunchContext { - capabilities: Pick< - AgentCapability, - "browserMcpScope" | "subagentMcpScope" | "computerUseMcpScope" | "chromeMcpScope" - >; - presentationMode: ThreadPresentationMode; - projectLocation: ProjectLocation; - hostPlatform: NodeJS.Platform; -} - -export interface PluginLaunchPreviewContext extends PluginAppLaunchContext { - disabledBuiltInMcpServers: BuiltInMcpServerDisabled; - agentSettings: Readonly> | undefined; + const server = plugin.mcpServers.find((candidate) => candidate.name === serverName); + return Boolean(state.enabled && server && !state.disabledMcpServerNames.includes(serverName)); } +/** + * Maps a Poracode runtime-owned MCP server onto the thread config flag that + * turns it on. These servers mint a URL and bearer token per thread, so plugins + * reference them by stable id rather than declaring transport details. + */ export const PLUGIN_MCP_CONFIG_ENTRIES = [ ["browser", "browserMcp"], ["subagents", "subagentMcp"], @@ -284,6 +140,55 @@ export const PLUGIN_MCP_CONFIG_ENTRIES = [ export type PluginMcpConfigKey = (typeof PLUGIN_MCP_CONFIG_ENTRIES)[number][1]; +/** The installed plugin that owns a built-in server, if any plugin claims it. */ +export function getInstalledPluginForMcpServer( + plugins: readonly LoadedPlugin[], + installedPlugins: InstalledPlugins, + serverId: BuiltInMcpServerId, +): LoadedPlugin | undefined { + return plugins.find( + (plugin) => + installedPlugins[plugin.name] !== undefined && + plugin.poracode.apps.some((app) => app.builtInMcpServerId === serverId), + ); +} + +export function isBuiltInMcpServerEnabledByPlugin( + plugins: readonly LoadedPlugin[], + installedPlugins: InstalledPlugins, + serverId: BuiltInMcpServerId, +): boolean { + return plugins.some((plugin) => { + const state = installedPlugins[plugin.name]; + if (!state) return false; + const app = plugin.poracode.apps.find((candidate) => candidate.builtInMcpServerId === serverId); + return app ? isPluginAppEnabled(plugin, state, app.id) : false; + }); +} + +/** + * True when nothing blocks the server: either no installed plugin claims it, or + * the plugin that does is enabled. + */ +export function isBuiltInMcpServerAvailableByPlugin( + plugins: readonly LoadedPlugin[], + installedPlugins: InstalledPlugins, + serverId: BuiltInMcpServerId, +): boolean { + const plugin = getInstalledPluginForMcpServer(plugins, installedPlugins, serverId); + return !plugin || installedPlugins[plugin.name]?.enabled === true; +} + +/** Config keys a disabled plugin hard-blocks, whatever provider settings say. */ +export function getGloballyDisabledPluginConfigKeys( + plugins: readonly LoadedPlugin[], + installedPlugins: InstalledPlugins, +): PluginMcpConfigKey[] { + return PLUGIN_MCP_CONFIG_ENTRIES.flatMap(([serverId, key]) => + isBuiltInMcpServerAvailableByPlugin(plugins, installedPlugins, serverId) ? [] : [key], + ); +} + export function isBrowserMcpEnabledByAgentSettings( agentSettings: Readonly> | undefined, ): boolean { @@ -298,12 +203,12 @@ export function isBrowserMcpEnabledByConfigOrAgentSettings( } export function arePluginSkillRequiredAppsEnabled( - manifest: PluginManifest, - skill: PluginSkillContribution, + plugin: LoadedPlugin, + folder: string, config: ThreadConfig, ): boolean { - return skill.requiredAppIds.every((appId) => { - const app = manifest.apps.find((candidate) => candidate.id === appId); + return getPluginSkillPolicy(plugin, folder).requiredAppIds.every((appId) => { + const app = plugin.poracode.apps.find((candidate) => candidate.id === appId); const configEntry = app ? PLUGIN_MCP_CONFIG_ENTRIES.find(([serverId]) => serverId === app.builtInMcpServerId) : undefined; @@ -311,20 +216,19 @@ export function arePluginSkillRequiredAppsEnabled( }); } -export function getGloballyDisabledPluginConfigKeys( - installedPlugins: InstalledPlugins, -): PluginMcpConfigKey[] { - return PLUGIN_MCP_CONFIG_ENTRIES.flatMap(([serverId, key]) => - isBuiltInMcpServerAvailableByPlugin(installedPlugins, serverId) ? [] : [key], - ); +export interface PluginAppLaunchContext { + capabilities: Pick< + AgentCapability, + "browserMcpScope" | "subagentMcpScope" | "computerUseMcpScope" | "chromeMcpScope" + >; + presentationMode: ThreadPresentationMode; + projectLocation: ProjectLocation; + hostPlatform: NodeJS.Platform; } -export function isBuiltInMcpServerAvailableByPlugin( - installedPlugins: InstalledPlugins, - serverId: BuiltInMcpServerId, -): boolean { - const manifest = getInstalledPluginForMcpServer(installedPlugins, serverId); - return !manifest || installedPlugins[manifest.id]?.enabled === true; +export interface PluginLaunchPreviewContext extends PluginAppLaunchContext { + disabledBuiltInMcpServers: BuiltInMcpServerDisabled; + agentSettings: Readonly> | undefined; } export function isBuiltInMcpServerSupportedForLaunch( @@ -353,17 +257,18 @@ export function isBuiltInMcpServerSupportedForLaunch( /** Add installed plugin apps to a new launch without serializing live MCP transport details. */ export function resolvePluginAppsForThreadConfig( config: ThreadConfig, + plugins: readonly LoadedPlugin[], installedPlugins: InstalledPlugins, context: PluginAppLaunchContext, ): { config: ThreadConfig; disabledConfigKeys: PluginMcpConfigKey[] } { - const disabledConfigKeys = getGloballyDisabledPluginConfigKeys(installedPlugins); + const disabledConfigKeys = getGloballyDisabledPluginConfigKeys(plugins, installedPlugins); let next = config; for (const key of disabledConfigKeys) { if (next[key] !== false) next = { ...next, [key]: false }; } for (const [serverId, key] of PLUGIN_MCP_CONFIG_ENTRIES) { if ( - isBuiltInMcpServerEnabledByPlugin(installedPlugins, serverId) && + isBuiltInMcpServerEnabledByPlugin(plugins, installedPlugins, serverId) && isBuiltInMcpServerSupportedForLaunch(serverId, context) && next[key] !== true ) { @@ -375,11 +280,12 @@ export function resolvePluginAppsForThreadConfig( export function resolvePluginLaunchPreview( config: ThreadConfig, + plugins: readonly LoadedPlugin[], installedPlugins: InstalledPlugins, context: PluginLaunchPreviewContext, ): ThreadConfig { const { config: appliedConfig, disabledConfigKeys: pluginDisabledConfigKeys } = - resolvePluginAppsForThreadConfig(config, installedPlugins, context); + resolvePluginAppsForThreadConfig(config, plugins, installedPlugins, context); let next = appliedConfig; for (const [serverId, key] of PLUGIN_MCP_CONFIG_ENTRIES) { if ( @@ -401,75 +307,91 @@ export function resolvePluginLaunchPreview( return next; } -export function installBuiltInPlugin( +export function installPlugin( installedPlugins: InstalledPlugins, - pluginId: string, + plugin: LoadedPlugin, ): InstalledPlugins { - if (installedPlugins[pluginId]) return installedPlugins; - const manifest = getBuiltInPluginManifest(pluginId); - if (!manifest) return installedPlugins; + if (installedPlugins[plugin.name]) return installedPlugins; return { ...installedPlugins, - [pluginId]: { - version: manifest.version, + [plugin.name]: { + version: plugin.manifest.version ?? "0.0.0", enabled: true, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }; } -export function uninstallBuiltInPlugin( +export function uninstallPlugin( installedPlugins: InstalledPlugins, - pluginId: string, + pluginName: string, ): InstalledPlugins { - if (!installedPlugins[pluginId]) return installedPlugins; + if (!installedPlugins[pluginName]) return installedPlugins; const next = { ...installedPlugins }; - delete next[pluginId]; + delete next[pluginName]; return next; } export function setInstalledPluginEnabled( installedPlugins: InstalledPlugins, - pluginId: string, + pluginName: string, enabled: boolean, ): InstalledPlugins { - const current = installedPlugins[pluginId]; + const current = installedPlugins[pluginName]; if (!current || current.enabled === enabled) return installedPlugins; - return { ...installedPlugins, [pluginId]: { ...current, enabled } }; + return { ...installedPlugins, [pluginName]: { ...current, enabled } }; } +type ContributionField = "disabledSkillIds" | "disabledAppIds" | "disabledMcpServerNames"; + function setContributionEnabled( installedPlugins: InstalledPlugins, - pluginId: string, + pluginName: string, contributionId: string, enabled: boolean, - field: "disabledSkillIds" | "disabledAppIds", + field: ContributionField, ): InstalledPlugins { - const current = installedPlugins[pluginId]; + const current = installedPlugins[pluginName]; if (!current) return installedPlugins; const wasDisabled = current[field].includes(contributionId); if (wasDisabled === !enabled) return installedPlugins; const disabled = new Set(current[field]); if (enabled) disabled.delete(contributionId); else disabled.add(contributionId); - return { ...installedPlugins, [pluginId]: { ...current, [field]: [...disabled] } }; + return { ...installedPlugins, [pluginName]: { ...current, [field]: [...disabled] } }; } export function setPluginSkillEnabled( installedPlugins: InstalledPlugins, - pluginId: string, - skillId: string, + pluginName: string, + folder: string, enabled: boolean, ): InstalledPlugins { - return setContributionEnabled(installedPlugins, pluginId, skillId, enabled, "disabledSkillIds"); + return setContributionEnabled(installedPlugins, pluginName, folder, enabled, "disabledSkillIds"); } export function setPluginAppEnabled( installedPlugins: InstalledPlugins, - pluginId: string, + pluginName: string, appId: string, enabled: boolean, ): InstalledPlugins { - return setContributionEnabled(installedPlugins, pluginId, appId, enabled, "disabledAppIds"); + return setContributionEnabled(installedPlugins, pluginName, appId, enabled, "disabledAppIds"); +} + +export function setPluginMcpServerEnabled( + installedPlugins: InstalledPlugins, + pluginName: string, + serverName: string, + enabled: boolean, +): InstalledPlugins { + return setContributionEnabled( + installedPlugins, + pluginName, + serverName, + enabled, + "disabledMcpServerNames", + ); } diff --git a/src/shared/plugins/spec/diagnostics.ts b/src/shared/plugins/spec/diagnostics.ts new file mode 100644 index 000000000..cda8a5590 --- /dev/null +++ b/src/shared/plugins/spec/diagnostics.ts @@ -0,0 +1,50 @@ +/** + * Diagnostics emitted while loading an Agent Plugins package. + * + * The specification requires clients to apply the *narrowest* applicable failure + * boundary and to keep loading everything outside it. A diagnostic records what + * was rejected and at which boundary, so nothing fails silently. + * + * @see https://agent-plugins.org/client-implementers/loading-and-discovery + */ + +/** Narrowest failure boundary that applied, ordered widest to narrowest. */ +export type PluginDiagnosticScope = + /** The whole package was rejected. */ + | "plugin" + /** One component type was disabled; the rest of the plugin still loads. */ + | "component-type" + /** A single skill directory was skipped. */ + | "skill" + /** A single `mcp.json` server entry was skipped. */ + | "mcp-server"; + +export type PluginDiagnosticSeverity = "error" | "warning"; + +export interface PluginDiagnostic { + /** `error` rejected something; `warning` was tolerated and ignored. */ + severity: PluginDiagnosticSeverity; + scope: PluginDiagnosticScope; + /** Stable machine-readable reason, e.g. `manifest-invalid`, `path-escapes-root`. */ + code: string; + /** Human-readable detail. Not localized: these are developer-facing. */ + message: string; + /** Skill folder, MCP server name, or path the diagnostic applies to. */ + target?: string; +} + +export function pluginDiagnostic( + severity: PluginDiagnosticSeverity, + scope: PluginDiagnosticScope, + code: string, + message: string, + target?: string, +): PluginDiagnostic { + return { severity, scope, code, message, ...(target ? { target } : {}) }; +} + +/** Compact one-line rendering for logs. */ +export function formatPluginDiagnostic(diagnostic: PluginDiagnostic): string { + const target = diagnostic.target ? ` (${diagnostic.target})` : ""; + return `[${diagnostic.severity}] ${diagnostic.scope}/${diagnostic.code}${target}: ${diagnostic.message}`; +} diff --git a/src/shared/plugins/spec/extensions.ts b/src/shared/plugins/spec/extensions.ts new file mode 100644 index 000000000..fff6bd5da --- /dev/null +++ b/src/shared/plugins/spec/extensions.ts @@ -0,0 +1,154 @@ +import { z } from "zod"; +import { BUILT_IN_MCP_SERVER_IDS } from "../../contracts/mcpServer"; +import { pluginDiagnostic, type PluginDiagnostic } from "./diagnostics"; +import type { AgentPluginManifest } from "./manifest"; + +/** + * Poracode's client extension namespace. + * + * The specification defines exactly two component types — skills and `mcp.json` + * servers — and reserves `extensions` with reverse-domain keys for everything a + * client needs beyond that. Poracode's runtime-owned MCP servers (Browser, + * Chrome, Computer Use, Subagents) mint their URL and bearer token per thread, + * so they cannot be expressed as static `mcp.json` entries. They are declared + * here instead, by stable id, and the supervisor wires the live transport. + * + * @see https://agent-plugins.org/specification + */ + +export const PORACODE_EXTENSION_NAMESPACE = "com.poracode.client"; + +export const pluginCategorySchema = z.enum([ + "automation", + "communication", + "developer-tools", + "productivity", +]); +export type PluginCategory = z.infer; + +export const pluginPlatformSchema = z.enum(["win32", "darwin", "linux"]); +export type PluginPlatform = z.infer; + +export const pluginProjectKindSchema = z.enum(["windows", "posix", "wsl"]); +export type PluginProjectKind = z.infer; + +/** A Poracode runtime-owned MCP server surfaced as a plugin contribution. */ +export const pluginAppContributionSchema = z + .object({ + id: z.string().min(1), + name: z.string().min(1), + description: z.string().min(1), + builtInMcpServerId: z.enum(BUILT_IN_MCP_SERVER_IDS), + }) + .strict(); +export type PluginAppContribution = z.infer; + +/** Extra policy for a skill discovered at `skills//SKILL.md`. */ +export const pluginSkillPolicySchema = z + .object({ + name: z.string().min(1).optional(), + description: z.string().min(1).optional(), + /** Apps that must be active for the skill to be offered or injected. */ + requiredAppIds: z.array(z.string().min(1)).default([]), + }) + .strict(); +export type PluginSkillPolicyEntry = z.infer; + +export const poracodePluginExtensionSchema = z + .object({ + /** Display title. The spec's `name` is an identifier, not a label. */ + title: z.string().min(1).optional(), + category: pluginCategorySchema.default("developer-tools"), + featured: z.boolean().default(false), + /** Starter prompt offered on the plugin's detail page. */ + examplePrompt: z.string().min(1).optional(), + /** + * Set when the MCP server this package launches is maintained by a third + * party rather than the vendor it integrates with. Surfaced in the UI so the + * user knows whose code is about to run. + */ + communityMaintained: z.boolean().default(false), + platforms: z.array(pluginPlatformSchema).optional(), + projectKinds: z.array(pluginProjectKindSchema).optional(), + apps: z.array(pluginAppContributionSchema).default([]), + /** Keyed by skill folder name under `skills/`. */ + skills: z.record(z.string().min(1), pluginSkillPolicySchema).default({}), + }) + .strict(); +export type PoracodePluginExtension = z.infer; + +export const EMPTY_PORACODE_EXTENSION: PoracodePluginExtension = { + category: "developer-tools", + featured: false, + communityMaintained: false, + apps: [], + skills: {}, +}; + +export interface ParsedPoracodeExtension { + extension: PoracodePluginExtension; + diagnostics: PluginDiagnostic[]; +} + +/** + * Reads Poracode's namespace out of a validated manifest. + * + * A malformed block degrades to "no Poracode extras" with a warning; it never + * rejects the plugin, because the spec-defined components are still valid. + */ +export function parsePoracodeExtension(manifest: AgentPluginManifest): ParsedPoracodeExtension { + const raw = manifest.extensions?.[PORACODE_EXTENSION_NAMESPACE]; + if (raw === undefined) return { extension: EMPTY_PORACODE_EXTENSION, diagnostics: [] }; + + const parsed = poracodePluginExtensionSchema.safeParse(raw); + if (!parsed.success) { + return { + extension: EMPTY_PORACODE_EXTENSION, + diagnostics: [ + pluginDiagnostic( + "warning", + "plugin", + "extension-invalid", + `Ignoring '${PORACODE_EXTENSION_NAMESPACE}' extension: ${parsed.error.issues[0]?.message ?? "invalid"}`, + PORACODE_EXTENSION_NAMESPACE, + ), + ], + }; + } + + const diagnostics: PluginDiagnostic[] = []; + const seenAppIds = new Set(); + const apps = parsed.data.apps.filter((app) => { + if (seenAppIds.has(app.id)) { + diagnostics.push( + pluginDiagnostic( + "warning", + "plugin", + "extension-duplicate-app", + `Ignoring duplicate app '${app.id}'`, + app.id, + ), + ); + return false; + } + seenAppIds.add(app.id); + return true; + }); + + for (const [folder, policy] of Object.entries(parsed.data.skills)) { + for (const appId of policy.requiredAppIds) { + if (seenAppIds.has(appId)) continue; + diagnostics.push( + pluginDiagnostic( + "warning", + "plugin", + "extension-unknown-required-app", + `Skill '${folder}' requires app '${appId}', which this plugin does not declare`, + folder, + ), + ); + } + } + + return { extension: { ...parsed.data, apps }, diagnostics }; +} diff --git a/src/shared/plugins/spec/index.ts b/src/shared/plugins/spec/index.ts new file mode 100644 index 000000000..0f15739d0 --- /dev/null +++ b/src/shared/plugins/spec/index.ts @@ -0,0 +1,4 @@ +export * from "./diagnostics"; +export * from "./extensions"; +export * from "./manifest"; +export * from "./mcpConfig"; diff --git a/src/shared/plugins/spec/manifest.ts b/src/shared/plugins/spec/manifest.ts new file mode 100644 index 000000000..e3bffcef0 --- /dev/null +++ b/src/shared/plugins/spec/manifest.ts @@ -0,0 +1,190 @@ +import { z } from "zod"; +import { pluginDiagnostic, type PluginDiagnostic } from "./diagnostics"; + +/** + * Agent Plugins Specification 1.0.0 — root `plugin.json` manifest. + * + * @see https://agent-plugins.org/specification + */ + +export const AGENT_PLUGINS_VERSION = "1.0.0"; +export const AGENT_PLUGINS_SCHEMA_BASE = "https://agent-plugins.org/schemas"; +export const AGENT_PLUGINS_MANIFEST_SCHEMA_URL = `${AGENT_PLUGINS_SCHEMA_BASE}/${AGENT_PLUGINS_VERSION}/plugin.schema.json`; + +/** + * Schema identifiers this build understands. Selection is local by design — the + * spec forbids retrieving a schema while loading a plugin. + */ +const SUPPORTED_MANIFEST_SCHEMA_URLS = new Set([AGENT_PLUGINS_MANIFEST_SCHEMA_URL]); + +/** + * Returns the Agent Plugins version encoded in a published schema identifier, so + * `plugin.json` and `mcp.json` can be checked for a matching version. + */ +export function agentPluginsSchemaVersion(schemaUrl: string): string | undefined { + const match = new RegExp( + `^${AGENT_PLUGINS_SCHEMA_BASE}/([^/]+)/(?:plugin|mcp)\\.schema\\.json$`, + "u", + ).exec(schemaUrl.trim()); + return match?.[1]; +} + +/** + * Plugin name: 1–64 chars of `a-z0-9-.`, alphanumeric at both ends, and no `--` + * or `..` runs. + */ +const PLUGIN_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u; + +export function isValidPluginName(value: string): boolean { + return ( + value.length >= 1 && + value.length <= 64 && + PLUGIN_NAME_PATTERN.test(value) && + !value.includes("--") && + !value.includes("..") + ); +} + +export const pluginNameSchema = z.string().refine(isValidPluginName, { + message: + "Plugin name must be 1-64 characters of a-z, 0-9, '-' or '.', start and end alphanumeric, with no '--' or '..' runs", +}); + +export const pluginAuthorSchema = z + .object({ + name: z.string().min(1).optional(), + email: z.string().min(1).optional(), + url: z.string().min(1).optional(), + }) + .strict(); +export type PluginAuthor = z.infer; + +/** + * Client-specific data lives under reverse-domain namespace keys. Values are + * opaque here; each client parses only its own namespace. + */ +export const pluginExtensionsSchema = z.record(z.string().min(1), z.unknown()); +export type PluginExtensions = z.infer; + +export const agentPluginManifestSchema = z + .object({ + $schema: z.string().min(1), + name: pluginNameSchema, + version: z.string().min(1).optional(), + description: z.string().min(1).optional(), + author: pluginAuthorSchema.optional(), + homepage: z.string().min(1).optional(), + repository: z.string().min(1).optional(), + license: z.string().min(1).optional(), + keywords: z.array(z.string().min(1)).optional(), + extensions: pluginExtensionsSchema.optional(), + }) + .strict(); +export type AgentPluginManifest = z.infer; + +const KNOWN_MANIFEST_KEYS = new Set([ + "$schema", + "name", + "version", + "description", + "author", + "homepage", + "repository", + "license", + "keywords", + "extensions", +]); + +export interface ParsedPluginManifest { + manifest?: AgentPluginManifest; + diagnostics: PluginDiagnostic[]; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Validates a `plugin.json` document against the closed 1.0.0 schema. + * + * Two of the spec's rules are non-fatal and cannot be expressed by a plain + * strict parse, so they are handled before validation: unknown top-level fields + * are reported and ignored, and a non-object `extensions` is reported and + * ignored. Every other schema violation rejects the plugin. + */ +export function parsePluginManifest(value: unknown): ParsedPluginManifest { + const diagnostics: PluginDiagnostic[] = []; + + if (!isPlainObject(value)) { + diagnostics.push( + pluginDiagnostic( + "error", + "plugin", + "manifest-not-object", + "plugin.json is not a JSON object", + ), + ); + return { diagnostics }; + } + + const candidate: Record = {}; + for (const [key, entry] of Object.entries(value)) { + if (!KNOWN_MANIFEST_KEYS.has(key)) { + diagnostics.push( + pluginDiagnostic( + "warning", + "plugin", + "manifest-unknown-field", + `Ignoring unknown top-level field '${key}'`, + key, + ), + ); + continue; + } + if (key === "extensions" && !isPlainObject(entry)) { + diagnostics.push( + pluginDiagnostic( + "warning", + "plugin", + "manifest-extensions-not-object", + "Ignoring 'extensions' because it is not a JSON object", + key, + ), + ); + continue; + } + candidate[key] = entry; + } + + const parsed = agentPluginManifestSchema.safeParse(candidate); + if (!parsed.success) { + for (const issue of parsed.error.issues) { + diagnostics.push( + pluginDiagnostic( + "error", + "plugin", + "manifest-invalid", + issue.message, + issue.path.join(".") || undefined, + ), + ); + } + return { diagnostics }; + } + + const schemaUrl = parsed.data.$schema.trim(); + if (!SUPPORTED_MANIFEST_SCHEMA_URLS.has(schemaUrl)) { + diagnostics.push( + pluginDiagnostic( + "error", + "plugin", + "manifest-schema-unsupported", + `Unsupported $schema '${schemaUrl}'; this build understands ${AGENT_PLUGINS_MANIFEST_SCHEMA_URL}`, + "$schema", + ), + ); + return { diagnostics }; + } + + return { manifest: parsed.data, diagnostics }; +} diff --git a/src/shared/plugins/spec/mcpConfig.ts b/src/shared/plugins/spec/mcpConfig.ts new file mode 100644 index 000000000..be1505d60 --- /dev/null +++ b/src/shared/plugins/spec/mcpConfig.ts @@ -0,0 +1,187 @@ +import { z } from "zod"; +import { pluginDiagnostic, type PluginDiagnostic } from "./diagnostics"; +import { AGENT_PLUGINS_SCHEMA_BASE, AGENT_PLUGINS_VERSION } from "./manifest"; + +/** + * Agent Plugins Specification 1.0.0 — root `mcp.json` document. + * + * The document and each server entry are validated independently: a malformed + * entry is skipped while its siblings still load. + * + * @see https://agent-plugins.org/client-implementers/mcp-runtime + */ + +export const AGENT_PLUGINS_MCP_SCHEMA_URL = `${AGENT_PLUGINS_SCHEMA_BASE}/${AGENT_PLUGINS_VERSION}/mcp.schema.json`; + +const SUPPORTED_MCP_SCHEMA_URLS = new Set([AGENT_PLUGINS_MCP_SCHEMA_URL]); + +/** Transports declared by `type`. Selection is explicit — never negotiated. */ +export const PLUGIN_MCP_TRANSPORT_TYPES = ["stdio", "streamable-http", "sse"] as const; +export type PluginMcpTransportType = (typeof PLUGIN_MCP_TRANSPORT_TYPES)[number]; + +/** + * Remote servers must be absolute HTTPS. Plain HTTP is permitted only for + * loopback, where there is no network to eavesdrop on. + */ +export function isPluginMcpUrlAllowed(value: string): boolean { + let url: URL; + try { + url = new URL(value.trim()); + } catch { + return false; + } + if (url.protocol === "https:") return true; + if (url.protocol !== "http:") return false; + const host = url.hostname.toLowerCase(); + return ( + host === "localhost" || + host.endsWith(".localhost") || + host === "[::1]" || + /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/u.test(host) + ); +} + +const pluginMcpUrlSchema = z.string().refine(isPluginMcpUrlAllowed, { + message: "MCP server url must be an absolute https URL (http is allowed only for localhost)", +}); + +export const pluginMcpStdioEntrySchema = z + .object({ + type: z.literal("stdio"), + command: z.string().min(1), + args: z.array(z.string()).default([]), + env: z.record(z.string().min(1), z.string()).default({}), + cwd: z.string().min(1).optional(), + }) + .strict(); +export type PluginMcpStdioEntry = z.infer; + +export const pluginMcpStreamableHttpEntrySchema = z + .object({ + type: z.literal("streamable-http"), + url: pluginMcpUrlSchema, + headers: z.record(z.string().min(1), z.string()).default({}), + }) + .strict(); +export type PluginMcpStreamableHttpEntry = z.infer; + +/** Legacy HTTP+SSE. Optional per the spec; supported here for plugin compatibility. */ +export const pluginMcpSseEntrySchema = z + .object({ + type: z.literal("sse"), + url: pluginMcpUrlSchema, + headers: z.record(z.string().min(1), z.string()).default({}), + }) + .strict(); +export type PluginMcpSseEntry = z.infer; + +export const pluginMcpEntrySchema = z.discriminatedUnion("type", [ + pluginMcpStdioEntrySchema, + pluginMcpStreamableHttpEntrySchema, + pluginMcpSseEntrySchema, +]); +export type PluginMcpEntry = z.infer; + +export interface PluginMcpServerDeclaration { + /** Key from `mcpServers`, as authored by the plugin. */ + name: string; + entry: PluginMcpEntry; +} + +export interface ParsedPluginMcpConfig { + servers: PluginMcpServerDeclaration[]; + diagnostics: PluginDiagnostic[]; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Validates an `mcp.json` document. + * + * @param manifestSchemaVersion Agent Plugins version declared by `plugin.json`. + * The conformance checklist requires the two documents to agree. + */ +export function parsePluginMcpConfig( + value: unknown, + manifestSchemaVersion: string, +): ParsedPluginMcpConfig { + const diagnostics: PluginDiagnostic[] = []; + + if (!isPlainObject(value)) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "mcp-document-not-object", + "mcp.json is not a JSON object", + ), + ); + return { servers: [], diagnostics }; + } + + const schemaUrl = typeof value.$schema === "string" ? value.$schema.trim() : ""; + if (!SUPPORTED_MCP_SCHEMA_URLS.has(schemaUrl)) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "mcp-schema-unsupported", + `Unsupported mcp.json $schema '${schemaUrl}'; this build understands ${AGENT_PLUGINS_MCP_SCHEMA_URL}`, + "$schema", + ), + ); + return { servers: [], diagnostics }; + } + if (manifestSchemaVersion !== AGENT_PLUGINS_VERSION) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "mcp-schema-version-mismatch", + `mcp.json declares Agent Plugins ${AGENT_PLUGINS_VERSION} but plugin.json declares ${manifestSchemaVersion}`, + "$schema", + ), + ); + return { servers: [], diagnostics }; + } + + if (value.mcpServers === undefined) return { servers: [], diagnostics }; + if (!isPlainObject(value.mcpServers)) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "mcp-servers-not-object", + "mcp.json 'mcpServers' is not a JSON object", + "mcpServers", + ), + ); + return { servers: [], diagnostics }; + } + + const servers: PluginMcpServerDeclaration[] = []; + for (const [name, rawEntry] of Object.entries(value.mcpServers)) { + if (name.trim() === "") { + diagnostics.push( + pluginDiagnostic( + "error", + "mcp-server", + "mcp-entry-invalid", + "Server name must not be empty", + name, + ), + ); + continue; + } + const parsed = pluginMcpEntrySchema.safeParse(rawEntry); + if (!parsed.success) { + const reason = parsed.error.issues[0]?.message ?? "invalid server entry"; + diagnostics.push(pluginDiagnostic("error", "mcp-server", "mcp-entry-invalid", reason, name)); + continue; + } + servers.push({ name, entry: parsed.data }); + } + return { servers, diagnostics }; +} diff --git a/src/shared/poracodePaths.test.ts b/src/shared/poracodePaths.test.ts index 50893fe61..a6d5a9493 100644 --- a/src/shared/poracodePaths.test.ts +++ b/src/shared/poracodePaths.test.ts @@ -26,6 +26,8 @@ describe("poracodePaths", () => { cacheDir: join(baseDir, "cache"), statusCachePath: join(baseDir, "cache", "agent-status-cache.json"), agentPluginsDir: join(baseDir, "agent-plugins"), + pluginsDir: join(baseDir, "plugins"), + pluginDataDir: join(baseDir, "plugin-data"), acpIconsDir: join(baseDir, "cache", "acp-icons"), }); }); diff --git a/src/shared/poracodePaths.ts b/src/shared/poracodePaths.ts index 65d1bc5ed..0e94f6715 100644 --- a/src/shared/poracodePaths.ts +++ b/src/shared/poracodePaths.ts @@ -14,6 +14,15 @@ export interface PoracodePaths { cacheDir: string; statusCachePath: string; agentPluginsDir: string; + /** + * Writable root for Agent Plugins packages the user installs. Each immediate + * child directory containing a `plugin.json` is loaded as one package. + * + * @see https://agent-plugins.org/client-implementers/loading-and-discovery + */ + pluginsDir: string; + /** Parent of the per-plugin `PLUGIN_DATA` directories handed to MCP servers. */ + pluginDataDir: string; /** * Cache directory for ACP registry agent icons. Icons are downloaded once * at install/backfill time, served from disk via the `poracode-local://` @@ -45,6 +54,8 @@ export function resolvePoracodePaths(baseDir: string = resolvePoracodeBaseDir()) cacheDir, statusCachePath: join(cacheDir, "agent-status-cache.json"), agentPluginsDir: join(baseDir, "agent-plugins"), + pluginsDir: join(baseDir, "plugins"), + pluginDataDir: join(baseDir, "plugin-data"), acpIconsDir: join(cacheDir, "acp-icons"), }; } diff --git a/src/shared/sshRemoteScripts.ts b/src/shared/sshRemoteScripts.ts index 6ca345413..a226e98b2 100644 --- a/src/shared/sshRemoteScripts.ts +++ b/src/shared/sshRemoteScripts.ts @@ -208,6 +208,7 @@ else PORACODE_APP_VERSION="$RUNTIME_HASH" \ PORACODE_WSL_HELPERS_DIR="$RUNTIME/wsl-helpers" \ PORACODE_BUNDLED_SKILLS_DIR="$RUNTIME/skills" \ + PORACODE_BUNDLED_PLUGINS_DIR="$RUNTIME/plugins" \ "$NODE" "$RUNTIME/server.cjs" >>"$LOG_FILE" 2>&1 "$PID_FILE" diff --git a/src/supervisor/ipcHandlers.ts b/src/supervisor/ipcHandlers.ts index df22a21ce..2d58f214c 100644 --- a/src/supervisor/ipcHandlers.ts +++ b/src/supervisor/ipcHandlers.ts @@ -23,6 +23,11 @@ export function createSupervisorIpcHandlers(runtime: SupervisorRuntime): Supervi const mcpOAuth = runtime.mcpOAuthService; const externalMcpDiscovery = runtime.externalMcpDiscoveryService; const skills = runtime.skillsService; + const pluginRegistry = runtime.pluginRegistry; + const listPlugins = () => ({ + plugins: pluginRegistry.listPlugins(), + userPluginsDir: pluginRegistry.ensureUserPluginsDir(), + }); return defineSupervisorIpcHandlers({ listWslDistros: () => registry.listWslDistros(), getAgentStatuses: (payload) => registry.getAgentStatuses(payload), @@ -253,5 +258,10 @@ export function createSupervisorIpcHandlers(runtime: SupervisorRuntime): Supervi importSkills: (payload) => skills.import(payload), listSkillMarketplace: (payload) => skills.listMarketplace(payload), installMarketplaceSkill: (payload) => skills.installMarketplace(payload), + listPlugins: () => listPlugins(), + refreshPlugins: () => { + pluginRegistry.refresh(); + return listPlugins(); + }, }); } diff --git a/src/supervisor/plugins/PluginLoader.ts b/src/supervisor/plugins/PluginLoader.ts new file mode 100644 index 000000000..84f8ab016 --- /dev/null +++ b/src/supervisor/plugins/PluginLoader.ts @@ -0,0 +1,282 @@ +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { basename, join } from "node:path"; +import type { LoadedPlugin, PluginSkillRef, PluginSource } from "@/shared/contracts"; +import { + agentPluginsSchemaVersion, + parsePluginManifest, + parsePluginMcpConfig, + parsePoracodeExtension, + pluginDiagnostic, + type PluginDiagnostic, +} from "@/shared/plugins/spec"; +import { relativePolicyPath, resolvePackageBoundary } from "./pathContainment"; + +/** + * Loads an Agent Plugins 1.0.0 package from a directory. + * + * Component discovery is limited to the two fixed locations the specification + * defines — `skills/` and `mcp.json` — and every failure is contained at the + * narrowest applicable boundary: + * + * 1. `plugin.json` invalid or escaping the root → reject the plugin + * 2. component location wrong kind or escaping → disable that component type + * 3. a skill directory escaping or malformed → skip that skill + * 4. an `mcp.json` entry invalid → skip that entry + * + * @see https://agent-plugins.org/client-implementers/loading-and-discovery + */ + +export const PLUGIN_MANIFEST_FILE = "plugin.json"; +export const PLUGIN_MCP_FILE = "mcp.json"; +export const PLUGIN_SKILLS_DIR = "skills"; +const SKILL_FILE = "SKILL.md"; + +export interface PluginLoadResult { + /** Absent when the plugin was rejected at the plugin-level boundary. */ + plugin?: LoadedPlugin; + diagnostics: PluginDiagnostic[]; +} + +type FileKind = "file" | "directory" | "other" | "missing"; + +function fileKind(path: string): FileKind { + try { + const stats = statSync(path); + if (stats.isFile()) return "file"; + if (stats.isDirectory()) return "directory"; + return "other"; + } catch { + return "missing"; + } +} + +function readJsonFile(path: string): { value?: unknown; error?: string } { + try { + return { value: JSON.parse(readFileSync(path, "utf8")) as unknown }; + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } +} + +export function loadPluginFromDirectory(directory: string, source: PluginSource): PluginLoadResult { + const diagnostics: PluginDiagnostic[] = []; + + const root = resolvePackageBoundary(directory); + if (!root) { + diagnostics.push( + pluginDiagnostic( + "error", + "plugin", + "root-unresolvable", + `Cannot resolve plugin directory '${directory}'`, + directory, + ), + ); + return { diagnostics }; + } + + const manifestPath = join(root, PLUGIN_MANIFEST_FILE); + if (fileKind(manifestPath) !== "file") { + diagnostics.push( + pluginDiagnostic( + "error", + "plugin", + "manifest-missing", + `No readable ${PLUGIN_MANIFEST_FILE} in '${root}'`, + manifestPath, + ), + ); + return { diagnostics }; + } + if (relativePolicyPath(root, manifestPath) === undefined) { + diagnostics.push( + pluginDiagnostic( + "error", + "plugin", + "path-escapes-root", + `${PLUGIN_MANIFEST_FILE} resolves outside the package boundary`, + manifestPath, + ), + ); + return { diagnostics }; + } + + const manifestJson = readJsonFile(manifestPath); + if (manifestJson.error !== undefined) { + diagnostics.push( + pluginDiagnostic( + "error", + "plugin", + "manifest-unreadable", + `Cannot read ${PLUGIN_MANIFEST_FILE}: ${manifestJson.error}`, + manifestPath, + ), + ); + return { diagnostics }; + } + + const parsedManifest = parsePluginManifest(manifestJson.value); + diagnostics.push(...parsedManifest.diagnostics); + const manifest = parsedManifest.manifest; + if (!manifest) return { diagnostics }; + + const extension = parsePoracodeExtension(manifest); + diagnostics.push(...extension.diagnostics); + + const skills = discoverSkills(root, diagnostics); + const mcpServers = discoverMcpServers(root, manifest.$schema, diagnostics); + + for (const folder of Object.keys(extension.extension.skills)) { + if (skills.some((skill) => skill.folder === folder)) continue; + diagnostics.push( + pluginDiagnostic( + "warning", + "plugin", + "extension-unknown-skill", + `Extension declares policy for skill '${folder}', which was not discovered under ${PLUGIN_SKILLS_DIR}/`, + folder, + ), + ); + } + + return { + plugin: { + name: manifest.name, + source, + root, + manifest, + poracode: extension.extension, + skills, + mcpServers, + diagnostics, + }, + diagnostics, + }; +} + +/** Immediate child directories of `skills/` that contain a `SKILL.md`. No recursion. */ +function discoverSkills(root: string, diagnostics: PluginDiagnostic[]): PluginSkillRef[] { + const skillsDir = join(root, PLUGIN_SKILLS_DIR); + const kind = fileKind(skillsDir); + if (kind === "missing") return []; + if (kind !== "directory") { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "skills-location-wrong-kind", + `${PLUGIN_SKILLS_DIR} exists but is not a directory; skills are disabled for this plugin`, + skillsDir, + ), + ); + return []; + } + if (relativePolicyPath(root, skillsDir) === undefined) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "path-escapes-root", + `${PLUGIN_SKILLS_DIR} resolves outside the package boundary; skills are disabled for this plugin`, + skillsDir, + ), + ); + return []; + } + + let entries: string[]; + try { + entries = readdirSync(skillsDir); + } catch (error) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "skills-unreadable", + `Cannot read ${PLUGIN_SKILLS_DIR}: ${error instanceof Error ? error.message : String(error)}`, + skillsDir, + ), + ); + return []; + } + + const skills: PluginSkillRef[] = []; + for (const entry of entries.sort()) { + const skillDir = join(skillsDir, entry); + if (fileKind(skillDir) !== "directory") continue; + const skillFile = join(skillDir, SKILL_FILE); + if (fileKind(skillFile) !== "file") continue; + if ( + relativePolicyPath(root, skillDir) === undefined || + relativePolicyPath(root, skillFile) === undefined + ) { + diagnostics.push( + pluginDiagnostic( + "error", + "skill", + "path-escapes-root", + `Skipping skill '${entry}' because it resolves outside the package boundary`, + entry, + ), + ); + continue; + } + skills.push({ folder: basename(skillDir), path: skillDir }); + } + return skills; +} + +function discoverMcpServers( + root: string, + manifestSchemaUrl: string, + diagnostics: PluginDiagnostic[], +): LoadedPlugin["mcpServers"] { + const mcpPath = join(root, PLUGIN_MCP_FILE); + const kind = fileKind(mcpPath); + if (kind === "missing") return []; + if (kind !== "file") { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "mcp-location-wrong-kind", + `${PLUGIN_MCP_FILE} exists but is not a file; MCP servers are disabled for this plugin`, + mcpPath, + ), + ); + return []; + } + if (relativePolicyPath(root, mcpPath) === undefined) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "path-escapes-root", + `${PLUGIN_MCP_FILE} resolves outside the package boundary; MCP servers are disabled for this plugin`, + mcpPath, + ), + ); + return []; + } + + const document = readJsonFile(mcpPath); + if (document.error !== undefined) { + diagnostics.push( + pluginDiagnostic( + "error", + "component-type", + "mcp-unreadable", + `Cannot read ${PLUGIN_MCP_FILE}: ${document.error}`, + mcpPath, + ), + ); + return []; + } + + const parsed = parsePluginMcpConfig( + document.value, + agentPluginsSchemaVersion(manifestSchemaUrl) ?? "", + ); + diagnostics.push(...parsed.diagnostics); + return parsed.servers; +} diff --git a/src/supervisor/plugins/PluginRegistry.ts b/src/supervisor/plugins/PluginRegistry.ts new file mode 100644 index 000000000..8b07e32b2 --- /dev/null +++ b/src/supervisor/plugins/PluginRegistry.ts @@ -0,0 +1,135 @@ +import { mkdirSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; +import type { LoadedPlugin, PluginSource } from "@/shared/contracts"; +import { formatPluginDiagnostic, type PluginDiagnostic } from "@/shared/plugins/spec"; +import { loadPluginFromDirectory, PLUGIN_MANIFEST_FILE } from "./PluginLoader"; + +/** + * Discovers Agent Plugins packages from the roots Poracode scans. + * + * Bundled packages ship with the app; user packages are whatever the user drops + * into the plugin directory. Bundled wins on a name collision, so a third-party + * package cannot shadow a first-party one. + */ + +export interface PluginRegistryOptions { + /** Read-only packages shipped with the app. */ + bundledPluginsDir: () => string | undefined; + /** Writable directory the user can drop packages into. */ + userPluginsDir: () => string; + onDiagnostics?: (pluginDirectory: string, lines: readonly string[]) => void; +} + +interface ScanRoot { + directory: string; + source: PluginSource; +} + +/** Cheap change signal: the set of candidate directories plus their mtimes. */ +function rootFingerprint(directory: string): string { + let entries: string[]; + try { + entries = readdirSync(directory).sort(); + } catch { + return `${directory}\0missing`; + } + const parts = entries.map((entry) => { + try { + return `${entry}:${statSync(join(directory, entry)).mtimeMs}`; + } catch { + return `${entry}:?`; + } + }); + return `${directory}\0${parts.join("\0")}`; +} + +export class PluginRegistry { + private cache: { fingerprint: string; plugins: LoadedPlugin[] } | undefined; + + constructor(private readonly options: PluginRegistryOptions) {} + + /** Absolute path of the writable plugin directory, created on demand. */ + ensureUserPluginsDir(): string { + const directory = this.options.userPluginsDir(); + try { + mkdirSync(directory, { recursive: true }); + } catch (error) { + console.warn(`[plugins] cannot create user plugin directory '${directory}':`, error); + } + return directory; + } + + /** Drops the cache so the next read rescans. */ + refresh(): void { + this.cache = undefined; + } + + listPlugins(): LoadedPlugin[] { + const roots = this.scanRoots(); + const fingerprint = roots.map((root) => rootFingerprint(root.directory)).join("\n"); + if (this.cache?.fingerprint === fingerprint) return this.cache.plugins; + const plugins = this.scan(roots); + this.cache = { fingerprint, plugins }; + return plugins; + } + + getPlugin(name: string): LoadedPlugin | undefined { + return this.listPlugins().find((plugin) => plugin.name === name); + } + + private scanRoots(): ScanRoot[] { + const roots: ScanRoot[] = []; + const bundled = this.options.bundledPluginsDir(); + if (bundled) roots.push({ directory: bundled, source: "bundled" }); + roots.push({ directory: this.options.userPluginsDir(), source: "user" }); + return roots; + } + + private scan(roots: readonly ScanRoot[]): LoadedPlugin[] { + const byName = new Map(); + for (const root of roots) { + for (const directory of candidateDirectories(root.directory)) { + const result = loadPluginFromDirectory(directory, root.source); + this.report(directory, result.diagnostics); + const plugin = result.plugin; + if (!plugin) continue; + const existing = byName.get(plugin.name); + if (existing) { + console.warn( + `[plugins] ignoring '${directory}': plugin name '${plugin.name}' is already provided by '${existing.root}'`, + ); + continue; + } + byName.set(plugin.name, plugin); + } + } + return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)); + } + + private report(directory: string, diagnostics: readonly PluginDiagnostic[]): void { + if (diagnostics.length === 0) return; + const lines = diagnostics.map(formatPluginDiagnostic); + for (const line of lines) console.warn(`[plugins] ${directory}: ${line}`); + this.options.onDiagnostics?.(directory, lines); + } +} + +/** Immediate child directories that contain a `plugin.json`. */ +function candidateDirectories(root: string): string[] { + let entries: string[]; + try { + entries = readdirSync(root).sort(); + } catch { + return []; + } + return entries.flatMap((entry) => { + const directory = join(root, entry); + try { + if (!statSync(directory).isDirectory()) return []; + if (!statSync(join(directory, PLUGIN_MANIFEST_FILE)).isFile()) return []; + } catch { + return []; + } + return [directory]; + }); +} diff --git a/src/supervisor/plugins/conformance.test.ts b/src/supervisor/plugins/conformance.test.ts new file mode 100644 index 000000000..959986c2e --- /dev/null +++ b/src/supervisor/plugins/conformance.test.ts @@ -0,0 +1,536 @@ +import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { InstalledPlugins } from "@/shared/contracts"; +import { + AGENT_PLUGINS_MANIFEST_SCHEMA_URL, + AGENT_PLUGINS_MCP_SCHEMA_URL, + isPluginMcpUrlAllowed, + isValidPluginName, +} from "@/shared/plugins/spec"; +import { loadPluginFromDirectory } from "./PluginLoader"; +import { PluginRegistry } from "./PluginRegistry"; +import { resolvePluginMcpServers } from "./pluginMcpRuntime"; + +/** + * Client conformance for Agent Plugins Specification 1.0.0. + * + * Each case maps to a line in the published client checklist and the + * failure-boundary table. + * + * @see https://agent-plugins.org/client-implementers/conformance + */ + +let root: string; + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), "poracode-agent-plugins-")); +}); + +afterEach(async () => { + await rm(root, { recursive: true, force: true }); +}); + +async function writePackage( + name: string, + files: { + manifest?: unknown; + manifestText?: string; + skills?: Record; + mcp?: unknown; + mcpText?: string; + }, +): Promise { + const dir = join(root, name); + await mkdir(dir, { recursive: true }); + if (files.manifestText !== undefined) { + await writeFile(join(dir, "plugin.json"), files.manifestText, "utf8"); + } else if (files.manifest !== undefined) { + await writeFile(join(dir, "plugin.json"), JSON.stringify(files.manifest), "utf8"); + } + for (const [folder, body] of Object.entries(files.skills ?? {})) { + const skillDir = join(dir, "skills", folder); + await mkdir(skillDir, { recursive: true }); + if (body !== null) await writeFile(join(skillDir, "SKILL.md"), body, "utf8"); + } + if (files.mcpText !== undefined) { + await writeFile(join(dir, "mcp.json"), files.mcpText, "utf8"); + } else if (files.mcp !== undefined) { + await writeFile(join(dir, "mcp.json"), JSON.stringify(files.mcp), "utf8"); + } + return dir; +} + +const manifest = (name: string, extra: Record = {}) => ({ + $schema: AGENT_PLUGINS_MANIFEST_SCHEMA_URL, + name, + ...extra, +}); + +const skillBody = (name: string) => + `---\nname: ${JSON.stringify(name)}\ndescription: "${name} description"\n---\n\n# ${name}\n`; + +const codes = (diagnostics: readonly { code: string }[]) => diagnostics.map((d) => d.code); + +describe("plugin name validation", () => { + it("accepts and rejects names per the specification", () => { + expect(isValidPluginName("a")).toBe(true); + expect(isValidPluginName("my-plugin.v2")).toBe(true); + expect(isValidPluginName("")).toBe(false); + expect(isValidPluginName("A")).toBe(false); + expect(isValidPluginName("-lead")).toBe(false); + expect(isValidPluginName("trail-")).toBe(false); + expect(isValidPluginName("double--hyphen")).toBe(false); + expect(isValidPluginName("double..period")).toBe(false); + expect(isValidPluginName("x".repeat(65))).toBe(false); + expect(isValidPluginName("x".repeat(64))).toBe(true); + }); +}); + +describe("manifest loading", () => { + it("loads a minimal package with only the two required fields", async () => { + const dir = await writePackage("minimal", { manifest: manifest("minimal") }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin).toMatchObject({ name: "minimal", skills: [], mcpServers: [] }); + expect(result.diagnostics).toEqual([]); + }); + + it("reports and ignores unknown top-level fields without rejecting the plugin", async () => { + const dir = await writePackage("unknown-field", { + manifest: manifest("unknown-field", { totallyUnknown: { a: 1 } }), + }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin?.name).toBe("unknown-field"); + expect(codes(result.diagnostics)).toEqual(["manifest-unknown-field"]); + }); + + it("reports and ignores a non-object extensions field", async () => { + const dir = await writePackage("bad-extensions", { + manifest: manifest("bad-extensions", { extensions: "nope" }), + }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin?.manifest.extensions).toBeUndefined(); + expect(codes(result.diagnostics)).toEqual(["manifest-extensions-not-object"]); + }); + + it("rejects a plugin whose name violates the specification", async () => { + const dir = await writePackage("bad-name", { manifest: manifest("Bad_Name") }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin).toBeUndefined(); + expect(codes(result.diagnostics)).toContain("manifest-invalid"); + }); + + it("rejects an unknown $schema instead of retrieving it", async () => { + const dir = await writePackage("future", { + manifest: { + $schema: "https://agent-plugins.org/schemas/9.9.9/plugin.schema.json", + name: "f", + }, + }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin).toBeUndefined(); + expect(codes(result.diagnostics)).toEqual(["manifest-schema-unsupported"]); + }); + + it("rejects a package with unreadable JSON", async () => { + const dir = await writePackage("broken", { manifestText: "{ not json" }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin).toBeUndefined(); + expect(codes(result.diagnostics)).toEqual(["manifest-unreadable"]); + }); + + it("rejects a directory with no manifest", async () => { + const dir = await writePackage("empty", {}); + expect(loadPluginFromDirectory(dir, "bundled").plugin).toBeUndefined(); + }); +}); + +describe("component discovery", () => { + it("discovers immediate skill directories and does not recurse", async () => { + const dir = await writePackage("skills", { + manifest: manifest("skills"), + skills: { alpha: skillBody("alpha"), beta: skillBody("beta") }, + }); + await mkdir(join(dir, "skills", "alpha", "nested"), { recursive: true }); + await writeFile( + join(dir, "skills", "alpha", "nested", "SKILL.md"), + skillBody("nested"), + "utf8", + ); + + const result = loadPluginFromDirectory(dir, "bundled"); + expect(result.plugin?.skills.map((skill) => skill.folder)).toEqual(["alpha", "beta"]); + }); + + it("skips a directory that has no SKILL.md without reporting an error", async () => { + const dir = await writePackage("partial", { + manifest: manifest("partial"), + skills: { good: skillBody("good"), empty: null }, + }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin?.skills.map((skill) => skill.folder)).toEqual(["good"]); + expect(result.diagnostics).toEqual([]); + }); + + it("treats missing component locations as a valid absence", async () => { + const dir = await writePackage("bare", { manifest: manifest("bare") }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.diagnostics).toEqual([]); + expect(result.plugin).toBeDefined(); + }); + + it("disables only the component type when a location has the wrong filesystem kind", async () => { + const dir = await writePackage("wrong-kind", { manifest: manifest("wrong-kind") }); + await writeFile(join(dir, "skills"), "not a directory", "utf8"); + await mkdir(join(dir, "mcp.json"), { recursive: true }); + + const result = loadPluginFromDirectory(dir, "bundled"); + expect(result.plugin).toBeDefined(); + expect(codes(result.diagnostics)).toEqual([ + "skills-location-wrong-kind", + "mcp-location-wrong-kind", + ]); + }); +}); + +describe("package boundary", () => { + it("skips a skill that resolves outside the package boundary", async ({ skip }) => { + const outside = join(root, "outside-skill"); + await mkdir(outside, { recursive: true }); + await writeFile(join(outside, "SKILL.md"), skillBody("escaped"), "utf8"); + const dir = await writePackage("escaping", { + manifest: manifest("escaping"), + skills: { kept: skillBody("kept") }, + }); + try { + await symlink(outside, join(dir, "skills", "escaped"), "junction"); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (["EACCES", "ENOSYS", "ENOTSUP", "EPERM", "UNKNOWN"].includes(code ?? "")) { + skip(); + return; + } + throw error; + } + + const result = loadPluginFromDirectory(dir, "bundled"); + expect(result.plugin?.skills.map((skill) => skill.folder)).toEqual(["kept"]); + expect(codes(result.diagnostics)).toEqual(["path-escapes-root"]); + }); +}); + +describe("mcp.json", () => { + const mcpDoc = (servers: Record) => ({ + $schema: AGENT_PLUGINS_MCP_SCHEMA_URL, + mcpServers: servers, + }); + + it("validates the document and each entry independently", async () => { + const dir = await writePackage("mixed-servers", { + manifest: manifest("mixed-servers"), + mcp: mcpDoc({ + good: { type: "stdio", command: "server" }, + bad: { type: "stdio" }, + remote: { type: "streamable-http", url: "https://tools.example.com/mcp" }, + }), + }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin?.mcpServers.map((server) => server.name)).toEqual(["good", "remote"]); + expect(codes(result.diagnostics)).toEqual(["mcp-entry-invalid"]); + }); + + it("accepts the legacy sse transport", async () => { + const dir = await writePackage("legacy", { + manifest: manifest("legacy"), + mcp: mcpDoc({ old: { type: "sse", url: "https://tools.example.com/sse" } }), + }); + expect(loadPluginFromDirectory(dir, "bundled").plugin?.mcpServers[0]?.entry.type).toBe("sse"); + }); + + it("disables MCP servers when the document schema is unsupported", async () => { + const dir = await writePackage("bad-mcp-schema", { + manifest: manifest("bad-mcp-schema"), + mcp: { $schema: "https://example.com/other.json", mcpServers: {} }, + }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin?.mcpServers).toEqual([]); + expect(codes(result.diagnostics)).toEqual(["mcp-schema-unsupported"]); + }); + + it("requires absolute https urls, allowing http only for loopback", () => { + expect(isPluginMcpUrlAllowed("https://tools.example.com/mcp")).toBe(true); + expect(isPluginMcpUrlAllowed("http://localhost:3000/mcp")).toBe(true); + expect(isPluginMcpUrlAllowed("http://127.0.0.1:3000/mcp")).toBe(true); + expect(isPluginMcpUrlAllowed("http://[::1]:3000/mcp")).toBe(true); + expect(isPluginMcpUrlAllowed("http://tools.example.com/mcp")).toBe(false); + expect(isPluginMcpUrlAllowed("/relative")).toBe(false); + }); +}); + +describe("mcp runtime", () => { + const installed = (name: string): InstalledPlugins => ({ + [name]: { + version: "1.0.0", + enabled: true, + disabledSkillIds: [], + disabledAppIds: [], + disabledMcpServerNames: [], + }, + }); + + async function loadWithServers(name: string, servers: Record) { + const dir = await writePackage(name, { + manifest: manifest(name), + mcp: { $schema: AGENT_PLUGINS_MCP_SCHEMA_URL, mcpServers: servers }, + }); + const plugin = loadPluginFromDirectory(dir, "bundled").plugin; + if (!plugin) throw new Error("plugin failed to load"); + return plugin; + } + + it("expands placeholders only in args, env values, and cwd", async () => { + const plugin = await loadWithServers("expansion", { + main: { + type: "stdio", + command: "server", + args: ["--data", "${PLUGIN_DATA}", "--root", "${PLUGIN_ROOT}"], + env: { DATA: "${PLUGIN_DATA}", LITERAL: "no placeholder" }, + cwd: "${PLUGIN_ROOT}", + }, + }); + const pluginDataRoot = join(root, "plugin-data"); + const { servers } = resolvePluginMcpServers([plugin], installed("expansion"), { + pluginDataRoot, + }); + const transport = servers[0]?.transport; + + expect(servers[0]?.name).toBe("expansion.main"); + expect(transport).toMatchObject({ + type: "stdio", + command: "server", + args: ["--data", join(pluginDataRoot, "expansion"), "--root", plugin.root], + cwd: plugin.root, + }); + expect(transport?.type === "stdio" && transport.env).toEqual({ + DATA: join(pluginDataRoot, "expansion"), + LITERAL: "no placeholder", + PLUGIN_ROOT: plugin.root, + PLUGIN_DATA: join(pluginDataRoot, "expansion"), + }); + }); + + it("does not let configured env override PLUGIN_ROOT or PLUGIN_DATA", async () => { + const plugin = await loadWithServers("env-precedence", { + main: { + type: "stdio", + command: "server", + env: { PLUGIN_ROOT: "/hijacked", PLUGIN_DATA: "/hijacked" }, + }, + }); + const pluginDataRoot = join(root, "plugin-data"); + const { servers } = resolvePluginMcpServers([plugin], installed("env-precedence"), { + pluginDataRoot, + }); + const transport = servers[0]?.transport; + + expect(transport?.type === "stdio" && transport.env).toMatchObject({ + PLUGIN_ROOT: plugin.root, + PLUGIN_DATA: join(pluginDataRoot, "env-precedence"), + }); + }); + + it("does not expand placeholders in remote headers", async () => { + const plugin = await loadWithServers("headers", { + remote: { + type: "streamable-http", + url: "https://tools.example.com/mcp", + headers: { "X-Root": "${PLUGIN_ROOT}" }, + }, + }); + const { servers } = resolvePluginMcpServers([plugin], installed("headers"), { + pluginDataRoot: join(root, "plugin-data"), + }); + + expect(servers[0]?.transport).toMatchObject({ + type: "http", + headers: { "X-Root": "${PLUGIN_ROOT}" }, + }); + }); + + it("resolves a './' command against the plugin root and rejects other paths", async () => { + const relative = await loadWithServers("relative-cmd", { + main: { type: "stdio", command: "./bin/server" }, + }); + const escaping = await loadWithServers("escaping-cmd", { + main: { type: "stdio", command: "../outside/server" }, + }); + const context = { pluginDataRoot: join(root, "plugin-data") }; + + expect( + resolvePluginMcpServers([relative], installed("relative-cmd"), context).servers[0]?.transport, + ).toMatchObject({ command: join(relative.root, "bin", "server") }); + + const rejected = resolvePluginMcpServers([escaping], installed("escaping-cmd"), context); + expect(rejected.servers).toEqual([]); + expect(codes(rejected.diagnostics)).toEqual(["mcp-entry-unresolvable"]); + }); + + it("rejects a cwd that escapes the package boundary", async () => { + const plugin = await loadWithServers("bad-cwd", { + main: { type: "stdio", command: "server", cwd: "../outside" }, + }); + const result = resolvePluginMcpServers([plugin], installed("bad-cwd"), { + pluginDataRoot: join(root, "plugin-data"), + }); + + expect(result.servers).toEqual([]); + expect(codes(result.diagnostics)).toEqual(["mcp-entry-unresolvable"]); + }); + + it("keeps sibling servers when one entry cannot be resolved", async () => { + const plugin = await loadWithServers("isolation", { + broken: { type: "stdio", command: "/absolute/server" }, + working: { type: "stdio", command: "server" }, + }); + const result = resolvePluginMcpServers([plugin], installed("isolation"), { + pluginDataRoot: join(root, "plugin-data"), + }); + + expect(result.servers.map((server) => server.name)).toEqual(["isolation.working"]); + expect(codes(result.diagnostics)).toEqual(["mcp-entry-unresolvable"]); + }); + + it("skips servers a plugin is not installed or enabled for", async () => { + const plugin = await loadWithServers("gated", { + main: { type: "stdio", command: "server" }, + }); + const context = { pluginDataRoot: join(root, "plugin-data") }; + + expect(resolvePluginMcpServers([plugin], {}, context).servers).toEqual([]); + expect( + resolvePluginMcpServers( + [plugin], + { gated: { ...installed("gated").gated!, enabled: false } }, + context, + ).servers, + ).toEqual([]); + expect( + resolvePluginMcpServers( + [plugin], + { gated: { ...installed("gated").gated!, disabledMcpServerNames: ["main"] } }, + context, + ).servers, + ).toEqual([]); + }); + + it("skips a server whose namespaced name is not usable", async () => { + const plugin = await loadWithServers("naming", { + // Composes to "naming.my server", which is not a usable MCP server name. + "my server": { type: "stdio", command: "server" }, + }); + const result = resolvePluginMcpServers([plugin], installed("naming"), { + pluginDataRoot: join(root, "plugin-data"), + }); + + expect(result.servers).toEqual([]); + expect(codes(result.diagnostics)).toEqual(["mcp-name-unusable"]); + }); + + it("rejects an empty server name in mcp.json", async () => { + const dir = await writePackage("empty-name", { + manifest: manifest("empty-name"), + mcp: { + $schema: AGENT_PLUGINS_MCP_SCHEMA_URL, + mcpServers: { "": { type: "stdio", command: "server" } }, + }, + }); + const result = loadPluginFromDirectory(dir, "bundled"); + + expect(result.plugin?.mcpServers).toEqual([]); + expect(codes(result.diagnostics)).toEqual(["mcp-entry-invalid"]); + }); +}); + +describe("registry", () => { + it("prefers a bundled package over a user package with the same name", async () => { + const bundledDir = join(root, "bundled"); + const userDir = join(root, "user"); + await mkdir(join(bundledDir, "dup"), { recursive: true }); + await mkdir(join(userDir, "dup"), { recursive: true }); + await writeFile( + join(bundledDir, "dup", "plugin.json"), + JSON.stringify(manifest("dup", { version: "1.0.0" })), + "utf8", + ); + await writeFile( + join(userDir, "dup", "plugin.json"), + JSON.stringify(manifest("dup", { version: "2.0.0" })), + "utf8", + ); + + const registry = new PluginRegistry({ + bundledPluginsDir: () => bundledDir, + userPluginsDir: () => userDir, + }); + const plugins = registry.listPlugins(); + + expect(plugins).toHaveLength(1); + expect(plugins[0]).toMatchObject({ source: "bundled", manifest: { version: "1.0.0" } }); + }); + + it("ignores directories without a manifest and rescans on refresh", async () => { + const userDir = join(root, "user"); + await mkdir(join(userDir, "not-a-plugin"), { recursive: true }); + const registry = new PluginRegistry({ + bundledPluginsDir: () => undefined, + userPluginsDir: () => userDir, + }); + expect(registry.listPlugins()).toEqual([]); + + await mkdir(join(userDir, "added"), { recursive: true }); + await writeFile( + join(userDir, "added", "plugin.json"), + JSON.stringify(manifest("added")), + "utf8", + ); + registry.refresh(); + + expect(registry.listPlugins().map((plugin) => plugin.name)).toEqual(["added"]); + }); +}); + +describe("shipped packages", () => { + it("loads every package in resources/plugins", () => { + const shippedDir = join(process.cwd(), "resources", "plugins"); + const shipped = [ + "browser-tools", + "chrome-tools", + "computer-use", + "github", + "outlook", + "subagent-delegation", + ]; + for (const name of shipped) { + const result = loadPluginFromDirectory(join(shippedDir, name), "bundled"); + expect(result.diagnostics, `${name}: ${JSON.stringify(result.diagnostics)}`).toEqual([]); + expect(result.plugin?.name).toBe(name); + expect(result.plugin?.skills.length).toBeGreaterThan(0); + // Each package contributes at least one runnable server, either a + // Poracode-owned app or an mcp.json entry. + expect( + (result.plugin?.poracode.apps.length ?? 0) + (result.plugin?.mcpServers.length ?? 0), + ).toBeGreaterThan(0); + } + }); +}); diff --git a/src/supervisor/plugins/index.ts b/src/supervisor/plugins/index.ts new file mode 100644 index 000000000..ab9804213 --- /dev/null +++ b/src/supervisor/plugins/index.ts @@ -0,0 +1,4 @@ +export * from "./PluginLoader"; +export * from "./PluginRegistry"; +export * from "./pathContainment"; +export * from "./pluginMcpRuntime"; diff --git a/src/supervisor/plugins/pathContainment.ts b/src/supervisor/plugins/pathContainment.ts new file mode 100644 index 000000000..21ea54f67 --- /dev/null +++ b/src/supervisor/plugins/pathContainment.ts @@ -0,0 +1,87 @@ +import { realpathSync } from "node:fs"; +import { isAbsolute, relative, resolve } from "node:path"; + +/** + * Filesystem containment used to enforce the Agent Plugins package boundary. + * + * The specification requires that every package file remain within the + * filesystem-resolved root *after* resolving symlinks, junctions, and reparse + * points — so containment is decided on real paths, not lexical ones. + * + * @see https://agent-plugins.org/client-implementers/loading-and-discovery + */ + +/** Strips Windows `\\?\` / `\\?\UNC\` prefixes so paths compare consistently. */ +export function normalizeWindowsNamespacePath(path: string): string { + let normalized = path; + if (normalized.startsWith("\\\\?\\UNC\\")) normalized = `\\\\${normalized.slice(8)}`; + else if (normalized.startsWith("\\\\?\\")) normalized = normalized.slice(4); + else if (normalized.startsWith("//?/UNC/")) normalized = `//${normalized.slice(8)}`; + else if (normalized.startsWith("//?/")) normalized = normalized.slice(4); + return normalized; +} + +/** Lexical containment. Returns the relative path when `target` is under `root`. */ +export function relativePathInside(root: string, target: string): string | undefined { + const candidate = relative(root, target); + if (!candidate || isAbsolute(candidate) || candidate.split(/[\\/]/u)[0] === "..") { + return undefined; + } + return candidate; +} + +/** + * Real-path containment. Returns the relative path when `target` resolves inside + * `root`, or `undefined` when it escapes. + * + * Falls back to normalized lexical comparison when a path does not exist yet, + * which is what callers validating a *configured* path (not an existing file) + * need. + */ +export function relativePolicyPath(root: string, target: string): string | undefined { + const normalizedRoot = resolve(normalizeWindowsNamespacePath(root)); + const normalizedTarget = resolve(normalizeWindowsNamespacePath(target)); + try { + return relativePathInside( + resolve(realpathSync.native(root)), + resolve(realpathSync.native(target)), + ); + } catch { + // Fall through to the normalized aliases for non-existent paths. + } + const direct = relativePathInside(normalizedRoot, normalizedTarget); + if (direct) return direct; + try { + return relativePathInside( + resolve(realpathSync.native(normalizedRoot)), + resolve(realpathSync.native(normalizedTarget)), + ); + } catch { + return undefined; + } +} + +/** True when `target` is the same path as `root` or resolves inside it. */ +export function isPathInsideRoot(root: string, target: string): boolean { + if (relativePolicyPath(root, target) !== undefined) return true; + try { + return resolve(realpathSync.native(root)) === resolve(realpathSync.native(target)); + } catch { + return ( + resolve(normalizeWindowsNamespacePath(root)) === + resolve(normalizeWindowsNamespacePath(target)) + ); + } +} + +/** + * Resolves the filesystem-resolved package boundary for a plugin directory. + * Returns `undefined` when the directory cannot be resolved. + */ +export function resolvePackageBoundary(root: string): string | undefined { + try { + return resolve(realpathSync.native(root)); + } catch { + return undefined; + } +} diff --git a/src/supervisor/plugins/pluginMcpRuntime.ts b/src/supervisor/plugins/pluginMcpRuntime.ts new file mode 100644 index 000000000..a5333dff9 --- /dev/null +++ b/src/supervisor/plugins/pluginMcpRuntime.ts @@ -0,0 +1,244 @@ +import { mkdirSync } from "node:fs"; +import { isAbsolute, join, resolve } from "node:path"; +import type { InstalledPlugins, LoadedPlugin, McpServer, McpTransport } from "@/shared/contracts"; +import { DEFAULT_MCP_SERVER_TIMEOUT_MS, isValidMcpServerName } from "@/shared/contracts"; +import { + pluginDiagnostic, + type PluginDiagnostic, + type PluginMcpEntry, + type PluginMcpStdioEntry, +} from "@/shared/plugins/spec"; +import { relativePathInside } from "./pathContainment"; + +/** + * Turns `mcp.json` declarations into the provider-agnostic `McpServer` records + * the launch pipeline already understands. + * + * This is what makes plugins work across every provider Poracode supports: + * Poracode is the Agent Plugins client, and once a declaration becomes an + * `McpServer` the existing per-provider translators in + * `src/supervisor/agents/userMcp/translate.ts` emit native config for Claude, + * Codex, Gemini, OpenCode, and ACP. The provider never needs to know the Agent + * Plugins specification exists. + * + * Known limitation: for remote servers the provider CLI owns the HTTP client, so + * the specification's "do not forward configured headers across origins on + * redirect" rule can only be enforced in Poracode's own probe path + * (`src/supervisor/mcp/probeMcpServer.ts`), not inside the provider process. + * + * @see https://agent-plugins.org/client-implementers/mcp-runtime + */ + +export interface PluginMcpRuntimeContext { + /** Parent directory holding one persistent data directory per plugin. */ + pluginDataRoot: string; +} + +export interface ResolvedPluginMcpServers { + servers: McpServer[]; + diagnostics: PluginDiagnostic[]; +} + +/** `PLUGIN_DATA` for a plugin. Created before launch, as the spec requires. */ +export function pluginDataDirectory(pluginDataRoot: string, pluginName: string): string { + return join(pluginDataRoot, pluginName); +} + +function ensureDirectory(path: string): boolean { + try { + mkdirSync(path, { recursive: true }); + return true; + } catch (error) { + console.warn(`[plugins] cannot create plugin data directory '${path}':`, error); + return false; + } +} + +/** + * Single, non-recursive textual replacement of the two plugin placeholders. + * One `replace` pass means substituted text is never rescanned. + */ +function expandPlaceholders(value: string, root: string, data: string): string { + return value.replace(/\$\{(PLUGIN_ROOT|PLUGIN_DATA)\}/gu, (_match, key: string) => + key === "PLUGIN_ROOT" ? root : data, + ); +} + +/** + * Resolves a stdio `command`. The spec allows a bare executable name, resolved + * by platform search rules, or a `./`-relative path resolved against the plugin + * root with containment enforced. Anything else is rejected. + */ +function resolveStdioCommand( + command: string, + root: string, +): { command: string } | { error: string } { + if (command.startsWith("./") || command.startsWith(".\\")) { + const target = resolve(root, command); + if (relativePathInside(root, target) === undefined) { + return { error: `command '${command}' resolves outside the package boundary` }; + } + return { command: target }; + } + if (isAbsolute(command) || command.includes("/") || command.includes("\\")) { + return { + error: `command '${command}' must be a bare executable name or a './'-relative path`, + }; + } + return { command }; +} + +function resolveStdioCwd( + cwd: string | undefined, + root: string, + data: string, +): { cwd: string } | { error: string } { + if (cwd === undefined) return { cwd: root }; + const expanded = expandPlaceholders(cwd, root, data); + // `${PLUGIN_DATA}` legitimately points outside the package, so it is the one + // absolute working directory the spec permits. + if (expanded === data || relativePathInside(data, expanded) !== undefined) { + return { cwd: expanded }; + } + const target = isAbsolute(expanded) ? resolve(expanded) : resolve(root, expanded); + // The package root itself is inside the boundary; `relativePathInside` returns + // an empty relative path for it, which is not a containment failure. + if (target !== resolve(root) && relativePathInside(root, target) === undefined) { + return { error: `cwd '${cwd}' resolves outside the package boundary` }; + } + return { cwd: target }; +} + +function buildTransport( + entry: PluginMcpEntry, + root: string, + data: string, +): { transport: McpTransport } | { error: string } { + if (entry.type !== "stdio") { + // Headers are intentionally not placeholder-expanded: the spec limits + // expansion to args, env values, and cwd. + return { + transport: { + type: entry.type === "streamable-http" ? "http" : "sse", + url: entry.url.trim(), + headers: { ...entry.headers }, + }, + }; + } + + const stdio: PluginMcpStdioEntry = entry; + const command = resolveStdioCommand(stdio.command, root); + if ("error" in command) return command; + const cwd = resolveStdioCwd(stdio.cwd, root, data); + if ("error" in cwd) return cwd; + + const env: Record = {}; + for (const [key, value] of Object.entries(stdio.env)) { + env[key] = expandPlaceholders(value, root, data); + } + // Set last so a plugin cannot override the values it is meant to read. + env.PLUGIN_ROOT = root; + env.PLUGIN_DATA = data; + + return { + transport: { + type: "stdio", + command: command.command, + args: stdio.args.map((arg) => expandPlaceholders(arg, root, data)), + env, + cwd: cwd.cwd, + }, + }; +} + +/** Stable id so per-server settings survive a rescan. */ +export function pluginMcpServerId(pluginName: string, serverName: string): string { + return `plugin:${pluginName}:${serverName}`; +} + +/** Provider-visible name. Namespaced by plugin so two plugins cannot collide. */ +export function pluginMcpServerName(pluginName: string, serverName: string): string { + return `${pluginName}.${serverName}`; +} + +/** + * Builds the MCP servers contributed by enabled plugins. + * + * A server that fails to resolve is skipped with a diagnostic; its siblings, the + * other components, and the plugin all keep loading. + */ +export function resolvePluginMcpServers( + plugins: readonly LoadedPlugin[], + installedPlugins: InstalledPlugins, + context: PluginMcpRuntimeContext, +): ResolvedPluginMcpServers { + const servers: McpServer[] = []; + const diagnostics: PluginDiagnostic[] = []; + + for (const plugin of plugins) { + const state = installedPlugins[plugin.name]; + if (!state?.enabled || plugin.mcpServers.length === 0) continue; + + const data = pluginDataDirectory(context.pluginDataRoot, plugin.name); + let dataReady: boolean | undefined; + + for (const declaration of plugin.mcpServers) { + if (state.disabledMcpServerNames.includes(declaration.name)) continue; + + const name = pluginMcpServerName(plugin.name, declaration.name); + if (!isValidMcpServerName(name)) { + diagnostics.push( + pluginDiagnostic( + "error", + "mcp-server", + "mcp-name-unusable", + `Skipping server '${declaration.name}': '${name}' is not a usable MCP server name`, + declaration.name, + ), + ); + continue; + } + + if (declaration.entry.type === "stdio") { + dataReady ??= ensureDirectory(data); + if (!dataReady) { + diagnostics.push( + pluginDiagnostic( + "error", + "mcp-server", + "plugin-data-unavailable", + `Skipping server '${declaration.name}': cannot create PLUGIN_DATA at '${data}'`, + declaration.name, + ), + ); + continue; + } + } + + const built = buildTransport(declaration.entry, plugin.root, data); + if ("error" in built) { + diagnostics.push( + pluginDiagnostic( + "error", + "mcp-server", + "mcp-entry-unresolvable", + `Skipping server '${declaration.name}': ${built.error}`, + declaration.name, + ), + ); + continue; + } + + servers.push({ + id: pluginMcpServerId(plugin.name, declaration.name), + name, + description: plugin.manifest.description ?? "", + enabled: true, + timeoutMs: DEFAULT_MCP_SERVER_TIMEOUT_MS, + transport: built.transport, + }); + } + } + + return { servers, diagnostics }; +} diff --git a/src/supervisor/runtime/supervisorSharedSettings.test.ts b/src/supervisor/runtime/supervisorSharedSettings.test.ts index 28972796d..160cc5551 100644 --- a/src/supervisor/runtime/supervisorSharedSettings.test.ts +++ b/src/supervisor/runtime/supervisorSharedSettings.test.ts @@ -28,6 +28,7 @@ describe("SupervisorSharedSettingsCache", () => { enabled: true, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }); expect(cache.readFresh().installedPlugins["browser-tools"]?.enabled).toBe(true); @@ -38,6 +39,7 @@ describe("SupervisorSharedSettingsCache", () => { enabled: false, disabledSkillIds: [], disabledAppIds: [], + disabledMcpServerNames: [], }, }); expect(cache.readFresh().installedPlugins["browser-tools"]?.enabled).toBe(false); diff --git a/src/supervisor/runtime/threadSession/managerOptions.ts b/src/supervisor/runtime/threadSession/managerOptions.ts index f47240854..231e702d2 100644 --- a/src/supervisor/runtime/threadSession/managerOptions.ts +++ b/src/supervisor/runtime/threadSession/managerOptions.ts @@ -106,6 +106,12 @@ export interface ThreadSessionManagerOptions { launchConfig?: ThreadConfig; }, ): PromptSegment[] | Promise; + /** + * MCP servers contributed by enabled Agent Plugins packages via `mcp.json`. + * They join the user's own servers, so every provider translator picks them up + * without knowing the Agent Plugins specification exists. + */ + resolvePluginMcpServers?(): McpServer[]; /** Apply supported Poracode plugin app contributions before enforcing global MCP disables. */ applyPluginAppsToConfig?( config: ThreadConfig, diff --git a/src/supervisor/runtime/threadSession/spawnPipeline.test.ts b/src/supervisor/runtime/threadSession/spawnPipeline.test.ts index 8cf788fba..4835df54d 100644 --- a/src/supervisor/runtime/threadSession/spawnPipeline.test.ts +++ b/src/supervisor/runtime/threadSession/spawnPipeline.test.ts @@ -1,7 +1,32 @@ import { describe, expect, it } from "vitest"; -import type { ThreadConfig } from "@/shared/contracts"; +import type { LoadedPlugin, ThreadConfig } from "@/shared/contracts"; import type { AgentAdapter } from "../../agents/base"; -import { installBuiltInPlugin, resolvePluginAppsForThreadConfig } from "@/shared/plugins/catalog"; +import { installPlugin, resolvePluginAppsForThreadConfig } from "@/shared/plugins/catalog"; +import { AGENT_PLUGINS_MANIFEST_SCHEMA_URL } from "@/shared/plugins/spec"; + +const BROWSER_TOOLS: LoadedPlugin = { + name: "browser-tools", + source: "bundled", + root: "/plugins/browser-tools", + manifest: { $schema: AGENT_PLUGINS_MANIFEST_SCHEMA_URL, name: "browser-tools", version: "1.0.0" }, + poracode: { + category: "developer-tools", + featured: false, + communityMaintained: false, + apps: [ + { + id: "browser", + name: "Browser", + description: "Control the in-app browser.", + builtInMcpServerId: "browser", + }, + ], + skills: {}, + }, + skills: [], + mcpServers: [], + diagnostics: [], +}; import { effectiveLaunchConfig, effectiveStructuredTurnConfig, @@ -58,7 +83,8 @@ describe("effectiveLaunchConfig — single gate for built-in MCP disables", () = const config: ThreadConfig = { model: "test-model" }; const pluginConfig = resolvePluginAppsForThreadConfig( config, - installBuiltInPlugin({}, "browser-tools"), + [BROWSER_TOOLS], + installPlugin({}, BROWSER_TOOLS), { capabilities: { browserMcpScope: { terminal: "launch" } }, presentationMode: "terminal", diff --git a/src/supervisor/runtime/threadSession/spawnPipeline.ts b/src/supervisor/runtime/threadSession/spawnPipeline.ts index 9e2d47e35..d0bd0374c 100644 --- a/src/supervisor/runtime/threadSession/spawnPipeline.ts +++ b/src/supervisor/runtime/threadSession/spawnPipeline.ts @@ -407,7 +407,10 @@ export class SpawnPipeline { threadId: payload.threadId, title: initialPrompt.split("\n", 1)[0]?.trim() ?? "", }; - let mcpServers = resolveEnabledMcpServers(payload.mcpServers ?? []); + let mcpServers = resolveEnabledMcpServers([ + ...(payload.mcpServers ?? []), + ...(this.ctx.options.resolvePluginMcpServers?.() ?? []), + ]); if (this.ctx.options.applyMcpServerAuthorization) { mcpServers = await this.ctx.options.applyMcpServerAuthorization(mcpServers); } diff --git a/src/supervisor/skills/SkillsService.test.ts b/src/supervisor/skills/SkillsService.test.ts index 261323938..5657f2e36 100644 --- a/src/supervisor/skills/SkillsService.test.ts +++ b/src/supervisor/skills/SkillsService.test.ts @@ -1,4 +1,5 @@ import { + copyFile, lstat, mkdtemp, mkdir, @@ -12,16 +13,59 @@ import { execFileSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join, parse } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { ProjectLocation } from "@/shared/contracts"; +import type { LoadedPlugin, ProjectLocation } from "@/shared/contracts"; import type { InstalledPlugins } from "@/shared/contracts/plugin"; import { - installBuiltInPlugin, + installPlugin, setInstalledPluginEnabled, setPluginSkillEnabled, } from "@/shared/plugins/catalog"; +import { AGENT_PLUGINS_MANIFEST_SCHEMA_URL } from "@/shared/plugins/spec"; +import { loadPluginFromDirectory } from "@/supervisor/plugins"; import type { AgentAdapter } from "../agents/base"; import { SkillsService } from "./SkillsService"; +/** Packages ship with the app; tests reuse the real manifests, not copies of them. */ +const SHIPPED_PLUGINS_DIR = join(process.cwd(), "resources", "plugins"); + +async function writePluginPackage(root: string, name: string, skills: readonly string[]) { + const dir = join(root, "plugins", name); + const skillsDir = join(dir, "skills"); + await mkdir(skillsDir, { recursive: true }); + await copyFile(join(SHIPPED_PLUGINS_DIR, name, "plugin.json"), join(dir, "plugin.json")); + for (const skill of skills) await writeSkill(join(skillsDir, skill), skill); + const result = loadPluginFromDirectory(dir, "bundled"); + if (!result.plugin) { + throw new Error( + `${name} failed to load: ${result.diagnostics.map((d) => d.message).join("; ")}`, + ); + } + return { plugin: result.plugin, dir, skillsDir }; +} + +/** + * A package that is never read from disk. Used by the WSL canonicalization tests, + * which exercise path policy against a fixed Windows root. + */ +function fakePluginPackage(name: string, root: string, skills: readonly string[]): LoadedPlugin { + return { + name, + source: "bundled", + root, + manifest: { $schema: AGENT_PLUGINS_MANIFEST_SCHEMA_URL, name, version: "1.0.0" }, + poracode: { + category: "developer-tools", + featured: false, + communityMaintained: false, + apps: [], + skills: {}, + }, + skills: skills.map((folder) => ({ folder, path: join(root, "skills", folder) })), + mcpServers: [], + diagnostics: [], + }; +} + async function writeSkill(path: string, name: string, description = `${name} description`) { await mkdir(path, { recursive: true }); await writeFile( @@ -296,14 +340,13 @@ describe("SkillsService", () => { }); it("shows installed plugin skills as immutable owned contributions and honors disablement", async () => { - const bundledDir = join(root, "bundled-skills"); - await writeSkill(join(bundledDir, "browser-control"), "browser-control", "Control the browser"); + const pkg = await writePluginPackage(root, "browser-tools", ["browser-control"]); let installedPlugins: InstalledPlugins = {}; const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, readInstalledPlugins: () => installedPlugins, + readPlugins: () => [pkg.plugin], }); expect( @@ -312,7 +355,7 @@ describe("SkillsService", () => { ), ).toBeUndefined(); - installedPlugins = installBuiltInPlugin(installedPlugins, "browser-tools"); + installedPlugins = installPlugin(installedPlugins, pkg.plugin); const installedScan = await bundledService.scan({ projectLocation, agentKind: "claude" }); const installedSkill = installedScan.skills.find((skill) => skill.name === "browser-control"); expect(installedSkill).toMatchObject({ @@ -344,20 +387,20 @@ describe("SkillsService", () => { }); it("filters bundled plugin skill segments against current installation state", async () => { - const bundledDir = join(root, "bundled-skills"); - let installedPlugins = installBuiltInPlugin({}, "browser-tools"); + const pkg = await writePluginPackage(root, "browser-tools", ["browser-control"]); + let installedPlugins = installPlugin({}, pkg.plugin); const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, readInstalledPlugins: () => installedPlugins, + readPlugins: () => [pkg.plugin], hostPlatform: "win32", }); const textSegment = { kind: "text" as const, content: "Inspect this page." }; const pluginSegment = { kind: "skill" as const, name: "browser-control", - path: join(bundledDir, "browser-control", "SKILL.md"), + path: join(pkg.skillsDir, "browser-control", "SKILL.md"), invocation: "/browser-control", provider: "Browser Tools", scope: "global" as const, @@ -385,7 +428,7 @@ describe("SkillsService", () => { expect(await bundledService.filterPluginSkillSegments(segments)).toEqual([textSegment]); installedPlugins = setInstalledPluginEnabled( - installBuiltInPlugin({}, "browser-tools"), + installPlugin({}, pkg.plugin), "browser-tools", false, ); @@ -395,12 +438,12 @@ describe("SkillsService", () => { expect(await bundledService.filterPluginSkillSegments([pluginSegment])).toEqual([]); expect( await bundledService.filterPluginSkillSegments([ - { ...pluginSegment, path: join(bundledDir, "Browser-Control", "SKILL.md") }, + { ...pluginSegment, path: join(pkg.skillsDir, "Browser-Control", "SKILL.md") }, ]), ).toEqual([]); expect( await bundledService.filterPluginSkillSegments([ - { ...pluginSegment, path: join(bundledDir, "browser-control", "nested", "SKILL.md") }, + { ...pluginSegment, path: join(pkg.skillsDir, "browser-control", "nested", "SKILL.md") }, ]), ).toEqual([]); }); @@ -408,11 +451,10 @@ describe("SkillsService", () => { it("does not classify bundled-root links to outside skills as plugin skills", async ({ skip, }) => { - const bundledDir = join(root, "bundled-skills"); + const pkg = await writePluginPackage(root, "browser-tools", []); const outsideSkillDir = join(root, "outside-browser-control"); - const linkedSkillDir = join(bundledDir, "browser-control"); + const linkedSkillDir = join(pkg.skillsDir, "browser-control"); await writeSkill(outsideSkillDir, "browser-control"); - await mkdir(bundledDir, { recursive: true }); try { await symlink( outsideSkillDir, @@ -431,8 +473,8 @@ describe("SkillsService", () => { const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, readInstalledPlugins: () => ({}), + readPlugins: () => [pkg.plugin], hostPlatform: "win32", }); const segment = { @@ -450,18 +492,17 @@ describe("SkillsService", () => { it.runIf(process.platform === "win32")( "filters Windows namespace and Volume GUID aliases for disabled plugin skills", async () => { - const bundledDir = join(root, "bundled-skills"); - await writeSkill(join(bundledDir, "browser-control"), "browser-control"); - const junction = join(bundledDir, "browser-alias"); - await symlink(join(bundledDir, "browser-control"), junction, "junction"); + const pkg = await writePluginPackage(root, "browser-tools", ["browser-control"]); + const junction = join(pkg.skillsDir, "browser-alias"); + await symlink(join(pkg.skillsDir, "browser-control"), junction, "junction"); const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, readInstalledPlugins: () => ({}), + readPlugins: () => [pkg.plugin], hostPlatform: "win32", }); - const pluginPath = join(bundledDir, "browser-control", "SKILL.md"); + const pluginPath = join(pkg.skillsDir, "browser-control", "SKILL.md"); const driveRoot = parse(pluginPath).root; const volumeRoot = execFileSync("mountvol", [driveRoot, "/L"], { encoding: "utf8", @@ -487,13 +528,12 @@ describe("SkillsService", () => { ); it("hides and filters Computer Use plugin skills on unsupported Linux hosts", async () => { - const bundledDir = join(root, "bundled-skills"); - await writeSkill(join(bundledDir, "computer-use"), "computer-use", "Control desktop apps"); + const pkg = await writePluginPackage(root, "computer-use", ["computer-use"]); const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, - readInstalledPlugins: () => installBuiltInPlugin({}, "computer-use"), + readInstalledPlugins: () => installPlugin({}, pkg.plugin), + readPlugins: () => [pkg.plugin], hostPlatform: "linux", }); @@ -508,7 +548,7 @@ describe("SkillsService", () => { { kind: "skill", name: "computer-use", - path: join(bundledDir, "computer-use", "SKILL.md"), + path: join(pkg.skillsDir, "computer-use", "SKILL.md"), invocation: "/computer-use", provider: "Computer Use", scope: "global", @@ -518,16 +558,14 @@ describe("SkillsService", () => { }); it("filters plugin skills whose companion apps cannot run in WSL projects", async () => { - const bundledDir = join(root, "bundled-skills"); - const installedPlugins = installBuiltInPlugin( - installBuiltInPlugin({}, "chrome-tools"), - "computer-use", - ); + const chrome = await writePluginPackage(root, "chrome-tools", ["chrome-control"]); + const computerUse = await writePluginPackage(root, "computer-use", ["computer-use"]); + const installedPlugins = [chrome.plugin, computerUse.plugin].reduce(installPlugin, {}); const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, readInstalledPlugins: () => installedPlugins, + readPlugins: () => [chrome.plugin, computerUse.plugin], hostPlatform: "win32", }); const wslProject = { @@ -543,7 +581,7 @@ describe("SkillsService", () => { { kind: "skill", name: "chrome-control", - path: join(bundledDir, "chrome-control", "SKILL.md"), + path: join(chrome.skillsDir, "chrome-control", "SKILL.md"), invocation: "/chrome-control", provider: "Chrome Tools", scope: "global", @@ -551,7 +589,7 @@ describe("SkillsService", () => { { kind: "skill", name: "computer-use", - path: join(bundledDir, "computer-use", "SKILL.md"), + path: join(computerUse.skillsDir, "computer-use", "SKILL.md"), invocation: "/computer-use", provider: "Computer Use", scope: "global", @@ -563,8 +601,9 @@ describe("SkillsService", () => { }); it("canonicalizes WSL aliases before applying bundled plugin policy", async () => { - const bundledDir = "E:\\Poracode\\resources\\skills"; - const bundledWslRoot = "/mnt/e/Poracode/resources/skills"; + const pluginRoot = "E:\\Poracode\\resources\\plugins\\browser-tools"; + const pluginWslSkillsRoot = "/mnt/e/Poracode/resources/plugins/browser-tools/skills"; + const plugin = fakePluginPackage("browser-tools", pluginRoot, ["browser-control"]); const wslProject: ProjectLocation = { kind: "wsl", distro: "Ubuntu", @@ -574,17 +613,17 @@ describe("SkillsService", () => { const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, readInstalledPlugins: () => ({}), + readPlugins: () => [plugin], hostPlatform: "win32", - resolveHostPathForWsl: async () => bundledWslRoot, + resolveHostPathForWsl: async () => pluginWslSkillsRoot, resolveWslWindowsPaths: async () => [], resolveWslRealPaths: async (_distro, paths) => paths.map((path) => path === "/tmp/plugin-alias/SKILL.md" - ? `${bundledWslRoot}/browser-control/SKILL.md` + ? `${pluginWslSkillsRoot}/browser-control/SKILL.md` : path === "/tmp/mixed-case-alias/SKILL.md" - ? "/mnt/e/PORACODE/RESOURCES/SKILLS/BROWSER-CONTROL/SKILL.MD" + ? "/MNT/E/PORACODE/RESOURCES/PLUGINS/BROWSER-TOOLS/SKILLS/BROWSER-CONTROL/SKILL.MD" : path, ), }); @@ -626,8 +665,12 @@ describe("SkillsService", () => { const bundledService = new SkillsService({ adapters, homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: "E:\\Poracode\\resources\\skills" }, readInstalledPlugins: () => ({}), + readPlugins: () => [ + fakePluginPackage("browser-tools", "E:\\Poracode\\resources\\plugins\\browser-tools", [ + "browser-control", + ]), + ], hostPlatform: "win32", resolveHostPathForWsl: async () => undefined, }); @@ -653,8 +696,7 @@ describe("SkillsService", () => { }); it("matches plugin skills to the active provider presentation", async () => { - const bundledDir = join(root, "bundled-skills"); - await writeSkill(join(bundledDir, "browser-control"), "browser-control"); + const pkg = await writePluginPackage(root, "browser-tools", ["browser-control"]); const adapter = { kind: "claude", label: "Claude Code", @@ -672,14 +714,14 @@ describe("SkillsService", () => { const bundledService = new SkillsService({ adapters: new Map([["claude", adapter]]), homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, - readInstalledPlugins: () => installBuiltInPlugin({}, "browser-tools"), + readInstalledPlugins: () => installPlugin({}, pkg.plugin), + readPlugins: () => [pkg.plugin], hostPlatform: "win32", }); const pluginSegment = { kind: "skill" as const, name: "browser-control", - path: join(bundledDir, "browser-control", "SKILL.md"), + path: join(pkg.skillsDir, "browser-control", "SKILL.md"), invocation: "/browser-control", provider: "Browser Tools", scope: "global" as const, @@ -855,8 +897,7 @@ describe("SkillsService", () => { }); it("keeps plugin skills as the final fallback when provider precedence omits agents", async () => { - const bundledDir = join(root, "bundled-skills"); - await writeSkill(join(bundledDir, "browser-control"), "browser-control", "Plugin copy"); + const pkg = await writePluginPackage(root, "browser-tools", ["browser-control"]); await writeSkill( join(home, ".agents", "skills", "browser-control"), "browser-control", @@ -876,8 +917,8 @@ describe("SkillsService", () => { const pluginService = new SkillsService({ adapters: new Map([[adapter.kind, adapter]]), homeDirectory: () => home, - env: { PORACODE_BUNDLED_SKILLS_DIR: bundledDir }, - readInstalledPlugins: () => installBuiltInPlugin({}, "browser-tools"), + readInstalledPlugins: () => installPlugin({}, pkg.plugin), + readPlugins: () => [pkg.plugin], }); const scan = await pluginService.scan({ diff --git a/src/supervisor/skills/SkillsService.ts b/src/supervisor/skills/SkillsService.ts index 53a4bddc7..af0614470 100644 --- a/src/supervisor/skills/SkillsService.ts +++ b/src/supervisor/skills/SkillsService.ts @@ -39,6 +39,7 @@ import type { SkillScanResult, SkillScope, InstalledPlugins, + LoadedPlugin, } from "@/shared/contracts"; import { deleteSkillPayloadSchema, @@ -65,15 +66,18 @@ import { isPathUnderAny, selectSkillSegmentsForInjection, } from "./skillPromptInjection"; +import { PLUGIN_SKILLS_DIR } from "@/supervisor/plugins"; import { - BUNDLED_PROVIDER_ID, + pluginSkillProviderId, PluginSkillPolicy, type PluginSkillPolicyContext, + type PluginSkillRoot, } from "./pluginSkillPolicy"; const SKILL_FILE = "SKILL.md"; const MANIFEST_FILE = ".poracode-skill.json"; /** Root id/label for read-only skills shipped with the app (resources/skills). */ +export const BUNDLED_PROVIDER_ID = "poracode-built-in"; const BUNDLED_PROVIDER_LABEL = "Poracode built-ins"; const PORACODE_PROVIDER_GROUP_ID = "poracode"; const PORACODE_PROVIDER_GROUP_LABEL = "Poracode"; @@ -160,6 +164,8 @@ export interface SkillsServiceOptions { wslFsPath?: (distro: string, linuxPath: string) => string; fetch?: typeof fetch; readInstalledPlugins?: () => InstalledPlugins; + /** Agent Plugins packages discovered by the supervisor's plugin registry. */ + readPlugins?: () => readonly LoadedPlugin[]; hostPlatform?: NodeJS.Platform; } @@ -466,6 +472,7 @@ export class SkillsService { private readonly wslFsPath: (distro: string, linuxPath: string) => string; private readonly fetchImpl: typeof fetch; private readonly pluginSkillPolicy: PluginSkillPolicy; + private readonly readPlugins: () => readonly LoadedPlugin[]; private readonly marketplaceCache = new Map< string, { expiresAt: number; result: SkillMarketplaceResult } @@ -481,8 +488,9 @@ export class SkillsService { this.resolveWslRealPaths = options.resolveWslRealPaths ?? resolveWslRealPaths; this.wslFsPath = options.wslFsPath ?? toWslUncPath; this.fetchImpl = options.fetch ?? fetch; + this.readPlugins = options.readPlugins ?? (() => []); this.pluginSkillPolicy = new PluginSkillPolicy({ - bundledRoot: () => this.bundledRoot()?.fsPath, + readPluginRoots: () => this.pluginSkillRoots(), readInstalledPlugins: options.readInstalledPlugins ?? (() => ({})), hostPlatform: options.hostPlatform ?? process.platform, resolveWslRealPaths: this.resolveWslRealPaths, @@ -1503,6 +1511,7 @@ export class SkillsService { } const bundledRoot = this.bundledRoot(); if (bundledRoot) roots.push(bundledRoot); + roots.push(...this.pluginLocatedRoots()); const seen = new Set(roots.map((root) => normalizePath(root.fsPath))); for (const adapter of selectedAdapters ?? this.adapters.values()) { @@ -1604,6 +1613,38 @@ export class SkillsService { }; } + /** + * One scan root per loaded Agent Plugins package that ships skills. Each root + * is that package's own `skills/` directory, so containment and attribution + * follow the package boundary rather than a shared folder. + */ + private pluginSkillRoots(): PluginSkillRoot[] { + return this.readPlugins().flatMap((plugin) => + plugin.skills.length > 0 + ? [{ plugin, skillsRoot: join(plugin.root, PLUGIN_SKILLS_DIR) }] + : [], + ); + } + + private pluginLocatedRoots(): LocatedRoot[] { + return this.pluginSkillRoots().map(({ plugin, skillsRoot }) => { + const label = plugin.poracode.title ?? plugin.name; + return { + providerId: pluginSkillProviderId(plugin.name), + providerLabel: label, + providerGroupId: pluginSkillProviderId(plugin.name), + providerGroupLabel: label, + providerGroupOrder: -2, + scope: "global", + scopeLabel: "Global", + fsPath: skillsRoot, + displayPath: skillsRoot.replace(/\\/gu, "/"), + origin: "plugin", + mutable: false, + }; + }); + } + private locateRoot( environment: ResolvedEnvironment, spec: AgentSkillRootSpec, diff --git a/src/supervisor/skills/pluginSkillPolicy.ts b/src/supervisor/skills/pluginSkillPolicy.ts index bcaaab45e..1b3949e67 100644 --- a/src/supervisor/skills/pluginSkillPolicy.ts +++ b/src/supervisor/skills/pluginSkillPolicy.ts @@ -1,8 +1,8 @@ -import { realpathSync } from "node:fs"; -import { isAbsolute, posix, relative, resolve } from "node:path"; +import { posix } from "node:path"; import type { AgentCapability, InstalledPlugins, + LoadedPlugin, ProjectLocation, PromptSegment, SkillEntry, @@ -11,15 +11,33 @@ import type { } from "@/shared/contracts"; import { arePluginSkillRequiredAppsEnabled, - getBundledPluginSkill, isPluginSkillEnabled, isPluginSkillSupportedForLaunch, } from "@/shared/plugins/catalog"; +import { relativePolicyPath } from "@/supervisor/plugins"; import { parseWslUncPath } from "@/shared/wsl"; import { batchWslCommandsAsync, quotePosixShellArg } from "../agents/base"; +/** + * Enforces plugin policy on skills contributed by Agent Plugins packages. + * + * Each package owns its own `skills/` root, so containment is checked against + * that package's boundary rather than a shared directory: a skill can only be + * attributed to — and injected on behalf of — the plugin it actually lives in. + */ + const SKILL_FILE = "SKILL.md"; -export const BUNDLED_PROVIDER_ID = "poracode-built-in"; + +/** Provider id used for skills contributed by a plugin. */ +export function pluginSkillProviderId(pluginName: string): string { + return `plugin:${pluginName}`; +} + +export interface PluginSkillRoot { + plugin: LoadedPlugin; + /** `/skills`, the root a plugin's skill entries are scanned from. */ + skillsRoot: string; +} export interface PluginSkillPolicyContext { projectLocation?: ProjectLocation; @@ -29,7 +47,7 @@ export interface PluginSkillPolicyContext { } export interface PluginSkillPolicyOptions { - bundledRoot: () => string | undefined; + readPluginRoots: () => readonly PluginSkillRoot[]; readInstalledPlugins: () => InstalledPlugins; hostPlatform: NodeJS.Platform; resolveWslRealPaths: ( @@ -43,44 +61,11 @@ export interface PluginSkillPolicyOptions { ) => Promise; } -function normalizeWindowsNamespacePath(path: string): string { - let normalized = path; - if (normalized.startsWith("\\\\?\\UNC\\")) normalized = `\\\\${normalized.slice(8)}`; - else if (normalized.startsWith("\\\\?\\")) normalized = normalized.slice(4); - else if (normalized.startsWith("//?/UNC/")) normalized = `//${normalized.slice(8)}`; - else if (normalized.startsWith("//?/")) normalized = normalized.slice(4); - return normalized; -} - -function relativePathInside(root: string, target: string): string | undefined { - const candidate = relative(root, target); - if (!candidate || isAbsolute(candidate) || candidate.split(/[\\/]/u)[0] === "..") { - return undefined; - } - return candidate; -} - -function relativePolicyPath(root: string, target: string): string | undefined { - const normalizedRoot = resolve(normalizeWindowsNamespacePath(root)); - const normalizedTarget = resolve(normalizeWindowsNamespacePath(target)); - try { - return relativePathInside( - resolve(realpathSync.native(root)), - resolve(realpathSync.native(target)), - ); - } catch { - // Fall through to the normalized aliases for non-existent paths. - } - const direct = relativePathInside(normalizedRoot, normalizedTarget); - if (direct) return direct; - try { - return relativePathInside( - resolve(realpathSync.native(normalizedRoot)), - resolve(realpathSync.native(normalizedTarget)), - ); - } catch { - return undefined; - } +function normalizeRootKey(path: string): string { + return path + .replace(/[\\/]+$/u, "") + .replace(/\\/gu, "/") + .toLowerCase(); } function relativePosixPolicyPath(root: string, target: string): string | undefined { @@ -120,8 +105,14 @@ async function resolveWslWindowsPaths( return results.map((result) => (result?.ok && result.stdout ? result.stdout : undefined)); } +/** A skill segment matched to the plugin whose package boundary contains it. */ +interface MatchedSegment { + root: PluginSkillRoot; + relativePath: string; +} + export class PluginSkillPolicy { - private readonly bundledRootWslPaths = new Map>(); + private readonly wslRootPaths = new Map>(); private readonly resolveHostPathForWsl: ( distro: string, hostPath: string, @@ -136,63 +127,69 @@ export class PluginSkillPolicy { this.resolveWslWindowsPaths = options.resolveWslWindowsPaths ?? resolveWslWindowsPaths; } + /** + * Labels plugin-contributed entries and hides the ones whose plugin is not + * installed or cannot run in this environment. + */ resolveScanEntries( entries: readonly SkillEntry[], context: PluginSkillPolicyContext, ): SkillEntry[] { + const roots = this.options.readPluginRoots(); + if (roots.length === 0) return [...entries]; + const byRoot = new Map(roots.map((root) => [normalizeRootKey(root.skillsRoot), root])); const installedPlugins = this.options.readInstalledPlugins(); + return entries.flatMap((skill) => { - if (skill.providerId !== BUNDLED_PROVIDER_ID) return [skill]; - const bundledSkill = getBundledPluginSkill(skill.folderName); - if (!bundledSkill) return [skill]; - const { manifest, contribution } = bundledSkill; - const state = installedPlugins[manifest.id]; + const root = byRoot.get(normalizeRootKey(skill.rootPath)); + if (!root) return [skill]; + const { plugin } = root; + const state = installedPlugins[plugin.name]; if (!state) return []; - if ( - !isPluginSkillSupportedForLaunch(manifest, contribution, { - hostPlatform: this.options.hostPlatform, - ...(context.projectLocation ? { projectLocation: context.projectLocation } : {}), - ...(context.capabilities ? { capabilities: context.capabilities } : {}), - ...(context.presentationMode ? { presentationMode: context.presentationMode } : {}), - }) - ) { - return []; - } + if (!this.isSupported(plugin, skill.folderName, context)) return []; + const label = plugin.poracode.title ?? plugin.name; return [ { ...skill, - id: `${skill.scope}:plugin:${manifest.id}:${skill.folderName}`, - providerId: `plugin:${manifest.id}`, - providerLabel: manifest.name, - providerGroupId: `plugin:${manifest.id}`, - providerGroupLabel: manifest.name, + id: `${skill.scope}:plugin:${plugin.name}:${skill.folderName}`, + providerId: pluginSkillProviderId(plugin.name), + providerLabel: label, + providerGroupId: pluginSkillProviderId(plugin.name), + providerGroupLabel: label, providerGroupOrder: -2, origin: "plugin" as const, - pluginId: manifest.id, - pluginName: manifest.name, - enabled: isPluginSkillEnabled(manifest, state, contribution.id), + pluginId: plugin.name, + pluginName: label, + enabled: isPluginSkillEnabled(plugin, state, skill.folderName), }, ]; }); } + /** + * Drops plugin skill segments that policy does not allow into a launch. A + * segment inside a plugin's boundary that no longer passes is removed; a + * segment that only *looks* like it belongs to a plugin is left alone. + */ async filterSegments( segments: PromptSegment[], context: PluginSkillPolicyContext = {}, ): Promise { - const bundledRoot = this.options.bundledRoot(); - if (!bundledRoot || !segments.some((segment) => segment.kind === "skill")) return segments; + const roots = this.options.readPluginRoots(); + if (roots.length === 0 || !segments.some((segment) => segment.kind === "skill")) { + return segments; + } - const relativePaths = new Map(); + const matched = new Map(); const unresolvedWslPaths = new Map< string, Array<{ segment: PromptSegment; linuxPath: string }> >(); for (const segment of segments) { if (segment.kind !== "skill") continue; - const relativePath = relativePolicyPath(bundledRoot, segment.path); - if (relativePath) { - relativePaths.set(segment, relativePath); + const hostMatch = this.matchHostPath(roots, segment.path); + if (hostMatch) { + matched.set(segment, hostMatch); continue; } const parsedWslPath = parseWslUncPath(segment.path); @@ -215,8 +212,8 @@ export class PluginSkillPolicy { const rejectedWslSegments = new Set(); await Promise.all( [...unresolvedWslPaths].map(async ([distro, pending]) => { - const bundledWslRoot = await this.resolveBundledRootWslPath(distro, bundledRoot); - if (!bundledWslRoot) { + const wslRoots = await this.resolveWslRoots(distro, roots); + if (wslRoots.length === 0) { pending.forEach(({ segment }) => rejectedWslSegments.add(segment)); return; } @@ -237,10 +234,18 @@ export class PluginSkillPolicy { return; } const windowsPath = windowsPaths[index]; - const relativePath = - (windowsPath ? relativePolicyPath(bundledRoot, windowsPath) : undefined) ?? - relativeWslPolicyPath(bundledWslRoot, resolvedPath); - if (relativePath) relativePaths.set(segment, relativePath); + const hostMatch = windowsPath ? this.matchHostPath(roots, windowsPath) : undefined; + if (hostMatch) { + matched.set(segment, hostMatch); + return; + } + for (const { root, wslRoot } of wslRoots) { + const relativePath = relativeWslPolicyPath(wslRoot, resolvedPath); + if (relativePath) { + matched.set(segment, { root, relativePath }); + return; + } + } }); }), ); @@ -253,27 +258,20 @@ export class PluginSkillPolicy { changed = true; return false; } - const relativePath = relativePaths.get(segment); - if (!relativePath) return true; - const parts = relativePath.split(/[\\/]/u); - const folder = parts[0]!.toLowerCase(); - const bundledSkill = getBundledPluginSkill(folder); - if (!bundledSkill) return true; - const { manifest, contribution } = bundledSkill; - const state = installedPlugins[manifest.id]; + const match = matched.get(segment); + if (!match) return true; + const parts = match.relativePath.split(/[\\/]/u); + const folder = parts[0]!; + const { plugin } = match.root; + const state = installedPlugins[plugin.name]; const allowed = Boolean( parts.length === 2 && parts[1] === SKILL_FILE && state && - isPluginSkillSupportedForLaunch(manifest, contribution, { - hostPlatform: this.options.hostPlatform, - ...(context.projectLocation ? { projectLocation: context.projectLocation } : {}), - ...(context.capabilities ? { capabilities: context.capabilities } : {}), - ...(context.presentationMode ? { presentationMode: context.presentationMode } : {}), - }) && - isPluginSkillEnabled(manifest, state, contribution.id) && + this.isSupported(plugin, folder, context) && + isPluginSkillEnabled(plugin, state, folder) && (!context.launchConfig || - arePluginSkillRequiredAppsEnabled(manifest, contribution, context.launchConfig)), + arePluginSkillRequiredAppsEnabled(plugin, folder, context.launchConfig)), ); if (!allowed) changed = true; return allowed; @@ -281,22 +279,57 @@ export class PluginSkillPolicy { return changed ? filtered : segments; } - private async resolveBundledRootWslPath( + private isSupported( + plugin: LoadedPlugin, + folder: string, + context: PluginSkillPolicyContext, + ): boolean { + return isPluginSkillSupportedForLaunch(plugin, folder, { + hostPlatform: this.options.hostPlatform, + ...(context.projectLocation ? { projectLocation: context.projectLocation } : {}), + ...(context.capabilities ? { capabilities: context.capabilities } : {}), + ...(context.presentationMode ? { presentationMode: context.presentationMode } : {}), + }); + } + + /** WSL skill folder names are case-sensitive, so match is case-preserving here. */ + private matchHostPath( + roots: readonly PluginSkillRoot[], + path: string, + ): MatchedSegment | undefined { + for (const root of roots) { + const relativePath = relativePolicyPath(root.skillsRoot, path); + if (relativePath) return { root, relativePath }; + } + return undefined; + } + + private async resolveWslRoots( distro: string, - bundledRoot: string, - ): Promise { - const key = `${distro.toLowerCase()}\0${bundledRoot}`; - const cached = this.bundledRootWslPaths.get(key); + roots: readonly PluginSkillRoot[], + ): Promise> { + const resolved = await Promise.all( + roots.map(async (root) => { + const wslRoot = await this.resolveWslRootPath(distro, root.skillsRoot); + return wslRoot ? { root, wslRoot } : undefined; + }), + ); + return resolved.filter((entry) => entry !== undefined); + } + + private async resolveWslRootPath(distro: string, hostRoot: string): Promise { + const key = `${distro.toLowerCase()}\0${hostRoot}`; + const cached = this.wslRootPaths.get(key); if (cached) return cached; const pending = (async () => { - const linuxPath = await this.resolveHostPathForWsl(distro, bundledRoot); + const linuxPath = await this.resolveHostPathForWsl(distro, hostRoot); if (!linuxPath) return undefined; const [resolvedPath] = await this.options.resolveWslRealPaths(distro, [linuxPath]); return resolvedPath; })().catch(() => undefined); - this.bundledRootWslPaths.set(key, pending); + this.wslRootPaths.set(key, pending); const resolvedPath = await pending; - if (!resolvedPath) this.bundledRootWslPaths.delete(key); + if (!resolvedPath) this.wslRootPaths.delete(key); return resolvedPath; } } diff --git a/src/supervisor/supervisorRuntime.ts b/src/supervisor/supervisorRuntime.ts index 2e2f86d1c..3028c4bce 100644 --- a/src/supervisor/supervisorRuntime.ts +++ b/src/supervisor/supervisorRuntime.ts @@ -62,6 +62,7 @@ import { prepareMcpToolFilters } from "./mcp/McpToolFilterService"; import { ExternalMcpDiscoveryService } from "./mcp/ExternalMcpDiscoveryService"; import { SkillsService } from "./skills/SkillsService"; import { resolvePluginAppsForThreadConfig } from "@/shared/plugins/catalog"; +import { PluginRegistry, resolvePluginMcpServers } from "./plugins"; export { detectWslAgentStatuses, writeSubmittedPrompt }; @@ -94,6 +95,8 @@ export class SupervisorRuntime { readonly mcpOAuthService: McpOAuthService; readonly mcpProbeService: McpProbeService; readonly skillsService: SkillsService; + readonly pluginRegistry: PluginRegistry; + private readonly pluginDataDir: string; private readonly subagentMcpIngress: SubagentMcpIngress; private readonly subagentRunManager: SubagentRunManager; private readonly orchestratorThreadManager: OrchestratorThreadManager; @@ -154,9 +157,15 @@ export class SupervisorRuntime { getAgentStatusService: () => this.agentStatusService, }); this.agentRegistryService.refreshAgentRegistryAdapters(); + this.pluginRegistry = new PluginRegistry({ + bundledPluginsDir: () => process.env.PORACODE_BUNDLED_PLUGINS_DIR?.trim() || undefined, + userPluginsDir: () => paths.pluginsDir, + }); + this.pluginDataDir = paths.pluginDataDir; this.skillsService = new SkillsService({ adapters: this.adapters, readInstalledPlugins: () => this.sharedSettingsCache.readFresh().installedPlugins, + readPlugins: () => this.pluginRegistry.listPlugins(), }); mkdirSync(paths.cacheDir, { recursive: true }); mkdirSync(this.logsDir, { recursive: true }); @@ -411,11 +420,19 @@ export class SupervisorRuntime { prepareMcpToolFilters, applyPluginAppsToConfig: (config, context) => { const installedPlugins = this.sharedSettingsCache.readFresh().installedPlugins; - return resolvePluginAppsForThreadConfig(config, installedPlugins, { - ...context, - hostPlatform: process.platform, - }); + return resolvePluginAppsForThreadConfig( + config, + this.pluginRegistry.listPlugins(), + installedPlugins, + { ...context, hostPlatform: process.platform }, + ); }, + resolvePluginMcpServers: () => + resolvePluginMcpServers( + this.pluginRegistry.listPlugins(), + this.sharedSettingsCache.readFresh().installedPlugins, + { pluginDataRoot: this.pluginDataDir }, + ).servers, prepareSkillsForLaunch: async (projectLocation, agentKind) => { try { await this.skillsService.prepareForLaunch(projectLocation, agentKind); From 934c914cf5d7deb8d29507581ed5cd253a9023f1 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Thu, 6 Aug 2026 15:05:16 -0700 Subject: [PATCH 3/6] fix(plugins): handle disabled MCP servers and localized plugin errors - Skip globally disabled MCP servers and carry the effective launchConfig through spawn, restart, and session recovery - Filter plugin skill segments on steer with WSL-aware path resolution and fail-closed policy checks - Add a retry state to plugin settings load failures and switch plugin buttons to HeroUI Button - Localize the OAuth sign-out error message and update all i18n catalogs - Add coverage for config propagation, disabled-server conformance, and WSL skill paths --- .../components/plugins/PluginDetail.tsx | 10 +-- .../components/plugins/PluginMarketplace.tsx | 23 +++--- .../components/plugins/usePluginOauth.ts | 16 ++-- src/renderer/locales/de/messages.po | 5 ++ src/renderer/locales/en/messages.po | 5 ++ src/renderer/locales/es/messages.po | 5 ++ src/renderer/locales/fr/messages.po | 5 ++ src/renderer/locales/ja/messages.po | 5 ++ src/renderer/locales/ko/messages.po | 5 ++ src/renderer/locales/pl/messages.po | 5 ++ src/renderer/locales/pt-BR/messages.po | 5 ++ src/renderer/locales/ru/messages.po | 5 ++ src/renderer/locales/tr/messages.po | 5 ++ src/renderer/locales/uk/messages.po | 5 ++ src/renderer/locales/vi/messages.po | 5 ++ src/renderer/locales/zh-CN/messages.po | 5 ++ .../SettingsOverlay/parts/PluginsSettings.tsx | 25 ++++++ src/supervisor/plugins/conformance.test.ts | 76 +++++++++++++++++++ src/supervisor/plugins/pluginMcpRuntime.ts | 32 ++++++-- src/supervisor/runtime/sessionTypes.ts | 2 + .../runtime/threadOutputPipeline.ts | 1 + .../invalidSessionRecovery.test.ts | 1 + .../threadSession/invalidSessionRecovery.ts | 1 + .../runtime/threadSession/managerOptions.ts | 10 ++- .../runtime/threadSession/spawnPipeline.ts | 43 +++++++++-- ...readSessionManager.restartTerminal.test.ts | 1 + .../runtime/threadSessionManager.ts | 32 +++++++- src/supervisor/skills/SkillsService.test.ts | 44 +++++++++++ src/supervisor/skills/pluginSkillPolicy.ts | 13 +++- src/supervisor/supervisorRuntime.ts | 16 +++- 30 files changed, 366 insertions(+), 45 deletions(-) diff --git a/src/renderer/components/plugins/PluginDetail.tsx b/src/renderer/components/plugins/PluginDetail.tsx index 36aca6cc0..b2d0f9c6c 100644 --- a/src/renderer/components/plugins/PluginDetail.tsx +++ b/src/renderer/components/plugins/PluginDetail.tsx @@ -123,17 +123,17 @@ export function PluginDetail(props: {
{examplePrompt ? ( - + ) : null} {problems.length > 0 ? ( diff --git a/src/renderer/components/plugins/PluginMarketplace.tsx b/src/renderer/components/plugins/PluginMarketplace.tsx index 572f3d0b8..f2e25b493 100644 --- a/src/renderer/components/plugins/PluginMarketplace.tsx +++ b/src/renderer/components/plugins/PluginMarketplace.tsx @@ -88,16 +88,18 @@ export function PluginMarketplace(props: {
{installed.map((entry) => ( - + ))}
@@ -174,15 +176,16 @@ function PluginCard(props: {
- + {plugin.source === "user" ? ( External diff --git a/src/renderer/components/plugins/usePluginOauth.ts b/src/renderer/components/plugins/usePluginOauth.ts index a98ffdf25..064ecb222 100644 --- a/src/renderer/components/plugins/usePluginOauth.ts +++ b/src/renderer/components/plugins/usePluginOauth.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from "react"; +import { useLingui } from "@lingui/react/macro"; import type { LoadedPlugin, McpServer } from "@/shared/contracts"; import { DEFAULT_MCP_SERVER_TIMEOUT_MS } from "@/shared/contracts"; import { readBridge } from "@/renderer/bridge"; @@ -32,6 +33,7 @@ function toMcpServer(plugin: LoadedPlugin, serverName: string, url: string): Mcp } export function usePluginOauth(plugin: LoadedPlugin) { + const { t } = useLingui(); const [authorizedUrls, setAuthorizedUrls] = useState(); const [pending, setPending] = useState(); const [error, setError] = useState(); @@ -73,20 +75,20 @@ export function usePluginOauth(plugin: LoadedPlugin) { server: toMcpServer(plugin, serverName, url), }); if (begin.status === "error") { - setError(begin.message); + setError(t`Could not sign in to ${serverName}.`); return; } if (begin.status === "redirect") { - await bridge.openExternal(begin.authorizationUrl); + await bridge.openExternalNative(begin.authorizationUrl); const result = await bridge.waitMcpServerOauth({ flowId: begin.flowId }); if (result.status === "error") { - setError(result.message); + setError(t`Could not sign in to ${serverName}.`); return; } } await refresh(); - } catch (cause) { - setError(cause instanceof Error ? cause.message : String(cause)); + } catch { + setError(t`Could not sign in to ${serverName}.`); } finally { setPending(undefined); } @@ -100,8 +102,8 @@ export function usePluginOauth(plugin: LoadedPlugin) { try { await readBridge().clearMcpServerOauth({ url }); await refresh(); - } catch (cause) { - setError(cause instanceof Error ? cause.message : String(cause)); + } catch { + setError(t`Could not sign out of ${serverName}.`); } }; diff --git a/src/renderer/locales/de/messages.po b/src/renderer/locales/de/messages.po index b72fe3a93..aab4fced6 100644 --- a/src/renderer/locales/de/messages.po +++ b/src/renderer/locales/de/messages.po @@ -2921,6 +2921,7 @@ msgstr "Verbindungsfehler." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Verbindung fehlgeschlagen." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Anmeldung bei {serverName} fehlgeschlagen: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Anmeldung bei {serverName} fehlgeschlagen." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Abmeldung von {serverName} fehlgeschlagen." @@ -6085,6 +6088,7 @@ msgstr "Workflow-Ausführungen werden geladen" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Laden…" @@ -9442,6 +9446,7 @@ msgstr "Ziel fortsetzen" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Erneut versuchen" diff --git a/src/renderer/locales/en/messages.po b/src/renderer/locales/en/messages.po index cad7d3527..f91480f57 100644 --- a/src/renderer/locales/en/messages.po +++ b/src/renderer/locales/en/messages.po @@ -2926,6 +2926,7 @@ msgstr "Connection error." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Connection failed." @@ -3175,10 +3176,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Could not sign in to {serverName}: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Could not sign in to {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Could not sign out of {serverName}." @@ -6090,6 +6093,7 @@ msgstr "Loading workflow runs" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Loading…" @@ -9447,6 +9451,7 @@ msgstr "Resume goal" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Retry" diff --git a/src/renderer/locales/es/messages.po b/src/renderer/locales/es/messages.po index 4fba5a32c..edfe43dc8 100644 --- a/src/renderer/locales/es/messages.po +++ b/src/renderer/locales/es/messages.po @@ -2921,6 +2921,7 @@ msgstr "Error de conexión." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Error de conexión." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "No se pudo iniciar sesión en {serverName}: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "No se pudo iniciar sesión en {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "No se pudo cerrar sesión en {serverName}." @@ -6085,6 +6088,7 @@ msgstr "Cargando ejecuciones de flujos de trabajo" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Cargando…" @@ -9442,6 +9446,7 @@ msgstr "Reanudar objetivo" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Reintentar" diff --git a/src/renderer/locales/fr/messages.po b/src/renderer/locales/fr/messages.po index 33bd6bfa5..bef87eec9 100644 --- a/src/renderer/locales/fr/messages.po +++ b/src/renderer/locales/fr/messages.po @@ -2921,6 +2921,7 @@ msgstr "Erreur de connexion." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Échec de la connexion." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Impossible de se connecter à {serverName} : {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Impossible de se connecter à {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Impossible de se déconnecter de {serverName}." @@ -6085,6 +6088,7 @@ msgstr "Chargement des exécutions de workflows" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Chargement…" @@ -9441,6 +9445,7 @@ msgstr "Reprendre l'objectif" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Réessayer" diff --git a/src/renderer/locales/ja/messages.po b/src/renderer/locales/ja/messages.po index 375d3c39d..64c5b78ab 100644 --- a/src/renderer/locales/ja/messages.po +++ b/src/renderer/locales/ja/messages.po @@ -2920,6 +2920,7 @@ msgstr "接続エラーです。" #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "接続に失敗しました。" @@ -3169,10 +3170,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "{serverName} にサインインできませんでした: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "{serverName} にサインインできませんでした。" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "{serverName} からサインアウトできませんでした。" @@ -6084,6 +6087,7 @@ msgstr "ワークフロー実行を読み込み中" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "読み込み中…" @@ -9440,6 +9444,7 @@ msgstr "目標を再開" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "再試行" diff --git a/src/renderer/locales/ko/messages.po b/src/renderer/locales/ko/messages.po index e95b7b7dd..af7bbc0ea 100644 --- a/src/renderer/locales/ko/messages.po +++ b/src/renderer/locales/ko/messages.po @@ -2921,6 +2921,7 @@ msgstr "연결 오류입니다." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "연결에 실패했습니다." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "{serverName}에 로그인할 수 없습니다: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "{serverName}에 로그인할 수 없습니다." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "{serverName}에서 로그아웃할 수 없습니다." @@ -6085,6 +6088,7 @@ msgstr "워크플로 실행 불러오는 중" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "로드 중…" @@ -9442,6 +9446,7 @@ msgstr "목표 재개" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "다시 시도" diff --git a/src/renderer/locales/pl/messages.po b/src/renderer/locales/pl/messages.po index f5ebe20b2..e171d6ba4 100644 --- a/src/renderer/locales/pl/messages.po +++ b/src/renderer/locales/pl/messages.po @@ -2921,6 +2921,7 @@ msgstr "Błąd połączenia." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Połączenie nie powiodło się." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Nie udało się zalogować do {serverName}: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Nie udało się zalogować do {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Nie udało się wylogować z {serverName}." @@ -6085,6 +6088,7 @@ msgstr "Ładowanie uruchomień przepływów pracy" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Ładowanie…" @@ -9442,6 +9446,7 @@ msgstr "Wznów cel" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Spróbuj ponownie" diff --git a/src/renderer/locales/pt-BR/messages.po b/src/renderer/locales/pt-BR/messages.po index 5a61d7322..ca969e12c 100644 --- a/src/renderer/locales/pt-BR/messages.po +++ b/src/renderer/locales/pt-BR/messages.po @@ -2921,6 +2921,7 @@ msgstr "Erro de conexão." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Falha na conexão." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Não foi possível entrar em {serverName}: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Não foi possível entrar em {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Não foi possível sair de {serverName}." @@ -6085,6 +6088,7 @@ msgstr "Carregando execuções de fluxo de trabalho" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Carregando…" @@ -9442,6 +9446,7 @@ msgstr "Retomar objetivo" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Tentar novamente" diff --git a/src/renderer/locales/ru/messages.po b/src/renderer/locales/ru/messages.po index d5510e258..e05664c65 100644 --- a/src/renderer/locales/ru/messages.po +++ b/src/renderer/locales/ru/messages.po @@ -2921,6 +2921,7 @@ msgstr "Ошибка подключения." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Не удалось подключиться." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Не удалось войти в {serverName}: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Не удалось войти в {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Не удалось выйти из {serverName}." @@ -6085,6 +6088,7 @@ msgstr "Загрузка запусков рабочих процессов" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Загрузка…" @@ -9442,6 +9446,7 @@ msgstr "Возобновить цель" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Повторить" diff --git a/src/renderer/locales/tr/messages.po b/src/renderer/locales/tr/messages.po index e31cf044e..ac9d5a27e 100644 --- a/src/renderer/locales/tr/messages.po +++ b/src/renderer/locales/tr/messages.po @@ -2921,6 +2921,7 @@ msgstr "Bağlantı hatası." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Bağlantı başarısız." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "{serverName} oturumu açılamadı: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "{serverName} oturumu açılamadı." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "{serverName} oturumu kapatılamadı." @@ -6085,6 +6088,7 @@ msgstr "İş akışı çalıştırmaları yükleniyor" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Yükleniyor…" @@ -9442,6 +9446,7 @@ msgstr "Hedefi sürdür" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Tekrar dene" diff --git a/src/renderer/locales/uk/messages.po b/src/renderer/locales/uk/messages.po index 8c5fec019..0b0772de5 100644 --- a/src/renderer/locales/uk/messages.po +++ b/src/renderer/locales/uk/messages.po @@ -2921,6 +2921,7 @@ msgstr "Помилка підключення." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Не вдалося підключитися." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Не вдалося ввійти до {serverName}: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Не вдалося ввійти до {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Не вдалося вийти з {serverName}." @@ -6085,6 +6088,7 @@ msgstr "Завантаження запусків робочих процесі #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Завантаження…" @@ -9442,6 +9446,7 @@ msgstr "Відновити мету" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Повторить" diff --git a/src/renderer/locales/vi/messages.po b/src/renderer/locales/vi/messages.po index 948526071..54d6c7859 100644 --- a/src/renderer/locales/vi/messages.po +++ b/src/renderer/locales/vi/messages.po @@ -2921,6 +2921,7 @@ msgstr "Lỗi kết nối." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Kết nối thất bại." @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "Không thể đăng nhập vào {serverName}: {message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "Không thể đăng nhập vào {serverName}." #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "Không thể đăng xuất khỏi {serverName}." @@ -6085,6 +6088,7 @@ msgstr "Đang tải các lần chạy quy trình" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Đang tải…" @@ -9442,6 +9446,7 @@ msgstr "Tiếp tục mục tiêu" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "Thử lại" diff --git a/src/renderer/locales/zh-CN/messages.po b/src/renderer/locales/zh-CN/messages.po index 1de2a432f..190e24401 100644 --- a/src/renderer/locales/zh-CN/messages.po +++ b/src/renderer/locales/zh-CN/messages.po @@ -2921,6 +2921,7 @@ msgstr "连接错误。" #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "连接失败。" @@ -3170,10 +3171,12 @@ msgid "Could not sign in to {serverName}: {message}" msgstr "无法登录 {serverName}:{message}" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign in to {serverName}." msgstr "无法登录 {serverName}。" #: src/renderer/components/mcp/useMcpServerOauth.ts +#: src/renderer/components/plugins/usePluginOauth.ts msgid "Could not sign out of {serverName}." msgstr "无法退出 {serverName}。" @@ -6085,6 +6088,7 @@ msgstr "正在加载工作流运行记录" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "加载中..." @@ -9441,6 +9445,7 @@ msgstr "继续目标" #: src/renderer/components/mcp/McpExternalImportModal.tsx #: src/renderer/components/skills/SkillsManager.tsx #: src/renderer/views/GitReviewOverlay/parts/GitStackedDiff.tsx +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteAccessSettings.tsx msgid "Retry" msgstr "重试" diff --git a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx index 9b0437350..8b3e8442d 100644 --- a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +++ b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx @@ -1,4 +1,6 @@ import { useEffect, useRef, useState } from "react"; +import { Trans } from "@lingui/react/macro"; +import { Button } from "@/renderer/components/common"; import { PluginDetail } from "@/renderer/components/plugins/PluginDetail"; import { PluginMarketplace } from "@/renderer/components/plugins/PluginMarketplace"; import { useLocalizedPluginCatalog } from "@/renderer/components/plugins/pluginCopy"; @@ -8,6 +10,8 @@ import { readBridge } from "@/renderer/bridge"; export function PluginsSettings() { const plugins = useLocalizedPluginCatalog(); const loadPlugins = usePlugins((state) => state.load); + const loaded = usePlugins((state) => state.loaded); + const error = usePlugins((state) => state.error); const [selectedPluginId, setSelectedPluginId] = useState(); const returnFocusPluginId = useRef(undefined); const selectedPlugin = plugins.find((entry) => entry.plugin.name === selectedPluginId); @@ -34,6 +38,27 @@ export function PluginsSettings() { returnFocusPluginId.current = undefined; }, [selectedPluginId]); + if (!loaded) { + return ( +
+ Loading… +
+ ); + } + + if (error && plugins.length === 0) { + return ( +
+

+ Connection failed. +

+ +
+ ); + } + const openPlugin = (pluginId: string) => { returnFocusPluginId.current = pluginId; setSelectedPluginId(pluginId); diff --git a/src/supervisor/plugins/conformance.test.ts b/src/supervisor/plugins/conformance.test.ts index 62f6de30f..c34a4c46f 100644 --- a/src/supervisor/plugins/conformance.test.ts +++ b/src/supervisor/plugins/conformance.test.ts @@ -385,6 +385,82 @@ describe("mcp runtime", () => { expect(codes(rejected.diagnostics)).toEqual(["mcp-entry-unresolvable"]); }); + it("skips MCP servers unsupported by the host or project", async () => { + const plugin = await writePackage("unsupported", { + manifest: manifest("unsupported", { + extensions: { + "com.poracode.client": { + platforms: ["darwin"], + projectKinds: ["windows"], + }, + }, + }), + mcp: { + $schema: AGENT_PLUGINS_MCP_SCHEMA_URL, + mcpServers: { main: { type: "stdio", command: "server" } }, + }, + }); + const loaded = loadPluginFromDirectory(plugin, "bundled").plugin; + if (!loaded) throw new Error("plugin failed to load"); + const context = { + pluginDataRoot: join(root, "plugin-data"), + hostPlatform: "win32" as const, + projectLocation: { + kind: "wsl" as const, + distro: "Ubuntu", + linuxPath: "/repo", + uncPath: "\\\\wsl.localhost\\Ubuntu\\repo", + }, + }; + + expect(resolvePluginMcpServers([loaded], installed("unsupported"), context).servers).toEqual( + [], + ); + }); + + it("rejects stdio command and cwd symlinks that escape the package", async ({ skip }) => { + const outside = join(root, "outside"); + await mkdir(outside, { recursive: true }); + await writeFile(join(outside, "server"), "", "utf8"); + const commandDir = await writePackage("symlink-command", { + manifest: manifest("symlink-command"), + mcp: { + $schema: AGENT_PLUGINS_MCP_SCHEMA_URL, + mcpServers: { main: { type: "stdio", command: "./bin/server" } }, + }, + }); + const cwdDir = await writePackage("symlink-cwd", { + manifest: manifest("symlink-cwd"), + mcp: { + $schema: AGENT_PLUGINS_MCP_SCHEMA_URL, + mcpServers: { main: { type: "stdio", command: "server", cwd: "./work" } }, + }, + }); + try { + await symlink(outside, join(commandDir, "bin"), "junction"); + await symlink(outside, join(cwdDir, "work"), "junction"); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (["EACCES", "ENOSYS", "ENOTSUP", "EPERM", "UNKNOWN"].includes(code ?? "")) { + skip(); + return; + } + throw error; + } + + const commandPlugin = loadPluginFromDirectory(commandDir, "bundled").plugin; + const cwdPlugin = loadPluginFromDirectory(cwdDir, "bundled").plugin; + if (!commandPlugin || !cwdPlugin) throw new Error("plugin failed to load"); + const result = resolvePluginMcpServers( + [commandPlugin, cwdPlugin], + { ...installed("symlink-command"), ...installed("symlink-cwd") }, + { pluginDataRoot: join(root, "plugin-data") }, + ); + + expect(result.servers).toEqual([]); + expect(codes(result.diagnostics)).toEqual(["mcp-entry-unresolvable", "mcp-entry-unresolvable"]); + }); + it("rejects a cwd that escapes the package boundary", async () => { const plugin = await loadWithServers("bad-cwd", { main: { type: "stdio", command: "server", cwd: "../outside" }, diff --git a/src/supervisor/plugins/pluginMcpRuntime.ts b/src/supervisor/plugins/pluginMcpRuntime.ts index a5333dff9..dfa129c22 100644 --- a/src/supervisor/plugins/pluginMcpRuntime.ts +++ b/src/supervisor/plugins/pluginMcpRuntime.ts @@ -1,14 +1,21 @@ import { mkdirSync } from "node:fs"; import { isAbsolute, join, resolve } from "node:path"; -import type { InstalledPlugins, LoadedPlugin, McpServer, McpTransport } from "@/shared/contracts"; +import type { + InstalledPlugins, + LoadedPlugin, + McpServer, + McpTransport, + ProjectLocation, +} from "@/shared/contracts"; import { DEFAULT_MCP_SERVER_TIMEOUT_MS, isValidMcpServerName } from "@/shared/contracts"; +import { isPluginSupportedForProject } from "@/shared/plugins/catalog"; import { pluginDiagnostic, type PluginDiagnostic, type PluginMcpEntry, type PluginMcpStdioEntry, } from "@/shared/plugins/spec"; -import { relativePathInside } from "./pathContainment"; +import { relativePolicyPath } from "./pathContainment"; /** * Turns `mcp.json` declarations into the provider-agnostic `McpServer` records @@ -32,6 +39,9 @@ import { relativePathInside } from "./pathContainment"; export interface PluginMcpRuntimeContext { /** Parent directory holding one persistent data directory per plugin. */ pluginDataRoot: string; + /** Host/project policy is optional for direct conformance helpers. */ + hostPlatform?: NodeJS.Platform; + projectLocation?: ProjectLocation; } export interface ResolvedPluginMcpServers { @@ -75,7 +85,7 @@ function resolveStdioCommand( ): { command: string } | { error: string } { if (command.startsWith("./") || command.startsWith(".\\")) { const target = resolve(root, command); - if (relativePathInside(root, target) === undefined) { + if (relativePolicyPath(root, target) === undefined) { return { error: `command '${command}' resolves outside the package boundary` }; } return { command: target }; @@ -97,13 +107,13 @@ function resolveStdioCwd( const expanded = expandPlaceholders(cwd, root, data); // `${PLUGIN_DATA}` legitimately points outside the package, so it is the one // absolute working directory the spec permits. - if (expanded === data || relativePathInside(data, expanded) !== undefined) { + if (expanded === data || relativePolicyPath(data, expanded) !== undefined) { return { cwd: expanded }; } const target = isAbsolute(expanded) ? resolve(expanded) : resolve(root, expanded); // The package root itself is inside the boundary; `relativePathInside` returns // an empty relative path for it, which is not a containment failure. - if (target !== resolve(root) && relativePathInside(root, target) === undefined) { + if (target !== resolve(root) && relativePolicyPath(root, target) === undefined) { return { error: `cwd '${cwd}' resolves outside the package boundary` }; } return { cwd: target }; @@ -177,7 +187,17 @@ export function resolvePluginMcpServers( for (const plugin of plugins) { const state = installedPlugins[plugin.name]; - if (!state?.enabled || plugin.mcpServers.length === 0) continue; + if ( + !state?.enabled || + plugin.mcpServers.length === 0 || + !isPluginSupportedForProject( + plugin, + context.hostPlatform ?? process.platform, + context.projectLocation, + ) + ) { + continue; + } const data = pluginDataDirectory(context.pluginDataRoot, plugin.name); let dataReady: boolean | undefined; diff --git a/src/supervisor/runtime/sessionTypes.ts b/src/supervisor/runtime/sessionTypes.ts index 0b1994b71..08d7a7575 100644 --- a/src/supervisor/runtime/sessionTypes.ts +++ b/src/supervisor/runtime/sessionTypes.ts @@ -48,6 +48,8 @@ export interface SessionRuntime { pty?: IPty; projectLocation: ProjectLocation; config: ThreadConfig; + /** Effective provider launch config with globally disabled MCP cleared. */ + launchConfig?: ThreadConfig; /** MCP launch snapshot reused by restart and recovery paths. */ mcpLaunchSnapshot: McpLaunchSnapshot; sessionRef?: SessionRef; diff --git a/src/supervisor/runtime/threadOutputPipeline.ts b/src/supervisor/runtime/threadOutputPipeline.ts index fbe539202..b92030c57 100644 --- a/src/supervisor/runtime/threadOutputPipeline.ts +++ b/src/supervisor/runtime/threadOutputPipeline.ts @@ -158,6 +158,7 @@ export class ThreadOutputPipeline { status: session.status, attention: session.attention, config: session.config, + ...(session.launchConfig ? { launchConfig: session.launchConfig } : {}), ...(session.sessionRef ? { sessionRef: session.sessionRef } : {}), ...(session.slashCommands ? { slashCommands: session.slashCommands } : {}), canResumeWithConfig: session.canResumeWithConfig, diff --git a/src/supervisor/runtime/threadSession/invalidSessionRecovery.test.ts b/src/supervisor/runtime/threadSession/invalidSessionRecovery.test.ts index 720c1440c..b885c704d 100644 --- a/src/supervisor/runtime/threadSession/invalidSessionRecovery.test.ts +++ b/src/supervisor/runtime/threadSession/invalidSessionRecovery.test.ts @@ -188,6 +188,7 @@ describe("InvalidSessionRecoveryCoordinator", () => { const spawnInput = harness.spawnThread.mock.calls[0]![0]; expect(spawnInput).not.toHaveProperty("sessionRef"); expect(spawnInput.mcpLaunchSnapshot).toBe(harness.session.mcpLaunchSnapshot); + expect(spawnInput.launchConfig).toEqual(CONFIG); expect(spawnInput).toMatchObject({ threadId: THREAD_ID, launchPrompt: "", diff --git a/src/supervisor/runtime/threadSession/invalidSessionRecovery.ts b/src/supervisor/runtime/threadSession/invalidSessionRecovery.ts index 5f5439ec9..6df8474e5 100644 --- a/src/supervisor/runtime/threadSession/invalidSessionRecovery.ts +++ b/src/supervisor/runtime/threadSession/invalidSessionRecovery.ts @@ -134,6 +134,7 @@ export class InvalidSessionRecoveryCoordinator { launchPrompt: session.launchPrompt, command, mcpLaunchSnapshot, + launchConfig, ...(Object.keys(cliHookExtras.env).length > 0 ? { extraEnv: cliHookExtras.env } : {}), }); } diff --git a/src/supervisor/runtime/threadSession/managerOptions.ts b/src/supervisor/runtime/threadSession/managerOptions.ts index 0461cd463..c662d6e32 100644 --- a/src/supervisor/runtime/threadSession/managerOptions.ts +++ b/src/supervisor/runtime/threadSession/managerOptions.ts @@ -6,6 +6,7 @@ import type { ProjectLocation, PromptSegment, ThreadServerRequestId, + ThreadPresentationMode, } from "@/shared/contracts"; import type { CrossagentMcpHttpConfig } from "@/supervisor/agents/crossagentMcp"; import type { WslHostAccessResolver } from "@/supervisor/wsl/hostAccess"; @@ -81,7 +82,7 @@ export interface ThreadSessionManagerOptions { * They join the user's own servers, so every provider translator picks them up * without knowing the Agent Plugins specification exists. */ - resolvePluginMcpServers?(): McpServer[]; + resolvePluginMcpServers?(projectLocation: ProjectLocation): McpServer[]; /** Wrap servers with disabled tools in Poracode's same-environment filtering proxy. */ prepareMcpToolFilters?( servers: McpServer[], @@ -89,6 +90,13 @@ export interface ThreadSessionManagerOptions { ): Promise; /** Synchronize Poracode-owned provider skill projections before a new agent process starts. */ prepareSkillsForLaunch?(projectLocation: ProjectLocation, agentKind: AgentKind): Promise; + /** Enforce plugin skill policy before a segment reaches a provider. */ + filterPluginSkillSegments?(input: { + agentKind: AgentKind; + projectLocation: ProjectLocation; + presentationMode?: ThreadPresentationMode; + segments: PromptSegment[]; + }): Promise; /** * Portable-skills fallback for structured turns: returns inline SKILL.md * instructions for skill segments the provider can't load natively, or diff --git a/src/supervisor/runtime/threadSession/spawnPipeline.ts b/src/supervisor/runtime/threadSession/spawnPipeline.ts index d3c90e107..e561b06dc 100644 --- a/src/supervisor/runtime/threadSession/spawnPipeline.ts +++ b/src/supervisor/runtime/threadSession/spawnPipeline.ts @@ -98,6 +98,7 @@ export interface SpawnThreadInput { initialAttention?: ThreadAttention; suppressInitialStructuredIdle?: boolean; mcpLaunchSnapshot: McpLaunchSnapshot; + launchConfig?: ThreadConfig; } /** @@ -275,14 +276,22 @@ export class SpawnPipeline { // Terminal skills fallback: skill segments the CLI can't resolve natively // become short path-hint text before the prompt is typed into the PTY. // Structured turns keep the raw segments (they use inline injection). + const policySegments = wslSegments?.some((segment) => segment.kind === "skill") + ? ((await ctx.options.filterPluginSkillSegments?.({ + agentKind: payload.agentKind, + projectLocation: payload.projectLocation, + presentationMode: requestedPresentation, + segments: wslSegments, + })) ?? wslSegments) + : wslSegments; const effectiveSegments = - !useStructuredFlow && wslSegments?.some((segment) => segment.kind === "skill") + !useStructuredFlow && policySegments?.some((segment) => segment.kind === "skill") ? ((await ctx.options.rewriteTerminalSkillSegments?.({ agentKind: payload.agentKind, projectLocation: payload.projectLocation, - segments: wslSegments, - })) ?? wslSegments) - : wslSegments; + segments: policySegments, + })) ?? policySegments) + : policySegments; const initialPrompt = effectiveSegments && effectiveSegments.length > 0 ? (adapter.formatPromptSegments?.(effectiveSegments) ?? @@ -320,8 +329,12 @@ export class SpawnPipeline { payload.userMessageItemId, ) : undefined; + const optimisticLaunchConfig = effectiveLaunchConfig( + payload.config, + payload.disabledBuiltInMcpServerIds ?? [], + ); if (optimisticUserMessageItemId) { - this.emitOptimisticWorkingState(payload.threadId, payload.config); + this.emitOptimisticWorkingState(payload.threadId, payload.config, optimisticLaunchConfig); } // Prime the user's interactive-shell env (fnm / nvm / asdf / mise cd-hooks @@ -341,7 +354,7 @@ export class SpawnPipeline { }; let mcpServers = resolveEnabledMcpServers([ ...(payload.mcpServers ?? []), - ...(this.ctx.options.resolvePluginMcpServers?.() ?? []), + ...(this.ctx.options.resolvePluginMcpServers?.(payload.projectLocation) ?? []), ]); if (this.ctx.options.applyMcpServerAuthorization) { mcpServers = await this.ctx.options.applyMcpServerAuthorization(mcpServers); @@ -445,6 +458,7 @@ export class SpawnPipeline { suppressInitialStructuredIdle: optimisticUserMessageItemId !== undefined && !startInterrupted, mcpLaunchSnapshot, + launchConfig, }); if ( !startInterrupted && @@ -589,6 +603,7 @@ export class SpawnPipeline { ...(keepStructuredSession ? { structuredSession } : {}), ...(resolvedSessionRef ? { sessionRef: resolvedSessionRef } : {}), mcpLaunchSnapshot, + launchConfig, ...(shouldQueueInitialPrompt ? { pendingLaunchPrompt: initialPrompt } : {}), presentationMode: requestedPresentation, ...(deferToTerminal && !useStructuredFlow @@ -716,6 +731,7 @@ export class SpawnPipeline { structuredSession, sessionRef: session.sessionRef, mcpLaunchSnapshot, + launchConfig, ...(session.presentationMode ? { presentationMode: session.presentationMode } : {}), }); if (prompt.trim().length > 0 && structuredSession.startTurn) { @@ -814,12 +830,15 @@ export class SpawnPipeline { ...(keepStructuredSession ? { structuredSession } : {}), sessionRef: session.sessionRef, mcpLaunchSnapshot, + launchConfig, ...(session.presentationMode ? { presentationMode: session.presentationMode } : {}), }); } spawnThread(input: SpawnThreadInput): SessionRuntime { const ctx = this.ctx; + const mcpLaunchSnapshot = + input.mcpLaunchSnapshot ?? ({ mcpServers: [], disabledBuiltInMcpServerIds: [] } as const); // `thread-reset` is only consumed by the terminal panel (renderer scrollback // reset) and the renderer-side runtime-event/server-request slice clear. // GUI threads have no terminal scrollback, and clearing the slice would @@ -904,7 +923,10 @@ export class SpawnPipeline { ...(pty && command?.cleanup ? { launchCleanup: command.cleanup } : {}), projectLocation: input.projectLocation, config: input.config, - mcpLaunchSnapshot: input.mcpLaunchSnapshot, + mcpLaunchSnapshot, + launchConfig: + input.launchConfig ?? + effectiveLaunchConfig(input.config, mcpLaunchSnapshot.disabledBuiltInMcpServerIds), terminalSize: input.initialSize, launchPrompt: input.launchPrompt, ...(input.sessionRef ? { sessionRef: input.sessionRef } : {}), @@ -1218,13 +1240,18 @@ export class SpawnPipeline { return true; } - private emitOptimisticWorkingState(threadId: string, config: ThreadConfig): void { + private emitOptimisticWorkingState( + threadId: string, + config: ThreadConfig, + launchConfig: ThreadConfig, + ): void { this.ctx.options.emit({ type: "thread-state", threadId, status: "working", attention: "working", config, + launchConfig, canResumeWithConfig: false, threadStatusSource: "server", }); diff --git a/src/supervisor/runtime/threadSessionManager.restartTerminal.test.ts b/src/supervisor/runtime/threadSessionManager.restartTerminal.test.ts index 836d21a8e..75e4a9e99 100644 --- a/src/supervisor/runtime/threadSessionManager.restartTerminal.test.ts +++ b/src/supervisor/runtime/threadSessionManager.restartTerminal.test.ts @@ -226,6 +226,7 @@ describe("ThreadSessionManager terminal restart", () => { expect(restarted!.launchPrompt).toBe("resume work"); expect(restarted!.sessionRef).toEqual({ providerSessionId: "ses_existing" }); expect(restarted!.presentationMode).toBe("terminal"); + expect(restarted!.launchConfig).toEqual(CONFIG); expect(restarted!.structuredSession).toBeUndefined(); }); diff --git a/src/supervisor/runtime/threadSessionManager.ts b/src/supervisor/runtime/threadSessionManager.ts index a9723b2bd..085c05769 100644 --- a/src/supervisor/runtime/threadSessionManager.ts +++ b/src/supervisor/runtime/threadSessionManager.ts @@ -222,6 +222,7 @@ export class ThreadSessionManager { status: session.status, attention: session.attention, config: session.config, + ...(session.launchConfig ? { launchConfig: session.launchConfig } : {}), ...(session.sessionRef ? { sessionRef: session.sessionRef } : {}), ...(session.slashCommands ? { slashCommands: session.slashCommands } : {}), canResumeWithConfig: session.canResumeWithConfig, @@ -496,13 +497,14 @@ export class ThreadSessionManager { } const usesStructuredFlow = session.adapter.capabilities.liveInputMode === "server" || session.presentationMode === "gui"; - const effectiveSegments = payload.segments + const wslSegments = payload.segments ? await rewriteSegmentsForWsl(payload.segments, session.projectLocation, { preserveImageAttachments: usesStructuredFlow, preservePdfAttachments: usesStructuredFlow && session.adapter.capabilities.readsPdfAttachmentsFromHost === true, }) : undefined; + const effectiveSegments = await this.filterPluginSkillSegments(session, wslSegments); const prompt = this.formatSegmentsForPrompt(session, effectiveSegments, payload.prompt); const effectiveConfig = @@ -707,7 +709,8 @@ export class ThreadSessionManager { preserveImageAttachments: false, }) : undefined; - const effectiveSegments = await this.localizeWorkspaceAttachments(session, wslSegments); + const policySegments = await this.filterPluginSkillSegments(session, wslSegments); + const effectiveSegments = await this.localizeWorkspaceAttachments(session, policySegments); const formatted = this.formatSegmentsForPrompt(session, effectiveSegments, payload.prompt); // Collapse newlines so a raw PTY write cannot accidentally submit the line // (a bare \n reads as Enter to most shells/TUIs); the user submits manually. @@ -728,6 +731,24 @@ export class ThreadSessionManager { : fallbackPrompt; } + /** Enforce current plugin skill policy before a segment reaches a provider. */ + private async filterPluginSkillSegments( + session: SessionRuntime, + segments: PromptSegment[] | undefined, + ): Promise { + if (!segments?.some((segment) => segment.kind === "skill")) return segments; + return ( + (await this.options.filterPluginSkillSegments?.({ + agentKind: session.agentKind, + projectLocation: session.projectLocation, + ...(session.presentationMode + ? { presentationMode: session.presentationMode } + : { presentationMode: session.adapter.capabilities.presentationMode }), + segments, + })) ?? segments + ); + } + /** Portable-skills fallback for a structured turn (see managerOptions). */ private async resolveSkillTurnInjection( session: SessionRuntime, @@ -822,7 +843,12 @@ export class ThreadSessionManager { */ async setPendingSteer(payload: SetPendingSteerPayload): Promise { const session = this.requireSession(payload.threadId); - await this.steerCoordinator.setPendingSteer(session, payload); + if (payload.segments === undefined) { + await this.steerCoordinator.setPendingSteer(session, payload); + return; + } + const segments = await this.filterPluginSkillSegments(session, payload.segments); + await this.steerCoordinator.setPendingSteer(session, { ...payload, segments }); } /** diff --git a/src/supervisor/skills/SkillsService.test.ts b/src/supervisor/skills/SkillsService.test.ts index 84902213a..e022eb027 100644 --- a/src/supervisor/skills/SkillsService.test.ts +++ b/src/supervisor/skills/SkillsService.test.ts @@ -652,6 +652,50 @@ describe("SkillsService", () => { ).toEqual([userSkill]); }); + it("keeps an enabled plugin skill matched through the /mnt fallback", async () => { + // `wslpath -w` is unavailable here, so containment falls back to comparing + // the case-insensitive `/mnt` paths. That comparison must not leak its + // case folding into the relative path, or `SKILL.md` stops matching and an + // enabled skill is silently stripped from the prompt. + const pluginRoot = "E:\\Poracode\\resources\\plugins\\browser-tools"; + const pluginWslSkillsRoot = "/mnt/e/Poracode/resources/plugins/browser-tools/skills"; + const plugin = fakePluginPackage("browser-tools", pluginRoot, ["browser-control"]); + const bundledService = new SkillsService({ + adapters, + homeDirectory: () => home, + readInstalledPlugins: () => installPlugin({}, plugin), + readPlugins: () => [plugin], + hostPlatform: "win32", + resolveHostPathForWsl: async () => pluginWslSkillsRoot, + resolveWslWindowsPaths: async () => [], + resolveWslRealPaths: async (_distro, paths) => + paths.map((path) => + path === "/tmp/plugin-alias/SKILL.md" + ? `${pluginWslSkillsRoot}/browser-control/SKILL.md` + : path, + ), + }); + const pluginAlias = { + kind: "skill" as const, + name: "browser-control", + path: "/tmp/plugin-alias/SKILL.md", + invocation: "/browser-control", + provider: "Browser Tools", + scope: "global" as const, + }; + + expect( + await bundledService.filterPluginSkillSegments([pluginAlias], { + projectLocation: { + kind: "wsl", + distro: "Ubuntu", + linuxPath: "/repo", + uncPath: "\\\\wsl.localhost\\Ubuntu\\repo", + }, + }), + ).toEqual([pluginAlias]); + }); + it("fails closed when a WSL skill path cannot be canonicalized", async () => { const bundledService = new SkillsService({ adapters, diff --git a/src/supervisor/skills/pluginSkillPolicy.ts b/src/supervisor/skills/pluginSkillPolicy.ts index 7e6bd89ed..6d8947182 100644 --- a/src/supervisor/skills/pluginSkillPolicy.ts +++ b/src/supervisor/skills/pluginSkillPolicy.ts @@ -75,9 +75,16 @@ function relativePosixPolicyPath(root: string, target: string): string | undefin function relativeWslPolicyPath(root: string, target: string): string | undefined { const rootDrive = /^\/mnt\/([a-z])(?:\/|$)/iu.exec(root)?.[1]; const targetDrive = /^\/mnt\/([a-z])(?:\/|$)/iu.exec(target)?.[1]; - return rootDrive && targetDrive && rootDrive.toLowerCase() === targetDrive.toLowerCase() - ? relativePosixPolicyPath(root.toLowerCase(), target.toLowerCase()) - : relativePosixPolicyPath(root, target); + if (!rootDrive || !targetDrive || rootDrive.toLowerCase() !== targetDrive.toLowerCase()) { + return relativePosixPolicyPath(root, target); + } + // A `/mnt` DrvFs mount is case-insensitive, so containment is decided on the + // case-folded paths. The relative path itself keeps the target's own casing: + // callers still have to see the authored skill folder and `SKILL.md`. + const folded = relativePosixPolicyPath(root.toLowerCase(), target.toLowerCase()); + if (folded === undefined) return undefined; + const segments = posix.resolve(target).split("/"); + return segments.slice(segments.length - folded.split("/").length).join("/"); } async function resolveHostPathForWsl( diff --git a/src/supervisor/supervisorRuntime.ts b/src/supervisor/supervisorRuntime.ts index 77d87bf01..2d5203a29 100644 --- a/src/supervisor/supervisorRuntime.ts +++ b/src/supervisor/supervisorRuntime.ts @@ -425,11 +425,15 @@ export class SupervisorRuntime { }, applyMcpServerAuthorization: (servers) => this.mcpOAuthService.applyAuthorization(servers), prepareMcpToolFilters, - resolvePluginMcpServers: () => + resolvePluginMcpServers: (projectLocation) => resolvePluginMcpServers( this.pluginRegistry.listPlugins(), this.sharedSettingsCache.readFresh().installedPlugins, - { pluginDataRoot: this.pluginDataDir }, + { + pluginDataRoot: this.pluginDataDir, + hostPlatform: process.platform, + projectLocation, + }, ).servers, prepareSkillsForLaunch: async (projectLocation, agentKind) => { try { @@ -438,6 +442,14 @@ export class SupervisorRuntime { console.warn("[skills] failed to prepare provider skill projections:", error); } }, + filterPluginSkillSegments: async (input) => { + try { + return await this.skillsService.filterPluginSkillSegments(input.segments, input); + } catch (error) { + console.warn("[skills] failed to apply plugin skill policy:", error); + return [...input.segments]; + } + }, buildSkillTurnInjection: async (input) => { try { return await this.skillsService.buildTurnSkillInjection(input); From 76474d4c7387cc6df638572caba704a0eb0ac38e Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Thu, 6 Aug 2026 15:42:03 -0700 Subject: [PATCH 4/6] fix(plugins): normalize skill names and surface oauth/data errors - Slugify bundled plugin skill names (ci-debug, github, reviewer, outlook-*) to match their folders and enforce it in the conformance suite - Show OAuth sign-in errors on the plugin detail page and localize the new marketplace/diagnostic strings across all 12 catalogs - Validate MCP server skill presence against the plugin manifest at runtime and report missing data-folder diagnostics - Stamp the plugin registry cache with manifest/MCP file mtimes so edits invalidate cached entries - Harden WSL plugin-skill root policy, MCP server authorization, and catalog disabled-server state with tests --- .../plugins/github/skills/ci-debug/SKILL.md | 2 +- .../plugins/github/skills/github/SKILL.md | 2 +- .../github/skills/publish-changes/SKILL.md | 2 +- .../github/skills/review-follow-up/SKILL.md | 2 +- resources/plugins/outlook/plugin.json | 6 +- .../outlook/skills/outlook-calendar/SKILL.md | 2 +- .../outlook/skills/outlook-email/SKILL.md | 2 +- .../components/plugins/PluginDetail.tsx | 13 +- .../plugins/PluginMarketplace.test.tsx | 10 +- .../components/plugins/PluginMarketplace.tsx | 4 +- src/renderer/components/plugins/pluginCopy.ts | 73 ++++++++++- .../components/plugins/usePluginOauth.ts | 42 ++++-- src/renderer/locales/de/messages.po | 122 +++++++++++++++++- src/renderer/locales/en/messages.po | 122 +++++++++++++++++- src/renderer/locales/es/messages.po | 122 +++++++++++++++++- src/renderer/locales/fr/messages.po | 122 +++++++++++++++++- src/renderer/locales/ja/messages.po | 122 +++++++++++++++++- src/renderer/locales/ko/messages.po | 122 +++++++++++++++++- src/renderer/locales/pl/messages.po | 122 +++++++++++++++++- src/renderer/locales/pt-BR/messages.po | 122 +++++++++++++++++- src/renderer/locales/ru/messages.po | 122 +++++++++++++++++- src/renderer/locales/tr/messages.po | 122 +++++++++++++++++- src/renderer/locales/uk/messages.po | 122 +++++++++++++++++- src/renderer/locales/vi/messages.po | 122 +++++++++++++++++- src/renderer/locales/zh-CN/messages.po | 122 +++++++++++++++++- src/renderer/state/pluginsStore.ts | 7 - .../parts/PluginsSettings.test.tsx | 17 +++ .../SettingsOverlay/parts/PluginsSettings.tsx | 45 ++++--- src/shared/contracts/plugin.ts | 22 ++-- src/shared/plugins/catalog.test.ts | 31 +++++ src/shared/plugins/catalog.ts | 19 +-- src/supervisor/plugins/PluginRegistry.ts | 19 ++- src/supervisor/plugins/conformance.test.ts | 48 +++++++ src/supervisor/plugins/pluginMcpRuntime.ts | 34 +++-- .../runtime/threadSession/spawnPipeline.ts | 14 +- src/supervisor/skills/SkillsService.test.ts | 32 +++-- src/supervisor/skills/SkillsService.ts | 1 - src/supervisor/skills/pluginSkillPolicy.ts | 36 +++++- 38 files changed, 1886 insertions(+), 185 deletions(-) diff --git a/resources/plugins/github/skills/ci-debug/SKILL.md b/resources/plugins/github/skills/ci-debug/SKILL.md index 41401ceff..2eeb63b16 100644 --- a/resources/plugins/github/skills/ci-debug/SKILL.md +++ b/resources/plugins/github/skills/ci-debug/SKILL.md @@ -1,5 +1,5 @@ --- -name: "CI Debug" +name: ci-debug description: "Diagnose a failing GitHub Actions check by reading the real logs and finding the first true failure." --- diff --git a/resources/plugins/github/skills/github/SKILL.md b/resources/plugins/github/skills/github/SKILL.md index deff6f8ca..817da4109 100644 --- a/resources/plugins/github/skills/github/SKILL.md +++ b/resources/plugins/github/skills/github/SKILL.md @@ -1,5 +1,5 @@ --- -name: "GitHub" +name: github description: "Inspect repositories, review pull requests, triage issues, and follow CI through the GitHub MCP server." --- diff --git a/resources/plugins/github/skills/publish-changes/SKILL.md b/resources/plugins/github/skills/publish-changes/SKILL.md index 069858020..a9ea72ab8 100644 --- a/resources/plugins/github/skills/publish-changes/SKILL.md +++ b/resources/plugins/github/skills/publish-changes/SKILL.md @@ -1,5 +1,5 @@ --- -name: "Publish Changes" +name: publish-changes description: "Commit work, push a branch, and open a pull request with an accurate description." --- diff --git a/resources/plugins/github/skills/review-follow-up/SKILL.md b/resources/plugins/github/skills/review-follow-up/SKILL.md index 507819d9d..65d7f885a 100644 --- a/resources/plugins/github/skills/review-follow-up/SKILL.md +++ b/resources/plugins/github/skills/review-follow-up/SKILL.md @@ -1,5 +1,5 @@ --- -name: "Review Follow-up" +name: review-follow-up description: "Work through pull request review feedback: sort what is actionable, fix it, and reply accurately." --- diff --git a/resources/plugins/outlook/plugin.json b/resources/plugins/outlook/plugin.json index 55313948f..5cc753604 100644 --- a/resources/plugins/outlook/plugin.json +++ b/resources/plugins/outlook/plugin.json @@ -3,7 +3,10 @@ "name": "outlook", "version": "1.0.0", "description": "Triage Microsoft Outlook mail and manage your calendar.", - "author": { "name": "Softeria", "url": "https://github.com/softeria" }, + "author": { + "name": "Softeria", + "url": "https://github.com/softeria" + }, "homepage": "https://github.com/softeria/ms-365-mcp-server", "repository": "https://github.com/softeria/ms-365-mcp-server", "license": "MIT", @@ -13,6 +16,7 @@ "title": "Outlook", "category": "communication", "featured": true, + "projectKinds": ["windows", "posix"], "communityMaintained": true, "examplePrompt": "Triage my inbox, summarize the important threads, and show what's on my calendar today", "skills": { diff --git a/resources/plugins/outlook/skills/outlook-calendar/SKILL.md b/resources/plugins/outlook/skills/outlook-calendar/SKILL.md index b6f202fc6..6c47989be 100644 --- a/resources/plugins/outlook/skills/outlook-calendar/SKILL.md +++ b/resources/plugins/outlook/skills/outlook-calendar/SKILL.md @@ -1,5 +1,5 @@ --- -name: "Outlook Calendar" +name: outlook-calendar description: "Read an Outlook calendar, find meeting times, and create or move events through the Microsoft 365 MCP server." --- diff --git a/resources/plugins/outlook/skills/outlook-email/SKILL.md b/resources/plugins/outlook/skills/outlook-email/SKILL.md index 546c9e439..355201dea 100644 --- a/resources/plugins/outlook/skills/outlook-email/SKILL.md +++ b/resources/plugins/outlook/skills/outlook-email/SKILL.md @@ -1,5 +1,5 @@ --- -name: "Outlook Email" +name: outlook-email description: "Triage an Outlook inbox, summarize threads, and draft replies through the Microsoft 365 MCP server." --- diff --git a/src/renderer/components/plugins/PluginDetail.tsx b/src/renderer/components/plugins/PluginDetail.tsx index b2d0f9c6c..4e8000caa 100644 --- a/src/renderer/components/plugins/PluginDetail.tsx +++ b/src/renderer/components/plugins/PluginDetail.tsx @@ -14,7 +14,7 @@ import { import { PluginIcon } from "./PluginIcon"; import { PluginTag } from "./PluginTag"; import { usePluginOauth } from "./usePluginOauth"; -import type { LocalizedPlugin } from "./pluginCopy"; +import { useLocalizedPluginDiagnostic, type LocalizedPlugin } from "./pluginCopy"; export function PluginDetail(props: { plugin: LocalizedPlugin; @@ -39,6 +39,7 @@ export function PluginDetail(props: { const examplePrompt = plugin.poracode.examplePrompt; const closeSettings = usePanelStore((panel) => panel.closeSettings); const oauth = usePluginOauth(plugin); + const describeDiagnostic = useLocalizedPluginDiagnostic(); // Warnings are tolerated by the loader; errors mean something was dropped. const problems = plugin.diagnostics.filter((diagnostic) => diagnostic.severity === "error"); @@ -146,7 +147,9 @@ export function PluginDetail(props: {
    {problems.map((diagnostic, index) => ( -
  • {diagnostic.message}
  • +
  • + {describeDiagnostic(diagnostic)} +
  • ))}
@@ -224,7 +227,11 @@ export function PluginDetail(props: { ) : null} - {oauth.error ?

{oauth.error}

: null} + {oauth.error ? ( +

+ {oauth.error} +

+ ) : null} {props.plugin.skills.length > 0 ? ( { const onOpen = vi.fn<(pluginId: string) => void>(); render(); + // The shortcut is named distinctly from the card title so a screen reader + // does not read two identically-named controls for the same plugin. const strip = screen.getByRole("heading", { name: "Installed" }).closest("section")!; - expect(within(strip).getByRole("button", { name: "Chrome Tools" })).toBeInTheDocument(); - expect(within(strip).queryByRole("button", { name: "Browser Tools" })).not.toBeInTheDocument(); + expect(within(strip).getByRole("button", { name: "Open Chrome Tools" })).toBeInTheDocument(); + expect( + within(strip).queryByRole("button", { name: "Open Browser Tools" }), + ).not.toBeInTheDocument(); - fireEvent.click(within(strip).getByRole("button", { name: "Chrome Tools" })); + fireEvent.click(within(strip).getByRole("button", { name: "Open Chrome Tools" })); expect(onOpen).toHaveBeenCalledWith("chrome-tools"); }); diff --git a/src/renderer/components/plugins/PluginMarketplace.tsx b/src/renderer/components/plugins/PluginMarketplace.tsx index f2e25b493..05f4c8c8d 100644 --- a/src/renderer/components/plugins/PluginMarketplace.tsx +++ b/src/renderer/components/plugins/PluginMarketplace.tsx @@ -93,8 +93,8 @@ export function PluginMarketplace(props: { isIconOnly size="sm" variant="tertiary" - aria-label={entry.name} - data-plugin-id={entry.plugin.name} + aria-label={t`Open ${entry.name}`} + data-plugin-shortcut-id={entry.plugin.name} className="size-10 rounded-xl border border-[var(--hairline)] bg-surface-secondary p-0 text-foreground hover:border-[var(--hairline-strong)] focus-visible:border-[var(--hairline-strong)]" onPress={() => props.onOpen(entry.plugin.name)} > diff --git a/src/renderer/components/plugins/pluginCopy.ts b/src/renderer/components/plugins/pluginCopy.ts index 6e0efaecb..189fa77d2 100644 --- a/src/renderer/components/plugins/pluginCopy.ts +++ b/src/renderer/components/plugins/pluginCopy.ts @@ -1,5 +1,5 @@ import { useLingui } from "@lingui/react/macro"; -import type { LoadedPlugin, SkillEntry } from "@/shared/contracts"; +import type { LoadedPlugin, PluginDiagnostic, SkillEntry } from "@/shared/contracts"; import { usePlugins } from "@/renderer/state/pluginsStore"; /** @@ -145,3 +145,74 @@ export function resolveLocalizedPluginSkill( ); return { localizedPlugin, pluginSkill, localizedSkill }; } + +/** + * User-facing text for a loader diagnostic. + * + * `PluginDiagnostic.message` is written in the supervisor, which carries no + * catalogs, so it is English developer prose ("skills/ exists but is not a + * directory"). The `code` is the stable part — translate that and keep the raw + * message only as the technical detail for a code we do not recognize. + */ +export function useLocalizedPluginDiagnostic(): (diagnostic: PluginDiagnostic) => string { + const { t } = useLingui(); + + return (diagnostic) => { + const target = diagnostic.target; + switch (diagnostic.code) { + case "root-unresolvable": + return t`This plugin's folder could not be read.`; + case "manifest-missing": + return t`plugin.json is missing.`; + case "manifest-unreadable": + case "manifest-not-object": + return t`plugin.json could not be read as JSON.`; + case "manifest-invalid": + return t`plugin.json is not valid for the Agent Plugins specification.`; + case "manifest-schema-unsupported": + return t`plugin.json targets an Agent Plugins version this build does not support.`; + case "manifest-unknown-field": + return t`Ignored an unrecognized field in plugin.json.`; + case "manifest-extensions-not-object": + case "extension-invalid": + return t`This plugin's Poracode settings were ignored because they are not valid.`; + case "extension-unknown-skill": + return t`This plugin describes a skill it does not actually ship.`; + case "path-escapes-root": + return target + ? t`Skipped ${target} because it points outside the plugin folder.` + : t`Skipped a file because it points outside the plugin folder.`; + case "skills-location-wrong-kind": + case "skills-unreadable": + return t`This plugin's skills could not be read.`; + case "mcp-location-wrong-kind": + case "mcp-unreadable": + case "mcp-document-not-object": + return t`mcp.json could not be read, so this plugin's servers were skipped.`; + case "mcp-schema-unsupported": + case "mcp-schema-version-mismatch": + return t`mcp.json targets an Agent Plugins version this build does not support.`; + case "mcp-servers-not-object": + case "mcp-entry-invalid": + return target + ? t`Server ${target} is not configured correctly and was skipped.` + : t`A server is not configured correctly and was skipped.`; + case "mcp-entry-unresolvable": + return target + ? t`Server ${target} could not be started and was skipped.` + : t`A server could not be started and was skipped.`; + case "mcp-entry-host-only": + return target + ? t`Server ${target} runs on this computer and is unavailable for WSL projects.` + : t`This server runs on this computer and is unavailable for WSL projects.`; + case "mcp-name-unusable": + return target + ? t`Server ${target} has a name Poracode cannot use.` + : t`A server has a name Poracode cannot use.`; + case "plugin-data-unavailable": + return t`Poracode could not create this plugin's data folder.`; + default: + return diagnostic.message; + } + }; +} diff --git a/src/renderer/components/plugins/usePluginOauth.ts b/src/renderer/components/plugins/usePluginOauth.ts index 064ecb222..1a579c570 100644 --- a/src/renderer/components/plugins/usePluginOauth.ts +++ b/src/renderer/components/plugins/usePluginOauth.ts @@ -1,8 +1,9 @@ -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useLingui } from "@lingui/react/macro"; import type { LoadedPlugin, McpServer } from "@/shared/contracts"; import { DEFAULT_MCP_SERVER_TIMEOUT_MS } from "@/shared/contracts"; import { readBridge } from "@/renderer/bridge"; +import { pluginMcpServerId, pluginMcpServerName } from "@/shared/plugins/catalog"; /** * Connection state for a plugin's remote MCP servers. @@ -17,18 +18,33 @@ import { readBridge } from "@/renderer/bridge"; type ConnectionState = "unknown" | "connected" | "disconnected" | "connecting"; function remoteServerUrl(entry: LoadedPlugin["mcpServers"][number]["entry"]): string | undefined { - return entry.type === "stdio" ? undefined : entry.url; + // Must match what `pluginMcpRuntime.buildTransport` launches: the token store + // is keyed on the exact URL string. + return entry.type === "stdio" ? undefined : entry.url.trim(); } -/** Mirrors the shape `pluginMcpRuntime` builds, so the supervisor authorizes the same server. */ -function toMcpServer(plugin: LoadedPlugin, serverName: string, url: string): McpServer { +/** + * Mirrors the record `pluginMcpRuntime` builds so the supervisor authorizes the + * same server. `McpOAuthService.begin` only reads `transport.url`, but the + * transport kind and headers are carried through so the two sides cannot drift. + */ +function toMcpServer( + plugin: LoadedPlugin, + declaration: LoadedPlugin["mcpServers"][number], + url: string, +): McpServer { + const entry = declaration.entry; return { - id: `plugin:${plugin.name}:${serverName}`, - name: `${plugin.name}.${serverName}`, + id: pluginMcpServerId(plugin.name, declaration.name), + name: pluginMcpServerName(plugin.name, declaration.name), description: plugin.manifest.description ?? "", enabled: true, timeoutMs: DEFAULT_MCP_SERVER_TIMEOUT_MS, - transport: { type: "http", url, headers: {} }, + transport: { + type: entry.type === "streamable-http" ? "http" : "sse", + url, + headers: entry.type === "stdio" ? {} : { ...entry.headers }, + }, }; } @@ -38,7 +54,7 @@ export function usePluginOauth(plugin: LoadedPlugin) { const [pending, setPending] = useState(); const [error, setError] = useState(); - const refresh = useCallback(async () => { + const refresh = async () => { try { const status = await readBridge().getMcpOauthStatus({}); setAuthorizedUrls(status.authenticatedUrls); @@ -46,13 +62,15 @@ export function usePluginOauth(plugin: LoadedPlugin) { // Leave the state unknown rather than claiming a server is disconnected. setAuthorizedUrls(undefined); } - }, []); + }; const hasRemoteServer = plugin.mcpServers.some((server) => remoteServerUrl(server.entry)); useEffect(() => { if (hasRemoteServer) void refresh(); - }, [hasRemoteServer, refresh]); + // `refresh` only closes over setState, which is stable. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasRemoteServer]); const stateFor = (serverName: string): ConnectionState => { const server = plugin.mcpServers.find((candidate) => candidate.name === serverName); @@ -66,13 +84,13 @@ export function usePluginOauth(plugin: LoadedPlugin) { const connect = async (serverName: string) => { const server = plugin.mcpServers.find((candidate) => candidate.name === serverName); const url = server ? remoteServerUrl(server.entry) : undefined; - if (!url) return; + if (!server || !url) return; setPending(serverName); setError(undefined); try { const bridge = readBridge(); const begin = await bridge.beginMcpServerOauth({ - server: toMcpServer(plugin, serverName, url), + server: toMcpServer(plugin, server, url), }); if (begin.status === "error") { setError(t`Could not sign in to ${serverName}.`); diff --git a/src/renderer/locales/de/messages.po b/src/renderer/locales/de/messages.po index aab4fced6..8976242d7 100644 --- a/src/renderer/locales/de/messages.po +++ b/src/renderer/locales/de/messages.po @@ -693,6 +693,18 @@ msgstr "Ein Kandidat befindet sich nicht mehr auf seinem Experiment-Branch." msgid "A frozen worktree source requires a full commit hash" msgstr "Eine eingefrorene Worktree-Quelle erfordert einen vollständigen Commit-Hash" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Ein Server konnte nicht gestartet werden und wurde übersprungen." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Ein Server hat einen Namen, den Poracode nicht verwenden kann." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Ein Server ist nicht korrekt konfiguriert und wurde übersprungen." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "ein unterstützter Codierungsagent" @@ -2921,7 +2933,6 @@ msgstr "Verbindungsfehler." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Verbindung fehlgeschlagen." @@ -3213,6 +3224,10 @@ msgstr "{0} konnte nicht installiert werden." msgid "Couldn't list repositories." msgstr "Repositorys konnten nicht aufgelistet werden." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Plugins konnten nicht geladen werden." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Das Änderungsprotokoll konnte nicht geladen werden. Prüfe deine Verbindung oder sieh es dir auf GitHub an." @@ -5439,6 +5454,10 @@ msgstr "Pfad zur Identitätsdatei" msgid "Idle" msgstr "Leerlauf" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Ein unbekanntes Feld in plugin.json wurde ignoriert." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Notizen werden geladen…" #~ msgid "Loading PDF…" #~ msgstr "PDF wird geladen…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Plugins werden geladen…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "PR wird geladen…" @@ -6088,7 +6111,6 @@ msgstr "Workflow-Ausführungen werden geladen" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Laden…" @@ -6434,6 +6456,14 @@ msgstr "MCP-Toolaufrufe" msgid "MCP transport type" msgstr "MCP-Transporttyp" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "mcp.json konnte nicht gelesen werden, daher wurden die Server dieses Plugins übersprungen." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json zielt auf eine Agent-Plugins-Version, die dieser Build nicht unterstützt." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Änderungen von {crownedLabel} in {mergeTarget} zusammenführen?" @@ -7650,10 +7680,10 @@ msgstr "Nur wenn nicht fokussiert" msgid "Open" msgstr "Offen" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "{0} öffnen" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "{0} öffnen" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Plugin" msgid "Plugin folder" msgstr "Plugin-Ordner" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "plugin.json konnte nicht als JSON gelesen werden." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "plugin.json fehlt." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json entspricht nicht der Agent-Plugins-Spezifikation." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json zielt auf eine Agent-Plugins-Version, die dieser Build nicht unterstützt." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Beliebte Editor-Themes, an Poracode angepasst. Jedes folgt dem oben gew msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode und Ihre Agenten werden auf dem Remotecomputer ausgeführt. SSH wird nur zum Starten der Remote-Umgebung und zum Absichern des lokalen Tunnels verwendet." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode konnte den Datenordner dieses Plugins nicht anlegen." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode speichert Marketplace-Zugangsdaten nicht unverschlüsselt in den Einstellungen. Öffne den Katalog, um die sicherheitsgeprüften Skills zu durchsuchen." @@ -10170,6 +10220,38 @@ msgstr "Auswahl an Terminal gesendet." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Code-Konten nach Konfigurationsverzeichnis trennen oder ein Profil auf einen externen Anbieter (z.ai, …) ausrichten. Öffnen Sie ein Profil, um dessen Umgebungsvariablen, Modelle und Reasoning-Stufe zu konfigurieren." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Server {target} konnte nicht gestartet werden und wurde übersprungen." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "Server {target} hat einen Namen, den Poracode nicht verwenden kann." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "Server {target} ist nicht korrekt konfiguriert und wurde übersprungen." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "Server {target} läuft auf diesem Computer und ist für WSL-Projekte nicht verfügbar." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Server, die dieses Plugin in mcp.json deklariert. Poracode übergibt sie an jeden unterstützten Agenten." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Übersprungen" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target} wurde übersprungen, weil es außerhalb des Plugin-Ordners liegt." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Eine Datei wurde übersprungen, weil sie außerhalb des Plugin-Ordners liegt." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Dadurch wird der Branch „{0}“ dauerhaft von seinem Remote gelöscht. msgid "This permanently deletes the branch \"{0}\"." msgstr "Dadurch wird der Branch „{0}“ dauerhaft gelöscht." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Dieses Plugin beschreibt einen Skill, den es gar nicht mitliefert." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Der Ordner dieses Plugins konnte nicht gelesen werden." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Die Poracode-Einstellungen dieses Plugins wurden ignoriert, weil sie ungültig sind." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Die Skills dieses Plugins konnten nicht gelesen werden." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "dieses Projekt" @@ -11592,6 +11698,10 @@ msgstr "Dadurch wird der Worktree auf „{0}“ entfernt und anschließend der B msgid "This server requires authentication before Poracode can check it." msgstr "Dieser Server erfordert eine Authentifizierung, bevor Poracode ihn prüfen kann." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Dieser Server läuft auf diesem Computer und ist für WSL-Projekte nicht verfügbar." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Diese systemweite Tastenkombination ist nicht verfügbar. Wähle eine andere Tastenkombination." diff --git a/src/renderer/locales/en/messages.po b/src/renderer/locales/en/messages.po index f91480f57..50f466777 100644 --- a/src/renderer/locales/en/messages.po +++ b/src/renderer/locales/en/messages.po @@ -698,6 +698,18 @@ msgstr "A candidate is no longer on its experiment branch." msgid "A frozen worktree source requires a full commit hash" msgstr "A frozen worktree source requires a full commit hash" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "A server could not be started and was skipped." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "A server has a name Poracode cannot use." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "A server is not configured correctly and was skipped." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "a supported coding agent" @@ -2926,7 +2938,6 @@ msgstr "Connection error." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Connection failed." @@ -3218,6 +3229,10 @@ msgstr "Couldn't install {0}." msgid "Couldn't list repositories." msgstr "Couldn't list repositories." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Couldn't load plugins." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Couldn't load the changelog. Check your connection, or view it on GitHub." @@ -5444,6 +5459,10 @@ msgstr "Identity file path" msgid "Idle" msgstr "Idle" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Ignored an unrecognized field in plugin.json." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6037,6 +6056,10 @@ msgstr "Loading notes…" #~ msgid "Loading PDF…" #~ msgstr "Loading PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Loading plugins…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Loading PR…" @@ -6093,7 +6116,6 @@ msgstr "Loading workflow runs" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Loading…" @@ -6439,6 +6461,14 @@ msgstr "MCP tool calls" msgid "MCP transport type" msgstr "MCP transport type" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "mcp.json could not be read, so this plugin's servers were skipped." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json targets an Agent Plugins version this build does not support." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Merge {crownedLabel}'s changes into {mergeTarget}?" @@ -7655,10 +7685,10 @@ msgstr "Only when unfocused" msgid "Open" msgstr "Open" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Open {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Open {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8302,6 +8332,22 @@ msgstr "Plugin" msgid "Plugin folder" msgstr "Plugin folder" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "plugin.json could not be read as JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "plugin.json is missing." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json is not valid for the Agent Plugins specification." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json targets an Agent Plugins version this build does not support." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8325,6 +8371,10 @@ msgstr "Popular editor themes adapted to Poracode. Each follows the light or dar msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode could not create this plugin's data folder." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." @@ -10175,6 +10225,38 @@ msgstr "Sent selection to terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "Server \"{target}\" could not be started and was skipped." + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "Server \"{target}\" has a name Poracode cannot use." + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "Server \"{target}\" is not configured correctly and was skipped." + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "Server \"{target}\" runs on this computer and is unavailable for WSL projects." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Server {target} could not be started and was skipped." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "Server {target} has a name Poracode cannot use." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "Server {target} is not configured correctly and was skipped." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "Server {target} runs on this computer and is unavailable for WSL projects." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." @@ -10555,6 +10637,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Skipped" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "Skipped {target} because it points outside the plugin folder." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Skipped a file because it points outside the plugin folder." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11561,6 +11651,22 @@ msgstr "This permanently deletes the branch \"{0}\" from its remote." msgid "This permanently deletes the branch \"{0}\"." msgstr "This permanently deletes the branch \"{0}\"." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "This plugin describes a skill it does not actually ship." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "This plugin's folder could not be read." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "This plugin's Poracode settings were ignored because they are not valid." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "This plugin's skills could not be read." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "this project" @@ -11597,6 +11703,10 @@ msgstr "This removes the worktree on \"{0}\", then deletes the branch." msgid "This server requires authentication before Poracode can check it." msgstr "This server requires authentication before Poracode can check it." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "This server runs on this computer and is unavailable for WSL projects." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "This system-wide shortcut is unavailable. Choose another key combination." diff --git a/src/renderer/locales/es/messages.po b/src/renderer/locales/es/messages.po index edfe43dc8..7c2aafa3c 100644 --- a/src/renderer/locales/es/messages.po +++ b/src/renderer/locales/es/messages.po @@ -693,6 +693,18 @@ msgstr "Un candidato ya no está en la rama de su experimento." msgid "A frozen worktree source requires a full commit hash" msgstr "Una fuente de worktree inmovilizada requiere un hash de commit completo" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "No se pudo iniciar un servidor y se omitió." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Un servidor tiene un nombre que Poracode no puede usar." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Un servidor no está configurado correctamente y se omitió." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "un agente de codificación compatible" @@ -2921,7 +2933,6 @@ msgstr "Error de conexión." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Error de conexión." @@ -3213,6 +3224,10 @@ msgstr "No se pudo instalar {0}." msgid "Couldn't list repositories." msgstr "No se pudieron listar los repositorios." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "No se pudieron cargar los plugins." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "No se pudo cargar el registro de cambios. Revisa tu conexión o consúltalo en GitHub." @@ -5439,6 +5454,10 @@ msgstr "Ruta del archivo de identidad" msgid "Idle" msgstr "Inactivo" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Se ignoró un campo desconocido en plugin.json." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Cargando notas…" #~ msgid "Loading PDF…" #~ msgstr "Cargando PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Cargando plugins…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Cargando PR…" @@ -6088,7 +6111,6 @@ msgstr "Cargando ejecuciones de flujos de trabajo" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Cargando…" @@ -6434,6 +6456,14 @@ msgstr "Llamadas a herramientas MCP" msgid "MCP transport type" msgstr "Tipo de transporte MCP" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "No se pudo leer mcp.json, así que se omitieron los servidores de este plugin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json apunta a una versión de Agent Plugins que esta compilación no admite." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "¿Fusionar los cambios de {crownedLabel} en {mergeTarget}?" @@ -7650,10 +7680,10 @@ msgstr "Solo cuando no esté en primer plano" msgid "Open" msgstr "Abrir" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Abrir {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Abrir {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Plugin" msgid "Plugin folder" msgstr "Carpeta de plugins" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "No se pudo leer plugin.json como JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "Falta plugin.json." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json no es válido para la especificación Agent Plugins." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json apunta a una versión de Agent Plugins que esta compilación no admite." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Temas populares de editores adaptados a Poracode. Cada uno sigue el modo msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode y tus agentes se ejecutan en la máquina remota. SSH solo se usa para iniciar el entorno remoto y proteger su túnel local." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode no pudo crear la carpeta de datos de este plugin." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode no guarda las credenciales del marketplace como texto sin cifrar en los ajustes. Abre el catálogo para explorar sus skills analizadas por seguridad." @@ -10170,6 +10220,38 @@ msgstr "Selección enviada al terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Separa las cuentas de Claude Code por directorio de configuración, o apunta un perfil a un proveedor externo (z.ai, …). Abre un perfil para configurar sus variables de entorno, modelos y esfuerzo." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "No se pudo iniciar el servidor {target} y se omitió." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "El servidor {target} tiene un nombre que Poracode no puede usar." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "El servidor {target} no está configurado correctamente y se omitió." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "El servidor {target} se ejecuta en este equipo y no está disponible para proyectos WSL." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Servidores que este plugin declara en mcp.json. Poracode los pasa a todos los agentes compatibles." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Omitido" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "Se omitió {target} porque apunta fuera de la carpeta del plugin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Se omitió un archivo porque apunta fuera de la carpeta del plugin." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Esto elimina permanentemente la rama \"{0}\" de su remoto." msgid "This permanently deletes the branch \"{0}\"." msgstr "Esto elimina permanentemente la rama \"{0}\"." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Este plugin describe una skill que en realidad no incluye." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "No se pudo leer la carpeta de este plugin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Se ignoraron los ajustes de Poracode de este plugin porque no son válidos." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "No se pudieron leer las skills de este plugin." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "este proyecto" @@ -11592,6 +11698,10 @@ msgstr "Esto elimina el worktree en \"{0}\" y luego elimina la rama." msgid "This server requires authentication before Poracode can check it." msgstr "Este servidor requiere autenticación antes de que Poracode pueda comprobarlo." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Este servidor se ejecuta en este equipo y no está disponible para proyectos WSL." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Este atajo global no está disponible. Elige otra combinación de teclas." diff --git a/src/renderer/locales/fr/messages.po b/src/renderer/locales/fr/messages.po index bef87eec9..a0db3b7b6 100644 --- a/src/renderer/locales/fr/messages.po +++ b/src/renderer/locales/fr/messages.po @@ -693,6 +693,18 @@ msgstr "Un candidat n’est plus sur la branche de son expérience." msgid "A frozen worktree source requires a full commit hash" msgstr "Une source d'arbre de travail figée nécessite un hash de commit complet" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Un serveur n'a pas pu démarrer et a été ignoré." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Un serveur porte un nom que Poracode ne peut pas utiliser." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Un serveur n'est pas configuré correctement et a été ignoré." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "un agent de codage pris en charge" @@ -2921,7 +2933,6 @@ msgstr "Erreur de connexion." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Échec de la connexion." @@ -3213,6 +3224,10 @@ msgstr "Impossible d’installer {0}." msgid "Couldn't list repositories." msgstr "Impossible de lister les référentiels." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Impossible de charger les plugins." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Impossible de charger le journal des modifications. Vérifiez votre connexion ou consultez-le sur GitHub." @@ -5439,6 +5454,10 @@ msgstr "Chemin du fichier d’identité" msgid "Idle" msgstr "Inactif" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Un champ inconnu de plugin.json a été ignoré." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Chargement des notes…" #~ msgid "Loading PDF…" #~ msgstr "Chargement du PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Chargement des plugins…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Chargement de la PR…" @@ -6088,7 +6111,6 @@ msgstr "Chargement des exécutions de workflows" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Chargement…" @@ -6434,6 +6456,14 @@ msgstr "Appels d’outils MCP" msgid "MCP transport type" msgstr "Type de transport MCP" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "mcp.json n'a pas pu être lu, les serveurs de ce plugin ont donc été ignorés." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json vise une version d'Agent Plugins que cette build ne prend pas en charge." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Fusionner les modifications de {crownedLabel} dans {mergeTarget} ?" @@ -7649,10 +7679,10 @@ msgstr "Uniquement lorsque la fenêtre n'est pas active" msgid "Open" msgstr "Ouvrir" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Ouvrir {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Ouvrir {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8296,6 +8326,22 @@ msgstr "Plugin" msgid "Plugin folder" msgstr "Dossier des plugins" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "plugin.json n'a pas pu être lu en tant que JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "plugin.json est manquant." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json n'est pas valide pour la spécification Agent Plugins." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json vise une version d'Agent Plugins que cette build ne prend pas en charge." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8319,6 +8365,10 @@ msgstr "Thèmes d'éditeur populaires adaptés à Poracode. Chacun suit le mode msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode et vos agents s’exécutent sur la machine distante. SSH sert uniquement à démarrer l’environnement distant et à sécuriser son tunnel local." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode n'a pas pu créer le dossier de données de ce plugin." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode ne stocke pas les identifiants de marketplace en clair dans les paramètres. Ouvrez le catalogue pour parcourir ses skills analysées sur le plan de la sécurité." @@ -10169,6 +10219,38 @@ msgstr "Sélection envoyée au terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Séparez les comptes Claude Code par dossier de configuration, ou dirigez un profil vers un fournisseur externe (z.ai, …). Ouvrez un profil pour configurer ses variables d'environnement, ses modèles et son effort." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Le serveur {target} n'a pas pu démarrer et a été ignoré." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "Le serveur {target} porte un nom que Poracode ne peut pas utiliser." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "Le serveur {target} n'est pas configuré correctement et a été ignoré." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "Le serveur {target} s'exécute sur cet ordinateur et n'est pas disponible pour les projets WSL." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Serveurs déclarés par ce plugin dans mcp.json. Poracode les transmet à tous les agents pris en charge." @@ -10549,6 +10631,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Ignoré" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target} a été ignoré car il pointe hors du dossier du plugin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Un fichier a été ignoré car il pointe hors du dossier du plugin." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11555,6 +11645,22 @@ msgstr "Cela supprime définitivement la branche \"{0}\" de son dépôt distant. msgid "This permanently deletes the branch \"{0}\"." msgstr "Cela supprime définitivement la branche \"{0}\"." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Ce plugin décrit une compétence qu'il ne fournit pas réellement." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Le dossier de ce plugin n'a pas pu être lu." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Les réglages Poracode de ce plugin ont été ignorés car ils ne sont pas valides." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Les compétences de ce plugin n'ont pas pu être lues." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "ce projet" @@ -11591,6 +11697,10 @@ msgstr "Cela supprime l'arbre de travail sur \"{0}\", puis supprime la branche." msgid "This server requires authentication before Poracode can check it." msgstr "Ce serveur nécessite une authentification avant que Poracode puisse le vérifier." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Ce serveur s'exécute sur cet ordinateur et n'est pas disponible pour les projets WSL." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Ce raccourci système n’est pas disponible. Choisissez une autre combinaison de touches." diff --git a/src/renderer/locales/ja/messages.po b/src/renderer/locales/ja/messages.po index 64c5b78ab..3068e40be 100644 --- a/src/renderer/locales/ja/messages.po +++ b/src/renderer/locales/ja/messages.po @@ -692,6 +692,18 @@ msgstr "候補が実験ブランチ上にありません。" msgid "A frozen worktree source requires a full commit hash" msgstr "固定されたワークツリーのソースには完全なコミットハッシュが必要です" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "サーバーを起動できなかったためスキップしました。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Poracode が使用できない名前のサーバーがあります。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "正しく設定されていないサーバーをスキップしました。" + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "サポートされているコーディング エージェント" @@ -2920,7 +2932,6 @@ msgstr "接続エラーです。" #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "接続に失敗しました。" @@ -3212,6 +3223,10 @@ msgstr "{0} をインストールできませんでした。" msgid "Couldn't list repositories." msgstr "リポジトリを一覧表示できませんでした。" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "プラグインを読み込めませんでした。" + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "変更履歴を読み込めませんでした。接続を確認するか、GitHub でご覧ください。" @@ -5438,6 +5453,10 @@ msgstr "秘密鍵ファイルのパス" msgid "Idle" msgstr "アイドル状態" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "plugin.json の不明なフィールドを無視しました。" + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6031,6 +6050,10 @@ msgstr "メモを読み込んでいます…" #~ msgid "Loading PDF…" #~ msgstr "PDFを読み込み中…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "プラグインを読み込み中…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "PRを読み込み中…" @@ -6087,7 +6110,6 @@ msgstr "ワークフロー実行を読み込み中" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "読み込み中…" @@ -6433,6 +6455,14 @@ msgstr "MCP ツール呼び出し" msgid "MCP transport type" msgstr "MCPトランスポートタイプ" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "mcp.json を読み取れなかったため、このプラグインのサーバーをスキップしました。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json はこのビルドが対応していない Agent Plugins バージョンを指定しています。" + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "{crownedLabel} の変更を {mergeTarget} にマージしますか?" @@ -7648,10 +7678,10 @@ msgstr "焦点が合っていないときのみ" msgid "Open" msgstr "開く" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "{0} を開く" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "{0} を開く" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8295,6 +8325,22 @@ msgstr "プラグイン" msgid "Plugin folder" msgstr "プラグインフォルダ" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "plugin.json を JSON として読み取れませんでした。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "plugin.json がありません。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json は Agent Plugins 仕様として正しくありません。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json はこのビルドが対応していない Agent Plugins バージョンを指定しています。" + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8318,6 +8364,10 @@ msgstr "人気のエディター テーマを Poracode に適合させました msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode とエージェントはリモートマシン上で実行されます。SSH はリモート環境の起動とローカルトンネルの保護にのみ使用されます。" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode はこのプラグインのデータフォルダを作成できませんでした。" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode はマーケットプレイスの認証情報を設定に平文で保存しません。カタログを開いて、セキュリティスキャン済みのスキルを確認してください。" @@ -10168,6 +10218,38 @@ msgstr "選択内容を端末に送信しました。" msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Codeのアカウントを設定ディレクトリごとに分離するか、プロファイルを外部プロバイダー(z.aiなど)に向けます。プロファイルを開くと、その環境変数・モデル・推論レベルを設定できます。" +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "サーバー {target} を起動できなかったためスキップしました。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "サーバー {target} は Poracode が使用できない名前です。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "サーバー {target} は正しく設定されていないためスキップしました。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "サーバー {target} はこのコンピューターで動作するため、WSL プロジェクトでは利用できません。" + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "このプラグインが mcp.json で宣言しているサーバーです。Poracode はサポートされているすべてのエージェントに渡します。" @@ -10548,6 +10630,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "スキップ済み" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target} はプラグインフォルダの外を指しているためスキップしました。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "プラグインフォルダの外を指しているファイルをスキップしました。" + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11554,6 +11644,22 @@ msgstr "これにより、ブランチ「{0}」がリモートから完全に削 msgid "This permanently deletes the branch \"{0}\"." msgstr "これにより、ブランチ「{0}」が完全に削除されます。" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "このプラグインは実際には含まれていないスキルを記述しています。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "このプラグインのフォルダを読み取れませんでした。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "このプラグインの Poracode 設定は無効なため無視されました。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "このプラグインのスキルを読み取れませんでした。" + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "このプロジェクト" @@ -11590,6 +11696,10 @@ msgstr "これにより、「{0}」のワークツリーが削除され、その msgid "This server requires authentication before Poracode can check it." msgstr "Poracode が確認するには、事前にこのサーバーでの認証が必要です。" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "このサーバーはこのコンピューターで動作するため、WSL プロジェクトでは利用できません。" + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "このシステム全体のショートカットは使用できません。別のキーの組み合わせを選択してください。" diff --git a/src/renderer/locales/ko/messages.po b/src/renderer/locales/ko/messages.po index af7bbc0ea..f278aa9c3 100644 --- a/src/renderer/locales/ko/messages.po +++ b/src/renderer/locales/ko/messages.po @@ -693,6 +693,18 @@ msgstr "후보가 더 이상 실험 브랜치에 있지 않습니다." msgid "A frozen worktree source requires a full commit hash" msgstr "고정된 작업 트리 소스에는 전체 커밋 해시가 필요합니다" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "서버를 시작할 수 없어 건너뛰었습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Poracode가 사용할 수 없는 이름의 서버가 있습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "올바르게 구성되지 않은 서버를 건너뛰었습니다." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "지원되는 코딩 에이전트" @@ -2921,7 +2933,6 @@ msgstr "연결 오류입니다." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "연결에 실패했습니다." @@ -3213,6 +3224,10 @@ msgstr "{0}을(를) 설치할 수 없습니다." msgid "Couldn't list repositories." msgstr "저장소를 나열할 수 없습니다." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "플러그인을 불러오지 못했습니다." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "변경 내역을 불러오지 못했습니다. 연결을 확인하거나 GitHub에서 확인하세요." @@ -5439,6 +5454,10 @@ msgstr "ID 파일 경로" msgid "Idle" msgstr "유휴" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "plugin.json의 알 수 없는 필드를 무시했습니다." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "메모 로드 중…" #~ msgid "Loading PDF…" #~ msgstr "PDF 불러오는 중…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "플러그인 불러오는 중…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "PR 로드 중…" @@ -6088,7 +6111,6 @@ msgstr "워크플로 실행 불러오는 중" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "로드 중…" @@ -6434,6 +6456,14 @@ msgstr "MCP 도구 호출" msgid "MCP transport type" msgstr "MCP 전송 유형" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "mcp.json을 읽을 수 없어 이 플러그인의 서버를 건너뛰었습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json이 이 빌드에서 지원하지 않는 Agent Plugins 버전을 지정합니다." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "{crownedLabel}의 변경 사항을 {mergeTarget}에 병합할까요?" @@ -7650,10 +7680,10 @@ msgstr "포커스가 없을 때만" msgid "Open" msgstr "열기" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "{0} 열기" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "{0} 열기" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "플러그인" msgid "Plugin folder" msgstr "플러그인 폴더" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "plugin.json을 JSON으로 읽을 수 없습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "plugin.json이 없습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json이 Agent Plugins 사양에 맞지 않습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json이 이 빌드에서 지원하지 않는 Agent Plugins 버전을 지정합니다." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Poracode에 맞게 조정된 인기 있는 편집기 테마입니다. msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode와 에이전트는 원격 머신에서 실행됩니다. SSH는 원격 환경을 시작하고 로컬 터널을 보호하는 데만 사용됩니다." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode가 이 플러그인의 데이터 폴더를 만들지 못했습니다." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode는 마켓플레이스 자격 증명을 설정에 일반 텍스트로 저장하지 않습니다. 카탈로그를 열어 보안 검사를 마친 스킬을 살펴보세요." @@ -10170,6 +10220,38 @@ msgstr "선택 항목을 터미널로 보냈습니다." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Code 계정을 설정 디렉터리별로 분리하거나, 프로필을 외부 제공자(z.ai 등)로 지정합니다. 프로필을 열어 환경 변수, 모델, 추론 강도를 구성하세요." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "서버 {target}을(를) 시작할 수 없어 건너뛰었습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "서버 {target}의 이름은 Poracode가 사용할 수 없습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "서버 {target}이(가) 올바르게 구성되지 않아 건너뛰었습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "서버 {target}은(는) 이 컴퓨터에서 실행되며 WSL 프로젝트에서는 사용할 수 없습니다." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "이 플러그인이 mcp.json에 선언한 서버입니다. Poracode가 지원되는 모든 에이전트에 전달합니다." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "건너뜀" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target}이(가) 플러그인 폴더 밖을 가리켜 건너뛰었습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "플러그인 폴더 밖을 가리키는 파일을 건너뛰었습니다." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "이렇게 하면 원격에서 \"{0}\" 분기가 영구적으로 삭제 msgid "This permanently deletes the branch \"{0}\"." msgstr "이렇게 하면 \"{0}\" 분기가 영구적으로 삭제됩니다." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "이 플러그인은 실제로 제공하지 않는 스킬을 설명합니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "이 플러그인의 폴더를 읽을 수 없습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "이 플러그인의 Poracode 설정이 유효하지 않아 무시되었습니다." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "이 플러그인의 스킬을 읽을 수 없습니다." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "이 프로젝트" @@ -11592,6 +11698,10 @@ msgstr "이렇게 하면 \"{0}\"의 작업 트리가 제거된 다음 분기가 msgid "This server requires authentication before Poracode can check it." msgstr "Poracode에서 이 서버를 확인하려면 먼저 인증해야 합니다." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "이 서버는 이 컴퓨터에서 실행되며 WSL 프로젝트에서는 사용할 수 없습니다." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "이 시스템 전역 단축키를 사용할 수 없습니다. 다른 키 조합을 선택하세요." diff --git a/src/renderer/locales/pl/messages.po b/src/renderer/locales/pl/messages.po index e171d6ba4..1d62a1b04 100644 --- a/src/renderer/locales/pl/messages.po +++ b/src/renderer/locales/pl/messages.po @@ -693,6 +693,18 @@ msgstr "Kandydat nie znajduje się już na swojej gałęzi eksperymentu." msgid "A frozen worktree source requires a full commit hash" msgstr "Zamrożone źródło drzewa roboczego wymaga pełnego skrótu commita" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Nie udało się uruchomić serwera, więc został pominięty." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Serwer ma nazwę, której Poracode nie może użyć." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Serwer nie jest poprawnie skonfigurowany i został pominięty." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "obsługiwany agent kodujący" @@ -2921,7 +2933,6 @@ msgstr "Błąd połączenia." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Połączenie nie powiodło się." @@ -3213,6 +3224,10 @@ msgstr "Nie udało się zainstalować {0}." msgid "Couldn't list repositories." msgstr "Nie udało się wyświetlić listy repozytoriów." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Nie udało się wczytać wtyczek." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Nie udało się wczytać dziennika zmian. Sprawdź połączenie lub zobacz go na GitHubie." @@ -5439,6 +5454,10 @@ msgstr "Ścieżka pliku tożsamości" msgid "Idle" msgstr "Bezczynny" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Zignorowano nieznane pole w plugin.json." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Ładowanie notatek…" #~ msgid "Loading PDF…" #~ msgstr "Wczytywanie pliku PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Wczytywanie wtyczek…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Ładowanie PR…" @@ -6088,7 +6111,6 @@ msgstr "Ładowanie uruchomień przepływów pracy" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Ładowanie…" @@ -6434,6 +6456,14 @@ msgstr "Wywołania narzędzi MCP" msgid "MCP transport type" msgstr "Typ transportu MCP" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "Nie udało się odczytać mcp.json, więc serwery tej wtyczki zostały pominięte." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json wskazuje wersję Agent Plugins nieobsługiwaną przez tę kompilację." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Scalić zmiany {crownedLabel} z {mergeTarget}?" @@ -7650,10 +7680,10 @@ msgstr "Tylko gdy nieaktywne" msgid "Open" msgstr "Otwórz" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Otwórz {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Otwórz {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Wtyczka" msgid "Plugin folder" msgstr "Folder wtyczek" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "Nie udało się odczytać plugin.json jako JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "Brakuje pliku plugin.json." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json nie jest zgodny ze specyfikacją Agent Plugins." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json wskazuje wersję Agent Plugins nieobsługiwaną przez tę kompilację." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Popularne motywy edytorów dostosowane do Poracode. Każdy z nich jest z msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode i Twoi agenci działają na komputerze zdalnym. SSH służy tylko do uruchamiania środowiska zdalnego i zabezpieczania jego lokalnego tunelu." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode nie mógł utworzyć folderu danych tej wtyczki." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode nie przechowuje danych logowania do marketplace w ustawieniach jako zwykłego tekstu. Otwórz katalog, aby przeglądać skille przeskanowane pod kątem bezpieczeństwa." @@ -10170,6 +10220,38 @@ msgstr "Wysłano zaznaczenie do terminala." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Oddziel konta Claude Code według katalogu konfiguracji lub skieruj profil na zewnętrznego dostawcę (z.ai, …). Otwórz profil, aby skonfigurować jego zmienne środowiskowe, modele i poziom rozumowania." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Nie udało się uruchomić serwera {target}, więc został pominięty." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "Serwer {target} ma nazwę, której Poracode nie może użyć." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "Serwer {target} nie jest poprawnie skonfigurowany i został pominięty." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "Serwer {target} działa na tym komputerze i jest niedostępny dla projektów WSL." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Serwery deklarowane przez tę wtyczkę w mcp.json. Poracode przekazuje je każdemu obsługiwanemu agentowi." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Pominięto" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "Pominięto {target}, ponieważ wskazuje poza folder wtyczki." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Pominięto plik, ponieważ wskazuje poza folder wtyczki." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Spowoduje to trwałe usunięcie gałęzi „{0}” ze zdalnego repozytor msgid "This permanently deletes the branch \"{0}\"." msgstr "Spowoduje to trwałe usunięcie gałęzi „{0}”." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Ta wtyczka opisuje umiejętność, której faktycznie nie dostarcza." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Nie udało się odczytać folderu tej wtyczki." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Ustawienia Poracode tej wtyczki zostały zignorowane, ponieważ są nieprawidłowe." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Nie udało się odczytać umiejętności tej wtyczki." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "ten projekt" @@ -11592,6 +11698,10 @@ msgstr "Spowoduje to usunięcie drzewa roboczego na „{0}”, a następnie usun msgid "This server requires authentication before Poracode can check it." msgstr "Ten serwer wymaga uwierzytelnienia, zanim aplikacja Poracode będzie mogła go sprawdzić." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Ten serwer działa na tym komputerze i jest niedostępny dla projektów WSL." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Ten skrót globalny jest niedostępny. Wybierz inną kombinację klawiszy." diff --git a/src/renderer/locales/pt-BR/messages.po b/src/renderer/locales/pt-BR/messages.po index ca969e12c..06e3986aa 100644 --- a/src/renderer/locales/pt-BR/messages.po +++ b/src/renderer/locales/pt-BR/messages.po @@ -693,6 +693,18 @@ msgstr "Um candidato não está mais na ramificação do experimento." msgid "A frozen worktree source requires a full commit hash" msgstr "Uma origem congelada da árvore de trabalho requer um hash completo do commit" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Não foi possível iniciar um servidor, que foi ignorado." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Um servidor tem um nome que o Poracode não pode usar." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Um servidor não está configurado corretamente e foi ignorado." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "um agente de codificação compatível" @@ -2921,7 +2933,6 @@ msgstr "Erro de conexão." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Falha na conexão." @@ -3213,6 +3224,10 @@ msgstr "Não foi possível instalar {0}." msgid "Couldn't list repositories." msgstr "Não foi possível listar os repositórios." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Não foi possível carregar os plugins." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Não foi possível carregar o registro de alterações. Verifique sua conexão ou veja no GitHub." @@ -5439,6 +5454,10 @@ msgstr "Caminho do arquivo de identidade" msgid "Idle" msgstr "Inativo" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Um campo desconhecido em plugin.json foi ignorado." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Carregando notas…" #~ msgid "Loading PDF…" #~ msgstr "Carregando PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Carregando plugins…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Carregando PR…" @@ -6088,7 +6111,6 @@ msgstr "Carregando execuções de fluxo de trabalho" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Carregando…" @@ -6434,6 +6456,14 @@ msgstr "Chamadas de ferramentas MCP" msgid "MCP transport type" msgstr "Tipo de transporte MCP" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "Não foi possível ler mcp.json, então os servidores deste plugin foram ignorados." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json aponta para uma versão do Agent Plugins que esta build não suporta." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Mesclar as alterações de {crownedLabel} em {mergeTarget}?" @@ -7650,10 +7680,10 @@ msgstr "Somente quando estiver fora de foco" msgid "Open" msgstr "Abrir" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Abrir {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Abrir {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Plugin" msgid "Plugin folder" msgstr "Pasta de plugins" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "Não foi possível ler plugin.json como JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "plugin.json está ausente." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json não é válido para a especificação Agent Plugins." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json aponta para uma versão do Agent Plugins que esta build não suporta." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Temas de editores populares adaptados ao Poracode. Cada um segue o modo msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "O Poracode e seus agentes são executados na máquina remota. O SSH é usado apenas para iniciar o ambiente remoto e proteger o túnel local." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "O Poracode não conseguiu criar a pasta de dados deste plugin." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "O Poracode não armazena credenciais do marketplace em texto simples nas configurações. Abra o catálogo para explorar as skills verificadas quanto à segurança." @@ -10170,6 +10220,38 @@ msgstr "Seleção enviada para o terminal." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Separe contas do Claude Code por diretório de configuração ou aponte um perfil para um provedor externo (z.ai, …). Abra um perfil para configurar suas variáveis de ambiente, modelos e esforço." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Não foi possível iniciar o servidor {target}, que foi ignorado." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "O servidor {target} tem um nome que o Poracode não pode usar." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "O servidor {target} não está configurado corretamente e foi ignorado." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "O servidor {target} roda neste computador e não está disponível para projetos WSL." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Servidores que este plugin declara em mcp.json. O Poracode os repassa a todos os agentes compatíveis." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Ignorado" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target} foi ignorado porque aponta para fora da pasta do plugin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Um arquivo foi ignorado porque aponta para fora da pasta do plugin." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Isso exclui permanentemente o branch \"{0}\" do remoto dele." msgid "This permanently deletes the branch \"{0}\"." msgstr "Isso exclui permanentemente o branch \"{0}\"." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Este plugin descreve uma skill que na verdade não fornece." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Não foi possível ler a pasta deste plugin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "As configurações do Poracode deste plugin foram ignoradas porque não são válidas." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Não foi possível ler as skills deste plugin." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "este projeto" @@ -11592,6 +11698,10 @@ msgstr "Isso remove a árvore de trabalho em \"{0}\" e depois exclui o branch." msgid "This server requires authentication before Poracode can check it." msgstr "Este servidor exige autenticação antes que o Poracode possa verificá-lo." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Este servidor roda neste computador e não está disponível para projetos WSL." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Este atalho global não está disponível. Escolha outra combinação de teclas." diff --git a/src/renderer/locales/ru/messages.po b/src/renderer/locales/ru/messages.po index e05664c65..380aac020 100644 --- a/src/renderer/locales/ru/messages.po +++ b/src/renderer/locales/ru/messages.po @@ -693,6 +693,18 @@ msgstr "Один из кандидатов больше не находится msgid "A frozen worktree source requires a full commit hash" msgstr "Для замороженного источника worktree требуется полный хеш коммита" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Не удалось запустить сервер, он пропущен." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "У сервера имя, которое Poracode не может использовать." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Сервер настроен неправильно и был пропущен." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "поддерживаемый агент кодирования" @@ -2921,7 +2933,6 @@ msgstr "Ошибка подключения." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Не удалось подключиться." @@ -3213,6 +3224,10 @@ msgstr "Не удалось установить {0}." msgid "Couldn't list repositories." msgstr "Не удалось получить список репозиториев." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Не удалось загрузить плагины." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Не удалось загрузить список изменений. Проверьте подключение или посмотрите его на GitHub." @@ -5439,6 +5454,10 @@ msgstr "Путь к файлу идентификации" msgid "Idle" msgstr "Ожидание" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Неизвестное поле в plugin.json проигнорировано." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Загрузка заметок…" #~ msgid "Loading PDF…" #~ msgstr "Загрузка PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Загрузка плагинов…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Загрузка PR…" @@ -6088,7 +6111,6 @@ msgstr "Загрузка запусков рабочих процессов" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Загрузка…" @@ -6434,6 +6456,14 @@ msgstr "Вызовы инструментов MCP" msgid "MCP transport type" msgstr "Тип транспорта MCP" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "Не удалось прочитать mcp.json, поэтому серверы этого плагина пропущены." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json указывает версию Agent Plugins, которую эта сборка не поддерживает." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Слить изменения {crownedLabel} в {mergeTarget}?" @@ -7650,10 +7680,10 @@ msgstr "Только когда окно неактивно" msgid "Open" msgstr "Открыть" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Открыть {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Открыть {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Плагин" msgid "Plugin folder" msgstr "Папка плагинов" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "Не удалось прочитать plugin.json как JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "Файл plugin.json отсутствует." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json не соответствует спецификации Agent Plugins." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json указывает версию Agent Plugins, которую эта сборка не поддерживает." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Популярные темы редакторов, адаптирова msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode и ваши агенты работают на удалённом компьютере. SSH используется только для запуска удалённой среды и защиты её локального туннеля." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode не смог создать папку данных этого плагина." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode не хранит учётные данные маркетплейса в настройках в открытом виде. Откройте каталог, чтобы просмотреть навыки, прошедшие проверку безопасности." @@ -10170,6 +10220,38 @@ msgstr "Выбор отправлен в терминал." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Разделяйте аккаунты Claude Code по каталогу конфигурации или направляйте профиль на внешнего провайдера (z.ai, …). Откройте профиль, чтобы настроить его переменные окружения, модели и уровни рассуждений." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Не удалось запустить сервер {target}, он пропущен." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "У сервера {target} имя, которое Poracode не может использовать." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "Сервер {target} настроен неправильно и был пропущен." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "Сервер {target} работает на этом компьютере и недоступен для проектов WSL." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Серверы, объявленные этим плагином в mcp.json. Poracode передаёт их всем поддерживаемым агентам." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Пропущено" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target} пропущен, так как указывает за пределы папки плагина." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Файл пропущен, так как указывает за пределы папки плагина." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Это безвозвратно удалит ветку \"{0}\" из е msgid "This permanently deletes the branch \"{0}\"." msgstr "Это безвозвратно удалит ветку \"{0}\"." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Этот плагин описывает навык, который на самом деле не поставляет." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Не удалось прочитать папку этого плагина." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Настройки Poracode этого плагина проигнорированы, так как они некорректны." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Не удалось прочитать навыки этого плагина." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "этот проект" @@ -11592,6 +11698,10 @@ msgstr "Это удаляет worktree в \"{0}\", а затем удаляет msgid "This server requires authentication before Poracode can check it." msgstr "Для проверки этого сервера в Poracode сначала нужно пройти аутентификацию." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Этот сервер работает на этом компьютере и недоступен для проектов WSL." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Это общесистемное сочетание клавиш недоступно. Выберите другое сочетание." diff --git a/src/renderer/locales/tr/messages.po b/src/renderer/locales/tr/messages.po index ac9d5a27e..ddde51f63 100644 --- a/src/renderer/locales/tr/messages.po +++ b/src/renderer/locales/tr/messages.po @@ -693,6 +693,18 @@ msgstr "Bir aday artık deney şubesinde değil." msgid "A frozen worktree source requires a full commit hash" msgstr "Dondurulmuş bir çalışma ağacı kaynağı tam commit karması gerektirir" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Bir sunucu başlatılamadı ve atlandı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Bir sunucunun adı Poracode tarafından kullanılamıyor." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Bir sunucu doğru yapılandırılmamış ve atlandı." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "desteklenen bir kodlama aracısı" @@ -2921,7 +2933,6 @@ msgstr "Bağlantı hatası." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Bağlantı başarısız." @@ -3213,6 +3224,10 @@ msgstr "{0} yüklenemedi." msgid "Couldn't list repositories." msgstr "Depolar listelenemedi." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Eklentiler yüklenemedi." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Değişiklik günlüğü yüklenemedi. Bağlantını kontrol et ya da GitHub'da görüntüle." @@ -5439,6 +5454,10 @@ msgstr "Kimlik dosyası yolu" msgid "Idle" msgstr "Boşta" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "plugin.json içindeki bilinmeyen bir alan yok sayıldı." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Notlar yükleniyor…" #~ msgid "Loading PDF…" #~ msgstr "PDF yükleniyor…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Eklentiler yükleniyor…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "PR yükleniyor…" @@ -6088,7 +6111,6 @@ msgstr "İş akışı çalıştırmaları yükleniyor" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Yükleniyor…" @@ -6434,6 +6456,14 @@ msgstr "MCP araç çağrıları" msgid "MCP transport type" msgstr "MCP aktarım türü" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "mcp.json okunamadı, bu yüzden bu eklentinin sunucuları atlandı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json, bu sürümün desteklemediği bir Agent Plugins sürümünü hedefliyor." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "{crownedLabel} adayının değişiklikleri {mergeTarget} ile birleştirilsin mi?" @@ -7650,10 +7680,10 @@ msgstr "Yalnızca odaklanmadığında" msgid "Open" msgstr "Açık" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "{0} aç" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "{0} aç" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Eklenti" msgid "Plugin folder" msgstr "Eklenti klasörü" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "plugin.json JSON olarak okunamadı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "plugin.json eksik." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json, Agent Plugins şartnamesine uygun değil." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json, bu sürümün desteklemediği bir Agent Plugins sürümünü hedefliyor." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Poracode'a uyarlanmış popüler editör temaları. Her biri yukarıdaki msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode ve aracılarınız uzak makinede çalışır. SSH yalnızca uzak ortamı başlatmak ve yerel tünelini güvenceye almak için kullanılır." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode bu eklentinin veri klasörünü oluşturamadı." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode, pazar yeri kimlik bilgilerini ayarlarda düz metin olarak saklamaz. Güvenlik taramasından geçmiş becerilere göz atmak için kataloğu açın." @@ -10170,6 +10220,38 @@ msgstr "Seçim terminale gönderildi." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Claude Code hesaplarını yapılandırma dizinine göre ayırın ya da bir profili harici bir sağlayıcıya (z.ai, …) yönlendirin. Ortam değişkenlerini, modellerini ve efor düzeyini yapılandırmak için bir profili açın." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "{target} sunucusu başlatılamadı ve atlandı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "{target} sunucusunun adı Poracode tarafından kullanılamıyor." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "{target} sunucusu doğru yapılandırılmamış ve atlandı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "{target} sunucusu bu bilgisayarda çalışır ve WSL projeleri için kullanılamaz." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Bu eklentinin mcp.json içinde tanımladığı sunucular. Poracode bunları desteklenen her ajana iletir." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Atlandı" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target} eklenti klasörünün dışını gösterdiği için atlandı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Bir dosya eklenti klasörünün dışını gösterdiği için atlandı." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Bu, \"{0}\" dalını uzak deposundan kalıcı olarak siler." msgid "This permanently deletes the branch \"{0}\"." msgstr "Bu, \"{0}\" dalını kalıcı olarak siler." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Bu eklenti, aslında sunmadığı bir beceriyi tanımlıyor." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Bu eklentinin klasörü okunamadı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Bu eklentinin Poracode ayarları geçersiz olduğu için yok sayıldı." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Bu eklentinin becerileri okunamadı." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "bu proje" @@ -11592,6 +11698,10 @@ msgstr "Bu, \"{0}\" üzerindeki çalışma ağacını kaldırır, ardından dal msgid "This server requires authentication before Poracode can check it." msgstr "Poracode'un bu sunucuyu kontrol edebilmesi için önce kimlik doğrulaması gerekir." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Bu sunucu bu bilgisayarda çalışır ve WSL projeleri için kullanılamaz." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Bu sistem genelindeki kısayol kullanılamıyor. Başka bir tuş birleşimi seçin." diff --git a/src/renderer/locales/uk/messages.po b/src/renderer/locales/uk/messages.po index 0b0772de5..1a957fce2 100644 --- a/src/renderer/locales/uk/messages.po +++ b/src/renderer/locales/uk/messages.po @@ -693,6 +693,18 @@ msgstr "Один із кандидатів більше не перебуває msgid "A frozen worktree source requires a full commit hash" msgstr "Для замороженого джерела worktree потрібен повний хеш коміту" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Не вдалося запустити сервер, його пропущено." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Сервер має ім'я, яке Poracode не може використати." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Сервер налаштовано неправильно, його пропущено." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "підтримуваний агент кодування" @@ -2921,7 +2933,6 @@ msgstr "Помилка підключення." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Не вдалося підключитися." @@ -3213,6 +3224,10 @@ msgstr "Не вдалося встановити {0}." msgid "Couldn't list repositories." msgstr "Не вдалося отримати список репозиторіїв." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Не вдалося завантажити плагіни." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Не вдалося завантажити список змін. Перевірте з'єднання або перегляньте його на GitHub." @@ -5439,6 +5454,10 @@ msgstr "Шлях до файлу ідентифікації" msgid "Idle" msgstr "Очікування" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Невідоме поле в plugin.json проігноровано." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Завантаження нотаток…" #~ msgid "Loading PDF…" #~ msgstr "Завантаження PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Завантаження плагінів…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Завантаження PR…" @@ -6088,7 +6111,6 @@ msgstr "Завантаження запусків робочих процесі #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Завантаження…" @@ -6434,6 +6456,14 @@ msgstr "Виклики інструментів MCP" msgid "MCP transport type" msgstr "Тип транспорту MCP" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "Не вдалося прочитати mcp.json, тому сервери цього плагіна пропущено." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json вказує версію Agent Plugins, яку ця збірка не підтримує." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Злити зміни {crownedLabel} у {mergeTarget}?" @@ -7650,10 +7680,10 @@ msgstr "Лише коли немає фокусу" msgid "Open" msgstr "Відкрити" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Відкрити {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Відкрити {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Плагін" msgid "Plugin folder" msgstr "Тека плагінів" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "Не вдалося прочитати plugin.json як JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "Файл plugin.json відсутній." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json не відповідає специфікації Agent Plugins." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json вказує версію Agent Plugins, яку ця збірка не підтримує." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Популярні теми редакторів, адаптовані msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode і ваші агенти працюють на віддаленому комп’ютері. SSH використовується лише для запуску віддаленого середовища та захисту його локального тунелю." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode не зміг створити теку даних цього плагіна." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode не зберігає облікові дані маркетплейсу в налаштуваннях відкритим текстом. Відкрийте каталог, щоб переглянути навички, перевірені на безпеку." @@ -10170,6 +10220,38 @@ msgstr "Вибір надіслано в термінал." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Розділяйте облікові записи Claude Code за каталогом конфігурації або спрямовуйте профіль на зовнішнього провайдера (z.ai, …). Відкрийте профіль, щоб налаштувати його змінні середовища, моделі та рівень зусиль." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Не вдалося запустити сервер {target}, його пропущено." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "Сервер {target} має ім'я, яке Poracode не може використати." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "Сервер {target} налаштовано неправильно, його пропущено." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "Сервер {target} працює на цьому комп'ютері й недоступний для проєктів WSL." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Сервери, які цей плагін оголошує в mcp.json. Poracode передає їх кожному підтримуваному агенту." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Пропущено" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "{target} пропущено, бо він вказує за межі теки плагіна." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Файл пропущено, бо він вказує за межі теки плагіна." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Це назавжди видалить гілку \"{0}\" з її від msgid "This permanently deletes the branch \"{0}\"." msgstr "Це назавжди видалить гілку \"{0}\"." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Цей плагін описує навичку, якої насправді не постачає." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Не вдалося прочитати теку цього плагіна." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Налаштування Poracode цього плагіна проігноровано, бо вони некоректні." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Не вдалося прочитати навички цього плагіна." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "цей проєкт" @@ -11592,6 +11698,10 @@ msgstr "Це видаляє worktree у \"{0}\", а потім видаляє г msgid "This server requires authentication before Poracode can check it." msgstr "Щоб Poracode міг перевірити цей сервер, спочатку потрібно автентифікуватися." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Цей сервер працює на цьому комп'ютері й недоступний для проєктів WSL." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Це загальносистемне сполучення клавіш недоступне. Виберіть інше сполучення." diff --git a/src/renderer/locales/vi/messages.po b/src/renderer/locales/vi/messages.po index 54d6c7859..b529dba68 100644 --- a/src/renderer/locales/vi/messages.po +++ b/src/renderer/locales/vi/messages.po @@ -693,6 +693,18 @@ msgstr "Một ứng viên không còn ở nhánh thử nghiệm của mình." msgid "A frozen worktree source requires a full commit hash" msgstr "Nguồn cây làm việc đã đóng băng yêu cầu hàm băm đầy đủ của commit" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "Không thể khởi động một máy chủ nên đã bỏ qua." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "Một máy chủ có tên mà Poracode không dùng được." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "Một máy chủ chưa được cấu hình đúng nên đã bỏ qua." + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "một tác nhân mã hóa được hỗ trợ" @@ -2921,7 +2933,6 @@ msgstr "Lỗi kết nối." #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "Kết nối thất bại." @@ -3213,6 +3224,10 @@ msgstr "Không thể cài đặt {0}." msgid "Couldn't list repositories." msgstr "Không thể liệt kê các kho lưu trữ." +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "Không thể tải plugin." + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "Không thể tải nhật ký thay đổi. Hãy kiểm tra kết nối hoặc xem trên GitHub." @@ -5439,6 +5454,10 @@ msgstr "Đường dẫn tệp danh tính" msgid "Idle" msgstr "Nhàn rỗi" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "Đã bỏ qua một trường không xác định trong plugin.json." + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "Đang tải ghi chú…" #~ msgid "Loading PDF…" #~ msgstr "Đang tải PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "Đang tải plugin…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "Đang tải PR…" @@ -6088,7 +6111,6 @@ msgstr "Đang tải các lần chạy quy trình" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "Đang tải…" @@ -6434,6 +6456,14 @@ msgstr "Lượt gọi công cụ MCP" msgid "MCP transport type" msgstr "Loại phương thức truyền MCP" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "Không đọc được mcp.json nên đã bỏ qua các máy chủ của plugin này." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json nhắm tới phiên bản Agent Plugins mà bản dựng này không hỗ trợ." + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "Hợp nhất các thay đổi của {crownedLabel} vào {mergeTarget}?" @@ -7650,10 +7680,10 @@ msgstr "Chỉ khi không tập trung" msgid "Open" msgstr "Mở" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "Mở {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "Mở {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8297,6 +8327,22 @@ msgstr "Plugin" msgid "Plugin folder" msgstr "Thư mục plugin" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "Không đọc được plugin.json dưới dạng JSON." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "Thiếu plugin.json." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json không hợp lệ theo đặc tả Agent Plugins." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json nhắm tới phiên bản Agent Plugins mà bản dựng này không hỗ trợ." + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8320,6 +8366,10 @@ msgstr "Các chủ đề biên tập phổ biến được điều chỉnh cho p msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode và các tác tử chạy trên máy từ xa. SSH chỉ được dùng để khởi động môi trường từ xa và bảo vệ đường hầm cục bộ của môi trường đó." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode không tạo được thư mục dữ liệu của plugin này." + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode không lưu thông tin xác thực marketplace dưới dạng văn bản thuần trong phần cài đặt. Mở danh mục để duyệt các skill đã được quét bảo mật." @@ -10170,6 +10220,38 @@ msgstr "Đã gửi lựa chọn đến thiết bị đầu cuối." msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "Tách các tài khoản Claude Code theo thư mục cấu hình, hoặc trỏ một hồ sơ tới nhà cung cấp bên ngoài (z.ai, …). Mở một hồ sơ để cấu hình biến môi trường, mô hình và mức nỗ lực của nó." +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "Không thể khởi động máy chủ {target} nên đã bỏ qua." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "Máy chủ {target} có tên mà Poracode không dùng được." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "Máy chủ {target} chưa được cấu hình đúng nên đã bỏ qua." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "Máy chủ {target} chạy trên máy này và không khả dụng cho dự án WSL." + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "Các máy chủ mà plugin này khai báo trong mcp.json. Poracode chuyển chúng tới mọi agent được hỗ trợ." @@ -10550,6 +10632,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "Đã bỏ qua" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "Đã bỏ qua {target} vì nó trỏ ra ngoài thư mục plugin." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "Đã bỏ qua một tệp vì nó trỏ ra ngoài thư mục plugin." + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11556,6 +11646,22 @@ msgstr "Việc này sẽ xóa vĩnh viễn nhánh \"{0}\" khỏi điều khiển msgid "This permanently deletes the branch \"{0}\"." msgstr "Việc này sẽ xóa vĩnh viễn nhánh \"{0}\"." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "Plugin này mô tả một skill mà thực tế nó không cung cấp." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "Không đọc được thư mục của plugin này." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "Cài đặt Poracode của plugin này bị bỏ qua vì không hợp lệ." + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "Không đọc được các skill của plugin này." + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "dự án này" @@ -11592,6 +11698,10 @@ msgstr "Thao tác này sẽ xóa cây làm việc trên \"{0}\", sau đó xóa n msgid "This server requires authentication before Poracode can check it." msgstr "Máy chủ này yêu cầu xác thực trước khi Poracode có thể kiểm tra máy chủ." +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "Máy chủ này chạy trên máy này và không khả dụng cho dự án WSL." + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "Phím tắt toàn hệ thống này không khả dụng. Hãy chọn tổ hợp phím khác." diff --git a/src/renderer/locales/zh-CN/messages.po b/src/renderer/locales/zh-CN/messages.po index 190e24401..16b6a6b89 100644 --- a/src/renderer/locales/zh-CN/messages.po +++ b/src/renderer/locales/zh-CN/messages.po @@ -693,6 +693,18 @@ msgstr "候选项已不在其实验分支上。" msgid "A frozen worktree source requires a full commit hash" msgstr "冻结的工作树源需要完整的提交哈希" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server could not be started and was skipped." +msgstr "无法启动某个服务器,已跳过。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server has a name Poracode cannot use." +msgstr "某个服务器的名称是 Poracode 无法使用的。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "A server is not configured correctly and was skipped." +msgstr "某个服务器配置不正确,已跳过。" + #: src/renderer/components/thread/threadDraftViewHelpers.ts msgid "a supported coding agent" msgstr "支持的编码代理" @@ -2921,7 +2933,6 @@ msgstr "连接错误。" #: src/renderer/components/mcp/McpServersManager.tsx #: src/renderer/state/remoteServersStore.ts -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx msgid "Connection failed." msgstr "连接失败。" @@ -3213,6 +3224,10 @@ msgstr "无法安装 {0}。" msgid "Couldn't list repositories." msgstr "无法列出存储库。" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Couldn't load plugins." +msgstr "无法加载插件。" + #: src/renderer/views/SettingsOverlay/parts/ChangelogSettings.tsx msgid "Couldn't load the changelog. Check your connection, or view it on GitHub." msgstr "无法加载变更日志。请检查网络连接,或在 GitHub 上查看。" @@ -5439,6 +5454,10 @@ msgstr "身份文件路径" msgid "Idle" msgstr "空闲" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Ignored an unrecognized field in plugin.json." +msgstr "已忽略 plugin.json 中一个无法识别的字段。" + #: src/renderer/components/thread/ChatPane/parts/items/ImageCard.tsx #: src/renderer/components/thread/ChatPane/parts/items/toolDisplay.ts msgid "Image" @@ -6032,6 +6051,10 @@ msgstr "正在加载笔记..." #~ msgid "Loading PDF…" #~ msgstr "正在加载 PDF…" +#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +msgid "Loading plugins…" +msgstr "正在加载插件…" + #: src/renderer/views/PrReviewOverlay/parts/PrReviewSidebar.tsx msgid "Loading PR…" msgstr "正在加载 PR…" @@ -6088,7 +6111,6 @@ msgstr "正在加载工作流运行记录" #: src/renderer/components/thread/ChatPane/parts/items/WorkflowOverlayBody.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/parts/TreeEntryRow.tsx #: src/renderer/views/FileEditorOverlay/parts/ProjectTreeView/ProjectTreeView.tsx -#: src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx #: src/renderer/views/SettingsOverlay/parts/RemoteHostFolderPicker.tsx msgid "Loading…" msgstr "加载中..." @@ -6434,6 +6456,14 @@ msgstr "MCP 工具调用" msgid "MCP transport type" msgstr "MCP 传输类型" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json could not be read, so this plugin's servers were skipped." +msgstr "无法读取 mcp.json,因此已跳过此插件的服务器。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "mcp.json targets an Agent Plugins version this build does not support." +msgstr "mcp.json 指向此版本不支持的 Agent Plugins 版本。" + #: src/renderer/views/ExperimentView/ExperimentView.tsx msgid "Merge {crownedLabel}'s changes into {mergeTarget}?" msgstr "将 {crownedLabel} 的更改合并到 {mergeTarget}?" @@ -7649,10 +7679,10 @@ msgstr "仅当窗口失去焦点时" msgid "Open" msgstr "打开" -#. placeholder {0}: thread.title -#: src/renderer/views/MainView/parts/Sidebar/parts/SidebarRemoteServers.tsx -#~ msgid "Open {0}" -#~ msgstr "打开 {0}" +#. placeholder {0}: entry.name +#: src/renderer/components/plugins/PluginMarketplace.tsx +msgid "Open {0}" +msgstr "打开 {0}" #: src/renderer/views/SettingsOverlay/parts/ClaudeProfileSettings.tsx msgid "Open {label}" @@ -8296,6 +8326,22 @@ msgstr "插件" msgid "Plugin folder" msgstr "插件文件夹" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json could not be read as JSON." +msgstr "无法将 plugin.json 作为 JSON 读取。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is missing." +msgstr "缺少 plugin.json。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json is not valid for the Agent Plugins specification." +msgstr "plugin.json 不符合 Agent Plugins 规范。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "plugin.json targets an Agent Plugins version this build does not support." +msgstr "plugin.json 指向此版本不支持的 Agent Plugins 版本。" + #: src/renderer/components/plugins/PluginMarketplace.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts #: src/renderer/views/SettingsOverlay/parts/SettingsSidebar.tsx @@ -8319,6 +8365,10 @@ msgstr "流行的编辑器主题适应 Poracode。每个都遵循上面的浅色 msgid "Poracode and your agents run on the remote machine. SSH is used only to start the remote environment and secure its local tunnel." msgstr "Poracode 和代理在远程计算机上运行。SSH 仅用于启动远程环境并保护其本地隧道。" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Poracode could not create this plugin's data folder." +msgstr "Poracode 无法创建此插件的数据文件夹。" + #: src/renderer/components/skills/SkillMarketplaceModal.tsx #~ msgid "Poracode does not store marketplace credentials in plain settings. Open the catalog to browse its security-scanned skills." #~ msgstr "Poracode 不会以明文形式在设置中存储市场凭据。打开目录即可浏览经过安全扫描的技能。" @@ -10169,6 +10219,38 @@ msgstr "将选择发送到终端。" msgid "Separate Claude Code accounts by config directory, or point a profile at an external provider (z.ai, …). Open a profile to configure its env vars, models, and effort." msgstr "按配置目录区分Claude Code账户,或将配置指向外部服务商(z.ai,…)。打开某个配置以设置其环境变量、模型和思考级别。" +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" could not be started and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" has a name Poracode cannot use." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" is not configured correctly and was skipped." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +#~ msgid "Server \"{target}\" runs on this computer and is unavailable for WSL projects." +#~ msgstr "" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} could not be started and was skipped." +msgstr "无法启动服务器 {target},已跳过。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} has a name Poracode cannot use." +msgstr "服务器 {target} 的名称是 Poracode 无法使用的。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} is not configured correctly and was skipped." +msgstr "服务器 {target} 配置不正确,已跳过。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Server {target} runs on this computer and is unavailable for WSL projects." +msgstr "服务器 {target} 在本机运行,WSL 项目无法使用。" + #: src/renderer/components/plugins/PluginDetail.tsx msgid "Servers this plugin declares in mcp.json. Poracode passes them to every supported agent." msgstr "此插件在 mcp.json 中声明的服务器。Poracode 会将它们传递给所有受支持的代理。" @@ -10549,6 +10631,14 @@ msgstr "Skills.sh" msgid "Skipped" msgstr "已跳过" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped {target} because it points outside the plugin folder." +msgstr "已跳过 {target},因为它指向插件文件夹之外。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "Skipped a file because it points outside the plugin folder." +msgstr "已跳过一个文件,因为它指向插件文件夹之外。" + #. placeholder {0}: skippedProviderNames.join(", ") #: src/renderer/views/SettingsOverlay/parts/CrossagentRoutingSection.tsx msgid "Skipped by Crossagents: {0}" @@ -11555,6 +11645,22 @@ msgstr "这将从远程永久删除分支“{0}”。" msgid "This permanently deletes the branch \"{0}\"." msgstr "这将永久删除分支“{0}”。" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin describes a skill it does not actually ship." +msgstr "此插件描述了一个它实际并未提供的技能。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's folder could not be read." +msgstr "无法读取此插件的文件夹。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's Poracode settings were ignored because they are not valid." +msgstr "此插件的 Poracode 设置无效,已被忽略。" + +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This plugin's skills could not be read." +msgstr "无法读取此插件的技能。" + #: src/renderer/components/skills/SkillsManager.tsx msgid "this project" msgstr "此项目" @@ -11591,6 +11697,10 @@ msgstr "这将删除“{0}”上的工作树,然后删除分支。" msgid "This server requires authentication before Poracode can check it." msgstr "此服务器需要先进行身份验证,Poracode 才能检查它。" +#: src/renderer/components/plugins/pluginCopy.ts +msgid "This server runs on this computer and is unavailable for WSL projects." +msgstr "此服务器在本机运行,WSL 项目无法使用。" + #: src/renderer/views/SettingsOverlay/parts/ShortcutsSettings.tsx msgid "This system-wide shortcut is unavailable. Choose another key combination." msgstr "此全局快捷键不可用。请选择其他按键组合。" diff --git a/src/renderer/state/pluginsStore.ts b/src/renderer/state/pluginsStore.ts index a8460f33c..5f1f1aead 100644 --- a/src/renderer/state/pluginsStore.ts +++ b/src/renderer/state/pluginsStore.ts @@ -42,10 +42,3 @@ export const usePlugins = create()((set, get) => ({ } }, })); - -export function findPlugin( - plugins: readonly LoadedPlugin[], - name: string, -): LoadedPlugin | undefined { - return plugins.find((plugin) => plugin.name === name); -} diff --git a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx index 7a9a0f201..ce8be16eb 100644 --- a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx +++ b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.test.tsx @@ -37,6 +37,23 @@ describe("PluginsSettings", () => { expect(screen.getByRole("textbox", { name: "Search plugins" })).toHaveValue("browser tools"); }); + it("restores focus to the card, not the installed shortcut, with no search query", () => { + // The installed strip renders before the card grid. Both used to carry + // `data-plugin-id`, so focus restore landed on the strip and jumped the user + // to the top of the page. Neither other focus test covers this: one types a + // query (hiding the strip) and the other uninstalls first (emptying it). + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); + render(); + + const card = screen.getByText("Browser Tools").closest("[class*='min-h-40']")!; + fireEvent.click(within(card).getByRole("button", { name: "Browser Tools" })); + fireEvent.click(screen.getByRole("button", { name: "Back to plugins" })); + + const restored = screen.getByText("Browser Tools").closest("[class*='min-h-40']")!; + expect(within(restored).getByRole("button", { name: "Browser Tools" })).toHaveFocus(); + expect(screen.getByRole("button", { name: "Open Browser Tools" })).not.toHaveFocus(); + }); + it("keeps focus on the card after uninstalling from the detail page", () => { useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); render(); diff --git a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx index 8b3e8442d..618b076f1 100644 --- a/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx +++ b/src/renderer/views/SettingsOverlay/parts/PluginsSettings.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { Trans } from "@lingui/react/macro"; -import { Button } from "@/renderer/components/common"; +import { Button, PixelLoader } from "@/renderer/components/common"; import { PluginDetail } from "@/renderer/components/plugins/PluginDetail"; import { PluginMarketplace } from "@/renderer/components/plugins/PluginMarketplace"; import { useLocalizedPluginCatalog } from "@/renderer/components/plugins/pluginCopy"; @@ -32,29 +32,18 @@ export function PluginsSettings() { const target = [...(marketplace?.querySelectorAll("[data-plugin-id]") ?? [])].find( (element) => element.dataset.pluginId === pluginId, ); - ( - target ?? marketplace?.querySelector('[role="tab"][aria-selected="true"]') - )?.focus(); + (target ?? marketplace?.querySelector('input[type="search"], input'))?.focus(); returnFocusPluginId.current = undefined; }, [selectedPluginId]); if (!loaded) { return ( -
- Loading… -
- ); - } - - if (error && plugins.length === 0) { - return ( -
-

- Connection failed. -

- +
+ + Loading plugins…
); } @@ -67,6 +56,24 @@ export function PluginsSettings() { return (
{selectedPlugin ? ( diff --git a/src/shared/contracts/plugin.ts b/src/shared/contracts/plugin.ts index 3c0075e15..5788458bb 100644 --- a/src/shared/contracts/plugin.ts +++ b/src/shared/contracts/plugin.ts @@ -25,6 +25,7 @@ export const pluginDiagnosticSchema = z target: z.string().min(1).optional(), }) .strict(); +export type PluginDiagnostic = z.infer; /** A `skills//SKILL.md` discovered inside the package boundary. */ export const pluginSkillRefSchema = z @@ -73,15 +74,20 @@ export type ListPluginsResult = z.infer; /** * Per-plugin user state, keyed by the manifest `name`. Contribution ids are the * skill folder name and the `mcp.json` server key. + * + * Deliberately NOT `.strict()`. This is persisted to `settings.json` and to the + * renderer's localStorage mirror, and `normalizeSharedSettings` parses the whole + * `installedPlugins` record as one setting — so a single entry carrying a field + * from an older build would reject the record and silently uninstall every + * plugin. Unknown keys are stripped per entry instead, which is the "tolerant + * parser" that `.agents/docs/versioning.md` requires for unversioned JSON stores. */ -export const installedPluginStateSchema = z - .object({ - version: z.string().min(1).default("0.0.0"), - enabled: z.boolean().default(true), - disabledSkillIds: z.array(z.string().min(1)).default([]), - disabledMcpServerNames: z.array(z.string().min(1)).default([]), - }) - .strict(); +export const installedPluginStateSchema = z.object({ + version: z.string().min(1).default("0.0.0"), + enabled: z.boolean().default(true), + disabledSkillIds: z.array(z.string().min(1)).default([]), + disabledMcpServerNames: z.array(z.string().min(1)).default([]), +}); export type InstalledPluginState = z.infer; export const installedPluginsSchema = z.record(z.string(), installedPluginStateSchema).default({}); diff --git a/src/shared/plugins/catalog.test.ts b/src/shared/plugins/catalog.test.ts index a68ad6720..1ef8cb405 100644 --- a/src/shared/plugins/catalog.test.ts +++ b/src/shared/plugins/catalog.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import type { LoadedPlugin } from "../contracts"; import { installedPluginsSchema } from "../contracts/plugin"; +import { normalizeSharedSettings } from "../settings"; import { AGENT_PLUGINS_MANIFEST_SCHEMA_URL, type PluginSkillPolicyEntry } from "./spec"; import { installPlugin, @@ -82,6 +83,36 @@ describe("plugin contracts", () => { it("defaults the version when a manifest omits it", () => { expect(installedPluginsSchema.parse({ "test-tools": {} })["test-tools"]?.version).toBe("0.0.0"); }); + + // An earlier build on this branch persisted `disabledAppIds`. `installedPlugins` + // is normalized as one setting, so a strict per-entry schema would reject the + // whole record and silently uninstall every plugin on upgrade. + it("keeps plugin state when an entry carries a field from an older build", () => { + const settings = normalizeSharedSettings({ + installedPlugins: { + "browser-tools": { + version: "1.0.0", + enabled: false, + disabledSkillIds: ["browser-control"], + disabledAppIds: ["browser"], + }, + github: { + version: "1.0.0", + enabled: true, + disabledSkillIds: [], + disabledMcpServerNames: ["github"], + }, + }, + }); + + expect(settings.installedPlugins["browser-tools"]).toEqual({ + version: "1.0.0", + enabled: false, + disabledSkillIds: ["browser-control"], + disabledMcpServerNames: [], + }); + expect(settings.installedPlugins.github?.disabledMcpServerNames).toEqual(["github"]); + }); }); describe("plugin catalog", () => { diff --git a/src/shared/plugins/catalog.ts b/src/shared/plugins/catalog.ts index 6531575f9..b369f7a87 100644 --- a/src/shared/plugins/catalog.ts +++ b/src/shared/plugins/catalog.ts @@ -5,7 +5,6 @@ import type { LoadedPlugin, PluginSkillRef, } from "../contracts/plugin"; -import type { PluginSkillPolicyEntry } from "./spec"; /** * Policy over loaded Agent Plugins packages. @@ -43,14 +42,6 @@ export function getPluginSkill(plugin: LoadedPlugin, folder: string): PluginSkil return plugin.skills.find((skill) => skill.folder === folder); } -/** Display copy a plugin declares for a skill, if any. */ -export function getPluginSkillPolicy( - plugin: LoadedPlugin, - folder: string, -): PluginSkillPolicyEntry | undefined { - return plugin.poracode.skills[folder]; -} - export interface PluginSkillLaunchContext { hostPlatform: NodeJS.Platform; projectLocation?: ProjectLocation; @@ -74,6 +65,16 @@ export function isPluginSkillEnabled( ); } +/** Stable id so per-server settings survive a rescan. */ +export function pluginMcpServerId(pluginName: string, serverName: string): string { + return `plugin:${pluginName}:${serverName}`; +} + +/** Provider-visible name, namespaced by plugin. */ +export function pluginMcpServerName(pluginName: string, serverName: string): string { + return `${pluginName}.${serverName}`; +} + export function isPluginMcpServerEnabled( plugin: LoadedPlugin, state: InstalledPluginState, diff --git a/src/supervisor/plugins/PluginRegistry.ts b/src/supervisor/plugins/PluginRegistry.ts index 8b07e32b2..2676f5904 100644 --- a/src/supervisor/plugins/PluginRegistry.ts +++ b/src/supervisor/plugins/PluginRegistry.ts @@ -2,7 +2,7 @@ import { mkdirSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; import type { LoadedPlugin, PluginSource } from "@/shared/contracts"; import { formatPluginDiagnostic, type PluginDiagnostic } from "@/shared/plugins/spec"; -import { loadPluginFromDirectory, PLUGIN_MANIFEST_FILE } from "./PluginLoader"; +import { loadPluginFromDirectory, PLUGIN_MANIFEST_FILE, PLUGIN_MCP_FILE } from "./PluginLoader"; /** * Discovers Agent Plugins packages from the roots Poracode scans. @@ -34,11 +34,18 @@ function rootFingerprint(directory: string): string { return `${directory}\0missing`; } const parts = entries.map((entry) => { - try { - return `${entry}:${statSync(join(directory, entry)).mtimeMs}`; - } catch { - return `${entry}:?`; - } + // A directory's mtime does not change when a file inside it is rewritten, so + // fold in the two manifests as well; otherwise editing a package in place + // keeps serving the stale parse to every launch and skill scan. + const stamp = (path: string): string => { + try { + return String(statSync(path).mtimeMs); + } catch { + return "?"; + } + }; + const dir = join(directory, entry); + return `${entry}:${stamp(dir)}:${stamp(join(dir, PLUGIN_MANIFEST_FILE))}:${stamp(join(dir, PLUGIN_MCP_FILE))}`; }); return `${directory}\0${parts.join("\0")}`; } diff --git a/src/supervisor/plugins/conformance.test.ts b/src/supervisor/plugins/conformance.test.ts index c34a4c46f..67c78241a 100644 --- a/src/supervisor/plugins/conformance.test.ts +++ b/src/supervisor/plugins/conformance.test.ts @@ -1,8 +1,10 @@ import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import { readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { InstalledPlugins } from "@/shared/contracts"; +import { isValidSkillName } from "@/shared/contracts"; import { AGENT_PLUGINS_MANIFEST_SCHEMA_URL, AGENT_PLUGINS_MCP_SCHEMA_URL, @@ -486,6 +488,39 @@ describe("mcp runtime", () => { expect(codes(result.diagnostics)).toEqual(["mcp-entry-unresolvable"]); }); + it("skips host-only stdio servers for a WSL project but keeps remote ones", async () => { + const plugin = await loadWithServers("wsl-mix", { + local: { type: "stdio", command: "server" }, + remote: { type: "streamable-http", url: "https://tools.example.com/mcp" }, + }); + const context = { pluginDataRoot: join(root, "plugin-data") }; + const wsl = { + kind: "wsl" as const, + distro: "Ubuntu", + linuxPath: "/repo", + uncPath: "\\\\wsl.localhost\\Ubuntu\\repo", + }; + + // A stdio server is launched by the CLI inside the distro; every path we + // would hand it is a Windows host path that cannot resolve there. + const onWsl = resolvePluginMcpServers([plugin], installed("wsl-mix"), { + ...context, + projectLocation: wsl, + }); + expect(onWsl.servers.map((server) => server.name)).toEqual(["wsl-mix.remote"]); + expect(codes(onWsl.diagnostics)).toEqual(["mcp-entry-host-only"]); + + const onWindows = resolvePluginMcpServers([plugin], installed("wsl-mix"), { + ...context, + projectLocation: { kind: "windows", path: "C:\repo" }, + }); + expect(onWindows.servers.map((server) => server.name)).toEqual([ + "wsl-mix.local", + "wsl-mix.remote", + ]); + expect(onWindows.diagnostics).toEqual([]); + }); + it("skips servers a plugin is not installed or enabled for", async () => { const plugin = await loadWithServers("gated", { main: { type: "stdio", command: "server" }, @@ -602,6 +637,19 @@ describe("shipped packages", () => { expect(result.plugin?.name).toBe(name); expect(result.plugin?.skills.length).toBeGreaterThan(0); expect(result.plugin?.poracode.title).toBeTruthy(); + + // SkillsService rejects a SKILL.md whose frontmatter `name` is not the + // folder name, and the Skills list then shows the rejection reason where + // the description belongs. Prove every shipped skill passes that gate. + for (const skill of result.plugin?.skills ?? []) { + const frontmatter = readFileSync(join(skill.path, "SKILL.md"), "utf8"); + const declared = /^name:[ \t]*"?([^"\r\n]+?)"?[ \t]*$/mu.exec(frontmatter)?.[1]; + expect(declared, `${name}/${skill.folder} has no frontmatter name`).toBeTruthy(); + expect(isValidSkillName(declared!), `${name}/${skill.folder} name '${declared}'`).toBe( + true, + ); + expect(declared, `${name}/${skill.folder} name must match its folder`).toBe(skill.folder); + } } }); }); diff --git a/src/supervisor/plugins/pluginMcpRuntime.ts b/src/supervisor/plugins/pluginMcpRuntime.ts index dfa129c22..fd12b98d0 100644 --- a/src/supervisor/plugins/pluginMcpRuntime.ts +++ b/src/supervisor/plugins/pluginMcpRuntime.ts @@ -8,7 +8,11 @@ import type { ProjectLocation, } from "@/shared/contracts"; import { DEFAULT_MCP_SERVER_TIMEOUT_MS, isValidMcpServerName } from "@/shared/contracts"; -import { isPluginSupportedForProject } from "@/shared/plugins/catalog"; +import { + isPluginSupportedForProject, + pluginMcpServerId, + pluginMcpServerName, +} from "@/shared/plugins/catalog"; import { pluginDiagnostic, type PluginDiagnostic, @@ -161,16 +165,6 @@ function buildTransport( }; } -/** Stable id so per-server settings survive a rescan. */ -export function pluginMcpServerId(pluginName: string, serverName: string): string { - return `plugin:${pluginName}:${serverName}`; -} - -/** Provider-visible name. Namespaced by plugin so two plugins cannot collide. */ -export function pluginMcpServerName(pluginName: string, serverName: string): string { - return `${pluginName}.${serverName}`; -} - /** * Builds the MCP servers contributed by enabled plugins. * @@ -220,6 +214,24 @@ export function resolvePluginMcpServers( } if (declaration.entry.type === "stdio") { + // A stdio server is launched by the provider CLI, which runs inside the + // distro for a WSL project. Every path we would hand it — `command`, + // `cwd`, PLUGIN_ROOT, PLUGIN_DATA — is a Windows host path resolved from + // the package directory, and nothing on the launch path rewrites them + // (`agents/userMcp/translate.ts` writes them verbatim). Skip rather than + // emit a config that cannot resolve inside the distro. + if (context.projectLocation?.kind === "wsl") { + diagnostics.push( + pluginDiagnostic( + "error", + "mcp-server", + "mcp-entry-host-only", + `Skipping server '${declaration.name}': stdio servers run on the host and cannot be reached from a WSL project`, + declaration.name, + ), + ); + continue; + } dataReady ??= ensureDirectory(data); if (!dataReady) { diagnostics.push( diff --git a/src/supervisor/runtime/threadSession/spawnPipeline.ts b/src/supervisor/runtime/threadSession/spawnPipeline.ts index e561b06dc..7f6ab877d 100644 --- a/src/supervisor/runtime/threadSession/spawnPipeline.ts +++ b/src/supervisor/runtime/threadSession/spawnPipeline.ts @@ -352,10 +352,16 @@ export class SpawnPipeline { threadId: payload.threadId, title: initialPrompt.split("\n", 1)[0]?.trim() ?? "", }; - let mcpServers = resolveEnabledMcpServers([ - ...(payload.mcpServers ?? []), - ...(this.ctx.options.resolvePluginMcpServers?.(payload.projectLocation) ?? []), - ]); + // Every provider translator keys its output record by `server.name`, so a + // plugin server sharing a name with a user-configured one would silently + // replace it — dropping the user's headers and tokens. The user's own + // servers win; the colliding plugin server is skipped. + const userMcpServers = payload.mcpServers ?? []; + const userMcpServerNames = new Set(userMcpServers.map((server) => server.name)); + const pluginMcpServers = ( + this.ctx.options.resolvePluginMcpServers?.(payload.projectLocation) ?? [] + ).filter((server) => !userMcpServerNames.has(server.name)); + let mcpServers = resolveEnabledMcpServers([...userMcpServers, ...pluginMcpServers]); if (this.ctx.options.applyMcpServerAuthorization) { mcpServers = await this.ctx.options.applyMcpServerAuthorization(mcpServers); } diff --git a/src/supervisor/skills/SkillsService.test.ts b/src/supervisor/skills/SkillsService.test.ts index e022eb027..c7a555fd3 100644 --- a/src/supervisor/skills/SkillsService.test.ts +++ b/src/supervisor/skills/SkillsService.test.ts @@ -696,7 +696,11 @@ describe("SkillsService", () => { ).toEqual([pluginAlias]); }); - it("fails closed when a WSL skill path cannot be canonicalized", async () => { + it("fails closed per segment when a WSL skill path cannot be canonicalized", async () => { + // The distro cannot translate the plugin roots (no /mnt automount, distro + // still starting, transient wsl.exe failure). Rejecting every WSL segment + // here would silently strip the user's own distro-local skills, so only + // paths that lexically sit under a plugin root are dropped. const bundledService = new SkillsService({ adapters, homeDirectory: () => home, @@ -709,7 +713,13 @@ describe("SkillsService", () => { hostPlatform: "win32", resolveHostPathForWsl: async () => undefined, }); - const segment = { + const wslProject = { + kind: "wsl" as const, + distro: "Ubuntu", + linuxPath: "/repo", + uncPath: "\\\\wsl.localhost\\Ubuntu\\repo", + }; + const userSkill = { kind: "skill" as const, name: "review", path: "/home/alice/.agents/skills/review/SKILL.md", @@ -717,15 +727,19 @@ describe("SkillsService", () => { provider: "User", scope: "global" as const, }; + const pluginSkill = { + ...userSkill, + name: "browser-control", + path: "/mnt/e/Poracode/resources/plugins/browser-tools/skills/browser-control/SKILL.md", + invocation: "/browser-control", + }; expect( - await bundledService.filterPluginSkillSegments([segment], { - projectLocation: { - kind: "wsl", - distro: "Ubuntu", - linuxPath: "/repo", - uncPath: "\\\\wsl.localhost\\Ubuntu\\repo", - }, + await bundledService.filterPluginSkillSegments([userSkill], { projectLocation: wslProject }), + ).toEqual([userSkill]); + expect( + await bundledService.filterPluginSkillSegments([pluginSkill], { + projectLocation: wslProject, }), ).toEqual([]); }); diff --git a/src/supervisor/skills/SkillsService.ts b/src/supervisor/skills/SkillsService.ts index 478667702..9457b0a43 100644 --- a/src/supervisor/skills/SkillsService.ts +++ b/src/supervisor/skills/SkillsService.ts @@ -1149,7 +1149,6 @@ export class SkillsService { ...(context.projectLocation ? { projectLocation: context.projectLocation } : {}), ...(adapter ? { capabilities: adapter.capabilities } : {}), ...(context.presentationMode ? { presentationMode: context.presentationMode } : {}), - ...(context.launchConfig ? { launchConfig: context.launchConfig } : {}), }); } diff --git a/src/supervisor/skills/pluginSkillPolicy.ts b/src/supervisor/skills/pluginSkillPolicy.ts index 6d8947182..43a25535a 100644 --- a/src/supervisor/skills/pluginSkillPolicy.ts +++ b/src/supervisor/skills/pluginSkillPolicy.ts @@ -6,7 +6,6 @@ import type { ProjectLocation, PromptSegment, SkillEntry, - ThreadConfig, ThreadPresentationMode, } from "@/shared/contracts"; import { isPluginSkillEnabled, isPluginSkillSupportedForLaunch } from "@/shared/plugins/catalog"; @@ -39,7 +38,6 @@ export interface PluginSkillPolicyContext { projectLocation?: ProjectLocation; capabilities?: AgentCapability; presentationMode?: ThreadPresentationMode; - launchConfig?: ThreadConfig; } export interface PluginSkillPolicyOptions { @@ -72,6 +70,17 @@ function relativePosixPolicyPath(root: string, target: string): string | undefin return candidate; } +/** + * `C:\a\b` -> `/mnt/c/a/b`. Pure string mapping with no `wsl.exe` round-trip, so + * it still works when the distro cannot translate paths for us. + */ +function lexicalWslRootPath(hostRoot: string): string | undefined { + const match = /^([a-z]):[\\/](.*)$/iu.exec(hostRoot); + if (!match) return undefined; + const rest = match[2]!.replace(/\\/gu, "/"); + return `/mnt/${match[1]!.toLowerCase()}${rest ? `/${rest}` : ""}`; +} + function relativeWslPolicyPath(root: string, target: string): string | undefined { const rootDrive = /^\/mnt\/([a-z])(?:\/|$)/iu.exec(root)?.[1]; const targetDrive = /^\/mnt\/([a-z])(?:\/|$)/iu.exec(target)?.[1]; @@ -217,7 +226,13 @@ export class PluginSkillPolicy { [...unresolvedWslPaths].map(async ([distro, pending]) => { const wslRoots = await this.resolveWslRoots(distro, roots); if (wslRoots.length === 0) { - pending.forEach(({ segment }) => rejectedWslSegments.add(segment)); + // The distro could not translate any plugin root (no /mnt automount, + // distro still starting, transient wsl.exe failure). Fail closed only + // for segments that lexically sit under a plugin root — dropping every + // WSL skill here would silently strip the user's own skills too. + pending.forEach(({ segment, linuxPath }) => { + if (this.matchLexicalWslPath(roots, linuxPath)) rejectedWslSegments.add(segment); + }); return; } const resolvedPaths = await this.options @@ -230,10 +245,11 @@ export class PluginSkillPolicy { distro, resolvedPaths.map((path) => path ?? "/"), ).catch(() => []); - pending.forEach(({ segment }, index) => { + pending.forEach(({ segment, linuxPath }, index) => { const resolvedPath = resolvedPaths[index]; if (!resolvedPath) { - rejectedWslSegments.add(segment); + // Same reasoning as the empty-wslRoots branch above. + if (this.matchLexicalWslPath(roots, linuxPath)) rejectedWslSegments.add(segment); return; } const windowsPath = windowsPaths[index]; @@ -299,6 +315,16 @@ export class PluginSkillPolicy { return undefined; } + /** True when a distro path lexically sits under one of the plugin skill roots. */ + private matchLexicalWslPath(roots: readonly PluginSkillRoot[], linuxPath: string): boolean { + return roots.some((root) => { + const lexicalRoot = lexicalWslRootPath(root.skillsRoot); + return ( + lexicalRoot !== undefined && relativeWslPolicyPath(lexicalRoot, linuxPath) !== undefined + ); + }); + } + private async resolveWslRoots( distro: string, roots: readonly PluginSkillRoot[], From af654c15063cca32a98d59869a277de49719e410 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Sat, 8 Aug 2026 16:39:00 -0700 Subject: [PATCH 5/6] feat(plugins): add native plugin mapping and MCP tool annotations - Annotate MCP tools with openWorld/destructive/readOnly hints across browser, chrome, computer-use, and crossagent registries - Map catalog plugins to Codex native plugins and built-in MCP servers, threading them through plugin resolution, skill turns, and session launch - Support plugin mentions in the composer, carrying pluginId/pluginName through serialization, slash chips, and prompt segments - Drop skill segments on plugin skill policy failure and extend skill scan and import environment handling - Bump plugin store revision, re-extract locale catalogs, and update smoke scenarios and targeted tests --- .../scripts/poracode-integration-smoke.mjs | 17 +- .../scripts/smoke-scenarios.mjs | 2 +- resources/plugins/browser-tools/plugin.json | 11 +- .../skills/browser-control/SKILL.md | 27 +++- resources/plugins/chrome-tools/plugin.json | 11 +- .../skills/chrome-control/SKILL.md | 26 +++- resources/plugins/computer-use/plugin.json | 11 +- .../computer-use/skills/computer-use/SKILL.md | 26 +++- resources/plugins/github/plugin.json | 20 ++- .../plugins/github/skills/ci-debug/SKILL.md | 3 + .../plugins/github/skills/github/SKILL.md | 9 ++ .../github/skills/publish-changes/SKILL.md | 4 +- .../github/skills/review-follow-up/SKILL.md | 3 + resources/plugins/outlook/plugin.json | 13 +- .../outlook/skills/outlook-calendar/SKILL.md | 8 +- .../outlook/skills/outlook-email/SKILL.md | 6 +- .../plugins/subagent-delegation/plugin.json | 5 +- .../skills/subagent-delegation/SKILL.md | 37 ++++- .../browser/external/ChromeMcpIngress.test.ts | 8 +- src/main/browser/external/chromeTools.ts | 37 ++++- src/main/browser/mcp/toolRegistry.test.ts | 12 ++ src/main/browser/mcp/tools/specs.ts | 53 ++++++- src/main/browser/mcp/tools/types.ts | 2 + .../computer-use/mcp/toolRegistry.test.ts | 9 ++ src/main/computer-use/mcp/toolRegistry.ts | 22 ++- .../components/composer/MentionInput.test.ts | 81 ++++++++++ .../components/composer/MentionInput.tsx | 56 ++++++- .../components/composer/MentionPopover.tsx | 22 ++- .../components/composer/SlashCommandChip.ts | 6 +- .../components/composer/composerMcpServers.ts | 12 +- src/renderer/components/composer/index.ts | 7 +- .../composer/serializeMentions.test.ts | 27 ++++ .../components/composer/serializeMentions.ts | 2 + .../components/plugins/PluginDetail.test.tsx | 54 ++++++- .../components/plugins/PluginDetail.tsx | 70 +++++---- src/renderer/components/plugins/pluginCopy.ts | 28 +++- .../components/skills/useSkills.test.ts | 42 ++++- src/renderer/components/skills/useSkills.ts | 110 ++++++++++--- .../ChatPane/parts/items/UserMessage.tsx | 18 ++- .../thread/ThreadComposerSection.tsx | 9 +- .../thread/ThreadDraftComposerArea.tsx | 12 +- src/renderer/locales/de/messages.po | 28 +--- src/renderer/locales/en/messages.po | 33 +--- src/renderer/locales/es/messages.po | 28 +--- src/renderer/locales/fr/messages.po | 28 +--- src/renderer/locales/ja/messages.po | 28 +--- src/renderer/locales/ko/messages.po | 28 +--- src/renderer/locales/pl/messages.po | 28 +--- src/renderer/locales/pt-BR/messages.po | 28 +--- src/renderer/locales/ru/messages.po | 28 +--- src/renderer/locales/tr/messages.po | 28 +--- src/renderer/locales/uk/messages.po | 28 +--- src/renderer/locales/vi/messages.po | 28 +--- src/renderer/locales/zh-CN/messages.po | 28 +--- src/renderer/state/pluginsStore.ts | 7 +- src/shared/contracts/agent.ts | 6 +- src/shared/contracts/mcpServer.ts | 10 ++ src/shared/contracts/runtimeEvent.ts | 8 +- src/shared/contracts/thread.ts | 2 + src/shared/plugins/catalog.test.ts | 36 +++++ src/shared/plugins/catalog.ts | 35 ++++- src/shared/plugins/spec/extensions.ts | 15 ++ src/shared/promptContent.test.ts | 25 +++ src/shared/promptContent.ts | 10 +- src/supervisor/agents/base/index.ts | 2 + src/supervisor/agents/base/types.ts | 14 +- src/supervisor/agents/codex/index.ts | 2 + .../agents/codex/nativePlugins.test.ts | 34 ++++ src/supervisor/agents/codex/nativePlugins.ts | 63 ++++++++ .../agents/userMcp/translate.test.ts | 16 +- src/supervisor/agents/userMcp/translate.ts | 13 +- .../crossagentMcp/toolRegistry.test.ts | 9 ++ src/supervisor/crossagentMcp/toolRegistry.ts | 30 +++- src/supervisor/crossagentMcp/types.ts | 2 + src/supervisor/plugins/PluginLoader.ts | 4 +- src/supervisor/plugins/conformance.test.ts | 114 ++++++++++++++ src/supervisor/plugins/pluginMcpRuntime.ts | 12 +- src/supervisor/runtime.test.ts | 17 +- src/supervisor/runtime/sessionTypes.ts | 9 +- .../threadSession/invalidSessionRecovery.ts | 2 + .../runtime/threadSession/managerOptions.ts | 21 ++- .../threadSession/spawnPipeline.test.ts | 24 +++ .../runtime/threadSession/spawnPipeline.ts | 58 +++++-- .../runtime/threadSessionManager.ts | 11 +- src/supervisor/skills/SkillsService.test.ts | 146 ++++++++++++++++++ src/supervisor/skills/SkillsService.ts | 63 +++++++- src/supervisor/skills/pluginSkillPolicy.ts | 5 + src/supervisor/supervisorRuntime.ts | 35 ++++- 88 files changed, 1641 insertions(+), 526 deletions(-) create mode 100644 src/supervisor/agents/codex/nativePlugins.test.ts create mode 100644 src/supervisor/agents/codex/nativePlugins.ts diff --git a/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs b/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs index 17614f263..1872efa65 100644 --- a/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs +++ b/.agents/skills/interactive-testing/scripts/poracode-integration-smoke.mjs @@ -560,9 +560,9 @@ async function pluginsSectionDeepDive(client) { installed: window.__poracodeDev.stores.sharedSettings.getState().installedPlugins["browser-tools"] !== undefined, back: buttonText.includes("Back to plugins"), uninstall: buttonText.includes("Uninstall"), - apps: headings.includes("Apps") && document.body.innerText.includes("Browser"), + mcpServers: headings.includes("MCP servers") && document.body.innerText.includes("Browser"), skills: headings.includes("Skills") && document.body.innerText.includes("Browser Control"), - appSwitch: switchNames.includes("Browser MCP"), + bundledMcpHasNoSeparateSwitch: !switchNames.includes("Browser MCP"), skillSwitch: switchNames.includes("Browser Control Skill"), }; })()`, @@ -571,16 +571,19 @@ async function pluginsSectionDeepDive(client) { state.installed && state.back && state.uninstall && - state.apps && + state.mcpServers && state.skills && - state.appSwitch && + state.bundledMcpHasNoSeparateSwitch && state.skillSwitch, "Browser Tools plugin detail", ); - assert(detailState.apps && detailState.skills, "Browser Tools contributions did not render"); assert( - detailState.appSwitch && detailState.skillSwitch, - "Browser Tools contribution controls did not render", + detailState.mcpServers && detailState.skills, + "Browser Tools contributions did not render", + ); + assert( + detailState.bundledMcpHasNoSeparateSwitch && detailState.skillSwitch, + "Browser Tools contribution controls did not match the combined plugin contract", ); const pluginsScreenshotPath = join(outDir, "smoke-02-plugins.png"); diff --git a/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs b/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs index 80fae8a9f..93531da54 100644 --- a/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs +++ b/.agents/skills/interactive-testing/scripts/smoke-scenarios.mjs @@ -96,7 +96,7 @@ export const functionalAreas = [ }, { id: "plugins-marketplace", - title: "Plugin marketplace, installation, and contributed apps and skills", + title: "Plugin marketplace, installation, and bundled MCP and skill contributions", patterns: [ /components\/plugins\//i, /shared\/(?:contracts\/plugin|plugins\/)/i, diff --git a/resources/plugins/browser-tools/plugin.json b/resources/plugins/browser-tools/plugin.json index 33e2fe1bd..62665663d 100644 --- a/resources/plugins/browser-tools/plugin.json +++ b/resources/plugins/browser-tools/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "browser-tools", - "version": "1.0.0", + "version": "1.1.0", "description": "Browse, inspect, and test websites in Poracode's isolated in-app browser.", "author": { "name": "Poracode", @@ -15,10 +15,17 @@ "title": "Browser Tools", "category": "developer-tools", "featured": true, + "examplePrompt": "Open my local app in Poracode's browser, test the requested flow, and report visual, console, and network evidence", + "coreSkill": "browser-control", + "nativePluginNames": ["browser"], + "nativeCoreSkill": "control-in-app-browser", + "builtInMcpServerIds": ["browser"], "skills": { "browser-control": { "name": "Browser Control", - "description": "Navigate, inspect, and test pages with the in-app Browser MCP." + "description": "Navigate, inspect, and test pages with the in-app Browser MCP.", + "nativePluginName": "browser", + "nativeSkill": "control-in-app-browser" } } } diff --git a/resources/plugins/browser-tools/skills/browser-control/SKILL.md b/resources/plugins/browser-tools/skills/browser-control/SKILL.md index d8d57fe44..7cd4fb07d 100644 --- a/resources/plugins/browser-tools/skills/browser-control/SKILL.md +++ b/resources/plugins/browser-tools/skills/browser-control/SKILL.md @@ -1,18 +1,29 @@ --- name: browser-control -description: Navigate, inspect, and test pages with Poracode's isolated in-app browser. +description: Open, inspect, interact with, and verify websites or local web apps in Poracode's isolated browser. Use for visible page state, navigation, screenshots, console or network evidence, and end-to-end UI testing; do not use it for semantic service operations when a purpose-built connector is available. --- # Browser Control -Use Poracode's Browser MCP when the task depends on a website, rendered page, or local web app. +Use Poracode's `browser` MCP when the task depends on a rendered page, visible interaction, or local web app. If the request is really about structured data or a service operation and a purpose-built connector is available, use that connector instead. An explicit request for Poracode's browser wins. ## Workflow -1. List the available browser tabs and reuse the relevant tab when possible. -2. Inspect the current URL and page snapshot before interacting. -3. Prefer semantic queries and targeted reads over coordinate-based actions. -4. After navigation or a state-changing action, wait for the expected page state and verify it. -5. Use screenshots when visual layout is part of the requirement. +1. Call `browser.api` when you need the current API map, then call `browser.enable` once before the first browser action. +2. Reuse a relevant tab from `browser.list_tabs`; otherwise open the exact URL the user supplied or the known local target. Do not guess a remote site or substitute web search when authentication blocks the requested page. +3. Establish the baseline with the current URL plus `browser.snapshot` or `browser.find`. Prefer accessible roles, names, and returned element refs over brittle selectors or coordinates. +4. Perform the smallest meaningful action. Use `fill` when replacing a field and `type` only when appending is intended. +5. After every navigation or state-changing action, wait for the expected URL, text, or element and inspect the resulting state. For web-app verification, also check relevant console errors and failed network requests. +6. Capture a screenshot when visual layout or appearance is part of the requirement. +7. Call `browser.disable` before asking the user for input, waiting on an external event, or finishing. -The in-app browser is isolated from the user's personal Chrome profile. Do not assume it contains the user's existing logins or extensions. +## Boundaries + +- The in-app browser is isolated from the user's personal Chrome profile. Do not assume it contains existing logins, cookies, or extensions. +- Never inspect cookies or storage unless the task requires it and the user authorized that data access. +- A successful click is not proof of success. Verify the user-visible or application state it was meant to produce. +- Pause before purchases, submissions, messages, deletions, or other irreversible external actions unless the user explicitly authorized that exact action. + +## Output + +Report the tested URL and flow, the final observed state, and the evidence used. Separate visual, console, and network findings, and state any step that could not be verified. diff --git a/resources/plugins/chrome-tools/plugin.json b/resources/plugins/chrome-tools/plugin.json index ee8f1135a..b87255045 100644 --- a/resources/plugins/chrome-tools/plugin.json +++ b/resources/plugins/chrome-tools/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "chrome-tools", - "version": "1.0.0", + "version": "1.1.0", "description": "Work with the pages and signed-in sessions already open in Chrome.", "author": { "name": "Poracode", @@ -15,11 +15,18 @@ "title": "Chrome Tools", "category": "automation", "featured": true, + "examplePrompt": "Use my existing Chrome session to complete this browser task and verify the final visible state", "projectKinds": ["windows", "posix"], + "coreSkill": "chrome-control", + "nativePluginNames": ["chrome"], + "nativeCoreSkill": "control-chrome", + "builtInMcpServerIds": ["chrome"], "skills": { "chrome-control": { "name": "Chrome Control", - "description": "Use Chrome safely when a task needs an existing browser session." + "description": "Use Chrome safely when a task needs an existing browser session.", + "nativePluginName": "chrome", + "nativeSkill": "control-chrome" } } } diff --git a/resources/plugins/chrome-tools/skills/chrome-control/SKILL.md b/resources/plugins/chrome-tools/skills/chrome-control/SKILL.md index 387e546ac..8b267f760 100644 --- a/resources/plugins/chrome-tools/skills/chrome-control/SKILL.md +++ b/resources/plugins/chrome-tools/skills/chrome-control/SKILL.md @@ -1,18 +1,28 @@ --- name: chrome-control -description: Work safely with pages and signed-in sessions already open in the user's Chrome browser. +description: Use the user's real Chrome tabs and signed-in sessions for visible browser workflows. Use when existing authentication, open tabs, cookies, or extensions matter; prefer a purpose-built connector for semantic service operations unless the user explicitly asks for Chrome. --- # Chrome Control -Use Poracode's Chrome MCP when a task depends on the user's current Chrome tabs, authenticated sessions, or installed extensions. +Use Poracode's `chrome` MCP when a task depends on the user's current Chrome tabs, authenticated sessions, or installed extensions. If a purpose-built connector can complete a semantic service operation, prefer it unless the user explicitly requested Chrome or visual interaction is part of the task. ## Workflow -1. List tabs and attach to the relevant existing tab instead of opening duplicates. -2. Inspect the page before typing, clicking, or evaluating scripts. -3. Keep actions scoped to the requested site and task. -4. Verify the resulting URL or visible state after each meaningful action. -5. Pause before irreversible submissions, purchases, deletions, or messages unless the user explicitly authorized them. +1. Call `chrome.chrome_status` first. If the extension is disconnected, ask the user to connect it rather than switching surfaces silently. +2. Call `chrome.enable` once before browser actions. Use the background Poracode workspace by default; call `chrome.chrome_attach` only when the user asked to operate an existing tab. +3. Inspect with `chrome.chrome_snapshot` or `chrome.chrome_find` before clicking or typing. Prefer returned element refs and use `chrome_fill` for replacement versus `chrome_type` for appending. +4. Keep every action scoped to the requested site and task. Do not explore other tabs or signed-in content for extra context. +5. After every meaningful action, wait for and verify the resulting URL, text, control state, or screenshot. +6. Call `chrome.disable` before asking the user for input, waiting on an external event, or finishing. -Treat cookies, page storage, and signed-in content as sensitive user data. +## Boundaries + +- Treat tabs, cookies, storage, and signed-in content as sensitive user data. Do not read cookies unless the task requires it and Chrome data access is enabled. +- Never attach to an unrelated existing tab merely because it is already authenticated. +- A successful tool call is not proof that the website accepted the action; verify the visible result. +- Confirm the exact target and payload before purchases, submissions, messages, deletions, account changes, or other irreversible actions unless already authorized. + +## Output + +Report the target site or tab, what changed, and the final visible evidence. State whether the background workspace or an existing user tab was used, and identify any action left pending for confirmation. diff --git a/resources/plugins/computer-use/plugin.json b/resources/plugins/computer-use/plugin.json index 90eb7ee9c..2f942ce16 100644 --- a/resources/plugins/computer-use/plugin.json +++ b/resources/plugins/computer-use/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "computer-use", - "version": "1.0.0", + "version": "1.1.0", "description": "Control desktop apps and complete visual workflows.", "author": { "name": "Poracode", @@ -15,12 +15,19 @@ "title": "Computer Use", "category": "automation", "featured": true, + "examplePrompt": "Operate the requested desktop app in small verified steps and report the final window state", "platforms": ["win32", "darwin"], "projectKinds": ["windows", "posix"], + "coreSkill": "computer-use", + "nativePluginNames": ["computer-use"], + "nativeCoreSkill": "computer-use", + "builtInMcpServerIds": ["computer-use"], "skills": { "computer-use": { "name": "Computer Use", - "description": "Operate desktop apps through Poracode's desktop-control tools." + "description": "Operate desktop apps through Poracode's desktop-control tools.", + "nativePluginName": "computer-use", + "nativeSkill": "computer-use" } } } diff --git a/resources/plugins/computer-use/skills/computer-use/SKILL.md b/resources/plugins/computer-use/skills/computer-use/SKILL.md index d510c5cc6..db2fab714 100644 --- a/resources/plugins/computer-use/skills/computer-use/SKILL.md +++ b/resources/plugins/computer-use/skills/computer-use/SKILL.md @@ -1,18 +1,28 @@ --- name: computer-use -description: Operate supported desktop apps through Poracode's desktop-control tools. +description: Inspect and operate native Windows or macOS applications through Poracode's desktop-control tools. Use for visual workflows that require real windows; prefer Browser for web pages and a purpose-built connector or API when one can complete the task directly. --- # Computer Use -Use Poracode's Computer Use MCP for tasks that require interacting with desktop applications or native windows. +Use Poracode's `computer_use` MCP for tasks that require interacting with desktop applications or native windows. Do not use it for a web page when Browser or Chrome is the intended surface, or for a semantic operation that a safer purpose-built connector can perform. ## Workflow -1. List applications and windows, then select the exact target. -2. Activate and inspect the target window before interacting. -3. Prefer named controls and keyboard shortcuts when they are reliable. -4. Use small, verifiable interaction steps and check the window state after each one. -5. Do not type secrets into an application unless the user explicitly supplied them for that purpose. +1. Call `computer_use.api` when you need the API map, then list applications and windows and select the exact target. +2. Capture `computer_use.get_window_state` before coordinate input. Use its returned window object and screenshot coordinates; refresh the window if it moved, resized, or became stale. +3. Call `computer_use.enable` immediately before the first interactive action. Keep the session enabled across uninterrupted related steps. +4. Prefer accessibility text, named controls, and reliable keyboard shortcuts. When coordinates are necessary, derive them from the latest screenshot rather than guessing. +5. Use small actions and inspect the window again after each meaningful change. Re-resolve the window after application navigation that may recreate it. +6. Call `computer_use.disable` before asking the user for input, waiting on an external event, or finishing. -Ask before destructive actions or actions that communicate externally when authorization is unclear. +## Boundaries + +- Interactive actions take control of the real mouse and keyboard and bring the target window to the foreground. Avoid unnecessary actions and do not operate a different application. +- Locked desktops, secure prompts, operating-system permission dialogs, passwords, and authentication surfaces require the user. +- Do not type or expose secrets unless the user supplied them for that exact purpose. +- Confirm before destructive changes or external communication unless the user already authorized the exact action. + +## Output + +Report the application and window used, the verified final state, and any step requiring user interaction. Do not claim completion from input dispatch alone. diff --git a/resources/plugins/github/plugin.json b/resources/plugins/github/plugin.json index a5db798cb..8828963ad 100644 --- a/resources/plugins/github/plugin.json +++ b/resources/plugins/github/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "github", - "version": "1.0.0", + "version": "1.1.0", "description": "Triage PRs, issues, CI, and publish flows.", "author": { "name": "GitHub", "url": "https://github.com" }, "homepage": "https://github.com/github/github-mcp-server", @@ -13,23 +13,33 @@ "title": "GitHub", "category": "developer-tools", "featured": true, + "coreSkill": "github", + "nativeCoreSkill": "github", "examplePrompt": "Inspect PRs, triage issues, debug failing checks, and prepare code changes for review", "skills": { "github": { "name": "GitHub", - "description": "Inspect PRs, issues, CI, and publish flows." + "description": "Inspect PRs, issues, CI, and publish flows.", + "nativePluginName": "github", + "nativeSkill": "github" }, "review-follow-up": { "name": "Review Follow-up", - "description": "Address actionable PR feedback." + "description": "Address actionable PR feedback.", + "nativePluginName": "github", + "nativeSkill": "gh-address-comments" }, "ci-debug": { "name": "CI Debug", - "description": "Debug failing GitHub Actions checks." + "description": "Debug failing GitHub Actions checks.", + "nativePluginName": "github", + "nativeSkill": "gh-fix-ci" }, "publish-changes": { "name": "Publish Changes", - "description": "Commit, push, and open a PR." + "description": "Commit, push, and open a PR.", + "nativePluginName": "github", + "nativeSkill": "yeet" } } } diff --git a/resources/plugins/github/skills/ci-debug/SKILL.md b/resources/plugins/github/skills/ci-debug/SKILL.md index 2eeb63b16..410caa8ed 100644 --- a/resources/plugins/github/skills/ci-debug/SKILL.md +++ b/resources/plugins/github/skills/ci-debug/SKILL.md @@ -43,6 +43,9 @@ commit rather than the branch head. Propose the narrowest fix that addresses the root cause. Re-run only the failed jobs when you can. If a re-run is needed to confirm a flake, say that is what you are doing and why. +Do not re-run, cancel, or dispatch a workflow merely to gather more evidence unless the user authorized that GitHub +action. When a fix is local, run the closest equivalent check before asking CI to confirm it. + ## Report Lead with the root cause and the evidence line from the log. Then the fix. If the fix is unverified because CI has not diff --git a/resources/plugins/github/skills/github/SKILL.md b/resources/plugins/github/skills/github/SKILL.md index 817da4109..311e9a3ad 100644 --- a/resources/plugins/github/skills/github/SKILL.md +++ b/resources/plugins/github/skills/github/SKILL.md @@ -8,6 +8,10 @@ description: "Inspect repositories, review pull requests, triage issues, and fol Work with GitHub through the connected `github` MCP server. Prefer its tools over shelling out to `gh` or `git` — they return structured data and work without a local checkout. +Use this core skill to orient the repository and route the request. Use `review-follow-up` for review threads, +`ci-debug` for failing Actions checks, and `publish-changes` for commit/push/PR work. Do not blend a read-only +inspection request into a publishing workflow. + ## Before you start Confirm the server is connected. If its tools are unavailable, say so and stop rather than silently falling back to @@ -22,6 +26,8 @@ Do not guess an owner or repo name. decisions; the description is often stale. - For a PR, read the diff before the discussion. Someone's summary of a change is not the change. - Quote file and line when you reference code, so the user can jump to it. +- Resolve repository, pull request, issue, check, and ref identifiers once and reuse the exact identifiers. If more + than one target plausibly matches, ask rather than acting on the first search result. ## Writing @@ -36,6 +42,9 @@ Never merge, close, or force-push on the user's behalf without them asking for t Give the answer, not a transcript of your API calls. When you list PRs or issues, include number, title, author, and current state, and lead with whatever the user actually asked about. +For any mutation, read back the resulting GitHub state and link the exact repository object. A successful tool call +without the expected state is not verification. + ## Related skills `review-follow-up` for working through review feedback, `ci-debug` for failing checks, `publish-changes` for diff --git a/resources/plugins/github/skills/publish-changes/SKILL.md b/resources/plugins/github/skills/publish-changes/SKILL.md index a9ea72ab8..274e013b1 100644 --- a/resources/plugins/github/skills/publish-changes/SKILL.md +++ b/resources/plugins/github/skills/publish-changes/SKILL.md @@ -31,7 +31,9 @@ and anything that needs a decision. Note what you did not do — deliberate omis Do not describe tests as passing unless you ran them and saw them pass. If something is unverified, say which part and why. -Opening a PR is outward-facing. Show the user the title and body before you create it. +When the user asked you to publish the finished work, that authorizes the commit, push, and draft PR described by this +workflow. Ask only when the branch, included changes, target repository/base, or PR content is materially ambiguous. +Read the created PR back after opening it and verify its head, base, title, and URL. ## After diff --git a/resources/plugins/github/skills/review-follow-up/SKILL.md b/resources/plugins/github/skills/review-follow-up/SKILL.md index 65d7f885a..76c71b3cd 100644 --- a/resources/plugins/github/skills/review-follow-up/SKILL.md +++ b/resources/plugins/github/skills/review-follow-up/SKILL.md @@ -35,6 +35,9 @@ until the code is actually written. Replies are outward-facing: show the user the text before posting. +Before posting, re-read the thread and the current diff so the reply describes the change that actually exists. After +posting, verify the reply is attached to the intended thread; do not resolve a thread unless the user requested it. + ## Report List what you changed, what you answered without changing, and what you deliberately left — with the reason. If some diff --git a/resources/plugins/outlook/plugin.json b/resources/plugins/outlook/plugin.json index 5cc753604..83769f019 100644 --- a/resources/plugins/outlook/plugin.json +++ b/resources/plugins/outlook/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "outlook", - "version": "1.0.0", + "version": "1.1.0", "description": "Triage Microsoft Outlook mail and manage your calendar.", "author": { "name": "Softeria", @@ -18,15 +18,22 @@ "featured": true, "projectKinds": ["windows", "posix"], "communityMaintained": true, + "coreSkill": "outlook-email", + "nativePluginNames": ["outlook-email", "outlook-calendar"], + "nativeCoreSkill": "outlook-email", "examplePrompt": "Triage my inbox, summarize the important threads, and show what's on my calendar today", "skills": { "outlook-email": { "name": "Outlook Email", - "description": "Triage inboxes, summarize threads, and draft replies." + "description": "Triage inboxes, summarize threads, and draft replies.", + "nativePluginName": "outlook-email", + "nativeSkill": "outlook-email" }, "outlook-calendar": { "name": "Outlook Calendar", - "description": "Read your schedule, find times, and manage events." + "description": "Read your schedule, find times, and manage events.", + "nativePluginName": "outlook-calendar", + "nativeSkill": "outlook-calendar" } } } diff --git a/resources/plugins/outlook/skills/outlook-calendar/SKILL.md b/resources/plugins/outlook/skills/outlook-calendar/SKILL.md index 6c47989be..09725e6d1 100644 --- a/resources/plugins/outlook/skills/outlook-calendar/SKILL.md +++ b/resources/plugins/outlook/skills/outlook-calendar/SKILL.md @@ -31,8 +31,9 @@ When you propose slots, give a few concrete options with dates and times, not a ## Writing -Creating, moving, or cancelling an event notifies other people. That is outward-facing: confirm the exact time, -duration, title, and attendee list with the user before you do it, every time. +Creating, moving, or cancelling an event notifies other people. If the user explicitly asked for the action and gave +an exact time, duration, title, and attendee list, perform it. Otherwise confirm only the missing or ambiguous details +before writing. Moving a meeting the user does not organize can be disruptive — say so before doing it rather than after. @@ -42,3 +43,6 @@ Never decline or accept an invitation on the user's behalf unless they asked for State what you found or what you changed, with times in the user's zone. If you could not find a workable slot, say that plainly and show the constraint that blocked it. + +After a write, read back the event and verify its organizer, attendees, start, end, recurrence, and time zone. A +successful API response without the expected calendar state is not completion. diff --git a/resources/plugins/outlook/skills/outlook-email/SKILL.md b/resources/plugins/outlook/skills/outlook-email/SKILL.md index 355201dea..5d4f41ef5 100644 --- a/resources/plugins/outlook/skills/outlook-email/SKILL.md +++ b/resources/plugins/outlook/skills/outlook-email/SKILL.md @@ -37,7 +37,8 @@ For a thread, read it in order and report the current state — the last message ## Drafting and sending **Never send mail without the user explicitly asking you to send that specific message.** Draft, show them the full -text and the recipient list, and wait. +text and the recipient list when approval is still needed. If the user explicitly supplied or approved the exact +recipients and message and asked you to send it, do not ask for duplicate confirmation. Check the recipients yourself before showing a draft: reply versus reply-all is a real mistake with real consequences, and so is an autocompleted wrong address. Say which one you chose. @@ -49,3 +50,6 @@ Deleting, moving, or marking mail read changes state the user can see. Confirm f ## Report Answer the question. If you triaged, lead with what needs them today. + +After a mailbox mutation, verify the resulting draft, sent item, folder, category, or read state. Keep private mailbox +evidence concise and do not reproduce more message content than the user needs. diff --git a/resources/plugins/subagent-delegation/plugin.json b/resources/plugins/subagent-delegation/plugin.json index 9bc3830e3..8649439a4 100644 --- a/resources/plugins/subagent-delegation/plugin.json +++ b/resources/plugins/subagent-delegation/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "subagent-delegation", - "version": "1.0.0", + "version": "1.1.0", "description": "Delegate focused work to other installed agents and coordinate the results.", "author": { "name": "Poracode", @@ -15,6 +15,9 @@ "title": "Subagent Delegation", "category": "productivity", "featured": true, + "examplePrompt": "Delegate independent parts of this task to the best available agents, then validate and consolidate their results", + "coreSkill": "subagent-delegation", + "builtInMcpServerIds": ["crossagents"], "skills": { "subagent-delegation": { "name": "Subagent Delegation", diff --git a/resources/plugins/subagent-delegation/skills/subagent-delegation/SKILL.md b/resources/plugins/subagent-delegation/skills/subagent-delegation/SKILL.md index deb8b24a4..f04905458 100644 --- a/resources/plugins/subagent-delegation/skills/subagent-delegation/SKILL.md +++ b/resources/plugins/subagent-delegation/skills/subagent-delegation/SKILL.md @@ -1,18 +1,39 @@ --- name: subagent-delegation -description: Choose, brief, and coordinate Poracode subagents for parallel work. +description: Delegate independent, bounded work to the best available Poracode agents and consolidate verified results. Use for parallel research, independent reviews, specialist work, or non-overlapping implementation; do not delegate trivial, sequential, tightly coupled, or context-heavy work. --- # Subagent Delegation -Use Poracode's Subagents MCP when independent, bounded work can run in parallel or when a specialist agent is a better fit. +Use Poracode's `crossagents` MCP when independent, bounded work can run in parallel or a specialist or independent second opinion will materially improve the result. The coordinator remains responsible for understanding the problem, protecting shared state, and validating the final answer. + +## Decide whether to delegate + +Delegate when at least one of these is true: + +- two or more subtasks can run independently; +- a distinct provider or specialist perspective is valuable; +- an independent review reduces correctness or security risk; +- a bounded search, test, or implementation lane can return a concrete artifact. + +Do not delegate a trivial task, a sequence whose next step depends on the previous result, overlapping edits, or work that would require copying most of the conversation. Do not delegate merely to avoid understanding the task. ## Workflow -1. Split work into concrete subtasks with clear deliverables and non-overlapping edit scope. -2. Choose an installed agent whose capabilities match each subtask. -3. Include the relevant context, constraints, and verification requirements in every brief. -4. Track active agents and resolve shared-worktree conflicts before accepting results. -5. Consolidate and verify all returned work against the original request. +1. Classify the work with one to five concise task tags. Use `list_agents` when selection matters; call `get_agent` only when you need a provider's detailed models, reasoning choices, Fast support, or permission information. +2. Omit provider, model, reasoning, and Fast unless the user chose them or the task requires a deliberate override. Let Crossagents apply learned and configured routing. +3. Split the work into concrete subtasks with clear deliverables and non-overlapping edit scope. Every prompt must be self-contained and include relevant context, constraints, authority, expected output, and verification. +4. For one short task, call `spawn_agent` in the foreground. Set `background=true` only when the coordinator has useful independent work to do before synchronization. Submit independent tasks together through one `tasks` call for actual parallelism. +5. At the next real synchronization point, wait once for every required background result. Do not repeatedly poll. Cancel or continue without a stalled optional run. +6. Inspect returned evidence and changes, resolve disagreements or shared-worktree conflicts, and verify the combined result against the original request. + +## Safety and retries + +- Child agents have powerful permissions. Their prompt must not authorize actions beyond the user's request. +- Use startup-only fallback retries by default. `any-failure` can repeat writes or external side effects and requires explicit justification and authority. +- Do not allow multiple agents to edit the same files concurrently. Assign exact ownership or make review lanes read-only. +- Treat a confident child response as a claim, not proof. Check the relevant files, commands, tests, sources, or runtime state yourself. + +## Output -Do not delegate a task merely to avoid understanding it. The coordinating agent remains responsible for the final result. +Lead with the consolidated result. Mention delegated lanes only when it helps explain evidence, disagreement, limitations, or provider diversity. State what was verified and what remains uncertain. diff --git a/src/main/browser/external/ChromeMcpIngress.test.ts b/src/main/browser/external/ChromeMcpIngress.test.ts index 9ba9ef5e3..95cf4f757 100644 --- a/src/main/browser/external/ChromeMcpIngress.test.ts +++ b/src/main/browser/external/ChromeMcpIngress.test.ts @@ -45,11 +45,17 @@ describe("ChromeMcpIngress", () => { const list = await postMcp(info, { jsonrpc: "2.0", id: 2, method: "tools/list" }); const listBody = (await list.json()) as { - result: { tools: Array<{ name: string }> }; + result: { tools: Array<{ name: string; annotations?: Record }> }; }; expect(listBody.result.tools.map((tool) => tool.name)).toContain("chrome_status"); expect(listBody.result.tools.map((tool) => tool.name)).toContain("enable"); expect(listBody.result.tools.map((tool) => tool.name)).toContain("disable"); + expect( + listBody.result.tools.find((tool) => tool.name === "chrome_snapshot")?.annotations, + ).toMatchObject({ readOnlyHint: true, destructiveHint: false }); + expect( + listBody.result.tools.find((tool) => tool.name === "chrome_click")?.annotations, + ).toMatchObject({ readOnlyHint: false, destructiveHint: true, openWorldHint: true }); }); it("routes Chrome tool calls and formats their result", async () => { diff --git a/src/main/browser/external/chromeTools.ts b/src/main/browser/external/chromeTools.ts index d46063258..6c7474f66 100644 --- a/src/main/browser/external/chromeTools.ts +++ b/src/main/browser/external/chromeTools.ts @@ -345,7 +345,7 @@ export function formatChromeToolResult(raw: unknown): McpToolResult { return { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) }; } -export const CHROME_TOOLS: ToolSpec[] = [ +const RAW_CHROME_TOOLS: ToolSpec[] = [ { name: "chrome_status", description: @@ -556,4 +556,39 @@ export const CHROME_TOOLS: ToolSpec[] = [ }, ]; +const READ_ONLY_CHROME_TOOL_NAMES = new Set([ + "chrome_status", + "chrome_list_tabs", + "chrome_get_url", + "chrome_get_title", + "chrome_snapshot", + "chrome_find", + "chrome_get", + "chrome_is", + "chrome_wait", + "chrome_screenshot", + "chrome_cookies", +]); +const SESSION_CHROME_TOOL_NAMES = new Set(["enable", "disable"]); +const DESTRUCTIVE_CHROME_TOOL_NAMES = new Set([ + "chrome_click", + "chrome_fill", + "chrome_type", + "chrome_press", + "chrome_eval", +]); + +export const CHROME_TOOLS: ToolSpec[] = RAW_CHROME_TOOLS.map((tool) => ({ + ...tool, + annotations: READ_ONLY_CHROME_TOOL_NAMES.has(tool.name) + ? { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } + : SESSION_CHROME_TOOL_NAMES.has(tool.name) + ? { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } + : { + readOnlyHint: false, + destructiveHint: DESTRUCTIVE_CHROME_TOOL_NAMES.has(tool.name), + openWorldHint: true, + }, +})); + export const CHROME_TOOL_NAMES = new Set(CHROME_TOOLS.map((t) => t.name)); diff --git a/src/main/browser/mcp/toolRegistry.test.ts b/src/main/browser/mcp/toolRegistry.test.ts index 64191b540..9ddec07dd 100644 --- a/src/main/browser/mcp/toolRegistry.test.ts +++ b/src/main/browser/mcp/toolRegistry.test.ts @@ -239,6 +239,18 @@ describe("browser MCP tool registry", () => { expect(formatted.content[0]?.text?.length).toBeLessThan(20_000); }); + it("advertises passive and state-changing tool annotations", () => { + expect(TOOLS.find((tool) => tool.name === "snapshot")?.annotations).toMatchObject({ + readOnlyHint: true, + destructiveHint: false, + }); + expect(TOOLS.find((tool) => tool.name === "click")?.annotations).toMatchObject({ + readOnlyHint: false, + destructiveHint: true, + openWorldHint: true, + }); + }); + it("recognizes agent-browser-style aliases", () => { expect(isKnownToolName("goto")).toBe(true); expect(isKnownToolName("key")).toBe(true); diff --git a/src/main/browser/mcp/tools/specs.ts b/src/main/browser/mcp/tools/specs.ts index da43fe2d7..b97dd77b7 100644 --- a/src/main/browser/mcp/tools/specs.ts +++ b/src/main/browser/mcp/tools/specs.ts @@ -3,7 +3,7 @@ import type { ToolSpec } from "./types"; export const BROWSER_MCP_INSTRUCTIONS = "Use the browser MCP server for browsing, inspecting, clicking, typing, screenshots, network/console checks, and local web app verification inside Poracode. Before the first browsing action, call browser.enable once and keep it enabled across the whole uninterrupted browser session so agent presence stays consistent between calls. Always call browser.disable before pausing to ask for user input, waiting for an external event, or finishing, and enable again when you resume. Prefer browser.snapshot or browser.find before browser.click/fill/type, use @e refs from snapshots when possible, and call browser.api when you need the complete API map."; -export const TOOLS: ToolSpec[] = [ +const RAW_TOOLS: ToolSpec[] = [ { name: "api", description: @@ -597,6 +597,57 @@ export const TOOLS: ToolSpec[] = [ }, ]; +const READ_ONLY_TOOL_NAMES = new Set([ + "api", + "list_tabs", + "get_url", + "get_title", + "screenshot", + "query", + "wait_for", + "snapshot", + "inspect", + "get", + "is", + "find", + "wait", + "wait_for_url", + "wait_for_text", + "wait_for_js", + "frames", +]); +const SESSION_TOOL_NAMES = new Set(["enable", "disable"]); +const DESTRUCTIVE_TOOL_NAMES = new Set([ + "close_tab", + "click", + "dblclick", + "type", + "fill", + "check", + "uncheck", + "select", + "eval", + "press", + "cookies", + "storage", + "dialog", + "addscript", + "addstyle", +]); + +export const TOOLS: ToolSpec[] = RAW_TOOLS.map((tool) => ({ + ...tool, + annotations: READ_ONLY_TOOL_NAMES.has(tool.name) + ? { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } + : SESSION_TOOL_NAMES.has(tool.name) + ? { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } + : { + readOnlyHint: false, + destructiveHint: DESTRUCTIVE_TOOL_NAMES.has(tool.name), + openWorldHint: true, + }, +})); + export const TOOL_NAMES = new Set(TOOLS.map((t) => t.name)); const TOOL_ALIASES = new Map([ diff --git a/src/main/browser/mcp/tools/types.ts b/src/main/browser/mcp/tools/types.ts index 47620bde6..ccff8c98d 100644 --- a/src/main/browser/mcp/tools/types.ts +++ b/src/main/browser/mcp/tools/types.ts @@ -1,4 +1,5 @@ import type { BrowserPanelManager } from "../../BrowserPanelManager"; +import type { McpToolAnnotations } from "@/shared/contracts"; export interface ToolContext { manager: BrowserPanelManager; @@ -14,6 +15,7 @@ export interface ToolSpec { name: string; description: string; inputSchema: Record; + annotations?: McpToolAnnotations; } export interface McpContent { diff --git a/src/main/computer-use/mcp/toolRegistry.test.ts b/src/main/computer-use/mcp/toolRegistry.test.ts index 9ceb9b27b..03ffa09ba 100644 --- a/src/main/computer-use/mcp/toolRegistry.test.ts +++ b/src/main/computer-use/mcp/toolRegistry.test.ts @@ -32,6 +32,15 @@ describe("computer-use toolRegistry", () => { expect(isInteractiveToolName("type")).toBe(true); expect(isInteractiveToolName("get_window_state")).toBe(false); expect(isInteractiveToolName("list_windows")).toBe(false); + expect(TOOLS.find((tool) => tool.name === "get_window_state")?.annotations).toMatchObject({ + readOnlyHint: true, + destructiveHint: false, + }); + expect(TOOLS.find((tool) => tool.name === "click")?.annotations).toMatchObject({ + readOnlyHint: false, + destructiveHint: true, + openWorldHint: true, + }); }); it("preserves the refreshed window returned by interactive driver actions", async () => { diff --git a/src/main/computer-use/mcp/toolRegistry.ts b/src/main/computer-use/mcp/toolRegistry.ts index 8cbba40dc..000a761d4 100644 --- a/src/main/computer-use/mcp/toolRegistry.ts +++ b/src/main/computer-use/mcp/toolRegistry.ts @@ -1,4 +1,5 @@ import type { ComputerUseDriver, ComputerUseScreenshot, ComputerUseWindowState } from "./types"; +import type { McpToolAnnotations } from "@/shared/contracts"; import { readNumber, readString, readWindow } from "../drivers/common"; export interface ToolContext { @@ -11,6 +12,7 @@ export interface ToolSpec { name: string; description: string; inputSchema: Record; + annotations?: McpToolAnnotations; } const WINDOW_SCHEMA = { @@ -30,7 +32,7 @@ const WINDOW_SCHEMA = { export const COMPUTER_USE_MCP_INSTRUCTIONS = "Use the computer_use MCP server to inspect and control native macOS or Windows apps on the host desktop (including when the user is driving from a paired phone/remote client — agents still run on that desktop). Start with computer_use.api or computer_use.list_apps, choose a returned window, then call computer_use.get_window_state before coordinate input. Immediately before the first interactive action, call computer_use.enable once; it keeps the Computer Use overlay visible across the whole uninterrupted control session. Keep it enabled between related actions, including passive inspection calls. Always call computer_use.disable before you pause to ask for user input, wait for an external event, or finish; call enable again when you resume. Prefer ordinary Win32 desktop apps when you have a choice — some Store/WinUI apps recreate window handles during activation, so always prefer the `window` object returned by interactive tools (or re-call list_windows/get_window) before the next click/type. list/get/screenshot operations are passive and do not steal focus; click, drag, scroll, type_text, press_key, activate_window, and launch_app switch to interactive mode, bring the target app to the FOREGROUND, and take exclusive control of the real mouse/keyboard — nobody should use the host machine while interactive computer-use is running. Coordinates (x/y) are window-relative with the origin at the TOP-LEFT of the window frame (including the title bar), matching the top-left pixel of the most recent get_window_state screenshot for that window; if the window may have moved or resized, call get_window_state again before sending coordinates. If a tool reports that the window is no longer available (windows are re-identified after they move/resize), call computer_use.list_windows or computer_use.get_window to obtain a fresh window id and retry. Prefer the browser MCP server for web pages. Locked desktops, secure prompts, OS permission prompts, and password/authentication surfaces require the user."; -export const TOOLS: ToolSpec[] = [ +const RAW_TOOLS: ToolSpec[] = [ { name: "api", description: @@ -173,6 +175,24 @@ export const TOOLS: ToolSpec[] = [ }, ]; +const READ_ONLY_TOOL_NAMES = new Set([ + "api", + "list_apps", + "list_windows", + "get_window", + "get_window_state", +]); +const SESSION_TOOL_NAMES = new Set(["enable", "disable"]); + +export const TOOLS: ToolSpec[] = RAW_TOOLS.map((tool) => ({ + ...tool, + annotations: READ_ONLY_TOOL_NAMES.has(tool.name) + ? { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } + : SESSION_TOOL_NAMES.has(tool.name) + ? { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } + : { readOnlyHint: false, destructiveHint: true, openWorldHint: true }, +})); + export const TOOL_NAMES = new Set(TOOLS.map((tool) => tool.name)); const INTERACTIVE_TOOL_NAMES = new Set([ diff --git a/src/renderer/components/composer/MentionInput.test.ts b/src/renderer/components/composer/MentionInput.test.ts index 7bc1aa69c..3044c446c 100644 --- a/src/renderer/components/composer/MentionInput.test.ts +++ b/src/renderer/components/composer/MentionInput.test.ts @@ -8,6 +8,7 @@ import { MentionInput, type McpMentionItem, type MentionInputHandle, + type PluginMentionItem, } from "./MentionInput"; vi.mock("./MentionPopover", () => ({ MentionPopover: () => null })); @@ -50,6 +51,35 @@ describe("buildMentionResults", () => { detail: "Computer Use", enabled: true, }; + const github: PluginMentionItem = { + id: "github", + name: "GitHub", + detail: "Plugin", + command: { + id: "github", + label: "GitHub", + skillName: "github", + skillPath: "C:\\plugins\\github\\skills\\github\\SKILL.md", + skillInvocation: "$github", + skillProvider: "GitHub", + skillScope: "global", + pluginId: "github", + pluginName: "GitHub", + }, + }; + + it("shows a plugin as one result before its underlying MCP and files", () => { + expect(buildMentionResults(fileResults, "git", [browser], [github])).toEqual([ + { + type: "plugin", + path: "github", + name: "GitHub", + detail: "Plugin", + command: github.command, + }, + ...fileResults, + ]); + }); it("shows Browser when typing an empty @ mention", () => { expect(buildMentionResults(fileResults, "", [browser])).toEqual([ @@ -226,6 +256,57 @@ describe("MCP mention selection", () => { }); }); +describe("plugin mention selection", () => { + it("inserts one plugin badge that preserves the core skill and plugin identity", () => { + const ref = createRef(); + render( + createElement(MentionInput, { + ref, + placeholder: "Send a message...", + projectLocation: undefined, + onTextChange: vi.fn<(hasText: boolean) => void>(), + onSubmit: vi.fn<(segments: PromptSegment[]) => void>(), + pluginMentions: [ + { + id: "github", + name: "GitHub", + detail: "Plugin", + command: { + id: "github", + label: "GitHub", + skillName: "github", + skillPath: "C:\\plugins\\github\\skills\\github\\SKILL.md", + skillInvocation: "$github", + skillProvider: "GitHub", + skillScope: "global", + pluginId: "github", + pluginName: "GitHub", + }, + }, + ], + }), + ); + + const editor = typeMention("git"); + fireEvent.keyDown(editor, { key: "Enter" }); + + expect(editor.querySelector('[data-plugin-id="github"]')).toHaveTextContent("GitHub"); + expect(ref.current?.serializeSegments()).toEqual([ + { + kind: "skill", + name: "github", + path: "C:\\plugins\\github\\skills\\github\\SKILL.md", + invocation: "$github", + provider: "GitHub", + scope: "global", + pluginId: "github", + pluginName: "GitHub", + }, + { kind: "text", content: " " }, + ]); + }); +}); + describe("Enter handling", () => { const baseProps = { placeholder: "Send a message...", diff --git a/src/renderer/components/composer/MentionInput.tsx b/src/renderer/components/composer/MentionInput.tsx index f4fdab0f9..26adf4183 100644 --- a/src/renderer/components/composer/MentionInput.tsx +++ b/src/renderer/components/composer/MentionInput.tsx @@ -30,13 +30,23 @@ export interface McpMentionItem { enabled: boolean; } +/** An installed Agent Plugin surfaced as one `@`-mention. */ +export interface PluginMentionItem { + id: string; + name: string; + detail: string; + command: AgentSlashCommand; +} + /** Stable empty list so an omitted `mcpMentions` prop doesn't churn renders. */ const EMPTY_MCP_MENTIONS: readonly McpMentionItem[] = []; +const EMPTY_PLUGIN_MENTIONS: readonly PluginMentionItem[] = []; export function buildMentionResults( fileResults: FileEntry[], query: string, mcpMentions: readonly McpMentionItem[] = EMPTY_MCP_MENTIONS, + pluginMentions: readonly PluginMentionItem[] = EMPTY_PLUGIN_MENTIONS, ): MentionEntry[] { const q = query.trim().toLowerCase(); // Case-insensitive prefix match on the display name, matching the legacy @@ -51,7 +61,16 @@ export function buildMentionResults( detail: item.detail, enabled: item.enabled, })); - return [...mcpResults, ...fileResults]; + const pluginResults: MentionEntry[] = pluginMentions + .filter((item) => item.name.toLowerCase().startsWith(q)) + .map((item) => ({ + type: "plugin", + path: item.id, + name: item.name, + detail: item.detail, + command: item.command, + })); + return [...pluginResults, ...mcpResults, ...fileResults]; } export interface MentionInputHandle { @@ -196,6 +215,8 @@ function skillChipDataset(segment: Extract) { skillInvocation: segment.invocation, skillProvider: segment.provider, skillScope: segment.scope, + ...(segment.pluginId ? { pluginId: segment.pluginId } : {}), + ...(segment.pluginName ? { pluginName: segment.pluginName } : {}), }; } @@ -246,6 +267,7 @@ export const MentionInput = forwardRef< * call `onMcpMentionSelect` so the composer can enable them first. */ mcpMentions?: readonly McpMentionItem[]; + pluginMentions?: readonly PluginMentionItem[]; onMcpMentionSelect?: (id: string) => void; onSlashCommandChange?: (query: string | null) => void; commandListId?: string; @@ -277,9 +299,11 @@ export const MentionInput = forwardRef< onInterceptKey, } = props; const mcpMentions = props.mcpMentions ?? EMPTY_MCP_MENTIONS; + const pluginMentions = props.pluginMentions ?? EMPTY_PLUGIN_MENTIONS; // Stable dependency key: which MCP mentions are offered, independent of the // array's per-render identity (mirrors the old boolean flags in the effect). const mcpMentionKey = mcpMentions.map((item) => `${item.id}:${item.enabled}`).join(","); + const pluginMentionKey = pluginMentions.map((item) => item.id).join(","); const editorRef = useRef(null); const lastSlashQueryRef = useRef(null); const voicePreviewRef = useRef(null); @@ -292,11 +316,16 @@ export const MentionInput = forwardRef< mention !== null, projectId, ); - const results = buildMentionResults(fileResults, mention?.query ?? "", mcpMentions); + const results = buildMentionResults( + fileResults, + mention?.query ?? "", + mcpMentions, + pluginMentions, + ); useEffect(() => { setActiveIndex(0); - }, [mention?.query, fileResults, mcpMentionKey]); + }, [mention?.query, fileResults, mcpMentionKey, pluginMentionKey]); function insertPlainText(text: string) { const editor = editorRef.current; @@ -581,14 +610,31 @@ export const MentionInput = forwardRef< const range = detectTriggerRange("@"); if (!range) return; - if (entry.type === "mcp") { + if (entry.type === "mcp" || entry.type === "plugin") { + const pluginSegment = + entry.type === "plugin" ? skillSegmentFromSlashCommand(entry.command) : undefined; + if (entry.type === "plugin" && !pluginSegment) return; const sel = window.getSelection(); if (!sel) return; sel.removeAllRanges(); sel.addRange(range); range.deleteContents(); - if (entry.enabled) { + if (entry.type === "plugin") { + if (!pluginSegment) return; + const chip = createSlashCommandChipElement({ + id: entry.command.id, + ...skillChipDataset(pluginSegment), + }); + range.insertNode(chip); + const space = document.createTextNode(" "); + chip.after(space); + const nextRange = document.createRange(); + nextRange.setStartAfter(space); + nextRange.collapse(true); + sel.removeAllRanges(); + sel.addRange(nextRange); + } else if (entry.enabled) { const chip = createMcpMentionChipElement({ id: entry.path, name: entry.name }); range.insertNode(chip); // Trailing nbsp keeps the caret visually separate from the chip, matching diff --git a/src/renderer/components/composer/MentionPopover.tsx b/src/renderer/components/composer/MentionPopover.tsx index 71d7731ee..0990d8c79 100644 --- a/src/renderer/components/composer/MentionPopover.tsx +++ b/src/renderer/components/composer/MentionPopover.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import type { LucideIcon } from "lucide-react"; -import type { FileEntry } from "@/shared/contracts"; +import type { AgentSlashCommand, FileEntry } from "@/shared/contracts"; import { getEntryIconUrl } from "@/renderer/components/common/fileIcons"; +import { PluginIcon } from "@/renderer/components/plugins/PluginIcon"; /** * A composer MCP server (Browser, Crossagents, Computer Use, …) surfaced as an @@ -19,7 +20,15 @@ export type McpMentionEntry = { enabled: boolean; }; -export type MentionEntry = FileEntry | McpMentionEntry; +export type PluginMentionEntry = { + type: "plugin"; + path: string; + name: string; + detail: string; + command: AgentSlashCommand; +}; + +export type MentionEntry = FileEntry | McpMentionEntry | PluginMentionEntry; function getParentDir(path: string): string { const lastSlash = path.lastIndexOf("/"); @@ -70,8 +79,9 @@ export function MentionPopover(props: { {results.map((entry, index) => { const isActive = index === activeIndex; const isMcp = entry.type === "mcp"; + const isPlugin = entry.type === "plugin"; const McpIcon = isMcp ? entry.icon : null; - const dir = isMcp ? "" : getParentDir(entry.path); + const dir = isMcp || isPlugin ? "" : getParentDir(entry.path); return (
- {McpIcon ? ( + {isPlugin ? ( + + ) : McpIcon ? (
- {examplePrompt ? ( - @@ -123,9 +132,10 @@ export function PluginDetail(props: {
- {examplePrompt ? ( + {examplePrompt && coreSkill ? ( - ); - } - return ( - ); } @@ -361,7 +379,7 @@ function ConnectControl(props: { function ContributionSection(props: { icon: ReactNode; title: string; - description: string; + description?: string; children: ReactNode; }) { return ( @@ -370,7 +388,7 @@ function ContributionSection(props: { {props.icon}

{props.title}

-

{props.description}

+ {props.description ?

{props.description}

: null}
diff --git a/src/renderer/components/plugins/pluginCopy.ts b/src/renderer/components/plugins/pluginCopy.ts index 189fa77d2..6443d2f94 100644 --- a/src/renderer/components/plugins/pluginCopy.ts +++ b/src/renderer/components/plugins/pluginCopy.ts @@ -72,26 +72,26 @@ export function useLocalizedPluginCatalog(): LocalizedPlugin[] { const skills = plugin.skills.map((skill): LocalizedPluginContribution => { const policy = plugin.poracode.skills[skill.folder]; - switch (skill.folder) { - case "browser-control": + switch (`${plugin.name}:${skill.folder}`) { + case "browser-tools:browser-control": return { id: skill.folder, name: t`Browser Control`, description: t`Navigate, inspect, and test pages with the in-app Browser MCP.`, }; - case "chrome-control": + case "chrome-tools:chrome-control": return { id: skill.folder, name: t`Chrome Control`, description: t`Use Chrome safely when a task needs an existing browser session.`, }; - case "computer-use": + case "computer-use:computer-use": return { id: skill.folder, name: t`Computer Use`, description: t`Operate desktop apps through Poracode's desktop-control tools.`, }; - case "subagent-delegation": + case "subagent-delegation:subagent-delegation": return { id: skill.folder, name: t`Subagent Delegation`, @@ -108,7 +108,22 @@ export function useLocalizedPluginCatalog(): LocalizedPlugin[] { // Server transport detail is author-supplied and identifies the endpoint, so // it is shown verbatim rather than translated. - const mcpServers = plugin.mcpServers.map((server): LocalizedPluginContribution => { + const builtInMcpServers = plugin.poracode.builtInMcpServerIds.map( + (id): LocalizedPluginContribution => ({ + id, + name: + id === "browser" + ? t`Browser` + : id === "chrome" + ? t`Chrome` + : id === "crossagents" + ? t`Crossagents` + : id === "computer-use" + ? t`Computer Use` + : id, + }), + ); + const declaredMcpServers = plugin.mcpServers.map((server): LocalizedPluginContribution => { const entry = server.entry; return { id: server.name, @@ -116,6 +131,7 @@ export function useLocalizedPluginCatalog(): LocalizedPlugin[] { description: entry.type === "stdio" ? entry.command : entry.url, }; }); + const mcpServers = [...builtInMcpServers, ...declaredMcpServers]; const category = plugin.poracode.category === "developer-tools" diff --git a/src/renderer/components/skills/useSkills.test.ts b/src/renderer/components/skills/useSkills.test.ts index ebf896a08..7d593bf27 100644 --- a/src/renderer/components/skills/useSkills.test.ts +++ b/src/renderer/components/skills/useSkills.test.ts @@ -5,15 +5,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SkillScanResult } from "@/shared/contracts"; import { dynamicActivate, i18n } from "@/renderer/i18n/i18n"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { usePlugins } from "@/renderer/state/pluginsStore"; import { pluginFixture, seedBuiltInPlugins } from "@/renderer/testUtils/plugins"; -import { buildSkillSlashCommands, useSkills, useSkillSlashCommandState } from "./useSkills"; +import { + buildSkillSlashCommands, + usePluginMentionItems, + useSkills, + useSkillSlashCommandState, +} from "./useSkills"; const { scanSkillsMock } = vi.hoisted(() => ({ scanSkillsMock: vi.fn<() => Promise>(), })); vi.mock("@/renderer/bridge", () => ({ - readBridge: () => ({ scanSkills: scanSkillsMock }), + readBridge: () => ({ platform: "win32", scanSkills: scanSkillsMock }), })); const invocationByProvider = { @@ -129,6 +135,38 @@ describe("useSkills", () => { await waitFor(() => expect(hook.result.current.resolved).toBe(true)); }); + it("rescans mounted composer skills when the plugin catalog refreshes", async () => { + scanSkillsMock.mockResolvedValueOnce(emptyScan()).mockResolvedValueOnce(emptyScan()); + const hook = renderHook(() => useSkills(undefined, "codex", "CatalogRefreshTest")); + await waitFor(() => expect(scanSkillsMock).toHaveBeenCalledTimes(1)); + + act(() => usePlugins.setState((state) => ({ revision: state.revision + 1 }))); + await waitFor(() => expect(scanSkillsMock).toHaveBeenCalledTimes(2)); + hook.unmount(); + }); + + it("represents an installed plugin as one mention backed by its core skill", async () => { + useSharedSettings.getState().installPlugin(pluginFixture("browser-tools")); + scanSkillsMock.mockResolvedValueOnce(pluginSkillScan()); + const hook = renderHook( + () => usePluginMentionItems({ kind: "windows", path: "C:\\PluginMentionTest" }, "codex"), + { wrapper: I18nWrapper }, + ); + + await waitFor(() => expect(hook.result.current).toHaveLength(1)); + expect(hook.result.current[0]).toMatchObject({ + id: "browser-tools", + name: "Browser Tools", + detail: "Plugin", + command: { + skillName: "browser-control", + skillInvocation: "$browser-control", + pluginId: "browser-tools", + pluginName: "Browser Tools", + }, + }); + }); + it("scopes composer scans to the active presentation", async () => { scanSkillsMock.mockResolvedValueOnce(emptyScan()); const projectLocation = { kind: "windows" as const, path: "C:\\PresentationSkillTest" }; diff --git a/src/renderer/components/skills/useSkills.ts b/src/renderer/components/skills/useSkills.ts index 5f7c11f82..afa7e8476 100644 --- a/src/renderer/components/skills/useSkills.ts +++ b/src/renderer/components/skills/useSkills.ts @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import { useLingui } from "@lingui/react/macro"; import type { AgentSlashCommand, InstalledPlugins, @@ -14,6 +15,13 @@ import { type LocalizedPlugin, } from "@/renderer/components/plugins/pluginCopy"; import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; +import { usePlugins } from "@/renderer/state/pluginsStore"; +import type { PluginMentionItem } from "@/renderer/components/composer/MentionInput"; +import { + getPluginCoreSkill, + isPluginSkillEnabled, + isPluginSupportedForProject, +} from "@/shared/plugins/catalog"; const scanCache = new Map(); const pendingScans = new Map>(); @@ -58,7 +66,8 @@ export function useSkills( presentationMode?: ThreadPresentationMode, ) { const installedPlugins = useSharedSettings((state) => state.installedPlugins); - const requestKey = `${agentKind ?? ""}\0${wslDistro ?? ""}\0${presentationMode ?? ""}\0${projectLocation ? JSON.stringify(projectLocation) : ""}\0${pluginSkillScanKey(installedPlugins)}`; + const pluginRevision = usePlugins((state) => state.revision); + const requestKey = `${agentKind ?? ""}\0${wslDistro ?? ""}\0${presentationMode ?? ""}\0${projectLocation ? JSON.stringify(projectLocation) : ""}\0${pluginSkillScanKey(installedPlugins)}\0${pluginRevision}`; const cachedScan = scanCache.get(requestKey); const [scanState, setScanState] = useState< | { @@ -142,35 +151,88 @@ export function buildSkillSlashCommands( scan: SkillScanResult | null, localizedPlugins: readonly LocalizedPlugin[] = [], ): AgentSlashCommand[] { - if (!scan?.invocation) return []; + const invocation = scan?.invocation; + if (!invocation) return []; const effective = new Set(scan.effectiveSkillIds); return scan.skills.flatMap((skill) => { if (!effective.has(skill.id)) return []; - const { localizedPlugin, localizedSkill } = resolveLocalizedPluginSkill( - localizedPlugins, - skill, + return [buildSkillSlashCommand(skill, invocation, localizedPlugins)]; + }); +} + +function buildSkillSlashCommand( + skill: SkillScanResult["skills"][number], + invocationKind: NonNullable, + localizedPlugins: readonly LocalizedPlugin[], +): AgentSlashCommand { + const { localizedPlugin, localizedSkill } = resolveLocalizedPluginSkill(localizedPlugins, skill); + const displayName = localizedSkill?.name ?? skill.name; + const description = localizedSkill?.description ?? skill.description; + const invocation = + invocationKind === "dollar" + ? `$${skill.name}` + : invocationKind === "skill" + ? `/skill:${skill.name}` + : invocationKind === "prompt" + ? `Use the ${skill.name} skill.` + : `/${skill.name}`; + return { + id: skill.name, + label: description ? `${displayName} — ${description}` : displayName, + ...(description ? { description } : {}), + section: "skills", + skillName: skill.name, + skillPath: skill.skillFilePath, + skillInvocation: invocation, + skillProvider: localizedPlugin?.name ?? skill.providerLabel, + skillScope: skill.scope, + ...(skill.pluginId ? { pluginId: skill.pluginId } : {}), + ...(skill.pluginName ? { pluginName: localizedPlugin?.name ?? skill.pluginName } : {}), + }; +} + +/** Installed plugins represented as one composer mention backed by their core skill. */ +export function usePluginMentionItems( + projectLocation: ProjectLocation, + agentKind: string, + presentationMode?: ThreadPresentationMode, +): PluginMentionItem[] { + const { t } = useLingui(); + const { scan } = useSkills(projectLocation, agentKind, undefined, presentationMode); + const localizedPlugins = useLocalizedPluginCatalog(); + const installedPlugins = useSharedSettings((state) => state.installedPlugins); + const disabledBuiltIns = useSharedSettings((state) => state.disabledBuiltInMcpServers); + const invocation = scan?.invocation; + if (!invocation) return []; + + return localizedPlugins.flatMap((localized): PluginMentionItem[] => { + const plugin = localized.plugin; + const state = installedPlugins[plugin.name]; + const core = getPluginCoreSkill(plugin); + if ( + !state?.enabled || + !core || + !isPluginSkillEnabled(plugin, state, core.folder) || + !isPluginSupportedForProject(plugin, readBridge().platform, projectLocation) || + plugin.poracode.builtInMcpServerIds.some((id) => disabledBuiltIns[id] === true) + ) { + return []; + } + const skill = scan.skills.find( + (candidate) => + candidate.pluginId === plugin.name && + candidate.folderName === core.folder && + candidate.enabled && + candidate.valid, ); - const displayName = localizedSkill?.name ?? skill.name; - const description = localizedSkill?.description ?? skill.description; - const invocation = - scan.invocation === "dollar" - ? `$${skill.name}` - : scan.invocation === "skill" - ? `/skill:${skill.name}` - : scan.invocation === "prompt" - ? `Use the ${skill.name} skill.` - : `/${skill.name}`; + if (!skill) return []; + const command = buildSkillSlashCommand(skill, invocation, localizedPlugins); return [ { - id: skill.name, - label: description ? `${displayName} — ${description}` : displayName, - ...(description ? { description } : {}), - section: "skills" as const, - skillName: skill.name, - skillPath: skill.skillFilePath, - skillInvocation: invocation, - skillProvider: localizedPlugin?.name ?? skill.providerLabel, - skillScope: skill.scope, + id: plugin.name, + name: localized.name, + detail: t`Plugin`, + command: { ...command, pluginId: plugin.name, pluginName: localized.name }, }, ]; }); diff --git a/src/renderer/components/thread/ChatPane/parts/items/UserMessage.tsx b/src/renderer/components/thread/ChatPane/parts/items/UserMessage.tsx index 8667d36dd..4da58b547 100644 --- a/src/renderer/components/thread/ChatPane/parts/items/UserMessage.tsx +++ b/src/renderer/components/thread/ChatPane/parts/items/UserMessage.tsx @@ -29,6 +29,7 @@ import { CheckpointRevertButton, type CheckpointRevertRequest } from "../Checkpo import { chatPromptSurfaceClass } from "./chatMessageSurface"; import { CopyTextButton } from "./CopyTextButton"; import { InlineFilePathChip } from "./InlineFilePathChip"; +import { PluginIcon } from "@/renderer/components/plugins/PluginIcon"; import { ItemMarkdown } from "./ItemMarkdown"; import { extractSelectorPayloads } from "./SelectorBadge"; import { @@ -322,7 +323,8 @@ function buildUserPromptText(content: CanonicalContentBlock[]): string { return content .map((block) => { if (block.kind === "text") return block.text; - if (block.kind === "skill") return block.invocation; + if (block.kind === "skill") + return block.pluginName ? `@${block.pluginName}` : block.invocation; if (block.kind === "diff_comment") return formatDiffCommentPrompt(block); if (block.kind === "mcp") return `@${block.name}`; if (block.kind === "file" && block.source !== "attachment") return block.path; @@ -360,9 +362,16 @@ function renderUserMessageInlineContent( nodes.push(