- {language.t("dialog.project.edit.color")}
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/packages/app/src/components/project-settings-extensions.tsx b/packages/app/src/components/project-settings-extensions.tsx
new file mode 100644
index 0000000000..4c427a2258
--- /dev/null
+++ b/packages/app/src/components/project-settings-extensions.tsx
@@ -0,0 +1,238 @@
+import { Icon } from "@opencode-ai/ui/icon"
+import { Switch } from "@opencode-ai/ui/v2/switch-v2"
+import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
+import { type Component, For, Show, createMemo, createResource, createSignal } from "solid-js"
+import { useLanguage } from "@/context/language"
+import { useMcpToggle } from "@/context/mcp"
+import { useSDK } from "@/context/sdk"
+import { useServerSDK } from "@/context/server-sdk"
+import { useServerSync } from "@/context/server-sync"
+import { useSync } from "@/context/sync"
+import { ExternalLink } from "./external-link"
+
+type SkillItem = {
+ name: string
+ location: string
+}
+
+const pluginName = (item: string | [string, Record
]) => (typeof item === "string" ? item : item[0])
+const skillKey = (item: SkillItem) => `${item.name}\n${item.location}`
+
+const ExtensionCard: Component<{ children: unknown }> = (props) => (
+ {props.children as any}
+)
+
+const ExtensionRow: Component<{
+ icon: "mcp" | "cube" | "post-skill" | "code"
+ name: string
+ status?: string
+ children?: unknown
+}> = (props) => (
+
+
+
+ {props.name}
+
+
+ {(status) => (
+
+
+ {status()}
+
+ )}
+
+ {props.children as any}
+
+)
+
+const SharedSection: Component<{
+ count: number
+ children: unknown
+}> = (props) => {
+ const language = useLanguage()
+ const [open, setOpen] = createSignal(false)
+ return (
+ 0}>
+
+
+
+ {props.children}
+
+
+
+ )
+}
+
+export const ProjectSettingsExtensions: Component = () => {
+ const language = useLanguage()
+ const serverSDK = useServerSDK()
+ const directorySDK = useSDK()
+ const serverSync = useServerSync()
+ const sync = useSync()
+ const toggleMcp = useMcpToggle()
+
+ const [serverMcp] = createResource(
+ serverSDK,
+ (sdk) =>
+ sdk.api.mcp
+ .list()
+ .then((result) => Object.fromEntries(result.data.map((server) => [server.name, server.status])))
+ .catch(() => ({})),
+ { initialValue: {} },
+ )
+ const globalMcpNames = createMemo(() =>
+ [...new Set([...Object.keys(serverSync().data.config.mcp ?? {}), ...Object.keys(serverMcp.latest)])].sort(),
+ )
+ const projectMcpNames = createMemo(() => {
+ const shared = new Set(globalMcpNames())
+ const configured = Object.keys(sync().data.config.mcp ?? {}).filter((name) => !shared.has(name))
+ if (configured.length > 0) return configured.sort()
+ return Object.keys(sync().data.mcp ?? {})
+ .filter((name) => !shared.has(name))
+ .sort()
+ })
+ const mcpEnabled = (name: string) => sync().data.mcp?.[name]?.status === "connected"
+
+ const globalPlugins = createMemo(() => (serverSync().data.config.plugin ?? []).map(pluginName))
+ const projectPlugins = createMemo(() => {
+ const shared = new Set(globalPlugins())
+ return (sync().data.config.plugin ?? []).map(pluginName).filter((name) => !shared.has(name))
+ })
+
+ const [serverSkills] = createResource(
+ serverSDK,
+ (sdk): Promise =>
+ sdk.api.skill.list().then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))),
+ { initialValue: [] },
+ )
+ const [directorySkills] = createResource(
+ directorySDK,
+ (sdk): Promise =>
+ sdk.api.skill
+ .list({ location: { directory: sdk.directory } })
+ .then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))),
+ { initialValue: [] },
+ )
+ const projectSkills = createMemo(() => {
+ const shared = new Set(serverSkills.latest.map(skillKey))
+ return directorySkills.latest.filter((item) => !shared.has(skillKey(item)))
+ })
+
+ const mcpRows = (items: string[]) => (
+
+ {(name) => (
+
+ {
+ if (toggleMcp.isPending) return
+ toggleMcp.mutate(name)
+ }}
+ >
+ {name}
+
+
+ )}
+
+ )
+
+ const pluginRows = (items: string[]) => {(name) => }
+
+ const skillRows = (items: SkillItem[]) => (
+ {(item) => }
+ )
+
+ return (
+
+
+
+
+
+ {language.t("settings.extensions.tab.mcps")}
+ {language.t("status.popover.tab.plugins")}
+ {language.t("settings.extensions.tab.skills")}
+ {language.t("project.settings.extensions.tab.lsps")}
+
+
+
+
+
+ 0}>
+ {mcpRows(projectMcpNames())}
+
+ {mcpRows(globalMcpNames())}
+
+
+
+
+
+
+ 0}>
+ {pluginRows(projectPlugins())}
+
+ {pluginRows(globalPlugins())}
+
+
+
+
+
+
+ 0}>
+ {skillRows(projectSkills())}
+
+ {skillRows(serverSkills.latest)}
+
+
+
+
+
+
+ 0}>
+
+
+ {(item) => (
+
+ )}
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx
index ce74be4586..57d71c0d6d 100644
--- a/packages/app/src/components/settings-keybinds.tsx
+++ b/packages/app/src/components/settings-keybinds.tsx
@@ -451,7 +451,10 @@ function SettingsKeybindsV2View(props: {
<>