mirror of
https://github.com/mfakbar127/Claude-Samurai
synced 2026-06-21 13:57:42 +00:00
add disable/enabled in commands and mcp
This commit is contained in:
+964
-451
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
mod commands;
|
||||
mod helper;
|
||||
mod tray;
|
||||
mod hook_server;
|
||||
|
||||
@@ -186,6 +187,7 @@ pub fn run() {
|
||||
check_mcp_server_exists,
|
||||
get_mcp_enabled_state,
|
||||
toggle_mcp_server_state,
|
||||
toggle_direct_mcp_server,
|
||||
get_mcp_servers_with_state,
|
||||
read_claude_projects,
|
||||
read_claude_config_file,
|
||||
@@ -209,7 +211,11 @@ pub fn run() {
|
||||
toggle_claude_command,
|
||||
read_claude_agents,
|
||||
write_claude_agent,
|
||||
delete_claude_agent
|
||||
delete_claude_agent,
|
||||
read_installed_plugins,
|
||||
toggle_plugin,
|
||||
read_plugin_commands,
|
||||
read_plugin_agents
|
||||
])
|
||||
.on_window_event(|window, event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CpuIcon,
|
||||
FileJsonIcon,
|
||||
FolderIcon,
|
||||
PackageIcon,
|
||||
SettingsIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
@@ -52,6 +53,11 @@ export function Layout() {
|
||||
icon: TerminalIcon,
|
||||
label: t("navigation.commands"),
|
||||
},
|
||||
{
|
||||
to: "/plugins",
|
||||
icon: PackageIcon,
|
||||
label: t("navigation.plugins"),
|
||||
},
|
||||
{
|
||||
to: "/notification",
|
||||
icon: BellIcon,
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"agents.noAgents": "No sub agents found. Click \"Add Agent\" to create one.",
|
||||
"agents.save": "Save",
|
||||
"agents.saving": "Saving...",
|
||||
"agents.sourceUser": "User",
|
||||
"agents.sourcePlugin": "Plugin",
|
||||
"agents.title": "Agents",
|
||||
"agents.validationError": "Validation Error",
|
||||
"app.title": "CC Mate",
|
||||
@@ -46,6 +48,8 @@
|
||||
"commands.noCommands": "No custom commands found. Click \"Add Command\" to create one.",
|
||||
"commands.save": "Save",
|
||||
"commands.saving": "Saving...",
|
||||
"commands.sourceUser": "User",
|
||||
"commands.sourcePlugin": "Plugin",
|
||||
"commands.title": "Commands",
|
||||
"commands.validationError": "Validation Error",
|
||||
"commands.viewInClaude": "View in Claude",
|
||||
@@ -72,6 +76,7 @@
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code Original Configuration",
|
||||
"configSwitcher.title": "Configurations",
|
||||
"error.title": "Error",
|
||||
"error.save": "Failed to save configuration",
|
||||
"error.validation": "Invalid JSON format",
|
||||
"glm.buyFromOfficial": "Buy from Official Website",
|
||||
@@ -136,6 +141,10 @@
|
||||
"mcp.runtimeDisabled": "Runtime Disabled",
|
||||
"mcp.scope": "Scope",
|
||||
"mcp.sourceType": "Source",
|
||||
"mcp.projectScope": "Project",
|
||||
"mcp.globalScope": "Global (User Scope)",
|
||||
"mcp.searchProject": "Search projects...",
|
||||
"mcp.noProjectFound": "No project found.",
|
||||
"mcp.invalidConfigError": "Configuration must be a JSON object.",
|
||||
"mcp.invalidJsonError": "Invalid JSON configuration for {{serverName}}",
|
||||
"mcp.invalidJsonTitle": "Invalid JSON",
|
||||
@@ -173,6 +182,7 @@
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.memory": "Memory",
|
||||
"navigation.notifications": "Notifications",
|
||||
"navigation.plugins": "Plugins",
|
||||
"navigation.projects": "Projects",
|
||||
"navigation.settings": "Settings",
|
||||
"navigation.usage": "Usage",
|
||||
@@ -188,6 +198,28 @@
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.toolUse": "Tool Use Notifications",
|
||||
"notifications.toolUseDescription": "Notify when Claude Code is using tools",
|
||||
"plugins.title": "Plugins",
|
||||
"plugins.description": "Manage Claude Code plugins",
|
||||
"plugins.error": "Error loading plugins: {{error}}",
|
||||
"plugins.noPlugins": "No plugins installed",
|
||||
"plugins.none": "None",
|
||||
"plugins.scope": "Scope",
|
||||
"plugins.scopeUser": "Global",
|
||||
"plugins.scopeLocal": "Local",
|
||||
"plugins.status": "Status",
|
||||
"plugins.enabled": "Enabled",
|
||||
"plugins.disabled": "Disabled",
|
||||
"plugins.enable": "Enable",
|
||||
"plugins.disable": "Disable",
|
||||
"plugins.packages": "Packages",
|
||||
"plugins.packageAgents": "Agents",
|
||||
"plugins.packageSkills": "Skills",
|
||||
"plugins.packageCommands": "Commands",
|
||||
"plugins.packageMcp": "MCP",
|
||||
"plugins.projectPath": "Project Path",
|
||||
"plugins.version": "Version",
|
||||
"plugins.toggleLocalError": "Cannot toggle local plugin \"{{pluginName}}\" - project path is missing",
|
||||
"plugins.validationError": "Validation Error",
|
||||
"projects.detail.backToProjects": "Back to Projects",
|
||||
"projects.detail.editor": "Project Configuration Editor",
|
||||
"projects.detail.invalidJson": "Invalid JSON format. Please fix the errors before saving.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"agents.noAgents": "Aucun sub agent trouvé. Cliquez sur \"Add Agent\" pour en créer un.",
|
||||
"agents.save": "Sauvegarder",
|
||||
"agents.saving": "Sauvegarde...",
|
||||
"agents.sourceUser": "Utilisateur",
|
||||
"agents.sourcePlugin": "Plugin",
|
||||
"agents.title": "Agent",
|
||||
"agents.validationError": "Erreur de validation",
|
||||
"app.title": "CC Mate",
|
||||
@@ -42,6 +44,8 @@
|
||||
"commands.noCommands": "Aucune commande personnalisée trouvée. Cliquez sur \"Ajouter une commande\" pour en créer une.",
|
||||
"commands.save": "Sauvegarder",
|
||||
"commands.saving": "Sauvegarde...",
|
||||
"commands.sourceUser": "Utilisateur",
|
||||
"commands.sourcePlugin": "Plugin",
|
||||
"commands.title": "Commandes",
|
||||
"commands.validationError": "Erreur de validation",
|
||||
"commands.viewInClaude": "Voir dans Claude",
|
||||
@@ -68,6 +72,7 @@
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Configuration d'origine de Claude Code",
|
||||
"configSwitcher.title": "Configurations",
|
||||
"error.title": "Erreur",
|
||||
"error.save": "Échec de la sauvegarde de la configuration",
|
||||
"error.validation": "Format JSON invalide",
|
||||
"glm.buyFromOfficial": "Acheter sur le site officiel",
|
||||
@@ -120,6 +125,10 @@
|
||||
"mcp.runtimeDisabled": "Désactivé au runtime",
|
||||
"mcp.scope": "Portée",
|
||||
"mcp.sourceType": "Source",
|
||||
"mcp.projectScope": "Projet",
|
||||
"mcp.globalScope": "Global (Portée utilisateur)",
|
||||
"mcp.searchProject": "Rechercher des projets...",
|
||||
"mcp.noProjectFound": "Aucun projet trouvé.",
|
||||
"mcp.invalidConfigError": "La configuration doit être un objet JSON.",
|
||||
"mcp.invalidJsonError": "Configuration JSON invalide pour {{serverName}}",
|
||||
"mcp.invalidJsonTitle": "JSON invalide",
|
||||
@@ -157,6 +166,7 @@
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.memory": "Mémoire",
|
||||
"navigation.notifications": "Notifications",
|
||||
"navigation.plugins": "Plugins",
|
||||
"navigation.projects": "Projets",
|
||||
"navigation.settings": "Paramètres",
|
||||
"navigation.usage": "Utilisation",
|
||||
@@ -172,6 +182,28 @@
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.toolUse": "Notifications d'utilisation d'outils",
|
||||
"notifications.toolUseDescription": "Notifier lorsque Claude Code utilise des outils",
|
||||
"plugins.title": "Plugins",
|
||||
"plugins.description": "Gérer les plugins Claude Code",
|
||||
"plugins.error": "Erreur lors du chargement des plugins : {{error}}",
|
||||
"plugins.noPlugins": "Aucun plugin installé",
|
||||
"plugins.none": "Aucun",
|
||||
"plugins.scope": "Portée",
|
||||
"plugins.scopeUser": "Global",
|
||||
"plugins.scopeLocal": "Local",
|
||||
"plugins.status": "Statut",
|
||||
"plugins.enabled": "Activé",
|
||||
"plugins.disabled": "Désactivé",
|
||||
"plugins.enable": "Activer",
|
||||
"plugins.disable": "Désactiver",
|
||||
"plugins.packages": "Packages",
|
||||
"plugins.packageAgents": "Agents",
|
||||
"plugins.packageSkills": "Compétences",
|
||||
"plugins.packageCommands": "Commandes",
|
||||
"plugins.packageMcp": "MCP",
|
||||
"plugins.projectPath": "Chemin du projet",
|
||||
"plugins.version": "Version",
|
||||
"plugins.toggleLocalError": "Impossible d'activer/désactiver le plugin \"{{pluginName}}\" - chemin du projet manquant",
|
||||
"plugins.validationError": "Erreur de validation",
|
||||
"projects.detail.backToProjects": "Retour aux projets",
|
||||
"projects.detail.editor": "Éditeur de configuration de projet",
|
||||
"projects.detail.invalidJson": "Format JSON invalide. Veuillez corriger les erreurs avant de sauvegarder.",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"agents.noAgents": "sub agent が見つかりません。「Add Agent」をクリックして作成してください。",
|
||||
"agents.save": "保存",
|
||||
"agents.saving": "保存中...",
|
||||
"agents.sourceUser": "ユーザー",
|
||||
"agents.sourcePlugin": "プラグイン",
|
||||
"agents.title": "Agent",
|
||||
"agents.validationError": "検証エラー",
|
||||
"app.title": "CC Mate",
|
||||
@@ -42,6 +44,8 @@
|
||||
"commands.noCommands": "カスタムコマンドが見つかりません。「コマンドを追加」をクリックして作成してください。",
|
||||
"commands.save": "保存",
|
||||
"commands.saving": "保存中...",
|
||||
"commands.sourceUser": "ユーザー",
|
||||
"commands.sourcePlugin": "プラグイン",
|
||||
"commands.title": "コマンド",
|
||||
"commands.validationError": "検証エラー",
|
||||
"commands.viewInClaude": "Claude で表示",
|
||||
@@ -68,6 +72,7 @@
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code の元の設定",
|
||||
"configSwitcher.title": "設定",
|
||||
"error.title": "エラー",
|
||||
"error.save": "設定の保存に失敗しました",
|
||||
"error.validation": "JSON 形式が無効です",
|
||||
"glm.buyFromOfficial": "公式サイトで購入",
|
||||
@@ -120,6 +125,10 @@
|
||||
"mcp.runtimeDisabled": "ランタイム無効",
|
||||
"mcp.scope": "スコープ",
|
||||
"mcp.sourceType": "ソース",
|
||||
"mcp.projectScope": "プロジェクト",
|
||||
"mcp.globalScope": "グローバル(ユーザースコープ)",
|
||||
"mcp.searchProject": "プロジェクトを検索...",
|
||||
"mcp.noProjectFound": "プロジェクトが見つかりません。",
|
||||
"mcp.invalidConfigError": "設定は JSON オブジェクトである必要があります。",
|
||||
"mcp.invalidJsonError": "{{serverName}} の JSON 設定が無効です",
|
||||
"mcp.invalidJsonTitle": "JSON が無効です",
|
||||
@@ -157,6 +166,7 @@
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.memory": "メモリ",
|
||||
"navigation.notifications": "通知",
|
||||
"navigation.plugins": "プラグイン",
|
||||
"navigation.projects": "プロジェクト",
|
||||
"navigation.settings": "設定",
|
||||
"navigation.usage": "使用量",
|
||||
@@ -172,6 +182,28 @@
|
||||
"notifications.title": "通知",
|
||||
"notifications.toolUse": "ツール使用通知",
|
||||
"notifications.toolUseDescription": "Claude Code がツールを使用する際に通知",
|
||||
"plugins.title": "プラグイン",
|
||||
"plugins.description": "Claude Code プラグインを管理",
|
||||
"plugins.error": "プラグインの読み込みエラー: {{error}}",
|
||||
"plugins.noPlugins": "プラグインがインストールされていません",
|
||||
"plugins.none": "なし",
|
||||
"plugins.scope": "スコープ",
|
||||
"plugins.scopeUser": "グローバル",
|
||||
"plugins.scopeLocal": "ローカル",
|
||||
"plugins.status": "ステータス",
|
||||
"plugins.enabled": "有効",
|
||||
"plugins.disabled": "無効",
|
||||
"plugins.enable": "有効化",
|
||||
"plugins.disable": "無効化",
|
||||
"plugins.packages": "パッケージ",
|
||||
"plugins.packageAgents": "エージェント",
|
||||
"plugins.packageSkills": "スキル",
|
||||
"plugins.packageCommands": "コマンド",
|
||||
"plugins.packageMcp": "MCP",
|
||||
"plugins.projectPath": "プロジェクトパス",
|
||||
"plugins.version": "バージョン",
|
||||
"plugins.toggleLocalError": "ローカルプラグイン \"{{pluginName}}\" を切り替えできません - プロジェクトパスがありません",
|
||||
"plugins.validationError": "検証エラー",
|
||||
"projects.detail.backToProjects": "プロジェクトに戻る",
|
||||
"projects.detail.editor": "プロジェクト設定エディター",
|
||||
"projects.detail.invalidJson": "JSON 形式が無効です。保存前にエラーを修正してください。",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"agents.noAgents": "未找到 sub agent。点击「Add Agent」来创建一个。",
|
||||
"agents.save": "保存",
|
||||
"agents.saving": "保存中...",
|
||||
"agents.sourceUser": "用户",
|
||||
"agents.sourcePlugin": "插件",
|
||||
"agents.title": "Agent",
|
||||
"agents.validationError": "验证错误",
|
||||
"app.title": "CC Mate",
|
||||
@@ -42,6 +44,8 @@
|
||||
"commands.noCommands": "未找到自定义命令。点击「添加命令」来创建一个。",
|
||||
"commands.save": "保存",
|
||||
"commands.saving": "保存中...",
|
||||
"commands.sourceUser": "用户",
|
||||
"commands.sourcePlugin": "插件",
|
||||
"commands.title": "命令",
|
||||
"commands.validationError": "验证错误",
|
||||
"commands.viewInClaude": "在 Claude 中查看",
|
||||
@@ -68,6 +72,7 @@
|
||||
"configSwitcher.originalConfig": "Claude",
|
||||
"configSwitcher.originalConfigDescription": "Claude Code 原有配置",
|
||||
"configSwitcher.title": "配置",
|
||||
"error.title": "错误",
|
||||
"error.save": "保存配置失败",
|
||||
"error.validation": "JSON 格式无效",
|
||||
"glm.buyFromOfficial": "前往官网购买",
|
||||
@@ -132,6 +137,10 @@
|
||||
"mcp.runtimeDisabled": "运行时禁用",
|
||||
"mcp.scope": "范围",
|
||||
"mcp.sourceType": "来源",
|
||||
"mcp.projectScope": "项目",
|
||||
"mcp.globalScope": "全局(用户范围)",
|
||||
"mcp.searchProject": "搜索项目...",
|
||||
"mcp.noProjectFound": "未找到项目。",
|
||||
"mcp.invalidConfigError": "配置必须是 JSON 对象。",
|
||||
"mcp.invalidJsonError": "{{serverName}} 的 JSON 配置无效",
|
||||
"mcp.invalidJsonTitle": "JSON 无效",
|
||||
@@ -169,6 +178,7 @@
|
||||
"navigation.mcp": "MCP",
|
||||
"navigation.memory": "记忆",
|
||||
"navigation.notifications": "通知",
|
||||
"navigation.plugins": "插件",
|
||||
"navigation.projects": "项目",
|
||||
"navigation.settings": "设置",
|
||||
"navigation.usage": "用量",
|
||||
@@ -184,6 +194,28 @@
|
||||
"notifications.title": "通知",
|
||||
"notifications.toolUse": "工具使用通知",
|
||||
"notifications.toolUseDescription": "当 Claude Code 使用工具时提醒",
|
||||
"plugins.title": "插件",
|
||||
"plugins.description": "管理 Claude Code 插件",
|
||||
"plugins.error": "加载插件时出错:{{error}}",
|
||||
"plugins.noPlugins": "未安装插件",
|
||||
"plugins.none": "无",
|
||||
"plugins.scope": "范围",
|
||||
"plugins.scopeUser": "全局",
|
||||
"plugins.scopeLocal": "本地",
|
||||
"plugins.status": "状态",
|
||||
"plugins.enabled": "已启用",
|
||||
"plugins.disabled": "已禁用",
|
||||
"plugins.enable": "启用",
|
||||
"plugins.disable": "禁用",
|
||||
"plugins.packages": "包",
|
||||
"plugins.packageAgents": "代理",
|
||||
"plugins.packageSkills": "技能",
|
||||
"plugins.packageCommands": "命令",
|
||||
"plugins.packageMcp": "MCP",
|
||||
"plugins.projectPath": "项目路径",
|
||||
"plugins.version": "版本",
|
||||
"plugins.toggleLocalError": "无法切换本地插件 \"{{pluginName}}\" - 缺少项目路径",
|
||||
"plugins.validationError": "验证错误",
|
||||
"projects.detail.backToProjects": "返回项目列表",
|
||||
"projects.detail.editor": "项目配置编辑器",
|
||||
"projects.detail.invalidJson": "JSON 格式无效。请在保存前修复错误。",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { yamlFrontmatter } from "@codemirror/lang-yaml";
|
||||
import { EditorView } from "@uiw/react-codemirror";
|
||||
|
||||
export const codeMirrorBasicSetup = {
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
} as const;
|
||||
|
||||
export const markdownExtensions = [
|
||||
yamlFrontmatter({
|
||||
content: markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
+118
-12
@@ -55,6 +55,25 @@ export interface CommandFile {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface PluginCommandFile {
|
||||
name: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
disabled: boolean;
|
||||
pluginName: string;
|
||||
pluginScope: string;
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
export interface PluginAgentFile {
|
||||
name: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
pluginName: string;
|
||||
pluginScope: string;
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
export const useConfigFiles = () => {
|
||||
return useQuery({
|
||||
queryKey: ["config-files"],
|
||||
@@ -400,32 +419,42 @@ export const useDeleteGlobalMcpServer = () => {
|
||||
export interface McpEnabledState {
|
||||
enabledMcpjsonServers: string[];
|
||||
disabledMcpjsonServers: string[];
|
||||
disabledMcpServers: string[];
|
||||
}
|
||||
|
||||
export const useGetMcpEnabledState = () => {
|
||||
export const useGetMcpEnabledState = (cwd?: string) => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["mcp-enabled-state"],
|
||||
queryFn: () => invoke<McpEnabledState>("get_mcp_enabled_state"),
|
||||
queryKey: ["mcp-enabled-state", cwd],
|
||||
queryFn: () => invoke<McpEnabledState>("get_mcp_enabled_state", { cwd }),
|
||||
});
|
||||
};
|
||||
|
||||
export const useToggleMcpServer = () => {
|
||||
export const useToggleMcpServer = (cwd?: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
serverName,
|
||||
enabled,
|
||||
sourceType,
|
||||
}: {
|
||||
serverName: string;
|
||||
enabled: boolean;
|
||||
}) => invoke<void>("toggle_mcp_server_state", { serverName, enabled }),
|
||||
sourceType: "mcpjson" | "direct" | "plugin";
|
||||
}) => {
|
||||
if (sourceType === "direct") {
|
||||
return invoke<void>("toggle_direct_mcp_server", { serverName, enabled, cwd });
|
||||
} else {
|
||||
// Both "mcpjson" and "plugin" use the same toggle mechanism
|
||||
return invoke<void>("toggle_mcp_server_state", { serverName, enabled, cwd });
|
||||
}
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(
|
||||
`MCP server ${variables.enabled ? "enabled" : "disabled"} successfully`,
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp-enabled-state"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp-servers-with-state"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp-enabled-state", cwd] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp-servers-with-state", cwd] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
@@ -438,8 +467,8 @@ export const useToggleMcpServer = () => {
|
||||
export interface McpServerState {
|
||||
name: string;
|
||||
config: Record<string, any>;
|
||||
sourceType: "mcpjson" | "direct";
|
||||
scope: "user" | "project" | "local";
|
||||
sourceType: "mcpjson" | "direct" | "plugin";
|
||||
scope: "user" | "project" | "local" | "plugin-user" | "plugin-local";
|
||||
definedIn: string;
|
||||
controllable: boolean;
|
||||
state: "disabled" | "enabled" | "runtime-disabled";
|
||||
@@ -447,10 +476,10 @@ export interface McpServerState {
|
||||
inDisabledArray: boolean;
|
||||
}
|
||||
|
||||
export const useGetMcpServersWithState = () => {
|
||||
export const useGetMcpServersWithState = (cwd?: string) => {
|
||||
return useSuspenseQuery({
|
||||
queryKey: ["mcp-servers-with-state"],
|
||||
queryFn: () => invoke<McpServerState[]>("get_mcp_servers_with_state"),
|
||||
queryKey: ["mcp-servers-with-state", cwd],
|
||||
queryFn: () => invoke<McpServerState[]>("get_mcp_servers_with_state", { cwd }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -589,6 +618,12 @@ export const useClaudeCommands = () =>
|
||||
queryFn: () => invoke<CommandFile[]>("read_claude_commands"),
|
||||
});
|
||||
|
||||
export const usePluginCommands = () =>
|
||||
useQuery({
|
||||
queryKey: ["plugin-commands"],
|
||||
queryFn: () => invoke<PluginCommandFile[]>("read_plugin_commands"),
|
||||
});
|
||||
|
||||
export const useWriteClaudeCommand = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -603,6 +638,7 @@ export const useWriteClaudeCommand = () => {
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.commandSaved"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-commands"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["plugin-commands"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
@@ -621,6 +657,7 @@ export const useDeleteClaudeCommand = () => {
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.commandDeleted"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-commands"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["plugin-commands"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
@@ -644,6 +681,7 @@ export const useToggleClaudeCommand = () => {
|
||||
onSuccess: () => {
|
||||
toast.success(i18n.t("toast.commandToggled"));
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-commands"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["plugin-commands"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
@@ -660,6 +698,12 @@ export const useClaudeAgents = () =>
|
||||
queryFn: () => invoke<CommandFile[]>("read_claude_agents"),
|
||||
});
|
||||
|
||||
export const usePluginAgents = () =>
|
||||
useQuery({
|
||||
queryKey: ["plugin-agents"],
|
||||
queryFn: () => invoke<PluginAgentFile[]>("read_plugin_agents"),
|
||||
});
|
||||
|
||||
export const useWriteClaudeAgent = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -674,6 +718,7 @@ export const useWriteClaudeAgent = () => {
|
||||
onSuccess: () => {
|
||||
toast.success("Agent saved successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-agents"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["plugin-agents"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
@@ -692,6 +737,7 @@ export const useDeleteClaudeAgent = () => {
|
||||
onSuccess: () => {
|
||||
toast.success("Agent deleted successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["claude-agents"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["plugin-agents"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
@@ -709,3 +755,63 @@ const rebuildTrayMenu = async () => {
|
||||
console.error("Failed to rebuild tray menu:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Plugin management types and hooks
|
||||
|
||||
export interface PluginPackages {
|
||||
hasAgents: boolean;
|
||||
hasSkills: boolean;
|
||||
hasCommands: boolean;
|
||||
hasMcp: boolean;
|
||||
}
|
||||
|
||||
export interface PluginInfo {
|
||||
name: string;
|
||||
scope: "user" | "local";
|
||||
version: string;
|
||||
projectPath?: string;
|
||||
enabled: boolean;
|
||||
packages: PluginPackages;
|
||||
installPath: string;
|
||||
installedAt: string;
|
||||
}
|
||||
|
||||
export const useInstalledPlugins = () =>
|
||||
useQuery({
|
||||
queryKey: ["installed-plugins"],
|
||||
queryFn: () => invoke<PluginInfo[]>("read_installed_plugins"),
|
||||
});
|
||||
|
||||
export const useTogglePlugin = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
pluginName,
|
||||
enabled,
|
||||
scope,
|
||||
projectPath,
|
||||
}: {
|
||||
pluginName: string;
|
||||
enabled: boolean;
|
||||
scope: string;
|
||||
projectPath?: string;
|
||||
}) =>
|
||||
invoke<void>("toggle_plugin", {
|
||||
pluginName,
|
||||
enabled,
|
||||
scope,
|
||||
projectPath,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("Plugin status updated successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["installed-plugins"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["mcp-servers-with-state"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to toggle plugin: ${errorMessage}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
+75
-73
@@ -1,7 +1,5 @@
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { yamlFrontmatter } from "@codemirror/lang-yaml";
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { BotIcon, PlusIcon, SaveIcon, TrashIcon } from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -11,6 +9,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,22 +22,57 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { codeMirrorBasicSetup, markdownExtensions } from "@/lib/codemirror-config";
|
||||
import {
|
||||
useClaudeAgents,
|
||||
useDeleteClaudeAgent,
|
||||
usePluginAgents,
|
||||
useWriteClaudeAgent,
|
||||
} from "@/lib/query";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
|
||||
type UnifiedAgent = {
|
||||
name: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
source: "user" | "plugin";
|
||||
pluginName?: string;
|
||||
pluginScope?: string;
|
||||
sourcePath: string;
|
||||
};
|
||||
|
||||
function AgentsPageContent() {
|
||||
const { t } = useTranslation();
|
||||
const { data: agents, isLoading, error } = useClaudeAgents();
|
||||
const { data: userAgents, isLoading: isLoadingUser, error: errorUser } = useClaudeAgents();
|
||||
const { data: pluginAgents, isLoading: isLoadingPlugin, error: errorPlugin } = usePluginAgents();
|
||||
const writeAgent = useWriteClaudeAgent();
|
||||
const deleteAgent = useDeleteClaudeAgent();
|
||||
const [agentEdits, setAgentEdits] = useState<Record<string, string>>({});
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
const isLoading = isLoadingUser || isLoadingPlugin;
|
||||
const error = errorUser || errorPlugin;
|
||||
|
||||
const agents: UnifiedAgent[] = [
|
||||
...(userAgents || []).map((agent): UnifiedAgent => ({
|
||||
name: agent.name,
|
||||
content: agent.content,
|
||||
exists: agent.exists,
|
||||
source: "user",
|
||||
sourcePath: `~/.claude/agents/${agent.name}.md`,
|
||||
})),
|
||||
...(pluginAgents || []).map((agent): UnifiedAgent => ({
|
||||
name: agent.name,
|
||||
content: agent.content,
|
||||
exists: agent.exists,
|
||||
source: "plugin",
|
||||
pluginName: agent.pluginName,
|
||||
pluginScope: agent.pluginScope,
|
||||
sourcePath: agent.sourcePath,
|
||||
})),
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
@@ -86,9 +120,9 @@ function AgentsPageContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
className="flex items-center justify-between sticky top-0 z-10 border-b p-3 bg-background"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
@@ -108,9 +142,7 @@ function AgentsPageContent() {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="">
|
||||
{t("agents.addAgentTitle")}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("agents.addAgentTitle")}</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-sm">
|
||||
{t("agents.addAgentDescription")}
|
||||
</DialogDescription>
|
||||
@@ -119,15 +151,15 @@ function AgentsPageContent() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="">
|
||||
{!agents || agents.length === 0 ? (
|
||||
<div>
|
||||
{agents.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t("agents.noAgents")}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="">
|
||||
<Accordion type="multiple" className="">
|
||||
<div>
|
||||
<Accordion type="multiple">
|
||||
{agents.map((agent) => (
|
||||
<AccordionItem
|
||||
key={agent.name}
|
||||
@@ -135,11 +167,25 @@ function AgentsPageContent() {
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<BotIcon size={12} />
|
||||
<span className="font-medium">{agent.name}</span>
|
||||
{agent.source === "user" ? (
|
||||
<Badge variant="default">
|
||||
{t("agents.sourceUser")}
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<Badge variant="outline">
|
||||
{agent.pluginName}
|
||||
</Badge>
|
||||
<Badge variant={agent.pluginScope === "user" ? "default" : "secondary"}>
|
||||
{agent.pluginScope === "user" ? t("plugins.scopeUser") : t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
{`~/.claude/agents/${agent.name}.md`}
|
||||
{agent.sourcePath}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
@@ -147,39 +193,15 @@ function AgentsPageContent() {
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={
|
||||
agentEdits[agent.name] !== undefined
|
||||
? agentEdits[agent.name]
|
||||
: agent.content
|
||||
}
|
||||
value={agentEdits[agent.name] ?? agent.content}
|
||||
height="180px"
|
||||
theme={codeMirrorTheme}
|
||||
onChange={(value) =>
|
||||
handleContentChange(agent.name, value)
|
||||
}
|
||||
placeholder={t("agents.contentPlaceholder")}
|
||||
extensions={[
|
||||
yamlFrontmatter({
|
||||
content: markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
extensions={markdownExtensions}
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
@@ -192,7 +214,7 @@ function AgentsPageContent() {
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} className="" />
|
||||
<SaveIcon size={14} />
|
||||
{writeAgent.isPending
|
||||
? t("agents.saving")
|
||||
: t("agents.save")}
|
||||
@@ -204,7 +226,7 @@ function AgentsPageContent() {
|
||||
onClick={() => handleDeleteAgent(agent.name)}
|
||||
disabled={deleteAgent.isPending}
|
||||
>
|
||||
<TrashIcon size={14} className="" />
|
||||
<TrashIcon size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,7 +258,11 @@ export function AgentsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function CreateAgentPanel({ onClose }: { onClose?: () => void }) {
|
||||
type CreateAgentPanelProps = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function CreateAgentPanel({ onClose }: CreateAgentPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [agentName, setAgentName] = useState("");
|
||||
const [agentContent, setAgentContent] = useState(`---
|
||||
@@ -257,7 +283,6 @@ the subagent should follow.`);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
const handleCreateAgent = async () => {
|
||||
// Validate agent name
|
||||
if (!agentName.trim()) {
|
||||
await message(t("agents.emptyNameError"), {
|
||||
title: t("agents.validationError"),
|
||||
@@ -266,9 +291,7 @@ the subagent should follow.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if agent already exists
|
||||
const exists = agents?.some((agent) => agent.name === agentName);
|
||||
if (exists) {
|
||||
if (agents?.some((a) => a.name === agentName)) {
|
||||
await message(t("agents.agentExistsError", { agentName }), {
|
||||
title: t("agents.agentExistsTitle"),
|
||||
kind: "info",
|
||||
@@ -276,7 +299,6 @@ the subagent should follow.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate content
|
||||
if (!agentContent.trim()) {
|
||||
await message(t("agents.emptyContentError"), {
|
||||
title: t("agents.validationError"),
|
||||
@@ -324,29 +346,9 @@ the subagent should follow.`);
|
||||
onChange={(value) => setAgentContent(value)}
|
||||
height="200px"
|
||||
theme={codeMirrorTheme}
|
||||
placeholder={t("agents.contentPlaceholder")}
|
||||
extensions={[
|
||||
yamlFrontmatter({
|
||||
content: markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
placeholder={t("agents.contentPlaceholder")}
|
||||
extensions={markdownExtensions}
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+77
-73
@@ -1,7 +1,5 @@
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { yamlFrontmatter } from "@codemirror/lang-yaml";
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { PlusIcon, SaveIcon, TerminalIcon, TrashIcon } from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -24,17 +22,31 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { codeMirrorBasicSetup, markdownExtensions } from "@/lib/codemirror-config";
|
||||
import {
|
||||
useClaudeCommands,
|
||||
useDeleteClaudeCommand,
|
||||
usePluginCommands,
|
||||
useToggleClaudeCommand,
|
||||
useWriteClaudeCommand,
|
||||
} from "@/lib/query";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
|
||||
type UnifiedCommand = {
|
||||
name: string;
|
||||
content: string;
|
||||
exists: boolean;
|
||||
disabled: boolean;
|
||||
source: "user" | "plugin";
|
||||
pluginName?: string;
|
||||
pluginScope?: string;
|
||||
sourcePath: string;
|
||||
};
|
||||
|
||||
function CommandsPageContent() {
|
||||
const { t } = useTranslation();
|
||||
const { data: commands, isLoading, error } = useClaudeCommands();
|
||||
const { data: userCommands, isLoading: isLoadingUser, error: errorUser } = useClaudeCommands();
|
||||
const { data: pluginCommands, isLoading: isLoadingPlugin, error: errorPlugin } = usePluginCommands();
|
||||
const writeCommand = useWriteClaudeCommand();
|
||||
const deleteCommand = useDeleteClaudeCommand();
|
||||
const toggleCommand = useToggleClaudeCommand();
|
||||
@@ -42,6 +54,30 @@ function CommandsPageContent() {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
const isLoading = isLoadingUser || isLoadingPlugin;
|
||||
const error = errorUser || errorPlugin;
|
||||
|
||||
const commands: UnifiedCommand[] = [
|
||||
...(userCommands || []).map((cmd): UnifiedCommand => ({
|
||||
name: cmd.name,
|
||||
content: cmd.content,
|
||||
exists: cmd.exists,
|
||||
disabled: cmd.disabled,
|
||||
source: "user",
|
||||
sourcePath: `~/.claude/commands/${cmd.name}.md${cmd.disabled ? '.disabled' : ''}`,
|
||||
})),
|
||||
...(pluginCommands || []).map((cmd): UnifiedCommand => ({
|
||||
name: cmd.name,
|
||||
content: cmd.content,
|
||||
exists: cmd.exists,
|
||||
disabled: cmd.disabled,
|
||||
source: "plugin",
|
||||
pluginName: cmd.pluginName,
|
||||
pluginScope: cmd.pluginScope,
|
||||
sourcePath: cmd.sourcePath,
|
||||
})),
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
@@ -93,9 +129,9 @@ function CommandsPageContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
className="flex items-center justify-between sticky top-0 z-10 border-b p-3 bg-background"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
@@ -115,9 +151,7 @@ function CommandsPageContent() {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="">
|
||||
{t("commands.addCommandTitle")}
|
||||
</DialogTitle>
|
||||
<DialogTitle>{t("commands.addCommandTitle")}</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground text-sm">
|
||||
{t("commands.addCommandDescription")}
|
||||
</DialogDescription>
|
||||
@@ -126,15 +160,15 @@ function CommandsPageContent() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="">
|
||||
{!commands || commands.length === 0 ? (
|
||||
<div>
|
||||
{commands.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t("commands.noCommands")}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="">
|
||||
<Accordion type="multiple" className="">
|
||||
<div>
|
||||
<Accordion type="multiple">
|
||||
{commands.map((command) => (
|
||||
<AccordionItem
|
||||
key={command.name}
|
||||
@@ -142,14 +176,28 @@ function CommandsPageContent() {
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<TerminalIcon size={12} />
|
||||
<span className="font-medium">{command.name}</span>
|
||||
<Badge variant={command.disabled ? "secondary" : "success"}>
|
||||
{command.disabled ? t("commands.disabled") : t("commands.enabled")}
|
||||
</Badge>
|
||||
{command.source === "user" ? (
|
||||
<Badge variant="default">
|
||||
{t("commands.sourceUser")}
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
<Badge variant="outline">
|
||||
{command.pluginName}
|
||||
</Badge>
|
||||
<Badge variant={command.pluginScope === "user" ? "default" : "secondary"}>
|
||||
{command.pluginScope === "user" ? t("plugins.scopeUser") : t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
{`~/.claude/commands/${command.name}.md${command.disabled ? '.disabled' : ''}`}
|
||||
{command.sourcePath}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
@@ -157,39 +205,15 @@ function CommandsPageContent() {
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
value={
|
||||
commandEdits[command.name] !== undefined
|
||||
? commandEdits[command.name]
|
||||
: command.content
|
||||
}
|
||||
value={commandEdits[command.name] ?? command.content}
|
||||
height="180px"
|
||||
theme={codeMirrorTheme}
|
||||
onChange={(value) =>
|
||||
handleContentChange(command.name, value)
|
||||
}
|
||||
placeholder={t("commands.contentPlaceholder")}
|
||||
extensions={[
|
||||
yamlFrontmatter({
|
||||
content: markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
extensions={markdownExtensions}
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
@@ -203,7 +227,7 @@ function CommandsPageContent() {
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} className="" />
|
||||
<SaveIcon size={14} />
|
||||
{writeCommand.isPending
|
||||
? t("commands.saving")
|
||||
: t("commands.save")}
|
||||
@@ -225,7 +249,7 @@ function CommandsPageContent() {
|
||||
onClick={() => handleDeleteCommand(command.name)}
|
||||
disabled={deleteCommand.isPending}
|
||||
>
|
||||
<TrashIcon size={14} className="" />
|
||||
<TrashIcon size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,7 +281,11 @@ export function CommandsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function CreateCommandPanel({ onClose }: { onClose?: () => void }) {
|
||||
type CreateCommandPanelProps = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function CreateCommandPanel({ onClose }: CreateCommandPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [commandName, setCommandName] = useState("");
|
||||
const [commandContent, setCommandContent] = useState("");
|
||||
@@ -266,7 +294,6 @@ function CreateCommandPanel({ onClose }: { onClose?: () => void }) {
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
const handleCreateCommand = async () => {
|
||||
// Validate command name
|
||||
if (!commandName.trim()) {
|
||||
await message(t("commands.emptyNameError"), {
|
||||
title: t("commands.validationError"),
|
||||
@@ -275,9 +302,7 @@ function CreateCommandPanel({ onClose }: { onClose?: () => void }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if command already exists
|
||||
const exists = commands && commands.some((cmd) => cmd.name === commandName);
|
||||
if (exists) {
|
||||
if (commands?.some((cmd) => cmd.name === commandName)) {
|
||||
await message(t("commands.commandExistsError", { commandName }), {
|
||||
title: t("commands.commandExistsTitle"),
|
||||
kind: "info",
|
||||
@@ -285,7 +310,6 @@ function CreateCommandPanel({ onClose }: { onClose?: () => void }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate content
|
||||
if (!commandContent.trim()) {
|
||||
await message(t("commands.emptyContentError"), {
|
||||
title: t("commands.validationError"),
|
||||
@@ -329,29 +353,9 @@ function CreateCommandPanel({ onClose }: { onClose?: () => void }) {
|
||||
onChange={(value) => setCommandContent(value)}
|
||||
height="200px"
|
||||
theme={codeMirrorTheme}
|
||||
placeholder={t("commands.contentPlaceholder")}
|
||||
extensions={[
|
||||
yamlFrontmatter({
|
||||
content: markdown({
|
||||
base: markdownLanguage,
|
||||
}),
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
basicSetup={{
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: true,
|
||||
foldGutter: false,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectionMatches: true,
|
||||
searchKeymap: false,
|
||||
}}
|
||||
placeholder={t("commands.contentPlaceholder")}
|
||||
extensions={markdownExtensions}
|
||||
basicSetup={codeMirrorBasicSetup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+177
-80
@@ -3,7 +3,10 @@ import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import {
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
ExternalLinkIcon,
|
||||
FolderIcon,
|
||||
HammerIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
@@ -11,7 +14,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { match } from "ts-pattern";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -20,6 +22,14 @@ import {
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -28,31 +38,40 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { builtInMcpServers } from "@/lib/builtInMCP";
|
||||
import {
|
||||
type McpServerState,
|
||||
useAddGlobalMcpServer,
|
||||
useClaudeProjects,
|
||||
useDeleteGlobalMcpServer,
|
||||
useGetMcpServersWithState,
|
||||
useToggleMcpServer,
|
||||
useUpdateGlobalMcpServer,
|
||||
} from "@/lib/query";
|
||||
import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function MCPPageContent() {
|
||||
const { t } = useTranslation();
|
||||
const { data: mcpServersWithState } = useGetMcpServersWithState();
|
||||
const { data: projects } = useClaudeProjects();
|
||||
const [currentCwd, setCurrentCwd] = useState<string | undefined>(undefined);
|
||||
const { data: mcpServersWithState } = useGetMcpServersWithState(currentCwd);
|
||||
const updateMcpServer = useUpdateGlobalMcpServer();
|
||||
const deleteMcpServer = useDeleteGlobalMcpServer();
|
||||
const toggleMcpServer = useToggleMcpServer();
|
||||
const toggleMcpServer = useToggleMcpServer(currentCwd);
|
||||
const [serverConfigs, setServerConfigs] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [comboboxOpen, setComboboxOpen] = useState(false);
|
||||
const codeMirrorTheme = useCodeMirrorTheme();
|
||||
|
||||
// Helper functions for badge display
|
||||
const getBadgeVariant = (state: string) => {
|
||||
function getBadgeVariant(state: string) {
|
||||
switch (state) {
|
||||
case "enabled":
|
||||
return "success";
|
||||
@@ -60,12 +79,12 @@ function MCPPageContent() {
|
||||
return "outline";
|
||||
case "runtime-disabled":
|
||||
return "secondary";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
};
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
}
|
||||
|
||||
const getBadgeLabel = (state: string) => {
|
||||
function getBadgeLabel(state: string) {
|
||||
switch (state) {
|
||||
case "enabled":
|
||||
return t("mcp.enabled");
|
||||
@@ -73,18 +92,17 @@ function MCPPageContent() {
|
||||
return t("mcp.disabled");
|
||||
case "runtime-disabled":
|
||||
return t("mcp.runtimeDisabled");
|
||||
default:
|
||||
return t("mcp.disabled");
|
||||
}
|
||||
};
|
||||
default:
|
||||
return t("mcp.disabled");
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleServer = async (server: McpServerState) => {
|
||||
if (!server.controllable) return;
|
||||
|
||||
const currentlyEnabled = server.state === "enabled" || server.state === "runtime-disabled";
|
||||
toggleMcpServer.mutate({
|
||||
serverName: server.name,
|
||||
enabled: !currentlyEnabled,
|
||||
sourceType: server.sourceType,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -114,7 +132,6 @@ function MCPPageContent() {
|
||||
};
|
||||
|
||||
const handleDeleteServer = async (serverName: string) => {
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(t("mcp.deleteServerConfirm", { serverName }), {
|
||||
title: t("mcp.deleteServerTitle"),
|
||||
kind: "warning",
|
||||
@@ -125,18 +142,23 @@ function MCPPageContent() {
|
||||
}
|
||||
};
|
||||
|
||||
const formatConfigForDisplay = (server: McpServerState): string => {
|
||||
function formatConfigForDisplay(server: McpServerState): string {
|
||||
return JSON.stringify(server.config, null, 2);
|
||||
};
|
||||
}
|
||||
|
||||
function getPluginNameFromDefinedIn(definedIn: string): string {
|
||||
const match = definedIn.match(/Plugin: (.+?) \(/);
|
||||
return match ? match[1] : "Plugin";
|
||||
}
|
||||
|
||||
const sortedServers = [...mcpServersWithState].sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
|
||||
className="flex items-center justify-between sticky top-0 z-10 border-b p-3 bg-background"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
@@ -166,24 +188,97 @@ function MCPPageContent() {
|
||||
<div className="py-3 mt-3">
|
||||
<MCPCreatePanel onClose={() => setIsDialogOpen(false)} />
|
||||
</div>
|
||||
{/* <div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="">
|
||||
{projects && projects.length > 0 && (
|
||||
<div className="p-3 border-b bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("mcp.projectScope")}:
|
||||
</span>
|
||||
<Popover open={comboboxOpen} onOpenChange={setComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={comboboxOpen}
|
||||
className="justify-between min-w-[250px]"
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
{currentCwd ? (
|
||||
<>
|
||||
<FolderIcon className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{currentCwd}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{t("mcp.globalScope")}</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t("mcp.searchProject")}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("mcp.noProjectFound")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="global"
|
||||
onSelect={() => {
|
||||
setCurrentCwd(undefined);
|
||||
setComboboxOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{t("mcp.globalScope")}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
!currentCwd ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
{projects.map((project) => (
|
||||
<CommandItem
|
||||
key={project.path}
|
||||
value={project.path}
|
||||
onSelect={() => {
|
||||
setCurrentCwd(project.path);
|
||||
setComboboxOpen(false);
|
||||
}}
|
||||
>
|
||||
<FolderIcon className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">{project.path}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
currentCwd === project.path
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{sortedServers.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t("mcp.noServersConfigured")}
|
||||
</div>
|
||||
) : (
|
||||
<Accordion type="multiple" className="">
|
||||
<Accordion type="multiple">
|
||||
{sortedServers.map((server) => (
|
||||
<AccordionItem
|
||||
key={server.name}
|
||||
@@ -191,15 +286,26 @@ function MCPPageContent() {
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<HammerIcon size={12} />
|
||||
<span className="font-medium">{server.name}</span>
|
||||
<Badge variant={getBadgeVariant(server.state)}>
|
||||
{getBadgeLabel(server.state)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.sourceType}
|
||||
</Badge>
|
||||
{server.sourceType === "plugin" ? (
|
||||
<>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getPluginNameFromDefinedIn(server.definedIn)}
|
||||
</Badge>
|
||||
<Badge variant={server.scope === "plugin-user" ? "default" : "secondary"} className="text-xs">
|
||||
{server.scope === "plugin-user" ? t("plugins.scopeUser") : t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.sourceType}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
@@ -219,7 +325,7 @@ function MCPPageContent() {
|
||||
placeholder="Enter MCP server configuration as JSON"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between bg-card">
|
||||
<div className="flex justify-between bg-card">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -227,7 +333,7 @@ function MCPPageContent() {
|
||||
disabled={updateMcpServer.isPending}
|
||||
size="sm"
|
||||
>
|
||||
<SaveIcon size={14} className="" />
|
||||
<SaveIcon size={14} />
|
||||
{updateMcpServer.isPending
|
||||
? t("mcp.saving")
|
||||
: t("mcp.save")}
|
||||
@@ -239,7 +345,7 @@ function MCPPageContent() {
|
||||
onClick={() => handleDeleteServer(server.name)}
|
||||
disabled={deleteMcpServer.isPending}
|
||||
>
|
||||
<TrashIcon size={14} className="" />
|
||||
<TrashIcon size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -249,12 +355,7 @@ function MCPPageContent() {
|
||||
}
|
||||
size="sm"
|
||||
onClick={() => handleToggleServer(server)}
|
||||
disabled={!server.controllable || toggleMcpServer.isPending}
|
||||
title={
|
||||
!server.controllable
|
||||
? t("mcp.notControllable")
|
||||
: undefined
|
||||
}
|
||||
disabled={toggleMcpServer.isPending}
|
||||
>
|
||||
{server.state === "enabled"
|
||||
? t("mcp.disable")
|
||||
@@ -273,11 +374,13 @@ function MCPPageContent() {
|
||||
}
|
||||
|
||||
export function MCPPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">Loading MCP servers...</div>
|
||||
<div className="text-center">{t("loading")}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -286,14 +389,18 @@ export function MCPPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function MCPCreatePanel({ onClose }: { onClose?: () => void }) {
|
||||
type MCPCreatePanelProps = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function MCPCreatePanel({ onClose }: MCPCreatePanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [currentTab, setCurrentTab] = useState<"recommend" | "manual">(
|
||||
"recommend",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<div className="flex mb-3 gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -313,19 +420,20 @@ function MCPCreatePanel({ onClose }: { onClose?: () => void }) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{match(currentTab)
|
||||
.with("recommend", () => {
|
||||
return <RecommendMCPPanel onClose={onClose} />;
|
||||
})
|
||||
.with("manual", () => {
|
||||
return <CustomMCPPanel onClose={onClose} />;
|
||||
})
|
||||
.exhaustive()}
|
||||
{currentTab === "recommend" ? (
|
||||
<RecommendMCPPanel onClose={onClose} />
|
||||
) : (
|
||||
<CustomMCPPanel onClose={onClose} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
type RecommendMCPPanelProps = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function RecommendMCPPanel({ onClose }: RecommendMCPPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const addMcpServer = useAddGlobalMcpServer();
|
||||
const { data: mcpServersWithState } = useGetMcpServersWithState();
|
||||
@@ -334,9 +442,8 @@ function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
mcpServer: (typeof builtInMcpServers)[0],
|
||||
) => {
|
||||
try {
|
||||
// Check if MCP server already exists using cached data
|
||||
const exists =
|
||||
mcpServersWithState && mcpServersWithState.some(s => s.name === mcpServer.name);
|
||||
mcpServersWithState?.some((s) => s.name === mcpServer.name);
|
||||
|
||||
if (exists) {
|
||||
await message(
|
||||
@@ -349,14 +456,12 @@ function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(
|
||||
t("mcp.addServerConfirm", { serverName: mcpServer.name }),
|
||||
{ title: t("mcp.addServerTitle"), kind: "info" },
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
// Parse the prefill JSON to get the config object
|
||||
const configObject = JSON.parse(`{${mcpServer.prefill}}`);
|
||||
|
||||
addMcpServer.mutate(
|
||||
@@ -366,7 +471,6 @@ function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Close dialog after successful addition
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
@@ -375,7 +479,7 @@ function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
} catch (error) {
|
||||
console.error("Failed to add MCP server:", error);
|
||||
await message(t("mcp.addServerError"), {
|
||||
title: "Error",
|
||||
title: t("error.title"),
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
@@ -402,15 +506,10 @@ function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
{t("mcp.source")}
|
||||
</a>
|
||||
</div>
|
||||
<div></div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{mcpServer.description}
|
||||
</p>
|
||||
{/* <Button size="sm" variant="outline" className="w-full text-sm">
|
||||
<PlusIcon />
|
||||
添加
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -418,7 +517,11 @@ function RecommendMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CustomMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
type CustomMCPPanelProps = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function CustomMCPPanel({ onClose }: CustomMCPPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [customConfig, setCustomConfig] = useState("");
|
||||
const addMcpServer = useAddGlobalMcpServer();
|
||||
@@ -427,11 +530,10 @@ function CustomMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
|
||||
const handleAddCustomMcpServer = async () => {
|
||||
try {
|
||||
// Validate JSON format
|
||||
let configObject;
|
||||
try {
|
||||
configObject = JSON.parse(customConfig);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
await message(t("mcp.addCustomServerError"), {
|
||||
title: t("mcp.invalidJsonTitle"),
|
||||
kind: "error",
|
||||
@@ -439,10 +541,9 @@ function CustomMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's an object with at least one server
|
||||
if (typeof configObject !== "object" || configObject === null) {
|
||||
await message(t("mcp.invalidConfigError"), {
|
||||
title: "Invalid Configuration",
|
||||
title: t("mcp.invalidJsonTitle"),
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
@@ -451,14 +552,13 @@ function CustomMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
const serverNames = Object.keys(configObject);
|
||||
if (serverNames.length === 0) {
|
||||
await message(t("mcp.noServersError"), {
|
||||
title: "Invalid Configuration",
|
||||
title: t("mcp.invalidJsonTitle"),
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate server names
|
||||
const existingNames = mcpServersWithState ? mcpServersWithState.map(s => s.name) : [];
|
||||
const existingNames = mcpServersWithState?.map((s) => s.name) ?? [];
|
||||
const duplicateNames = serverNames.filter((name) =>
|
||||
existingNames.includes(name),
|
||||
);
|
||||
@@ -476,14 +576,12 @@ function CustomMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
const confirmed = await ask(
|
||||
t("mcp.addCustomServersConfirm", { count: serverNames.length }),
|
||||
{ title: t("mcp.addCustomServersTitle"), kind: "info" },
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
// Add each server
|
||||
for (const [serverName, serverConfig] of Object.entries(configObject)) {
|
||||
addMcpServer.mutate({
|
||||
serverName,
|
||||
@@ -491,21 +589,20 @@ function CustomMCPPanel({ onClose }: { onClose?: () => void }) {
|
||||
});
|
||||
}
|
||||
|
||||
// Clear input and close dialog
|
||||
setCustomConfig("");
|
||||
onClose?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add custom MCP servers:", error);
|
||||
await message(t("mcp.addServerError"), {
|
||||
title: "Error",
|
||||
title: t("error.title"),
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg overflow-hidden border">
|
||||
<CodeMirror
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { BotIcon, PackageIcon, TerminalIcon } from "lucide-react";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useInstalledPlugins, useTogglePlugin } from "@/lib/query";
|
||||
|
||||
function PluginsPageContent() {
|
||||
const { t } = useTranslation();
|
||||
const { data: plugins, isLoading, error } = useInstalledPlugins();
|
||||
const togglePlugin = useTogglePlugin();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">{t("loading")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center text-red-500">
|
||||
{t("plugins.error", { error: error.message })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sortedPlugins = [...(plugins ?? [])].sort((a, b) => {
|
||||
const aDate = a.installedAt ?? "";
|
||||
const bDate = b.installedAt ?? "";
|
||||
return bDate.localeCompare(aDate);
|
||||
});
|
||||
|
||||
const handleTogglePlugin = async (
|
||||
pluginName: string,
|
||||
currentEnabled: boolean,
|
||||
scope: string,
|
||||
projectPath?: string,
|
||||
) => {
|
||||
if (scope === "local" && !projectPath) {
|
||||
await message(t("plugins.toggleLocalError", { pluginName }), {
|
||||
title: t("plugins.validationError"),
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
togglePlugin.mutate({
|
||||
pluginName,
|
||||
enabled: !currentEnabled,
|
||||
scope,
|
||||
projectPath,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center justify-between sticky top-0 z-10 border-b p-3 bg-background"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div data-tauri-drag-region>
|
||||
<h3 className="font-bold" data-tauri-drag-region>
|
||||
{t("plugins.title")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
|
||||
{t("plugins.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{!plugins || plugins.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t("plugins.noPlugins")}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full">
|
||||
<div>
|
||||
<Accordion type="multiple">
|
||||
{sortedPlugins.map((plugin) => (
|
||||
<AccordionItem
|
||||
key={`${plugin.name}-${plugin.scope}`}
|
||||
value={`${plugin.name}-${plugin.scope}`}
|
||||
className="bg-card"
|
||||
>
|
||||
<AccordionTrigger className="hover:no-underline px-4 py-2 bg-card hover:bg-accent duration-150">
|
||||
<div className="flex flex-col items-start gap-1 w-full">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<PackageIcon size={12} />
|
||||
<span className="font-medium">{plugin.name}</span>
|
||||
<Badge
|
||||
variant={
|
||||
plugin.scope === "user" ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{plugin.scope === "user"
|
||||
? t("plugins.scopeUser")
|
||||
: t("plugins.scopeLocal")}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={plugin.enabled ? "success" : "outline"}
|
||||
>
|
||||
{plugin.enabled
|
||||
? t("plugins.enabled")
|
||||
: t("plugins.disabled")}
|
||||
</Badge>
|
||||
{plugin.scope === "local" && plugin.projectPath ? (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{plugin.projectPath}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
v{plugin.version}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-3">
|
||||
<div className="px-3 pt-3 space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("plugins.packages")}:
|
||||
</span>
|
||||
{plugin.packages.hasAgents && (
|
||||
<Badge variant="secondary">
|
||||
<BotIcon size={12} className="mr-1" />
|
||||
{t("plugins.packageAgents")}
|
||||
</Badge>
|
||||
)}
|
||||
{plugin.packages.hasSkills && (
|
||||
<Badge variant="secondary">
|
||||
{t("plugins.packageSkills")}
|
||||
</Badge>
|
||||
)}
|
||||
{plugin.packages.hasCommands && (
|
||||
<Badge variant="secondary">
|
||||
<TerminalIcon size={12} className="mr-1" />
|
||||
{t("plugins.packageCommands")}
|
||||
</Badge>
|
||||
)}
|
||||
{plugin.packages.hasMcp && (
|
||||
<Badge variant="secondary">
|
||||
{t("plugins.packageMcp")}
|
||||
</Badge>
|
||||
)}
|
||||
{!plugin.packages.hasAgents &&
|
||||
!plugin.packages.hasSkills &&
|
||||
!plugin.packages.hasCommands &&
|
||||
!plugin.packages.hasMcp && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("plugins.none")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between bg-card">
|
||||
<Button
|
||||
variant={plugin.enabled ? "outline" : "default"}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleTogglePlugin(
|
||||
plugin.name,
|
||||
plugin.enabled,
|
||||
plugin.scope,
|
||||
plugin.projectPath,
|
||||
)
|
||||
}
|
||||
disabled={togglePlugin.isPending}
|
||||
>
|
||||
{plugin.enabled
|
||||
? t("plugins.disable")
|
||||
: t("plugins.enable")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">{t("loading")}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PluginsPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ConfigSwitcherPage } from "./pages/ConfigSwitcherPage";
|
||||
import { MCPPage } from "./pages/MCPPage";
|
||||
import { MemoryPage } from "./pages/MemoryPage";
|
||||
import { NotificationPage } from "./pages/NotificationPage";
|
||||
import { PluginsPage } from "./pages/PluginsPage";
|
||||
import { Detail } from "./pages/projects/Detail";
|
||||
import { ProjectsLayout } from "./pages/projects/Layout";
|
||||
import { List } from "./pages/projects/List";
|
||||
@@ -95,6 +96,14 @@ const router = createBrowserRouter([
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "plugins",
|
||||
element: (
|
||||
<RouteWrapper>
|
||||
<PluginsPage />
|
||||
</RouteWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "projects",
|
||||
element: (
|
||||
|
||||
Reference in New Issue
Block a user