sub agent

This commit is contained in:
Randy Lu
2025-11-02 21:46:04 +08:00
parent f3f2f7d03d
commit 69fd4e3aba
20 changed files with 797 additions and 28 deletions
+23 -19
View File
@@ -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"
]
}
+13
View File
@@ -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
+2
View File
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
target/
+1
View File
@@ -0,0 +1 @@
- Support manage sub agents
+3 -1
View File
@@ -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": {
+21
View File
@@ -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
+81
View File
@@ -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<Vec<AgentFile>, 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(())
}
+4 -1
View File
@@ -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")]
+1 -1
View File
@@ -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",
+7
View File
@@ -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,
+116
View File
@@ -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<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
+22
View File
@@ -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",
+22
View File
@@ -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",
+22
View File
@@ -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": "プロジェクトエディター",
+22
View File
@@ -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": "项目编辑器",
+48
View File
@@ -568,6 +568,54 @@ export const useDeleteClaudeCommand = () => {
});
};
// Agent management hooks
export const useClaudeAgents = () =>
useQuery({
queryKey: ["claude-agents"],
queryFn: () => invoke<CommandFile[]>("read_claude_agents"),
});
export const useWriteClaudeAgent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
agentName,
content,
}: {
agentName: string;
content: string;
}) => invoke<void>("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<void>("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 {
+366
View File
@@ -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<Record<string, string>>({});
const [isDialogOpen, setIsDialogOpen] = useState(false);
const codeMirrorTheme = useCodeMirrorTheme();
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("agents.error", { error: error.message })}
</div>
</div>
);
}
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 (
<div className="">
<div
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10"
data-tauri-drag-region
>
<div data-tauri-drag-region>
<h3 className="font-bold" data-tauri-drag-region>
{t("agents.title")}
</h3>
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
{t("agents.description")}
</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button variant="ghost" className="text-muted-foreground" size="sm">
<PlusIcon size={14} />
{t("agents.addAgent")}
</Button>
</DialogTrigger>
<DialogContent className="max-w-[600px]">
<DialogHeader>
<DialogTitle className="">
{t("agents.addAgentTitle")}
</DialogTitle>
<DialogDescription className="text-muted-foreground text-sm">
{t("agents.addAgentDescription")}
</DialogDescription>
</DialogHeader>
<CreateAgentPanel onClose={() => setIsDialogOpen(false)} />
</DialogContent>
</Dialog>
</div>
<div className="">
{!agents || 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="">
{agents.map((agent) => (
<AccordionItem
key={agent.name}
value={agent.name}
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">
<BotIcon size={12} />
<span className="font-medium">{agent.name}</span>
<span className="text-sm text-muted-foreground font-normal">
{`~/.claude/agents/${agent.name}.md`}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-3">
<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
}
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,
}}
/>
</div>
<div className="flex justify-between bg-card">
<Button
variant="outline"
onClick={() => handleSaveAgent(agent.name)}
disabled={
writeAgent.isPending ||
agentEdits[agent.name] === undefined
}
size="sm"
>
<SaveIcon size={14} className="" />
{writeAgent.isPending
? t("agents.saving")
: t("agents.save")}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteAgent(agent.name)}
disabled={deleteAgent.isPending}
>
<TrashIcon size={14} className="" />
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</ScrollArea>
)}
</div>
</div>
);
}
export function AgentsPage() {
const { t } = useTranslation();
return (
<Suspense
fallback={
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">{t("loading")}</div>
</div>
}
>
<AgentsPageContent />
</Suspense>
);
}
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 (
<div className="space-y-4 mt-4">
<div className="space-y-2">
<Label className="block" htmlFor="agent-name">{t("agents.agentName")}</Label>
<Input
id="agent-name"
value={agentName}
onChange={(e) => setAgentName(e.target.value)}
placeholder={t("agents.agentNamePlaceholder")}
/>
</div>
<div className="space-y-2">
<Label className="block" htmlFor="agent-content">{t("agents.agentContent")}</Label>
<div className="rounded-lg overflow-hidden border">
<CodeMirror
value={agentContent}
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,
}}
/>
</div>
</div>
<div className="flex justify-end">
<Button
onClick={handleCreateAgent}
disabled={
!agentName.trim() ||
!agentContent.trim() ||
writeAgent.isPending
}
>
{writeAgent.isPending
? t("agents.creating")
: t("agents.create")}
</Button>
</div>
</div>
);
}
+9 -4
View File
@@ -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,
]}
+5 -2
View File
@@ -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,
]}
+9
View File
@@ -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([
</RouteWrapper>
),
},
{
path: "agents",
element: (
<RouteWrapper>
<AgentsPage />
</RouteWrapper>
)
},
{
path: "usage",
element: (