From 69fd4e3aba90c5da9c5c4809ad4d430953a580e6 Mon Sep 17 00:00:00 2001 From: Randy Lu Date: Sun, 2 Nov 2025 21:46:04 +0800 Subject: [PATCH] sub agent --- .claude/settings.local.json | 42 ++-- .claude/skills/translate/SKILL.md | 13 ++ .gitignore | 2 + changelog/0.3.0.md | 1 + package.json | 4 +- pnpm-lock.yaml | 21 ++ src-tauri/src/commands.rs | 81 +++++++ src-tauri/src/lib.rs | 5 +- src-tauri/tauri.conf.json | 2 +- src/components/Layout.tsx | 7 + src/components/ui/drawer.tsx | 116 ++++++++++ src/i18n/locales/en.json | 22 ++ src/i18n/locales/fr.json | 22 ++ src/i18n/locales/ja.json | 22 ++ src/i18n/locales/zh.json | 22 ++ src/lib/query.ts | 48 ++++ src/pages/AgentsPage.tsx | 366 ++++++++++++++++++++++++++++++ src/pages/CommandsPage.tsx | 13 +- src/pages/MemoryPage.tsx | 7 +- src/router.tsx | 9 + 20 files changed, 797 insertions(+), 28 deletions(-) create mode 100644 .claude/skills/translate/SKILL.md create mode 100644 changelog/0.3.0.md create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/pages/AgentsPage.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d4b90d0..37b39f2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,21 +1,25 @@ { - "permissions": { - "allow": [ - "Bash(pnpm tsc:*)", - "mcp__exa__get_code_context_exa", - "mcp__context7__resolve-library-id", - "mcp__context7__get-library-docs", - "mcp__exa__web_search_exa", - "Bash(pnpm dlx shadcn@latest add native-select)", - "mcp__shadcn__get_project_registries", - "mcp__shadcn__search_items_in_registries", - "mcp__shadcn__get_add_command_for_items", - "Bash(pnpm dlx shadcn@latest add:*)", - "Bash(pnpm exec biome check:*)" - ], - "deny": [], - "ask": [] - }, - "enableAllProjectMcpServers": true, - "enabledMcpjsonServers": ["shadcn"] + "permissions": { + "allow": [ + "Bash(pnpm tsc:*)", + "mcp__exa__get_code_context_exa", + "mcp__context7__resolve-library-id", + "mcp__context7__get-library-docs", + "mcp__exa__web_search_exa", + "Bash(pnpm dlx shadcn@latest add native-select)", + "mcp__shadcn__get_project_registries", + "mcp__shadcn__search_items_in_registries", + "mcp__shadcn__get_add_command_for_items", + "Bash(pnpm dlx shadcn@latest add:*)", + "Bash(pnpm exec biome check:*)", + "Bash(pnpm install:*)", + "Bash(pnpm search:*)" + ], + "deny": [], + "ask": [] + }, + "enableAllProjectMcpServers": true, + "enabledMcpjsonServers": [ + "shadcn" + ] } diff --git a/.claude/skills/translate/SKILL.md b/.claude/skills/translate/SKILL.md new file mode 100644 index 0000000..5452235 --- /dev/null +++ b/.claude/skills/translate/SKILL.md @@ -0,0 +1,13 @@ +--- +name: translate skill +description: Use this skill when you are doing localization and translation work. +--- + +## Rules + +- Only localize the existing locales. Do not create new locales. +- Do not translate the following terms: + - Agent + - Agents + - Sub Agent + - Sub Agents \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..30164c5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +target/ \ No newline at end of file diff --git a/changelog/0.3.0.md b/changelog/0.3.0.md new file mode 100644 index 0000000..6e9475e --- /dev/null +++ b/changelog/0.3.0.md @@ -0,0 +1 @@ +- Support manage sub agents \ No newline at end of file diff --git a/package.json b/package.json index addf0de..4e745c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cc-mate", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", @@ -13,6 +13,7 @@ "@codemirror/fold": "^0.19.4", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-markdown": "^6.4.0", + "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.11.3", "@codemirror/language-data": "^6.5.1", "@hookform/resolvers": "^5.2.2", @@ -63,6 +64,7 @@ "tailwind-merge": "^3.3.1", "tailwind-variants": "^3.1.1", "ts-pattern": "^5.8.0", + "vaul": "^1.1.2", "zod": "^4.1.11" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b42a7e8..664507e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@codemirror/lang-markdown': specifier: ^6.4.0 version: 6.4.0 + '@codemirror/lang-yaml': + specifier: ^6.1.2 + version: 6.1.2 '@codemirror/language': specifier: ^6.11.3 version: 6.11.3 @@ -167,6 +170,9 @@ importers: ts-pattern: specifier: ^5.8.0 version: 5.8.0 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) zod: specifier: ^4.1.11 version: 4.1.11 @@ -5228,6 +5234,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -11534,6 +11546,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 27dacf9..d82ae6f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1985,3 +1985,84 @@ pub async fn delete_claude_command(command_name: String) -> Result<(), String> { Ok(()) } + +// Agent management functions + +#[derive(serde::Serialize, serde::Deserialize, Debug)] +pub struct AgentFile { + pub name: String, + pub content: String, + pub exists: bool, +} + +#[tauri::command] +pub async fn read_claude_agents() -> Result, String> { + let home_dir = dirs::home_dir().ok_or("Could not find home directory")?; + let agents_dir = home_dir.join(".claude/agents"); + + if !agents_dir.exists() { + return Ok(vec![]); + } + + let mut agent_files = Vec::new(); + + // Read all .md files in the agents directory + let entries = std::fs::read_dir(&agents_dir) + .map_err(|e| format!("Failed to read agents directory: {}", e))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; + let path = entry.path(); + + if path.is_file() && path.extension().map(|ext| ext == "md").unwrap_or(false) { + let file_name = path.file_stem() + .and_then(|name| name.to_str()) + .unwrap_or("unknown") + .to_string(); + + let content = std::fs::read_to_string(&path) + .map_err(|e| format!("Failed to read agent file {}: {}", path.display(), e))?; + + agent_files.push(AgentFile { + name: file_name, + content, + exists: true, + }); + } + } + + // Sort agents alphabetically by name + agent_files.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(agent_files) +} + +#[tauri::command] +pub async fn write_claude_agent(agent_name: String, content: String) -> Result<(), String> { + let home_dir = dirs::home_dir().ok_or("Could not find home directory")?; + let agents_dir = home_dir.join(".claude/agents"); + let agent_file_path = agents_dir.join(format!("{}.md", agent_name)); + + // Ensure .claude/agents directory exists + std::fs::create_dir_all(&agents_dir) + .map_err(|e| format!("Failed to create .claude/agents directory: {}", e))?; + + std::fs::write(&agent_file_path, content) + .map_err(|e| format!("Failed to write agent file: {}", e))?; + + Ok(()) +} + +#[tauri::command] +pub async fn delete_claude_agent(agent_name: String) -> Result<(), String> { + let home_dir = dirs::home_dir().ok_or("Could not find home directory")?; + let agents_dir = home_dir.join(".claude/agents"); + let agent_file_path = agents_dir.join(format!("{}.md", agent_name)); + + if agent_file_path.exists() { + std::fs::remove_file(&agent_file_path) + .map_err(|e| format!("Failed to delete agent file: {}", e))?; + } + + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index abb2c35..2e60e7d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -202,7 +202,10 @@ pub fn run() { remove_claude_code_hook, read_claude_commands, write_claude_command, - delete_claude_command + delete_claude_command, + read_claude_agents, + write_claude_agent, + delete_claude_agent ]) .on_window_event(|window, event| { #[cfg(target_os = "macos")] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index de33bff..647097a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CC Mate", - "version": "0.2.9", + "version": "0.3.0", "identifier": "org.randynamic.ccconfig", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index c13c216..f7787e2 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,12 +1,14 @@ import { ActivityIcon, BellIcon, + BotIcon, BrainIcon, CpuIcon, FileJsonIcon, FolderIcon, SettingsIcon, TerminalIcon, + UserIcon, } from "lucide-react"; import type React from "react"; import { useTranslation } from "react-i18next"; @@ -36,6 +38,11 @@ export function Layout() { icon: CpuIcon, label: t("navigation.mcp"), }, + { + to: "/agents", + icon: BotIcon, + label: "Agents", + }, { to: "/memory", icon: BrainIcon, diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..c17b0cc --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3dcc336..903a698 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -147,6 +147,28 @@ "toast.commandSaveFailed": "Failed to save command: {{error}}", "toast.commandDeleted": "Command deleted successfully", "toast.commandDeleteFailed": "Failed to delete command: {{error}}", + "agents.title": "Agents", + "agents.description": "Manage your Claude Code sub agents", + "agents.noAgents": "No sub agents found. Click \"Add Agent\" to create one.", + "agents.error": "Error loading agents: {{error}}", + "agents.addAgent": "Add Agent", + "agents.addAgentTitle": "Add Agent", + "agents.addAgentDescription": "Create a new sub agent for Claude Code", + "agents.agentName": "Agent Name", + "agents.agentNamePlaceholder": "e.g., code-reviewer", + "agents.agentContent": "Agent Content", + "agents.contentPlaceholder": "Enter your agent instructions...", + "agents.save": "Save", + "agents.saving": "Saving...", + "agents.create": "Create", + "agents.creating": "Creating...", + "agents.deleteConfirm": "Are you sure you want to delete the agent \"{{agentName}}\"? This action cannot be undone.", + "agents.deleteTitle": "Delete Agent", + "agents.emptyNameError": "Agent name cannot be empty", + "agents.emptyContentError": "Agent content cannot be empty", + "agents.validationError": "Validation Error", + "agents.agentExistsError": "Agent \"{{agentName}}\" already exists", + "agents.agentExistsTitle": "Agent Exists", "projects.title": "Projects", "projects.detail.editor": "Project Configuration Editor", "projects.detail.projectEditor": "Project Editor", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index d1c2aa3..543dc31 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -147,6 +147,28 @@ "toast.commandSaveFailed": "Échec de la sauvegarde de la commande : {{error}}", "toast.commandDeleted": "Commande supprimée avec succès", "toast.commandDeleteFailed": "Échec de la suppression de la commande : {{error}}", + "agents.title": "Agent", + "agents.description": "Gérer vos sub agent Claude Code", + "agents.noAgents": "Aucun sub agent trouvé. Cliquez sur \"Add Agent\" pour en créer un.", + "agents.error": "Erreur lors du chargement des agents : {{error}}", + "agents.addAgent": "Add Agent", + "agents.addAgentTitle": "Add Agent", + "agents.addAgentDescription": "Créer un nouveau sub agent pour Claude Code", + "agents.agentName": "Agent nom", + "agents.agentNamePlaceholder": "ex: code-reviewer", + "agents.agentContent": "Agent contenu", + "agents.contentPlaceholder": "Entrez vos instructions d'agent...", + "agents.save": "Sauvegarder", + "agents.saving": "Sauvegarde...", + "agents.create": "Créer", + "agents.creating": "Création...", + "agents.deleteConfirm": "Êtes-vous sûr de vouloir supprimer l'agent \"{{agentName}}\" ? Cette action ne peut pas être annulée.", + "agents.deleteTitle": "Supprimer Agent", + "agents.emptyNameError": "Agent nom ne peut pas être vide", + "agents.emptyContentError": "Agent contenu ne peut pas être vide", + "agents.validationError": "Erreur de validation", + "agents.agentExistsError": "Agent \"{{agentName}}\" existe déjà", + "agents.agentExistsTitle": "Agent existe déjà", "projects.title": "Projets", "projects.detail.editor": "Éditeur de configuration de projet", "projects.detail.projectEditor": "Éditeur de projet", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index d5919c8..95ccd7d 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -147,6 +147,28 @@ "toast.commandSaveFailed": "コマンドの保存に失敗しました:{{error}}", "toast.commandDeleted": "コマンドが正常に削除されました", "toast.commandDeleteFailed": "コマンドの削除に失敗しました:{{error}}", + "agents.title": "Agent", + "agents.description": "Claude Code sub agent を管理", + "agents.noAgents": "sub agent が見つかりません。「Add Agent」をクリックして作成してください。", + "agents.error": "agent の読み込みエラー:{{error}}", + "agents.addAgent": "Add Agent", + "agents.addAgentTitle": "Add Agent", + "agents.addAgentDescription": "Claude Code の新しい sub agent を作成", + "agents.agentName": "Agent 名", + "agents.agentNamePlaceholder": "例:code-reviewer", + "agents.agentContent": "Agent 内容", + "agents.contentPlaceholder": "agent の指示を入力...", + "agents.save": "保存", + "agents.saving": "保存中...", + "agents.create": "作成", + "agents.creating": "作成中...", + "agents.deleteConfirm": "agent \"{{agentName}}\" を削除してもよろしいですか?この操作は元に戻せません。", + "agents.deleteTitle": "Agent を削除", + "agents.emptyNameError": "Agent 名は空にできません", + "agents.emptyContentError": "Agent 内容は空にできません", + "agents.validationError": "検証エラー", + "agents.agentExistsError": "Agent \"{{agentName}}\" は既に存在します", + "agents.agentExistsTitle": "Agent が既に存在します", "projects.title": "プロジェクト", "projects.detail.editor": "プロジェクト設定エディター", "projects.detail.projectEditor": "プロジェクトエディター", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 37bd17f..1d6f404 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -147,6 +147,28 @@ "toast.commandSaveFailed": "保存命令失败:{{error}}", "toast.commandDeleted": "命令删除成功", "toast.commandDeleteFailed": "删除命令失败:{{error}}", + "agents.title": "Agent", + "agents.description": "管理您的 Claude Code sub agent", + "agents.noAgents": "未找到 sub agent。点击「Add Agent」来创建一个。", + "agents.error": "加载 agent 时出错:{{error}}", + "agents.addAgent": "Add Agent", + "agents.addAgentTitle": "Add Agent", + "agents.addAgentDescription": "为 Claude Code 创建新的 sub agent", + "agents.agentName": "Agent 名称", + "agents.agentNamePlaceholder": "例如:code-reviewer", + "agents.agentContent": "Agent 内容", + "agents.contentPlaceholder": "输入您的 agent 说明...", + "agents.save": "保存", + "agents.saving": "保存中...", + "agents.create": "创建", + "agents.creating": "创建中...", + "agents.deleteConfirm": "确定要删除 agent「{{agentName}}」吗?此操作无法撤销。", + "agents.deleteTitle": "删除 Agent", + "agents.emptyNameError": "Agent 名称不能为空", + "agents.emptyContentError": "Agent 内容不能为空", + "agents.validationError": "验证错误", + "agents.agentExistsError": "Agent「{{agentName}}」已存在", + "agents.agentExistsTitle": "Agent 已存在", "projects.title": "项目", "projects.detail.editor": "项目配置编辑器", "projects.detail.projectEditor": "项目编辑器", diff --git a/src/lib/query.ts b/src/lib/query.ts index ffe281a..6d3a2da 100644 --- a/src/lib/query.ts +++ b/src/lib/query.ts @@ -568,6 +568,54 @@ export const useDeleteClaudeCommand = () => { }); }; +// Agent management hooks +export const useClaudeAgents = () => + useQuery({ + queryKey: ["claude-agents"], + queryFn: () => invoke("read_claude_agents"), + }); + +export const useWriteClaudeAgent = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + agentName, + content, + }: { + agentName: string; + content: string; + }) => invoke("write_claude_agent", { agentName, content }), + onSuccess: () => { + toast.success("Agent saved successfully"); + queryClient.invalidateQueries({ queryKey: ["claude-agents"] }); + }, + onError: (error) => { + const errorMessage = + error instanceof Error ? error.message : String(error); + toast.error(`Failed to save agent: ${errorMessage}`); + }, + }); +}; + +export const useDeleteClaudeAgent = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (agentName: string) => + invoke("delete_claude_agent", { agentName }), + onSuccess: () => { + toast.success("Agent deleted successfully"); + queryClient.invalidateQueries({ queryKey: ["claude-agents"] }); + }, + onError: (error) => { + const errorMessage = + error instanceof Error ? error.message : String(error); + toast.error(`Failed to delete agent: ${errorMessage}`); + }, + }); +}; + // Helper function to rebuild tray menu const rebuildTrayMenu = async () => { try { diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx new file mode 100644 index 0000000..2ca90d2 --- /dev/null +++ b/src/pages/AgentsPage.tsx @@ -0,0 +1,366 @@ +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 { BotIcon, PlusIcon, SaveIcon, TrashIcon } from "lucide-react"; +import { Suspense, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + useClaudeAgents, + useDeleteClaudeAgent, + useWriteClaudeAgent, +} from "@/lib/query"; +import { useCodeMirrorTheme } from "@/lib/use-codemirror-theme"; + +function AgentsPageContent() { + const { t } = useTranslation(); + const { data: agents, isLoading, error } = useClaudeAgents(); + const writeAgent = useWriteClaudeAgent(); + const deleteAgent = useDeleteClaudeAgent(); + const [agentEdits, setAgentEdits] = useState>({}); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const codeMirrorTheme = useCodeMirrorTheme(); + + if (isLoading) { + return ( +
+
{t("loading")}
+
+ ); + } + + if (error) { + return ( +
+
+ {t("agents.error", { error: error.message })} +
+
+ ); + } + + const handleContentChange = (agentName: string, content: string) => { + setAgentEdits((prev) => ({ + ...prev, + [agentName]: content, + })); + }; + + const handleSaveAgent = async (agentName: string) => { + const content = agentEdits[agentName]; + if (content === undefined) return; + + writeAgent.mutate({ + agentName, + content, + }); + }; + + const handleDeleteAgent = async (agentName: string) => { + const confirmed = await ask(t("agents.deleteConfirm", { agentName }), { + title: t("agents.deleteTitle"), + kind: "warning", + }); + + if (confirmed) { + deleteAgent.mutate(agentName); + } + }; + + return ( +
+
+
+

+ {t("agents.title")} +

+

+ {t("agents.description")} +

+
+ + + + + + + + {t("agents.addAgentTitle")} + + + {t("agents.addAgentDescription")} + + + setIsDialogOpen(false)} /> + + +
+
+ {!agents || agents.length === 0 ? ( +
+ {t("agents.noAgents")} +
+ ) : ( + +
+ + {agents.map((agent) => ( + + +
+ + {agent.name} + + {`~/.claude/agents/${agent.name}.md`} + +
+
+ +
+
+ + 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, + }} + /> +
+
+ + + +
+
+
+
+ ))} +
+
+
+ )} +
+
+ ); +} + +export function AgentsPage() { + const { t } = useTranslation(); + + return ( + +
{t("loading")}
+
+ } + > + + + ); +} + +function CreateAgentPanel({ onClose }: { onClose?: () => void }) { + const { t } = useTranslation(); + const [agentName, setAgentName] = useState(""); + const [agentContent, setAgentContent] = useState(`--- +name: your-sub-agent-name +description: Description of when this subagent should be invoked +tools: tool1, tool2, tool3 # Optional - inherits all tools if omitted +model: sonnet # Optional - specify model alias or 'inherit' +--- + +Your subagent's system prompt goes here. This can be multiple paragraphs +and should clearly define the subagent's role, capabilities, and approach +to solving problems. + +Include specific instructions, best practices, and any constraints +the subagent should follow.`); + const writeAgent = useWriteClaudeAgent(); + const { data: agents } = useClaudeAgents(); + const codeMirrorTheme = useCodeMirrorTheme(); + + const handleCreateAgent = async () => { + // Validate agent name + if (!agentName.trim()) { + await message(t("agents.emptyNameError"), { + title: t("agents.validationError"), + kind: "error", + }); + return; + } + + // Check if agent already exists + const exists = agents?.some((agent) => agent.name === agentName); + if (exists) { + await message(t("agents.agentExistsError", { agentName }), { + title: t("agents.agentExistsTitle"), + kind: "info", + }); + return; + } + + // Validate content + if (!agentContent.trim()) { + await message(t("agents.emptyContentError"), { + title: t("agents.validationError"), + kind: "error", + }); + return; + } + + writeAgent.mutate( + { + agentName, + content: agentContent, + }, + { + onSuccess: () => { + setAgentName(""); + setAgentContent(""); + onClose?.(); + }, + }, + ); + }; + + return ( +
+
+ + setAgentName(e.target.value)} + placeholder={t("agents.agentNamePlaceholder")} + /> +
+ +
+ +
+ 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, + }} + /> +
+
+ +
+ +
+
+ ); +} diff --git a/src/pages/CommandsPage.tsx b/src/pages/CommandsPage.tsx index 3952ad4..ff95a4a 100644 --- a/src/pages/CommandsPage.tsx +++ b/src/pages/CommandsPage.tsx @@ -1,4 +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 { PlusIcon, SaveIcon, TerminalIcon, TrashIcon } from "lucide-react"; @@ -158,8 +159,10 @@ function CommandsPageContent() { } placeholder={t("commands.contentPlaceholder")} extensions={[ - markdown({ - base: markdownLanguage, + yamlFrontmatter({ + content: markdown({ + base: markdownLanguage, + }), }), EditorView.lineWrapping, ]} @@ -307,8 +310,10 @@ function CreateCommandPanel({ onClose }: { onClose?: () => void }) { theme={codeMirrorTheme} placeholder={t("commands.contentPlaceholder")} extensions={[ - markdown({ - base: markdownLanguage, + yamlFrontmatter({ + content: markdown({ + base: markdownLanguage, + }), }), EditorView.lineWrapping, ]} diff --git a/src/pages/MemoryPage.tsx b/src/pages/MemoryPage.tsx index daa1346..b90ed1f 100644 --- a/src/pages/MemoryPage.tsx +++ b/src/pages/MemoryPage.tsx @@ -1,4 +1,5 @@ import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; +import { yamlFrontmatter } from "@codemirror/lang-yaml"; import CodeMirror, { EditorView } from "@uiw/react-codemirror"; import { SaveIcon } from "lucide-react"; import { Suspense, useEffect, useState } from "react"; @@ -117,8 +118,10 @@ function MemoryPageContent() { value={content} height="100%" extensions={[ - markdown({ - base: markdownLanguage, + yamlFrontmatter({ + content: markdown({ + base: markdownLanguage, + }), }), EditorView.lineWrapping, ]} diff --git a/src/router.tsx b/src/router.tsx index 2571eb3..29f30f8 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -4,6 +4,7 @@ import { RouteWrapper } from "./components/RouteWrapper"; import { CommandsPage } from "./pages/CommandsPage"; import { ConfigEditorPage } from "./pages/ConfigEditorPage"; import { ConfigSwitcherPage } from "./pages/ConfigSwitcherPage"; +import { AgentsPage } from "./pages/AgentsPage"; import { MCPPage } from "./pages/MCPPage"; import { MemoryPage } from "./pages/MemoryPage"; import { NotificationPage } from "./pages/NotificationPage"; @@ -54,6 +55,14 @@ const router = createBrowserRouter([ ), }, + { + path: "agents", + element: ( + + + + ) + }, { path: "usage", element: (