diff --git a/packages/server/src/config/jsonc.ts b/packages/server/src/config/jsonc.ts new file mode 100644 index 000000000..f8c720aa2 --- /dev/null +++ b/packages/server/src/config/jsonc.ts @@ -0,0 +1,60 @@ +export function stripJsonc(input: string): string { + let output = "" + let inString = false + let escape = false + + for (let index = 0; index < input.length; index += 1) { + const char = input[index] + const next = input[index + 1] as string | undefined + + if (escape) { + output += char + escape = false + continue + } + + if (char === "\\" && inString) { + output += char + escape = true + continue + } + + if (char === '"') { + output += char + inString = !inString + continue + } + + if (!inString && char === "/" && next === "/") { + while (index < input.length && input[index] !== "\n") { + index += 1 + } + output += "\n" + continue + } + + if (!inString && char === "/" && next === "*") { + index += 2 + while (index < input.length && !(input[index] === "*" && input[index + 1] === "/")) { + output += input[index] === "\n" ? "\n" : "" + index += 1 + } + index += 1 + continue + } + + if (!inString && char === ",") { + let lookahead = index + 1 + while (lookahead < input.length && /\s/.test(input[lookahead]!)) { + lookahead += 1 + } + if (input[lookahead] === "}" || input[lookahead] === "]") { + continue + } + } + + output += char + } + + return output +} diff --git a/packages/server/src/opencode-plugin.ts b/packages/server/src/opencode-plugin.ts index 761292da5..843020df6 100644 --- a/packages/server/src/opencode-plugin.ts +++ b/packages/server/src/opencode-plugin.ts @@ -2,6 +2,7 @@ import { existsSync, readdirSync } from "fs" import path from "path" import { fileURLToPath, pathToFileURL } from "url" import { createLogger } from "./logger" +import { stripJsonc } from "./config/jsonc" const log = createLogger({ component: "opencode-plugin" }) const pluginPackageName = "@codenomad/codenomad-opencode-plugin" @@ -112,64 +113,3 @@ function normalizePluginEntries(value: unknown): string[] { } throw new Error("OPENCODE_CONFIG_CONTENT plugin field must be a string or string array") } - -function stripJsonc(input: string): string { - let output = "" - let inString = false - let escape = false - - for (let index = 0; index < input.length; index += 1) { - const char = input[index] - const next = input[index + 1] - - if (escape) { - output += char - escape = false - continue - } - - if (char === "\\" && inString) { - output += char - escape = true - continue - } - - if (char === '"') { - output += char - inString = !inString - continue - } - - if (!inString && char === "/" && next === "/") { - while (index < input.length && input[index] !== "\n") { - index += 1 - } - output += "\n" - continue - } - - if (!inString && char === "/" && next === "*") { - index += 2 - while (index < input.length && !(input[index] === "*" && input[index + 1] === "/")) { - output += input[index] === "\n" ? "\n" : "" - index += 1 - } - index += 1 - continue - } - - if (!inString && char === ",") { - let lookahead = index + 1 - while (lookahead < input.length && /\s/.test(input[lookahead])) { - lookahead += 1 - } - if (input[lookahead] === "}" || input[lookahead] === "]") { - continue - } - } - - output += char - } - - return output -} diff --git a/packages/server/src/server/routes/settings.ts b/packages/server/src/server/routes/settings.ts index 57de95458..f1f1f4885 100644 --- a/packages/server/src/server/routes/settings.ts +++ b/packages/server/src/server/routes/settings.ts @@ -1,9 +1,14 @@ import { FastifyInstance } from "fastify" import { z } from "zod" +import { readFile, writeFile } from "fs/promises" +import { existsSync } from "fs" +import path from "path" +import os from "os" import { probeBinaryVersion } from "../../workspaces/spawn" import type { SettingsService } from "../../settings/service" import type { Logger } from "../../logger" import { sanitizeConfigDoc, sanitizeConfigOwner } from "../../settings/public-config" +import { stripJsonc } from "../../config/jsonc" interface RouteDeps { settings: SettingsService @@ -14,6 +19,11 @@ const ValidateBinarySchema = z.object({ path: z.string(), }) +const PluginEntrySchema = z.string().min(1) +const UpdatePluginConfigSchema = z.object({ + plugins: z.array(PluginEntrySchema), +}) + function validateBinaryPath(binaryPath: string): { valid: boolean; version?: string; error?: string } { const result = probeBinaryVersion(binaryPath) return { valid: result.valid, version: result.version, error: result.error } @@ -49,6 +59,95 @@ export function enforceSpeechCredentialPairing(body: unknown, currentSpeech?: un return patch } +function safeJsonParse(raw: string): Record | null { + try { + const cleaned = stripJsonc(raw).replace(/,\s*}/g, "}").replace(/,\s*\]/g, "]") + const parsed = JSON.parse(cleaned) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as Record + } + return null + } catch { + return null + } +} + +type PluginEntry = { spec: string; enabled: boolean } + +function normalizePluginEntry(entry: unknown): PluginEntry | null { + if (typeof entry === "string" && entry.trim().length > 0) { + return { spec: entry.trim(), enabled: true } + } + if (Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && entry[0].trim().length > 0) { + const opts = entry[1] as Record | null | undefined + const enabled = opts && typeof opts === "object" && "enabled" in opts ? opts.enabled !== false : true + return { spec: entry[0].trim(), enabled } + } + return null +} + +function readPluginsFromConfig(config: Record): PluginEntry[] { + const raw = config.plugin + if (!Array.isArray(raw)) return [] + return raw.map(normalizePluginEntry).filter((entry): entry is PluginEntry => entry !== null) +} + +function getGlobalConfigPath(): string { + return path.join(os.homedir(), ".config", "opencode", "opencode.jsonc") +} + +async function readGlobalConfigPlugins(): Promise { + const configPath = getGlobalConfigPath() + if (!existsSync(configPath)) return [] + try { + const raw = await readFile(configPath, "utf-8") + const config = safeJsonParse(raw) + if (!config) return [] + return readPluginsFromConfig(config) + } catch { + return [] + } +} + +async function writeGlobalConfigPlugins(entries: PluginEntry[]): Promise { + const configPath = getGlobalConfigPath() + let config: Record = {} + if (existsSync(configPath)) { + try { + const raw = await readFile(configPath, "utf-8") + config = safeJsonParse(raw) ?? {} + } catch { /* use empty config */ } + } + + const serialized: unknown[] = entries.map((entry) => { + if (entry.enabled) return entry.spec + return [entry.spec, { enabled: false }] + }) + + if (serialized.length === 0) { + delete config.plugin + } else { + config.plugin = serialized + } + + config["$schema"] = config["$schema"] ?? "https://opencode.ai/config.json" + const dir = path.dirname(configPath) + const { mkdir } = await import("fs/promises") + await mkdir(dir, { recursive: true }) + await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8") +} + +function isValidPluginSpec(spec: string): boolean { + return ( + spec.startsWith("npm:") || + spec.startsWith("file://") || + spec.startsWith("https://") || + spec.startsWith("http://") || + spec.startsWith("/") || + spec.startsWith(".") + ) +} + export function registerSettingsRoutes(app: FastifyInstance, deps: RouteDeps) { // Full-document access app.get("/api/storage/config", async () => sanitizeConfigDoc(deps.settings.getDoc("config"))) @@ -127,4 +226,53 @@ export function registerSettingsRoutes(app: FastifyInstance, deps: RouteDeps) { return { valid: false, error: error instanceof Error ? error.message : "Invalid request" } } }) + + // Plugin config management — reads/writes the global opencode.jsonc plugin array + app.get("/api/opencode-plugin-config", async () => { + try { + const plugins = await readGlobalConfigPlugins() + return { plugins: plugins.map((p) => ({ spec: p.spec, enabled: p.enabled })) } + } catch { + return { plugins: [] } + } + }) + + app.put("/api/opencode-plugin-config", async (request, reply) => { + try { + const body = UpdatePluginConfigSchema.parse(request.body ?? {}) + for (const spec of body.plugins) { + if (!isValidPluginSpec(spec.trim())) { + reply.code(400) + return { error: `Invalid plugin spec: "${spec}". Must start with npm:, file://, https://, /, or .` } + } + } + const entries: PluginEntry[] = body.plugins.map((spec) => ({ spec: spec.trim(), enabled: true })) + await writeGlobalConfigPlugins(entries) + + // Also update CodeNomad server config so workspaces pick up plugins on launch + const rawConfig = deps.settings.getOwner("config", "server") ?? {} + const existingEnvVars = + typeof rawConfig === "object" && !Array.isArray(rawConfig) && rawConfig !== null + ? (rawConfig as Record).environmentVariables + : undefined + const envVars = + typeof existingEnvVars === "object" && !Array.isArray(existingEnvVars) && existingEnvVars !== null + ? { ...(existingEnvVars as Record) } + : {} + envVars.OPENCODE_CONFIG_CONTENT = JSON.stringify({ plugin: entries.map((p) => p.spec) }) + if (entries.length === 0) { + delete envVars.OPENCODE_CONFIG_CONTENT + } + deps.settings.mergePatchOwner("config", "server", { environmentVariables: envVars }) + + return { plugins: entries.map((p) => ({ spec: p.spec, enabled: p.enabled })) } + } catch (error) { + if (error instanceof z.ZodError) { + reply.code(400) + return { error: error.issues.map((i) => i.message).join("; ") } + } + reply.code(400) + return { error: error instanceof Error ? error.message : "Invalid plugin config" } + } + }) } diff --git a/packages/ui/src/components/instance-service-status.tsx b/packages/ui/src/components/instance-service-status.tsx index 15e3d17b1..cf3afa38d 100644 --- a/packages/ui/src/components/instance-service-status.tsx +++ b/packages/ui/src/components/instance-service-status.tsx @@ -1,9 +1,11 @@ import { For, Show, createMemo, createSignal, type Component } from "solid-js" import Switch from "@suid/material/Switch" +import { Settings } from "lucide-solid" import type { Instance, RawMcpStatus } from "../types/instance" import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context" import { useI18n } from "../lib/i18n" import { getLogger } from "../lib/logger" +import { PluginManagerModal } from "./provider-auth/plugin-manager-modal" const log = getLogger("session") @@ -75,6 +77,7 @@ const InstanceServiceStatus: Component = (props) => const [pendingMcpActions, setPendingMcpActions] = createSignal>({}) + const [pluginsModalOpen, setPluginsModalOpen] = createSignal(false) const setPendingMcpAction = (name: string, action?: "connect" | "disconnect") => { setPendingMcpActions((prev) => { @@ -227,8 +230,30 @@ const InstanceServiceStatus: Component = (props) => const renderPluginsSection = () => (
-
- {t("instanceServiceStatus.sections.plugins")} +
+
+ {t("instanceServiceStatus.sections.plugins")} +
+ +
+ + +
+
= (props) => {renderLspSection()} {renderMcpSection()} {renderPluginsSection()} +
) } diff --git a/packages/ui/src/components/provider-auth/plugin-manager-modal.tsx b/packages/ui/src/components/provider-auth/plugin-manager-modal.tsx new file mode 100644 index 000000000..7a1cc2975 --- /dev/null +++ b/packages/ui/src/components/provider-auth/plugin-manager-modal.tsx @@ -0,0 +1,273 @@ +import { Dialog } from "@kobalte/core/dialog" +import { createEffect, createSignal, For, Show, type Component } from "solid-js" +import { AlertTriangle, Loader2, Package, Plus, RefreshCw, Trash2, X } from "lucide-solid" +import { useI18n } from "../../lib/i18n" +import { CODENOMAD_API_BASE } from "../../lib/api-client" + +interface PluginManagerModalProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +type PluginEntry = { + spec: string + enabled: boolean +} + +const API_CONFIG_URL = `${CODENOMAD_API_BASE.replace(/\/+$/, "")}/api/opencode-plugin-config` + +function isValidPluginSpec(spec: string): boolean { + const trimmed = spec.trim() + return ( + trimmed.startsWith("npm:") || + trimmed.startsWith("file://") || + trimmed.startsWith("https://") || + trimmed.startsWith("http://") || + trimmed.startsWith("/") || + trimmed.startsWith(".") + ) +} + +async function fetchJSON(url: string, init?: RequestInit): Promise { + const response = await fetch(url, init) + if (!response.ok) { + const body = await response.json().catch(() => ({})) + const message = (body as Record)?.error ?? `HTTP ${response.status}` + throw new Error(String(message)) + } + return response.json() +} + +export const PluginManagerModal: Component = (props) => { + const { t } = useI18n() + const [plugins, setPlugins] = createSignal([]) + const [loading, setLoading] = createSignal(false) + const [loadError, setLoadError] = createSignal(null) + const [actionError, setActionError] = createSignal(null) + const [validationError, setValidationError] = createSignal(null) + const [newPluginSpec, setNewPluginSpec] = createSignal("") + const [adding, setAdding] = createSignal(false) + const [removingIndex, setRemovingIndex] = createSignal(null) + const [confirmRemoveIndex, setConfirmRemoveIndex] = createSignal(null) + + createEffect(() => { + if (!props.open) return + void loadPluginData() + }) + + async function loadPluginData(): Promise { + setLoading(true) + setLoadError(null) + try { + const data = (await fetchJSON(API_CONFIG_URL)) as { plugins?: PluginEntry[] } + setPlugins(data.plugins ?? []) + } catch (error) { + setLoadError(error instanceof Error ? error.message : t("settings.plugins.errors.loadFailed")) + } finally { + setLoading(false) + } + } + + function validateSpec(spec: string): string | null { + const trimmed = spec.trim() + if (!trimmed) return null + if (!isValidPluginSpec(trimmed)) { + return t("settings.plugins.errors.invalidSpec") + } + return null + } + + function handleInputChange(value: string) { + setNewPluginSpec(value) + if (value.trim()) { + setValidationError(validateSpec(value)) + } else { + setValidationError(null) + } + } + + function canAdd(): boolean { + const spec = newPluginSpec().trim() + return spec.length > 0 && isValidPluginSpec(spec) && !adding() + } + + async function addPlugin(): Promise { + const spec = newPluginSpec().trim() + if (!spec || !isValidPluginSpec(spec)) return + setAdding(true) + setActionError(null) + setValidationError(null) + try { + const currentList = [...plugins()] + if (!currentList.some((p) => p.spec === spec)) { + currentList.push({ spec, enabled: true }) + } + await fetchJSON(API_CONFIG_URL, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ plugins: currentList.map((p) => p.spec) }), + }) + setNewPluginSpec("") + await loadPluginData() + } catch (error) { + setActionError(error instanceof Error ? error.message : t("settings.plugins.errors.addFailed")) + } finally { + setAdding(false) + } + } + + function requestRemovePlugin(index: number) { + setConfirmRemoveIndex(index) + } + + function cancelRemove() { + setConfirmRemoveIndex(null) + } + + async function confirmRemovePlugin(): Promise { + const index = confirmRemoveIndex() + if (index === null) return + setRemovingIndex(index) + setConfirmRemoveIndex(null) + setActionError(null) + try { + const currentList = [...plugins()] + currentList.splice(index, 1) + await fetchJSON(API_CONFIG_URL, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ plugins: currentList.map((p) => p.spec) }), + }) + await loadPluginData() + } catch (error) { + setActionError(error instanceof Error ? error.message : t("settings.plugins.errors.removeFailed")) + } finally { + setRemovingIndex(null) + } + } + + return ( + + + + +
+
+ +
+ {t("settings.plugins.title")} +

{t("settings.plugins.subtitle")}

+
+
+ +
+ +
+
+
+
+ handleInputChange(event.currentTarget.value)} + onKeyDown={(event) => { if (event.key === "Enter") void addPlugin() }} + placeholder={t("settings.plugins.addPlaceholder")} + disabled={adding()} + /> + +
+ +
+ + {validationError()} +
+
+
+ +
+ + +
{loadError()}
+
+ +
{actionError()}
+
+ +
+ {t("settings.plugins.restartNote")} +
+ +
+

{t("settings.plugins.configured.title")}

+ +
{t("settings.plugins.loading")}
+
+ +
{t("settings.plugins.empty.noPlugins")}
+
+
+ + {(plugin, index) => { + const isConfirming = () => confirmRemoveIndex() === index() + const isRemoving = () => removingIndex() === index() + return ( +
+
+
+
+

{plugin.spec}

+
+

+ {plugin.enabled ? t("settings.plugins.status.enabled") : t("settings.plugins.status.disabled")} +

+
+
+ +
+ ) + }} +
+
+
+
+
+
+
+ ) +} diff --git a/packages/ui/src/lib/i18n/messages/de/settings.ts b/packages/ui/src/lib/i18n/messages/de/settings.ts index e26f5e073..7366760e5 100644 --- a/packages/ui/src/lib/i18n/messages/de/settings.ts +++ b/packages/ui/src/lib/i18n/messages/de/settings.ts @@ -414,4 +414,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "In Zwischenablage kopieren", "settings.info.diagnostics.download": ".txt herunterladen", "settings.info.diagnostics.copied": "Diagnoseinfo in Zwischenablage kopiert.", + + "instanceServiceStatus.plugins.manage": "Verwalten", + + "settings.plugins.title": "Plugin-Manager", + "settings.plugins.subtitle": "OpenCode-Plugins für den aktiven Arbeitsbereich hinzufügen oder entfernen.", + "settings.plugins.refresh": "Aktualisieren", + "settings.plugins.loading": "Plugins werden geladen...", + "settings.plugins.addPlaceholder": "npm:plugin-name oder file:///pfad/zum/plugin.ts", + "settings.plugins.restartNote": "Plugin-Änderungen erfordern einen Neustart des Arbeitsbereichs.", + "settings.plugins.configured.title": "Konfigurierte Plugins", + "settings.plugins.empty.noPlugins": "Keine zusätzlichen Plugins konfiguriert.", + "settings.plugins.empty.noInstance": "Starten Sie einen OpenCode-Arbeitsbereich, bevor Sie Plugins verwalten.", + "settings.plugins.status.enabled": "Aktiviert", + "settings.plugins.status.disabled": "Deaktiviert", + "settings.plugins.actions.add": "Hinzufügen", + "settings.plugins.actions.remove": "Entfernen", + "settings.plugins.errors.loadFailed": "Fehler beim Laden der Plugin-Konfiguration.", + "settings.plugins.errors.addFailed": "Fehler beim Hinzufügen des Plugins.", + "settings.plugins.errors.removeFailed": "Fehler beim Entfernen des Plugins.", + "settings.plugins.errors.invalidSpec": "Muss mit npm:, file://, https://, /, oder . beginnen", + "settings.plugins.remove.confirm": "Dieses Plugin entfernen?", + "settings.plugins.remove.confirmYes": "Entfernen", + "settings.plugins.remove.confirmNo": "Abbrechen", + } as const diff --git a/packages/ui/src/lib/i18n/messages/en/settings.ts b/packages/ui/src/lib/i18n/messages/en/settings.ts index 0b9f764bc..3b00dfd42 100644 --- a/packages/ui/src/lib/i18n/messages/en/settings.ts +++ b/packages/ui/src/lib/i18n/messages/en/settings.ts @@ -11,6 +11,28 @@ export const settingsMessages = { "instanceServiceStatus.mcp.toggleAriaLabel": "Toggle {name} MCP server", "instanceServiceStatus.plugins.loading": "Loading plugins...", "instanceServiceStatus.plugins.empty": "No plugins configured.", + "instanceServiceStatus.plugins.manage": "Manage", + + "settings.plugins.title": "Plugin Manager", + "settings.plugins.subtitle": "Add or remove OpenCode plugins for the active workspace.", + "settings.plugins.refresh": "Refresh", + "settings.plugins.loading": "Loading plugins...", + "settings.plugins.addPlaceholder": "npm:plugin-name or file:///path/to/plugin.ts", + "settings.plugins.restartNote": "Plugin changes require restarting the workspace to take effect.", + "settings.plugins.configured.title": "Configured Plugins", + "settings.plugins.empty.noPlugins": "No additional plugins configured.", + "settings.plugins.empty.noInstance": "Start an OpenCode workspace before managing plugins.", + "settings.plugins.status.enabled": "Enabled", + "settings.plugins.status.disabled": "Disabled", + "settings.plugins.actions.add": "Add", + "settings.plugins.actions.remove": "Remove", + "settings.plugins.errors.loadFailed": "Failed to load plugin configuration.", + "settings.plugins.errors.addFailed": "Failed to add plugin.", + "settings.plugins.errors.removeFailed": "Failed to remove plugin.", + "settings.plugins.errors.invalidSpec": "Must start with npm:, file://, https://, /, or .", + "settings.plugins.remove.confirm": "Remove this plugin?", + "settings.plugins.remove.confirmYes": "Remove", + "settings.plugins.remove.confirmNo": "Cancel", "permissionBanner.pendingRequests.one": "{count} pending request", "permissionBanner.pendingRequests.other": "{count} pending requests", diff --git a/packages/ui/src/lib/i18n/messages/es/settings.ts b/packages/ui/src/lib/i18n/messages/es/settings.ts index 1b1844bdc..31c6337c0 100644 --- a/packages/ui/src/lib/i18n/messages/es/settings.ts +++ b/packages/ui/src/lib/i18n/messages/es/settings.ts @@ -415,4 +415,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "Copiar al portapapeles", "settings.info.diagnostics.download": "Descargar .txt", "settings.info.diagnostics.copied": "Información de diagnóstico copiada al portapapeles.", + + "instanceServiceStatus.plugins.manage": "Gestionar", + + "settings.plugins.title": "Gestor de Plugins", + "settings.plugins.subtitle": "Agrega o elimina plugins de OpenCode para el espacio de trabajo activo.", + "settings.plugins.refresh": "Actualizar", + "settings.plugins.loading": "Cargando plugins...", + "settings.plugins.addPlaceholder": "npm:nombre-del-plugin o file:///ruta/al/plugin.ts", + "settings.plugins.restartNote": "Los cambios de plugins requieren reiniciar el espacio de trabajo para surtir efecto.", + "settings.plugins.configured.title": "Plugins Configurados", + "settings.plugins.empty.noPlugins": "No hay plugins adicionales configurados.", + "settings.plugins.empty.noInstance": "Inicia un espacio de trabajo de OpenCode antes de gestionar plugins.", + "settings.plugins.status.enabled": "Activado", + "settings.plugins.status.disabled": "Desactivado", + "settings.plugins.actions.add": "Agregar", + "settings.plugins.actions.remove": "Eliminar", + "settings.plugins.errors.loadFailed": "Error al cargar la configuración de plugins.", + "settings.plugins.errors.addFailed": "Error al agregar el plugin.", + "settings.plugins.errors.removeFailed": "Error al eliminar el plugin.", + "settings.plugins.errors.invalidSpec": "Debe comenzar con npm:, file://, https://, /, o .", + "settings.plugins.remove.confirm": "¿Eliminar este plugin?", + "settings.plugins.remove.confirmYes": "Eliminar", + "settings.plugins.remove.confirmNo": "Cancelar", + } as const diff --git a/packages/ui/src/lib/i18n/messages/fr/settings.ts b/packages/ui/src/lib/i18n/messages/fr/settings.ts index beed9479c..b760bc4ad 100644 --- a/packages/ui/src/lib/i18n/messages/fr/settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr/settings.ts @@ -415,4 +415,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "Copier dans le presse-papiers", "settings.info.diagnostics.download": "Télécharger .txt", "settings.info.diagnostics.copied": "Informations de diagnostic copiées dans le presse-papiers.", + + "instanceServiceStatus.plugins.manage": "Gérer", + + "settings.plugins.title": "Gestionnaire de Plugins", + "settings.plugins.subtitle": "Ajoutez ou supprimez des plugins OpenCode pour l'espace de travail actif.", + "settings.plugins.refresh": "Actualiser", + "settings.plugins.loading": "Chargement des plugins...", + "settings.plugins.addPlaceholder": "npm:nom-du-plugin ou file:///chemin/vers/plugin.ts", + "settings.plugins.restartNote": "Les modifications de plugins nécessitent un redémarrage de l'espace de travail.", + "settings.plugins.configured.title": "Plugins Configurés", + "settings.plugins.empty.noPlugins": "Aucun plugin supplémentaire configuré.", + "settings.plugins.empty.noInstance": "Démarrez un espace de travail OpenCode avant de gérer les plugins.", + "settings.plugins.status.enabled": "Activé", + "settings.plugins.status.disabled": "Désactivé", + "settings.plugins.actions.add": "Ajouter", + "settings.plugins.actions.remove": "Supprimer", + "settings.plugins.errors.loadFailed": "Échec du chargement de la configuration des plugins.", + "settings.plugins.errors.addFailed": "Échec de l'ajout du plugin.", + "settings.plugins.errors.removeFailed": "Échec de la suppression du plugin.", + "settings.plugins.errors.invalidSpec": "Doit commencer par npm:, file://, https://, /, ou .", + "settings.plugins.remove.confirm": "Supprimer ce plugin ?", + "settings.plugins.remove.confirmYes": "Supprimer", + "settings.plugins.remove.confirmNo": "Annuler", + } as const diff --git a/packages/ui/src/lib/i18n/messages/he/settings.ts b/packages/ui/src/lib/i18n/messages/he/settings.ts index 22fe1711a..3562cbe42 100644 --- a/packages/ui/src/lib/i18n/messages/he/settings.ts +++ b/packages/ui/src/lib/i18n/messages/he/settings.ts @@ -415,4 +415,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "העתק ללוח", "settings.info.diagnostics.download": "הורד .txt", "settings.info.diagnostics.copied": "מידע האבחון הועתק ללוח.", + + "instanceServiceStatus.plugins.manage": "ניהול", + + "settings.plugins.title": "מנהל התוספים", + "settings.plugins.subtitle": "הוסף או הסר תוספי OpenCode לסביבת העבודה הפעילה.", + "settings.plugins.refresh": "רענן", + "settings.plugins.loading": "טוען תוספים...", + "settings.plugins.addPlaceholder": "npm:שם-התוסף או file:///נתיב/אל/plugin.ts", + "settings.plugins.restartNote": "שינויים בתוספים דורשים הפעלה מחדש של סביבת העבודה.", + "settings.plugins.configured.title": "תוספים מוגדרים", + "settings.plugins.empty.noPlugins": "לא הוגדרו תוספים נוספים.", + "settings.plugins.empty.noInstance": "הפעל סביבת עבודה של OpenCode לפני ניהול תוספים.", + "settings.plugins.status.enabled": "פעיל", + "settings.plugins.status.disabled": "מושבת", + "settings.plugins.actions.add": "הוסף", + "settings.plugins.actions.remove": "הסר", + "settings.plugins.errors.loadFailed": "טעינת תצורת התוספים נכשלה.", + "settings.plugins.errors.addFailed": "הוספת התוסף נכשלה.", + "settings.plugins.errors.removeFailed": "הסרת התוסף נכשלה.", + "settings.plugins.errors.invalidSpec": "חייב להתחיל ב-npm:, file://, https://, /, או .", + "settings.plugins.remove.confirm": "להסיר תוסף זה?", + "settings.plugins.remove.confirmYes": "הסר", + "settings.plugins.remove.confirmNo": "בטל", + } as const diff --git a/packages/ui/src/lib/i18n/messages/ja/settings.ts b/packages/ui/src/lib/i18n/messages/ja/settings.ts index 9fc667e6a..3daa769c2 100644 --- a/packages/ui/src/lib/i18n/messages/ja/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja/settings.ts @@ -415,4 +415,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "クリップボードにコピー", "settings.info.diagnostics.download": ".txt をダウンロード", "settings.info.diagnostics.copied": "診断情報をクリップボードにコピーしました。", + + "instanceServiceStatus.plugins.manage": "管理", + + "settings.plugins.title": "プラグインマネージャー", + "settings.plugins.subtitle": "アクティブなワークスペースのOpenCodeプラグインを追加・削除します。", + "settings.plugins.refresh": "更新", + "settings.plugins.loading": "プラグインを読み込み中...", + "settings.plugins.addPlaceholder": "npm:プラグイン名 または file:///パス/plugin.ts", + "settings.plugins.restartNote": "プラグインの変更を反映するにはワークスペースの再起動が必要です。", + "settings.plugins.configured.title": "設定済みプラグイン", + "settings.plugins.empty.noPlugins": "追加のプラグインは設定されていません。", + "settings.plugins.empty.noInstance": "プラグインを管理する前にOpenCodeワークスペースを起動してください。", + "settings.plugins.status.enabled": "有効", + "settings.plugins.status.disabled": "無効", + "settings.plugins.actions.add": "追加", + "settings.plugins.actions.remove": "削除", + "settings.plugins.errors.loadFailed": "プラグイン設定の読み込みに失敗しました。", + "settings.plugins.errors.addFailed": "プラグインの追加に失敗しました。", + "settings.plugins.errors.removeFailed": "プラグインの削除に失敗しました。", + "settings.plugins.errors.invalidSpec": "npm:, file://, https://, /, . のいずれかで始める必要があります", + "settings.plugins.remove.confirm": "このプラグインを削除しますか?", + "settings.plugins.remove.confirmYes": "削除", + "settings.plugins.remove.confirmNo": "キャンセル", + } as const diff --git a/packages/ui/src/lib/i18n/messages/ne/settings.ts b/packages/ui/src/lib/i18n/messages/ne/settings.ts index ec0a72bd2..5512c8f91 100644 --- a/packages/ui/src/lib/i18n/messages/ne/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ne/settings.ts @@ -414,4 +414,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "क्लिपबोर्डमा प्रतिलिपि गर्नुहोस्", "settings.info.diagnostics.download": ".txt डाउनलोड गर्नुहोस्", "settings.info.diagnostics.copied": "निदान जानकारी क्लिपबोर्डमा प्रतिलिपि गरियो।", + + "instanceServiceStatus.plugins.manage": "व्यवस्थापन", + + "settings.plugins.title": "प्लगइन प्रबन्धक", + "settings.plugins.subtitle": "सक्रिय कार्यक्षेत्रको लागि OpenCode प्लगइनहरू थप्नुहोस् वा हटाउनुहोस्।", + "settings.plugins.refresh": "रिफ्रेस", + "settings.plugins.loading": "प्लगइनहरू लोड गर्दै...", + "settings.plugins.addPlaceholder": "npm:प्लगइन-नाम वा file:///पथ/plugin.ts", + "settings.plugins.restartNote": "प्लगइन परिवर्तनहरू कार्यान्वयन हुन कार्यक्षेत्र पुनः सुरु गर्न आवश्यक छ।", + "settings.plugins.configured.title": "कन्फिगर गरिएका प्लगइनहरू", + "settings.plugins.empty.noPlugins": "कुनै अतिरिक्त प्लगइन कन्फिगर गरिएको छैन।", + "settings.plugins.empty.noInstance": "प्लगइनहरू व्यवस्थापन गर्नु अघि OpenCode कार्यक्षेत्र सुरु गर्नुहोस्।", + "settings.plugins.status.enabled": "सक्रिय", + "settings.plugins.status.disabled": "निस्क्रिय", + "settings.plugins.actions.add": "थप्नुहोस्", + "settings.plugins.actions.remove": "हटाउनुहोस्", + "settings.plugins.errors.loadFailed": "प्लगइन कन्फिगरेसन लोड गर्न असफल।", + "settings.plugins.errors.addFailed": "प्लगइन थप्न असफल।", + "settings.plugins.errors.removeFailed": "प्लगइन हटाउन असफल।", + "settings.plugins.errors.invalidSpec": "npm:, file://, https://, /, वा . बाट सुरु हुनुपर्छ", + "settings.plugins.remove.confirm": "यो प्लगइन हटाउने?", + "settings.plugins.remove.confirmYes": "हटाउनुहोस्", + "settings.plugins.remove.confirmNo": "रद्द गर्नुहोस्", + } as const diff --git a/packages/ui/src/lib/i18n/messages/ru/settings.ts b/packages/ui/src/lib/i18n/messages/ru/settings.ts index 05ee7dbb3..86d001209 100644 --- a/packages/ui/src/lib/i18n/messages/ru/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ru/settings.ts @@ -415,4 +415,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "Скопировать в буфер обмена", "settings.info.diagnostics.download": "Скачать .txt", "settings.info.diagnostics.copied": "Диагностическая информация скопирована в буфер обмена.", + + "instanceServiceStatus.plugins.manage": "Управление", + + "settings.plugins.title": "Менеджер плагинов", + "settings.plugins.subtitle": "Добавление или удаление плагинов OpenCode для активного рабочего пространства.", + "settings.plugins.refresh": "Обновить", + "settings.plugins.loading": "Загрузка плагинов...", + "settings.plugins.addPlaceholder": "npm:имя-плагина или file:///путь/к/plugin.ts", + "settings.plugins.restartNote": "Изменения плагинов вступают в силу после перезапуска рабочего пространства.", + "settings.plugins.configured.title": "Настроенные плагины", + "settings.plugins.empty.noPlugins": "Дополнительные плагины не настроены.", + "settings.plugins.empty.noInstance": "Запустите рабочее пространство OpenCode перед управлением плагинами.", + "settings.plugins.status.enabled": "Включено", + "settings.plugins.status.disabled": "Отключено", + "settings.plugins.actions.add": "Добавить", + "settings.plugins.actions.remove": "Удалить", + "settings.plugins.errors.loadFailed": "Не удалось загрузить конфигурацию плагинов.", + "settings.plugins.errors.addFailed": "Не удалось добавить плагин.", + "settings.plugins.errors.removeFailed": "Не удалось удалить плагин.", + "settings.plugins.errors.invalidSpec": "Должен начинаться с npm:, file://, https://, /, или .", + "settings.plugins.remove.confirm": "Удалить этот плагин?", + "settings.plugins.remove.confirmYes": "Удалить", + "settings.plugins.remove.confirmNo": "Отмена", + } as const diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts index 1bf6924ca..660548fa6 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts @@ -415,4 +415,28 @@ export const settingsMessages = { "settings.info.diagnostics.copy": "复制到剪贴板", "settings.info.diagnostics.download": "下载 .txt", "settings.info.diagnostics.copied": "诊断信息已复制到剪贴板。", + + "instanceServiceStatus.plugins.manage": "管理", + + "settings.plugins.title": "插件管理器", + "settings.plugins.subtitle": "为当前工作区添加或移除 OpenCode 插件。", + "settings.plugins.refresh": "刷新", + "settings.plugins.loading": "正在加载插件...", + "settings.plugins.addPlaceholder": "npm:插件名 或 file:///路径/plugin.ts", + "settings.plugins.restartNote": "插件更改需要重启工作区才能生效。", + "settings.plugins.configured.title": "已配置的插件", + "settings.plugins.empty.noPlugins": "未配置额外插件。", + "settings.plugins.empty.noInstance": "请先启动 OpenCode 工作区再管理插件。", + "settings.plugins.status.enabled": "已启用", + "settings.plugins.status.disabled": "已禁用", + "settings.plugins.actions.add": "添加", + "settings.plugins.actions.remove": "移除", + "settings.plugins.errors.loadFailed": "加载插件配置失败。", + "settings.plugins.errors.addFailed": "添加插件失败。", + "settings.plugins.errors.removeFailed": "移除插件失败。", + "settings.plugins.errors.invalidSpec": "必须以 npm:、file://、https://、/ 或 . 开头", + "settings.plugins.remove.confirm": "移除此插件?", + "settings.plugins.remove.confirmYes": "移除", + "settings.plugins.remove.confirmNo": "取消", + } as const