diff --git a/apps/app/pr/mcp-enable-disable/mcp-enabled-pause-light.png b/apps/app/pr/mcp-enable-disable/mcp-enabled-pause-light.png new file mode 100644 index 000000000..fc7d0e785 Binary files /dev/null and b/apps/app/pr/mcp-enable-disable/mcp-enabled-pause-light.png differ diff --git a/apps/app/pr/mcp-enable-disable/mcp-paused-resume-dark.png b/apps/app/pr/mcp-enable-disable/mcp-paused-resume-dark.png new file mode 100644 index 000000000..5fb10b1ee Binary files /dev/null and b/apps/app/pr/mcp-enable-disable/mcp-paused-resume-dark.png differ diff --git a/apps/app/src/app/lib/openwork-server.ts b/apps/app/src/app/lib/openwork-server.ts index 7b7eb1963..c8d1456e8 100644 --- a/apps/app/src/app/lib/openwork-server.ts +++ b/apps/app/src/app/lib/openwork-server.ts @@ -189,6 +189,7 @@ export type OpenworkMcpItem = { name: string; config: Record; source: "config.project" | "config.global" | "config.remote"; + inherited?: boolean; disabledByTools?: boolean; }; @@ -1036,23 +1037,23 @@ export function createOpenworkServerClient(options: { baseUrl: string; token?: s method: "POST", body: payload, }), - removeMcp: (workspaceId: string, name: string) => - requestJson<{ items: OpenworkMcpItem[] }>(baseUrl, `/workspace/${workspaceId}/mcp/${encodeURIComponent(name)}`, { - token, - hostToken, - method: "DELETE", - }), setMcpEnabled: (workspaceId: string, name: string, enabled: boolean) => - requestJson<{ items: OpenworkMcpItem[] }>( + requestJson<{ items: OpenworkMcpItem[]; enabled: boolean; changed: boolean }>( baseUrl, - `/workspace/${workspaceId}/mcp/${encodeURIComponent(name)}/enabled`, + `/workspace/${workspaceId}/mcp/${encodeURIComponent(name)}`, { token, hostToken, - method: "POST", + method: "PATCH", body: { enabled }, }, ), + removeMcp: (workspaceId: string, name: string) => + requestJson<{ items: OpenworkMcpItem[] }>(baseUrl, `/workspace/${workspaceId}/mcp/${encodeURIComponent(name)}`, { + token, + hostToken, + method: "DELETE", + }), logoutMcpAuth: (workspaceId: string, name: string) => requestJson<{ ok: true }>(baseUrl, `/workspace/${workspaceId}/mcp/${encodeURIComponent(name)}/auth`, { diff --git a/apps/app/src/app/mcp.test.ts b/apps/app/src/app/mcp.test.ts new file mode 100644 index 000000000..c85b75223 --- /dev/null +++ b/apps/app/src/app/mcp.test.ts @@ -0,0 +1,189 @@ +import { describe, expect, test } from "bun:test"; +import { parse } from "jsonc-parser"; + +import { + parseEffectiveMcpServersFromContent, + parseMcpServersFromContent, + updateMcpEnabledInConfigContent, +} from "./mcp"; + +describe("mcp config helpers", () => { + test("updates enabled without changing the MCP config shape", () => { + const updated = updateMcpEnabledInConfigContent( + `{ + // keep workspace comments + "mcp": { + "stripe": { + "type": "remote", + "url": "https://example.com/mcp", + "headers": { + "x-team": "payments" + } + } + } +} +`, + "stripe", + false, + ); + + expect(updated).toContain("// keep workspace comments"); + const parsed = parse(updated) as { + mcp: { stripe: { type: string; url: string; enabled: boolean; headers: Record } }; + }; + expect(parsed.mcp.stripe).toEqual({ + type: "remote", + url: "https://example.com/mcp", + headers: { "x-team": "payments" }, + enabled: false, + }); + }); + + test("parses paused MCP apps as configured entries", () => { + const entries = parseMcpServersFromContent( + JSON.stringify({ + mcp: { + stripe: { + type: "remote", + url: "https://example.com/mcp", + enabled: false, + }, + }, + }), + ); + + expect(entries).toEqual([ + { + name: "stripe", + source: "config.project", + config: { + type: "remote", + url: "https://example.com/mcp", + enabled: false, + }, + }, + ]); + }); + + test("rejects unknown MCP app names", () => { + expect(() => + updateMcpEnabledInConfigContent( + JSON.stringify({ + mcp: { + stripe: { type: "remote", url: "https://example.com/mcp" }, + }, + }), + "linear", + false, + ), + ).toThrow("MCP server not found"); + }); + + test("pauses an inherited global MCP with a minimal workspace override", () => { + const updated = updateMcpEnabledInConfigContent( + "{}\n", + "linear", + false, + { + inheritedMcpServers: [ + { + name: "linear", + config: { + type: "remote", + url: "https://example.com/linear", + headers: { Authorization: "Bearer secret" }, + }, + }, + ], + }, + ); + + const parsed = parse(updated) as { mcp: { linear: Record } }; + expect(parsed.mcp.linear).toEqual({ enabled: false }); + expect(updated).not.toContain("example.com/linear"); + expect(updated).not.toContain("secret"); + }); + + test("resumes an inherited global MCP by removing the workspace override", () => { + const updated = updateMcpEnabledInConfigContent( + JSON.stringify({ + mcp: { + linear: { enabled: false }, + stripe: { type: "remote", url: "https://example.com/stripe" }, + }, + }), + "linear", + true, + { + inheritedMcpServers: [ + { + name: "linear", + config: { + type: "remote", + url: "https://example.com/linear", + }, + }, + ], + }, + ); + + const parsed = parse(updated) as { mcp: Record }; + expect(parsed.mcp.linear).toBeUndefined(); + expect(parsed.mcp.stripe).toEqual({ type: "remote", url: "https://example.com/stripe" }); + }); + + test("does not write an override when inherited global MCP is already disabled", () => { + const source = "{}\n"; + const updated = updateMcpEnabledInConfigContent(source, "linear", false, { + inheritedMcpServers: [ + { + name: "linear", + config: { + type: "remote", + url: "https://example.com/linear", + enabled: false, + }, + }, + ], + }); + + expect(updated).toBe(source); + }); + + test("lists inherited global MCPs with workspace pause overrides", () => { + const entries = parseEffectiveMcpServersFromContent( + JSON.stringify({ + mcp: { + linear: { enabled: false }, + stripe: { type: "remote", url: "https://example.com/stripe" }, + }, + }), + JSON.stringify({ + mcp: { + linear: { type: "remote", url: "https://example.com/linear" }, + github: { type: "remote", url: "https://example.com/github" }, + }, + }), + ); + + expect(entries).toEqual([ + { + name: "linear", + config: { type: "remote", url: "https://example.com/linear", enabled: false }, + source: "config.project", + inherited: true, + }, + { + name: "github", + config: { type: "remote", url: "https://example.com/github" }, + source: "config.global", + inherited: true, + }, + { + name: "stripe", + config: { type: "remote", url: "https://example.com/stripe" }, + source: "config.project", + }, + ]); + }); +}); diff --git a/apps/app/src/app/mcp.ts b/apps/app/src/app/mcp.ts index d740a6e32..882630a31 100644 --- a/apps/app/src/app/mcp.ts +++ b/apps/app/src/app/mcp.ts @@ -1,9 +1,10 @@ -import { parse } from "jsonc-parser"; +import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser"; import type { McpServerConfig, McpServerEntry } from "./types"; import { readOpencodeConfig, writeOpencodeConfig } from "./lib/desktop"; import { CHROME_DEVTOOLS_MCP_COMMAND, CHROME_DEVTOOLS_MCP_ID } from "./constants"; type McpConfigValue = Record | null | undefined; +const jsoncFormattingOptions = { insertSpaces: true, tabSize: 2, eol: "\n" }; export const CHROME_DEVTOOLS_AUTO_CONNECT_ARG = "--autoConnect"; @@ -51,10 +52,29 @@ export function validateMcpServerName(name: string): string { return trimmed; } +function isConfiguredMcpEntry(entry: unknown): entry is Record { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false; + const type = (entry as Record).type; + return type === "local" || type === "remote"; +} + +function isDisabledMcpOverride(entry: unknown): boolean { + return ( + Boolean(entry) && + typeof entry === "object" && + !Array.isArray(entry) && + (entry as Record).enabled === false && + !isConfiguredMcpEntry(entry) + ); +} + export async function removeMcpFromConfig( projectDir: string, name: string, ): Promise { + const globalConfigFile = await readOpencodeConfig("global", projectDir); + const inheritedMcpServers = parseMcpServersFromContent(globalConfigFile.content ?? ""); + const inheritedEntry = inheritedMcpServers.find((entry) => entry.name === name); const configFile = await readOpencodeConfig("project", projectDir); let existingConfig: Record = {}; if (configFile.exists && configFile.content?.trim()) { @@ -66,7 +86,16 @@ export async function removeMcpFromConfig( } const mcpSection = existingConfig["mcp"] as Record | undefined; - if (!mcpSection || !(name in mcpSection)) return; + const projectEntry = mcpSection?.[name]; + if (isDisabledMcpOverride(projectEntry) && inheritedEntry) { + throw new Error("This MCP app is inherited from the global config."); + } + if (!mcpSection || !(name in mcpSection)) { + if (inheritedEntry) { + throw new Error("This MCP app is inherited from the global config."); + } + return; + } delete mcpSection[name]; const writeResult = await writeOpencodeConfig( @@ -79,6 +108,98 @@ export async function removeMcpFromConfig( } } +export function updateMcpEnabledInConfigContent( + content: string, + name: string, + enabled: boolean, + options: { inheritedMcpServers?: readonly McpServerEntry[] } = {}, +): string { + const source = content.trim() ? content : "{}\n"; + const errors: { error: number; offset: number; length: number }[] = []; + const parsed = parse(source, errors, { allowTrailingComma: true }) as Record | undefined; + if (errors.length > 0) { + const detail = errors.map((error) => printParseErrorCode(error.error)).join(", "); + throw new Error(`Failed to parse opencode config${detail ? `: ${detail}` : ""}`); + } + + const mcpSection = parsed?.mcp as Record | undefined; + const entry = mcpSection?.[name]; + const inheritedEntry = options.inheritedMcpServers?.find((item) => item.name === name); + const inheritedEnabled = inheritedEntry ? inheritedEntry.config.enabled !== false : false; + + if (isConfiguredMcpEntry(entry)) { + const updated = applyEdits( + source, + modify(source, ["mcp", name, "enabled"], enabled, { + formattingOptions: jsoncFormattingOptions, + }), + ); + return updated.endsWith("\n") ? updated : `${updated}\n`; + } + + if (isDisabledMcpOverride(entry)) { + if (!inheritedEntry) { + throw new Error("MCP server not found"); + } + if (!enabled) return source.endsWith("\n") ? source : `${source}\n`; + if (!inheritedEnabled) { + throw new Error("This MCP server is disabled in the global config."); + } + const updated = applyEdits( + source, + modify(source, ["mcp", name], undefined, { + formattingOptions: jsoncFormattingOptions, + }), + ); + return updated.endsWith("\n") ? updated : `${updated}\n`; + } + + if (entry != null) { + throw new Error("MCP config is not a configurable server"); + } + + if (inheritedEntry) { + if (enabled) { + if (!inheritedEnabled) { + throw new Error("This MCP server is disabled in the global config."); + } + return source.endsWith("\n") ? source : `${source}\n`; + } + if (!inheritedEnabled) return source.endsWith("\n") ? source : `${source}\n`; + const updated = applyEdits( + source, + modify(source, ["mcp", name], { enabled: false }, { + formattingOptions: jsoncFormattingOptions, + }), + ); + return updated.endsWith("\n") ? updated : `${updated}\n`; + } + + throw new Error(mcpSection ? "MCP server not found" : "Workspace config has no MCP section yet"); +} + +export async function setMcpEnabledInConfig( + projectDir: string, + name: string, + enabled: boolean, +): Promise<{ changed: boolean }> { + const safeName = validateMcpServerName(name); + const configFile = await readOpencodeConfig("project", projectDir); + const projectContent = configFile.exists && configFile.content ? configFile.content : "{}\n"; + const globalConfigFile = await readOpencodeConfig("global", projectDir); + const inheritedMcpServers = parseMcpServersFromContent(globalConfigFile.content ?? ""); + + const nextContent = updateMcpEnabledInConfigContent(projectContent, safeName, enabled, { + inheritedMcpServers, + }); + if (nextContent === projectContent) return { changed: false }; + const writeResult = await writeOpencodeConfig("project", projectDir, nextContent); + if (!writeResult.ok) { + throw new Error(writeResult.stderr || writeResult.stdout || "Failed to write opencode.json"); + } + return { changed: true }; +} + export function parseMcpServersFromContent(content: string): McpServerEntry[] { if (!content.trim()) return []; @@ -106,3 +227,56 @@ export function parseMcpServersFromContent(content: string): McpServerEntry[] { return []; } } + +function parseMcpMap(content: string): Record { + if (!content.trim()) return {}; + try { + const parsed = parse(content) as Record | undefined; + const mcp = parsed?.mcp; + return mcp && typeof mcp === "object" && !Array.isArray(mcp) ? (mcp as Record) : {}; + } catch { + return {}; + } +} + +export function parseEffectiveMcpServersFromContent( + projectContent: string, + globalContent = "", +): McpServerEntry[] { + const projectMcp = parseMcpMap(projectContent); + const globalMcp = parseMcpMap(globalContent); + const entries: McpServerEntry[] = []; + + for (const [name, value] of Object.entries(globalMcp)) { + const projectValue = projectMcp[name]; + if (Object.prototype.hasOwnProperty.call(projectMcp, name)) { + if (isDisabledMcpOverride(projectValue) && isConfiguredMcpEntry(value)) { + entries.push({ + name, + config: { ...(value as McpServerConfig), enabled: false }, + source: "config.project", + inherited: true, + }); + } + continue; + } + if (!isConfiguredMcpEntry(value)) continue; + entries.push({ + name, + config: value as McpServerConfig, + source: "config.global", + inherited: true, + }); + } + + for (const [name, value] of Object.entries(projectMcp)) { + if (isDisabledMcpOverride(value) || !isConfiguredMcpEntry(value)) continue; + entries.push({ + name, + config: value as McpServerConfig, + source: "config.project", + }); + } + + return entries; +} diff --git a/apps/app/src/app/types.ts b/apps/app/src/app/types.ts index 9b215e0da..46f373ad7 100644 --- a/apps/app/src/app/types.ts +++ b/apps/app/src/app/types.ts @@ -330,6 +330,7 @@ export type McpServerEntry = { name: string; config: McpServerConfig; source?: McpServerSource; + inherited?: boolean; }; export type McpStatus = diff --git a/apps/app/src/i18n/locales/ca.ts b/apps/app/src/i18n/locales/ca.ts index 12b9d68e3..68c5fe55d 100644 --- a/apps/app/src/i18n/locales/ca.ts +++ b/apps/app/src/i18n/locales/ca.ts @@ -476,8 +476,8 @@ export default { "den.worker_not_ready_title": "Aquest worker encara no està preparat per obrir-se.", "den.worker_provider_label": "{provider} worker", "den.worker_secondary_cloud": "Worker Cloud", - "extensions.app_count_one": "Aplicació {count} connectada", - "extensions.app_count_many": "Aplicacions {count} connectades", + "extensions.app_count_one": "Aplicació {count} configurada", + "extensions.app_count_many": "Aplicacions {count} configurades", "extensions.apps_mcp_header": "Aplicacions (MCP)", "extensions.filter_all": "Tots", "extensions.filter_apps": "Aplicacions", @@ -654,7 +654,9 @@ export default { "mcp.advanced_settings": "Configuració avançada", "mcp.advanced_settings_hint": "Edita els fitxers de configuració i gestiona les connexions manualment.", "mcp.app_connected": "aplicació connectada", + "mcp.app_configured": "app configured", "mcp.apps_connected": "aplicacions connectades", + "mcp.apps_configured": "apps configured", "mcp.apps_subtitle": "Connecta les teves eines preferides perquè OpenWork les pugui fer servir per tu.", "mcp.apps_title": "Aplicacions", "mcp.auth.already_connected": "Ja està connectat", @@ -757,6 +759,7 @@ export default { "mcp.friendly_status_offline": "Fora de línia", "mcp.friendly_status_paused": "En pausa", "mcp.friendly_status_ready": "A punt", + "mcp.inherited_app": "Global app", "mcp.last_synced": "Sincronitzat", "mcp.login_action": "Inicia la sessió", "mcp.login_hint": "Connecta el teu compte per acabar de configurar aquesta app.", @@ -780,6 +783,11 @@ export default { "mcp.opening_label": "S'està obrint...", "mcp.pick_workspace_error": "Tria primer una carpeta de workspace.", "mcp.pick_workspace_first": "Tria primer una carpeta de workspace.", + "mcp.pause_app": "Pause", + "mcp.pause_modal_message": "Pause {server}? OpenWork will keep the app in your config, but its tools will stop loading after OpenCode reloads.", + "mcp.pause_modal_title": "Pause app?", + "mcp.pause_success": "Paused {server}.", + "mcp.pause_working": "Pausing...", "mcp.quick_connect_chrome_desc": "Controla pestanyes de Chrome amb automatització del navegador.", "mcp.quick_connect_chrome_title": "Control Chrome", "mcp.quick_connect_context7_desc": "Cerca documentació del producte amb més context.", @@ -796,14 +804,18 @@ export default { "mcp.reload_banner_description": "Toca Activa per acabar de connectar l'aplicació.", "mcp.reload_banner_description_blocked": "S'està executant una tasca. Atura-la primer i després activa-la.", "mcp.remote_workspace_url_hint": "Els workers remots es connecten més ràpidament amb servidors MCP basats en URL.", + "mcp.read_only": "MCP config is read-only.", "mcp.remove_app": "Eliminar", "mcp.remove_failed": "No s'ha pogut eliminar l'aplicació.", "mcp.remove_modal_message": "Esteu segur que voleu eliminar {server}? Sempre el podeu tornar a afegir més tard.", "mcp.remove_modal_title": "Elimina l'aplicació", "mcp.reveal_config_failed": "No s'ha pogut obrir el fitxer de configuració", "mcp.reveal_in_finder": "Mostra a Finder", + "mcp.resume_app": "Resume", + "mcp.resume_success": "Resumed {server}.", "mcp.scope_global": "Tots els workspaces", "mcp.scope_project": "Aquest workspace", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "Command", "mcp.server_command_hint": "La comanda de shell per arrencar el servidor.", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -812,6 +824,7 @@ export default { "mcp.server_type": "Tipus", "mcp.server_url": "URL del servidor", "mcp.server_url_placeholder": "https://api.githubcopilot.com/mcp/", + "mcp.toggle_failed": "Couldn't update the app.", "mcp.sign_in_section_label": "Inicia la sessió", "mcp.tap_to_connect": "Toca per connectar", "mcp.technical_details": "Detalls tècnics", diff --git a/apps/app/src/i18n/locales/en.ts b/apps/app/src/i18n/locales/en.ts index 166391e77..0a8a4ce2f 100644 --- a/apps/app/src/i18n/locales/en.ts +++ b/apps/app/src/i18n/locales/en.ts @@ -478,8 +478,8 @@ export default { "den.worker_not_ready_title": "This worker is not ready to open yet.", "den.worker_provider_label": "{provider} worker", "den.worker_secondary_cloud": "Cloud worker", - "extensions.app_count_one": "{count} app connected", - "extensions.app_count_many": "{count} apps connected", + "extensions.app_count_one": "{count} app configured", + "extensions.app_count_many": "{count} apps configured", "extensions.apps_mcp_header": "Apps (MCP)", "extensions.filter_all": "All", "extensions.filter_apps": "Apps", @@ -649,11 +649,7 @@ export default { "inbox_panel.uploading": "Uploading...", "inbox_panel.uploading_label": "Uploading {label}...", "mcp.activate_button": "Activate", - "mcp.disable_app": "Disable", - "mcp.enable_app": "Enable", "mcp.reloading_status": "Reloading MCP servers…", - "mcp.toggle_failed": "Failed to update MCP enabled state.", - "mcp.toggle_requires_server": "Connect to an OpenWork server to enable or disable MCPs.", "mcp.add_modal_subtitle": "Connect a custom MCP server by URL or local command.", "mcp.add_modal_title": "Add Custom App", "mcp.add_server_button": "Add App", @@ -661,7 +657,9 @@ export default { "mcp.advanced_settings": "Advanced settings", "mcp.advanced_settings_hint": "Edit config files and manage connections manually.", "mcp.app_connected": "app connected", + "mcp.app_configured": "app configured", "mcp.apps_connected": "apps connected", + "mcp.apps_configured": "apps configured", "mcp.apps_subtitle": "Connect your favorite tools so OpenWork can use them on your behalf.", "mcp.apps_title": "Apps", "mcp.auth.already_connected": "Already Connected", @@ -764,6 +762,7 @@ export default { "mcp.friendly_status_offline": "Offline", "mcp.friendly_status_paused": "Paused", "mcp.friendly_status_ready": "Ready", + "mcp.inherited_app": "Global app", "mcp.last_synced": "Synced", "mcp.login_action": "Sign in", "mcp.login_hint": "Connect your account to finish setting up this app.", @@ -787,6 +786,11 @@ export default { "mcp.opening_label": "Opening...", "mcp.pick_workspace_error": "Choose a workspace folder first.", "mcp.pick_workspace_first": "Choose a workspace folder first.", + "mcp.pause_app": "Pause", + "mcp.pause_modal_message": "Pause {server}? OpenWork will keep the app in your config, but its tools will stop loading after OpenCode reloads.", + "mcp.pause_modal_title": "Pause app?", + "mcp.pause_success": "Paused {server}.", + "mcp.pause_working": "Pausing...", "mcp.quick_connect_chrome_desc": "Drive Chrome tabs with browser automation.", "mcp.quick_connect_chrome_title": "Control Chrome", "mcp.quick_connect_context7_desc": "Search product docs with richer context.", @@ -803,14 +807,18 @@ export default { "mcp.reload_banner_description": "Tap Activate to finish connecting your app.", "mcp.reload_banner_description_blocked": "A task is running. Stop it first, then activate.", "mcp.remote_workspace_url_hint": "Remote workers connect fastest with URL-based MCP servers.", + "mcp.read_only": "MCP config is read-only.", "mcp.remove_app": "Remove", "mcp.remove_failed": "Couldn't remove the app.", "mcp.remove_modal_message": "Are you sure you want to remove {server}? You can always add it back later.", "mcp.remove_modal_title": "Remove app", "mcp.reveal_config_failed": "Couldn't open the config file", "mcp.reveal_in_finder": "Show in Finder", + "mcp.resume_app": "Resume", + "mcp.resume_success": "Resumed {server}.", "mcp.scope_global": "All workspaces", "mcp.scope_project": "This workspace", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "Command", "mcp.server_command_hint": "The shell command to start the server.", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -822,6 +830,7 @@ export default { "mcp.sign_in_section_label": "Sign-in", "mcp.tap_to_connect": "Tap to connect", "mcp.technical_details": "Technical details", + "mcp.toggle_failed": "Couldn't update the app.", "mcp.type_cloud": "Cloud (sign in with your account)", "mcp.type_local": "Local (runs on this device)", "mcp.type_local_cmd": "Local (command)", diff --git a/apps/app/src/i18n/locales/es.ts b/apps/app/src/i18n/locales/es.ts index da84f9ec7..c5744383a 100644 --- a/apps/app/src/i18n/locales/es.ts +++ b/apps/app/src/i18n/locales/es.ts @@ -476,8 +476,8 @@ export default { "den.worker_not_ready_title": "Este worker aún no está listo para abrir.", "den.worker_provider_label": "Worker de {provider}", "den.worker_secondary_cloud": "Worker de Cloud", - "extensions.app_count_one": "Aplicación {count} conectada", - "extensions.app_count_many": "Aplicaciones {count} conectadas", + "extensions.app_count_one": "Aplicación {count} configurada", + "extensions.app_count_many": "Aplicaciones {count} configuradas", "extensions.apps_mcp_header": "Aplicaciones (MCP)", "extensions.filter_all": "Todo", "extensions.filter_apps": "Aplicaciones", @@ -654,7 +654,9 @@ export default { "mcp.advanced_settings": "Ajustes avanzados", "mcp.advanced_settings_hint": "Edita archivos de configuración y gestiona conexiones manualmente.", "mcp.app_connected": "aplicación conectada", + "mcp.app_configured": "app configured", "mcp.apps_connected": "aplicaciones conectadas", + "mcp.apps_configured": "apps configured", "mcp.apps_subtitle": "Conecta tus herramientas favoritas para que OpenWork pueda usarlas por ti.", "mcp.apps_title": "Aplicaciones", "mcp.auth.already_connected": "Ya conectado", @@ -757,6 +759,7 @@ export default { "mcp.friendly_status_offline": "Desconectado", "mcp.friendly_status_paused": "En pausa", "mcp.friendly_status_ready": "Listo", + "mcp.inherited_app": "Global app", "mcp.last_synced": "Sincronizado", "mcp.login_action": "Iniciar sesión", "mcp.login_hint": "Conecta tu cuenta para terminar de configurar esta app.", @@ -780,6 +783,11 @@ export default { "mcp.opening_label": "Abriendo...", "mcp.pick_workspace_error": "Elige primero una carpeta del espacio de trabajo.", "mcp.pick_workspace_first": "Elige primero una carpeta del espacio de trabajo.", + "mcp.pause_app": "Pause", + "mcp.pause_modal_message": "Pause {server}? OpenWork will keep the app in your config, but its tools will stop loading after OpenCode reloads.", + "mcp.pause_modal_title": "Pause app?", + "mcp.pause_success": "Paused {server}.", + "mcp.pause_working": "Pausing...", "mcp.quick_connect_chrome_desc": "Controla pestañas de Chrome con automatización del navegador.", "mcp.quick_connect_chrome_title": "Control Chrome", "mcp.quick_connect_context7_desc": "Busca documentación del producto con más contexto.", @@ -796,14 +804,18 @@ export default { "mcp.reload_banner_description": "Toca Activar para terminar de conectar tu app.", "mcp.reload_banner_description_blocked": "Hay una tarea en ejecución. Deténla primero y luego actívalo.", "mcp.remote_workspace_url_hint": "Los workers remotos se conectan más rápido con servidores MCP basados en URL.", + "mcp.read_only": "MCP config is read-only.", "mcp.remove_app": "Eliminar", "mcp.remove_failed": "No se pudo eliminar la aplicación.", "mcp.remove_modal_message": "¿Seguro que quieres eliminar {server}? Siempre podrás volver a añadirlo más tarde.", "mcp.remove_modal_title": "Quitar aplicación", "mcp.reveal_config_failed": "No se pudo abrir el archivo de configuración", "mcp.reveal_in_finder": "Mostrar en Finder", + "mcp.resume_app": "Resume", + "mcp.resume_success": "Resumed {server}.", "mcp.scope_global": "Todos los espacios de trabajo", "mcp.scope_project": "Este espacio de trabajo", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "Command", "mcp.server_command_hint": "El Command de shell para iniciar el servidor.", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -812,6 +824,7 @@ export default { "mcp.server_type": "Tipo", "mcp.server_url": "URL del servidor", "mcp.server_url_placeholder": "https://api.githubcopilot.com/mcp/", + "mcp.toggle_failed": "Couldn't update the app.", "mcp.sign_in_section_label": "Iniciar sesión", "mcp.tap_to_connect": "Toca para conectarte", "mcp.technical_details": "Detalles técnicos", diff --git a/apps/app/src/i18n/locales/fr.ts b/apps/app/src/i18n/locales/fr.ts index f79a39fc0..61e635e77 100644 --- a/apps/app/src/i18n/locales/fr.ts +++ b/apps/app/src/i18n/locales/fr.ts @@ -476,8 +476,8 @@ export default { "den.worker_not_ready_title": "Ce worker n'est pas encore prêt à être ouvert.", "den.worker_provider_label": "worker {provider}", "den.worker_secondary_cloud": "Worker cloud", - "extensions.app_count_one": "{count} application connectée", - "extensions.app_count_many": "{count} applications connectées", + "extensions.app_count_one": "{count} application configurée", + "extensions.app_count_many": "{count} applications configurées", "extensions.apps_mcp_header": "Applications (MCP)", "extensions.filter_all": "Tout", "extensions.filter_apps": "Applications", @@ -654,7 +654,9 @@ export default { "mcp.advanced_settings": "Paramètres avancés", "mcp.advanced_settings_hint": "Modifiez les fichiers de configuration et gérez les connexions manuellement.", "mcp.app_connected": "application connectée", + "mcp.app_configured": "app configured", "mcp.apps_connected": "applications connectées", + "mcp.apps_configured": "apps configured", "mcp.apps_subtitle": "Connectez vos outils préférés pour qu'OpenWork puisse les utiliser en votre nom.", "mcp.apps_title": "Applications", "mcp.auth.already_connected": "Déjà connecté", @@ -757,6 +759,7 @@ export default { "mcp.friendly_status_offline": "Hors ligne", "mcp.friendly_status_paused": "En pause", "mcp.friendly_status_ready": "Prêt", + "mcp.inherited_app": "Global app", "mcp.last_synced": "Synchronisé", "mcp.login_action": "Se connecter", "mcp.login_hint": "Connectez votre compte pour terminer la configuration de cette application.", @@ -780,6 +783,11 @@ export default { "mcp.opening_label": "Ouverture...", "mcp.pick_workspace_error": "Choisissez d'abord un dossier d'espace de travail.", "mcp.pick_workspace_first": "Choisissez d'abord un dossier d'espace de travail.", + "mcp.pause_app": "Pause", + "mcp.pause_modal_message": "Pause {server}? OpenWork will keep the app in your config, but its tools will stop loading after OpenCode reloads.", + "mcp.pause_modal_title": "Pause app?", + "mcp.pause_success": "Paused {server}.", + "mcp.pause_working": "Pausing...", "mcp.quick_connect_chrome_desc": "Pilotez les onglets Chrome avec l'automatisation du navigateur.", "mcp.quick_connect_chrome_title": "Control Chrome", "mcp.quick_connect_context7_desc": "Recherchez dans la documentation produit avec un contexte plus riche.", @@ -796,14 +804,18 @@ export default { "mcp.reload_banner_description": "Appuyez sur Activer pour terminer la connexion de votre application.", "mcp.reload_banner_description_blocked": "Une tâche est en cours. Arrêtez-la d'abord, puis activez.", "mcp.remote_workspace_url_hint": "Les workers distants se connectent plus rapidement avec des serveurs MCP basés sur une URL.", + "mcp.read_only": "MCP config is read-only.", "mcp.remove_app": "Supprimer", "mcp.remove_failed": "Impossible de supprimer l'application.", "mcp.remove_modal_message": "Êtes-vous sûr de vouloir supprimer {server} ? Vous pourrez toujours l'ajouter de nouveau plus tard.", "mcp.remove_modal_title": "Supprimer l'application", "mcp.reveal_config_failed": "Impossible d'ouvrir le fichier de configuration", "mcp.reveal_in_finder": "Afficher dans le Finder", + "mcp.resume_app": "Resume", + "mcp.resume_success": "Resumed {server}.", "mcp.scope_global": "Tous les espaces de travail", "mcp.scope_project": "Cet espace de travail", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "Commande", "mcp.server_command_hint": "La commande shell pour démarrer le serveur.", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -812,6 +824,7 @@ export default { "mcp.server_type": "Type", "mcp.server_url": "URL du serveur", "mcp.server_url_placeholder": "https://api.githubcopilot.com/mcp/", + "mcp.toggle_failed": "Couldn't update the app.", "mcp.sign_in_section_label": "Connexion", "mcp.tap_to_connect": "Appuyez pour connecter", "mcp.technical_details": "Détails techniques", diff --git a/apps/app/src/i18n/locales/ja.ts b/apps/app/src/i18n/locales/ja.ts index f22e9cdd7..894186dfd 100644 --- a/apps/app/src/i18n/locales/ja.ts +++ b/apps/app/src/i18n/locales/ja.ts @@ -458,8 +458,8 @@ export default { "den.worker_not_ready_title": "このワーカーはまだ開ける状態ではありません。", "den.worker_provider_label": "{provider}ワーカー", "den.worker_secondary_cloud": "クラウドワーカー", - "extensions.app_count_one": "{count}件のアプリ接続済み", - "extensions.app_count_many": "{count}件のアプリ接続済み", + "extensions.app_count_one": "{count}件のアプリ設定済み", + "extensions.app_count_many": "{count}件のアプリ設定済み", "extensions.apps_mcp_header": "アプリ(MCP)", "extensions.filter_all": "すべて", "extensions.filter_apps": "アプリ", @@ -636,7 +636,9 @@ export default { "mcp.advanced_settings": "詳細設定", "mcp.advanced_settings_hint": "設定ファイルを編集し、接続を手動で管理します。", "mcp.app_connected": "アプリ接続済み", + "mcp.app_configured": "アプリ設定済み", "mcp.apps_connected": "アプリ接続済み", + "mcp.apps_configured": "アプリ設定済み", "mcp.apps_subtitle": "ツールを接続して、OpenWorkが代わりに操作できるようにしましょう。", "mcp.apps_title": "アプリ", "mcp.auth.already_connected": "既に接続済み", @@ -739,6 +741,7 @@ export default { "mcp.friendly_status_offline": "オフライン", "mcp.friendly_status_paused": "一時停止中", "mcp.friendly_status_ready": "準備完了", + "mcp.inherited_app": "グローバル app", "mcp.last_synced": "同期済み", "mcp.login_action": "サインイン", "mcp.login_hint": "アカウントを接続してこのアプリのセットアップを完了してください。", @@ -762,6 +765,11 @@ export default { "mcp.opening_label": "開いています…", "mcp.pick_workspace_error": "最初にワークスペースフォルダを選択してください。", "mcp.pick_workspace_first": "最初にワークスペースフォルダを選択してください。", + "mcp.pause_app": "一時停止", + "mcp.pause_modal_message": "{server} を一時停止しますか?OpenWork はこのアプリ設定を残しますが、OpenCode の再読み込み後はツールを読み込みません。", + "mcp.pause_modal_title": "アプリを一時停止しますか?", + "mcp.pause_success": "{server} を一時停止しました。", + "mcp.pause_working": "一時停止中…", "mcp.quick_connect_chrome_desc": "ブラウザ自動化でChromeタブを操作。", "mcp.quick_connect_chrome_title": "Chromeを操作", "mcp.quick_connect_context7_desc": "より豊富なコンテキストで製品ドキュメントを検索。", @@ -778,14 +786,18 @@ export default { "mcp.reload_banner_description": "有効化をタップしてアプリの接続を完了してください。", "mcp.reload_banner_description_blocked": "タスクが実行中です。先に停止してから有効化してください。", "mcp.remote_workspace_url_hint": "リモートワーカーはURLベースのMCPサーバーとの接続が最も速いです。", + "mcp.read_only": "MCP設定は読み取り専用です。", "mcp.remove_app": "削除", "mcp.remove_failed": "アプリを削除できませんでした。", "mcp.remove_modal_message": "{server} を削除してもよろしいですか?後で再度追加できます。", "mcp.remove_modal_title": "アプリを削除", "mcp.reveal_config_failed": "設定ファイルを開けませんでした", "mcp.reveal_in_finder": "Finderで表示", + "mcp.resume_app": "再開", + "mcp.resume_success": "{server} を再開しました。", "mcp.scope_global": "すべてのワークスペース", "mcp.scope_project": "このワークスペース", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "コマンド", "mcp.server_command_hint": "サーバーを起動するシェルコマンド。", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -797,6 +809,7 @@ export default { "mcp.sign_in_section_label": "サインイン", "mcp.tap_to_connect": "タップして接続", "mcp.technical_details": "技術的な詳細", + "mcp.toggle_failed": "アプリを更新できませんでした。", "mcp.type_cloud": "クラウド(アカウントでサインイン)", "mcp.type_local": "ローカル(このデバイスで実行)", "mcp.type_local_cmd": "ローカル(コマンド)", diff --git a/apps/app/src/i18n/locales/pt-BR.ts b/apps/app/src/i18n/locales/pt-BR.ts index 96e94bddf..574aba3c3 100644 --- a/apps/app/src/i18n/locales/pt-BR.ts +++ b/apps/app/src/i18n/locales/pt-BR.ts @@ -459,8 +459,8 @@ export default { "den.worker_not_ready_title": "Este worker ainda não está pronto para abrir.", "den.worker_provider_label": "Worker {provider}", "den.worker_secondary_cloud": "Worker na nuvem", - "extensions.app_count_one": "{count} app conectado", - "extensions.app_count_many": "{count} apps conectados", + "extensions.app_count_one": "{count} app configurado", + "extensions.app_count_many": "{count} apps configurados", "extensions.apps_mcp_header": "Apps (MCP)", "extensions.filter_all": "Todos", "extensions.filter_apps": "Apps", @@ -637,7 +637,9 @@ export default { "mcp.advanced_settings": "Configurações avançadas", "mcp.advanced_settings_hint": "Edite arquivos de configuração e gerencie conexões manualmente.", "mcp.app_connected": "app conectado", + "mcp.app_configured": "app configured", "mcp.apps_connected": "apps conectados", + "mcp.apps_configured": "apps configured", "mcp.apps_subtitle": "Conecte suas ferramentas favoritas para que o OpenWork as use em seu nome.", "mcp.apps_title": "Apps", "mcp.auth.already_connected": "Já Conectado", @@ -740,6 +742,7 @@ export default { "mcp.friendly_status_offline": "Offline", "mcp.friendly_status_paused": "Pausado", "mcp.friendly_status_ready": "Pronto", + "mcp.inherited_app": "Global app", "mcp.last_synced": "Sincronizado", "mcp.login_action": "Entrar", "mcp.login_hint": "Conecte sua conta para terminar de configurar este app.", @@ -763,6 +766,11 @@ export default { "mcp.opening_label": "Abrindo...", "mcp.pick_workspace_error": "Escolha primeiro uma pasta de workspace.", "mcp.pick_workspace_first": "Escolha primeiro uma pasta de workspace.", + "mcp.pause_app": "Pause", + "mcp.pause_modal_message": "Pause {server}? OpenWork will keep the app in your config, but its tools will stop loading after OpenCode reloads.", + "mcp.pause_modal_title": "Pause app?", + "mcp.pause_success": "Paused {server}.", + "mcp.pause_working": "Pausing...", "mcp.quick_connect_chrome_desc": "Controle abas do Chrome com automação de navegador.", "mcp.quick_connect_chrome_title": "Controlar Chrome", "mcp.quick_connect_context7_desc": "Pesquise docs de produto com contexto mais rico.", @@ -779,14 +787,18 @@ export default { "mcp.reload_banner_description": "Toque em Ativar para terminar de conectar seu app.", "mcp.reload_banner_description_blocked": "Uma tarefa está em execução. Pare-a primeiro e então ative.", "mcp.remote_workspace_url_hint": "Workers remotos se conectam mais rápido com servidores MCP baseados em URL.", + "mcp.read_only": "MCP config is read-only.", "mcp.remove_app": "Remover", "mcp.remove_failed": "Não foi possível remover o app.", "mcp.remove_modal_message": "Tem certeza que deseja remover {server}? Você pode adicioná-lo de volta a qualquer momento.", "mcp.remove_modal_title": "Remover app", "mcp.reveal_config_failed": "Não foi possível abrir o arquivo de configuração", "mcp.reveal_in_finder": "Mostrar no Finder", + "mcp.resume_app": "Resume", + "mcp.resume_success": "Resumed {server}.", "mcp.scope_global": "Todos os workspaces", "mcp.scope_project": "Este workspace", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "Comando", "mcp.server_command_hint": "O comando shell para iniciar o servidor.", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -795,6 +807,7 @@ export default { "mcp.server_type": "Tipo", "mcp.server_url": "URL do servidor", "mcp.server_url_placeholder": "https://api.githubcopilot.com/mcp/", + "mcp.toggle_failed": "Couldn't update the app.", "mcp.sign_in_section_label": "Login", "mcp.tap_to_connect": "Toque para conectar", "mcp.technical_details": "Detalhes técnicos", diff --git a/apps/app/src/i18n/locales/th.ts b/apps/app/src/i18n/locales/th.ts index 94ac119a1..68a4b52cb 100644 --- a/apps/app/src/i18n/locales/th.ts +++ b/apps/app/src/i18n/locales/th.ts @@ -459,8 +459,8 @@ export default { "den.worker_not_ready_title": "Worker นี้ยังไม่พร้อมเปิด", "den.worker_provider_label": "{provider} worker", "den.worker_secondary_cloud": "Cloud worker", - "extensions.app_count_one": "{count} แอปเชื่อมต่อแล้ว", - "extensions.app_count_many": "{count} แอปเชื่อมต่อแล้ว", + "extensions.app_count_one": "{count} แอปตั้งค่าแล้ว", + "extensions.app_count_many": "{count} แอปตั้งค่าแล้ว", "extensions.apps_mcp_header": "แอป (MCP)", "extensions.filter_all": "ทั้งหมด", "extensions.filter_apps": "แอป", @@ -637,7 +637,9 @@ export default { "mcp.advanced_settings": "การตั้งค่าขั้นสูง", "mcp.advanced_settings_hint": "แก้ไขไฟล์ config และจัดการการเชื่อมต่อด้วยตนเอง", "mcp.app_connected": "แอปที่เชื่อมต่อ", + "mcp.app_configured": "app configured", "mcp.apps_connected": "แอปที่เชื่อมต่อ", + "mcp.apps_configured": "apps configured", "mcp.apps_subtitle": "เชื่อมต่อเครื่องมือที่คุณชื่นชอบเพื่อให้ OpenWork ใช้งานแทนคุณ", "mcp.apps_title": "แอป", "mcp.auth.already_connected": "เชื่อมต่อแล้ว", @@ -740,6 +742,7 @@ export default { "mcp.friendly_status_offline": "ออฟไลน์", "mcp.friendly_status_paused": "หยุดชั่วคราว", "mcp.friendly_status_ready": "พร้อม", + "mcp.inherited_app": "Global app", "mcp.last_synced": "ซิงค์แล้ว", "mcp.login_action": "เข้าสู่ระบบ", "mcp.login_hint": "เชื่อมต่อบัญชีของคุณเพื่อตั้งค่าแอปนี้ให้เสร็จ", @@ -763,6 +766,11 @@ export default { "mcp.opening_label": "กำลังเปิด...", "mcp.pick_workspace_error": "เลือกโฟลเดอร์พื้นที่ทำงานก่อน", "mcp.pick_workspace_first": "เลือกโฟลเดอร์พื้นที่ทำงานก่อน", + "mcp.pause_app": "Pause", + "mcp.pause_modal_message": "Pause {server}? OpenWork will keep the app in your config, but its tools will stop loading after OpenCode reloads.", + "mcp.pause_modal_title": "Pause app?", + "mcp.pause_success": "Paused {server}.", + "mcp.pause_working": "Pausing...", "mcp.quick_connect_chrome_desc": "ควบคุมแท็บ Chrome ด้วย browser automation", "mcp.quick_connect_chrome_title": "ควบคุม Chrome", "mcp.quick_connect_context7_desc": "ค้นหาเอกสารผลิตภัณฑ์ด้วยบริบทที่สมบูรณ์ยิ่งขึ้น", @@ -779,14 +787,18 @@ export default { "mcp.reload_banner_description": "แตะ เปิดใช้งาน เพื่อเชื่อมต่อแอปให้เสร็จ", "mcp.reload_banner_description_blocked": "มีงานกำลังทำงานอยู่ หยุดก่อน แล้วเปิดใช้งาน", "mcp.remote_workspace_url_hint": "Remote workers เชื่อมต่อได้เร็วที่สุดกับ MCP server แบบ URL", + "mcp.read_only": "MCP config is read-only.", "mcp.remove_app": "ลบ", "mcp.remove_failed": "ลบแอปไม่สำเร็จ", "mcp.remove_modal_message": "คุณแน่ใจหรือไม่ว่าต้องการลบ {server}? คุณสามารถเพิ่มกลับได้ในภายหลัง", "mcp.remove_modal_title": "ลบแอป", "mcp.reveal_config_failed": "ไม่สามารถเปิดไฟล์ config", "mcp.reveal_in_finder": "เปิดในตัวจัดการไฟล์", + "mcp.resume_app": "Resume", + "mcp.resume_success": "Resumed {server}.", "mcp.scope_global": "ทุกพื้นที่ทำงาน", "mcp.scope_project": "พื้นที่ทำงานนี้", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "คำสั่ง", "mcp.server_command_hint": "คำสั่ง shell สำหรับเริ่มเซิร์ฟเวอร์", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -795,6 +807,7 @@ export default { "mcp.server_type": "ประเภท", "mcp.server_url": "URL ของเซิร์ฟเวอร์", "mcp.server_url_placeholder": "https://api.githubcopilot.com/mcp/", + "mcp.toggle_failed": "Couldn't update the app.", "mcp.sign_in_section_label": "การเข้าสู่ระบบ", "mcp.tap_to_connect": "แตะเพื่อเชื่อมต่อ", "mcp.technical_details": "รายละเอียดทางเทคนิค", diff --git a/apps/app/src/i18n/locales/vi.ts b/apps/app/src/i18n/locales/vi.ts index 1076b9ebc..9c3854986 100644 --- a/apps/app/src/i18n/locales/vi.ts +++ b/apps/app/src/i18n/locales/vi.ts @@ -459,8 +459,8 @@ export default { "den.worker_not_ready_title": "Worker này chưa sẵn sàng.", "den.worker_provider_label": "Worker {provider}", "den.worker_secondary_cloud": "Worker Cloud", - "extensions.app_count_one": "{count} ứng dụng đã kết nối", - "extensions.app_count_many": "{count} ứng dụng đã kết nối", + "extensions.app_count_one": "{count} ứng dụng đã cấu hình", + "extensions.app_count_many": "{count} ứng dụng đã cấu hình", "extensions.apps_mcp_header": "Ứng dụng (MCP)", "extensions.filter_all": "Tất cả", "extensions.filter_apps": "Ứng dụng", @@ -637,7 +637,9 @@ export default { "mcp.advanced_settings": "Cài đặt nâng cao", "mcp.advanced_settings_hint": "Chỉnh sửa tệp cấu hình và quản lý kết nối thủ công.", "mcp.app_connected": "ứng dụng đã kết nối", + "mcp.app_configured": "app configured", "mcp.apps_connected": "ứng dụng đã kết nối", + "mcp.apps_configured": "apps configured", "mcp.apps_subtitle": "Kết nối các công cụ yêu thích để OpenWork có thể sử dụng thay bạn.", "mcp.apps_title": "Ứng dụng", "mcp.auth.already_connected": "Đã kết nối", @@ -740,6 +742,7 @@ export default { "mcp.friendly_status_offline": "Ngoại tuyến", "mcp.friendly_status_paused": "Tạm dừng", "mcp.friendly_status_ready": "Sẵn sàng", + "mcp.inherited_app": "Global app", "mcp.last_synced": "Đã đồng bộ", "mcp.login_action": "Đăng nhập", "mcp.login_hint": "Kết nối tài khoản để hoàn tất thiết lập ứng dụng.", @@ -763,6 +766,11 @@ export default { "mcp.opening_label": "Đang mở...", "mcp.pick_workspace_error": "Vui lòng chọn thư mục workspace trước.", "mcp.pick_workspace_first": "Vui lòng chọn thư mục workspace trước.", + "mcp.pause_app": "Pause", + "mcp.pause_modal_message": "Pause {server}? OpenWork will keep the app in your config, but its tools will stop loading after OpenCode reloads.", + "mcp.pause_modal_title": "Pause app?", + "mcp.pause_success": "Paused {server}.", + "mcp.pause_working": "Pausing...", "mcp.quick_connect_chrome_desc": "Điều khiển tab Chrome với browser automation.", "mcp.quick_connect_chrome_title": "Điều khiển Chrome", "mcp.quick_connect_context7_desc": "Tìm kiếm tài liệu sản phẩm với ngữ cảnh phong phú hơn.", @@ -779,14 +787,18 @@ export default { "mcp.reload_banner_description": "Nhấn Kích hoạt để hoàn tất kết nối ứng dụng.", "mcp.reload_banner_description_blocked": "Một task đang chạy. Dừng trước rồi kích hoạt.", "mcp.remote_workspace_url_hint": "Workspace từ xa kết nối nhanh nhất với MCP server dạng URL.", + "mcp.read_only": "MCP config is read-only.", "mcp.remove_app": "Xóa", "mcp.remove_failed": "Không thể xóa ứng dụng.", "mcp.remove_modal_message": "Bạn có chắc muốn xóa {server}? Bạn luôn có thể thêm lại sau.", "mcp.remove_modal_title": "Xóa ứng dụng", "mcp.reveal_config_failed": "Không thể mở tệp cấu hình", "mcp.reveal_in_finder": "Hiện trong Finder", + "mcp.resume_app": "Resume", + "mcp.resume_success": "Resumed {server}.", "mcp.scope_global": "Tất cả workspaces", "mcp.scope_project": "Workspace này", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "Lệnh", "mcp.server_command_hint": "Lệnh shell để khởi động server.", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -795,6 +807,7 @@ export default { "mcp.server_type": "Loại", "mcp.server_url": "URL máy chủ", "mcp.server_url_placeholder": "https://api.githubcopilot.com/mcp/", + "mcp.toggle_failed": "Couldn't update the app.", "mcp.sign_in_section_label": "Đăng nhập", "mcp.tap_to_connect": "Nhấn để kết nối", "mcp.technical_details": "Chi tiết kỹ thuật", diff --git a/apps/app/src/i18n/locales/zh.ts b/apps/app/src/i18n/locales/zh.ts index 3418fb564..a21216b1a 100644 --- a/apps/app/src/i18n/locales/zh.ts +++ b/apps/app/src/i18n/locales/zh.ts @@ -462,8 +462,8 @@ export default { "den.worker_not_ready_title": "此工作区尚未准备就绪。", "den.worker_provider_label": "{provider}工作区", "den.worker_secondary_cloud": "云端工作区", - "extensions.app_count_one": "{count}个应用已连接", - "extensions.app_count_many": "{count}个应用已连接", + "extensions.app_count_one": "{count}个应用已配置", + "extensions.app_count_many": "{count}个应用已配置", "extensions.apps_mcp_header": "应用(MCP)", "extensions.filter_all": "全部", "extensions.filter_apps": "应用", @@ -640,7 +640,9 @@ export default { "mcp.advanced_settings": "高级设置", "mcp.advanced_settings_hint": "编辑配置文件和手动管理连接。", "mcp.app_connected": "个应用已连接", + "mcp.app_configured": "个应用已配置", "mcp.apps_connected": "个应用已连接", + "mcp.apps_configured": "个应用已配置", "mcp.apps_subtitle": "连接常用工具,让OpenWork代你使用。", "mcp.apps_title": "应用", "mcp.auth.already_connected": "已连接", @@ -743,6 +745,7 @@ export default { "mcp.friendly_status_offline": "离线", "mcp.friendly_status_paused": "已暂停", "mcp.friendly_status_ready": "就绪", + "mcp.inherited_app": "全局应用", "mcp.last_synced": "已同步", "mcp.login_action": "登录", "mcp.login_hint": "连接你的账户以完成此应用的设置。", @@ -766,6 +769,11 @@ export default { "mcp.opening_label": "正在打开", "mcp.pick_workspace_error": "请先选择工作区文件夹。", "mcp.pick_workspace_first": "请先选择工作区文件夹。", + "mcp.pause_app": "暂停", + "mcp.pause_modal_message": "暂停{server}?OpenWork会保留此应用配置,但OpenCode重新加载后不再加载它的工具。", + "mcp.pause_modal_title": "暂停应用?", + "mcp.pause_success": "已暂停{server}。", + "mcp.pause_working": "正在暂停...", "mcp.quick_connect_chrome_desc": "通过浏览器自动化操控Chrome标签页。", "mcp.quick_connect_chrome_title": "控制Chrome", "mcp.quick_connect_context7_desc": "以更丰富的上下文搜索产品文档。", @@ -782,14 +790,18 @@ export default { "mcp.reload_banner_description": "点击激活以完成应用连接。", "mcp.reload_banner_description_blocked": "任务正在运行。请先停止任务再激活。", "mcp.remote_workspace_url_hint": "远程工作区建议优先使用URL类型的MCP服务器。", + "mcp.read_only": "MCP配置为只读。", "mcp.remove_app": "移除", "mcp.remove_failed": "无法移除应用。", "mcp.remove_modal_message": "确定要移除{server}吗?你可以随时重新添加。", "mcp.remove_modal_title": "移除应用", "mcp.reveal_config_failed": "无法打开配置文件", "mcp.reveal_in_finder": "在文件管理器中显示", + "mcp.resume_app": "恢复", + "mcp.resume_success": "已恢复{server}。", "mcp.scope_global": "所有工作区", "mcp.scope_project": "此工作区", + "mcp.server_unavailable_read_only": "OpenWork server is unavailable. MCP config is read-only.", "mcp.server_command": "命令", "mcp.server_command_hint": "启动服务器的shell命令。", "mcp.server_command_placeholder": "npx -y @modelcontextprotocol/server-sequential-thinking", @@ -801,6 +813,7 @@ export default { "mcp.sign_in_section_label": "登录", "mcp.tap_to_connect": "点击连接", "mcp.technical_details": "技术详情", + "mcp.toggle_failed": "无法更新应用。", "mcp.type_cloud": "Cloud(使用账户登录)", "mcp.type_local": "本地(在此设备运行)", "mcp.type_local_cmd": "本地(命令)", diff --git a/apps/app/src/react-app/domains/connections/mcp-view.tsx b/apps/app/src/react-app/domains/connections/mcp-view.tsx index 04888c973..17b78bdb3 100644 --- a/apps/app/src/react-app/domains/connections/mcp-view.tsx +++ b/apps/app/src/react-app/domains/connections/mcp-view.tsx @@ -19,6 +19,8 @@ export type ConnectionsMcpStore = { authorizeMcp: (entry: McpServerEntry) => void; logoutMcpAuth: (name: string) => Promise | void; removeMcp: (name: string) => void; + setMcpEnabled: (name: string, enabled: boolean) => Promise | void; + canManageMcp?: boolean; }; export type ConnectionsMcpViewProps = { @@ -51,6 +53,8 @@ export default function ConnectionsMcpView(props: ConnectionsMcpViewProps) { authorizeMcp={connections.authorizeMcp} logoutMcpAuth={connections.logoutMcpAuth} removeMcp={connections.removeMcp} + setMcpEnabled={connections.setMcpEnabled} + canManageMcp={connections.canManageMcp} /> ); } diff --git a/apps/app/src/react-app/domains/connections/store.ts b/apps/app/src/react-app/domains/connections/store.ts index 0629be39d..cd603e939 100644 --- a/apps/app/src/react-app/domains/connections/store.ts +++ b/apps/app/src/react-app/domains/connections/store.ts @@ -19,7 +19,9 @@ import { import { toSessionTransportDirectory } from "../../../app/lib/session-scope"; import { parseMcpServersFromContent, + parseEffectiveMcpServersFromContent, removeMcpFromConfig, + setMcpEnabledInConfig, usesChromeDevtoolsAutoConnect, validateMcpServerName, } from "../../../app/mcp"; @@ -72,6 +74,7 @@ export function createConnectionsStore(options: { let started = false; let disposed = false; + let activeMcpToggleKey = ""; let lastWorkspaceContextKey = ""; let lastProjectDir = ""; let snapshot: ConnectionsStoreSnapshot; @@ -137,6 +140,24 @@ export function createConnectionsStore(options: { ) as McpStatusMap; }; + const applyServerMcpItems = ( + items: Array<{ name: string; config: Record; source?: McpServerEntry["source"]; inherited?: boolean }>, + ) => { + const next = items.map((entry) => ({ + name: entry.name, + config: entry.config as McpServerEntry["config"], + source: entry.source, + inherited: entry.inherited, + })); + mutateState((current) => ({ + ...current, + mcpServers: next, + mcpLastUpdatedAt: Date.now(), + mcpStatuses: filterConfiguredStatuses(current.mcpStatuses, next), + mcpStatus: next.length ? current.mcpStatus : "No MCP servers configured yet.", + })); + }; + const readMcpConfigFile = async (scope: "project" | "global"): Promise => { const projectDir = options.projectDir().trim(); const openworkSnapshot = getOpenworkSnapshot(); @@ -351,7 +372,9 @@ export function createConnectionsStore(options: { try { setStateField("mcpStatus", null); const config = await readOpencodeConfig("project", projectDir); - if (!config.exists || !config.content) { + const globalConfig = await readOpencodeConfig("global", projectDir); + const next = parseEffectiveMcpServersFromContent(config.content ?? "", globalConfig.content ?? ""); + if ((!config.exists || !config.content) && !next.length) { mutateState((current) => ({ ...current, mcpServers: [], @@ -361,7 +384,6 @@ export function createConnectionsStore(options: { return; } - const next = parseMcpServersFromContent(config.content); let nextStatuses = state.mcpStatuses; const activeClient = options.client(); if (activeClient) { @@ -770,34 +792,116 @@ export function createConnectionsStore(options: { } } - // Server-only path. Local fallback would rewrite opencode.jsonc whole and - // clobber inline comments — settings-route.tsx already gates the prop so - // this never gets called when the server is unavailable. Reload UX comes - // from the existing reload-required popup; no extra banner here. async function setMcpEnabled(name: string, enabled: boolean) { + const safeName = validateMcpServerName(name); + const operationWorkspaceContextKey = getWorkspaceContextKey(); + const operationToggleKey = `${operationWorkspaceContextKey}:${safeName}`; + const openworkSnapshot = getOpenworkSnapshot(); + const serverConnected = openworkSnapshot.openworkServerStatus === "connected"; + const serverReadOnly = + serverConnected && + openworkSnapshot.openworkServerCapabilities != null && + !openworkSnapshot.openworkServerCapabilities.mcp?.write; + const isRemoteWorkspace = + options.workspaceType() === "remote" || + (!isDesktopRuntime() && serverConnected); + try { - const openworkSnapshot = getOpenworkSnapshot(); - const openworkClient = openworkSnapshot.openworkServerClient; - const openworkWorkspaceId = options.runtimeWorkspaceId(); - const canUseOpenworkServer = - openworkSnapshot.openworkServerStatus === "connected" && - openworkClient && - openworkWorkspaceId && - openworkSnapshot.openworkServerCapabilities?.mcp?.write; + setStateField("mcpStatus", null); + setStateField("mcpConnectingName", safeName); + activeMcpToggleKey = operationToggleKey; + + const { openworkClient, openworkWorkspaceId, canUseOpenworkServer } = + await resolveWritableOpenworkTarget(); - if (!canUseOpenworkServer || !openworkClient || !openworkWorkspaceId) { - setStateField("mcpStatus", translate("mcp.toggle_requires_server")); + if (serverReadOnly) { + setStateField("mcpStatus", translate("mcp.read_only")); return; } - await openworkClient.setMcpEnabled(openworkWorkspaceId, name, enabled); - options.markReloadRequired?.("mcp", { type: "mcp", name, action: "updated" }); - await refreshMcpServers(); + if (isRemoteWorkspace && !canUseOpenworkServer) { + setStateField("mcpStatus", translate("mcp.server_unavailable_read_only")); + return; + } + + let serverItems: Array<{ + name: string; + config: Record; + source?: McpServerEntry["source"]; + inherited?: boolean; + }> | null = null; + let changed = true; + + if (canUseOpenworkServer && openworkClient && openworkWorkspaceId) { + const response = await openworkClient.setMcpEnabled(openworkWorkspaceId, safeName, enabled); + if (operationWorkspaceContextKey !== getWorkspaceContextKey()) return; + serverItems = response.items; + changed = response.changed; + } else { + if (!isDesktopRuntime()) { + setStateField("mcpStatus", translate("mcp.desktop_required")); + return; + } + const projectDir = options.projectDir().trim(); + if (!projectDir) { + setStateField("mcpStatus", translate("mcp.pick_workspace_first")); + return; + } + const response = await setMcpEnabledInConfig(projectDir, safeName, enabled); + if (operationWorkspaceContextKey !== getWorkspaceContextKey()) return; + changed = response.changed; + } + + if (!changed) { + if (serverItems) { + applyServerMcpItems(serverItems); + } + setStateField( + "mcpStatus", + translate(enabled ? "mcp.resume_success" : "mcp.pause_success").replace("{server}", safeName), + ); + return; + } + + const activeClient = options.client(); + const resolvedProjectDir = activeClient + ? await resolveProjectDir(activeClient, options.projectDir().trim()) + : ""; + if (activeClient && resolvedProjectDir) { + try { + if (enabled) { + await activeClient.mcp.connect({ directory: resolvedProjectDir, name: safeName }); + } else { + await activeClient.mcp.disconnect({ directory: resolvedProjectDir, name: safeName }); + } + const status = unwrap(await activeClient.mcp.status({ directory: resolvedProjectDir })); + if (operationWorkspaceContextKey !== getWorkspaceContextKey()) return; + setStateField("mcpStatuses", status as McpStatusMap); + } catch { + // Persisted config is the source of truth; runtime changes are best-effort. + } + } + + options.markReloadRequired?.("mcp", { type: "mcp", name: safeName, action: "updated" }); + if (serverItems) { + applyServerMcpItems(serverItems); + } else { + await refreshMcpServers(); + } + setStateField( + "mcpStatus", + translate(enabled ? "mcp.resume_success" : "mcp.pause_success").replace("{server}", safeName), + ); } catch (error) { setStateField( "mcpStatus", error instanceof Error ? error.message : translate("mcp.toggle_failed"), ); + } finally { + if (activeMcpToggleKey === operationToggleKey) { + activeMcpToggleKey = ""; + setStateField("mcpConnectingName", null); + } } } @@ -828,6 +932,8 @@ export function createConnectionsStore(options: { return; } + activeMcpToggleKey = ""; + setStateField("mcpConnectingName", null); void refreshMcpServers(); }; diff --git a/apps/app/src/react-app/domains/settings/pages/extensions-view.tsx b/apps/app/src/react-app/domains/settings/pages/extensions-view.tsx index 51bc3d54c..f11619f01 100644 --- a/apps/app/src/react-app/domains/settings/pages/extensions-view.tsx +++ b/apps/app/src/react-app/domains/settings/pages/extensions-view.tsx @@ -26,10 +26,8 @@ type SuggestedPlugin = { }>; }; -// The Solid ExtensionsView pulled the MCP-connected count from -// useConnections(). In React we let the parent pass that plus an -// already-rendered MCP view so we can ship this page before the full -// connections provider is ported. +// The Solid ExtensionsView pulled the MCP app count from useConnections(). +// In React we let the parent pass that plus an already-rendered MCP view. export type ExtensionsViewProps = { busy: boolean; selectedWorkspaceRoot: string; @@ -39,7 +37,7 @@ export type ExtensionsViewProps = { accessHint?: string | null; suggestedPlugins: SuggestedPlugin[]; extensions: PluginsExtensionsStore; - mcpConnectedAppsCount: number; + mcpConfiguredAppsCount: number; mcpView: ReactNode; onRefresh: () => void; initialSection?: ExtensionsSection; @@ -95,16 +93,16 @@ export function ExtensionsView(props: ExtensionsViewProps) {
- {props.mcpConnectedAppsCount > 0 ? ( -
-
- + {props.mcpConfiguredAppsCount > 0 ? ( +
+
+ {t( - props.mcpConnectedAppsCount === 1 + props.mcpConfiguredAppsCount === 1 ? "extensions.app_count_one" : "extensions.app_count_many", undefined, - { count: props.mcpConnectedAppsCount }, + { count: props.mcpConfiguredAppsCount }, )}
diff --git a/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx b/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx index df357832d..4385cc22a 100644 --- a/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx +++ b/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx @@ -12,9 +12,10 @@ import { Globe, Loader2, MonitorSmartphone, + Pause, Plug2, + Play, Plus, - Power, Settings, Settings2, Unplug, @@ -69,7 +70,8 @@ export type McpViewProps = { authorizeMcp: (entry: McpServerEntry) => void; logoutMcpAuth: (name: string) => Promise | void; removeMcp: (name: string) => void; - setMcpEnabled?: (name: string, enabled: boolean) => Promise | void; + setMcpEnabled: (name: string, enabled: boolean) => Promise | void; + canManageMcp?: boolean; }; const statusDot = (status: ReactMcpStatus) => { @@ -119,6 +121,36 @@ const statusBadgeStyle = (status: ReactMcpStatus) => { } }; +const statusPillStyle = (status: ReactMcpStatus) => { + switch (status) { + case "connected": + return "border-green-6 bg-green-3 text-green-11"; + case "needs_auth": + case "needs_client_registration": + return "border-amber-6 bg-amber-3 text-amber-11"; + case "disabled": + case "disconnected": + return "border-dls-border bg-dls-hover text-dls-secondary"; + default: + return "border-red-6 bg-red-3 text-red-11"; + } +}; + +const StatusPill = ({ + status, + locale, +}: { + status: ReactMcpStatus; + locale: Language; +}) => ( + + + {friendlyStatus(status, locale)} + +); + const serviceIcon = (name: string) => { const lower = name.toLowerCase(); if (lower.includes("notion")) return BookOpen; @@ -168,6 +200,9 @@ export function McpView(props: McpViewProps) { const [logoutBusy, setLogoutBusy] = useState(false); const [removeOpen, setRemoveOpen] = useState(false); const [removeTarget, setRemoveTarget] = useState(null); + const [pauseOpen, setPauseOpen] = useState(false); + const [pauseTarget, setPauseTarget] = useState(null); + const [pauseBusy, setPauseBusy] = useState(false); const [configScope, setConfigScope] = useState<"project" | "global">("project"); const [projectConfig, setProjectConfig] = useState(null); const [globalConfig, setGlobalConfig] = useState(null); @@ -175,13 +210,13 @@ export function McpView(props: McpViewProps) { const [revealBusy, setRevealBusy] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [addMcpModalOpen, setAddMcpModalOpen] = useState(false); - const [togglingMcp, setTogglingMcp] = useState(null); const [controlChromeModalOpen, setControlChromeModalOpen] = useState(false); const [controlChromeModalMode, setControlChromeModalMode] = useState<"connect" | "edit">("connect"); const [controlChromeExistingProfile, setControlChromeExistingProfile] = useState(false); const configRequestId = useRef(0); const quickConnectList = props.quickConnect; + const canManageMcp = props.canManageMcp !== false; useEffect(() => { const root = props.selectedWorkspaceRoot.trim(); @@ -248,8 +283,8 @@ export function McpView(props: McpViewProps) { const quickConnectStatus = (entry: McpDirectoryInfo) => props.mcpStatuses[getMcpIdentityKey(entry)]; - const isQuickConnectConfigured = (entry: McpDirectoryInfo) => - props.mcpServers.some((server) => server.name === getMcpIdentityKey(entry)); + const configuredMcpEntry = (entry: McpDirectoryInfo) => + props.mcpServers.find((server) => server.name === getMcpIdentityKey(entry)); const openControlChromeModal = ( mode: "connect" | "edit", @@ -286,9 +321,9 @@ export function McpView(props: McpViewProps) { return resolved?.status ?? "disconnected"; }; - const connectedCount = props.mcpServers.filter( - (entry) => resolveStatus(entry) === "connected", - ).length; + const configuredCount = props.mcpServers.length; + const connectedCount = props.mcpServers.filter((entry) => resolveStatus(entry) === "connected").length; + const pausedCount = props.mcpServers.filter((entry) => entry.config.enabled === false).length; const requestLogout = (name: string) => { if (!name.trim()) return; @@ -309,6 +344,19 @@ export function McpView(props: McpViewProps) { } }; + const confirmPause = async () => { + const name = pauseTarget; + if (!name || pauseBusy) return; + setPauseBusy(true); + try { + await props.setMcpEnabled(name, false); + } finally { + setPauseBusy(false); + setPauseOpen(false); + setPauseTarget(null); + } + }; + const revealConfig = async () => { if (!isDesktopRuntime() || revealBusy) return; const root = props.selectedWorkspaceRoot.trim(); @@ -347,12 +395,32 @@ export function McpView(props: McpViewProps) {

{tr("mcp.apps_title")}

{tr("mcp.apps_subtitle")}

- {connectedCount > 0 ? ( -
-
- - {connectedCount} {connectedCount === 1 ? tr("mcp.app_connected") : tr("mcp.apps_connected")} - + {configuredCount > 0 ? ( +
+ {connectedCount > 0 ? ( +
+
+ + {connectedCount} {connectedCount === 1 ? tr("mcp.app_connected") : tr("mcp.apps_connected")} + +
+ ) : null} + {pausedCount > 0 ? ( +
+
+ + {pausedCount} {tr("mcp.friendly_status_paused")} + +
+ ) : null} + {connectedCount === 0 && pausedCount === 0 ? ( +
+
+ + {configuredCount} {configuredCount === 1 ? tr("mcp.app_configured") : tr("mcp.apps_configured")} + +
+ ) : null}
) : null}
@@ -364,13 +432,18 @@ export function McpView(props: McpViewProps) {
) : null} -
+
-
-
{tr("mcp.add_modal_title")}
-
{tr("mcp.custom_app_cta_hint")}
+
+
+ +
+
+
{tr("mcp.add_modal_title")}
+
{tr("mcp.custom_app_cta_hint")}
+
- @@ -387,11 +460,19 @@ export function McpView(props: McpViewProps) {
{quickConnectList.map((entry) => { - const configured = isQuickConnectConfigured(entry); + const configuredEntry = configuredMcpEntry(entry); + const configured = Boolean(configuredEntry); + const configuredStatus = configuredEntry ? resolveStatus(configuredEntry) : undefined; const connecting = props.mcpConnectingName === entry.name; const Icon = serviceIcon(entry.name); const controlChrome = isChromeDevtoolsMcp(entry); const quickStatus = !configured ? quickConnectStatus(entry) : undefined; + const configuredCardClass = + configuredStatus === "connected" + ? "border-green-6 bg-green-2" + : configuredStatus === "disabled" + ? "border-gray-6 bg-gray-2" + : "border-dls-border bg-dls-surface"; return (
@@ -425,20 +506,26 @@ export function McpView(props: McpViewProps) { }} className={`group w-full rounded-xl border p-4 text-left transition-all ${ configured - ? "border-green-6 bg-green-2" - : "border-dls-border bg-dls-surface hover:bg-dls-hover hover:shadow-[0_4px_16px_rgba(17,24,39,0.06)]" + ? configuredCardClass + : "border-dls-border bg-dls-surface hover:border-gray-7 hover:bg-dls-hover" }`} >
{connecting ? ( - ) : configured ? ( + ) : configuredStatus === "connected" ? ( + ) : configuredStatus === "disabled" ? ( + ) : ( )} @@ -447,9 +534,9 @@ export function McpView(props: McpViewProps) {

{entry.name}

- {configured ? ( - - {tr("mcp.connected_badge")} + {configured && configuredStatus ? ( + + {friendlyStatus(configuredStatus, locale)} ) : null} {!configured && quickStatus ? ( @@ -497,7 +584,9 @@ export function McpView(props: McpViewProps) { const status = resolveStatus(entry); const Icon = serviceIcon(entry.name); const isSelected = props.selectedMcp === entry.name; + const isInherited = entry.inherited === true; const resolvedStatus = props.mcpStatuses[entry.name]; + const toggleBusy = props.mcpConnectingName === entry.name; const errorInfo = resolvedStatus && resolvedStatus.status === "failed" ? "error" in resolvedStatus @@ -508,15 +597,18 @@ export function McpView(props: McpViewProps) { return (
+ {isSelected ? ( +
+ ) : null}
-
- - {friendlyStatus(status, locale)} - +
@@ -552,7 +641,7 @@ export function McpView(props: McpViewProps) { {isSelected ? ( -
+
{tr("mcp.connection_type")} @@ -569,6 +658,11 @@ export function McpView(props: McpViewProps) { {tr("mcp.cap_signin")} ) : null} + {isInherited ? ( + + {tr("mcp.inherited_app")} + + ) : null}
{errorInfo ? ( @@ -590,7 +684,9 @@ export function McpView(props: McpViewProps) {
- {supportsOauth(entry) && status !== "connected" ? ( + {supportsOauth(entry) && + status !== "connected" && + status !== "disabled" ? ( <>
{tr("mcp.logout_label")}
@@ -637,38 +733,56 @@ export function McpView(props: McpViewProps) { {tr("mcp.control_chrome_edit")} ) : null} - {props.setMcpEnabled && entry.source !== "config.global" ? ( + {status === "disabled" ? ( + + ) : ( + )} + {!isInherited ? ( + ) : null} -
) : null} @@ -720,6 +834,23 @@ export function McpView(props: McpViewProps) { }} /> + { + if (pauseBusy) return; + setPauseOpen(false); + setPauseTarget(null); + }} + onConfirm={() => { + void confirmPause(); + }} + /> +