From e878be2faab43a0f87e7a96d62ae1143e71436ac Mon Sep 17 00:00:00 2001 From: Mufeed VH Date: Wed, 25 Jun 2025 01:31:24 +0530 Subject: [PATCH] feat(ui): enhance agent interface with modal viewer and improved icon picker - Add AgentRunOutputViewer modal component for inline run preview - Implement IconPicker component with expanded icon selection - Refactor AgentRunsList to use modal instead of full-page navigation - Improve CreateAgent form layout with better grid structure - Update agent icon system to support wider range of icons - Enhance UI components with better animations and styling - Add scroll-area component for better content scrolling - Remove unused AgentRunView in favor of modal approach --- src/components/AgentRunOutputViewer.tsx | 665 ++++++++++++++++++++++++ src/components/AgentRunsList.tsx | 186 ++++--- src/components/CCAgents.tsx | 55 +- src/components/CreateAgent.tsx | 321 ++++++------ src/components/GitHubAgentBrowser.tsx | 86 ++- src/components/IconPicker.tsx | 463 +++++++++++++++++ src/components/index.ts | 27 +- src/components/ui/badge.tsx | 2 +- src/components/ui/button.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- src/components/ui/scroll-area.tsx | 46 ++ src/components/ui/select.tsx | 4 +- src/components/ui/split-pane.tsx | 1 - src/components/ui/switch.tsx | 1 - src/components/ui/tabs.tsx | 3 +- src/styles.css | 44 ++ 16 files changed, 1608 insertions(+), 300 deletions(-) create mode 100644 src/components/AgentRunOutputViewer.tsx create mode 100644 src/components/IconPicker.tsx create mode 100644 src/components/ui/scroll-area.tsx diff --git a/src/components/AgentRunOutputViewer.tsx b/src/components/AgentRunOutputViewer.tsx new file mode 100644 index 0000000..834e435 --- /dev/null +++ b/src/components/AgentRunOutputViewer.tsx @@ -0,0 +1,665 @@ +import { useState, useEffect, useRef, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + X, + Maximize2, + Minimize2, + Copy, + RefreshCw, + RotateCcw, + ChevronDown, + Bot, + Clock, + Hash, + DollarSign, + ExternalLink +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Toast, ToastContainer } from '@/components/ui/toast'; +import { Popover } from '@/components/ui/popover'; +import { api, type AgentRunWithMetrics } from '@/lib/api'; +import { useOutputCache } from '@/lib/outputCache'; +import { listen, type UnlistenFn } from '@tauri-apps/api/event'; +import { StreamMessage } from './StreamMessage'; +import { ErrorBoundary } from './ErrorBoundary'; +import { formatISOTimestamp } from '@/lib/date-utils'; +import { AGENT_ICONS } from './CCAgents'; +import type { ClaudeStreamMessage } from './AgentExecution'; + +interface AgentRunOutputViewerProps { + /** + * The agent run to display + */ + run: AgentRunWithMetrics; + /** + * Callback when the viewer is closed + */ + onClose: () => void; + /** + * Optional callback to open full view + */ + onOpenFullView?: () => void; + /** + * Optional className for styling + */ + className?: string; +} + +/** + * AgentRunOutputViewer - Modal component for viewing agent execution output + * + * @example + * setSelectedRun(null)} + * /> + */ +export function AgentRunOutputViewer({ + run, + onClose, + onOpenFullView, + className +}: AgentRunOutputViewerProps) { + const [messages, setMessages] = useState([]); + const [rawJsonlOutput, setRawJsonlOutput] = useState([]); + const [loading, setLoading] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); + const [copyPopoverOpen, setCopyPopoverOpen] = useState(false); + const [hasUserScrolled, setHasUserScrolled] = useState(false); + + const scrollAreaRef = useRef(null); + const outputEndRef = useRef(null); + const fullscreenScrollRef = useRef(null); + const fullscreenMessagesEndRef = useRef(null); + const unlistenRefs = useRef([]); + const { getCachedOutput, setCachedOutput } = useOutputCache(); + + // Auto-scroll logic + const isAtBottom = () => { + const container = isFullscreen ? fullscreenScrollRef.current : scrollAreaRef.current; + if (container) { + const { scrollTop, scrollHeight, clientHeight } = container; + const distanceFromBottom = scrollHeight - scrollTop - clientHeight; + return distanceFromBottom < 1; + } + return true; + }; + + const scrollToBottom = () => { + if (!hasUserScrolled) { + const endRef = isFullscreen ? fullscreenMessagesEndRef.current : outputEndRef.current; + if (endRef) { + endRef.scrollIntoView({ behavior: 'smooth' }); + } + } + }; + + // Clean up listeners on unmount + useEffect(() => { + return () => { + unlistenRefs.current.forEach(unlisten => unlisten()); + }; + }, []); + + // Auto-scroll when messages change + useEffect(() => { + const shouldAutoScroll = !hasUserScrolled || isAtBottom(); + if (shouldAutoScroll) { + scrollToBottom(); + } + }, [messages, hasUserScrolled, isFullscreen]); + + const loadOutput = async (skipCache = false) => { + if (!run.id) return; + + try { + // Check cache first if not skipping cache + if (!skipCache) { + const cached = getCachedOutput(run.id); + if (cached) { + const cachedJsonlLines = cached.output.split('\n').filter(line => line.trim()); + setRawJsonlOutput(cachedJsonlLines); + setMessages(cached.messages); + // If cache is recent (less than 5 seconds old) and session isn't running, use cache only + if (Date.now() - cached.lastUpdated < 5000 && run.status !== 'running') { + return; + } + } + } + + setLoading(true); + const rawOutput = await api.getSessionOutput(run.id); + + // Parse JSONL output into messages + const jsonlLines = rawOutput.split('\n').filter(line => line.trim()); + setRawJsonlOutput(jsonlLines); + + const parsedMessages: ClaudeStreamMessage[] = []; + for (const line of jsonlLines) { + try { + const message = JSON.parse(line) as ClaudeStreamMessage; + parsedMessages.push(message); + } catch (err) { + console.error("Failed to parse message:", err, line); + } + } + setMessages(parsedMessages); + + // Update cache + setCachedOutput(run.id, { + output: rawOutput, + messages: parsedMessages, + lastUpdated: Date.now(), + status: run.status + }); + + // Set up live event listeners for running sessions + if (run.status === 'running') { + setupLiveEventListeners(); + + try { + await api.streamSessionOutput(run.id); + } catch (streamError) { + console.warn('Failed to start streaming, will poll instead:', streamError); + } + } + } catch (error) { + console.error('Failed to load agent output:', error); + setToast({ message: 'Failed to load agent output', type: 'error' }); + } finally { + setLoading(false); + } + }; + + const setupLiveEventListeners = async () => { + try { + // Clean up existing listeners + unlistenRefs.current.forEach(unlisten => unlisten()); + unlistenRefs.current = []; + + // Set up live event listeners + const outputUnlisten = await listen("agent-output", (event) => { + try { + // Store raw JSONL + setRawJsonlOutput(prev => [...prev, event.payload]); + + // Parse and display + const message = JSON.parse(event.payload) as ClaudeStreamMessage; + setMessages(prev => [...prev, message]); + } catch (err) { + console.error("Failed to parse message:", err, event.payload); + } + }); + + const errorUnlisten = await listen("agent-error", (event) => { + console.error("Agent error:", event.payload); + setToast({ message: event.payload, type: 'error' }); + }); + + const completeUnlisten = await listen("agent-complete", () => { + setToast({ message: 'Agent execution completed', type: 'success' }); + }); + + unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten]; + } catch (error) { + console.error('Failed to set up live event listeners:', error); + } + }; + + // Copy functionality + const handleCopyAsJsonl = async () => { + const jsonl = rawJsonlOutput.join('\n'); + await navigator.clipboard.writeText(jsonl); + setCopyPopoverOpen(false); + setToast({ message: 'Output copied as JSONL', type: 'success' }); + }; + + const handleCopyAsMarkdown = async () => { + let markdown = `# Agent Execution: ${run.agent_name}\n\n`; + markdown += `**Task:** ${run.task}\n`; + markdown += `**Model:** ${run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n`; + markdown += `**Date:** ${formatISOTimestamp(run.created_at)}\n`; + if (run.metrics?.duration_ms) markdown += `**Duration:** ${(run.metrics.duration_ms / 1000).toFixed(2)}s\n`; + if (run.metrics?.total_tokens) markdown += `**Total Tokens:** ${run.metrics.total_tokens}\n`; + if (run.metrics?.cost_usd) markdown += `**Cost:** $${run.metrics.cost_usd.toFixed(4)} USD\n`; + markdown += `\n---\n\n`; + + for (const msg of messages) { + if (msg.type === "system" && msg.subtype === "init") { + markdown += `## System Initialization\n\n`; + markdown += `- Session ID: \`${msg.session_id || 'N/A'}\`\n`; + markdown += `- Model: \`${msg.model || 'default'}\`\n`; + if (msg.cwd) markdown += `- Working Directory: \`${msg.cwd}\`\n`; + if (msg.tools?.length) markdown += `- Tools: ${msg.tools.join(', ')}\n`; + markdown += `\n`; + } else if (msg.type === "assistant" && msg.message) { + markdown += `## Assistant\n\n`; + for (const content of msg.message.content || []) { + if (content.type === "text") { + markdown += `${content.text}\n\n`; + } else if (content.type === "tool_use") { + markdown += `### Tool: ${content.name}\n\n`; + markdown += `\`\`\`json\n${JSON.stringify(content.input, null, 2)}\n\`\`\`\n\n`; + } + } + if (msg.message.usage) { + markdown += `*Tokens: ${msg.message.usage.input_tokens} in, ${msg.message.usage.output_tokens} out*\n\n`; + } + } else if (msg.type === "user" && msg.message) { + markdown += `## User\n\n`; + for (const content of msg.message.content || []) { + if (content.type === "text") { + markdown += `${content.text}\n\n`; + } else if (content.type === "tool_result") { + markdown += `### Tool Result\n\n`; + markdown += `\`\`\`\n${content.content}\n\`\`\`\n\n`; + } + } + } else if (msg.type === "result") { + markdown += `## Execution Result\n\n`; + if (msg.result) { + markdown += `${msg.result}\n\n`; + } + if (msg.error) { + markdown += `**Error:** ${msg.error}\n\n`; + } + } + } + + await navigator.clipboard.writeText(markdown); + setCopyPopoverOpen(false); + setToast({ message: 'Output copied as Markdown', type: 'success' }); + }; + + const refreshOutput = async () => { + setRefreshing(true); + await loadOutput(true); // Skip cache + setRefreshing(false); + }; + + const handleScroll = (e: React.UIEvent) => { + const target = e.currentTarget; + const { scrollTop, scrollHeight, clientHeight } = target; + const distanceFromBottom = scrollHeight - scrollTop - clientHeight; + setHasUserScrolled(distanceFromBottom > 50); + }; + + // Load output on mount + useEffect(() => { + if (!run.id) return; + + // Check cache immediately for instant display + const cached = getCachedOutput(run.id); + if (cached) { + const cachedJsonlLines = cached.output.split('\n').filter(line => line.trim()); + setRawJsonlOutput(cachedJsonlLines); + setMessages(cached.messages); + } + + // Then load fresh data + loadOutput(); + }, [run.id]); + + const displayableMessages = useMemo(() => { + return messages.filter((message) => { + if (message.isMeta && !message.leafUuid && !message.summary) return false; + + if (message.type === "user" && message.message) { + if (message.isMeta) return false; + + const msg = message.message; + if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) return false; + + if (Array.isArray(msg.content)) { + let hasVisibleContent = false; + for (const content of msg.content) { + if (content.type === "text") { hasVisibleContent = true; break; } + if (content.type === "tool_result") { + // Check if this tool result will be displayed as a widget + let willBeSkipped = false; + if (content.tool_use_id) { + // Find the corresponding tool use + for (let i = messages.indexOf(message) - 1; i >= 0; i--) { + const prevMsg = messages[i]; + if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) { + const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id); + if (toolUse) { + const toolName = toolUse.name?.toLowerCase(); + const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep']; + if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { + willBeSkipped = true; + } + break; + } + } + } + } + if (!willBeSkipped) { hasVisibleContent = true; break; } + } + } + if (!hasVisibleContent) return false; + } + } + return true; + }); + }, [messages]); + + const renderIcon = (iconName: string) => { + const Icon = AGENT_ICONS[iconName as keyof typeof AGENT_ICONS] || Bot; + return ; + }; + + const formatDuration = (ms?: number) => { + if (!ms) return "N/A"; + const seconds = Math.floor(ms / 1000); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; + }; + + const formatTokens = (tokens?: number) => { + if (!tokens) return "0"; + if (tokens >= 1000) { + return `${(tokens / 1000).toFixed(1)}k`; + } + return tokens.toString(); + }; + + return ( + <> + + + + + +
+
+
+ {renderIcon(run.agent_icon)} +
+
+ + {run.agent_name} + {run.status === 'running' && ( +
+
+ Running +
+ )} +
+

+ {run.task} +

+
+ + {run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'} + +
+ + {formatISOTimestamp(run.created_at)} +
+ {run.metrics?.duration_ms && ( + {formatDuration(run.metrics.duration_ms)} + )} + {run.metrics?.total_tokens && ( +
+ + {formatTokens(run.metrics.total_tokens)} +
+ )} + {run.metrics?.cost_usd && ( +
+ + ${run.metrics.cost_usd.toFixed(4)} +
+ )} +
+
+
+
+ + + Copy + + + } + content={ +
+ + +
+ } + open={copyPopoverOpen} + onOpenChange={setCopyPopoverOpen} + align="end" + /> + {onOpenFullView && ( + + )} + + + +
+
+
+ + {loading ? ( +
+
+ + Loading output... +
+
+ ) : messages.length === 0 ? ( +
+

No output available yet

+
+ ) : ( +
+ + {displayableMessages.map((message: ClaudeStreamMessage, index: number) => ( + + + + + + ))} + +
+
+ )} + + + + + {/* Fullscreen Modal */} + {isFullscreen && ( +
+
+
+ {renderIcon(run.agent_icon)} +
+

{run.agent_name}

+

{run.task}

+
+
+
+ + + Copy Output + + + } + content={ +
+ + +
+ } + align="end" + /> + + +
+
+
+
+ {messages.length === 0 ? ( +
+ No output available yet +
+ ) : ( + <> + + {displayableMessages.map((message: ClaudeStreamMessage, index: number) => ( + + + + + + ))} + +
+ + )} +
+
+
+ )} + + {/* Toast Notification */} + + {toast && ( + setToast(null)} + /> + )} + + + ); +} \ No newline at end of file diff --git a/src/components/AgentRunsList.tsx b/src/components/AgentRunsList.tsx index a7e6bdd..acb7b18 100644 --- a/src/components/AgentRunsList.tsx +++ b/src/components/AgentRunsList.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { motion } from "framer-motion"; +import { motion, AnimatePresence } from "framer-motion"; import { Play, Clock, Hash, Bot } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -8,6 +8,7 @@ import { cn } from "@/lib/utils"; import { formatISOTimestamp } from "@/lib/date-utils"; import type { AgentRunWithMetrics } from "@/lib/api"; import { AGENT_ICONS } from "./CCAgents"; +import { AgentRunOutputViewer } from "./AgentRunOutputViewer"; interface AgentRunsListProps { /** @@ -41,6 +42,7 @@ export const AgentRunsList: React.FC = ({ className, }) => { const [currentPage, setCurrentPage] = useState(1); + const [selectedRun, setSelectedRun] = useState(null); // Calculate pagination const totalPages = Math.ceil(runs.length / ITEMS_PER_PAGE); @@ -75,6 +77,16 @@ export const AgentRunsList: React.FC = ({ return tokens.toString(); }; + const handleRunClick = (run: AgentRunWithMetrics) => { + // If there's a callback, use it (for full-page navigation) + if (onRunClick) { + onRunClick(run); + } else { + // Otherwise, open in modal preview + setSelectedRun(run); + } + }; + if (runs.length === 0) { return (
@@ -83,92 +95,114 @@ export const AgentRunsList: React.FC = ({
); } - + return ( -
-
- {currentRuns.map((run, index) => ( - - onRunClick?.(run)} + <> +
+ + {currentRuns.map((run, index) => ( + - -
-
-
+ handleRunClick(run)} + > + +
+
{renderIcon(run.agent_icon)}
-
-
-

{run.task}

- - {run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'} - -
-
- by {run.agent_name} - {run.completed_at && ( - <> - -
- - {formatDuration(run.metrics?.duration_ms)} -
- - )} - {run.metrics?.total_tokens && ( - <> - -
- - {formatTokens(run.metrics?.total_tokens)} -
- - )} - {run.metrics?.cost_usd && ( - <> - - ${run.metrics?.cost_usd?.toFixed(4)} - + +
+
+

+ {run.agent_name} +

+ {run.status === "running" && ( +
+
+ Running +
)}
-

- {formatISOTimestamp(run.created_at)} + +

+ {run.task}

+ +
+
+ + {formatISOTimestamp(run.created_at)} +
+ + {run.metrics?.duration_ms && ( + {formatDuration(run.metrics.duration_ms)} + )} + + {run.metrics?.total_tokens && ( +
+ + {formatTokens(run.metrics.total_tokens)} +
+ )} +
+
+ +
+ + {run.status === "completed" ? "Completed" : + run.status === "running" ? "Running" : + run.status === "failed" ? "Failed" : + "Pending"} +
- {!run.completed_at && ( - - Running - - )} -
- - - - ))} + + + + ))} + + + {/* Pagination */} + {totalPages > 1 && ( +
+ +
+ )}
- - {totalPages > 1 && ( - setSelectedRun(null)} /> )} -
+ ); }; \ No newline at end of file diff --git a/src/components/CCAgents.tsx b/src/components/CCAgents.tsx index af45a2f..9262bae 100644 --- a/src/components/CCAgents.tsx +++ b/src/components/CCAgents.tsx @@ -6,14 +6,6 @@ import { Trash2, Play, Bot, - Brain, - Code, - Sparkles, - Zap, - Cpu, - Rocket, - Shield, - Terminal, ArrowLeft, History, Download, @@ -46,9 +38,9 @@ import { Toast, ToastContainer } from "@/components/ui/toast"; import { CreateAgent } from "./CreateAgent"; import { AgentExecution } from "./AgentExecution"; import { AgentRunsList } from "./AgentRunsList"; -import { AgentRunView } from "./AgentRunView"; import { RunningSessionsView } from "./RunningSessionsView"; import { GitHubAgentBrowser } from "./GitHubAgentBrowser"; +import { ICON_MAP } from "./IconPicker"; interface CCAgentsProps { /** @@ -61,18 +53,8 @@ interface CCAgentsProps { className?: string; } -// Available icons for agents -export const AGENT_ICONS = { - bot: Bot, - brain: Brain, - code: Code, - sparkles: Sparkles, - zap: Zap, - cpu: Cpu, - rocket: Rocket, - shield: Shield, - terminal: Terminal, -}; +// Available icons for agents - now using all icons from IconPicker +export const AGENT_ICONS = ICON_MAP; export type AgentIconName = keyof typeof AGENT_ICONS; @@ -90,10 +72,10 @@ export const CCAgents: React.FC = ({ onBack, className }) => { const [error, setError] = useState(null); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); const [currentPage, setCurrentPage] = useState(1); - const [view, setView] = useState<"list" | "create" | "edit" | "execute" | "viewRun">("list"); + const [view, setView] = useState<"list" | "create" | "edit" | "execute">("list"); const [activeTab, setActiveTab] = useState<"agents" | "running">("agents"); const [selectedAgent, setSelectedAgent] = useState(null); - const [selectedRunId, setSelectedRunId] = useState(null); + // const [selectedRunId, setSelectedRunId] = useState(null); const [showGitHubBrowser, setShowGitHubBrowser] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [agentToDelete, setAgentToDelete] = useState(null); @@ -195,12 +177,12 @@ export const CCAgents: React.FC = ({ onBack, className }) => { setToast({ message: "Agent updated successfully", type: "success" }); }; - const handleRunClick = (run: AgentRunWithMetrics) => { - if (run.id) { - setSelectedRunId(run.id); - setView("viewRun"); - } - }; + // const handleRunClick = (run: AgentRunWithMetrics) => { + // if (run.id) { + // setSelectedRunId(run.id); + // setView("viewRun"); + // } + // }; const handleExecutionComplete = async () => { // Reload runs when returning from execution @@ -270,7 +252,7 @@ export const CCAgents: React.FC = ({ onBack, className }) => { const paginatedAgents = agents.slice(startIndex, startIndex + AGENTS_PER_PAGE); const renderIcon = (iconName: string) => { - const Icon = AGENT_ICONS[iconName as AgentIconName] || Bot; + const Icon = AGENT_ICONS[iconName as AgentIconName] || AGENT_ICONS.bot; return ; }; @@ -305,14 +287,7 @@ export const CCAgents: React.FC = ({ onBack, className }) => { ); } - if (view === "viewRun" && selectedRunId) { - return ( - setView("list")} - /> - ); - } + // Removed viewRun case - now using modal preview in AgentRunsList return (
@@ -564,7 +539,9 @@ export const CCAgents: React.FC = ({ onBack, className }) => {
) : ( - + )}
)} diff --git a/src/components/CreateAgent.tsx b/src/components/CreateAgent.tsx index d433a55..bb8b875 100644 --- a/src/components/CreateAgent.tsx +++ b/src/components/CreateAgent.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { motion } from "framer-motion"; -import { ArrowLeft, Save, Loader2 } from "lucide-react"; +import { ArrowLeft, Save, Loader2, ChevronDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -8,8 +8,9 @@ import { Toast, ToastContainer } from "@/components/ui/toast"; import { api, type Agent } from "@/lib/api"; import { cn } from "@/lib/utils"; import MDEditor from "@uiw/react-md-editor"; -import { AGENT_ICONS, type AgentIconName } from "./CCAgents"; +import { type AgentIconName } from "./CCAgents"; import { AgentSandboxSettings } from "./AgentSandboxSettings"; +import { IconPicker, ICON_MAP } from "./IconPicker"; interface CreateAgentProps { /** @@ -54,6 +55,7 @@ export const CreateAgent: React.FC = ({ const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); + const [showIconPicker, setShowIconPicker] = useState(false); const isEditMode = !!agent; @@ -183,164 +185,172 @@ export const CreateAgent: React.FC = ({ )} {/* Form */} -
-
- {/* Agent Name */} -
- - setName(e.target.value)} - className="max-w-md" - /> -
- - {/* Icon Picker */} -
- -
- {(Object.keys(AGENT_ICONS) as AgentIconName[]).map((iconName) => { - const Icon = AGENT_ICONS[iconName]; - return ( - - ); - })} +
+ + {/* Basic Information */} +
+
+

Basic Information

-
- - {/* Model Selection */} -
- -
- + + {/* Name and Icon */} +
+
+ + setName(e.target.value)} + placeholder="e.g., Code Assistant" + required + /> +
- +
-
- {/* Default Task */} -
- - setDefaultTask(e.target.value)} - className="max-w-md" - /> -

- This will be used as the default task placeholder when executing the agent -

-
+ {/* Model Selection */} +
+ +
+ + + +
+
- {/* Sandbox Settings */} - { - if ('sandbox_enabled' in updates) setSandboxEnabled(updates.sandbox_enabled!); - if ('enable_file_read' in updates) setEnableFileRead(updates.enable_file_read!); - if ('enable_file_write' in updates) setEnableFileWrite(updates.enable_file_write!); - if ('enable_network' in updates) setEnableNetwork(updates.enable_network!); - }} - /> - - {/* System Prompt Editor */} -
- -

- Define the behavior and capabilities of your CC Agent -

-
- setSystemPrompt(val || "")} - preview="edit" - height={400} - visibleDragbar={false} + {/* Default Task */} +
+ + setDefaultTask(e.target.value)} + className="max-w-md" /> +

+ This will be used as the default task placeholder when executing the agent +

+
+ + {/* Sandbox Settings */} + { + if ('sandbox_enabled' in updates) setSandboxEnabled(updates.sandbox_enabled!); + if ('enable_file_read' in updates) setEnableFileRead(updates.enable_file_read!); + if ('enable_file_write' in updates) setEnableFileWrite(updates.enable_file_write!); + if ('enable_network' in updates) setEnableNetwork(updates.enable_network!); + }} + /> + + {/* System Prompt Editor */} +
+ +

+ Define the behavior and capabilities of your CC Agent +

+
+ setSystemPrompt(val || "")} + preview="edit" + height={400} + visibleDragbar={false} + /> +
-
+
@@ -354,6 +364,17 @@ export const CreateAgent: React.FC = ({ /> )} + + {/* Icon Picker Dialog */} + { + setSelectedIcon(iconName as AgentIconName); + setShowIconPicker(false); + }} + isOpen={showIconPicker} + onClose={() => setShowIconPicker(false)} + />
); }; \ No newline at end of file diff --git a/src/components/GitHubAgentBrowser.tsx b/src/components/GitHubAgentBrowser.tsx index 408b37a..422076e 100644 --- a/src/components/GitHubAgentBrowser.tsx +++ b/src/components/GitHubAgentBrowser.tsx @@ -3,7 +3,6 @@ import { motion, AnimatePresence } from "framer-motion"; import { Search, Download, - X, Loader2, AlertCircle, Eye, @@ -16,8 +15,10 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { api, type GitHubAgentFile, type AgentExport } from "@/lib/api"; -import { AGENT_ICONS, type AgentIconName } from "./CCAgents"; +import { api, type GitHubAgentFile, type AgentExport, type Agent } from "@/lib/api"; +import { type AgentIconName } from "./CCAgents"; +import { ICON_MAP } from "./IconPicker"; + interface GitHubAgentBrowserProps { isOpen: boolean; onClose: () => void; @@ -42,14 +43,24 @@ export const GitHubAgentBrowser: React.FC = ({ const [searchQuery, setSearchQuery] = useState(""); const [selectedAgent, setSelectedAgent] = useState(null); const [importing, setImporting] = useState(false); - const [importedAgents, setImportedAgents] = useState>(new Set()); + const [existingAgents, setExistingAgents] = useState([]); useEffect(() => { if (isOpen) { fetchAgents(); + fetchExistingAgents(); } }, [isOpen]); + const fetchExistingAgents = async () => { + try { + const agents = await api.listAgents(); + setExistingAgents(agents); + } catch (err) { + console.error("Failed to fetch existing agents:", err); + } + }; + const fetchAgents = async () => { try { setLoading(true); @@ -91,6 +102,13 @@ export const GitHubAgentBrowser: React.FC = ({ } }; + const isAgentImported = (fileName: string) => { + const agentName = getAgentDisplayName(fileName); + return existingAgents.some(agent => + agent.name.toLowerCase() === agentName.toLowerCase() + ); + }; + const handleImportAgent = async () => { if (!selectedAgent?.file) return; @@ -98,8 +116,8 @@ export const GitHubAgentBrowser: React.FC = ({ setImporting(true); await api.importAgentFromGitHub(selectedAgent.file.download_url); - // Mark as imported - setImportedAgents(prev => new Set(prev).add(selectedAgent.file.name)); + // Refresh existing agents list + await fetchExistingAgents(); // Close preview setSelectedAgent(null); @@ -126,7 +144,7 @@ export const GitHubAgentBrowser: React.FC = ({ }; const renderIcon = (iconName: string) => { - const Icon = AGENT_ICONS[iconName as AgentIconName] || AGENT_ICONS.bot; + const Icon = ICON_MAP[iconName as AgentIconName] || ICON_MAP.bot; return ; }; @@ -141,6 +159,25 @@ export const GitHubAgentBrowser: React.FC = ({
+ {/* Repository Info */} +
+

+ Agents are fetched from{" "} + + github.com/getAsterisk/claudia/cc_agents + + +

+

+ You can contribute your custom agents to the repository! +

+
+ {/* Search Bar */}
@@ -190,11 +227,20 @@ export const GitHubAgentBrowser: React.FC = ({ onClick={() => handlePreviewAgent(agent)}>
-

- {getAgentDisplayName(agent.name)} -

- {importedAgents.has(agent.name) && ( - +
+
+ {/* Default to bot icon for now, will be loaded from preview */} + {(() => { + const Icon = ICON_MAP.bot; + return ; + })()} +
+

+ {getAgentDisplayName(agent.name)} +

+
+ {isAgentImported(agent.name) && ( + Imported @@ -234,17 +280,7 @@ export const GitHubAgentBrowser: React.FC = ({ setSelectedAgent(null)}> - - Agent Preview - - + Agent Preview
@@ -333,14 +369,14 @@ export const GitHubAgentBrowser: React.FC = ({