From 16b8de546e40cd5cb550c503b673dbad821c3f1a Mon Sep 17 00:00:00 2001 From: Sedinha Date: Wed, 6 Aug 2025 23:18:43 -0300 Subject: [PATCH 1/2] feat: integrate claude-code-router for dynamic models --- scripts/ccr_wrapper.sh | 4 + src-tauri/src/claude_binary.rs | 13 + src-tauri/src/commands/ccr.rs | 60 +++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/main.rs | 3 + src/components/ClaudeCodeSession.tsx | 33 ++- src/components/FloatingPromptInput.tsx | 339 ++++++++----------------- src/lib/api.ts | 22 ++ 8 files changed, 232 insertions(+), 243 deletions(-) create mode 100755 scripts/ccr_wrapper.sh create mode 100644 src-tauri/src/commands/ccr.rs diff --git a/scripts/ccr_wrapper.sh b/scripts/ccr_wrapper.sh new file mode 100755 index 00000000..8f8d6dd8 --- /dev/null +++ b/scripts/ccr_wrapper.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# This script acts as a bridge between Claudia and the ccr tool. +# It takes all arguments passed to it and forwards them to "ccr code". +/home/sedinha/.nvm/versions/node/v20.18.2/bin/ccr code "$@" diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 272a1cc9..356d1917 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -146,6 +146,19 @@ fn source_preference(installation: &ClaudeInstallation) -> u8 { fn discover_system_installations() -> Vec { let mut installations = Vec::new(); + // Manually add ccr wrapper script path + let ccr_path = "/home/sedinha/Desktop/claudia/scripts/ccr_wrapper.sh".to_string(); + if PathBuf::from(&ccr_path).exists() { + info!("Found ccr binary at: {}", ccr_path); + let version = get_claude_version(&ccr_path).ok().flatten(); + installations.push(ClaudeInstallation { + path: ccr_path, + version, + source: "ccr".to_string(), + installation_type: InstallationType::Custom, + }); + } + // 1. Try 'which' command first (now works in production) if let Some(installation) = try_which_command() { installations.push(installation); diff --git a/src-tauri/src/commands/ccr.rs b/src-tauri/src/commands/ccr.rs new file mode 100644 index 00000000..836cb964 --- /dev/null +++ b/src-tauri/src/commands/ccr.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use log::{info, warn}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct CcrProvider { + name: String, + models: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct CcrRouter { + default: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct CcrConfig { + #[serde(rename = "Providers")] + providers: Vec, + #[serde(rename = "Router")] + router: CcrRouter, +} + +#[derive(Debug, Serialize, Clone)] +pub struct CcrModelInfo { + provider: String, + models: Vec, + default_model: String, +} + +#[tauri::command] +pub fn get_ccr_model_info() -> Result { + info!("Attempting to read claude-code-router config"); + let home_dir = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?; + let config_path = home_dir.join(".claude-code-router").join("config.json"); + + if !config_path.exists() { + let err_msg = format!("ccr config not found at {:?}", config_path); + warn!("{}", err_msg); + return Err(err_msg); + } + + let config_content = fs::read_to_string(&config_path) + .map_err(|e| format!("Failed to read ccr config file: {}", e))?; + + let config: CcrConfig = serde_json::from_str(&config_content) + .map_err(|e| format!("Failed to parse ccr config file: {}", e))?; + + // Assuming the first provider is the one we want, as per the user's setup. + let provider = config.providers.get(0).ok_or_else(|| "No providers found in ccr config".to_string())?; + + // The default model is in the format "provider,model_name". We need to extract the model name. + let default_model_name = config.router.default.split(',').nth(1).unwrap_or("").trim().to_string(); + + Ok(CcrModelInfo { + provider: provider.name.clone(), + models: provider.models.clone(), + default_model: default_model_name, + }) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index a0fa7e89..d5392b63 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -5,3 +5,4 @@ pub mod usage; pub mod storage; pub mod slash_commands; pub mod proxy; +pub mod ccr; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0589bef7..96354134 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -249,6 +249,9 @@ fn main() { // Proxy Settings get_proxy_settings, save_proxy_settings, + + // CCR Commands + commands::ccr::get_ccr_model_info, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index 20d32a00..779daaa9 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -95,7 +95,7 @@ export const ClaudeCodeSession: React.FC = ({ const [forkSessionName, setForkSessionName] = useState(""); // Queued prompts state - const [queuedPrompts, setQueuedPrompts] = useState>([]); + const [queuedPrompts, setQueuedPrompts] = useState>([]); // New state for preview feature const [showPreview, setShowPreview] = useState(false); @@ -111,7 +111,7 @@ export const ClaudeCodeSession: React.FC = ({ const unlistenRefs = useRef([]); const hasActiveSessionRef = useRef(false); const floatingPromptRef = useRef(null); - const queuedPromptsRef = useRef>([]); + const queuedPromptsRef = useRef>([]); const isMountedRef = useRef(true); const isListeningRef = useRef(false); const sessionStartTime = useRef(Date.now()); @@ -265,6 +265,25 @@ export const ClaudeCodeSession: React.FC = ({ onStreamingChange?.(isLoading, claudeSessionId); }, [isLoading, claudeSessionId, onStreamingChange]); + const handleModelChange = (provider: string, model: string) => { + const command = `/model ${provider},${model}`; + // We can't directly send a command without a user prompt. + // So, we add a user message to the history to show the change, + // and then we'll inject this command into the next `onSend` call. + // A better approach is to have a dedicated command channel. + // For now, let's just send it as a prompt. + handleSendPrompt(command, model); + + // Add a visual confirmation in the chat + const modelChangeMessage: ClaudeStreamMessage = { + type: "system", + subtype: "info", + result: `Switched model to ${model}`, + timestamp: new Date().toISOString() + }; + setMessages(prev => [...prev, modelChangeMessage]); + }; + // Auto-scroll to bottom when new messages arrive useEffect(() => { if (displayableMessages.length > 0) { @@ -424,7 +443,7 @@ export const ClaudeCodeSession: React.FC = ({ } }; - const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => { + const handleSendPrompt = async (prompt: string, model: string) => { console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath, claudeSessionId, effectiveSession }); if (!projectPath) { @@ -432,6 +451,13 @@ export const ClaudeCodeSession: React.FC = ({ return; } + // If the prompt is a /model command, handle it separately + if (prompt.startsWith('/model ')) { + // This is now handled by onModelChange, but we can keep this as a manual fallback + // Or just let it be sent like a normal prompt, which ccr should handle. + // For now, we'll let it pass through as a normal prompt. + } + // If already loading, queue the prompt if (isLoading) { const newPrompt = { @@ -1560,6 +1586,7 @@ export const ClaudeCodeSession: React.FC = ({ void; - /** - * Whether the input is loading - */ + onSend: (prompt: string, model: string) => void; + onModelChange?: (provider: string, model: string) => void; isLoading?: boolean; - /** - * Whether the input is disabled - */ disabled?: boolean; - /** - * Default model to select - */ - defaultModel?: "sonnet" | "opus"; - /** - * Project path for file picker - */ + defaultModel?: string; projectPath?: string; - /** - * Optional className for styling - */ className?: string; - /** - * Callback when cancel is clicked (only during loading) - */ onCancel?: () => void; } @@ -56,114 +37,51 @@ export interface FloatingPromptInputRef { addImage: (imagePath: string) => void; } -/** - * Thinking mode type definition - */ type ThinkingMode = "auto" | "think" | "think_hard" | "think_harder" | "ultrathink"; -/** - * Thinking mode configuration - */ type ThinkingModeConfig = { id: ThinkingMode; name: string; description: string; - level: number; // 0-4 for visual indicator - phrase?: string; // The phrase to append + level: number; + phrase?: string; }; const THINKING_MODES: ThinkingModeConfig[] = [ - { - id: "auto", - name: "Auto", - description: "Let Claude decide", - level: 0 - }, - { - id: "think", - name: "Think", - description: "Basic reasoning", - level: 1, - phrase: "think" - }, - { - id: "think_hard", - name: "Think Hard", - description: "Deeper analysis", - level: 2, - phrase: "think hard" - }, - { - id: "think_harder", - name: "Think Harder", - description: "Extensive reasoning", - level: 3, - phrase: "think harder" - }, - { - id: "ultrathink", - name: "Ultrathink", - description: "Maximum computation", - level: 4, - phrase: "ultrathink" - } + { id: "auto", name: "Auto", description: "Let Claude decide", level: 0 }, + { id: "think", name: "Think", description: "Basic reasoning", level: 1, phrase: "think" }, + { id: "think_hard", name: "Think Hard", description: "Deeper analysis", level: 2, phrase: "think hard" }, + { id: "think_harder", name: "Think Harder", description: "Extensive reasoning", level: 3, phrase: "think harder" }, + { id: "ultrathink", name: "Ultrathink", description: "Maximum computation", level: 4, phrase: "ultrathink" }, ]; -/** - * ThinkingModeIndicator component - Shows visual indicator bars for thinking level - */ -const ThinkingModeIndicator: React.FC<{ level: number }> = ({ level }) => { - return ( -
- {[1, 2, 3, 4].map((i) => ( -
- ))} -
- ); -}; +const ThinkingModeIndicator: React.FC<{ level: number }> = ({ level }) => ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+); type Model = { - id: "sonnet" | "opus"; + id: string; name: string; description: string; icon: React.ReactNode; }; -const MODELS: Model[] = [ - { - id: "sonnet", - name: "Claude 4 Sonnet", - description: "Faster, efficient for most tasks", - icon: - }, - { - id: "opus", - name: "Claude 4 Opus", - description: "More capable, better for complex tasks", - icon: - } +const DEFAULT_MODELS: Model[] = [ + { id: "sonnet", name: "Claude 4 Sonnet", description: "Faster, efficient for most tasks", icon: }, + { id: "opus", name: "Claude 4 Opus", description: "More capable, better for complex tasks", icon: }, ]; -/** - * FloatingPromptInput component - Fixed position prompt input with model picker - * - * @example - * const promptRef = useRef(null); - * console.log('Send:', prompt, model)} - * isLoading={false} - * /> - */ const FloatingPromptInputInner = ( { onSend, + onModelChange, isLoading = false, disabled = false, defaultModel = "sonnet", @@ -174,7 +92,10 @@ const FloatingPromptInputInner = ( ref: React.Ref, ) => { const [prompt, setPrompt] = useState(""); - const [selectedModel, setSelectedModel] = useState<"sonnet" | "opus">(defaultModel); + const [ccrInfo, setCcrInfo] = useState(null); + const [models, setModels] = useState(DEFAULT_MODELS); + const [selectedModel, setSelectedModel] = useState(defaultModel); + const [isCcrLoading, setIsCcrLoading] = useState(true); const [selectedThinkingMode, setSelectedThinkingMode] = useState("auto"); const [isExpanded, setIsExpanded] = useState(false); const [modelPickerOpen, setModelPickerOpen] = useState(false); @@ -191,28 +112,45 @@ const FloatingPromptInputInner = ( const expandedTextareaRef = useRef(null); const unlistenDragDropRef = useRef<(() => void) | null>(null); - // Expose a method to add images programmatically + useEffect(() => { + const fetchCcrInfo = async () => { + try { + setIsCcrLoading(true); + const info = await api.getCcrModelInfo(); + setCcrInfo(info); + const ccrModels: Model[] = info.models.map(m => ({ + id: m, + name: m, + description: `From ${info.provider}`, + icon: + })); + setModels(ccrModels); + setSelectedModel(info.default_model || ccrModels[0]?.id || defaultModel); + } catch (error) { + console.warn("Could not load claude-code-router config, falling back to default models.", error); + setModels(DEFAULT_MODELS); + setSelectedModel(defaultModel); + } finally { + setIsCcrLoading(false); + } + }; + fetchCcrInfo(); + }, [defaultModel]); + React.useImperativeHandle( ref, () => ({ addImage: (imagePath: string) => { setPrompt(currentPrompt => { const existingPaths = extractImagePaths(currentPrompt); - if (existingPaths.includes(imagePath)) { - return currentPrompt; // Image already added - } - - // Wrap path in quotes if it contains spaces + if (existingPaths.includes(imagePath)) return currentPrompt; const mention = imagePath.includes(' ') ? `@"${imagePath}"` : `@${imagePath}`; const newPrompt = currentPrompt + (currentPrompt.endsWith(' ') || currentPrompt === '' ? '' : ' ') + mention + ' '; - - // Focus the textarea setTimeout(() => { const target = isExpanded ? expandedTextareaRef.current : textareaRef.current; target?.focus(); target?.setSelectionRange(newPrompt.length, newPrompt.length); }, 0); - return newPrompt; }); } @@ -220,139 +158,66 @@ const FloatingPromptInputInner = ( [isExpanded] ); - // Helper function to check if a file is an image const isImageFile = (path: string): boolean => { - // Check if it's a data URL - if (path.startsWith('data:image/')) { - return true; - } - // Otherwise check file extension + if (path.startsWith('data:image/')) return true; const ext = path.split('.').pop()?.toLowerCase(); return ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'].includes(ext || ''); }; - // Extract image paths from prompt text const extractImagePaths = (text: string): string[] => { - console.log('[extractImagePaths] Input text length:', text.length); - - // Updated regex to handle both quoted and unquoted paths - // Pattern 1: @"path with spaces or data URLs" - quoted paths - // Pattern 2: @path - unquoted paths (continues until @ or end) const quotedRegex = /@"([^"]+)"/g; const unquotedRegex = /@([^@\n\s]+)/g; - - const pathsSet = new Set(); // Use Set to ensure uniqueness - - // First, extract quoted paths (including data URLs) + const pathsSet = new Set(); let matches = Array.from(text.matchAll(quotedRegex)); - console.log('[extractImagePaths] Quoted matches:', matches.length); - for (const match of matches) { - const path = match[1]; // No need to trim, quotes preserve exact path - console.log('[extractImagePaths] Processing quoted path:', path.startsWith('data:') ? 'data URL' : path); - - // For data URLs, use as-is; for file paths, convert to absolute - const fullPath = path.startsWith('data:') - ? path - : (path.startsWith('/') ? path : (projectPath ? `${projectPath}/${path}` : path)); - - if (isImageFile(fullPath)) { - pathsSet.add(fullPath); - } + const path = match[1]; + const fullPath = path.startsWith('data:') ? path : (path.startsWith('/') ? path : (projectPath ? `${projectPath}/${path}` : path)); + if (isImageFile(fullPath)) pathsSet.add(fullPath); } - - // Remove quoted mentions from text to avoid double-matching let textWithoutQuoted = text.replace(quotedRegex, ''); - - // Then extract unquoted paths (typically file paths) matches = Array.from(textWithoutQuoted.matchAll(unquotedRegex)); - console.log('[extractImagePaths] Unquoted matches:', matches.length); - for (const match of matches) { const path = match[1].trim(); - // Skip if it looks like a data URL fragment (shouldn't happen with proper quoting) if (path.includes('data:')) continue; - - console.log('[extractImagePaths] Processing unquoted path:', path); - - // Convert relative path to absolute if needed const fullPath = path.startsWith('/') ? path : (projectPath ? `${projectPath}/${path}` : path); - - if (isImageFile(fullPath)) { - pathsSet.add(fullPath); - } + if (isImageFile(fullPath)) pathsSet.add(fullPath); } - - const uniquePaths = Array.from(pathsSet); - console.log('[extractImagePaths] Final extracted paths (unique):', uniquePaths.length); - return uniquePaths; + return Array.from(pathsSet); }; - // Update embedded images when prompt changes useEffect(() => { - console.log('[useEffect] Prompt changed:', prompt); const imagePaths = extractImagePaths(prompt); - console.log('[useEffect] Setting embeddedImages to:', imagePaths); setEmbeddedImages(imagePaths); }, [prompt, projectPath]); - // Set up Tauri drag-drop event listener useEffect(() => { - // This effect runs only once on component mount to set up the listener. let lastDropTime = 0; - const setupListener = async () => { try { - // If a listener from a previous mount/render is still around, clean it up. - if (unlistenDragDropRef.current) { - unlistenDragDropRef.current(); - } - + if (unlistenDragDropRef.current) unlistenDragDropRef.current(); const webview = getCurrentWebviewWindow(); unlistenDragDropRef.current = await webview.onDragDropEvent((event) => { - if (event.payload.type === 'enter' || event.payload.type === 'over') { - setDragActive(true); - } else if (event.payload.type === 'leave') { + if (event.payload.type === 'enter' || event.payload.type === 'over') setDragActive(true); + else if (event.payload.type === 'leave') setDragActive(false); + else if (event.payload.type === 'drop' && event.payload.paths) { setDragActive(false); - } else if (event.payload.type === 'drop' && event.payload.paths) { - setDragActive(false); - const currentTime = Date.now(); - if (currentTime - lastDropTime < 200) { - // This debounce is crucial to handle the storm of drop events - // that Tauri/OS can fire for a single user action. - return; - } + if (currentTime - lastDropTime < 200) return; lastDropTime = currentTime; - const droppedPaths = event.payload.paths as string[]; const imagePaths = droppedPaths.filter(isImageFile); - if (imagePaths.length > 0) { setPrompt(currentPrompt => { const existingPaths = extractImagePaths(currentPrompt); const newPaths = imagePaths.filter(p => !existingPaths.includes(p)); - - if (newPaths.length === 0) { - return currentPrompt; // All dropped images are already in the prompt - } - - // Wrap paths with spaces in quotes for clarity - const mentionsToAdd = newPaths.map(p => { - // If path contains spaces, wrap in quotes - if (p.includes(' ')) { - return `@"${p}"`; - } - return `@${p}`; - }).join(' '); + if (newPaths.length === 0) return currentPrompt; + const mentionsToAdd = newPaths.map(p => p.includes(' ') ? `@"${p}"` : `@${p}`).join(' '); const newPrompt = currentPrompt + (currentPrompt.endsWith(' ') || currentPrompt === '' ? '' : ' ') + mentionsToAdd + ' '; - setTimeout(() => { const target = isExpanded ? expandedTextareaRef.current : textareaRef.current; target?.focus(); target?.setSelectionRange(newPrompt.length, newPrompt.length); }, 0); - return newPrompt; }); } @@ -362,43 +227,40 @@ const FloatingPromptInputInner = ( console.error('Failed to set up Tauri drag-drop listener:', error); } }; - setupListener(); - return () => { - // On unmount, ensure we clean up the listener. if (unlistenDragDropRef.current) { unlistenDragDropRef.current(); unlistenDragDropRef.current = null; } }; - }, []); // Empty dependency array ensures this runs only on mount/unmount. + }, []); useEffect(() => { - // Focus the appropriate textarea when expanded state changes - if (isExpanded && expandedTextareaRef.current) { - expandedTextareaRef.current.focus(); - } else if (!isExpanded && textareaRef.current) { - textareaRef.current.focus(); - } + if (isExpanded && expandedTextareaRef.current) expandedTextareaRef.current.focus(); + else if (!isExpanded && textareaRef.current) textareaRef.current.focus(); }, [isExpanded]); const handleSend = () => { if (prompt.trim() && !disabled) { let finalPrompt = prompt.trim(); - - // Append thinking phrase if not auto mode const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode); - if (thinkingMode && thinkingMode.phrase) { - finalPrompt = `${finalPrompt}.\n\n${thinkingMode.phrase}.`; - } - + if (thinkingMode && thinkingMode.phrase) finalPrompt = `${finalPrompt}.\n\n${thinkingMode.phrase}.`; onSend(finalPrompt, selectedModel); setPrompt(""); setEmbeddedImages([]); } }; + const handleModelSelect = (modelId: string) => { + setSelectedModel(modelId); + setModelPickerOpen(false); + if (onModelChange && ccrInfo) { + onModelChange(ccrInfo.provider, modelId); + } + }; + + // ... (keep the rest of the handlers: handleTextChange, handleFileSelect, etc. as they were) const handleTextChange = (e: React.ChangeEvent) => { const newValue = e.target.value; const newCursorPosition = e.target.selectionStart || 0; @@ -688,19 +550,19 @@ const FloatingPromptInputInner = ( } // For file paths, use the original logic - const escapedPath = imagePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const escapedRelativePath = imagePath.replace(projectPath + '/', '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escapedPath = imagePath.replace(/[.*+?^${}()|[\\]/g, '\\$&'); + const escapedRelativePath = imagePath.replace(projectPath + '/', '').replace(/[.*+?^${}()|[\\]/g, '\\$&'); // Create patterns for both quoted and unquoted mentions const patterns = [ // Quoted full path - new RegExp(`@"${escapedPath}"\\s?`, 'g'), + new RegExp(`@"${escapedPath}"\s?`, 'g'), // Unquoted full path - new RegExp(`@${escapedPath}\\s?`, 'g'), + new RegExp(`@${escapedPath}\s?`, 'g'), // Quoted relative path - new RegExp(`@"${escapedRelativePath}"\\s?`, 'g'), + new RegExp(`@"${escapedRelativePath}"\s?`, 'g'), // Unquoted relative path - new RegExp(`@${escapedRelativePath}\\s?`, 'g') + new RegExp(`@${escapedRelativePath}\s?`) ]; let newPrompt = prompt; @@ -711,7 +573,7 @@ const FloatingPromptInputInner = ( setPrompt(newPrompt.trim()); }; - const selectedModelData = MODELS.find(m => m.id === selectedModel) || MODELS[0]; + const selectedModelData = models.find(m => m.id === selectedModel) || models[0] || DEFAULT_MODELS[0]; return ( <> @@ -893,23 +755,20 @@ const FloatingPromptInputInner = ( } content={ -
- {MODELS.map((model) => ( +
+ {models.map((model) => ( {/* Back button */} +

{projectPath}

{/* Display the current project path */} +
+ {/* Loop through the sessions array */} + {sessions.map((session) => ( + onSessionClick?.(session)}> + +
+

Session ID: {session.id.slice(0, 8)}...

{/* Display truncated session ID */} + {/* Display the first message preview if available */} + {session.first_message &&

First msg: {session.first_message}

} + {/* ... other session info like timestamps */} +
+ {/* ... click handler */} +
+
+ ))} +
+ {/* ... Pagination */} +
+ ); +}; +``` + +The `SessionList.tsx` component receives the list of sessions for a *single* project (again, fetched from the backend). It shows you the project path you're currently viewing and lists each session, often including its ID, creation time, and a preview of the first message. Clicking a session calls `onSessionClick`, which will lead to the conversation view (`ClaudeCodeSession.tsx`). + +## How it Works: Under the Hood + +The frontend components we just saw need data to display. This data is provided by the backend code, which runs in Rust using the Tauri framework. The backend's job for Session/Project Management is to read the files in the `~/.claude` directory and structure that information for the frontend. + +Here's a simplified step-by-step of what happens when the frontend asks for the list of projects: + +1. The frontend calls a backend command, specifically `list_projects`. +2. The backend code starts by finding the `~/.claude` directory on your computer. +3. It then looks inside the `~/.claude/projects` directory. +4. For each directory found inside `projects`, it treats it as a potential project. +5. It reads the name of the project directory (which is an encoded path) and tries to find the actual project path by looking at the session files inside. +6. It also counts the number of `.jsonl` files (sessions) inside that project directory. +7. It gets the creation timestamp of the project directory. +8. It gathers this information (project ID, path, sessions list, creation time) into a `Project` struct. +9. It repeats this for all project directories. +10. Finally, it sends a list of these `Project` structs back to the frontend. + +Fetching sessions for a specific project follows a similar pattern: + +1. The frontend calls the `get_project_sessions` command, providing the `project_id`. +2. The backend finds the specific project directory inside `~/.claude/projects` using the provided `project_id`. +3. It looks inside that project directory for all `.jsonl` files. +4. For each `.jsonl` file (session), it extracts the session ID from the filename. +5. It gets the file's creation timestamp. +6. It reads the *first few lines* of the `.jsonl` file to find the first user message and its timestamp, for display as a preview in the UI. +7. It might also check for related files like todo data (`.json` files in `~/.claude/todos` linked by session ID). +8. It gathers this info into a `Session` struct. +9. It repeats this for all session files in the project directory. +10. Finally, it sends a list of `Session` structs back to the frontend. + +Here's a sequence diagram illustrating the `list_projects` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Open Projects View + Frontend->>Backend: Call list_projects() + Backend->>Filesystem: Read ~/.claude/projects directory + Filesystem-->>Backend: List of project directories + Backend->>Filesystem: For each directory: Read contents (session files) + Filesystem-->>Backend: List of session files + Backend->>Backend: Process directories and files (create Project structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Project List +``` + +And the `get_project_sessions` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Click on a Project + Frontend->>Backend: Call get_project_sessions(projectId) + Backend->>Filesystem: Read ~/.claude/projects/projectId/ directory + Filesystem-->>Backend: List of session files (.jsonl) + Backend->>Filesystem: For each session file: Read first lines, read metadata + Filesystem-->>Backend: First message, timestamp, creation time, etc. + Backend->>Backend: Process session files (create Session structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Session List for Project +``` + +## Diving into the Code + +Let's look at some specific parts of the Rust code in `src-tauri/src/commands/claude.rs` that handle this logic. + +First, the data structures that represent a project and a session: + +```rust +// src-tauri/src/commands/claude.rs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, // The encoded directory name + pub path: String, // The decoded or detected real path + pub sessions: Vec, // List of session file names (IDs) + pub created_at: u64, // Timestamp +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: String, // The session file name (UUID) + pub project_id: String, // Link back to the project + pub project_path: String, // The project's real path + pub todo_data: Option, // Optional associated data + pub created_at: u64, // Timestamp + pub first_message: Option, // Preview of the first user message + pub message_timestamp: Option, // Timestamp of the first message +} +// ... rest of the file +``` + +These `struct` definitions tell us what information the backend collects and sends to the frontend for projects and sessions. Notice the `Serialize` and `Deserialize` derives; this is what allows Tauri to easily pass these structures between the Rust backend and the JavaScript/TypeScript frontend. + +Here's the function that finds the base `~/.claude` directory: + +```rust +// src-tauri/src/commands/claude.rs +fn get_claude_dir() -> Result { + dirs::home_dir() // Find the user's home directory + .context("Could not find home directory")? // Handle potential error + .join(".claude") // Append the .claude directory name + .canonicalize() // Resolve symbolic links, etc. + .context("Could not find ~/.claude directory") // Handle potential error +} +// ... rest of the file +``` + +This simple function is crucial as all project and session data is located relative to `~/.claude`. + +Now, a look at the `list_projects` function. We'll skip some error handling and logging for brevity here: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn list_projects() -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let projects_dir = claude_dir.join("projects"); // Path to ~/.claude/projects + + if !projects_dir.exists() { + return Ok(Vec::new()); // Return empty list if directory doesn't exist + } + + let mut projects = Vec::new(); + + // Iterate over entries inside ~/.claude/projects + let entries = fs::read_dir(&projects_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + if path.is_dir() { // Only process directories + let dir_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| "...").unwrap(); + + // Get creation/modification timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Determine the actual project path (explained next) + let project_path = match get_project_path_from_sessions(&path) { + Ok(p) => p, + Err(_) => decode_project_path(dir_name) // Fallback if session files don't exist + }; + + // Find all session files (.jsonl) in this project directory + let mut sessions = Vec::new(); + if let Ok(session_entries) = fs::read_dir(&path) { + for session_entry in session_entries.flatten() { + let session_path = session_entry.path(); + if session_path.is_file() && session_path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str()) { + sessions.push(session_id.to_string()); // Store session ID (filename) + } + } + } + } + + // Add the project to the list + projects.push(Project { + id: dir_name.to_string(), + path: project_path, + sessions, + created_at, + }); + } + } + + projects.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(projects) +} +// ... rest of the file +``` + +This code reads the `projects` directory, identifies subdirectories as projects, and collects basic information for each. A key part is determining the *actual* project path, as the directory name is an encoded version of the path where Claude Code was run. The `get_project_path_from_sessions` function handles this: + +```rust +// src-tauri/src/commands/claude.rs +fn get_project_path_from_sessions(project_dir: &PathBuf) -> Result { + // Try to read any JSONL file in the directory + let entries = fs::read_dir(project_dir) + .map_err(|e| format!("..."))?; + + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + // Read the first line of the JSONL file + if let Ok(file) = fs::File::open(&path) { + let reader = BufReader::new(file); + if let Some(Ok(first_line)) = reader.lines().next() { + // Parse the JSON and extract "cwd" (current working directory) + if let Ok(json) = serde_json::from_str::(&first_line) { + if let Some(cwd) = json.get("cwd").and_then(|v| v.as_str()) { + return Ok(cwd.to_string()); // Found the project path! + } + } + } + } + } + } + } + + Err("Could not determine project path from session files".to_string()) // Failed to find it +} +// ... rest of the file +``` + +This function is smarter than just decoding the directory name. It opens the first session file it finds within a project directory, reads the very first line (which usually contains metadata including the `cwd` - current working directory - where Claude Code was launched), and uses that `cwd` as the definitive project path. This is more reliable than trying to decode the directory name. + +Finally, let's look at `get_project_sessions`: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn get_project_sessions(project_id: String) -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let project_dir = claude_dir.join("projects").join(&project_id); // Path to specific project dir + let todos_dir = claude_dir.join("todos"); // Path to todo data + + if !project_dir.exists() { + return Err(format!("Project directory not found: {}", project_id)); + } + + // Determine the actual project path + let project_path = match get_project_path_from_sessions(&project_dir) { + Ok(p) => p, + Err(_) => decode_project_path(&project_id) // Fallback + }; + + let mut sessions = Vec::new(); + + // Read all files in the project directory + let entries = fs::read_dir(&project_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + // Process only .jsonl files + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = path.file_stem().and_then(|s| s.to_str()) { // Get filename as session ID + // Get file creation timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Extract first user message for preview (explained next) + let (first_message, message_timestamp) = extract_first_user_message(&path); + + // Check for associated todo data file + let todo_path = todos_dir.join(format!("{}.json", session_id)); + let todo_data = if todo_path.exists() { + // ... read and parse todo.json ... + None // Simplified: just show if it exists, not the data + } else { + None + }; + + // Add the session to the list + sessions.push(Session { + id: session_id.to_string(), + project_id: project_id.clone(), + project_path: project_path.clone(), + todo_data, + created_at, + first_message, + message_timestamp, + }); + } + } + } + + sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(sessions) +} +// ... rest of the file +``` + +This function is similar to `list_projects` but focuses on one project directory. It iterates through the files, identifies the `.jsonl` session files, extracts metadata like ID and timestamp, and importantly, calls `extract_first_user_message` to get a quick preview of the conversation's start for the UI. + +The `extract_first_user_message` function reads the session's `.jsonl` file line by line, parses each line as JSON, and looks for the first entry that represents a message from the "user" role, making sure to skip certain types of messages (like the initial system caveat or command outputs) to find the actual user prompt. + +## Putting it Together + +So, the Session/Project Management feature in `claudia` works by: + +1. Reading the file structure created by the Claude Code CLI in `~/.claude`. +2. Identifying directories in `~/.claude/projects` as projects and `.jsonl` files within them as sessions. +3. Extracting key metadata (IDs, paths, timestamps, first message previews). +4. Providing this structured data to the frontend UI via Tauri commands (`list_projects`, `get_project_sessions`). +5. Allowing the frontend (`ProjectList.tsx`, `SessionList.tsx`) to display this information in an organized, browsable way. +6. Enabling the user to select a session, triggering navigation to the main session view (`ClaudeCodeSession.tsx`) where they can see the full history (loaded using `load_session_history`) and potentially resume the conversation. + +This abstraction provides the essential foundation for interacting with your past Claude Code work, allowing you to manage your conversation history effectively. + +## Conclusion + +In this chapter, we learned how `claudia` discovers, lists, and displays your Claude Code projects and sessions by reading files from the `~/.claude` directory. We saw how the frontend components like `ProjectList` and `SessionList` use data provided by backend commands like `list_projects` and `get_project_sessions` to build the navigation interface. We also briefly touched upon how session data (`.jsonl` files) is parsed to show previews. + +Understanding how `claudia` manages sessions and projects is the first step in seeing how it builds a rich user interface on top of the command-line tool. In the next chapter, we'll dive into the concept of [Agents](02_agents_.md), which are central to how Claude Code and `claudia` understand the context of your work. + +[Next Chapter: Agents](02_agents_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ProjectList.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/SessionList.tsx) diff --git a/Claudia-docs/V_1/claudia-10.MCP.md b/Claudia-docs/V_1/claudia-10.MCP.md new file mode 100644 index 00000000..cc0e754f --- /dev/null +++ b/Claudia-docs/V_1/claudia-10.MCP.md @@ -0,0 +1,505 @@ +# Chapter 10: MCP (Model Context Protocol) + +Welcome to the final chapter of the `claudia` tutorial! We've covered a lot, from managing your work with [Session/Project Management](01_session_project_management_.md) and defining specialized [Agents](02_agents_.md), to understanding how the [Frontend UI Components](03_frontend_ui_components_.md) are built and how they talk to the backend using [Tauri Commands](04_tauri_commands_.md). We've seen how `claudia` interacts with the core [Claude CLI Interaction](05_claude_cli_interaction_.md), how [Sandboxing](06_sandboxing_.md) keeps your environment secure, how [Streamed Output Processing](07_streamed_output_processing_.md) provides real-time feedback, and how the [Process Registry](08_process_registry_.md) tracks running tasks. Finally, we explored [Checkpointing](09_checkpointing_.md) for versioning your sessions. + +Now, let's look at a feature that allows `claudia` (specifically, the `claude` CLI it controls) to go beyond just interacting with Anthropic's standard Claude API: **MCP (Model Context Protocol)**. + +## The Problem: Connecting to Different AI Sources + +By default, the `claude` CLI is primarily designed to connect to Anthropic's Claude API endpoints (like the ones that power Sonnet, Opus, etc.). But what if you want to use a different AI model? Perhaps a smaller model running locally on your machine, a specialized AI tool you built, or an internal AI service within your company? + +These other AI sources might have different ways of communicating. You need a standard way for `claudia` (or rather, the `claude` CLI it manages) to talk to *any* AI service that can process prompts, use tools, and respond, regardless of who built it or how it runs. + +This is the problem MCP solves. It provides a standardized "language" or "interface" that allows `claude` to communicate with any external program or service that "speaks" MCP. + +Imagine `claudia` is a smart home hub. It needs to talk to various devices – lights, thermostats, speakers – made by different companies. Instead of needing a unique connection method for every single brand, they all agree to use a standard protocol (like Wi-Fi and a common API). MCP is that standard protocol for AI model servers. + +## What is MCP (Model Context Protocol)? + +MCP stands for **Model Context Protocol**. It's a standard protocol used by the `claude` CLI to exchange information with external programs or services that act as AI models or tools. + +When you configure an "MCP Server" in `claude` (and thus in `claudia`), you're telling `claude` about an external AI source that it can connect to using the MCP standard. + +This abstraction layer manages: + +1. **Defining Servers:** Telling `claude` about external MCP sources by giving them a name and specifying how to connect (e.g., run a specific command, connect to a URL). +2. **Listing Servers:** Seeing which MCP servers are configured. +3. **Interacting:** When a session or Agent is configured to use a specific MCP server, the `claude` CLI connects to that server (instead of the default Anthropic API) and uses the MCP to send prompts and receive responses. + +This capability extends `claudia`'s potential far beyond just Anthropic's hosted models, enabling connections to a variety of AI models or services that implement the MCP standard. + +## Key Concepts + +Here are the main ideas behind MCP in `claudia` (and `claude`): + +| Concept | Description | Analogy | +| :---------------- | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | +| **MCP Server** | An external program or service that speaks the MCP standard and can act as an AI model or provide tools. | A smart device (light, speaker) in a smart home system. | +| **Transport** | How `claude` connects to the MCP Server. Common types are `stdio` (running the server as a command-line process) or `sse` (connecting to a network URL via Server-Sent Events). | How the hub talks to the device (e.g., Wi-Fi, Bluetooth). | +| **Scope** | Where the MCP server configuration is stored. Affects who can see/use it: `user` (all projects), `project` (via `.mcp.json` in the project directory), `local` (only this `claudia` instance's settings, usually linked to a project). | Where you save the device setup (e.g., globally in the app, specific to one room setup). | +| **MCP Configuration** | The details needed to connect to a server: name, transport type, command/URL, environment variables, scope. | The device's settings (name, type, how to connect, what room it's in). | + +## Using MCP in the UI + +`claudia` provides a dedicated section to manage MCP servers. You'll typically find this under "Settings" or a similar menu item. + +The `MCPManager.tsx` component is the main view for this: + +```typescript +// src/components/MCPManager.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; +// ... other imports like api, MCPServerList, MCPAddServer, MCPImportExport ... + +export const MCPManager: React.FC = ({ onBack, className }) => { + const [activeTab, setActiveTab] = useState("servers"); // State for the active tab + const [servers, setServers] = useState([]); // State for the list of servers + const [loading, setLoading] = useState(true); + // ... error/toast state ... + + // Load servers when the component mounts + useEffect(() => { + loadServers(); + }, []); + + // Function to load servers from the backend + const loadServers = async () => { + try { + setLoading(true); + // Call the backend command to list servers + const serverList = await api.mcpList(); + setServers(serverList); // Update state + } catch (err) { + console.error("Failed to load MCP servers:", err); + // ... set error state ... + } finally { + setLoading(false); + } + }; + + // Callbacks for child components (Add, List, Import) + const handleServerAdded = () => { + loadServers(); // Refresh the list after adding + setActiveTab("servers"); // Switch back to the list view + // ... show success toast ... + }; + + const handleServerRemoved = (name: string) => { + setServers(prev => prev.filter(s => s.name !== name)); // Remove server from state + // ... show success toast ... + }; + + const handleImportCompleted = (imported: number, failed: number) => { + loadServers(); // Refresh after import + // ... show import result toast ... + }; + + return ( +
{/* Layout container */} + {/* Header with Back button */} +
+ +

MCP Servers

+
+ + {/* Tabs for navigating sections */} + + + Servers + Add Server + Import/Export + + + {/* Server List Tab Content */} + + {/* Using a Card component */} + + + + + {/* Add Server Tab Content */} + + {/* Using a Card component */} + + + + + {/* Import/Export Tab Content */} + + {/* Using a Card component */} + + + + + + {/* ... Toast notifications ... */} +
+ ); +}; +``` + +This main component uses tabs to organize the different MCP management tasks: +* **Servers:** Shows a list of configured servers using the `MCPServerList` component. +* **Add Server:** Provides a form to manually add a new server using the `MCPAddServer` component. +* **Import/Export:** Contains options to import servers (e.g., from a JSON file or Claude Desktop config) or potentially export them, using the `MCPImportExport` component. + +The `MCPServerList.tsx` component simply takes the list of `MCPServer` objects and displays them, grouped by scope (User, Project, Local). It provides buttons to remove or test the connection for each server, calling the relevant `onServerRemoved` or backend test command. + +The `MCPAddServer.tsx` component presents a form where you can enter the details of a new server: name, select the transport type (Stdio or SSE), provide the command or URL, add environment variables, and choose the scope. When you click "Add", it calls the backend `api.mcpAdd` command. + +```typescript +// src/components/MCPAddServer.tsx (Simplified) +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { SelectComponent } from "@/components/ui/select"; +// ... other imports like api ... + +export const MCPAddServer: React.FC = ({ onServerAdded, onError }) => { + const [transport, setTransport] = useState<"stdio" | "sse">("stdio"); + const [serverName, setServerName] = useState(""); + const [commandOrUrl, setCommandOrUrl] = useState(""); + const [scope, setScope] = useState("local"); + // ... state for args, env vars, saving indicator ... + + const handleAddServer = async () => { + if (!serverName.trim() || !commandOrUrl.trim()) { + onError("Name and Command/URL are required"); + return; + } + + try { + // ... set saving state ... + + // Prepare arguments based on transport type + const command = transport === "stdio" ? commandOrUrl : undefined; + const url = transport === "sse" ? commandOrUrl : undefined; + const args = transport === "stdio" ? commandOrUrl.split(/\s+/).slice(1) : []; // Simplified arg parsing + const env = {}; // Simplified env vars + + // Call the backend API command + const result = await api.mcpAdd( + serverName, + transport, + command, + args, + env, + url, + scope + ); + + if (result.success) { + // Reset form and notify parent + setServerName(""); + setCommandOrUrl(""); + setScope("local"); + // ... reset args/env ... + onServerAdded(); + } else { + onError(result.message); // Show error from backend + } + } catch (error) { + onError("Failed to add server"); + console.error("Failed to add MCP server:", error); + } finally { + // ... unset saving state ... + } + }; + + return ( +
+

Add MCP Server

+ setTransport(v as "stdio" | "sse")}> + + Stdio + SSE + + {/* ... Form fields based on transport type (Name, Command/URL, Scope, Env) ... */} + + +
+ ); +}; +``` + +This component collects user input and passes it to the `api.mcpAdd` function, which is a wrapper around the backend Tauri command. + +Once an MCP server is configured, it can potentially be selected as the "model" for an Agent run or an interactive session, although the integration point for selecting MCP servers specifically during session execution might be evolving or limited in the current `claudia` UI compared to standard Anthropic models. The core mechanism is that the `claude` CLI itself is told *which* configured MCP server to use for a task via command-line arguments, rather than connecting directly to Anthropic. + +## How it Works: Under the Hood (Backend) + +The MCP management in `claudia`'s backend (Rust) doesn't re-implement the MCP standard or manage external processes/connections directly for all servers. Instead, it primarily acts as a wrapper around the **`claude mcp`** subcommand provided by the `claude` CLI itself. + +When you use the MCP management features in `claudia`'s UI: + +1. **Frontend Calls Command:** The frontend calls a Tauri command like `mcp_add`, `mcp_list`, or `mcp_remove` ([Chapter 4: Tauri Commands]). +2. **Backend Calls `claude mcp`:** The backend command receives the request and constructs the appropriate command-line arguments for the `claude mcp` subcommand (e.g., `claude mcp add`, `claude mcp list`, `claude mcp remove`). +3. **Backend Spawns Process:** The backend spawns the `claude` binary as a child process, executing it with the prepared `mcp` arguments ([Chapter 5: Claude CLI Interaction]). +4. **`claude` CLI Handles Logic:** The `claude` CLI process receives the `mcp` command and performs the requested action: + * `claude mcp add`: Parses the provided configuration (name, transport, command/URL, scope) and saves it to its own configuration file (usually `~/.claude/mcp.json` for user/local scope, or writes to `.mcp.json` in the project path for project scope). + * `claude mcp list`: Reads its configuration files and prints the list of configured servers to standard output in a specific text format. + * `claude mcp remove`: Removes the specified server from its configuration files. +5. **Backend Captures Output/Status:** `claudia`'s backend captures the standard output and standard error of the `claude mcp` process ([Chapter 7: Streamed Output Processing], though for simple `mcp` commands it's usually just capturing the final output). +6. **Backend Returns Result:** The backend processes the captured output (e.g., parses the list for `mcp list`, checks for success/failure messages for `mcp add`/`remove`) and returns the result back to the frontend. + +For managing project-scoped servers via `.mcp.json`, the backend also contains specific commands (`mcp_read_project_config`, `mcp_save_project_config`) that read and write the `.mcp.json` file directly using Rust's filesystem functions and JSON parsing. This is an alternative way to manage project-specific MCP configurations that doesn't strictly go through the `claude mcp` CLI commands. + +Here's a sequence diagram showing the flow for adding an MCP server using the `mcp_add` command: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (MCPAddServer.tsx) + participant Backend as Backend Commands (mcp.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + User->>Frontend: Fill form & click "Add Server" + Frontend->>Backend: Call mcp_add(name, transport, command, ...) + Backend->>Backend: Construct arguments for "claude mcp add" + Backend->>OS: Spawn process (claude mcp add ...) + OS-->>ClaudeCLI: Start claude binary + ClaudeCLI->>ClaudeCLI: Parse args, update MCP config file (~/.claude/mcp.json or .mcp.json) + ClaudeCLI-->>OS: Process finishes (exit code 0 on success) + OS-->>Backend: Process status & captured output/error + Backend->>Backend: Check status, parse output for result message + Backend-->>Frontend: Return AddServerResult { success, message } + Frontend->>Frontend: Handle result (show toast, refresh list) + Frontend->>User: User sees confirmation/error +``` + +This diagram shows that for server *management* operations (add, list, remove), `claudia` acts as a GUI frontend to the `claude mcp` command-line interface. + +When a session or Agent is configured to *use* one of these registered MCP servers for its AI interactions, the `claude` binary (launched by `claudia` as described in [Chapter 5: Claude CLI Interaction]) is invoked with arguments telling it *which* server to connect to (e.g., `--model mcp:my-server`). The `claude` binary then uses the configuration it previously saved to establish communication with the specified external MCP server using the correct transport (stdio or sse) and protocol. `claudia`'s role during this phase is primarily launching and monitoring the `claude` process, and streaming its output, as covered in previous chapters. + +## Diving into the Backend Code + +Let's look at some snippets from `src-tauri/src/commands/mcp.rs`. + +The helper function `execute_claude_mcp_command` is central to wrapping the CLI calls: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use tauri::AppHandle; +use anyhow::{Context, Result}; +use std::process::Command; +use log::info; + +/// Executes a claude mcp command +fn execute_claude_mcp_command(app_handle: &AppHandle, args: Vec<&str>) -> Result { + info!("Executing claude mcp command with args: {:?}", args); + + // Find the claude binary path (logic from Chapter 5) + let claude_path = super::claude::find_claude_binary(app_handle)?; + + // Create a command with inherited environment (helper from Chapter 5) + let mut cmd = super::claude::create_command_with_env(&claude_path); + + cmd.arg("mcp"); // Add the 'mcp' subcommand + for arg in args { + cmd.arg(arg); // Add specific arguments (add, list, remove, get, serve, test-connection, etc.) + } + + // Run the command and capture output + let output = cmd.output() + .context("Failed to execute claude mcp command")?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) // Return stdout on success + } else { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + Err(anyhow::anyhow!("Command failed: {}", stderr)) // Return stderr as error + } +} +``` + +This function simply prepares and runs the `claude mcp ...` command and handles returning the result or error message based on the exit status. + +Now, let's see how `mcp_add` uses this helper: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... + +/// Adds a new MCP server +#[tauri::command] +pub async fn mcp_add( + app: AppHandle, + name: String, + transport: String, + command: Option, + args: Vec, + env: HashMap, + url: Option, + scope: String, +) -> Result { + info!("Adding MCP server: {} with transport: {}", name, transport); + + let mut cmd_args = vec!["add"]; // The 'add' subcommand argument + + // Add arguments for scope, transport, env, name, command/url + // These match the expected arguments for 'claude mcp add' + cmd_args.push("-s"); + cmd_args.push(&scope); + + if transport == "sse" { + cmd_args.push("--transport"); + cmd_args.push("sse"); + } + + for (key, value) in env.iter() { + cmd_args.push("-e"); + cmd_args.push(&format!("{}={}", key, value)); // Format env vars correctly + } + + cmd_args.push(&name); // The server name + + if transport == "stdio" { + if let Some(cmd_str) = &command { + // Handle commands with spaces/args by adding "--" separator if needed + cmd_args.push("--"); + cmd_args.push(cmd_str); + for arg in &args { + cmd_args.push(arg); + } + } else { /* ... error handling ... */ } + } else if transport == "sse" { + if let Some(url_str) = &url { + cmd_args.push(url_str); // The URL for SSE + } else { /* ... error handling ... */ } + } else { /* ... error handling ... */ } + + // Execute the command using the helper + match execute_claude_mcp_command(&app, cmd_args) { + Ok(output) => { + // Parse the output message from claude mcp add + Ok(AddServerResult { + success: true, + message: output.trim().to_string(), + server_name: Some(name), + }) + } + Err(e) => { + // Handle errors from the command execution + Ok(AddServerResult { + success: false, + message: e.to_string(), + server_name: None, + }) + } + } +} +``` + +This command function demonstrates how it builds the `cmd_args` vector, carefully adding the correct flags and values expected by the `claude mcp add` command. It then passes these arguments to `execute_claude_mcp_command` and formats the result into the `AddServerResult` struct for the frontend. + +The `mcp_list` command is similar, executing `claude mcp list` and then parsing the text output (which can be complex, as noted in the code comments) to build the `Vec` structure returned to the frontend. + +Direct file access for `.mcp.json` (project scope) looks like this: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use std::path::PathBuf; +use std::fs; +use serde::{Serialize, Deserialize}; + +// Structs mirroring the .mcp.json structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPProjectConfig { + #[serde(rename = "mcpServers")] + pub mcp_servers: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPServerConfig { + pub command: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub env: HashMap, +} + + +/// Reads .mcp.json from the current project +#[tauri::command] +pub async fn mcp_read_project_config(project_path: String) -> Result { + log::info!("Reading .mcp.json from project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + if !mcp_json_path.exists() { + // Return empty config if file doesn't exist + return Ok(MCPProjectConfig { mcp_servers: HashMap::new() }); + } + + match fs::read_to_string(&mcp_json_path) { // Read the file content + Ok(content) => { + match serde_json::from_str::(&content) { // Parse JSON + Ok(config) => Ok(config), + Err(e) => { + log::error!("Failed to parse .mcp.json: {}", e); + Err(format!("Failed to parse .mcp.json: {}", e)) + } + } + } + Err(e) => { + log::error!("Failed to read .mcp.json: {}", e); + Err(format!("Failed to read .mcp.json: {}", e)) + } + } +} + +/// Saves .mcp.json to the current project +#[tauri::command] +pub async fn mcp_save_project_config( + project_path: String, + config: MCPProjectConfig, +) -> Result { + log::info!("Saving .mcp.json to project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + let json_content = serde_json::to_string_pretty(&config) // Serialize config to JSON + .map_err(|e| format!("Failed to serialize config: {}", e))?; + + fs::write(&mcp_json_path, json_content) // Write to the file + .map_err(|e| format!("Failed to write .mcp.json: {}", e))?; + + Ok("Project MCP configuration saved".to_string()) +} +``` + +These commands directly interact with the `.mcp.json` file in the project directory, allowing the UI to edit project-specific configurations without necessarily going through the `claude mcp` command for every change, although `claude` itself will still read this file when run within that project. + +## Conclusion + +In this final chapter, we explored **MCP (Model Context Protocol)**, the standard that allows the `claude` CLI to communicate with external AI model servers running outside the main Claude API. We learned that `claudia` leverages the `claude mcp` subcommand to manage configurations for these external servers, supporting different transport methods (stdio, sse) and scopes (user, project, local). + +We saw how the `claudia` UI provides dedicated sections to list, add, and import MCP servers, and how these actions map to backend Tauri commands. We then looked under the hood to understand that `claudia`'s backend primarily acts as a wrapper, executing `claude mcp` commands to let the `claude` CLI handle the actual configuration management and, during session execution, the communication with the external MCP servers. `claudia` also provides direct file-based management for project-scoped `.mcp.json` configurations. + +Understanding MCP highlights how `claudia` builds a flexible interface on top of `claude`, enabling connections to a potentially diverse ecosystem of AI tools and models that implement this protocol. This extends `claudia`'s capabilities beyond simply interacting with Anthropic's hosted services. + +This concludes our tutorial on the core concepts behind the `claudia` project. We hope this journey through its various components has provided you with a solid understanding of how this application works! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mcp.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPAddServer.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPImportExport.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPManager.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPServerList.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-2.Agents.md b/Claudia-docs/V_1/claudia-2.Agents.md new file mode 100644 index 00000000..d3b08287 --- /dev/null +++ b/Claudia-docs/V_1/claudia-2.Agents.md @@ -0,0 +1,526 @@ +# Chapter 2: Agents + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we learned how `claudia` helps you keep track of your conversations with Claude Code by organizing them into projects and sessions stored in the `~/.claude` directory. + +Now that you know how to find your past work, let's talk about the next key concept in `claudia`: **Agents**. + +## What is an Agent? + +Imagine you use Claude Code for different kinds of tasks. Sometimes you need it to act as a strict code reviewer, sometimes as a creative brainstorming partner, and other times as a focused debugger. Each task might require Claude to have a different "personality" or set of instructions. + +Instead of typing out the same long system prompt (the initial instructions you give to Claude) every time, `claudia` lets you save these configurations as **Agents**. + +Think of an Agent as a pre-packaged, specialized assistant you create within `claudia`. Each Agent is designed for a specific purpose, with its own instructions and capabilities already defined. + +**In simpler terms:** + +* An Agent is like a saved profile for how you want Claude Code to behave. +* You give it a name (like "Bug Hunter" or "Documentation Writer"). +* You give it an icon to easily spot it. +* You give it a "System Prompt" - this is the set of rules or instructions that tell Claude how to act for this specific Agent. For example, a "Bug Hunter" agent might have a system prompt like, "You are an expert Python debugger. Analyze the provided code snippets for potential bugs, common errors, and suggest fixes." +* You can set what permissions it has (like if it's allowed to read or write files). +* You choose which Claude model it should use (like Sonnet or Opus). + +Once an Agent is created, you can select it, give it a specific task (like "debug the function in `main.py`"), choose a project directory, and hit "Execute". `claudia` then runs the Claude Code CLI using *that Agent's* configuration. + +This is much more efficient than manually setting options every time you use Claude Code for a particular job! + +## Key Parts of an Agent + +Let's break down the core components that make up an Agent in `claudia`. You'll configure these when you create or edit an Agent: + +| Part | Description | Why it's important | +| :-------------- | :-------------------------------------------------------------------------- | :-------------------------------------------------- | +| **Name** | A human-readable label (e.g., "Code Reviewer", "Creative Writer"). | Helps you identify the Agent. | +| **Icon** | A visual symbol (e.g., 🤖, ✨, 🛠️). | Makes it easy to find the right Agent at a glance. | +| **System Prompt** | The core instructions given to Claude at the start of the conversation. | Defines the Agent's role, personality, and rules. | +| **Model** | Which Claude model (e.g., Sonnet, Opus) the Agent should use. | Affects performance, capabilities, and cost. | +| **Permissions** | Controls what the Agent is allowed to do (file read/write, network). | **Crucial for security** when running code or tools. | +| **Default Task**| Optional pre-filled text for the task input field when running the Agent. | Saves time for common tasks with this Agent. | + +## Creating and Managing Agents + +`claudia` provides a friendly user interface for managing your Agents. You'll typically find this in the main menu under something like "CC Agents". + +### The Agents List + +When you go to the Agents section, you'll see a list (or grid) of all the Agents you've created. + +You can see their name, icon, and options to: + +* **Execute:** Run the Agent with a new task. +* **Edit:** Change the Agent's configuration. +* **Delete:** Remove the Agent. +* **Create:** Add a brand new Agent. + +Let's look at a simplified frontend component (`CCAgents.tsx`) that displays this list: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +export const CCAgents: React.FC = ({ onBack, className }) => { + const [agents, setAgents] = useState([]); + // ... state for loading, view mode, etc. ... + + useEffect(() => { + // Fetch agents from the backend when the component loads + const loadAgents = async () => { + try { + const agentsList = await api.listAgents(); // Call backend API + setAgents(agentsList); + } catch (err) { + console.error("Failed to load agents:", err); + } + }; + loadAgents(); + }, []); + + // ... handleDeleteAgent, handleEditAgent, handleExecuteAgent functions ... + // ... state for pagination ... + + return ( + // ... layout code ... + {/* Agents Grid */} +
+ {/* Loop through the fetched agents */} + {agents.map((agent) => ( + + +
{/* Render agent icon */}
+

{agent.name}

+ {/* ... other agent info ... */} +
+ + {/* Buttons to Execute, Edit, Delete */} + + + + +
+ ))} +
+ // ... pagination and other UI elements ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This simplified code shows how the `CCAgents` component fetches a list of `Agent` objects from the backend using `api.listAgents()` and then displays them in cards, providing buttons for common actions. + +### Creating or Editing an Agent + +Clicking "Create" or "Edit" takes you to a different view (`CreateAgent.tsx`). Here, you'll find a form where you can fill in the details of the Agent: name, choose an icon, write the system prompt, select the model, set permissions, and add an optional default task. + +A snippet from the `CreateAgent.tsx` component: + +```typescript +// src/components/CreateAgent.tsx (Simplified) +// ... imports ... +export const CreateAgent: React.FC = ({ + agent, // If provided, we are editing + onBack, + onAgentCreated, + className, +}) => { + const [name, setName] = useState(agent?.name || ""); + const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || ""); + // ... state for icon, model, permissions, etc. ... + + const isEditMode = !!agent; + + const handleSave = async () => { + // ... validation ... + try { + // ... set saving state ... + if (isEditMode && agent.id) { + // Call backend API to update agent + await api.updateAgent(agent.id, name, /* ... other fields ... */ systemPrompt, /* ... */); + } else { + // Call backend API to create new agent + await api.createAgent(name, /* ... other fields ... */ systemPrompt, /* ... */); + } + onAgentCreated(); // Notify parent component + } catch (err) { + console.error("Failed to save agent:", err); + // ... show error ... + } finally { + // ... unset saving state ... + } + }; + + // ... handleBack function with confirmation ... + + return ( + // ... layout code ... +
+ {/* Header with Back and Save buttons */} +
+ +

{isEditMode ? "Edit CC Agent" : "Create CC Agent"}

+ +
+ + {/* Form fields */} +
+ {/* Name Input */} +
+ + setName(e.target.value)} /> +
+ + {/* Icon Picker */} + {/* ... component for selecting icon ... */} + + {/* Model Selection */} + {/* ... buttons/radios for model selection ... */} + + {/* Default Task Input */} + {/* ... input for default task ... */} + + {/* Sandbox Settings (Separate Component) */} + + + {/* System Prompt Editor */} +
+ + {/* ... MDEditor component for system prompt ... */} +
+
+
+ // ... Toast Notification ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component manages the state for the agent's properties and calls either `api.createAgent` or `api.updateAgent` from the backend API layer when the "Save" button is clicked. + +Notice the inclusion of `AgentSandboxSettings`. This is a smaller component (`AgentSandboxSettings.tsx`) specifically for managing the permission toggles: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +export const AgentSandboxSettings: React.FC = ({ + agent, // Receives the current agent state + onUpdate, // Callback to notify parent of changes + className +}) => { + // ... handleToggle function ... + + return ( + + {/* ... Header with Shield icon ... */} +
+ {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} // Update parent state + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} // Update parent state + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} // Update parent state + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} // Update parent state + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} +
+
+ ); +}; +``` + +This component simply displays the current sandbox settings for the agent and provides switches to toggle them. When a switch is toggled, it calls the `onUpdate` prop to inform the parent (`CreateAgent`) component, which manages the overall agent state. + +## Executing an Agent + +Once you have agents created, the main purpose is to *run* them. Selecting an agent from the list and clicking "Execute" (or the Play button) takes you to the Agent Execution view (`AgentExecution.tsx`). + +Here's where you: + +1. Select a **Project Path**: This is the directory where the agent will run and where it can potentially read/write files (subject to its permissions). This ties back to the projects we discussed in [Chapter 1: Session/Project Management](01_session_project_management_.md). +2. Enter the **Task**: This is the specific request you have for the agent *for this particular run*. +3. (Optional) Override the **Model**: Choose a different model (Sonnet/Opus) just for this run if needed. +4. Click **Execute**. + +The `AgentExecution.tsx` component handles this: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +export const AgentExecution: React.FC = ({ + agent, // The agent being executed + onBack, + className, +}) => { + const [projectPath, setProjectPath] = useState(""); + const [task, setTask] = useState(""); + const [model, setModel] = useState(agent.model || "sonnet"); // Default to agent's model + const [isRunning, setIsRunning] = useState(false); + const [messages, setMessages] = useState([]); // Output messages + // ... state for stats, errors, etc. ... + + // ... useEffect for listeners and timers ... + + const handleSelectPath = async () => { + // Use Tauri dialog to select a directory + const selected = await open({ directory: true, multiple: false }); + if (selected) { + setProjectPath(selected as string); + } + }; + + const handleExecute = async () => { + if (!projectPath || !task.trim()) return; // Basic validation + + try { + setIsRunning(true); + setMessages([]); // Clear previous output + // ... reset stats, setup listeners ... + + // Call backend API to execute the agent + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... show error, update state ... + } + }; + + // ... handleStop, handleBackWithConfirmation functions ... + + return ( + // ... layout code ... +
+ {/* Header with Back button and Agent Name */} +
+ +

{agent.name}

+ {/* ... Running status indicator ... */} +
+ + {/* Configuration Section */} +
+ {/* ... Error display ... */} + {/* Project Path Input with Select Button */} +
+ + setProjectPath(e.target.value)} disabled={isRunning} /> + +
+ {/* Model Selection Buttons */} + {/* ... buttons for Sonnet/Opus selection ... */} + {/* Task Input with Execute/Stop Button */} +
+ + setTask(e.target.value)} disabled={isRunning} /> + +
+
+ + {/* Output Display Section */} +
+ {/* Messages are displayed here, streaming as they arrive */} + {/* ... Rendering messages using StreamMessage component ... */} +
+ + {/* Floating Execution Control Bar */} + {/* ... Component showing elapsed time, tokens, etc. ... */} +
+ // ... Fullscreen Modal ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component uses the `api.executeAgent` Tauri command to start the agent's run. It also sets up event listeners (`agent-output`, `agent-error`, `agent-complete`) to receive data and status updates from the backend *while* the agent is running. This streaming output is then displayed to the user, which we'll cover in more detail in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md). + +## How it Works: Under the Hood + +Let's peek behind the curtain to understand how `claudia` handles Agents in the backend (Rust code). + +### Agent Storage + +Unlike projects and sessions which are managed by the Claude Code CLI itself in the filesystem (`~/.claude`), `claudia` stores its Agent definitions in a local SQLite database file, typically located within `claudia`'s application data directory (e.g., `~/.config/claudia/agents.db` on Linux, or similar paths on macOS/Windows). + +The `Agent` struct in the Rust backend corresponds to the data stored for each agent: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + pub id: Option, // Database ID + pub name: String, + pub icon: String, + pub system_prompt: String, + pub default_task: Option, + pub model: String, // e.g., "sonnet", "opus" + // Permissions managed directly on the agent struct + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, + pub enable_network: bool, + pub created_at: String, + pub updated_at: String, +} +// ... rest of the file +``` + +The database initialization (`init_database` function) creates the `agents` table to store this information. Backend functions like `list_agents`, `create_agent`, `update_agent`, and `delete_agent` interact with this SQLite database to perform the requested actions. They simply execute standard SQL commands (SELECT, INSERT, UPDATE, DELETE) to manage the `Agent` records. + +Here's a tiny snippet showing a database interaction (listing agents): + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[tauri::command] +pub async fn list_agents(db: State<'_, AgentDb>) -> Result, String> { + let conn = db.0.lock().map_err(|e| e.to_string())?; // Get database connection + + let mut stmt = conn + .prepare("SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC") + .map_err(|e| e.to_string())?; // Prepare SQL query + + let agents = stmt + .query_map([], |row| { // Map database rows to Agent structs + Ok(Agent { + id: Some(row.get(0)?), + name: row.get(1)?, + // ... map other fields ... + system_prompt: row.get(3)?, + model: row.get::<_, String>(5).unwrap_or_else(|_| "sonnet".to_string()), + sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true), + enable_file_read: row.get::<_, bool>(7).unwrap_or(true), + enable_file_write: row.get::<_, bool>(8).unwrap_or(true), + enable_network: row.get::<_, bool>(9).unwrap_or(false), + created_at: row.get(10)?, + updated_at: row.get(11)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(agents) // Return the list of Agent structs +} +``` + +This snippet shows how `list_agents` connects to the database, prepares a simple `SELECT` statement, and then uses `query_map` to convert each row returned by the database into an `Agent` struct, which is then sent back to the frontend. + +### Agent Execution Flow + +When you click "Execute" for an Agent: + +1. The frontend (`AgentExecution.tsx`) calls the backend command `execute_agent` ([Chapter 4: Tauri Commands](04_tauri_commands_.md)), passing the agent's ID, the selected project path, and the entered task. +2. The backend receives the call and retrieves the full details of the selected Agent from the database. +3. It creates a record in the `agent_runs` database table. This table keeps track of each individual execution run of an agent, including which agent was run, the task given, the project path, and its current status (pending, running, completed, failed, cancelled). This links back to the run history shown in the `CCAgents.tsx` component and managed by the `AgentRun` struct: + ```rust + // src-tauri/src/commands/agents.rs (Simplified) + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct AgentRun { + pub id: Option, // Database ID for this run + pub agent_id: i64, // Foreign key linking to the Agent + pub agent_name: String, // Stored for convenience + pub agent_icon: String, // Stored for convenience + pub task: String, // The task given for this run + pub model: String, // The model used for this run + pub project_path: String, // The directory where it was executed + pub session_id: String, // The UUID from the Claude Code CLI session + pub status: String, // 'pending', 'running', 'completed', 'failed', 'cancelled' + pub pid: Option, // Process ID if running + pub process_started_at: Option, + pub created_at: String, + pub completed_at: Option, + } + ``` + When the run starts, the status is set to 'running', and the Process ID (PID) is recorded. +4. Based on the Agent's configured permissions (`enable_file_read`, `enable_file_write`, `enable_network`), the backend constructs a sandbox profile. This process involves defining rules that the operating system will enforce to limit what the `claude` process can access or do. This is a core part of the [Sandboxing](06_sandboxing_.md) concept. +5. The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It includes arguments like: + * `-p "the task"` + * `--system-prompt "the agent's system prompt"` + * `--model "the selected model"` + * `--output-format stream-json` (to get structured output) + * `--dangerously-skip-permissions` (since `claudia` manages permissions via the sandbox, it tells `claude` not to ask the user). + * The command is also set to run in the specified project directory. +6. The backend then *spawns* the `claude` process within the sandbox environment. +7. As the `claude` process runs, its standard output (stdout) and standard error (stderr) streams are captured by the backend ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +8. The backend processes this output. For JSONL output from Claude Code, it extracts information like message content and session IDs. +9. It emits events back to the frontend (`agent-output`, `agent-error`) using the Tauri event system. +10. The frontend (`AgentExecution.tsx`) listens for these events and updates the displayed messages in real-time. +11. The backend also detects when the `claude` process finishes (either successfully, with an error, or if killed). +12. When the process finishes, the backend updates the `agent_runs` record in the database, setting the status to 'completed', 'failed', or 'cancelled' and recording the completion timestamp. + +Here's a simplified sequence diagram for Agent execution: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI (AgentExecution.tsx) + participant Backend as Tauri Commands (agents.rs) + participant Database as agents.db + participant Sandbox + participant ClaudeCLI as claude binary + + User->>Frontend: Clicks "Execute Agent" + Frontend->>Backend: Call execute_agent(agentId, path, task, model) + Backend->>Database: Read Agent config by ID + Database-->>Backend: Return Agent config + Backend->>Database: Create AgentRun record (status=pending/running) + Database-->>Backend: Return runId + Backend->>Sandbox: Prepare environment based on Agent permissions + Sandbox-->>Backend: Prepared environment/command + Backend->>ClaudeCLI: Spawn process (with task, prompt, model, in sandbox, in project path) + ClaudeCLI-->>Backend: Stream stdout/stderr (JSONL) + Backend->>Frontend: Emit "agent-output" events (parsed messages) + Frontend->>User: Display messages in UI + ClaudeCLI-->>Backend: Process finishes + Backend->>Database: Update AgentRun record (status=completed/failed/cancelled) + Database-->>Backend: Confirmation + Backend->>Frontend: Emit "agent-complete" event + Frontend->>User: Update UI (execution finished) +``` + +This diagram illustrates how the frontend initiates the run, the backend fetches the agent's configuration, prepares the environment (including sandbox rules), launches the `claude` process, captures its output, and updates the UI and database based on the process's progress and completion. + +## Conclusion + +In this chapter, we introduced the concept of **Agents** in `claudia`. We learned that Agents are customizable configurations for the Claude Code CLI, allowing you to define specific roles, instructions (System Prompt), models, and crucially, permissions for different types of tasks. + +We saw how the `claudia` UI allows you to easily create, edit, list, and execute these Agents, and how the backend stores Agent definitions in a local database. We also got a high-level view of the execution process, understanding that `claudia` launches the `claude` binary with the Agent's settings and captures its output. A key part of this is the preparation of a secure execution environment based on the Agent's defined permissions, which introduces the idea of sandboxing. + +Understanding Agents is fundamental, as they are the primary way you'll interact with Claude Code through `claudia` for structured tasks. In the next chapter, we'll zoom out and look at how the different visual parts of the `claudia` application you've seen connect together – diving into [Frontend UI Components](03_frontend_ui_components_.md). + +[Next Chapter: Frontend UI Components](03_frontend_ui_components_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[3]] +``` +(https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CCAgents.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CreateAgent.tsx) diff --git a/Claudia-docs/V_1/claudia-3.Frontend UI Components.md b/Claudia-docs/V_1/claudia-3.Frontend UI Components.md new file mode 100644 index 00000000..18288e56 --- /dev/null +++ b/Claudia-docs/V_1/claudia-3.Frontend UI Components.md @@ -0,0 +1,328 @@ +# Chapter 3: Frontend UI Components + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we explored how `claudia` keeps track of your conversations. In [Chapter 2: Agents](02_agents_.md), we learned about creating and managing specialized configurations for Claude Code tasks. + +Now, let's shift our focus to what you actually *see* and *interact with* when you use `claudia`: its graphical interface. This interface is built using **Frontend UI Components**. + +## What are Frontend UI Components? + +Imagine building something complex, like a house. You don't start by crafting every tiny screw and nail from raw metal. Instead, you use pre-made bricks, windows, doors, and roof tiles. These are like reusable building blocks. + +Frontend UI Components in `claudia` are exactly like these building blocks, but for the visual parts of the application. They are self-contained pieces of the user interface, like: + +* A **Button** you click. +* A **Card** that displays information (like a project or an agent). +* An **Input** field where you type text. +* A **List** that shows multiple items. +* A **Dialog** box that pops up. + +`claudia` uses a popular web development framework called **React** to build these components. They are written using **TypeScript** (which adds type safety) and styled using **Tailwind CSS** (a way to add styles quickly using special class names). + +The key idea is reusability. Instead of designing a button from scratch every time it's needed, you create a `Button` component once and use it everywhere. This makes the UI consistent and development faster. + +## Building Views by Combining Components + +Just like you combine bricks and windows to build a wall, `claudia` combines different UI components to create full views (pages) of the application. + +For example, the list of projects you saw in Chapter 1 is a view. This view isn't one giant piece of code; it's made by combining: + +* `Button` components (like the "Back to Home" button). +* `Card` components, each displaying information about a single project. +* A `ProjectList` component which *contains* all the individual project `Card`s and handles looping through the list of projects. +* Layout components (like `div`s with Tailwind classes) to arrange everything. + +Let's look at a simplified structure of the `App.tsx` file, which acts like the main blueprint for `claudia`'s views. It decides *which* major component (view) to show based on the current state (`view` variable): + +```typescript +// src/App.tsx (Simplified) +import { useState } from "react"; +import { Button } from "@/components/ui/button"; // Import a UI component +import { Card } from "@/components/ui/card"; // Import another UI component +import { ProjectList } from "@/components/ProjectList"; // Import a view component +import { CCAgents } from "@/components/CCAgents"; // Import another view component +// ... other imports ... + +type View = "welcome" | "projects" | "agents" | "settings" | "claude-code-session"; + +function App() { + const [view, setView] = useState("welcome"); // State variable to control current view + // ... other state variables ... + + const renderContent = () => { + switch (view) { + case "welcome": + // Show the welcome view, using Card and Button components + return ( +
{/* Layout */} + setView("agents")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Agents

+
+
+ setView("projects")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Projects

+
+
+
+ ); + + case "agents": + // Show the Agents view, which is handled by the CCAgents component + return setView("welcome")} />; // Uses CCAgents component + + case "projects": + // Show the Projects/Sessions view + return ( +
{/* Layout */} + {/* Uses Button */} + {/* ... displays either ProjectList or SessionList based on selectedProject state ... */} +
+ ); + + // ... other cases for settings, session view, etc. ... + + default: + return null; + } + }; + + return ( +
+ {/* Topbar component */} + {/* Main content area */} +
+ {renderContent()} {/* Renders the selected view */} +
+ {/* ... other global components like dialogs ... */} +
+ ); +} + +export default App; +``` + +As you can see, `App.tsx` doesn't contain the detailed code for *every* button or card. Instead, it imports and uses components like `Button`, `Card`, `CCAgents`, and `ProjectList`. The `renderContent` function simply decides which larger component to display based on the `view` state. + +## How Components Work Together + +Components communicate with each other primarily through **props** (short for properties) and **callbacks** (functions passed as props). + +* **Props:** Data is passed *down* from parent components to child components using props. For example, the `App` component might pass the list of `projects` to the `ProjectList` component. The `ProjectList` component then passes individual `project` objects down to the `Card` components it renders. +* **Callbacks:** When something happens inside a child component (like a button click), it needs to tell its parent. It does this by calling a function that was passed down as a prop (a callback). For example, when a `Card` in the `ProjectList` is clicked, it calls the `onProjectClick` function that was given to it by `ProjectList`. `ProjectList` received this function from `App`. + +Let's revisit the `ProjectList` component from Chapter 1: + +```typescript +// src/components/ProjectList.tsx (Simplified) +// ... imports ... +import { Card, CardContent } from "@/components/ui/card"; // Uses Card component +import type { Project } from "@/lib/api"; + +interface ProjectListProps { + projects: Project[]; // Prop: receives array of project data + onProjectClick: (project: Project) => void; // Prop: receives a function (callback) +} + +export const ProjectList: React.FC = ({ projects, onProjectClick }) => { + return ( +
+ {/* Loops through the projects array received via props */} + {projects.map((project) => ( + // Renders a Card component for each project + onProjectClick(project)}> {/* Calls the onProjectClick callback when clicked */} + {/* Uses CardContent sub-component */} +
+

{project.path}

{/* Displays data received from the project prop */} +

{project.sessions.length} sessions

{/* Displays data from the project prop */} +
+
+
+ ))} +
+ ); +}; +``` + +This component clearly shows: +1. It receives data (`projects` array) and a function (`onProjectClick`) as props. +2. It loops through the `projects` array. +3. For each item, it renders a `Card` component (another UI component). +4. It passes data (`project.path`, `project.sessions.length`) into the `CardContent` to be displayed. +5. It attaches an `onClick` handler to the `Card` that calls the `onProjectClick` callback function, passing the relevant `project` data back up to the parent component (`App` in this case). + +Similarly, the `CCAgents` component from Chapter 2 receives data and callbacks: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +import { Card, CardContent, CardFooter } from "@/components/ui/card"; // Uses Card components +import { Button } from "@/components/ui/button"; // Uses Button component +// ... types and state ... + +export const CCAgents: React.FC = ({ onBack, className }) => { + // ... state for agents data ... + + // ... useEffect to load agents (calls backend, covered in Chapter 2) ... + + // Callback functions for actions + const handleExecuteAgent = (agent: Agent) => { + // ... navigate to execution view ... + }; + const handleEditAgent = (agent: Agent) => { + // ... navigate to edit view ... + }; + const handleDeleteAgent = (agentId: number) => { + // ... call backend API to delete ... + }; + + return ( +
+ {/* ... Back button using Button component calling onBack prop ... */} + + {/* Agents Grid */} +
+ {/* Loop through agents state */} + {agents.map((agent) => ( + {/* Uses Card */} + {/* Uses CardContent */} + {/* ... display agent icon, name (data from agent state) ... */} + + {/* Uses CardFooter */} + {/* Buttons using Button component, calling local callbacks */} + + + + + + ))} +
+ {/* ... pagination ... */} +
+ ); +}; +``` + +This component shows how UI components (`Card`, `Button`) are used within a larger view component (`CCAgents`). `CCAgents` manages its own state (the list of `agents`) and defines callback functions (`handleExecuteAgent`, `handleEditAgent`, `handleDeleteAgent`) which are triggered by user interaction with the child `Button` components. It also receives an `onBack` prop from its parent (`App`) to navigate back. + +## Common UI Components in `claudia` + +`claudia` uses a set of pre-built, simple UI components provided by a library often referred to as "shadcn/ui" (though integrated directly into the project). You saw some examples in the code: + +* **`Button`**: Used for clickable actions (`components/ui/button.tsx`). +* **`Card`**: Used to group related information with a border and shadow (`components/ui/card.tsx`). It often has `CardHeader`, `CardContent`, and `CardFooter` sub-components for structure. +* **`Input`**: Used for single-line text entry fields (similar to standard HTML ``, used in `CreateAgent`, `AgentExecution`). +* **`Textarea`**: Used for multi-line text entry, like for the system prompt (`components/ui/textarea.tsx`, used in `CreateAgent`). +* **`Switch`**: Used for toggling options on/off, like permissions in the sandbox settings (`components/ui/switch.tsx`, used in `AgentSandboxSettings`). +* **`Label`**: Used to associate text labels with form elements (`components/ui/label.tsx`). +* **`Popover`**: Used to display floating content when a trigger is clicked (`components/ui/popover.tsx`). +* **`Toast`**: Used for temporary notification messages (`components/ui/toast.tsx`). + +You can find these components and others in the `src/components/ui/` directory. Each file defines a single, reusable UI component using React's functional component pattern, TypeScript for typing props, and Tailwind CSS classes for styling. + +For example, the `Button` component (`components/ui/button.tsx`) defines different visual `variant`s (default, destructive, outline, secondary, ghost, link) and `size`s (default, sm, lg, icon) using `class-variance-authority` and then applies the corresponding Tailwind classes (`cn` utility combines class names). When you use ``. + +## How it Works: Under the Hood (Frontend) + +The core idea behind these UI components in React is quite simple: + +1. **They are functions or classes:** A component is essentially a JavaScript/TypeScript function (or class) that receives data as `props`. +2. **They return UI:** This function returns a description of what the UI should look like (React elements, often resembling HTML). +3. **React renders the UI:** React takes this description and efficiently updates the actual web page (the Document Object Model or DOM) to match. +4. **State for interactivity:** Some components manage their own internal data called `state` (e.g., an input component's text value, whether a dialog is open). When state changes, the component re-renders. +5. **Event Handlers:** Components respond to user interactions (like clicks, typing) by calling functions defined within them or received via props (callbacks). + +The process looks like this: + +```mermaid +graph TD + A[App.tsx] --> B(Passes props like projects, callbacks like handleProjectClick) + B --> C{ProjectList Component} + C --> D(Iterates through projects, passes individual project + onProjectClick to Cards) + D --> E{Card Component (for a single project)} + E --> F(Receives project data + onProjectClick) + F -- Displays Data --> G[UI on screen (a Card)] + G -- User Clicks Card --> H(onClick handler in Card) + H --> I(Calls the onProjectClick callback received via props) + I --> J(Returns the clicked project data) + J --> C(ProjectList receives data) + C --> K(Calls the onProjectClick callback received via props) + K --> A(App.tsx receives clicked project data) + A -- Updates state (e.g., setSelectedProject) --> A + A -- Re-renders with new view --> L[New UI on screen (e.g., SessionList)] +``` + +This diagram shows the flow of data (props) and events (callbacks) that allows components to work together to create a dynamic interface. `App.tsx` is at the top, managing the main state (`view`, `selectedProject`). It passes data and functions down to its children (`ProjectList`). `ProjectList` loops and renders more children (`Card`). When a `Card` receives a user action, it calls a function passed down (`onProjectClick`), sending relevant data back up the chain, which triggers state changes in the parent (`App`), leading to a re-render and a different view being displayed. + +## Conclusion + +In this chapter, we explored Frontend UI Components, the reusable building blocks that form the visual interface of `claudia`. We learned that these components, built with React, TypeScript, and Tailwind CSS, are combined like Lego bricks to create complex views like project lists, agent managers, and the main session interface. + +We saw how components receive data through `props` and communicate back to their parents using `callbacks`. This system allows the UI to be modular, consistent, and maintainable. Understanding these components is key to seeing how `claudia` presents information and interacts with the user. + +In the next chapter, we'll bridge the gap between the frontend UI components and the backend Rust logic by learning about [Tauri Commands](04_tauri_commands_.md). These commands are the communication layer that allows the components to ask the backend for data (like listing projects) or request actions (like executing an agent). + +[Next Chapter: Tauri Commands](04_tauri_commands_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/App.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/index.ts), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/badge.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/button.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/card.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/popover.tsx), [[7]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/textarea.tsx) +``` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-4.Tauri Commands.md b/Claudia-docs/V_1/claudia-4.Tauri Commands.md new file mode 100644 index 00000000..b1cb0b4c --- /dev/null +++ b/Claudia-docs/V_1/claudia-4.Tauri Commands.md @@ -0,0 +1,299 @@ +# Chapter 4: Tauri Commands + +Welcome back to the `claudia` tutorial! In [Chapter 3: Frontend UI Components](03_frontend_ui_components_.md), we explored the visual building blocks that make up `claudia`'s interface, like Buttons and Cards, and how they communicate with each other using props and callbacks. + +But those frontend components, written in TypeScript/JavaScript, can't directly talk to your operating system. They can't read files, launch other programs, or perform heavy computations safely and efficiently. This is where the backend, written in Rust, comes in. + +We need a way for the frontend UI (your browser-like window) to ask the powerful native backend to do things for it. That communication bridge is what **Tauri Commands** are all about. + +## What are Tauri Commands? + +Think of Tauri Commands as a special "phone line" or "API" that connects the frontend world (where the user clicks buttons and sees things) to the backend world (where the native code runs). + +When the user clicks a button in `claudia`'s UI, and that button needs to do something like: + +* List your projects (which requires reading the file system). +* Create a new Agent (which requires saving to a database). +* Execute a Claude Code session (which requires launching a separate process). + +...the frontend can't do this itself. Instead, it calls a specific **Tauri Command** that lives in the Rust backend. The backend command performs the requested action and then sends the result back to the frontend. + +**In simple terms:** + +* Tauri Commands are functions in the Rust backend. +* They are specifically marked so that Tauri knows they can be called from the frontend. +* The frontend calls these functions using a special `invoke` mechanism provided by Tauri. +* This allows the frontend to trigger native actions and get data from the backend. + +This separation keeps the UI responsive and safe, while the backend handles the heavy lifting and privileged operations. + +## How to Call a Tauri Command from the Frontend + +In `claudia`'s frontend (written in TypeScript), you call a backend command using the `invoke` function from the `@tauri-apps/api/core` library. + +The `invoke` function is straightforward: + +```typescript +import { invoke } from "@tauri-apps/api/core"; + +// ... later in your component or API helper ... + +async function exampleCall() { + try { + // Call the command named 'list_projects' + // If the command takes arguments, pass them as the second parameter (an object) + const result = await invoke("list_projects"); + + console.log("Projects received:", result); // Handle the result + // result will be the value returned by the Rust function + + } catch (error) { + console.error("Error calling list_projects:", error); // Handle errors + } +} + +// To actually trigger it, you might call exampleCall() in response to a button click or when a page loads. +``` + +Let's look at the `src/lib/api.ts` file, which we briefly mentioned in previous chapters. This file provides a cleaner way to call backend commands instead of using `invoke` directly everywhere. It defines functions like `listProjects`, `getProjectSessions`, `listAgents`, `createAgent`, `executeAgent`, etc., which wrap the `invoke` calls. + +Here's how the `listProjects` function is defined in `src/lib/api.ts`: + +```typescript +// src/lib/api.ts (Simplified) +import { invoke } from "@tauri-apps/api/core"; +// ... other imports and type definitions ... + +/** + * Represents a project in the ~/.claude/projects directory + */ +export interface Project { + // ... project fields ... +} + +/** + * API client for interacting with the Rust backend + */ +export const api = { + /** + * Lists all projects in the ~/.claude/projects directory + * @returns Promise resolving to an array of projects + */ + async listProjects(): Promise { // Defines a friendly TypeScript function + try { + // Calls the actual Tauri command named "list_projects" + return await invoke("list_projects"); + } catch (error) { + console.error("Failed to list projects:", error); + throw error; // Re-throw the error for the caller to handle + } + }, + + // ... other API functions like getProjectSessions, listAgents, etc. +}; +``` + +Now, in a frontend component like `ProjectList.tsx` or its parent view, instead of `invoke`, you'll see code calling `api.listProjects()`: + +```typescript +// src/components/ProjectList.tsx (Simplified - from Chapter 1) +import React, { useEffect, useState } from 'react'; +// ... other imports ... +import { api, type Project } from "@/lib/api"; // Import the api client and types + +// ... component definition ... + +export const ProjectList: React.FC = ({ onProjectClick }) => { + const [projects, setProjects] = useState([]); + // ... other state ... + + useEffect(() => { + // Fetch projects from the backend when the component loads + const loadProjects = async () => { + try { + // Call the backend command via the api helper + const projectsList = await api.listProjects(); + setProjects(projectsList); // Update the component's state with the data + } catch (err) { + console.error("Failed to load projects:", err); + } + }; + loadProjects(); // Call the function to load data + }, []); // Empty dependency array means this runs once after initial render + + // ... render function using the 'projects' state ... + // Uses projects.map to display each project (as shown in Chapter 1) +}; +``` + +This shows the typical pattern: A frontend component needs data, so it calls a function in `src/lib/api.ts` (like `api.listProjects`), which in turn uses `invoke` to call the corresponding backend command. The component then uses the received data (`projectsList`) to update its state and render the UI. + +## How to Define a Tauri Command in the Backend (Rust) + +Now, let's look at the other side: how the backend tells Tauri that a specific Rust function can be called as a command. + +This is done using the `#[tauri::command]` attribute right above the function definition. These command functions typically live in modules within the `src-tauri/src/commands/` directory (like `claude.rs` or `agents.rs`). + +Here's the simplified Rust code for the `list_projects` command, located in `src-tauri/src/commands/claude.rs`: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +use tauri::command; +use serde::{Serialize, Deserialize}; // Needed for sending data back + +// Define the structure that will be sent back to the frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, + pub path: String, + pub sessions: Vec, + pub created_at: u64, +} + +// Mark this function as a Tauri command +#[command] +pub async fn list_projects() -> Result, String> { + // ... Code to find ~/.claude and read project directories ... + // This is where the file system access happens (backend logic) + + let mut projects = Vec::new(); + + // Simplified: Imagine we found some projects and populated the 'projects' vector + // For a real implementation, see the detailed code snippet in Chapter 1 + + // Example placeholder data: + projects.push(Project { + id: "encoded-path-1".to_string(), + path: "/path/to/my/project1".to_string(), + sessions: vec!["session1_id".to_string(), "session2_id".to_string()], + created_at: 1678886400, // Example timestamp + }); + projects.push(Project { + id: "encoded-path-2".to_string(), + path: "/path/to/my/project2".to_string(), + sessions: vec!["session3_id".to_string()], + created_at: 1678972800, // Example timestamp + }); + + + // Return the vector of Project structs. + // Result is often used for commands that might fail. + // Tauri automatically serializes Vec into JSON for the frontend. + Ok(projects) +} + +// ... other commands defined in this file ... +``` + +Key points here: + +1. `#[tauri::command]`: This attribute is essential. It tells Tauri to generate the necessary code to make this Rust function callable from the frontend JavaScript. +2. `pub async fn`: Commands are typically `async` functions because they often perform non-blocking operations (like reading files, launching processes) that shouldn't block the main UI thread. They must also be `pub` (public) so Tauri can access them. +3. `Result, String>`: This is the return type. `Result` is a standard Rust type for handling operations that can either succeed (`Ok`) or fail (`Err`). Here, on success, it returns a `Vec` (a list of `Project` structs); on failure, it returns a `String` error message. Tauri handles converting this Rust `Result` into a JavaScript Promise that resolves on `Ok` and rejects on `Err`. +4. `#[derive(Serialize, Deserialize)]`: Any custom data structures (like `Project` here) that you want to send between the frontend and backend must be able to be converted to/from a common format like JSON. `serde` is a Rust library for this, and deriving `Serialize` and `Deserialize` (for data going back and forth) makes this automatic. + +## Registering Commands + +Finally, for Tauri to know about your command functions, they need to be registered in the main application entry point, `src-tauri/src/main.rs`. + +In `src-tauri/src/main.rs`, there's a section using `tauri::generate_handler!` that lists all the command functions that the frontend is allowed to call: + +```rust +// src-tauri/src/main.rs (Simplified) +// ... imports ... + +mod commands; // Import your commands module + +use commands::claude::{ + list_projects, // Import the specific command functions + get_project_sessions, + // ... import other claude commands ... +}; +use commands::agents::{ + list_agents, // Import agent commands + create_agent, + execute_agent, + // ... import other agent commands ... +}; +// ... import commands from other modules like sandbox, usage, mcp ... + +fn main() { + // ... setup code ... + + tauri::Builder::default() + // ... plugins and setup ... + .invoke_handler(tauri::generate_handler![ // **This is where commands are registered!** + list_projects, // List the name of each command function + get_project_sessions, + list_agents, + create_agent, + execute_agent, + // ... list all other commands you want to expose ... + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +The `tauri::generate_handler! macro` takes a list of function names that are marked with `#[tauri::command]`. It generates the code needed for Tauri's core to receive `invoke` calls from the frontend and route them to the correct Rust function. If a command isn't listed here, the frontend can't call it. + +## How it Works: Under the Hood + +Let's visualize the flow when the frontend calls a Tauri Command. + +Imagine the user is on the Projects screen, and the `ProjectList` component needs the list of projects: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ProjectList.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant Filesystem as Filesystem + + FrontendUI->>FrontendAPI: Need projects list + FrontendAPI->>TauriCore: invoke("list_projects") + Note over TauriCore: Tauri routes call to registered handler + TauriCore->>BackendCommands: Call list_projects() function + BackendCommands->>Filesystem: Read ~/.claude/projects + Filesystem-->>BackendCommands: Return directory contents + BackendCommands->>BackendCommands: Process data (create Project structs) + BackendCommands-->>TauriCore: Return Result, String> + TauriCore-->>FrontendAPI: Resolve invoke Promise with Vec + FrontendAPI-->>FrontendUI: Return projects data + FrontendUI->>FrontendUI: Update state with projects data + FrontendUI->>FrontendUI: Render UI (display projects) +``` + +1. The `ProjectList` component (Frontend UI) decides it needs the list of projects, perhaps in a `useEffect` hook when it mounts. +2. It calls `api.listProjects()` (Frontend API wrapper). +3. `api.listProjects()` calls `invoke("list_projects")`, which sends a message to the Tauri Core. +4. The Tauri Core receives the message "call command 'list\_projects'" and looks up the corresponding registered Rust function. +5. The Tauri Core executes the `list_projects()` function in the Backend Commands module. +6. The Rust function performs its logic, which involves interacting with the Filesystem (reading directories and files). +7. The Filesystem returns the necessary data to the Rust function. +8. The Rust function processes this data and constructs the `Vec` result. +9. The Rust function returns the `Result, String>`. Tauri automatically serializes the `Vec` into JSON. +10. The Tauri Core receives the result and sends it back to the frontend process. +11. The Promise returned by the initial `invoke` call in `api.ts` resolves with the JSON data, which Tauri automatically deserializes back into a TypeScript `Project[]` array. +12. `api.listProjects()` returns this array to the `ProjectList` component. +13. The `ProjectList` component updates its internal state, triggering React to re-render the component, displaying the list of projects on the screen. + +This same pattern is used for almost all interactions where the frontend needs to get information or trigger actions in the backend. For example, when you click "Execute" for an Agent (as seen in Chapter 2), the `AgentExecution.tsx` component calls `api.executeAgent()`, which calls the backend `execute_agent` command, which then launches the `claude` binary (as we'll see in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). + +## Conclusion + +In this chapter, we learned about **Tauri Commands**, the essential communication layer that bridges the gap between the frontend UI (built with React/TypeScript) and the native backend logic (written in Rust). + +We saw how the frontend uses `invoke` (often wrapped by helpful functions in `src/lib/api.ts`) to call named backend commands, passing arguments and receiving results via Promises. We also saw how backend Rust functions are defined using `#[tauri::command]`, must be `pub async fn`, return a `Result`, and how data is serialized using `serde`. Finally, we looked at how these commands are registered in `src-tauri/src/main.rs` using `tauri::generate_handler!`. + +Understanding Tauri Commands is crucial because they are the fundamental way `claudia`'s UI interacts with the powerful, native capabilities provided by the Rust backend. This mechanism allows the frontend to stay focused on presentation while relying on the backend for tasks like file system access, process management, and database interaction. + +In the next chapter, we'll delve into the very core of `claudia`'s function: how it interacts with the command-line `claude` binary to run sessions and execute tasks. + +[Next Chapter: Claude CLI Interaction](05_claude_cli_interaction_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mod.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/lib.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/main.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/lib/api.ts) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-5.Claude CLI interaction.md b/Claudia-docs/V_1/claudia-5.Claude CLI interaction.md new file mode 100644 index 00000000..e2e64675 --- /dev/null +++ b/Claudia-docs/V_1/claudia-5.Claude CLI interaction.md @@ -0,0 +1,373 @@ +# Chapter 5: Claude CLI Interaction + +Welcome back to the `claudia` tutorial! In our previous chapters, we learned about managing your work with Claude Code through [Session/Project Management](01_session_project_management_.md), creating specialized [Agents](02_agents_.md) to define how Claude should behave, how the [Frontend UI Components](03_frontend_ui_components_.md) like buttons and lists build the interface, and how [Tauri Commands](04_tauri_commands_.md) allow the frontend (TypeScript/React) to talk to the backend (Rust). + +Now, let's dive into the core action: how `claudia` actually makes the powerful `claude` command-line tool run and communicate with it. This chapter is all about the **Claude CLI Interaction** layer. + +## The Problem: GUI Needs to Talk to CLI + +You're using `claudia`, which is a beautiful graphical application. You click buttons, type in text boxes, and see output in a nice interface. But the actual intelligence, the part that runs your requests and generates code or text, is the `claude` command-line interface (CLI) tool that you installed separately. + +So, how does `claudia`'s backend, written in Rust, tell the `claude` CLI, which is a separate program running on your computer, what to do? How does it get the response back in real-time to show you? + +This is exactly what the Claude CLI Interaction part of `claudia` handles. It's the bridge between the graphical application and the underlying CLI tool. + +Imagine you're the director of an orchestra (`claudia`). You have a conductor's stand (the UI), but the music is played by the musicians (`claude`). You need a way to signal to the musicians what piece to play, at what tempo, and capture their performance to share with the audience. `claudia`'s CLI Interaction is your way of signaling to the `claude` process and listening to its "music" (the output). + +## What the Claude CLI Interaction Does + +The core function of this layer in `claudia`'s backend is to: + +1. **Find the `claude` binary:** Figure out where the `claude` executable is located on your system. +2. **Prepare the command:** Build the command line that needs to be run, including the `claude` binary path and all the necessary arguments (like the prompt, model, system prompt, etc.). +3. **Spawn the process:** Start the `claude` binary as a separate process. +4. **Control the environment:** Set the working directory for the `claude` process (the project path) and potentially adjust its environment variables (like the PATH). +5. **Manage sandboxing (Optional but important):** If sandboxing is enabled, ensure the `claude` process runs within the defined security restrictions (more on this in [Chapter 6: Sandboxing](06_sandboxing_.md)). +6. **Capture output:** Get the standard output (stdout) and standard error (stderr) streams from the running `claude` process in real-time. +7. **Process output:** Take the raw output (which is in a special JSONL format for Claude Code) and process it. +8. **Report status/output:** Send the processed output and status updates (running, complete, failed) back to the frontend so the user interface can update. +9. **Manage process lifecycle:** Keep track of the running process and handle requests to stop or kill it. + +## Triggering a Claude Code Run from the Frontend + +You've already seen in Chapter 4 how frontend components use `api` functions to call backend commands. This is how you initiate a Claude Code run. + +Whether you're executing an Agent (from `AgentExecution.tsx`) or starting/continuing a direct session (from `ClaudeCodeSession.tsx`), the frontend makes a call to a specific backend command responsible for launching `claude`. + +Here's a simplified look at how `AgentExecution.tsx` initiates a run: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { api, type Agent } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleExecute = async () => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Set up event listeners first (covered in Chapter 7) + // These listeners will receive output and status updates from the backend + const outputUnlisten = await listen("agent-output", (event) => { + // Process received output line (JSONL) + // ... update messages state ... + }); + const completeUnlisten = await listen("agent-complete", (event) => { + // Process completion status + // ... update isRunning state ... + }); + // ... store unlisten functions ... + + // Call the backend command to execute the agent + // This command prepares and spawns the 'claude' process + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... handle error ... + } +}; + +// ... render function with button calling handleExecute ... +``` + +And here's a similar pattern from `ClaudeCodeSession.tsx` for starting a new session: + +```typescript +// src/components/ClaudeCodeSession.tsx (Simplified) +// ... imports ... +import { api, type Session } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Add the user message to the UI immediately + // ... update messages state ... + + // Clean up old listeners, set up new ones (for "claude-output", "claude-complete") + // ... setup listeners (covered in Chapter 7) ... + + // Call the appropriate backend command + // This command prepares and spawns the 'claude' process + if (isFirstPrompt && !session) { + await api.executeClaudeCode(projectPath, prompt, model); // New session + } else if (session && isFirstPrompt) { + await api.resumeClaudeCode(projectPath, session.id, prompt, model); // Resume session + } else { + await api.continueClaudeCode(projectPath, prompt, model); // Continue conversation + } + + } catch (err) { + console.error("Failed to send prompt:", err); + // ... handle error ... + } +}; + +// ... render function with FloatingPromptInput component calling handleSendPrompt ... +``` + +These snippets show that from the frontend's perspective, starting a Claude Code interaction is simply calling a backend API function (a Tauri Command wrapper) and then listening for events that the backend sends back as the process runs and finishes. + +## How it Works: Under the Hood (Backend) + +When the backend receives a Tauri command like `execute_agent` or `execute_claude_code`, it performs a series of steps to launch and manage the `claude` process. + +Here's a simplified step-by-step flow: + +1. **Find the `claude` executable:** The backend needs the full path to the `claude` binary. It looks in common installation locations and potentially a path saved in `claudia`'s settings. +2. **Determine process parameters:** It gathers the necessary information for the command: the prompt (`-p`), the system prompt (`--system-prompt`, from the Agent config or CLAUDE.md), the model (`--model`), the output format (`--output-format stream-json` is crucial for real-time processing), flags like `--verbose` and `--dangerously-skip-permissions` (since `claudia` handles permissions via sandboxing), and the working directory (`--current-dir` or set via `Command`). +3. **Prepare Sandbox (if enabled):** Based on Agent permissions or global settings, the backend constructs sandbox rules using the `gaol` library. This involves defining what file paths (`file_read_all`, `file_write_all`) and network connections (`network_outbound`) the `claude` process is allowed to make. This is tightly linked to the actual command execution. +4. **Build the Command object:** Rust's standard library (and the `tokio` library for asynchronous operations) provides a `Command` struct to build process commands. The backend creates a `Command` instance, sets the `claude` binary path, adds all the arguments, sets the working directory (`current_dir`), and configures standard input/output (`stdin`, `stdout`, `stderr`) to be piped so the backend can capture them. +5. **Spawn the child process:** The `Command` object is executed using a method like `spawn()`. This starts the `claude` process and gives the backend a handle to it (a `Child` object). +6. **Capture Output Streams:** The `stdout` and `stderr` streams of the child process, which were configured to be piped, are now available as asynchronous readers. The backend spawns separate asynchronous tasks (using `tokio::spawn`) to continuously read lines from these streams. +7. **Process and Emit:** As each line of output (usually a JSON object in the JSONL format) or error arrives, the reading tasks process it (e.g., parse JSON, extract relevant data) and immediately emit it as a Tauri event back to the frontend (`agent-output`, `claude-output`, `agent-error`, `claude-error`). This provides the real-time streaming experience. +8. **Monitor Completion:** The backend also has a task that waits for the `claude` process to finish (`child.wait().await`). When it exits, the task notifies the frontend (e.g., via `agent-complete`, `claude-complete`) and potentially updates internal state or a database record (like the `agent_runs` table for Agents). +9. **Handle Cancellation:** If the user requests to stop the process (e.g., clicking a "Stop" button for an Agent run), the backend uses the process ID (PID) to send a termination signal (`kill`). + +Here's a sequence diagram showing the flow for a standard `execute_claude_code` call: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ClaudeCodeSession.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + FrontendUI->>FrontendAPI: User submits prompt (call executeClaudeCode) + FrontendAPI->>TauriCore: invoke("execute_claude_code", { prompt, path, model }) + Note over TauriCore: Tauri routes call + TauriCore->>BackendCommands: Call execute_claude_code() + BackendCommands->>BackendCommands: Find claude binary path (find_claude_binary) + BackendCommands->>BackendCommands: Prepare Command object (args, cwd, piped streams) + BackendCommands->>OS: Spawn process (claude binary) + OS-->>BackendCommands: Return Child process handle + BackendCommands->>BackendCommands: Spawn tasks to read stdout/stderr + loop While ClaudeCLI is running & produces output + ClaudeCLI-->>OS: Write to stdout/stderr pipe + OS-->>BackendCommands: Data available in pipe + BackendCommands->>BackendCommands: Read & process output line + BackendCommands->>TauriCore: Emit "claude-output" or "claude-error" event + TauriCore-->>FrontendUI: Receive event data + FrontendUI->>FrontendUI: Display output line in UI + end + ClaudeCLI-->>OS: Process exits + OS-->>BackendCommands: Process termination status + BackendCommands->>BackendCommands: Task waits for process exit + BackendCommands->>TauriCore: Emit "claude-complete" event + TauriCore-->>FrontendUI: Receive event + FrontendUI->>FrontendUI: Update UI (execution finished) +``` + +This diagram visually outlines how the request flows from the frontend to the backend, how the backend launches the separate `claude` process via the OS, how output streams back through the backend and Tauri, and finally how the frontend is updated in real-time. + +## Diving into the Backend Code + +Let's look at some key parts of the Rust code in `src-tauri/src/commands/claude.rs` and `src-tauri/src/commands/agents.rs` that handle this process interaction. + +First, finding the binary and setting up the environment: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::process::Command; +use std::process::Stdio; +use tauri::{AppHandle, Emitter, Manager}; + +// This function tries to locate the 'claude' executable +fn find_claude_binary(app_handle: &AppHandle) -> Result { + // ... logic to check settings, common paths, 'which' command ... + // Returns the found path or an error + Ok("path/to/claude".to_string()) // Simplified +} + +// This function creates a Tokio Command object, setting environment variables +fn create_command_with_env(program: &str) -> Command { + let mut cmd = Command::new(program); + + // Inherit essential environment variables like PATH, HOME, etc. + // This helps the 'claude' binary find Node.js and other dependencies + for (key, value) in std::env::vars() { + // ... filtering for safe/necessary variables ... + cmd.env(&key, &value); + } + + cmd // Return the Command object +} + +// ... rest of the file ... +``` + +`find_claude_binary` is crucial to ensure `claudia` can actually find the executable regardless of how it was installed. `create_command_with_env` is a helper to build the base command object and ensure it inherits essential environment variables, which is often necessary for `claude` to run correctly, especially on macOS GUI launches where the default PATH is minimal. + +Next, the core logic for spawning the process and handling its output streams. This is extracted into a helper function `spawn_claude_process` used by `execute_claude_code`, `continue_claude_code`, and `resume_claude_code`. A similar pattern exists within `execute_agent`. + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + log::info!("Spawning Claude process..."); + + // Configure stdout and stderr to be piped + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + // Spawn the process asynchronously + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + log::info!("Claude process spawned successfully with PID: {:?}", child.id()); + + // Take the piped stdout and stderr handles + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stderr = child.stderr.take().ok_or("Failed to get stderr")?; + + // Create asynchronous buffered readers for the streams + let stdout_reader = BufReader::new(stdout); + let stderr_reader = BufReader::new(stderr); + + // Spawn a separate task to read and process stdout lines + let app_handle_stdout = app.clone(); + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::debug!("Claude stdout: {}", line); + // Emit the line as an event to the frontend + // Frontend listens for "claude-output" + let _ = app_handle_stdout.emit("claude-output", &line); + } + log::info!("Finished reading Claude stdout."); + }); + + // Spawn a separate task to read and process stderr lines + let app_handle_stderr = app.clone(); + tokio::spawn(async move { + let mut lines = stderr_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::error!("Claude stderr: {}", line); + // Emit error lines as an event to the frontend + // Frontend listens for "claude-error" + let _ = app_handle_stderr.emit("claude-error", &line); + } + log::info!("Finished reading Claude stderr."); + }); + + // Spawn a task to wait for the process to finish + let app_handle_complete = app.clone(); + tokio::spawn(async move { + match child.wait().await { // Wait for the process to exit + Ok(status) => { + log::info!("Claude process exited with status: {}", status); + // Emit a completion event to the frontend + // Frontend listens for "claude-complete" + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", status.success()); + } + Err(e) => { + log::error!("Failed to wait for Claude process: {}", e); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", false); // Indicate failure + } + } + }); + + Ok(()) +} + +// ... rest of the file with commands like execute_claude_code calling spawn_claude_process ... +``` + +This `spawn_claude_process` function is the heart of the interaction. It sets up the communication channels (`stdout`, `stderr` pipes), starts the `claude` process, and then uses `tokio::spawn` to run multiple things concurrently: reading output, reading errors, and waiting for the process to finish. Each piece of output or status change triggers an `app.emit` call, sending the information via Tauri's event system back to the frontend. + +Finally, handling cancellation for Agent runs involves finding the process ID (PID) and sending a signal. + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +use rusqlite::{params, Connection, Result as SqliteResult}; // For database access + +/// Kill a running agent session +#[tauri::command] +pub async fn kill_agent_session( + db: State<'_, AgentDb>, // Access to the database state + run_id: i64, +) -> Result { + log::info!("Attempting to kill agent session run: {}", run_id); + + let pid_result = { + let conn = db.0.lock().map_err(|e| e.to_string())?; + // Retrieve the PID from the database for the specific run + conn.query_row( + "SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'", + params![run_id], + |row| row.get::<_, Option>(0) + ) + .map_err(|e| e.to_string())? + }; + + if let Some(pid) = pid_result { + log::info!("Found PID {} for run {}", pid, run_id); + // Use the standard library to send a kill signal + // Behavior differs slightly on Windows vs Unix-like systems + let kill_result = if cfg!(target_os = "windows") { + std::process::Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) // Force kill by PID + .output() + } else { + std::process::Command::new("kill") + .args(["-TERM", &pid.to_string()]) // Send termination signal + .output() + }; + + match kill_result { + Ok(output) if output.status.success() => { + log::info!("Successfully sent kill signal to process {}", pid); + } + Ok(_) => { + log::warn!("Kill command failed for PID {}", pid); + } + Err(e) => { + log::warn!("Failed to execute kill command for PID {}: {}", pid, e); + } + } + } else { + log::warn!("No running PID found for run {}", run_id); + } + + // Update the database to mark the run as cancelled, regardless of kill success + let conn = db.0.lock().map_err(|e| e.to_string())?; + let updated = conn.execute( + "UPDATE agent_runs SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP WHERE id = ?1 AND status = 'running'", + params![run_id], + ).map_err(|e| e.to_string())?; + + Ok(updated > 0) // Return true if a record was updated +} + +// ... rest of the file ... +``` + +This `kill_agent_session` command looks up the process ID associated with the agent run in the database, then attempts to terminate that process using system commands (`kill` or `taskkill`). Finally, it updates the database record for the run to mark it as "cancelled". + +## Conclusion + +In this chapter, we explored the **Claude CLI Interaction** layer, which is fundamental to how `claudia` functions. We learned that this part of the backend is responsible for finding the `claude` binary, preparing the command with all necessary arguments, spawning the `claude` process, setting its environment (including sandboxing), capturing its output and errors in real-time, and managing its lifecycle until completion or cancellation. + +We saw how frontend calls to Tauri Commands trigger this process, how the backend uses Rust's `Command` features and `tokio` for asynchronous stream handling, and how output and status updates are sent back to the frontend via Tauri events, enabling the real-time display of results. This interaction layer effectively turns the `claude` CLI into a powerful engine driven by the user-friendly `claudia` graphical interface. + +Next, we'll take a closer look at a critical aspect touched upon in this chapter: **Sandboxing**. We'll see how `claudia` uses operating system features to limit the permissions of the `claude` process, enhancing security when running code or interacting with your file system. + +[Next Chapter: Sandboxing](06_sandboxing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-6.Sandboxing.md b/Claudia-docs/V_1/claudia-6.Sandboxing.md new file mode 100644 index 00000000..cb21212a --- /dev/null +++ b/Claudia-docs/V_1/claudia-6.Sandboxing.md @@ -0,0 +1,396 @@ +# Chapter 6: Sandboxing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, and how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md). + +Now, let's talk about a crucial aspect of security: **Sandboxing**. + +## The Problem: Running Untrusted Code + +When you use `claudia` to run an Agent or a direct Claude Code session, you are essentially asking the application to launch the separate `claude` binary on your computer. This `claude` binary can then execute code or perform actions based on the instructions it receives from Claude (and indirectly, from you). + +Imagine you ask Claude to "write a script to delete all files in `/tmp`". While this is a harmless directory, what if you accidentally asked it to delete files in your `/Users/yourname/Documents` folder, or worse, system files? Or what if a malicious instruction somehow slipped into the context? + +Running external processes, especially ones that might execute code or interact with your file system and network, introduces a security risk. By default, any program you run has the same permissions as you do. It could potentially read your sensitive files, delete important data, or connect to unwanted places on the internet. + +This is where **Sandboxing** comes in. + +## What is Sandboxing? + +Sandboxing is like putting a protective barrier around the process that `claudia` launches (the `claude` binary). It creates a restricted environment that limits what that process can see and do on your computer, based on a predefined set of rules. + +Think of it like giving the AI a restricted workspace. You give it access only to the specific tools and areas it needs to do its job for this particular task, and nothing more. + +In `claudia`, sandboxing is primarily used to control the `claude` process's access to: + +1. **File System:** Prevent reading or writing files outside of specific allowed directories (like your project folder). +2. **Network:** Prevent making unwanted connections to the internet or local network. +3. **System Information:** Limit access to potentially sensitive system details. + +By default, `claudia` aims to run Agents and sessions within a sandbox, giving you control over their permissions. + +## Sandboxing with Agents + +The primary way you interact with sandboxing settings in `claudia` is through the **Agent configuration**. As you saw in [Chapter 2: Agents](02_agents_.md), each Agent has specific permission toggles. + +Let's revisit the simplified `AgentSandboxSettings.tsx` component from Chapter 2: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +import { Switch } from "@/components/ui/switch"; +// ... other components ... + +export const AgentSandboxSettings: React.FC = ({ + agent, + onUpdate, + className +}) => { + // ... handleToggle function ... + + return ( + // ... Card and layout ... + {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} + // ... end Card ... + ); +}; +``` + +These switches directly control whether the `claude` process launched *by this specific Agent* will be sandboxed and what high-level permissions it will have: + +* **Enable Sandbox:** The main switch. If off, sandboxing is disabled for this Agent, and the process runs with full permissions (like running `claude` directly in your terminal). This should be used with caution. +* **File Read Access:** If enabled, the sandboxed process can read files. Without this, it might not even be able to read the source files in your project directory. +* **File Write Access:** If enabled, the sandboxed process can create or modify files. +* **Network Access:** If enabled, the sandboxed process can make outbound network connections (e.g., accessing APIs, cloning repositories). + +These Agent-specific toggles allow you to quickly define a security posture tailored to the Agent's purpose. A "Code Reader" Agent might only need File Read. A "Code Fixer" might need File Read and Write. A "Web API Helper" might need Network Access. + +## How it Works: Under the Hood + +When you click "Execute" for an Agent or start a session, `claudia`'s backend takes the Agent's sandbox settings (or default settings for direct sessions) and translates them into concrete rules that the operating system can enforce. + +`claudia` uses system-level sandboxing mechanisms through a library called `gaol`. `gaol` provides a way for the parent process (`claudia`'s backend) to define restrictions for a child process (`claude`). + +Here's a simplified look at the steps when `claudia` launches a sandboxed `claude` process: + +1. **Get Agent Permissions:** The backend fetches the selected Agent's configuration from the database, including the `sandbox_enabled`, `enable_file_read`, `enable_file_write`, and `enable_network` fields. +2. **Load Sandbox Profile & Rules:** `claudia` stores more detailed, reusable sandbox configurations called "Profiles" and "Rules" in its database ([Chapter 2: Agents](02_agents_.md)). The Agent might be linked to a specific Profile, or a default Profile is used. The backend loads the rules associated with this Profile. +3. **Combine Agent Permissions and Rules:** The backend logic combines the high-level Agent toggles with the detailed Profile rules. For example, if the Agent has `enable_file_read: false`, any "file read" rules from the loaded Profile are ignored for this run. If `enable_file_read: true`, the specific paths defined in the Profile rules (like "allow reading subpaths of the project directory") are used. The project path itself (from [Chapter 1: Session/Project Management](01_session_project_management_.md)) is crucial here, as file access is often restricted to this directory. +4. **Build `gaol` Profile:** The combined set of effective rules is used to build a `gaol::profile::Profile` object in memory. This object contains the precise operations the child process will be allowed or denied. +5. **Prepare & Spawn Command:** The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It configures the command to run within the sandbox environment defined by the `gaol` Profile. This might involve setting special environment variables or using `gaol`'s API to spawn the child process with the restrictions already applied by the parent. +6. **OS Enforces Sandbox:** When the `claude` process starts, the operating system, guided by the `gaol` library and the configured profile, actively monitors the process. If the `claude` process attempts an action that is *not* allowed by the sandbox rules (like trying to read a file outside the permitted paths when file read is enabled, or any file if file read is disabled), the operating system blocks the action immediately. +7. **Violation Logging:** If a sandboxed process attempts a forbidden action, `claudia` can detect this violation and log it to its database. This helps you understand if an Agent is trying to do something unexpected. + +Here's a simplified sequence diagram illustrating the sandboxing flow during execution: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Database as agents.db + participant SandboxLogic as Sandbox Module (Rust) + participant OS as Operating System + participant ClaudeCLI as claude binary + + Frontend->>Backend: Call execute_agent(...) + Backend->>Database: Get Agent Config (incl. permissions) + Database-->>Backend: Agent Config + Backend->>Database: Get Sandbox Profile & Rules + Database-->>Backend: Profile & Rules + Backend->>SandboxLogic: Combine Agent Permissions & Rules + SandboxLogic->>SandboxLogic: Build gaol::Profile + SandboxLogic-->>Backend: gaol::Profile ready + Backend->>OS: Spawn claude process (with gaol::Profile / env) + OS-->>Backend: Process Handle, PID + Note over OS,ClaudeCLI: OS enforces sandbox rules + ClaudeCLI->>OS: Attempt operation (e.g., read file) + alt Operation Allowed + OS-->>ClaudeCLI: Operation succeeds + else Operation Denied (Violation) + OS-->>ClaudeCLI: Operation fails (Permission denied) + Note over OS: Violation detected + OS->>SandboxLogic: Notify of violation (if configured) + SandboxLogic->>Database: Log Violation + end + ClaudeCLI-->>OS: Process exits + OS-->>Backend: Process status + Backend->>Frontend: Notify completion/output +``` + +This diagram shows how the Agent's settings propagate through the backend to influence the creation of the sandbox profile, which is then enforced by the operating system when the `claude` process is launched. + +## Diving into the Backend Code + +Let's look at snippets from the Rust code related to sandboxing, found primarily in the `src-tauri/src/sandbox/` module and `src-tauri/src/commands/sandbox.rs`. + +The `Agent` struct (from `src-tauri/src/commands/agents.rs`) holds the basic toggles: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + // ... other fields ... + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, // Note: This permission is often difficult to enforce precisely via sandboxing alone and might require manual user confirmation or is inherently less secure. + pub enable_network: bool, + // ... other fields ... +} +``` + +The `src-tauri/src/commands/sandbox.rs` file contains Tauri commands for managing sandbox profiles and rules stored in the database, and for viewing violations: + +```rust +// src-tauri/src/commands/sandbox.rs (Simplified) +// ... imports ... + +// Represents a detailed rule in a sandbox profile +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxRule { + pub id: Option, + pub profile_id: i64, // Links to a profile + pub operation_type: String, // e.g., "file_read_all", "network_outbound" + pub pattern_type: String, // e.g., "subpath", "literal" + pub pattern_value: String, // e.g., "{{PROJECT_PATH}}", "/home/user/.config" + pub enabled: bool, + pub platform_support: Option, // e.g., "[\"macos\", \"linux\"]" + pub created_at: String, +} + +// Represents a log entry for a denied operation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxViolation { + pub id: Option, + pub profile_id: Option, // What profile was active? + pub agent_id: Option, // What agent was running? + pub agent_run_id: Option, // What specific run? + pub operation_type: String, // What was attempted? + pub pattern_value: Option, // What path/address was involved? + pub process_name: Option, // Which binary? + pub pid: Option, // Which process ID? + pub denied_at: String, // When did it happen? +} + +// Tauri command to list sandbox profiles +#[tauri::command] +pub async fn list_sandbox_profiles(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to list rules for a profile +#[tauri::command] +pub async fn list_sandbox_rules(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to view recorded violations +#[tauri::command] +pub async fn list_sandbox_violations(/* ... */) -> Result, String> { /* ... */ } + +// ... other commands for creating/updating/deleting profiles and rules ... +``` + +These commands allow the frontend to manage the detailed sandbox configurations that underpin the Agent's simpler toggles. For example, when you enable "File Read Access" on an Agent, the backend loads rules of `operation_type: "file_read_all"` from the selected profile. + +The logic to combine Agent permissions, Profile rules, and build the `gaol::profile::Profile` happens in the `src-tauri/src/sandbox/profile.rs` and `src-tauri/src/sandbox/executor.rs` modules. + +The `ProfileBuilder` is used to translate `SandboxRule` database entries into `gaol::profile::Operation` objects: + +```rust +// src-tauri/src/sandbox/profile.rs (Simplified) +// ... imports ... +use gaol::profile::{Operation, PathPattern, AddressPattern, Profile}; +// ... SandboxRule struct ... + +pub struct ProfileBuilder { + project_path: PathBuf, // The current project directory + home_dir: PathBuf, // The user's home directory +} + +impl ProfileBuilder { + // ... constructor ... + + /// Build a gaol Profile from database rules, filtered by agent permissions + pub fn build_agent_profile(&self, rules: Vec, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result { + // If sandbox is disabled, return empty profile (no restrictions) + if !sandbox_enabled { + // ... create and return empty profile ... + } + + let mut effective_rules = Vec::new(); + + for rule in rules { + if !rule.enabled { continue; } + + // Filter rules based on Agent permissions: + let include_rule = match rule.operation_type.as_str() { + "file_read_all" | "file_read_metadata" => enable_file_read, + "network_outbound" => enable_network, + "system_info_read" => true, // System info often needed, allow if sandbox is ON + _ => true // Default to include if unknown + }; + + if include_rule { + effective_rules.push(rule); + } + } + + // Always ensure project path access is included if file read is ON + if enable_file_read { + // ... add rule for project path if not already present ... + } + + // Now build the actual gaol Profile from the effective rules + self.build_profile_with_serialization(effective_rules) // This translates rules into gaol::Operation + } + + /// Translates SandboxRules into gaol::Operation and serialized form + fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result> { + match rule.operation_type.as_str() { + "file_read_all" => { + let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::FileReadAll(pattern), SerializedOperation::FileReadAll { path, is_subpath }))) + }, + "network_outbound" => { + let (pattern, serialized) = self.build_address_pattern_with_serialization(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::NetworkOutbound(pattern), serialized))) + }, + // ... handle other operation types ... + _ => Ok(None) + } + } + + // ... helper functions to build path/address patterns ... +} +``` + +The `build_agent_profile` function is key. It takes the raw rules from the database and the Agent's simple boolean toggles, then filters the rules. It also ensures essential access (like reading the project directory) is granted if file read is enabled. Finally, it calls `build_profile_with_serialization` to create the actual `gaol::Profile` object and a simplified, serializable representation of the rules (`SerializedProfile`). + +This `SerializedProfile` is then passed to the `SandboxExecutor`: + +```rust +// src-tauri/src/sandbox/executor.rs (Simplified) +// ... imports ... +use gaol::sandbox::Sandbox; +use tokio::process::Command; +use std::path::Path; + +pub struct SandboxExecutor { + profile: gaol::profile::Profile, // The gaol profile object + project_path: PathBuf, + serialized_profile: Option, // Serialized rules for child process +} + +impl SandboxExecutor { + // ... constructor ... + + /// Prepare a tokio Command for sandboxed execution + /// The sandbox will be activated in the child process by reading environment variables + pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command { + let mut cmd = Command::new(command); + cmd.args(args).current_dir(cwd); + + // ... inherit environment variables like PATH, HOME ... + + // Serialize the sandbox rules and set environment variables + if let Some(ref serialized) = self.serialized_profile { + let rules_json = serde_json::to_string(serialized).expect("Failed to serialize rules"); + // NOTE: These environment variables are currently commented out in the actual code + // for debugging and compatibility reasons. + // In a fully enabled child-side sandboxing model, these would be set: + // cmd.env("GAOL_SANDBOX_ACTIVE", "1"); + // cmd.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref()); + // cmd.env("GAOL_SANDBOX_RULES", &rules_json); + log::warn!("🚨 Sandboxing environment variables for child process are currently disabled!"); + } else { + log::warn!("🚨 No serialized profile - running without sandbox environment!"); + } + + cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()) + } + + // ... Other execution methods ... +} +``` + +The `prepare_sandboxed_command` function takes the `gaol::profile::Profile` and the `SerializedProfile`. Although the environment variable mechanism shown above is temporarily disabled in the provided code snippets, the *intention* is for the parent process (`claudia`'s backend) to set up the environment for the child process (`claude`). The child process, if it supports this model (like `gaol`'s `ChildSandbox::activate()`), would read these environment variables upon startup and activate the sandbox *within itself* before executing the main task. + +Alternatively, `gaol` also supports launching the child process directly from the sandboxed parent using `Sandbox::start()`. The provided code attempts this first but falls back due to current `gaol` library limitations regarding getting the child process handle back. + +The `src-tauri/src/sandbox/platform.rs` file defines what kind of sandboxing capabilities are available and supported on the current operating system (Linux, macOS, FreeBSD have some support). + +```rust +// src-tauri/src/sandbox/platform.rs (Simplified) +// ... imports ... + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlatformCapabilities { + pub os: String, + pub sandboxing_supported: bool, // Is sandboxing generally possible? + // ... details about specific operation support ... +} + +pub fn get_platform_capabilities() -> PlatformCapabilities { /* ... detects OS and returns capabilities ... */ } +pub fn is_sandboxing_available() -> bool { /* ... checks if OS is supported ... */ } +``` + +This is used by the UI (via the `get_platform_capabilities` command) to inform the user if sandboxing is fully supported or if there are limitations on their platform. + +In summary, sandboxing in `claudia` works by: +1. Allowing users to set high-level permissions (read/write/network) on Agents via the UI. +2. Storing detailed, reusable sandbox Profiles and Rules in the backend database. +3. Combining Agent permissions with Profile rules in the backend to create a specific set of restrictions for a given process run. +4. Using system-level sandboxing features (via the `gaol` library and potentially environment variables) to apply these restrictions when launching the `claude` process. +5. Logging any attempts by the sandboxed process to violate these rules. + +This multi-layered approach provides both ease of use (Agent toggles) and flexibility (detailed rules in Profiles), significantly improving security when running AI-generated instructions or code. + +## Conclusion + +In this chapter, we explored **Sandboxing**, `claudia`'s security system. We learned why running external processes requires security measures and how sandboxing provides a protective barrier to limit what the `claude` process can access or do. + +We saw how you control sandboxing primarily through Agent permissions in the UI, enabling or disabling file read, file write, and network access. We then dived into the backend to understand how these simple toggles are combined with detailed Sandbox Profile rules to build a concrete `gaol::profile::Profile`. This profile is then used to launch the `claude` binary within a restricted environment enforced by the operating system, with potential violations being logged. + +Understanding sandboxing is key to securely leveraging the power of Claude Code, especially when it interacts with your local file system. + +In the next chapter, we'll learn how `claudia` handles the continuous stream of output from the `claude` binary to update the UI in real-time: [Streamed Output Processing](07_streamed_output_processing_.md). + +[Next Chapter: Streamed Output Processing](07_streamed_output_processing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/sandbox.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/mod.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/platform.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/profile.rs), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-7.Streamed Output Processing.md b/Claudia-docs/V_1/claudia-7.Streamed Output Processing.md new file mode 100644 index 00000000..dd0bfc9a --- /dev/null +++ b/Claudia-docs/V_1/claudia-7.Streamed Output Processing.md @@ -0,0 +1,295 @@ +# Chapter 7: Streamed Output Processing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md), and how [Sandboxing](06_sandboxing_.md) keeps things secure. + +Now, let's look at how `claudia` handles the constant flow of information coming *from* the `claude` binary while it's running. This is the concept of **Streamed Output Processing**. + +## The Problem: Real-time Updates + +Imagine you ask Claude Code to perform a complex task, like analyzing your codebase or generating a long piece of documentation. This process can take time. The `claude` command-line tool doesn't just wait until it's completely finished and then dump all the results at once. Instead, it often sends its output piece by piece: a thought process here, a tool call there, a chunk of generated text, and finally, a result message. + +As a user of `claudia`'s graphical interface, you don't want to stare at a frozen screen waiting for everything to finish. You want to see what Claude is doing *right now*, as it's happening. You want a live view of its progress. + +This is the problem that Streamed Output Processing solves. `claudia` needs to capture this real-time, piece-by-piece output from the `claude` process and display it to you instantly. + +Think of it like watching a live news feed or a chat application. Messages appear as they are sent, not all bundled up and delivered at the very end. + +## What is Streamed Output Processing? + +Streamed Output Processing in `claudia` refers to the entire system that: + +1. **Captures** the output from the running `claude` process *as it is generated*. +2. **Receives** this output in the backend, often as a stream of data. +3. **Parses** this data (which is typically in a specific format called JSONL) line by line. +4. **Transforms** each parsed piece into a structured message that the frontend understands. +5. **Sends** these structured messages from the backend to the frontend immediately. +6. **Displays** these messages in the user interface as they arrive, providing a live, dynamic view. + +The core idea is that the output is treated as a *stream* – a continuous flow of data arriving over time – rather than a single large block of data at the end. + +## How it Looks in the UI + +When you execute an Agent or run an interactive session in `claudia`, the main part of the screen fills up with messages as they come in. + +You'll see different types of messages appear: + +* Initial system messages (showing session info, tools available). +* Assistant messages (Claude's thoughts, text, tool calls). +* User messages (your prompts, tool results sent back to Claude). +* Result messages (indicating the overall success or failure of a step). + +Each of these appears in the UI as soon as `claudia` receives the corresponding piece of output from the `claude` process. + +In the frontend code (like `src/components/AgentExecution.tsx` or `src/components/ClaudeCodeSession.tsx`), there's a state variable, typically an array, that holds all the messages displayed. When a new piece of output arrives, this array is updated, and React automatically re-renders the list to include the new message. + +For example, in `AgentExecution.tsx`, you'll find code like this managing the displayed messages: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... + +interface AgentExecutionProps { + // ... props ... +} + +export interface ClaudeStreamMessage { + type: "system" | "assistant" | "user" | "result"; + // ... other fields based on the JSONL structure ... +} + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + // State to hold the list of messages displayed in the UI + const [messages, setMessages] = useState([]); + // ... other state variables ... + + // ... handleExecute function ... + + // When a new message arrives (handled by an event listener, shown below): + const handleNewMessage = (newMessage: ClaudeStreamMessage) => { + setMessages(prev => [...prev, newMessage]); // Add the new message to the array + }; + + // ... render function ... + // The rendering logic maps over the `messages` array to display each one + // using the StreamMessage component + /* + return ( + // ... layout ... +
+ {messages.map((message, index) => ( + // Render each message + ))} +
+ // ... rest of component ... + ); + */ +}; +// ... rest of file ... +``` + +This state update (`setMessages`) is the frontend's way of saying, "Hey React, something new arrived, please update the list!" + +## How it Works: The Data Flow + +The communication happens in several steps, involving the `claude` binary, the operating system's pipes, the `claudia` backend (Rust), the Tauri framework, and the `claudia` frontend (TypeScript/React). + +1. **`claude` writes output:** The `claude` process executes your request. When it has a piece of output to share (like a tool call or a chunk of text), it writes it to its standard output (stdout). +2. **OS captures output:** Because `claudia`'s backend spawned `claude` with piped stdout ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), the operating system redirects `claude`'s stdout into a temporary buffer or pipe that the `claudia` backend can read from. +3. **Backend reads line by line:** The `claudia` backend continuously reads from this pipe. It's specifically looking for newline characters to know when a complete line (a complete JSONL entry) has arrived. +4. **Backend emits event:** As soon as the backend reads a complete line, it takes the raw string data and emits it as a Tauri event. These events have a specific name (like `"agent-output"` or `"claude-output"`) that the frontend is listening for. +5. **Tauri delivers event:** The Tauri framework acts as the messenger, efficiently delivering the event and its data payload from the backend Rust process to the frontend JavaScript process. +6. **Frontend receives event:** The frontend has registered event listeners using Tauri's event API. When an event with the matching name arrives, the registered callback function is executed. +7. **Frontend processes and updates:** The callback function receives the raw output line. It parses the JSONL string into a JavaScript object and updates the component's state (`messages` array). +8. **UI re-renders:** React detects the state change and updates only the necessary parts of the UI to display the new message. + +Here is a simplified sequence diagram for this process: + +```mermaid +sequenceDiagram + participant ClaudeCLI as claude binary + participant OS as OS Pipe + participant Backend as Backend Commands (Rust) + participant Tauri as Tauri Core + participant Frontend as Frontend UI (TS/React) + + ClaudeCLI->>OS: Write line (JSONL) to stdout + OS-->>Backend: Data available in pipe + Backend->>Backend: Read line from pipe + Backend->>Tauri: Emit event "claude-output" with line data + Tauri->>Frontend: Deliver event + Frontend->>Frontend: Receive event in listener + Frontend->>Frontend: Parse JSONL line to message object + Frontend->>Frontend: Update state (add message to list) + Frontend->>Frontend: UI re-renders + Frontend->>User: Display new message in UI +``` + +This flow repeats every time `claude` outputs a new line, providing the smooth, real-time updates you see in the `claudia` interface. + +## Diving into the Code + +Let's look at the relevant code snippets from both the backend (Rust) and the frontend (TypeScript). + +### Backend: Reading and Emitting + +As seen in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md), the backend uses `tokio` to handle the asynchronous reading of the process's standard output. It spawns a task that reads line by line and emits events. + +Here's a simplified look at the part of `src-tauri/src/commands/claude.rs` (or similar module) that does this: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; +use tauri::{AppHandle, Manager}; +use tokio::process::Command; // Assuming command is already built + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + // ... Configure stdout/stderr pipes ... + cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stdout_reader = BufReader::new(stdout); + + // Spawn a task to read stdout line by line and emit events + let app_handle_stdout = app.clone(); // Clone handle for the async task + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + // Log or process the raw line + log::debug!("Claude stdout line: {}", line); + // Emit the line as an event to the frontend + let _ = app_handle_stdout.emit("claude-output", &line); // <-- Emitting the event! + } + log::info!("Finished reading Claude stdout."); + }); + + // ... Similar task for stderr ... + // ... Task to wait for process exit and emit completion event ... + + Ok(()) +} + +// Example Tauri command calling the helper +/* +#[tauri::command] +pub async fn execute_claude_code(app: AppHandle, project_path: String, prompt: String, model: String) -> Result<(), String> { + // ... build the Command object 'cmd' ... + spawn_claude_process(app, cmd).await // Calls the streaming helper +} +*/ +``` + +The crucial part here is the `tokio::spawn` block that reads lines (`lines.next_line().await`) and, for each line, calls `app_handle_stdout.emit("claude-output", &line)`. This sends the raw JSONL line string to the frontend via the Tauri event system. The `"claude-output"` string is the event name. + +### Frontend: Listening and Processing + +In the frontend (TypeScript), the component that displays the output (like `AgentExecution.tsx` or `ClaudeCodeSession.tsx`) needs to set up listeners for these events when it loads and clean them up when it unmounts. + +Here's a simplified look at the event listener setup in `AgentExecution.tsx`: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +// ... ClaudeStreamMessage type ... + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + const [messages, setMessages] = useState([]); + const [rawJsonlOutput, setRawJsonlOutput] = useState([]); // Keep raw output too + // ... other state variables ... + + const unlistenRefs = useRef([]); // Ref to store unlisten functions + + useEffect(() => { + // Set up event listeners when the component mounts or execution starts + let outputUnlisten: UnlistenFn | undefined; + let errorUnlisten: UnlistenFn | undefined; + let completeUnlisten: UnlistenFn | undefined; + + const setupListeners = async () => { + try { + // Listen for lines from stdout + outputUnlisten = await listen("agent-output", (event) => { // <-- Listening for the event! + try { + // The event payload is the raw JSONL line string + const rawLine = event.payload; + setRawJsonlOutput(prev => [...prev, rawLine]); // Store raw line + + // Parse the JSONL string into a JavaScript object + const message = JSON.parse(rawLine) as ClaudeStreamMessage; + + // Update the messages state, triggering a UI re-render + setMessages(prev => [...prev, message]); // <-- Updating state! + + } catch (err) { + console.error("Failed to process Claude output line:", err, event.payload); + // Handle parsing errors if necessary + } + }); + + // Listen for stderr lines (errors) + errorUnlisten = await listen("agent-error", (event) => { + console.error("Claude stderr:", event.payload); + // You might want to display these errors in the UI too + }); + + // Listen for the process completion event + completeUnlisten = await listen("agent-complete", (event) => { + console.log("Claude process complete:", event.payload); + // Update UI state (e.g., hide loading indicator) + // ... update isRunning state ... + }); + + // Store unlisten functions so we can clean them up later + unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten]; + + } catch (err) { + console.error("Failed to set up event listeners:", err); + // Handle listener setup errors + } + }; + + setupListeners(); + + // Clean up listeners when the component unmounts + return () => { + unlistenRefs.current.forEach(unlisten => unlisten()); + }; + }, []); // Empty dependency array means setup runs once on mount + + // ... render function ... +}; +// ... rest of file ... +``` + +This `useEffect` hook sets up the listener using `listen("agent-output", ...)`. The callback function receives the event, accesses the raw JSONL string via `event.payload`, parses it with `JSON.parse`, and then updates the `messages` state using `setMessages`. This sequence is the core of the streamed output processing on the frontend. The `useRef` and the cleanup function in the `useEffect` are standard React patterns for managing resources (like event listeners) that persist across renders but need to be cleaned up when the component is no longer needed. + +The parsed `message` object is then passed down to the `StreamMessage` component (referenced in the provided code snippet for `src/components/StreamMessage.tsx`) which knows how to interpret the different `type` and `subtype` fields (like "assistant", "tool_use", "tool_result", "result") and render them with appropriate icons, formatting, and potentially syntax highlighting (using libraries like `react-markdown` and `react-syntax-highlighter`) or custom widgets ([ToolWidgets.tsx]). + +## Conclusion + +In this chapter, we explored **Streamed Output Processing**, understanding how `claudia` handles the real-time flow of information from the running `claude` command-line tool. We learned that `claude` sends output piece by piece in JSONL format, and that `claudia`'s backend captures this stream, reads it line by line, and immediately emits each line as a Tauri event to the frontend. + +On the frontend, we saw how components use `listen` to subscribe to these events, parse the JSONL payload into structured message objects, and update their state to display the new information dynamically. This entire process ensures that the `claudia` UI provides a responsive, live view of the AI's progress and actions during interactive sessions and Agent runs. + +Understanding streamed output is key to seeing how `claudia` provides its core real-time chat and execution experience on top of a command-line binary. + +In the next chapter, we'll look at how `claudia` keeps track of multiple potentially running processes, like Agent runs or direct sessions: [Process Registry](08_process_registry_.md). + +[Next Chapter: Process Registry](08_process_registry_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/StreamMessage.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ToolWidgets.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/types/enhanced-messages.ts) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-8.Process Registry.md b/Claudia-docs/V_1/claudia-8.Process Registry.md new file mode 100644 index 00000000..592990ce --- /dev/null +++ b/Claudia-docs/V_1/claudia-8.Process Registry.md @@ -0,0 +1,371 @@ +# Chapter 8: Process Registry + +Welcome back to the `claudia` tutorial! In our last chapter, [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md), we learned how `claudia` captures and displays the output from the `claude` command-line tool in real-time as it's running. + +Now, let's talk about something that happens just *before* that output starts streaming: launching the `claude` tool itself. When you click "Execute" for an Agent or start a new session, `claudia` doesn't just run the command and wait; it starts the `claude` binary as a separate **process** that runs in the background. + +What if you run multiple agents? What if you start a session and then switch to look at something else while it's running? How does `claudia` keep track of all these separate `claude` processes? How does it know which process is which? And how can it show you their status or let you stop them if needed? + +This is where the **Process Registry** comes in. + +## What is the Process Registry? + +Think of the Process Registry as `claudia`'s internal "Task Manager" specifically for the `claude` processes it starts. It's a system within the `claudia` backend (the Rust code) that keeps a list of all the `claude` processes that are currently running. + +For each running process, the registry stores important information, such as: + +* A unique identifier for this specific "run" (like the `run_id` we saw for Agent Runs in [Chapter 2: Agents](02_agents_.md)). +* The **Process ID (PID)** assigned by the operating system. This is like the process's unique phone number that the operating system uses to identify it. +* The current **status** (like "running", "completed", "failed", "cancelled"). +* Information about *what* is being run (like which Agent, the task description, the project path). +* A reference to the process itself, allowing `claudia` to interact with it (like sending a signal to stop it). +* A temporary buffer to hold the most recent output, allowing quick access to live status without reading the entire JSONL file every time. + +The Process Registry allows `claudia` to monitor these background processes, provide access to their live output streams (as discussed in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)), and offer controls like stopping a running task. + +## The Use Case: Managing Running Sessions + +The most visible use case for the Process Registry in `claudia` is the "Running Sessions" screen. This screen lists all the Agent runs or interactive sessions that `claudia` has started and are still considered "active" (running or perhaps recently finished but not yet fully cleaned up). + +Here's a simplified look at the frontend component responsible for this, `RunningSessionsView.tsx`: + +```typescript +// src/components/RunningSessionsView.tsx (Simplified) +import { useState, useEffect } from 'react'; +// ... other imports ... +import { api } from '@/lib/api'; // Import API helper +import type { AgentRun } from '@/lib/api'; // Import data type + +export function RunningSessionsView({ /* ... props ... */ }) { + const [runningSessions, setRunningSessions] = useState([]); // State to hold list + const [loading, setLoading] = useState(true); + // ... other state ... + + // Function to fetch the list of running sessions + const loadRunningSessions = async () => { + try { + // Call the backend command to get running sessions + const sessions = await api.listRunningAgentSessions(); + setRunningSessions(sessions); // Update state with the list + } catch (error) { + console.error('Failed to load running sessions:', error); + // ... handle error ... + } finally { + setLoading(false); + } + }; + + // Function to stop a session + const killSession = async (runId: number, agentName: string) => { + try { + // Call the backend command to kill a session + const success = await api.killAgentSession(runId); + if (success) { + console.log(`${agentName} session stopped.`); + // Refresh the list after killing + await loadRunningSessions(); + } else { + console.warn('Session may have already finished'); + } + } catch (error) { + console.error('Failed to kill session:', error); + // ... handle error ... + } + }; + + useEffect(() => { + loadRunningSessions(); // Load sessions when component mounts + + // Set up auto-refresh + const interval = setInterval(() => { + loadRunningSessions(); + }, 5000); // Refresh every 5 seconds + + return () => clearInterval(interval); // Clean up interval + }, []); + + if (loading) { + return

Loading running sessions...

; // Loading indicator + } + + return ( +
+

Running Agent Sessions

+ {runningSessions.length === 0 ? ( +

No agent sessions are currently running

+ ) : ( +
+ {/* Map over the runningSessions list to display each one */} + {runningSessions.map((session) => ( +
{/* Card or similar display */} +

{session.agent_name}

+

Status: {session.status}

+

PID: {session.pid}

+ {/* ... other details like task, project path, duration ... */} + + {/* Buttons to interact with the session */} + {/* Set state to open viewer */} + +
+ ))} +
+ )} + + {/* Session Output Viewer component (shown when selectedSession is not null) */} + {selectedSession && ( + setSelectedSession(null)} + /> + )} +
+ ); +} +``` + +This component demonstrates how the frontend relies on the backend's Process Registry: +1. It calls `api.listRunningAgentSessions()` to get the current list. +2. It displays information for each running process, including the PID and status. +3. It provides "Stop" buttons that call `api.killAgentSession(runId)`, requesting the backend to terminate the corresponding process. +4. It provides a "View Output" button that, when clicked, might fetch the live output buffer from the registry (using a command like `api.getLiveSessionOutput(runId)`) before potentially switching to file-based streaming ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +5. It automatically refreshes this list periodically by calling `loadRunningSessions` again. + +## How it Works: Under the Hood + +The Process Registry is implemented in the Rust backend, primarily in the `src-tauri/src/process/registry.rs` file. + +Here's a simplified look at what happens step-by-step: + +1. **Process Spawned:** When a backend command like `execute_agent` or `execute_claude_code` needs to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), it prepares the command and then calls `child.spawn()`. +2. **Registration:** Immediately after `child.spawn()` successfully starts the process, the backend extracts the **PID** from the returned `Child` object. It then takes the `run_id` (generated when the Agent run record was created in the database), the PID, and other relevant info (Agent name, task, project path) and calls a method on the `ProcessRegistry` instance, typically `registry.register_process(...)`. +3. **Registry Storage:** The `ProcessRegistry` stores this information in an in-memory data structure, like a `HashMap`, where the key is the `run_id` and the value is an object containing the `ProcessInfo` and the actual `Child` handle. It also initializes a buffer for live output for this specific run. +4. **Output Appending:** As the streaming output processing ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)) reads lines from the process's stdout/stderr pipes, it also appends these lines to the live output buffer associated with this run_id in the Process Registry using `registry.append_live_output(run_id, line)`. +5. **Listing Processes:** When the frontend calls `list_running_agent_sessions` (which maps to a backend command like `list_running_sessions`), the backend accesses the `ProcessRegistry` and asks it for the list of currently registered processes (`registry.get_running_processes()`). The registry returns the stored `ProcessInfo` for each active entry in its map. +6. **Viewing Live Output:** When the frontend calls `get_live_session_output(runId)`, the backend asks the registry for the live output buffer associated with that `runId` (`registry.get_live_output(runId)`), and returns it to the frontend. +7. **Killing Process:** When the frontend calls `kill_agent_session(runId)`, the backend first tells the `ProcessRegistry` to attempt to terminate the process (`registry.kill_process(runId)`). The registry uses the stored `Child` handle or PID to send a termination signal to the operating system. After attempting the kill, the backend also updates the database record for that run to mark its status as 'cancelled'. +8. **Cleanup:** Periodically, `claudia` runs a cleanup task (`cleanup_finished_processes`) that checks the status of processes currently in the registry. If a process has exited (e.g., finished naturally or was killed), the registry removes its entry (`registry.unregister_process(runId)`). This also helps keep the database status accurate. + +Here's a simple sequence diagram showing the core interactions: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Backend Commands + participant Registry as Process Registry + participant OS as Operating System + + User->>Frontend: Open Running Sessions View + Frontend->>Backend: Call list_running_sessions() + Backend->>Registry: get_running_processes() + Registry-->>Backend: Return List + Backend-->>Frontend: Return List (mapped from ProcessInfo) + Frontend->>User: Display List + + User->>Frontend: Click Stop Button (for runId) + Frontend->>Backend: Call kill_agent_session(runId) + Backend->>Registry: kill_process(runId) + Registry->>OS: Send terminate signal (using PID/Handle) + OS-->>Registry: Confirmation/Status + Registry-->>Backend: Return success/failure + Backend->>Backend: Update AgentRun status in DB + Backend-->>Frontend: Return confirmation + Frontend->>Frontend: Refresh list / Update UI +``` + +This diagram illustrates how the frontend relies on backend commands to query and manage the processes tracked by the Process Registry. + +## Diving into the Backend Code + +The core implementation of the Process Registry is found in `src-tauri/src/process/registry.rs`. + +First, let's look at the `ProcessInfo` struct, which holds the basic details about a running process: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Information about a running agent process +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessInfo { + pub run_id: i64, // Matches the agent_runs database ID + pub agent_id: i64, // Which agent started this run + pub agent_name: String, // Agent's name + pub pid: u32, // Operating System Process ID + pub started_at: DateTime, // When it started + pub project_path: String, // Where it's running + pub task: String, // The task given + pub model: String, // The model used +} +``` + +The `ProcessRegistry` struct itself is simple; it just holds the map and uses `Arc>` for thread-safe access because multiple parts of the backend might need to interact with it concurrently. + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tokio::process::Child; // Need the process handle itself + +/// Information about a running process with handle +pub struct ProcessHandle { + pub info: ProcessInfo, + pub child: Arc>>, // The handle to the child process + pub live_output: Arc>, // Buffer for live output +} + +/// Registry for tracking active agent processes +pub struct ProcessRegistry { + // Map from run_id to the ProcessHandle + processes: Arc>>, +} + +impl ProcessRegistry { + pub fn new() -> Self { + Self { + processes: Arc::new(Mutex::new(HashMap::new())), + } + } + + // ... methods like register_process, unregister_process, get_running_processes, kill_process, append_live_output, get_live_output ... +} + +// Tauri State wrapper for the registry +pub struct ProcessRegistryState(pub Arc); +// ... Default impl ... +``` + +When a process is spawned, the `execute_agent` command (in `src-tauri/src/commands/agents.rs`) calls `registry.register_process`: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +// Assuming 'registry' is the State +// Assuming 'child' is the tokio::process::Child from cmd.spawn()... +// Assuming 'run_id', 'agent_id', etc., are defined... + +// Register the process in the registry +registry.0.register_process( + run_id, + agent_id, + agent.name.clone(), // Agent name + pid, // Process ID + project_path.clone(), + task.clone(), + execution_model.clone(), + child, // Pass the child handle +).map_err(|e| format!("Failed to register process: {}", e))?; + +info!("📋 Registered process in registry"); + +// ... rest of the async task waiting for process to finish ... +``` + +The `register_process` method in the `ProcessRegistry` then locks the internal map and inserts the new entry: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Register a new running process +pub fn register_process( + &self, + run_id: i64, + agent_id: i64, + agent_name: String, + pid: u32, + project_path: String, + task: String, + model: String, + child: Child, // Receives the child handle +) -> Result<(), String> { + let mut processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + let process_info = ProcessInfo { + run_id, agent_id, agent_name, pid, + started_at: Utc::now(), + project_path, task, model, + }; + + let process_handle = ProcessHandle { + info: process_info, + child: Arc::new(Mutex::new(Some(child))), // Store the handle + live_output: Arc::new(Mutex::new(String::new())), // Init output buffer + }; + + processes.insert(run_id, process_handle); // Insert into the map + Ok(()) +} +``` + +Listing running processes involves locking the map and collecting the `ProcessInfo` from each `ProcessHandle`: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Get all running processes +pub fn get_running_processes(&self) -> Result, String> { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + // Iterate through the map's values (ProcessHandle), clone the info field, collect into a Vec + Ok(processes.values().map(|handle| handle.info.clone()).collect()) +} +``` + +Killing a process involves looking up the `ProcessHandle` by `run_id`, accessing the stored `Child` handle, and calling its `kill` method: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... +use tokio::process::Child; + +/// Kill a running process +pub async fn kill_process(&self, run_id: i64) -> Result { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + if let Some(handle) = processes.get(&run_id) { + let child_arc = handle.child.clone(); + drop(processes); // IMPORTANT: Release the lock before calling async kill() + + let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?; // Lock the child handle + if let Some(ref mut child) = child_guard.as_mut() { + match child.kill().await { // Call the async kill method + Ok(_) => { + *child_guard = None; // Clear the handle after killing + Ok(true) + } + Err(e) => Err(format!("Failed to kill process: {}", e)), + } + } else { + Ok(false) // Process was already killed or completed + } + } else { + Ok(false) // Process not found in registry + } +} +``` + +Note that the `kill_agent_session` Tauri command ([src-tauri/src/commands/agents.rs]) first calls `registry.kill_process` to try terminating the *actual* OS process via the `Child` handle, and *then* updates the database status. This ensures the UI accurately reflects the state even if the process doesn't immediately exit after the signal. + +The `cleanup_finished_processes` command (also in `src-tauri/src/commands/agents.rs`) periodically checks all processes currently in the registry using `registry.is_process_running()` and, if they are no longer running, updates their status in the database and removes them from the registry. + +This Process Registry provides the backend's central point for managing and interacting with all the separate `claude` instances that `claudia` is running, enabling features like the "Running Sessions" view and the ability to stop tasks. + +## Conclusion + +In this chapter, we introduced the **Process Registry**, `claudia`'s internal system for tracking the `claude` command-line tool processes it launches in the background. We learned that it stores essential information like PID, status, and associated run details, allowing `claudia` to monitor and control these separate tasks. + +We saw how the Process Registry is used to power features like the "Running Sessions" view in the UI, enabling users to see what's currently executing, view live output, and stop processes. We also delved into the backend implementation, seeing how processes are registered upon spawning, how the registry stores their handles, and how backend commands interact with the registry to list, kill, and manage these running tasks. + +Understanding the Process Registry is key to seeing how `claudia` manages concurrency and provides visibility and control over the AI tasks running on your system. + +In the next chapter, we'll explore **Checkpointing**, a feature that allows Claude Code to save and restore its state, enabling longer, more complex interactions across multiple runs. + +[Next Chapter: Checkpointing](09_checkpointing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/registry.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/RunningSessionsView.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia-9.Checkpoiting.md b/Claudia-docs/V_1/claudia-9.Checkpoiting.md new file mode 100644 index 00000000..51eb81f1 --- /dev/null +++ b/Claudia-docs/V_1/claudia-9.Checkpoiting.md @@ -0,0 +1,935 @@ +# Chapter 9: Checkpointing + +Welcome back to the `claudia` tutorial! In our previous chapter, [Chapter 8: Process Registry](08_process_registry_.md), we learned how `claudia` keeps track of and manages the individual `claude` processes it launches. This allows the application to handle multiple running tasks simultaneously and provides a view of what's currently active. + +Now, let's talk about preserving the state of those tasks over time, even after they finish or the application closes. This is the powerful concept of **Checkpointing**. + +## The Problem: Sessions Are Temporary + +Imagine you're working with Claude Code on a complex feature development within a project. You have a long conversation, make several changes to files, get some code snippets, debug an issue, and maybe even use tools to run tests. This interaction might span hours or even days across multiple `claude` runs. + +Each run of `claude` is a session ([Chapter 1: Session/Project Management](01_session_project_management_.md)), and the CLI automatically saves the message history for that session. But what about the state of your project files? What if you want to go back to how the files looked *before* Claude made a specific set of changes? What if you want to experiment with a different approach, but keep the option to return to the current state? + +The basic session history saves the *conversation*, but it doesn't version control your *project files*. This is where checkpoints become essential. + +Think of it like writing a book. The message history is like your rough draft – a linear flow of words. But sometimes you want to save a specific version (e.g., "finished Chapter 5"), experiment with rewriting a scene, and maybe decide later to revert to that saved version or start a new version branched from it. Checkpointing provides this capability for your AI-assisted coding sessions. + +## What is Checkpointing? + +Checkpointing in `claudia` is a system for creating save points of your entire working state for a specific Claude Code session. A checkpoint captures two main things at a particular moment: + +1. **The complete message history** up to that point in the session. +2. **Snapshots of your project files** that have changed since the last checkpoint (or are being tracked). + +When you create a checkpoint, `claudia` records the session's conversation history and saves copies of the relevant files in a special location. This lets you revisit that exact moment later. + +**In simpler terms:** + +* A Checkpoint is a snapshot of your conversation *and* your project files at a specific point in time. +* You can create checkpoints manually whenever you want to save a significant state (like "After implementing Login feature"). +* `claudia` can also create checkpoints automatically based on certain events (like after a tool makes changes to files). +* Checkpoints are organized in a **Timeline**, showing the history of your session like a branching tree (similar to how git commits work). +* You can **Restore** a checkpoint to revert your message history and project files to that saved state. +* You can **Fork** from a checkpoint to start a new conversation branch from a previous state. +* You can **Diff** between checkpoints to see exactly which files were changed and what the changes were. + +## Key Concepts in Checkpointing + +Let's break down the core ideas behind `claudia`'s checkpointing system: + +| Concept | Description | Analogy | +| :----------------- | :--------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | +| **Checkpoint** | A specific save point containing session messages and file snapshots. | Saving your game progress. | +| **Timeline** | The chronological history of checkpoints for a session, shown as a tree structure reflecting branching (forks). | A Git history tree or a family tree. | +| **File Snapshot** | A saved copy of a project file's content and metadata at a specific checkpoint. Only saves changes efficiently. | Saving individual changed files in a commit. | +| **Restoring** | Reverting the current session messages and project files to the state captured in a chosen checkpoint. | Loading a previous save game. | +| **Forking** | Creating a new session branch starting from a specific checkpoint. | Branching in Git or creating an alternate story. | +| **Automatic Checkpoints** | Checkpoints created by `claudia` based on predefined rules (e.g., after certain actions). | Auto-save feature in software. | +| **Checkpoint Strategy** | The specific rule defining when automatic checkpoints are created (Per Prompt, Per Tool Use, Smart). | Different auto-save frequencies/triggers. | +| **Diffing** | Comparing two checkpoints to see the differences in file content and token usage. | `git diff` command. | + +## Using Checkpointing in the UI + +You interact with checkpointing primarily within a specific session view (like `ClaudeCodeSession.tsx`), typically via a dedicated section or side panel. + +The `TimelineNavigator.tsx` component is the central piece of the UI for browsing and interacting with checkpoints: + +```typescript +// src/components/TimelineNavigator.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { GitBranch, Save, RotateCcw, GitFork, Diff } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { api, type Checkpoint, type TimelineNode, type SessionTimeline, type CheckpointDiff } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * Visual timeline navigator for checkpoint management + */ +export const TimelineNavigator: React.FC = ({ + sessionId, + projectId, + projectPath, + currentMessageIndex, + onCheckpointSelect, // Callback for selecting a checkpoint (e.g., for Diff) + onFork, // Callback for triggering a fork + refreshVersion = 0, // Prop to force reload + className +}) => { + const [timeline, setTimeline] = useState(null); // State for the timeline data + const [selectedCheckpoint, setSelectedCheckpoint] = useState(null); // State for the currently selected checkpoint (for diffing, etc.) + const [showCreateDialog, setShowCreateDialog] = useState(false); // State for the "Create Checkpoint" dialog + const [checkpointDescription, setCheckpointDescription] = useState(""); // State for the description input + const [isLoading, setIsLoading] = useState(false); + // ... other state for diff dialog, errors, etc. ... + + // Effect to load the timeline when the component mounts or needs refreshing + useEffect(() => { + loadTimeline(); + }, [sessionId, projectId, projectPath, refreshVersion]); // Dependencies + + // Function to load timeline data from backend + const loadTimeline = async () => { + try { + setIsLoading(true); + // Call backend API to get the timeline + const timelineData = await api.getSessionTimeline(sessionId, projectId, projectPath); + setTimeline(timelineData); // Update state + // ... logic to auto-expand current branch ... + } catch (err) { + console.error("Failed to load timeline:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle manual checkpoint creation + const handleCreateCheckpoint = async () => { + try { + setIsLoading(true); + // Call backend API to create a checkpoint + await api.createCheckpoint( + sessionId, + projectId, + projectPath, + currentMessageIndex, // Pass current message count + checkpointDescription || undefined // Pass optional description + ); + setCheckpointDescription(""); // Clear input + setShowCreateDialog(false); // Close dialog + await loadTimeline(); // Reload timeline to show the new checkpoint + } catch (err) { + console.error("Failed to create checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle restoring a checkpoint + const handleRestoreCheckpoint = async (checkpoint: Checkpoint) => { + // ... confirmation logic ... + try { + setIsLoading(true); + // Call backend API to restore the checkpoint + await api.restoreCheckpoint(checkpoint.id, sessionId, projectId, projectPath); + await loadTimeline(); // Reload timeline + // Notify parent component or session view about the restore + // This might trigger reloading the message history from the checkpoint + onCheckpointSelect(checkpoint); + } catch (err) { + console.error("Failed to restore checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle forking (delegates to parent component via callback) + const handleFork = async (checkpoint: Checkpoint) => { + // This component doesn't *create* the new session, it tells the parent + // session view to initiate a fork from this checkpoint ID + onFork(checkpoint.id); + }; + + // Function to handle comparing checkpoints + const handleCompare = async (checkpoint: Checkpoint) => { + if (!selectedCheckpoint) { + // If no checkpoint is selected for comparison, select this one + setSelectedCheckpoint(checkpoint); + // You might update UI to show this checkpoint is selected for compare + return; + } + // If a checkpoint is already selected, perform the comparison + try { + setIsLoading(true); + const diffData = await api.getCheckpointDiff( + selectedCheckpoint.id, // The first selected checkpoint + checkpoint.id, // The checkpoint being compared against + sessionId, projectId // Session/Project context + ); + // ... show diffData in a dialog ... + setDiff(diffData); + // ... open diff dialog ... + } catch (err) { + console.error("Failed to get diff:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + + // Recursive function to render the timeline tree structure + const renderTimelineNode = (node: TimelineNode, depth: number = 0) => { + // ... rendering logic for node, its children, and buttons ... + // Each node displays checkpoint info and buttons for Restore, Fork, Diff + const isCurrent = timeline?.currentCheckpointId === node.checkpoint.id; + const isSelected = selectedCheckpoint?.id === node.checkpoint.id; // For compare selection + + + return ( +
+ {/* UI representation of the checkpoint */} + setSelectedCheckpoint(node.checkpoint)} // Select for compare/info + > + + {/* Display checkpoint ID, timestamp, description, metadata (tokens, files) */} +

{node.checkpoint.id.slice(0, 8)}...

+

{node.checkpoint.timestamp}

+

{node.checkpoint.description}

+ {node.checkpoint.metadata.totalTokens} tokens + {node.checkpoint.metadata.fileChanges} files changed + + {/* Action Buttons */} + + + +
+
+ + {/* Recursively render children */} + {/* ... Conditional rendering based on expanded state ... */} +
+ {node.children.map((child) => renderTimelineNode(child, depth + 1))} +
+
+ ); + }; + + return ( +
+ {/* ... Warning message ... */} + {/* Header with "Checkpoint" button */} +
+
+ +

Timeline

+ {/* Display total checkpoints badge */} +
+ +
+ + {/* Error display */} + {/* ... */} + + {/* Render the timeline tree starting from the root node */} + {timeline?.rootNode ? ( +
+ {renderTimelineNode(timeline.rootNode)} +
+ ) : ( + // ... Loading/empty state ... + )} + + {/* Create checkpoint dialog */} + + + + Create Checkpoint + {/* ... Dialog description and input for description ... */} + +
+
+ + setCheckpointDescription(e.target.value)} /> +
+
+ + {/* ... Cancel and Create buttons calling handleCreateCheckpoint ... */} + +
+
+ + {/* Diff dialog (not shown here, but would display diff state) */} + {/* ... Dialog for showing diff results ... */} +
+ ); +}; +``` + +This component displays the timeline tree structure, fetched from the backend using `api.getSessionTimeline`. Each node in the tree represents a checkpoint (`TimelineNode` contains a `Checkpoint` struct). The component provides buttons to trigger actions like creating a manual checkpoint (`handleCreateCheckpoint`), restoring a checkpoint (`handleRestoreCheckpoint`), forking (`handleFork`), and comparing checkpoints (`handleCompare`). These actions call corresponding backend API functions via `src/lib/api.ts`. + +You can also configure automatic checkpointing and cleanup using the `CheckpointSettings.tsx` component: + +```typescript +// src/components/CheckpointSettings.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Settings, Save, Trash2, HardDrive } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { SelectComponent } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { api, type CheckpointStrategy } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * CheckpointSettings component for managing checkpoint configuration + */ +export const CheckpointSettings: React.FC = ({ + sessionId, + projectId, + projectPath, + onClose, + className, +}) => { + const [autoCheckpointEnabled, setAutoCheckpointEnabled] = useState(true); + const [checkpointStrategy, setCheckpointStrategy] = useState("smart"); + const [totalCheckpoints, setTotalCheckpoints] = useState(0); + const [keepCount, setKeepCount] = useState(10); // State for cleanup setting + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + // ... error/success states ... + + const strategyOptions: SelectOption[] = [ + { value: "manual", label: "Manual Only" }, + { value: "per_prompt", label: "After Each Prompt" }, + { value: "per_tool_use", label: "After Tool Use" }, + { value: "smart", label: "Smart (Recommended)" }, + ]; + + // Load settings when component mounts + useEffect(() => { + loadSettings(); + }, [sessionId, projectId, projectPath]); + + const loadSettings = async () => { + try { + setIsLoading(true); + // Call backend API to get settings + const settings = await api.getCheckpointSettings(sessionId, projectId, projectPath); + setAutoCheckpointEnabled(settings.auto_checkpoint_enabled); + setCheckpointStrategy(settings.checkpoint_strategy); + setTotalCheckpoints(settings.total_checkpoints); // Get total count for cleanup info + } catch (err) { + console.error("Failed to load checkpoint settings:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + const handleSaveSettings = async () => { + try { + setIsSaving(true); + // Call backend API to update settings + await api.updateCheckpointSettings( + sessionId, + projectId, + projectPath, + autoCheckpointEnabled, + checkpointStrategy + ); + // ... show success message ... + } catch (err) { + console.error("Failed to save checkpoint settings:", err); + // ... set error state ... + } finally { + setIsSaving(false); + } + }; + + const handleCleanup = async () => { + // ... confirmation ... + try { + setIsLoading(true); + // Call backend API to cleanup + const removed = await api.cleanupOldCheckpoints( + sessionId, + projectId, + projectPath, + keepCount // Pass how many recent checkpoints to keep + ); + // ... show success message ... + await loadSettings(); // Refresh count + } catch (err) { + console.error("Failed to cleanup checkpoints:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* ... Experimental Warning ... */} + {/* Header */} +
+ {/* ... Title and icon ... */} + {onClose && } +
+ + {/* Error/Success messages */} + {/* ... */} + +
+ {/* Auto-checkpoint toggle */} +
+
+ +

Automatically create checkpoints

+
+ +
+ + {/* Checkpoint strategy select */} +
+ + setCheckpointStrategy(value as CheckpointStrategy)} + options={strategyOptions} + disabled={isLoading || !autoCheckpointEnabled} // Disable if auto-checkpoint is off + /> + {/* ... Strategy description text ... */} +
+ + {/* Save button */} + +
+ + {/* Storage Management Section */} +
+
+ {/* ... "Storage Management" title and icon ... */} +

Total checkpoints: {totalCheckpoints}

{/* Display count */} +
+ {/* Cleanup settings */} +
+ +
+ setKeepCount(parseInt(e.target.value) || 10)} disabled={isLoading} className="flex-1"/> + +
+ {/* ... Cleanup description text ... */} +
+
+ + ); +}; +``` + +This component allows you to toggle automatic checkpoints, select a strategy (Manual, Per Prompt, Per Tool Use, Smart), set how many recent checkpoints to keep, and trigger a cleanup. These actions are handled by backend commands called via `api`. + +## How it Works: Under the Hood (Backend) + +The checkpointing logic resides in the `src-tauri/src/checkpoint/` module. This module contains several key parts: + +1. **`checkpoint::mod.rs`**: Defines the main data structures (`Checkpoint`, `FileSnapshot`, `SessionTimeline`, `TimelineNode`, `CheckpointStrategy`, etc.) and utility structs (`CheckpointPaths`, `CheckpointDiff`). +2. **`checkpoint::storage.rs`**: Handles reading from and writing to disk. It manages saving/loading checkpoint metadata, messages, and file snapshots. It uses content-addressable storage for file contents to save space. +3. **`checkpoint::manager.rs`**: The core logic for managing a *single session*'s checkpoints. It tracks file changes (`FileTracker`), keeps the current message history (`current_messages`), interacts with `CheckpointStorage` for saving/loading, manages the session's `Timeline`, and handles operations like creating, restoring, and forking. +4. **`checkpoint::state.rs`**: A stateful manager (similar to the Process Registry) that holds `CheckpointManager` instances for *all active sessions* in memory. This prevents needing to recreate managers for each command call. + +Checkpoint data is stored within the `~/.claude` directory, specifically within the project's timeline directory: + +`~/.claude/projects//.timelines//` + +Inside this session timeline directory, you'll find: +* `timeline.json`: Stores the `SessionTimeline` structure (the tree metadata). +* `checkpoints/`: A directory containing subdirectories for each checkpoint ID. Each checkpoint directory (`checkpoints//`) holds `metadata.json` and `messages.jsonl` (the compressed messages). +* `files/`: A directory containing file snapshots, organized into a `content_pool/` (actual compressed file contents, stored by hash) and `refs/` (references from each checkpoint back to the content pool, stored as small JSON files). + +### The `CheckpointState` + +Just like the Process Registry manages active processes, the `CheckpointState` manages active `CheckpointManager` instances. When a session starts or is loaded in the UI, the frontend calls a backend command which then uses `CheckpointState::get_or_create_manager` to get the manager for that session. + +```rust +// src-tauri/src/checkpoint/state.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; // For thread-safe async access + +use super::manager::CheckpointManager; + +/// Manages checkpoint managers for active sessions +#[derive(Default, Clone)] +pub struct CheckpointState { + /// Map of session_id to CheckpointManager + managers: Arc>>>, // Use RwLock for concurrent reads/writes + claude_dir: Arc>>, // Claude dir path needed for storage initialization +} + +impl CheckpointState { + // ... new(), set_claude_dir(), remove_manager(), clear_all() methods ... + + /// Gets or creates a CheckpointManager for a session + pub async fn get_or_create_manager( + &self, + session_id: String, + project_id: String, + project_path: PathBuf, + ) -> Result> { + let mut managers = self.managers.write().await; // Lock for writing + + // Check if manager already exists + if let Some(manager) = managers.get(&session_id) { + return Ok(Arc::clone(manager)); // Return existing manager (Arc::clone increases ref count) + } + + // ... get claude_dir ... + + // Create new manager if it doesn't exist + let manager = CheckpointManager::new( + project_id, + session_id.clone(), + project_path, + claude_dir, + ).await?; // CheckpointManager::new handles loading/init storage + + let manager_arc = Arc::new(manager); + managers.insert(session_id, Arc::clone(&manager_arc)); // Store new manager + + Ok(manager_arc) + } + + // ... get_manager(), list_active_sessions() methods ... +} +``` + +This structure ensures that the heavy work of loading the timeline and setting up file tracking only happens once per session when it's first accessed, not for every single checkpoint-related command. + +### Creating a Checkpoint Flow + +When the frontend requests to create a checkpoint (manually or automatically), the backend command retrieves the session's `CheckpointManager` from the `CheckpointState` and calls `manager.create_checkpoint(...)`. + +Here's a simplified look at what happens inside `CheckpointManager::create_checkpoint`: + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... new(), track_message(), track_file_modification(), etc. ... + + /// Create a checkpoint + pub async fn create_checkpoint( + &self, + description: Option, + parent_checkpoint_id: Option, // Optional parent ID for explicit forks + ) -> Result { + let messages = self.current_messages.read().await; // Get current messages + let message_index = messages.len().saturating_sub(1); + + // ... Extract metadata (prompt, tokens, etc.) from messages ... + + // Ensure all files in the project are tracked before snapshotting + // This discovers new files and adds them to the file tracker + let mut all_files = Vec::new(); + let _ = collect_files(&self.project_path, &self.project_path, &mut all_files); + for rel in all_files { + if let Some(p) = rel.to_str() { + let _ = self.track_file_modification(p).await; // Adds/updates tracker state + } + } + + // Generate a unique ID for the new checkpoint + let checkpoint_id = storage::CheckpointStorage::generate_checkpoint_id(); + + // Create file snapshots based on the *current* state of tracked files + // This reads the content of files marked as modified by track_file_modification + let file_snapshots = self.create_file_snapshots(&checkpoint_id).await?; + + // Build the Checkpoint metadata struct + let checkpoint = Checkpoint { + id: checkpoint_id.clone(), + session_id: self.session_id.clone(), + project_id: self.project_id.clone(), + message_index, + timestamp: Utc::now(), + description, + parent_checkpoint_id: parent_checkpoint_id.or_else(|| self.timeline.read().await.current_checkpoint_id.clone()), // Link to current parent or explicit parent + // ... include extracted metadata ... + }; + + // Save the checkpoint using the storage layer + let messages_content = messages.join("\n"); + let result = self.storage.save_checkpoint( + &self.project_id, + &self.session_id, + &checkpoint, + file_snapshots, // Pass the actual snapshots + &messages_content, // Pass the message content + )?; + + // ... Reload timeline from disk to incorporate new node ... + // ... Update current_checkpoint_id in in-memory timeline ... + // ... Reset is_modified flag in the file tracker ... + + Ok(result) + } + + // Helper to create FileSnapshots from the FileTracker state + async fn create_file_snapshots(&self, checkpoint_id: &str) -> Result> { + let tracker = self.file_tracker.read().await; + let mut snapshots = Vec::new(); + + for (rel_path, state) in &tracker.tracked_files { + // Only snapshot files marked as modified or deleted + if !state.is_modified && state.exists { // Only include if modified OR was deleted + continue; // Skip if not modified AND still exists + } + if state.is_modified || !state.exists { // Snapshot if modified or is now deleted + // ... read file content, calculate hash, get metadata ... + let (content, exists, permissions, size, current_hash) = { /* ... */ }; + + snapshots.push(FileSnapshot { + checkpoint_id: checkpoint_id.to_string(), + file_path: rel_path.clone(), + content, // Content will be empty for deleted files + hash: current_hash, // Hash will be empty for deleted files + is_deleted: !exists, + permissions, + size, + }); + } + } + Ok(snapshots) + } + + // ... other methods ... +} +``` + +The `create_checkpoint` function coordinates the process: it reads current messages, identifies changed files using the `FileTracker`, generates file snapshots by reading changed file contents, creates the checkpoint metadata, saves everything to disk via `CheckpointStorage`, and updates the timeline. + +The `FileTracker` keeps a list of files that have been referenced (either by the user or by tool outputs). The `track_file_modification` method is called whenever a file might have changed (e.g., mentioned in an edit tool output). It checks the file's current state (existence, hash, modification time) and marks it as `is_modified` if it differs from the last known state. + +The `CheckpointStorage::save_checkpoint` method handles the actual disk writing, including compressing messages and file contents and managing the content-addressable storage for file snapshots (`save_file_snapshot`). + +```rust +// src-tauri/src/checkpoint/storage.rs (Simplified) +// ... imports ... + +impl CheckpointStorage { + // ... new(), init_storage(), load_checkpoint(), etc. ... + + /// Save a checkpoint to disk + pub fn save_checkpoint(/* ... arguments ... */) -> Result { + // ... create directories ... + // ... save metadata.json ... + // ... save compressed messages.jsonl ... + + // Save file snapshots (calling save_file_snapshot for each) + let mut files_processed = 0; + for snapshot in &file_snapshots { + if self.save_file_snapshot(&paths, snapshot).is_ok() { // Calls helper + files_processed += 1; + } + } + + // Update timeline file on disk + self.update_timeline_with_checkpoint(/* ... */)?; + + // ... return result ... + Ok(CheckpointResult { /* ... */ }) + } + + /// Save a single file snapshot using content-addressable storage + fn save_file_snapshot(&self, paths: &CheckpointPaths, snapshot: &FileSnapshot) -> Result<()> { + // Directory where actual file content is stored by hash + let content_pool_dir = paths.files_dir.join("content_pool"); + fs::create_dir_all(&content_pool_dir)?; + + // Path to the content file based on its hash + let content_file = content_pool_dir.join(&snapshot.hash); + + // Only write content if the file doesn't exist (avoids duplicates) + if !content_file.exists() && !snapshot.is_deleted { + // Compress and save file content + let compressed_content = encode_all(snapshot.content.as_bytes(), self.compression_level) + .context("Failed to compress file content")?; + fs::write(&content_file, compressed_content)?; + } + + // Create a reference file for this checkpoint's view of the file + let checkpoint_refs_dir = paths.files_dir.join("refs").join(&snapshot.checkpoint_id); + fs::create_dir_all(&checkpoint_refs_dir)?; + + // Save a small JSON file containing metadata and a pointer (hash) to the content pool + let ref_metadata = serde_json::json!({ + "path": snapshot.file_path, + "hash": snapshot.hash, + "is_deleted": snapshot.is_deleted, + "permissions": snapshot.permissions, + "size": snapshot.size, + }); + let safe_filename = snapshot.file_path.to_string_lossy().replace('/', "_").replace('\\', "_"); + let ref_path = checkpoint_refs_dir.join(format!("{}.json", safe_filename)); + fs::write(&ref_path, serde_json::to_string_pretty(&ref_metadata)?)?; + + Ok(()) + } + + // ... update_timeline_with_checkpoint() and other methods ... +} +``` + +This snippet shows how `save_file_snapshot` stores the *actual* file content in a `content_pool` directory, named by the file's hash. This means if the same file content appears in multiple checkpoints, it's only stored once on disk. Then, in a `refs` directory specific to the checkpoint, a small file is saved that just contains the file's metadata and a pointer (the hash) back to the content pool. + +Here is a simplified sequence diagram for creating a manual checkpoint: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (TimelineNavigator.tsx) + participant Backend as Backend Commands (claude.rs) + participant CheckpointState as CheckpointState (state.rs) + participant CheckpointManager as CheckpointManager (manager.rs) + participant CheckpointStorage as CheckpointStorage (storage.rs) + participant Filesystem as Filesystem + + User->>Frontend: Clicks "Checkpoint" button + Frontend->>Backend: Call create_checkpoint(...) + Backend->>CheckpointState: get_or_create_manager(session_id, ...) + CheckpointState->>CheckpointState: Look up manager in map + alt Manager exists + CheckpointState-->>Backend: Return existing manager + else Manager does not exist + CheckpointState->>CheckpointManager: Create new Manager() + CheckpointManager->>CheckpointStorage: init_storage(...) + CheckpointStorage->>Filesystem: Create directories, load timeline.json + Filesystem-->>CheckpointStorage: Return timeline data / Success + CheckpointStorage-->>CheckpointManager: Success + CheckpointManager-->>CheckpointState: Return new manager + CheckpointState->>CheckpointState: Store new manager in map + CheckpointState-->>Backend: Return new manager + end + Backend->>CheckpointManager: create_checkpoint(description, ...) + CheckpointManager->>CheckpointManager: Read current messages + CheckpointManager->>Filesystem: Walk project directory + Filesystem-->>CheckpointManager: List of files + loop For each project file + CheckpointManager->>Filesystem: Read file content & metadata + Filesystem-->>CheckpointManager: File data + CheckpointManager->>CheckpointManager: Track file state (hash, modified) + end + CheckpointManager->>CheckpointStorage: save_checkpoint(checkpoint, snapshots, messages) + CheckpointStorage->>Filesystem: Write metadata.json, messages.jsonl (compressed) + loop For each modified file + CheckpointStorage->>Filesystem: Check if hash exists in content_pool + alt Hash exists + CheckpointStorage->>Filesystem: Skip writing content + else Hash does not exist + CheckpointStorage->>Filesystem: Write compressed file content to content_pool (by hash) + end + CheckpointStorage->>Filesystem: Write reference file (metadata + hash) to refs/ + end + CheckpointStorage->>Filesystem: Update timeline.json + Filesystem-->>CheckpointStorage: Success + CheckpointStorage-->>CheckpointManager: Return success/result + CheckpointManager-->>Backend: Return success/result + Backend-->>Frontend: Resolve Promise + Frontend->>Frontend: Call loadTimeline() to refresh UI + Frontend->>User: Display new checkpoint in timeline +``` + +This diagram illustrates the flow from the user clicking a button to the backend coordinating with the manager, which in turn uses the storage layer to read and write data to the filesystem, resulting in a new checkpoint entry and updated timeline on disk. + +### Restoring a Checkpoint Flow + +Restoring a checkpoint works in reverse. When the frontend calls `api.restoreCheckpoint(checkpointId, ...)`, the backend finds the `CheckpointManager` and calls `manager.restore_checkpoint(checkpointId)`. + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... create_checkpoint() etc. ... + + /// Restore a checkpoint + pub async fn restore_checkpoint(&self, checkpoint_id: &str) -> Result { + // Load checkpoint data using the storage layer + let (checkpoint, file_snapshots, messages) = self.storage.load_checkpoint( + &self.project_id, + &self.session_id, + checkpoint_id, + )?; + + // Get list of all files currently in the project directory + let mut current_files = Vec::new(); + let _ = collect_all_project_files(&self.project_path, &self.project_path, &mut current_files); + + // Determine which files need to be deleted (exist now, but not in snapshot as non-deleted) + let mut checkpoint_files_set = std::collections::HashSet::new(); + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + checkpoint_files_set.insert(snapshot.file_path.clone()); + } + } + + // Delete files not present (as non-deleted) in the checkpoint + for current_file in current_files { + if !checkpoint_files_set.contains(¤t_file) { + let full_path = self.project_path.join(¤t_file); + // ... attempt fs::remove_file(&full_path) ... + log::info!("Deleted file not in checkpoint: {:?}", current_file); + } + } + // ... attempt to remove empty directories ... + + + // Restore/overwrite files from snapshots + let mut files_processed = 0; + for snapshot in &file_snapshots { + // This helper handles creating parent dirs, writing content, setting permissions, or deleting + match self.restore_file_snapshot(snapshot).await { // Calls helper + Ok(_) => { /* ... */ }, + Err(e) => { /* ... collect warnings ... */ }, + } + files_processed += 1; + } + + // Update in-memory messages buffer + let mut current_messages = self.current_messages.write().await; + current_messages.clear(); + for line in messages.lines() { + current_messages.push(line.to_string()); + } + + // Update the current_checkpoint_id in the in-memory timeline + let mut timeline = self.timeline.write().await; + timeline.current_checkpoint_id = Some(checkpoint_id.to_string()); + + // Reset the file tracker state to match the restored checkpoint + let mut tracker = self.file_tracker.write().await; + tracker.tracked_files.clear(); // Clear old state + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + tracker.tracked_files.insert( + snapshot.file_path.clone(), + FileState { + last_hash: snapshot.hash.clone(), + is_modified: false, // Assume clean state after restore + last_modified: Utc::now(), // Or snapshot timestamp if available? + exists: true, + } + ); + } + } + + + Ok(CheckpointResult { /* ... checkpoint, files_processed, warnings ... */ }) + } + + // Helper to restore a single file from its snapshot data + async fn restore_file_snapshot(&self, snapshot: &FileSnapshot) -> Result<()> { + let full_path = self.project_path.join(&snapshot.file_path); + + if snapshot.is_deleted { + // If snapshot indicates deleted, remove the file if it exists + if full_path.exists() { + fs::remove_file(&full_path).context("Failed to delete file")?; + } + } else { + // If snapshot exists, create parent directories and write content + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).context("Failed to create parent directories")?; + } + fs::write(&full_path, &snapshot.content).context("Failed to write file")?; + + // Restore permissions (Unix only) + #[cfg(unix)] + if let Some(mode) = snapshot.permissions { + use std::os::unix::fs::PermissionsExt; + let permissions = std::fs::Permissions::from_mode(mode); + fs::set_permissions(&full_path, permissions).context("Failed to set file permissions")?; + } + } + Ok(()) + } + + // ... other methods ... +} +``` + +The `restore_checkpoint` function reads the checkpoint data from disk using `CheckpointStorage::load_checkpoint`. It then gets a list of the *current* files in the project directory. By comparing the current files with the files present in the checkpoint snapshot, it identifies which files need to be deleted. It iterates through the snapshots, using `restore_file_snapshot` to either delete files or write their content back to the project directory, recreating parent directories and setting permissions as needed. Finally, it updates the in-memory message list and the current checkpoint pointer in the timeline manager. + +This process effectively reverts the project directory and the session's state to match the chosen checkpoint. + +### Forking + +Forking is implemented by first restoring the session to the chosen checkpoint and then immediately creating a *new* checkpoint from that restored state. The key is that the new checkpoint explicitly sets its `parent_checkpoint_id` to the checkpoint it forked *from*, causing the timeline to branch. + +### Automatic Checkpointing + +Automatic checkpointing is controlled by the `auto_checkpoint_enabled` flag and the `checkpoint_strategy` setting stored in the `SessionTimeline`. When a new message arrives in the session (handled by the streaming output processing, [Chapter 7]), the `CheckpointManager::should_auto_checkpoint` method is called. This checks the strategy. For example, if the strategy is `PerPrompt`, it checks if the message is a user prompt. If the strategy is `Smart`, it checks if the message indicates a potentially destructive tool use (like `write`, `edit`, `bash`). If `should_auto_checkpoint` returns `true`, the backend triggers the `create_checkpoint` flow described above. + +### Cleanup + +The `Cleanup` feature in the `CheckpointSettings.tsx` component calls a backend command that uses `CheckpointStorage::cleanup_old_checkpoints`. This function loads the timeline, sorts checkpoints chronologically, identifies checkpoints older than the `keep_count`, and removes their metadata and references from disk. Crucially, it then calls `CheckpointStorage::garbage_collect_content` to find any actual file content in the `content_pool` directory that is *no longer referenced by any remaining checkpoints* and deletes that orphaned content to free up disk space. + +## Conclusion + +In this chapter, we delved into **Checkpointing**, a powerful feature in `claudia` that provides version control for your Claude Code sessions. We learned that checkpoints save snapshots of both your session's message history and the state of your project files, organized into a visual timeline. + +We explored how you can use the UI to create manual checkpoints, restore to previous states, fork off new branches of work, view differences between checkpoints, and configure automatic checkpointing and cleanup settings. + +Under the hood, we saw how the backend uses a `CheckpointManager` per session, coordinates with `CheckpointStorage` for reading and writing to disk, tracks file changes using a `FileTracker`, and uses a content-addressable storage mechanism for file snapshots to save disk space. We walked through the steps involved in creating and restoring checkpoints, including managing file changes and updating the session state. + +Understanding checkpointing empowers you to use Claude Code for more complex and iterative tasks with confidence, knowing you can always revert to a previous state or explore different paths. + +In the next and final chapter, we will explore **MCP (Model Context Protocol)**, the standardized format Claude Code uses for exchanging information with tools and other components, which plays a role in enabling features like checkpointing and tool execution. + +[Next Chapter: MCP (Model Context Protocol)](10_mcp__model_context_protocol__.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/manager.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/state.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/storage.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CheckpointSettings.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/TimelineNavigator.tsx) +```` \ No newline at end of file diff --git a/Claudia-docs/V_1/claudia_1.md b/Claudia-docs/V_1/claudia_1.md new file mode 100644 index 00000000..da2ff5ef --- /dev/null +++ b/Claudia-docs/V_1/claudia_1.md @@ -0,0 +1,1954 @@ + +# Chapter 1: Session/Project Management + +Welcome to the first chapter of the `claudia` tutorial! In this chapter, we'll explore how `claudia` helps you keep track of your work with Claude Code using its Session/Project Management feature. + +Imagine you're using Claude Code to help you build a new feature in a software project. You spend hours talking to Claude, asking it to write code, explain concepts, and debug issues. This interaction is a "session". Your feature development is happening within a specific folder on your computer – that's your "project". + +As you work on different projects, you'll have many sessions. How do you find that helpful conversation you had last week about a bug fix in your "website-redesign" project? How do you pick up where you left off? This is exactly what the Session/Project Management part of `claudia` solves! + +It's like having a digital filing cabinet for all your Claude Code conversations, organized by the project you were working on. + +## What are Projects and Sessions in `claudia`? + +At its core, Session/Project Management deals with two main ideas: + +1. **Projects:** A "Project" in `claudia` (and the underlying Claude Code CLI) corresponds to a directory on your computer where you were running Claude Code. When you start Claude Code in a folder, it recognizes that as a project. +2. **Sessions:** A "Session" is a single, continuous conversation you had with Claude Code within a specific project. Every time you run the `claude` command (or `claude --resume`, `claude --continue`), you're starting or continuing a session. + +The Claude Code CLI automatically saves your conversation history. `claudia` reads this saved history to show you what you've done. + +## Where is the Data Stored? + +The Claude Code CLI stores everything it needs inside a special directory in your home folder: `~/.claude`. + +Inside `~/.claude`, you'll find: + +* A `projects` directory: This is where information about your projects and their sessions is kept. +* Other files like `settings.json` or `CLAUDE.md` (we'll talk about settings and `CLAUDE.md` later). + +Each project you've worked on will have a subdirectory inside `~/.claude/projects`. The name of this subdirectory is a special encoded version of the project's file path. + +Inside a project's directory (`~/.claude/projects/your-project-id/`), you'll find files ending in `.jsonl`. Each `.jsonl` file is a single **session**. The name of the file (before `.jsonl`) is the unique ID for that session. These files contain a history of messages, commands, and tool outputs for that specific conversation. + +## How Does `claudia` Show You Your History? + +Let's look at the user interface of `claudia`. When you open it, you'll likely see a list of your recent projects. Clicking on a project takes you to a list of sessions within that project. You can then click on a session to view its history or resume it. + +Here's a simplified look at how the frontend components display this information: + +```typescript +// src/components/ProjectList.tsx - Simplified structure +import { Card, CardContent } from "@/components/ui/card"; +import type { Project } from "@/lib/api"; + +interface ProjectListProps { + projects: Project[]; // This array comes from the backend + onProjectClick: (project: Project) => void; +} + +export const ProjectList: React.FC = ({ projects, onProjectClick }) => { + return ( +
+ {/* Loop through the projects array */} + {projects.map((project) => ( + onProjectClick(project)}> + +
+

{project.path}

{/* Display the project path */} +

{project.sessions.length} sessions

{/* Show session count */} + {/* ... other project info like creation date */} +
+ {/* ... click handler */} +
+
+ ))} + {/* ... Pagination */} +
+ ); +}; +``` + +This component (`ProjectList.tsx`) takes a list of `Project` objects (fetched from the backend) and renders a card for each one, showing basic info like the project path and how many sessions it contains. When you click a card, it calls the `onProjectClick` function, typically navigating you to the sessions list for that project. + +Next, let's look at how the sessions for a selected project are displayed: + +```typescript +// src/components/SessionList.tsx - Simplified structure +import { Card, CardContent } from "@/components/ui/card"; +import type { Session } from "@/lib/api"; + +interface SessionListProps { + sessions: Session[]; // This array comes from the backend for the selected project + projectPath: string; + onSessionClick?: (session: Session) => void; + onBack: () => void; // Button to go back to project list +} + +export const SessionList: React.FC = ({ sessions, projectPath, onSessionClick, onBack }) => { + return ( +
+ {/* Back button */} +

{projectPath}

{/* Display the current project path */} +
+ {/* Loop through the sessions array */} + {sessions.map((session) => ( + onSessionClick?.(session)}> + +
+

Session ID: {session.id.slice(0, 8)}...

{/* Display truncated session ID */} + {/* Display the first message preview if available */} + {session.first_message &&

First msg: {session.first_message}

} + {/* ... other session info like timestamps */} +
+ {/* ... click handler */} +
+
+ ))} +
+ {/* ... Pagination */} +
+ ); +}; +``` + +The `SessionList.tsx` component receives the list of sessions for a *single* project (again, fetched from the backend). It shows you the project path you're currently viewing and lists each session, often including its ID, creation time, and a preview of the first message. Clicking a session calls `onSessionClick`, which will lead to the conversation view (`ClaudeCodeSession.tsx`). + +## How it Works: Under the Hood + +The frontend components we just saw need data to display. This data is provided by the backend code, which runs in Rust using the Tauri framework. The backend's job for Session/Project Management is to read the files in the `~/.claude` directory and structure that information for the frontend. + +Here's a simplified step-by-step of what happens when the frontend asks for the list of projects: + +1. The frontend calls a backend command, specifically `list_projects`. +2. The backend code starts by finding the `~/.claude` directory on your computer. +3. It then looks inside the `~/.claude/projects` directory. +4. For each directory found inside `projects`, it treats it as a potential project. +5. It reads the name of the project directory (which is an encoded path) and tries to find the actual project path by looking at the session files inside. +6. It also counts the number of `.jsonl` files (sessions) inside that project directory. +7. It gets the creation timestamp of the project directory. +8. It gathers this information (project ID, path, sessions list, creation time) into a `Project` struct. +9. It repeats this for all project directories. +10. Finally, it sends a list of these `Project` structs back to the frontend. + +Fetching sessions for a specific project follows a similar pattern: + +1. The frontend calls the `get_project_sessions` command, providing the `project_id`. +2. The backend finds the specific project directory inside `~/.claude/projects` using the provided `project_id`. +3. It looks inside that project directory for all `.jsonl` files. +4. For each `.jsonl` file (session), it extracts the session ID from the filename. +5. It gets the file's creation timestamp. +6. It reads the *first few lines* of the `.jsonl` file to find the first user message and its timestamp, for display as a preview in the UI. +7. It might also check for related files like todo data (`.json` files in `~/.claude/todos` linked by session ID). +8. It gathers this info into a `Session` struct. +9. It repeats this for all session files in the project directory. +10. Finally, it sends a list of `Session` structs back to the frontend. + +Here's a sequence diagram illustrating the `list_projects` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Open Projects View + Frontend->>Backend: Call list_projects() + Backend->>Filesystem: Read ~/.claude/projects directory + Filesystem-->>Backend: List of project directories + Backend->>Filesystem: For each directory: Read contents (session files) + Filesystem-->>Backend: List of session files + Backend->>Backend: Process directories and files (create Project structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Project List +``` + +And the `get_project_sessions` flow: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Filesystem as ~/.claude + + User->>Frontend: Click on a Project + Frontend->>Backend: Call get_project_sessions(projectId) + Backend->>Filesystem: Read ~/.claude/projects/projectId/ directory + Filesystem-->>Backend: List of session files (.jsonl) + Backend->>Filesystem: For each session file: Read first lines, read metadata + Filesystem-->>Backend: First message, timestamp, creation time, etc. + Backend->>Backend: Process session files (create Session structs) + Backend-->>Frontend: Return List + Frontend->>User: Display Session List for Project +``` + +## Diving into the Code + +Let's look at some specific parts of the Rust code in `src-tauri/src/commands/claude.rs` that handle this logic. + +First, the data structures that represent a project and a session: + +```rust +// src-tauri/src/commands/claude.rs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, // The encoded directory name + pub path: String, // The decoded or detected real path + pub sessions: Vec, // List of session file names (IDs) + pub created_at: u64, // Timestamp +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: String, // The session file name (UUID) + pub project_id: String, // Link back to the project + pub project_path: String, // The project's real path + pub todo_data: Option, // Optional associated data + pub created_at: u64, // Timestamp + pub first_message: Option, // Preview of the first user message + pub message_timestamp: Option, // Timestamp of the first message +} +// ... rest of the file +``` + +These `struct` definitions tell us what information the backend collects and sends to the frontend for projects and sessions. Notice the `Serialize` and `Deserialize` derives; this is what allows Tauri to easily pass these structures between the Rust backend and the JavaScript/TypeScript frontend. + +Here's the function that finds the base `~/.claude` directory: + +```rust +// src-tauri/src/commands/claude.rs +fn get_claude_dir() -> Result { + dirs::home_dir() // Find the user's home directory + .context("Could not find home directory")? // Handle potential error + .join(".claude") // Append the .claude directory name + .canonicalize() // Resolve symbolic links, etc. + .context("Could not find ~/.claude directory") // Handle potential error +} +// ... rest of the file +``` + +This simple function is crucial as all project and session data is located relative to `~/.claude`. + +Now, a look at the `list_projects` function. We'll skip some error handling and logging for brevity here: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn list_projects() -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let projects_dir = claude_dir.join("projects"); // Path to ~/.claude/projects + + if !projects_dir.exists() { + return Ok(Vec::new()); // Return empty list if directory doesn't exist + } + + let mut projects = Vec::new(); + + // Iterate over entries inside ~/.claude/projects + let entries = fs::read_dir(&projects_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + if path.is_dir() { // Only process directories + let dir_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| "...").unwrap(); + + // Get creation/modification timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Determine the actual project path (explained next) + let project_path = match get_project_path_from_sessions(&path) { + Ok(p) => p, + Err(_) => decode_project_path(dir_name) // Fallback if session files don't exist + }; + + // Find all session files (.jsonl) in this project directory + let mut sessions = Vec::new(); + if let Ok(session_entries) = fs::read_dir(&path) { + for session_entry in session_entries.flatten() { + let session_path = session_entry.path(); + if session_path.is_file() && session_path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str()) { + sessions.push(session_id.to_string()); // Store session ID (filename) + } + } + } + } + + // Add the project to the list + projects.push(Project { + id: dir_name.to_string(), + path: project_path, + sessions, + created_at, + }); + } + } + + projects.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(projects) +} +// ... rest of the file +``` + +This code reads the `projects` directory, identifies subdirectories as projects, and collects basic information for each. A key part is determining the *actual* project path, as the directory name is an encoded version of the path where Claude Code was run. The `get_project_path_from_sessions` function handles this: + +```rust +// src-tauri/src/commands/claude.rs +fn get_project_path_from_sessions(project_dir: &PathBuf) -> Result { + // Try to read any JSONL file in the directory + let entries = fs::read_dir(project_dir) + .map_err(|e| format!("..."))?; + + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + // Read the first line of the JSONL file + if let Ok(file) = fs::File::open(&path) { + let reader = BufReader::new(file); + if let Some(Ok(first_line)) = reader.lines().next() { + // Parse the JSON and extract "cwd" (current working directory) + if let Ok(json) = serde_json::from_str::(&first_line) { + if let Some(cwd) = json.get("cwd").and_then(|v| v.as_str()) { + return Ok(cwd.to_string()); // Found the project path! + } + } + } + } + } + } + } + + Err("Could not determine project path from session files".to_string()) // Failed to find it +} +// ... rest of the file +``` + +This function is smarter than just decoding the directory name. It opens the first session file it finds within a project directory, reads the very first line (which usually contains metadata including the `cwd` - current working directory - where Claude Code was launched), and uses that `cwd` as the definitive project path. This is more reliable than trying to decode the directory name. + +Finally, let's look at `get_project_sessions`: + +```rust +// src-tauri/src/commands/claude.rs +#[tauri::command] +pub async fn get_project_sessions(project_id: String) -> Result, String> { + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let project_dir = claude_dir.join("projects").join(&project_id); // Path to specific project dir + let todos_dir = claude_dir.join("todos"); // Path to todo data + + if !project_dir.exists() { + return Err(format!("Project directory not found: {}", project_id)); + } + + // Determine the actual project path + let project_path = match get_project_path_from_sessions(&project_dir) { + Ok(p) => p, + Err(_) => decode_project_path(&project_id) // Fallback + }; + + let mut sessions = Vec::new(); + + // Read all files in the project directory + let entries = fs::read_dir(&project_dir).map_err(|e| format!("..."))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("..."))?; + let path = entry.path(); + + // Process only .jsonl files + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + if let Some(session_id) = path.file_stem().and_then(|s| s.to_str()) { // Get filename as session ID + // Get file creation timestamp + let metadata = fs::metadata(&path).map_err(|e| format!("..."))?; + let created_at = metadata.created().or_else(|_| metadata.modified()).unwrap_or(SystemTime::UNIX_EPOCH).duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + + // Extract first user message for preview (explained next) + let (first_message, message_timestamp) = extract_first_user_message(&path); + + // Check for associated todo data file + let todo_path = todos_dir.join(format!("{}.json", session_id)); + let todo_data = if todo_path.exists() { + // ... read and parse todo.json ... + None // Simplified: just show if it exists, not the data + } else { + None + }; + + // Add the session to the list + sessions.push(Session { + id: session_id.to_string(), + project_id: project_id.clone(), + project_path: project_path.clone(), + todo_data, + created_at, + first_message, + message_timestamp, + }); + } + } + } + + sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); // Sort newest first + Ok(sessions) +} +// ... rest of the file +``` + +This function is similar to `list_projects` but focuses on one project directory. It iterates through the files, identifies the `.jsonl` session files, extracts metadata like ID and timestamp, and importantly, calls `extract_first_user_message` to get a quick preview of the conversation's start for the UI. + +The `extract_first_user_message` function reads the session's `.jsonl` file line by line, parses each line as JSON, and looks for the first entry that represents a message from the "user" role, making sure to skip certain types of messages (like the initial system caveat or command outputs) to find the actual user prompt. + +## Putting it Together + +So, the Session/Project Management feature in `claudia` works by: + +1. Reading the file structure created by the Claude Code CLI in `~/.claude`. +2. Identifying directories in `~/.claude/projects` as projects and `.jsonl` files within them as sessions. +3. Extracting key metadata (IDs, paths, timestamps, first message previews). +4. Providing this structured data to the frontend UI via Tauri commands (`list_projects`, `get_project_sessions`). +5. Allowing the frontend (`ProjectList.tsx`, `SessionList.tsx`) to display this information in an organized, browsable way. +6. Enabling the user to select a session, triggering navigation to the main session view (`ClaudeCodeSession.tsx`) where they can see the full history (loaded using `load_session_history`) and potentially resume the conversation. + +This abstraction provides the essential foundation for interacting with your past Claude Code work, allowing you to manage your conversation history effectively. + +## Conclusion + +In this chapter, we learned how `claudia` discovers, lists, and displays your Claude Code projects and sessions by reading files from the `~/.claude` directory. We saw how the frontend components like `ProjectList` and `SessionList` use data provided by backend commands like `list_projects` and `get_project_sessions` to build the navigation interface. We also briefly touched upon how session data (`.jsonl` files) is parsed to show previews. + +Understanding how `claudia` manages sessions and projects is the first step in seeing how it builds a rich user interface on top of the command-line tool. In the next chapter, we'll dive into the concept of [Agents](02_agents_.md), which are central to how Claude Code and `claudia` understand the context of your work. + +[Next Chapter: Agents](02_agents_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ProjectList.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/SessionList.tsx) +# Chapter 2: Agents + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we learned how `claudia` helps you keep track of your conversations with Claude Code by organizing them into projects and sessions stored in the `~/.claude` directory. + +Now that you know how to find your past work, let's talk about the next key concept in `claudia`: **Agents**. + +## What is an Agent? + +Imagine you use Claude Code for different kinds of tasks. Sometimes you need it to act as a strict code reviewer, sometimes as a creative brainstorming partner, and other times as a focused debugger. Each task might require Claude to have a different "personality" or set of instructions. + +Instead of typing out the same long system prompt (the initial instructions you give to Claude) every time, `claudia` lets you save these configurations as **Agents**. + +Think of an Agent as a pre-packaged, specialized assistant you create within `claudia`. Each Agent is designed for a specific purpose, with its own instructions and capabilities already defined. + +**In simpler terms:** + +* An Agent is like a saved profile for how you want Claude Code to behave. +* You give it a name (like "Bug Hunter" or "Documentation Writer"). +* You give it an icon to easily spot it. +* You give it a "System Prompt" - this is the set of rules or instructions that tell Claude how to act for this specific Agent. For example, a "Bug Hunter" agent might have a system prompt like, "You are an expert Python debugger. Analyze the provided code snippets for potential bugs, common errors, and suggest fixes." +* You can set what permissions it has (like if it's allowed to read or write files). +* You choose which Claude model it should use (like Sonnet or Opus). + +Once an Agent is created, you can select it, give it a specific task (like "debug the function in `main.py`"), choose a project directory, and hit "Execute". `claudia` then runs the Claude Code CLI using *that Agent's* configuration. + +This is much more efficient than manually setting options every time you use Claude Code for a particular job! + +## Key Parts of an Agent + +Let's break down the core components that make up an Agent in `claudia`. You'll configure these when you create or edit an Agent: + +| Part | Description | Why it's important | +| :-------------- | :-------------------------------------------------------------------------- | :-------------------------------------------------- | +| **Name** | A human-readable label (e.g., "Code Reviewer", "Creative Writer"). | Helps you identify the Agent. | +| **Icon** | A visual symbol (e.g., 🤖, ✨, 🛠️). | Makes it easy to find the right Agent at a glance. | +| **System Prompt** | The core instructions given to Claude at the start of the conversation. | Defines the Agent's role, personality, and rules. | +| **Model** | Which Claude model (e.g., Sonnet, Opus) the Agent should use. | Affects performance, capabilities, and cost. | +| **Permissions** | Controls what the Agent is allowed to do (file read/write, network). | **Crucial for security** when running code or tools. | +| **Default Task**| Optional pre-filled text for the task input field when running the Agent. | Saves time for common tasks with this Agent. | + +## Creating and Managing Agents + +`claudia` provides a friendly user interface for managing your Agents. You'll typically find this in the main menu under something like "CC Agents". + +### The Agents List + +When you go to the Agents section, you'll see a list (or grid) of all the Agents you've created. + +You can see their name, icon, and options to: + +* **Execute:** Run the Agent with a new task. +* **Edit:** Change the Agent's configuration. +* **Delete:** Remove the Agent. +* **Create:** Add a brand new Agent. + +Let's look at a simplified frontend component (`CCAgents.tsx`) that displays this list: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +export const CCAgents: React.FC = ({ onBack, className }) => { + const [agents, setAgents] = useState([]); + // ... state for loading, view mode, etc. ... + + useEffect(() => { + // Fetch agents from the backend when the component loads + const loadAgents = async () => { + try { + const agentsList = await api.listAgents(); // Call backend API + setAgents(agentsList); + } catch (err) { + console.error("Failed to load agents:", err); + } + }; + loadAgents(); + }, []); + + // ... handleDeleteAgent, handleEditAgent, handleExecuteAgent functions ... + // ... state for pagination ... + + return ( + // ... layout code ... + {/* Agents Grid */} +
+ {/* Loop through the fetched agents */} + {agents.map((agent) => ( + + +
{/* Render agent icon */}
+

{agent.name}

+ {/* ... other agent info ... */} +
+ + {/* Buttons to Execute, Edit, Delete */} + + + + +
+ ))} +
+ // ... pagination and other UI elements ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This simplified code shows how the `CCAgents` component fetches a list of `Agent` objects from the backend using `api.listAgents()` and then displays them in cards, providing buttons for common actions. + +### Creating or Editing an Agent + +Clicking "Create" or "Edit" takes you to a different view (`CreateAgent.tsx`). Here, you'll find a form where you can fill in the details of the Agent: name, choose an icon, write the system prompt, select the model, set permissions, and add an optional default task. + +A snippet from the `CreateAgent.tsx` component: + +```typescript +// src/components/CreateAgent.tsx (Simplified) +// ... imports ... +export const CreateAgent: React.FC = ({ + agent, // If provided, we are editing + onBack, + onAgentCreated, + className, +}) => { + const [name, setName] = useState(agent?.name || ""); + const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || ""); + // ... state for icon, model, permissions, etc. ... + + const isEditMode = !!agent; + + const handleSave = async () => { + // ... validation ... + try { + // ... set saving state ... + if (isEditMode && agent.id) { + // Call backend API to update agent + await api.updateAgent(agent.id, name, /* ... other fields ... */ systemPrompt, /* ... */); + } else { + // Call backend API to create new agent + await api.createAgent(name, /* ... other fields ... */ systemPrompt, /* ... */); + } + onAgentCreated(); // Notify parent component + } catch (err) { + console.error("Failed to save agent:", err); + // ... show error ... + } finally { + // ... unset saving state ... + } + }; + + // ... handleBack function with confirmation ... + + return ( + // ... layout code ... +
+ {/* Header with Back and Save buttons */} +
+ +

{isEditMode ? "Edit CC Agent" : "Create CC Agent"}

+ +
+ + {/* Form fields */} +
+ {/* Name Input */} +
+ + setName(e.target.value)} /> +
+ + {/* Icon Picker */} + {/* ... component for selecting icon ... */} + + {/* Model Selection */} + {/* ... buttons/radios for model selection ... */} + + {/* Default Task Input */} + {/* ... input for default task ... */} + + {/* Sandbox Settings (Separate Component) */} + + + {/* System Prompt Editor */} +
+ + {/* ... MDEditor component for system prompt ... */} +
+
+
+ // ... Toast Notification ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component manages the state for the agent's properties and calls either `api.createAgent` or `api.updateAgent` from the backend API layer when the "Save" button is clicked. + +Notice the inclusion of `AgentSandboxSettings`. This is a smaller component (`AgentSandboxSettings.tsx`) specifically for managing the permission toggles: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +export const AgentSandboxSettings: React.FC = ({ + agent, // Receives the current agent state + onUpdate, // Callback to notify parent of changes + className +}) => { + // ... handleToggle function ... + + return ( + + {/* ... Header with Shield icon ... */} +
+ {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} // Update parent state + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} // Update parent state + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} // Update parent state + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} // Update parent state + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} +
+
+ ); +}; +``` + +This component simply displays the current sandbox settings for the agent and provides switches to toggle them. When a switch is toggled, it calls the `onUpdate` prop to inform the parent (`CreateAgent`) component, which manages the overall agent state. + +## Executing an Agent + +Once you have agents created, the main purpose is to *run* them. Selecting an agent from the list and clicking "Execute" (or the Play button) takes you to the Agent Execution view (`AgentExecution.tsx`). + +Here's where you: + +1. Select a **Project Path**: This is the directory where the agent will run and where it can potentially read/write files (subject to its permissions). This ties back to the projects we discussed in [Chapter 1: Session/Project Management](01_session_project_management_.md). +2. Enter the **Task**: This is the specific request you have for the agent *for this particular run*. +3. (Optional) Override the **Model**: Choose a different model (Sonnet/Opus) just for this run if needed. +4. Click **Execute**. + +The `AgentExecution.tsx` component handles this: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +export const AgentExecution: React.FC = ({ + agent, // The agent being executed + onBack, + className, +}) => { + const [projectPath, setProjectPath] = useState(""); + const [task, setTask] = useState(""); + const [model, setModel] = useState(agent.model || "sonnet"); // Default to agent's model + const [isRunning, setIsRunning] = useState(false); + const [messages, setMessages] = useState([]); // Output messages + // ... state for stats, errors, etc. ... + + // ... useEffect for listeners and timers ... + + const handleSelectPath = async () => { + // Use Tauri dialog to select a directory + const selected = await open({ directory: true, multiple: false }); + if (selected) { + setProjectPath(selected as string); + } + }; + + const handleExecute = async () => { + if (!projectPath || !task.trim()) return; // Basic validation + + try { + setIsRunning(true); + setMessages([]); // Clear previous output + // ... reset stats, setup listeners ... + + // Call backend API to execute the agent + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... show error, update state ... + } + }; + + // ... handleStop, handleBackWithConfirmation functions ... + + return ( + // ... layout code ... +
+ {/* Header with Back button and Agent Name */} +
+ +

{agent.name}

+ {/* ... Running status indicator ... */} +
+ + {/* Configuration Section */} +
+ {/* ... Error display ... */} + {/* Project Path Input with Select Button */} +
+ + setProjectPath(e.target.value)} disabled={isRunning} /> + +
+ {/* Model Selection Buttons */} + {/* ... buttons for Sonnet/Opus selection ... */} + {/* Task Input with Execute/Stop Button */} +
+ + setTask(e.target.value)} disabled={isRunning} /> + +
+
+ + {/* Output Display Section */} +
+ {/* Messages are displayed here, streaming as they arrive */} + {/* ... Rendering messages using StreamMessage component ... */} +
+ + {/* Floating Execution Control Bar */} + {/* ... Component showing elapsed time, tokens, etc. ... */} +
+ // ... Fullscreen Modal ... + ); +}; +// ... AGENT_ICONS and other types ... +``` + +This component uses the `api.executeAgent` Tauri command to start the agent's run. It also sets up event listeners (`agent-output`, `agent-error`, `agent-complete`) to receive data and status updates from the backend *while* the agent is running. This streaming output is then displayed to the user, which we'll cover in more detail in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md). + +## How it Works: Under the Hood + +Let's peek behind the curtain to understand how `claudia` handles Agents in the backend (Rust code). + +### Agent Storage + +Unlike projects and sessions which are managed by the Claude Code CLI itself in the filesystem (`~/.claude`), `claudia` stores its Agent definitions in a local SQLite database file, typically located within `claudia`'s application data directory (e.g., `~/.config/claudia/agents.db` on Linux, or similar paths on macOS/Windows). + +The `Agent` struct in the Rust backend corresponds to the data stored for each agent: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + pub id: Option, // Database ID + pub name: String, + pub icon: String, + pub system_prompt: String, + pub default_task: Option, + pub model: String, // e.g., "sonnet", "opus" + // Permissions managed directly on the agent struct + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, + pub enable_network: bool, + pub created_at: String, + pub updated_at: String, +} +// ... rest of the file +``` + +The database initialization (`init_database` function) creates the `agents` table to store this information. Backend functions like `list_agents`, `create_agent`, `update_agent`, and `delete_agent` interact with this SQLite database to perform the requested actions. They simply execute standard SQL commands (SELECT, INSERT, UPDATE, DELETE) to manage the `Agent` records. + +Here's a tiny snippet showing a database interaction (listing agents): + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[tauri::command] +pub async fn list_agents(db: State<'_, AgentDb>) -> Result, String> { + let conn = db.0.lock().map_err(|e| e.to_string())?; // Get database connection + + let mut stmt = conn + .prepare("SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC") + .map_err(|e| e.to_string())?; // Prepare SQL query + + let agents = stmt + .query_map([], |row| { // Map database rows to Agent structs + Ok(Agent { + id: Some(row.get(0)?), + name: row.get(1)?, + // ... map other fields ... + system_prompt: row.get(3)?, + model: row.get::<_, String>(5).unwrap_or_else(|_| "sonnet".to_string()), + sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true), + enable_file_read: row.get::<_, bool>(7).unwrap_or(true), + enable_file_write: row.get::<_, bool>(8).unwrap_or(true), + enable_network: row.get::<_, bool>(9).unwrap_or(false), + created_at: row.get(10)?, + updated_at: row.get(11)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(agents) // Return the list of Agent structs +} +``` + +This snippet shows how `list_agents` connects to the database, prepares a simple `SELECT` statement, and then uses `query_map` to convert each row returned by the database into an `Agent` struct, which is then sent back to the frontend. + +### Agent Execution Flow + +When you click "Execute" for an Agent: + +1. The frontend (`AgentExecution.tsx`) calls the backend command `execute_agent` ([Chapter 4: Tauri Commands](04_tauri_commands_.md)), passing the agent's ID, the selected project path, and the entered task. +2. The backend receives the call and retrieves the full details of the selected Agent from the database. +3. It creates a record in the `agent_runs` database table. This table keeps track of each individual execution run of an agent, including which agent was run, the task given, the project path, and its current status (pending, running, completed, failed, cancelled). This links back to the run history shown in the `CCAgents.tsx` component and managed by the `AgentRun` struct: + ```rust + // src-tauri/src/commands/agents.rs (Simplified) + #[derive(Debug, Serialize, Deserialize, Clone)] + pub struct AgentRun { + pub id: Option, // Database ID for this run + pub agent_id: i64, // Foreign key linking to the Agent + pub agent_name: String, // Stored for convenience + pub agent_icon: String, // Stored for convenience + pub task: String, // The task given for this run + pub model: String, // The model used for this run + pub project_path: String, // The directory where it was executed + pub session_id: String, // The UUID from the Claude Code CLI session + pub status: String, // 'pending', 'running', 'completed', 'failed', 'cancelled' + pub pid: Option, // Process ID if running + pub process_started_at: Option, + pub created_at: String, + pub completed_at: Option, + } + ``` + When the run starts, the status is set to 'running', and the Process ID (PID) is recorded. +4. Based on the Agent's configured permissions (`enable_file_read`, `enable_file_write`, `enable_network`), the backend constructs a sandbox profile. This process involves defining rules that the operating system will enforce to limit what the `claude` process can access or do. This is a core part of the [Sandboxing](06_sandboxing_.md) concept. +5. The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It includes arguments like: + * `-p "the task"` + * `--system-prompt "the agent's system prompt"` + * `--model "the selected model"` + * `--output-format stream-json` (to get structured output) + * `--dangerously-skip-permissions` (since `claudia` manages permissions via the sandbox, it tells `claude` not to ask the user). + * The command is also set to run in the specified project directory. +6. The backend then *spawns* the `claude` process within the sandbox environment. +7. As the `claude` process runs, its standard output (stdout) and standard error (stderr) streams are captured by the backend ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +8. The backend processes this output. For JSONL output from Claude Code, it extracts information like message content and session IDs. +9. It emits events back to the frontend (`agent-output`, `agent-error`) using the Tauri event system. +10. The frontend (`AgentExecution.tsx`) listens for these events and updates the displayed messages in real-time. +11. The backend also detects when the `claude` process finishes (either successfully, with an error, or if killed). +12. When the process finishes, the backend updates the `agent_runs` record in the database, setting the status to 'completed', 'failed', or 'cancelled' and recording the completion timestamp. + +Here's a simplified sequence diagram for Agent execution: + +```mermaid +sequenceDiagram + participant User + participant Frontend as Claudia UI (AgentExecution.tsx) + participant Backend as Tauri Commands (agents.rs) + participant Database as agents.db + participant Sandbox + participant ClaudeCLI as claude binary + + User->>Frontend: Clicks "Execute Agent" + Frontend->>Backend: Call execute_agent(agentId, path, task, model) + Backend->>Database: Read Agent config by ID + Database-->>Backend: Return Agent config + Backend->>Database: Create AgentRun record (status=pending/running) + Database-->>Backend: Return runId + Backend->>Sandbox: Prepare environment based on Agent permissions + Sandbox-->>Backend: Prepared environment/command + Backend->>ClaudeCLI: Spawn process (with task, prompt, model, in sandbox, in project path) + ClaudeCLI-->>Backend: Stream stdout/stderr (JSONL) + Backend->>Frontend: Emit "agent-output" events (parsed messages) + Frontend->>User: Display messages in UI + ClaudeCLI-->>Backend: Process finishes + Backend->>Database: Update AgentRun record (status=completed/failed/cancelled) + Database-->>Backend: Confirmation + Backend->>Frontend: Emit "agent-complete" event + Frontend->>User: Update UI (execution finished) +``` + +This diagram illustrates how the frontend initiates the run, the backend fetches the agent's configuration, prepares the environment (including sandbox rules), launches the `claude` process, captures its output, and updates the UI and database based on the process's progress and completion. + +## Conclusion + +In this chapter, we introduced the concept of **Agents** in `claudia`. We learned that Agents are customizable configurations for the Claude Code CLI, allowing you to define specific roles, instructions (System Prompt), models, and crucially, permissions for different types of tasks. + +We saw how the `claudia` UI allows you to easily create, edit, list, and execute these Agents, and how the backend stores Agent definitions in a local database. We also got a high-level view of the execution process, understanding that `claudia` launches the `claude` binary with the Agent's settings and captures its output. A key part of this is the preparation of a secure execution environment based on the Agent's defined permissions, which introduces the idea of sandboxing. + +Understanding Agents is fundamental, as they are the primary way you'll interact with Claude Code through `claudia` for structured tasks. In the next chapter, we'll zoom out and look at how the different visual parts of the `claudia` application you've seen connect together – diving into [Frontend UI Components](03_frontend_ui_components_.md). + +[Next Chapter: Frontend UI Components](03_frontend_ui_components_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[3]] +``` +(https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CCAgents.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CreateAgent.tsx) + +# Chapter 3: Frontend UI Components + +Welcome back to the `claudia` tutorial! In [Chapter 1: Session/Project Management](01_session_project_management_.md), we explored how `claudia` keeps track of your conversations. In [Chapter 2: Agents](02_agents_.md), we learned about creating and managing specialized configurations for Claude Code tasks. + +Now, let's shift our focus to what you actually *see* and *interact with* when you use `claudia`: its graphical interface. This interface is built using **Frontend UI Components**. + +## What are Frontend UI Components? + +Imagine building something complex, like a house. You don't start by crafting every tiny screw and nail from raw metal. Instead, you use pre-made bricks, windows, doors, and roof tiles. These are like reusable building blocks. + +Frontend UI Components in `claudia` are exactly like these building blocks, but for the visual parts of the application. They are self-contained pieces of the user interface, like: + +* A **Button** you click. +* A **Card** that displays information (like a project or an agent). +* An **Input** field where you type text. +* A **List** that shows multiple items. +* A **Dialog** box that pops up. + +`claudia` uses a popular web development framework called **React** to build these components. They are written using **TypeScript** (which adds type safety) and styled using **Tailwind CSS** (a way to add styles quickly using special class names). + +The key idea is reusability. Instead of designing a button from scratch every time it's needed, you create a `Button` component once and use it everywhere. This makes the UI consistent and development faster. + +## Building Views by Combining Components + +Just like you combine bricks and windows to build a wall, `claudia` combines different UI components to create full views (pages) of the application. + +For example, the list of projects you saw in Chapter 1 is a view. This view isn't one giant piece of code; it's made by combining: + +* `Button` components (like the "Back to Home" button). +* `Card` components, each displaying information about a single project. +* A `ProjectList` component which *contains* all the individual project `Card`s and handles looping through the list of projects. +* Layout components (like `div`s with Tailwind classes) to arrange everything. + +Let's look at a simplified structure of the `App.tsx` file, which acts like the main blueprint for `claudia`'s views. It decides *which* major component (view) to show based on the current state (`view` variable): + +```typescript +// src/App.tsx (Simplified) +import { useState } from "react"; +import { Button } from "@/components/ui/button"; // Import a UI component +import { Card } from "@/components/ui/card"; // Import another UI component +import { ProjectList } from "@/components/ProjectList"; // Import a view component +import { CCAgents } from "@/components/CCAgents"; // Import another view component +// ... other imports ... + +type View = "welcome" | "projects" | "agents" | "settings" | "claude-code-session"; + +function App() { + const [view, setView] = useState("welcome"); // State variable to control current view + // ... other state variables ... + + const renderContent = () => { + switch (view) { + case "welcome": + // Show the welcome view, using Card and Button components + return ( +
{/* Layout */} + setView("agents")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Agents

+
+
+ setView("projects")}> {/* Uses Card */} +
+ {/* Icon component */} +

CC Projects

+
+
+
+ ); + + case "agents": + // Show the Agents view, which is handled by the CCAgents component + return setView("welcome")} />; // Uses CCAgents component + + case "projects": + // Show the Projects/Sessions view + return ( +
{/* Layout */} + {/* Uses Button */} + {/* ... displays either ProjectList or SessionList based on selectedProject state ... */} +
+ ); + + // ... other cases for settings, session view, etc. ... + + default: + return null; + } + }; + + return ( +
+ {/* Topbar component */} + {/* Main content area */} +
+ {renderContent()} {/* Renders the selected view */} +
+ {/* ... other global components like dialogs ... */} +
+ ); +} + +export default App; +``` + +As you can see, `App.tsx` doesn't contain the detailed code for *every* button or card. Instead, it imports and uses components like `Button`, `Card`, `CCAgents`, and `ProjectList`. The `renderContent` function simply decides which larger component to display based on the `view` state. + +## How Components Work Together + +Components communicate with each other primarily through **props** (short for properties) and **callbacks** (functions passed as props). + +* **Props:** Data is passed *down* from parent components to child components using props. For example, the `App` component might pass the list of `projects` to the `ProjectList` component. The `ProjectList` component then passes individual `project` objects down to the `Card` components it renders. +* **Callbacks:** When something happens inside a child component (like a button click), it needs to tell its parent. It does this by calling a function that was passed down as a prop (a callback). For example, when a `Card` in the `ProjectList` is clicked, it calls the `onProjectClick` function that was given to it by `ProjectList`. `ProjectList` received this function from `App`. + +Let's revisit the `ProjectList` component from Chapter 1: + +```typescript +// src/components/ProjectList.tsx (Simplified) +// ... imports ... +import { Card, CardContent } from "@/components/ui/card"; // Uses Card component +import type { Project } from "@/lib/api"; + +interface ProjectListProps { + projects: Project[]; // Prop: receives array of project data + onProjectClick: (project: Project) => void; // Prop: receives a function (callback) +} + +export const ProjectList: React.FC = ({ projects, onProjectClick }) => { + return ( +
+ {/* Loops through the projects array received via props */} + {projects.map((project) => ( + // Renders a Card component for each project + onProjectClick(project)}> {/* Calls the onProjectClick callback when clicked */} + {/* Uses CardContent sub-component */} +
+

{project.path}

{/* Displays data received from the project prop */} +

{project.sessions.length} sessions

{/* Displays data from the project prop */} +
+
+
+ ))} +
+ ); +}; +``` + +This component clearly shows: +1. It receives data (`projects` array) and a function (`onProjectClick`) as props. +2. It loops through the `projects` array. +3. For each item, it renders a `Card` component (another UI component). +4. It passes data (`project.path`, `project.sessions.length`) into the `CardContent` to be displayed. +5. It attaches an `onClick` handler to the `Card` that calls the `onProjectClick` callback function, passing the relevant `project` data back up to the parent component (`App` in this case). + +Similarly, the `CCAgents` component from Chapter 2 receives data and callbacks: + +```typescript +// src/components/CCAgents.tsx (Simplified) +// ... imports ... +import { Card, CardContent, CardFooter } from "@/components/ui/card"; // Uses Card components +import { Button } from "@/components/ui/button"; // Uses Button component +// ... types and state ... + +export const CCAgents: React.FC = ({ onBack, className }) => { + // ... state for agents data ... + + // ... useEffect to load agents (calls backend, covered in Chapter 2) ... + + // Callback functions for actions + const handleExecuteAgent = (agent: Agent) => { + // ... navigate to execution view ... + }; + const handleEditAgent = (agent: Agent) => { + // ... navigate to edit view ... + }; + const handleDeleteAgent = (agentId: number) => { + // ... call backend API to delete ... + }; + + return ( +
+ {/* ... Back button using Button component calling onBack prop ... */} + + {/* Agents Grid */} +
+ {/* Loop through agents state */} + {agents.map((agent) => ( + {/* Uses Card */} + {/* Uses CardContent */} + {/* ... display agent icon, name (data from agent state) ... */} + + {/* Uses CardFooter */} + {/* Buttons using Button component, calling local callbacks */} + + + + + + ))} +
+ {/* ... pagination ... */} +
+ ); +}; +``` + +This component shows how UI components (`Card`, `Button`) are used within a larger view component (`CCAgents`). `CCAgents` manages its own state (the list of `agents`) and defines callback functions (`handleExecuteAgent`, `handleEditAgent`, `handleDeleteAgent`) which are triggered by user interaction with the child `Button` components. It also receives an `onBack` prop from its parent (`App`) to navigate back. + +## Common UI Components in `claudia` + +`claudia` uses a set of pre-built, simple UI components provided by a library often referred to as "shadcn/ui" (though integrated directly into the project). You saw some examples in the code: + +* **`Button`**: Used for clickable actions (`components/ui/button.tsx`). +* **`Card`**: Used to group related information with a border and shadow (`components/ui/card.tsx`). It often has `CardHeader`, `CardContent`, and `CardFooter` sub-components for structure. +* **`Input`**: Used for single-line text entry fields (similar to standard HTML ``, used in `CreateAgent`, `AgentExecution`). +* **`Textarea`**: Used for multi-line text entry, like for the system prompt (`components/ui/textarea.tsx`, used in `CreateAgent`). +* **`Switch`**: Used for toggling options on/off, like permissions in the sandbox settings (`components/ui/switch.tsx`, used in `AgentSandboxSettings`). +* **`Label`**: Used to associate text labels with form elements (`components/ui/label.tsx`). +* **`Popover`**: Used to display floating content when a trigger is clicked (`components/ui/popover.tsx`). +* **`Toast`**: Used for temporary notification messages (`components/ui/toast.tsx`). + +You can find these components and others in the `src/components/ui/` directory. Each file defines a single, reusable UI component using React's functional component pattern, TypeScript for typing props, and Tailwind CSS classes for styling. + +For example, the `Button` component (`components/ui/button.tsx`) defines different visual `variant`s (default, destructive, outline, secondary, ghost, link) and `size`s (default, sm, lg, icon) using `class-variance-authority` and then applies the corresponding Tailwind classes (`cn` utility combines class names). When you use ``. + +## How it Works: Under the Hood (Frontend) + +The core idea behind these UI components in React is quite simple: + +1. **They are functions or classes:** A component is essentially a JavaScript/TypeScript function (or class) that receives data as `props`. +2. **They return UI:** This function returns a description of what the UI should look like (React elements, often resembling HTML). +3. **React renders the UI:** React takes this description and efficiently updates the actual web page (the Document Object Model or DOM) to match. +4. **State for interactivity:** Some components manage their own internal data called `state` (e.g., an input component's text value, whether a dialog is open). When state changes, the component re-renders. +5. **Event Handlers:** Components respond to user interactions (like clicks, typing) by calling functions defined within them or received via props (callbacks). + +The process looks like this: + +```mermaid +graph TD + A[App.tsx] --> B(Passes props like projects, callbacks like handleProjectClick) + B --> C{ProjectList Component} + C --> D(Iterates through projects, passes individual project + onProjectClick to Cards) + D --> E{Card Component (for a single project)} + E --> F(Receives project data + onProjectClick) + F -- Displays Data --> G[UI on screen (a Card)] + G -- User Clicks Card --> H(onClick handler in Card) + H --> I(Calls the onProjectClick callback received via props) + I --> J(Returns the clicked project data) + J --> C(ProjectList receives data) + C --> K(Calls the onProjectClick callback received via props) + K --> A(App.tsx receives clicked project data) + A -- Updates state (e.g., setSelectedProject) --> A + A -- Re-renders with new view --> L[New UI on screen (e.g., SessionList)] +``` + +This diagram shows the flow of data (props) and events (callbacks) that allows components to work together to create a dynamic interface. `App.tsx` is at the top, managing the main state (`view`, `selectedProject`). It passes data and functions down to its children (`ProjectList`). `ProjectList` loops and renders more children (`Card`). When a `Card` receives a user action, it calls a function passed down (`onProjectClick`), sending relevant data back up the chain, which triggers state changes in the parent (`App`), leading to a re-render and a different view being displayed. + +## Conclusion + +In this chapter, we explored Frontend UI Components, the reusable building blocks that form the visual interface of `claudia`. We learned that these components, built with React, TypeScript, and Tailwind CSS, are combined like Lego bricks to create complex views like project lists, agent managers, and the main session interface. + +We saw how components receive data through `props` and communicate back to their parents using `callbacks`. This system allows the UI to be modular, consistent, and maintainable. Understanding these components is key to seeing how `claudia` presents information and interacts with the user. + +In the next chapter, we'll bridge the gap between the frontend UI components and the backend Rust logic by learning about [Tauri Commands](04_tauri_commands_.md). These commands are the communication layer that allows the components to ask the backend for data (like listing projects) or request actions (like executing an agent). + +[Next Chapter: Tauri Commands](04_tauri_commands_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/App.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/index.ts), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/badge.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/button.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/card.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/popover.tsx), [[7]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ui/textarea.tsx) + +# Chapter 4: Tauri Commands + +Welcome back to the `claudia` tutorial! In [Chapter 3: Frontend UI Components](03_frontend_ui_components_.md), we explored the visual building blocks that make up `claudia`'s interface, like Buttons and Cards, and how they communicate with each other using props and callbacks. + +But those frontend components, written in TypeScript/JavaScript, can't directly talk to your operating system. They can't read files, launch other programs, or perform heavy computations safely and efficiently. This is where the backend, written in Rust, comes in. + +We need a way for the frontend UI (your browser-like window) to ask the powerful native backend to do things for it. That communication bridge is what **Tauri Commands** are all about. + +## What are Tauri Commands? + +Think of Tauri Commands as a special "phone line" or "API" that connects the frontend world (where the user clicks buttons and sees things) to the backend world (where the native code runs). + +When the user clicks a button in `claudia`'s UI, and that button needs to do something like: + +* List your projects (which requires reading the file system). +* Create a new Agent (which requires saving to a database). +* Execute a Claude Code session (which requires launching a separate process). + +...the frontend can't do this itself. Instead, it calls a specific **Tauri Command** that lives in the Rust backend. The backend command performs the requested action and then sends the result back to the frontend. + +**In simple terms:** + +* Tauri Commands are functions in the Rust backend. +* They are specifically marked so that Tauri knows they can be called from the frontend. +* The frontend calls these functions using a special `invoke` mechanism provided by Tauri. +* This allows the frontend to trigger native actions and get data from the backend. + +This separation keeps the UI responsive and safe, while the backend handles the heavy lifting and privileged operations. + +## How to Call a Tauri Command from the Frontend + +In `claudia`'s frontend (written in TypeScript), you call a backend command using the `invoke` function from the `@tauri-apps/api/core` library. + +The `invoke` function is straightforward: + +```typescript +import { invoke } from "@tauri-apps/api/core"; + +// ... later in your component or API helper ... + +async function exampleCall() { + try { + // Call the command named 'list_projects' + // If the command takes arguments, pass them as the second parameter (an object) + const result = await invoke("list_projects"); + + console.log("Projects received:", result); // Handle the result + // result will be the value returned by the Rust function + + } catch (error) { + console.error("Error calling list_projects:", error); // Handle errors + } +} + +// To actually trigger it, you might call exampleCall() in response to a button click or when a page loads. +``` + +Let's look at the `src/lib/api.ts` file, which we briefly mentioned in previous chapters. This file provides a cleaner way to call backend commands instead of using `invoke` directly everywhere. It defines functions like `listProjects`, `getProjectSessions`, `listAgents`, `createAgent`, `executeAgent`, etc., which wrap the `invoke` calls. + +Here's how the `listProjects` function is defined in `src/lib/api.ts`: + +```typescript +// src/lib/api.ts (Simplified) +import { invoke } from "@tauri-apps/api/core"; +// ... other imports and type definitions ... + +/** + * Represents a project in the ~/.claude/projects directory + */ +export interface Project { + // ... project fields ... +} + +/** + * API client for interacting with the Rust backend + */ +export const api = { + /** + * Lists all projects in the ~/.claude/projects directory + * @returns Promise resolving to an array of projects + */ + async listProjects(): Promise { // Defines a friendly TypeScript function + try { + // Calls the actual Tauri command named "list_projects" + return await invoke("list_projects"); + } catch (error) { + console.error("Failed to list projects:", error); + throw error; // Re-throw the error for the caller to handle + } + }, + + // ... other API functions like getProjectSessions, listAgents, etc. +}; +``` + +Now, in a frontend component like `ProjectList.tsx` or its parent view, instead of `invoke`, you'll see code calling `api.listProjects()`: + +```typescript +// src/components/ProjectList.tsx (Simplified - from Chapter 1) +import React, { useEffect, useState } from 'react'; +// ... other imports ... +import { api, type Project } from "@/lib/api"; // Import the api client and types + +// ... component definition ... + +export const ProjectList: React.FC = ({ onProjectClick }) => { + const [projects, setProjects] = useState([]); + // ... other state ... + + useEffect(() => { + // Fetch projects from the backend when the component loads + const loadProjects = async () => { + try { + // Call the backend command via the api helper + const projectsList = await api.listProjects(); + setProjects(projectsList); // Update the component's state with the data + } catch (err) { + console.error("Failed to load projects:", err); + } + }; + loadProjects(); // Call the function to load data + }, []); // Empty dependency array means this runs once after initial render + + // ... render function using the 'projects' state ... + // Uses projects.map to display each project (as shown in Chapter 1) +}; +``` + +This shows the typical pattern: A frontend component needs data, so it calls a function in `src/lib/api.ts` (like `api.listProjects`), which in turn uses `invoke` to call the corresponding backend command. The component then uses the received data (`projectsList`) to update its state and render the UI. + +## How to Define a Tauri Command in the Backend (Rust) + +Now, let's look at the other side: how the backend tells Tauri that a specific Rust function can be called as a command. + +This is done using the `#[tauri::command]` attribute right above the function definition. These command functions typically live in modules within the `src-tauri/src/commands/` directory (like `claude.rs` or `agents.rs`). + +Here's the simplified Rust code for the `list_projects` command, located in `src-tauri/src/commands/claude.rs`: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +use tauri::command; +use serde::{Serialize, Deserialize}; // Needed for sending data back + +// Define the structure that will be sent back to the frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub id: String, + pub path: String, + pub sessions: Vec, + pub created_at: u64, +} + +// Mark this function as a Tauri command +#[command] +pub async fn list_projects() -> Result, String> { + // ... Code to find ~/.claude and read project directories ... + // This is where the file system access happens (backend logic) + + let mut projects = Vec::new(); + + // Simplified: Imagine we found some projects and populated the 'projects' vector + // For a real implementation, see the detailed code snippet in Chapter 1 + + // Example placeholder data: + projects.push(Project { + id: "encoded-path-1".to_string(), + path: "/path/to/my/project1".to_string(), + sessions: vec!["session1_id".to_string(), "session2_id".to_string()], + created_at: 1678886400, // Example timestamp + }); + projects.push(Project { + id: "encoded-path-2".to_string(), + path: "/path/to/my/project2".to_string(), + sessions: vec!["session3_id".to_string()], + created_at: 1678972800, // Example timestamp + }); + + + // Return the vector of Project structs. + // Result is often used for commands that might fail. + // Tauri automatically serializes Vec into JSON for the frontend. + Ok(projects) +} + +// ... other commands defined in this file ... +``` + +Key points here: + +1. `#[tauri::command]`: This attribute is essential. It tells Tauri to generate the necessary code to make this Rust function callable from the frontend JavaScript. +2. `pub async fn`: Commands are typically `async` functions because they often perform non-blocking operations (like reading files, launching processes) that shouldn't block the main UI thread. They must also be `pub` (public) so Tauri can access them. +3. `Result, String>`: This is the return type. `Result` is a standard Rust type for handling operations that can either succeed (`Ok`) or fail (`Err`). Here, on success, it returns a `Vec` (a list of `Project` structs); on failure, it returns a `String` error message. Tauri handles converting this Rust `Result` into a JavaScript Promise that resolves on `Ok` and rejects on `Err`. +4. `#[derive(Serialize, Deserialize)]`: Any custom data structures (like `Project` here) that you want to send between the frontend and backend must be able to be converted to/from a common format like JSON. `serde` is a Rust library for this, and deriving `Serialize` and `Deserialize` (for data going back and forth) makes this automatic. + +## Registering Commands + +Finally, for Tauri to know about your command functions, they need to be registered in the main application entry point, `src-tauri/src/main.rs`. + +In `src-tauri/src/main.rs`, there's a section using `tauri::generate_handler!` that lists all the command functions that the frontend is allowed to call: + +```rust +// src-tauri/src/main.rs (Simplified) +// ... imports ... + +mod commands; // Import your commands module + +use commands::claude::{ + list_projects, // Import the specific command functions + get_project_sessions, + // ... import other claude commands ... +}; +use commands::agents::{ + list_agents, // Import agent commands + create_agent, + execute_agent, + // ... import other agent commands ... +}; +// ... import commands from other modules like sandbox, usage, mcp ... + +fn main() { + // ... setup code ... + + tauri::Builder::default() + // ... plugins and setup ... + .invoke_handler(tauri::generate_handler![ // **This is where commands are registered!** + list_projects, // List the name of each command function + get_project_sessions, + list_agents, + create_agent, + execute_agent, + // ... list all other commands you want to expose ... + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +The `tauri::generate_handler! macro` takes a list of function names that are marked with `#[tauri::command]`. It generates the code needed for Tauri's core to receive `invoke` calls from the frontend and route them to the correct Rust function. If a command isn't listed here, the frontend can't call it. + +## How it Works: Under the Hood + +Let's visualize the flow when the frontend calls a Tauri Command. + +Imagine the user is on the Projects screen, and the `ProjectList` component needs the list of projects: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ProjectList.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant Filesystem as Filesystem + + FrontendUI->>FrontendAPI: Need projects list + FrontendAPI->>TauriCore: invoke("list_projects") + Note over TauriCore: Tauri routes call to registered handler + TauriCore->>BackendCommands: Call list_projects() function + BackendCommands->>Filesystem: Read ~/.claude/projects + Filesystem-->>BackendCommands: Return directory contents + BackendCommands->>BackendCommands: Process data (create Project structs) + BackendCommands-->>TauriCore: Return Result, String> + TauriCore-->>FrontendAPI: Resolve invoke Promise with Vec + FrontendAPI-->>FrontendUI: Return projects data + FrontendUI->>FrontendUI: Update state with projects data + FrontendUI->>FrontendUI: Render UI (display projects) +``` + +1. The `ProjectList` component (Frontend UI) decides it needs the list of projects, perhaps in a `useEffect` hook when it mounts. +2. It calls `api.listProjects()` (Frontend API wrapper). +3. `api.listProjects()` calls `invoke("list_projects")`, which sends a message to the Tauri Core. +4. The Tauri Core receives the message "call command 'list\_projects'" and looks up the corresponding registered Rust function. +5. The Tauri Core executes the `list_projects()` function in the Backend Commands module. +6. The Rust function performs its logic, which involves interacting with the Filesystem (reading directories and files). +7. The Filesystem returns the necessary data to the Rust function. +8. The Rust function processes this data and constructs the `Vec` result. +9. The Rust function returns the `Result, String>`. Tauri automatically serializes the `Vec` into JSON. +10. The Tauri Core receives the result and sends it back to the frontend process. +11. The Promise returned by the initial `invoke` call in `api.ts` resolves with the JSON data, which Tauri automatically deserializes back into a TypeScript `Project[]` array. +12. `api.listProjects()` returns this array to the `ProjectList` component. +13. The `ProjectList` component updates its internal state, triggering React to re-render the component, displaying the list of projects on the screen. + +This same pattern is used for almost all interactions where the frontend needs to get information or trigger actions in the backend. For example, when you click "Execute" for an Agent (as seen in Chapter 2), the `AgentExecution.tsx` component calls `api.executeAgent()`, which calls the backend `execute_agent` command, which then launches the `claude` binary (as we'll see in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). + +## Conclusion + +In this chapter, we learned about **Tauri Commands**, the essential communication layer that bridges the gap between the frontend UI (built with React/TypeScript) and the native backend logic (written in Rust). + +We saw how the frontend uses `invoke` (often wrapped by helpful functions in `src/lib/api.ts`) to call named backend commands, passing arguments and receiving results via Promises. We also saw how backend Rust functions are defined using `#[tauri::command]`, must be `pub async fn`, return a `Result`, and how data is serialized using `serde`. Finally, we looked at how these commands are registered in `src-tauri/src/main.rs` using `tauri::generate_handler!`. + +Understanding Tauri Commands is crucial because they are the fundamental way `claudia`'s UI interacts with the powerful, native capabilities provided by the Rust backend. This mechanism allows the frontend to stay focused on presentation while relying on the backend for tasks like file system access, process management, and database interaction. + +In the next chapter, we'll delve into the very core of `claudia`'s function: how it interacts with the command-line `claude` binary to run sessions and execute tasks. + +[Next Chapter: Claude CLI Interaction](05_claude_cli_interaction_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mod.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/lib.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/main.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/lib/api.ts) +# Chapter 5: Claude CLI Interaction + +Welcome back to the `claudia` tutorial! In our previous chapters, we learned about managing your work with Claude Code through [Session/Project Management](01_session_project_management_.md), creating specialized [Agents](02_agents_.md) to define how Claude should behave, how the [Frontend UI Components](03_frontend_ui_components_.md) like buttons and lists build the interface, and how [Tauri Commands](04_tauri_commands_.md) allow the frontend (TypeScript/React) to talk to the backend (Rust). + +Now, let's dive into the core action: how `claudia` actually makes the powerful `claude` command-line tool run and communicate with it. This chapter is all about the **Claude CLI Interaction** layer. + +## The Problem: GUI Needs to Talk to CLI + +You're using `claudia`, which is a beautiful graphical application. You click buttons, type in text boxes, and see output in a nice interface. But the actual intelligence, the part that runs your requests and generates code or text, is the `claude` command-line interface (CLI) tool that you installed separately. + +So, how does `claudia`'s backend, written in Rust, tell the `claude` CLI, which is a separate program running on your computer, what to do? How does it get the response back in real-time to show you? + +This is exactly what the Claude CLI Interaction part of `claudia` handles. It's the bridge between the graphical application and the underlying CLI tool. + +Imagine you're the director of an orchestra (`claudia`). You have a conductor's stand (the UI), but the music is played by the musicians (`claude`). You need a way to signal to the musicians what piece to play, at what tempo, and capture their performance to share with the audience. `claudia`'s CLI Interaction is your way of signaling to the `claude` process and listening to its "music" (the output). + +## What the Claude CLI Interaction Does + +The core function of this layer in `claudia`'s backend is to: + +1. **Find the `claude` binary:** Figure out where the `claude` executable is located on your system. +2. **Prepare the command:** Build the command line that needs to be run, including the `claude` binary path and all the necessary arguments (like the prompt, model, system prompt, etc.). +3. **Spawn the process:** Start the `claude` binary as a separate process. +4. **Control the environment:** Set the working directory for the `claude` process (the project path) and potentially adjust its environment variables (like the PATH). +5. **Manage sandboxing (Optional but important):** If sandboxing is enabled, ensure the `claude` process runs within the defined security restrictions (more on this in [Chapter 6: Sandboxing](06_sandboxing_.md)). +6. **Capture output:** Get the standard output (stdout) and standard error (stderr) streams from the running `claude` process in real-time. +7. **Process output:** Take the raw output (which is in a special JSONL format for Claude Code) and process it. +8. **Report status/output:** Send the processed output and status updates (running, complete, failed) back to the frontend so the user interface can update. +9. **Manage process lifecycle:** Keep track of the running process and handle requests to stop or kill it. + +## Triggering a Claude Code Run from the Frontend + +You've already seen in Chapter 4 how frontend components use `api` functions to call backend commands. This is how you initiate a Claude Code run. + +Whether you're executing an Agent (from `AgentExecution.tsx`) or starting/continuing a direct session (from `ClaudeCodeSession.tsx`), the frontend makes a call to a specific backend command responsible for launching `claude`. + +Here's a simplified look at how `AgentExecution.tsx` initiates a run: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { api, type Agent } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleExecute = async () => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Set up event listeners first (covered in Chapter 7) + // These listeners will receive output and status updates from the backend + const outputUnlisten = await listen("agent-output", (event) => { + // Process received output line (JSONL) + // ... update messages state ... + }); + const completeUnlisten = await listen("agent-complete", (event) => { + // Process completion status + // ... update isRunning state ... + }); + // ... store unlisten functions ... + + // Call the backend command to execute the agent + // This command prepares and spawns the 'claude' process + await api.executeAgent(agent.id!, projectPath, task, model); + + } catch (err) { + console.error("Failed to execute agent:", err); + // ... handle error ... + } +}; + +// ... render function with button calling handleExecute ... +``` + +And here's a similar pattern from `ClaudeCodeSession.tsx` for starting a new session: + +```typescript +// src/components/ClaudeCodeSession.tsx (Simplified) +// ... imports ... +import { api, type Session } from "@/lib/api"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; + +// ... component definition ... + +const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => { + // ... validation and state updates (isLoading, etc.) ... + + try { + // Add the user message to the UI immediately + // ... update messages state ... + + // Clean up old listeners, set up new ones (for "claude-output", "claude-complete") + // ... setup listeners (covered in Chapter 7) ... + + // Call the appropriate backend command + // This command prepares and spawns the 'claude' process + if (isFirstPrompt && !session) { + await api.executeClaudeCode(projectPath, prompt, model); // New session + } else if (session && isFirstPrompt) { + await api.resumeClaudeCode(projectPath, session.id, prompt, model); // Resume session + } else { + await api.continueClaudeCode(projectPath, prompt, model); // Continue conversation + } + + } catch (err) { + console.error("Failed to send prompt:", err); + // ... handle error ... + } +}; + +// ... render function with FloatingPromptInput component calling handleSendPrompt ... +``` + +These snippets show that from the frontend's perspective, starting a Claude Code interaction is simply calling a backend API function (a Tauri Command wrapper) and then listening for events that the backend sends back as the process runs and finishes. + +## How it Works: Under the Hood (Backend) + +When the backend receives a Tauri command like `execute_agent` or `execute_claude_code`, it performs a series of steps to launch and manage the `claude` process. + +Here's a simplified step-by-step flow: + +1. **Find the `claude` executable:** The backend needs the full path to the `claude` binary. It looks in common installation locations and potentially a path saved in `claudia`'s settings. +2. **Determine process parameters:** It gathers the necessary information for the command: the prompt (`-p`), the system prompt (`--system-prompt`, from the Agent config or CLAUDE.md), the model (`--model`), the output format (`--output-format stream-json` is crucial for real-time processing), flags like `--verbose` and `--dangerously-skip-permissions` (since `claudia` handles permissions via sandboxing), and the working directory (`--current-dir` or set via `Command`). +3. **Prepare Sandbox (if enabled):** Based on Agent permissions or global settings, the backend constructs sandbox rules using the `gaol` library. This involves defining what file paths (`file_read_all`, `file_write_all`) and network connections (`network_outbound`) the `claude` process is allowed to make. This is tightly linked to the actual command execution. +4. **Build the Command object:** Rust's standard library (and the `tokio` library for asynchronous operations) provides a `Command` struct to build process commands. The backend creates a `Command` instance, sets the `claude` binary path, adds all the arguments, sets the working directory (`current_dir`), and configures standard input/output (`stdin`, `stdout`, `stderr`) to be piped so the backend can capture them. +5. **Spawn the child process:** The `Command` object is executed using a method like `spawn()`. This starts the `claude` process and gives the backend a handle to it (a `Child` object). +6. **Capture Output Streams:** The `stdout` and `stderr` streams of the child process, which were configured to be piped, are now available as asynchronous readers. The backend spawns separate asynchronous tasks (using `tokio::spawn`) to continuously read lines from these streams. +7. **Process and Emit:** As each line of output (usually a JSON object in the JSONL format) or error arrives, the reading tasks process it (e.g., parse JSON, extract relevant data) and immediately emit it as a Tauri event back to the frontend (`agent-output`, `claude-output`, `agent-error`, `claude-error`). This provides the real-time streaming experience. +8. **Monitor Completion:** The backend also has a task that waits for the `claude` process to finish (`child.wait().await`). When it exits, the task notifies the frontend (e.g., via `agent-complete`, `claude-complete`) and potentially updates internal state or a database record (like the `agent_runs` table for Agents). +9. **Handle Cancellation:** If the user requests to stop the process (e.g., clicking a "Stop" button for an Agent run), the backend uses the process ID (PID) to send a termination signal (`kill`). + +Here's a sequence diagram showing the flow for a standard `execute_claude_code` call: + +```mermaid +sequenceDiagram + participant FrontendUI as Frontend UI (ClaudeCodeSession.tsx) + participant FrontendAPI as Frontend API (api.ts) + participant TauriCore as Tauri Core + participant BackendCommands as Backend Commands (claude.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + FrontendUI->>FrontendAPI: User submits prompt (call executeClaudeCode) + FrontendAPI->>TauriCore: invoke("execute_claude_code", { prompt, path, model }) + Note over TauriCore: Tauri routes call + TauriCore->>BackendCommands: Call execute_claude_code() + BackendCommands->>BackendCommands: Find claude binary path (find_claude_binary) + BackendCommands->>BackendCommands: Prepare Command object (args, cwd, piped streams) + BackendCommands->>OS: Spawn process (claude binary) + OS-->>BackendCommands: Return Child process handle + BackendCommands->>BackendCommands: Spawn tasks to read stdout/stderr + loop While ClaudeCLI is running & produces output + ClaudeCLI-->>OS: Write to stdout/stderr pipe + OS-->>BackendCommands: Data available in pipe + BackendCommands->>BackendCommands: Read & process output line + BackendCommands->>TauriCore: Emit "claude-output" or "claude-error" event + TauriCore-->>FrontendUI: Receive event data + FrontendUI->>FrontendUI: Display output line in UI + end + ClaudeCLI-->>OS: Process exits + OS-->>BackendCommands: Process termination status + BackendCommands->>BackendCommands: Task waits for process exit + BackendCommands->>TauriCore: Emit "claude-complete" event + TauriCore-->>FrontendUI: Receive event + FrontendUI->>FrontendUI: Update UI (execution finished) +``` + +This diagram visually outlines how the request flows from the frontend to the backend, how the backend launches the separate `claude` process via the OS, how output streams back through the backend and Tauri, and finally how the frontend is updated in real-time. + +## Diving into the Backend Code + +Let's look at some key parts of the Rust code in `src-tauri/src/commands/claude.rs` and `src-tauri/src/commands/agents.rs` that handle this process interaction. + +First, finding the binary and setting up the environment: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::process::Command; +use std::process::Stdio; +use tauri::{AppHandle, Emitter, Manager}; + +// This function tries to locate the 'claude' executable +fn find_claude_binary(app_handle: &AppHandle) -> Result { + // ... logic to check settings, common paths, 'which' command ... + // Returns the found path or an error + Ok("path/to/claude".to_string()) // Simplified +} + +// This function creates a Tokio Command object, setting environment variables +fn create_command_with_env(program: &str) -> Command { + let mut cmd = Command::new(program); + + // Inherit essential environment variables like PATH, HOME, etc. + // This helps the 'claude' binary find Node.js and other dependencies + for (key, value) in std::env::vars() { + // ... filtering for safe/necessary variables ... + cmd.env(&key, &value); + } + + cmd // Return the Command object +} + +// ... rest of the file ... +``` + +`find_claude_binary` is crucial to ensure `claudia` can actually find the executable regardless of how it was installed. `create_command_with_env` is a helper to build the base command object and ensure it inherits essential environment variables, which is often necessary for `claude` to run correctly, especially on macOS GUI launches where the default PATH is minimal. + +Next, the core logic for spawning the process and handling its output streams. This is extracted into a helper function `spawn_claude_process` used by `execute_claude_code`, `continue_claude_code`, and `resume_claude_code`. A similar pattern exists within `execute_agent`. + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + log::info!("Spawning Claude process..."); + + // Configure stdout and stderr to be piped + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + // Spawn the process asynchronously + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + log::info!("Claude process spawned successfully with PID: {:?}", child.id()); + + // Take the piped stdout and stderr handles + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stderr = child.stderr.take().ok_or("Failed to get stderr")?; + + // Create asynchronous buffered readers for the streams + let stdout_reader = BufReader::new(stdout); + let stderr_reader = BufReader::new(stderr); + + // Spawn a separate task to read and process stdout lines + let app_handle_stdout = app.clone(); + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::debug!("Claude stdout: {}", line); + // Emit the line as an event to the frontend + // Frontend listens for "claude-output" + let _ = app_handle_stdout.emit("claude-output", &line); + } + log::info!("Finished reading Claude stdout."); + }); + + // Spawn a separate task to read and process stderr lines + let app_handle_stderr = app.clone(); + tokio::spawn(async move { + let mut lines = stderr_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::error!("Claude stderr: {}", line); + // Emit error lines as an event to the frontend + // Frontend listens for "claude-error" + let _ = app_handle_stderr.emit("claude-error", &line); + } + log::info!("Finished reading Claude stderr."); + }); + + // Spawn a task to wait for the process to finish + let app_handle_complete = app.clone(); + tokio::spawn(async move { + match child.wait().await { // Wait for the process to exit + Ok(status) => { + log::info!("Claude process exited with status: {}", status); + // Emit a completion event to the frontend + // Frontend listens for "claude-complete" + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", status.success()); + } + Err(e) => { + log::error!("Failed to wait for Claude process: {}", e); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Small delay + let _ = app_handle_complete.emit("claude-complete", false); // Indicate failure + } + } + }); + + Ok(()) +} + +// ... rest of the file with commands like execute_claude_code calling spawn_claude_process ... +``` + +This `spawn_claude_process` function is the heart of the interaction. It sets up the communication channels (`stdout`, `stderr` pipes), starts the `claude` process, and then uses `tokio::spawn` to run multiple things concurrently: reading output, reading errors, and waiting for the process to finish. Each piece of output or status change triggers an `app.emit` call, sending the information via Tauri's event system back to the frontend. + +Finally, handling cancellation for Agent runs involves finding the process ID (PID) and sending a signal. + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +use rusqlite::{params, Connection, Result as SqliteResult}; // For database access + +/// Kill a running agent session +#[tauri::command] +pub async fn kill_agent_session( + db: State<'_, AgentDb>, // Access to the database state + run_id: i64, +) -> Result { + log::info!("Attempting to kill agent session run: {}", run_id); + + let pid_result = { + let conn = db.0.lock().map_err(|e| e.to_string())?; + // Retrieve the PID from the database for the specific run + conn.query_row( + "SELECT pid FROM agent_runs WHERE id = ?1 AND status = 'running'", + params![run_id], + |row| row.get::<_, Option>(0) + ) + .map_err(|e| e.to_string())? + }; + + if let Some(pid) = pid_result { + log::info!("Found PID {} for run {}", pid, run_id); + // Use the standard library to send a kill signal + // Behavior differs slightly on Windows vs Unix-like systems + let kill_result = if cfg!(target_os = "windows") { + std::process::Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) // Force kill by PID + .output() + } else { + std::process::Command::new("kill") + .args(["-TERM", &pid.to_string()]) // Send termination signal + .output() + }; + + match kill_result { + Ok(output) if output.status.success() => { + log::info!("Successfully sent kill signal to process {}", pid); + } + Ok(_) => { + log::warn!("Kill command failed for PID {}", pid); + } + Err(e) => { + log::warn!("Failed to execute kill command for PID {}: {}", pid, e); + } + } + } else { + log::warn!("No running PID found for run {}", run_id); + } + + // Update the database to mark the run as cancelled, regardless of kill success + let conn = db.0.lock().map_err(|e| e.to_string())?; + let updated = conn.execute( + "UPDATE agent_runs SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP WHERE id = ?1 AND status = 'running'", + params![run_id], + ).map_err(|e| e.to_string())?; + + Ok(updated > 0) // Return true if a record was updated +} + +// ... rest of the file ... +``` + +This `kill_agent_session` command looks up the process ID associated with the agent run in the database, then attempts to terminate that process using system commands (`kill` or `taskkill`). Finally, it updates the database record for the run to mark it as "cancelled". + +## Conclusion + +In this chapter, we explored the **Claude CLI Interaction** layer, which is fundamental to how `claudia` functions. We learned that this part of the backend is responsible for finding the `claude` binary, preparing the command with all necessary arguments, spawning the `claude` process, setting its environment (including sandboxing), capturing its output and errors in real-time, and managing its lifecycle until completion or cancellation. + +We saw how frontend calls to Tauri Commands trigger this process, how the backend uses Rust's `Command` features and `tokio` for asynchronous stream handling, and how output and status updates are sent back to the frontend via Tauri events, enabling the real-time display of results. This interaction layer effectively turns the `claude` CLI into a powerful engine driven by the user-friendly `claudia` graphical interface. + +Next, we'll take a closer look at a critical aspect touched upon in this chapter: **Sandboxing**. We'll see how `claudia` uses operating system features to limit the permissions of the `claude` process, enhancing security when running code or interacting with your file system. + +[Next Chapter: Sandboxing](06_sandboxing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/claude.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx) diff --git a/Claudia-docs/V_1/claudia_2.md b/Claudia-docs/V_1/claudia_2.md new file mode 100644 index 00000000..a2a56c32 --- /dev/null +++ b/Claudia-docs/V_1/claudia_2.md @@ -0,0 +1,2500 @@ +# Chapter 6: Sandboxing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, and how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md). + +Now, let's talk about a crucial aspect of security: **Sandboxing**. + +## The Problem: Running Untrusted Code + +When you use `claudia` to run an Agent or a direct Claude Code session, you are essentially asking the application to launch the separate `claude` binary on your computer. This `claude` binary can then execute code or perform actions based on the instructions it receives from Claude (and indirectly, from you). + +Imagine you ask Claude to "write a script to delete all files in `/tmp`". While this is a harmless directory, what if you accidentally asked it to delete files in your `/Users/yourname/Documents` folder, or worse, system files? Or what if a malicious instruction somehow slipped into the context? + +Running external processes, especially ones that might execute code or interact with your file system and network, introduces a security risk. By default, any program you run has the same permissions as you do. It could potentially read your sensitive files, delete important data, or connect to unwanted places on the internet. + +This is where **Sandboxing** comes in. + +## What is Sandboxing? + +Sandboxing is like putting a protective barrier around the process that `claudia` launches (the `claude` binary). It creates a restricted environment that limits what that process can see and do on your computer, based on a predefined set of rules. + +Think of it like giving the AI a restricted workspace. You give it access only to the specific tools and areas it needs to do its job for this particular task, and nothing more. + +In `claudia`, sandboxing is primarily used to control the `claude` process's access to: + +1. **File System:** Prevent reading or writing files outside of specific allowed directories (like your project folder). +2. **Network:** Prevent making unwanted connections to the internet or local network. +3. **System Information:** Limit access to potentially sensitive system details. + +By default, `claudia` aims to run Agents and sessions within a sandbox, giving you control over their permissions. + +## Sandboxing with Agents + +The primary way you interact with sandboxing settings in `claudia` is through the **Agent configuration**. As you saw in [Chapter 2: Agents](02_agents_.md), each Agent has specific permission toggles. + +Let's revisit the simplified `AgentSandboxSettings.tsx` component from Chapter 2: + +```typescript +// src/components/AgentSandboxSettings.tsx (Simplified) +// ... imports ... +import { Switch } from "@/components/ui/switch"; +// ... other components ... + +export const AgentSandboxSettings: React.FC = ({ + agent, + onUpdate, + className +}) => { + // ... handleToggle function ... + + return ( + // ... Card and layout ... + {/* Master sandbox toggle */} +
+ + handleToggle('sandbox_enabled', checked)} + /> +
+ + {/* Permission toggles - conditional render */} + {agent.sandbox_enabled && ( +
+ {/* File Read Toggle */} +
+ + handleToggle('enable_file_read', checked)} + /> +
+ {/* File Write Toggle */} +
+ + handleToggle('enable_file_write', checked)} + /> +
+ {/* Network Toggle */} +
+ + handleToggle('enable_network', checked)} + /> +
+
+ )} + {/* ... Warning when sandbox disabled ... */} + // ... end Card ... + ); +}; +``` + +These switches directly control whether the `claude` process launched *by this specific Agent* will be sandboxed and what high-level permissions it will have: + +* **Enable Sandbox:** The main switch. If off, sandboxing is disabled for this Agent, and the process runs with full permissions (like running `claude` directly in your terminal). This should be used with caution. +* **File Read Access:** If enabled, the sandboxed process can read files. Without this, it might not even be able to read the source files in your project directory. +* **File Write Access:** If enabled, the sandboxed process can create or modify files. +* **Network Access:** If enabled, the sandboxed process can make outbound network connections (e.g., accessing APIs, cloning repositories). + +These Agent-specific toggles allow you to quickly define a security posture tailored to the Agent's purpose. A "Code Reader" Agent might only need File Read. A "Code Fixer" might need File Read and Write. A "Web API Helper" might need Network Access. + +## How it Works: Under the Hood + +When you click "Execute" for an Agent or start a session, `claudia`'s backend takes the Agent's sandbox settings (or default settings for direct sessions) and translates them into concrete rules that the operating system can enforce. + +`claudia` uses system-level sandboxing mechanisms through a library called `gaol`. `gaol` provides a way for the parent process (`claudia`'s backend) to define restrictions for a child process (`claude`). + +Here's a simplified look at the steps when `claudia` launches a sandboxed `claude` process: + +1. **Get Agent Permissions:** The backend fetches the selected Agent's configuration from the database, including the `sandbox_enabled`, `enable_file_read`, `enable_file_write`, and `enable_network` fields. +2. **Load Sandbox Profile & Rules:** `claudia` stores more detailed, reusable sandbox configurations called "Profiles" and "Rules" in its database ([Chapter 2: Agents](02_agents_.md)). The Agent might be linked to a specific Profile, or a default Profile is used. The backend loads the rules associated with this Profile. +3. **Combine Agent Permissions and Rules:** The backend logic combines the high-level Agent toggles with the detailed Profile rules. For example, if the Agent has `enable_file_read: false`, any "file read" rules from the loaded Profile are ignored for this run. If `enable_file_read: true`, the specific paths defined in the Profile rules (like "allow reading subpaths of the project directory") are used. The project path itself (from [Chapter 1: Session/Project Management](01_session_project_management_.md)) is crucial here, as file access is often restricted to this directory. +4. **Build `gaol` Profile:** The combined set of effective rules is used to build a `gaol::profile::Profile` object in memory. This object contains the precise operations the child process will be allowed or denied. +5. **Prepare & Spawn Command:** The backend prepares the command to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)). It configures the command to run within the sandbox environment defined by the `gaol` Profile. This might involve setting special environment variables or using `gaol`'s API to spawn the child process with the restrictions already applied by the parent. +6. **OS Enforces Sandbox:** When the `claude` process starts, the operating system, guided by the `gaol` library and the configured profile, actively monitors the process. If the `claude` process attempts an action that is *not* allowed by the sandbox rules (like trying to read a file outside the permitted paths when file read is enabled, or any file if file read is disabled), the operating system blocks the action immediately. +7. **Violation Logging:** If a sandboxed process attempts a forbidden action, `claudia` can detect this violation and log it to its database. This helps you understand if an Agent is trying to do something unexpected. + +Here's a simplified sequence diagram illustrating the sandboxing flow during execution: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Tauri Commands + participant Database as agents.db + participant SandboxLogic as Sandbox Module (Rust) + participant OS as Operating System + participant ClaudeCLI as claude binary + + Frontend->>Backend: Call execute_agent(...) + Backend->>Database: Get Agent Config (incl. permissions) + Database-->>Backend: Agent Config + Backend->>Database: Get Sandbox Profile & Rules + Database-->>Backend: Profile & Rules + Backend->>SandboxLogic: Combine Agent Permissions & Rules + SandboxLogic->>SandboxLogic: Build gaol::Profile + SandboxLogic-->>Backend: gaol::Profile ready + Backend->>OS: Spawn claude process (with gaol::Profile / env) + OS-->>Backend: Process Handle, PID + Note over OS,ClaudeCLI: OS enforces sandbox rules + ClaudeCLI->>OS: Attempt operation (e.g., read file) + alt Operation Allowed + OS-->>ClaudeCLI: Operation succeeds + else Operation Denied (Violation) + OS-->>ClaudeCLI: Operation fails (Permission denied) + Note over OS: Violation detected + OS->>SandboxLogic: Notify of violation (if configured) + SandboxLogic->>Database: Log Violation + end + ClaudeCLI-->>OS: Process exits + OS-->>Backend: Process status + Backend->>Frontend: Notify completion/output +``` + +This diagram shows how the Agent's settings propagate through the backend to influence the creation of the sandbox profile, which is then enforced by the operating system when the `claude` process is launched. + +## Diving into the Backend Code + +Let's look at snippets from the Rust code related to sandboxing, found primarily in the `src-tauri/src/sandbox/` module and `src-tauri/src/commands/sandbox.rs`. + +The `Agent` struct (from `src-tauri/src/commands/agents.rs`) holds the basic toggles: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Agent { + // ... other fields ... + pub sandbox_enabled: bool, + pub enable_file_read: bool, + pub enable_file_write: bool, // Note: This permission is often difficult to enforce precisely via sandboxing alone and might require manual user confirmation or is inherently less secure. + pub enable_network: bool, + // ... other fields ... +} +``` + +The `src-tauri/src/commands/sandbox.rs` file contains Tauri commands for managing sandbox profiles and rules stored in the database, and for viewing violations: + +```rust +// src-tauri/src/commands/sandbox.rs (Simplified) +// ... imports ... + +// Represents a detailed rule in a sandbox profile +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxRule { + pub id: Option, + pub profile_id: i64, // Links to a profile + pub operation_type: String, // e.g., "file_read_all", "network_outbound" + pub pattern_type: String, // e.g., "subpath", "literal" + pub pattern_value: String, // e.g., "{{PROJECT_PATH}}", "/home/user/.config" + pub enabled: bool, + pub platform_support: Option, // e.g., "[\"macos\", \"linux\"]" + pub created_at: String, +} + +// Represents a log entry for a denied operation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SandboxViolation { + pub id: Option, + pub profile_id: Option, // What profile was active? + pub agent_id: Option, // What agent was running? + pub agent_run_id: Option, // What specific run? + pub operation_type: String, // What was attempted? + pub pattern_value: Option, // What path/address was involved? + pub process_name: Option, // Which binary? + pub pid: Option, // Which process ID? + pub denied_at: String, // When did it happen? +} + +// Tauri command to list sandbox profiles +#[tauri::command] +pub async fn list_sandbox_profiles(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to list rules for a profile +#[tauri::command] +pub async fn list_sandbox_rules(/* ... */) -> Result, String> { /* ... */ } + +// Tauri command to view recorded violations +#[tauri::command] +pub async fn list_sandbox_violations(/* ... */) -> Result, String> { /* ... */ } + +// ... other commands for creating/updating/deleting profiles and rules ... +``` + +These commands allow the frontend to manage the detailed sandbox configurations that underpin the Agent's simpler toggles. For example, when you enable "File Read Access" on an Agent, the backend loads rules of `operation_type: "file_read_all"` from the selected profile. + +The logic to combine Agent permissions, Profile rules, and build the `gaol::profile::Profile` happens in the `src-tauri/src/sandbox/profile.rs` and `src-tauri/src/sandbox/executor.rs` modules. + +The `ProfileBuilder` is used to translate `SandboxRule` database entries into `gaol::profile::Operation` objects: + +```rust +// src-tauri/src/sandbox/profile.rs (Simplified) +// ... imports ... +use gaol::profile::{Operation, PathPattern, AddressPattern, Profile}; +// ... SandboxRule struct ... + +pub struct ProfileBuilder { + project_path: PathBuf, // The current project directory + home_dir: PathBuf, // The user's home directory +} + +impl ProfileBuilder { + // ... constructor ... + + /// Build a gaol Profile from database rules, filtered by agent permissions + pub fn build_agent_profile(&self, rules: Vec, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result { + // If sandbox is disabled, return empty profile (no restrictions) + if !sandbox_enabled { + // ... create and return empty profile ... + } + + let mut effective_rules = Vec::new(); + + for rule in rules { + if !rule.enabled { continue; } + + // Filter rules based on Agent permissions: + let include_rule = match rule.operation_type.as_str() { + "file_read_all" | "file_read_metadata" => enable_file_read, + "network_outbound" => enable_network, + "system_info_read" => true, // System info often needed, allow if sandbox is ON + _ => true // Default to include if unknown + }; + + if include_rule { + effective_rules.push(rule); + } + } + + // Always ensure project path access is included if file read is ON + if enable_file_read { + // ... add rule for project path if not already present ... + } + + // Now build the actual gaol Profile from the effective rules + self.build_profile_with_serialization(effective_rules) // This translates rules into gaol::Operation + } + + /// Translates SandboxRules into gaol::Operation and serialized form + fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result> { + match rule.operation_type.as_str() { + "file_read_all" => { + let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::FileReadAll(pattern), SerializedOperation::FileReadAll { path, is_subpath }))) + }, + "network_outbound" => { + let (pattern, serialized) = self.build_address_pattern_with_serialization(&rule.pattern_type, &rule.pattern_value)?; + Ok(Some((Operation::NetworkOutbound(pattern), serialized))) + }, + // ... handle other operation types ... + _ => Ok(None) + } + } + + // ... helper functions to build path/address patterns ... +} +``` + +The `build_agent_profile` function is key. It takes the raw rules from the database and the Agent's simple boolean toggles, then filters the rules. It also ensures essential access (like reading the project directory) is granted if file read is enabled. Finally, it calls `build_profile_with_serialization` to create the actual `gaol::Profile` object and a simplified, serializable representation of the rules (`SerializedProfile`). + +This `SerializedProfile` is then passed to the `SandboxExecutor`: + +```rust +// src-tauri/src/sandbox/executor.rs (Simplified) +// ... imports ... +use gaol::sandbox::Sandbox; +use tokio::process::Command; +use std::path::Path; + +pub struct SandboxExecutor { + profile: gaol::profile::Profile, // The gaol profile object + project_path: PathBuf, + serialized_profile: Option, // Serialized rules for child process +} + +impl SandboxExecutor { + // ... constructor ... + + /// Prepare a tokio Command for sandboxed execution + /// The sandbox will be activated in the child process by reading environment variables + pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command { + let mut cmd = Command::new(command); + cmd.args(args).current_dir(cwd); + + // ... inherit environment variables like PATH, HOME ... + + // Serialize the sandbox rules and set environment variables + if let Some(ref serialized) = self.serialized_profile { + let rules_json = serde_json::to_string(serialized).expect("Failed to serialize rules"); + // NOTE: These environment variables are currently commented out in the actual code + // for debugging and compatibility reasons. + // In a fully enabled child-side sandboxing model, these would be set: + // cmd.env("GAOL_SANDBOX_ACTIVE", "1"); + // cmd.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref()); + // cmd.env("GAOL_SANDBOX_RULES", &rules_json); + log::warn!("🚨 Sandboxing environment variables for child process are currently disabled!"); + } else { + log::warn!("🚨 No serialized profile - running without sandbox environment!"); + } + + cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()) + } + + // ... Other execution methods ... +} +``` + +The `prepare_sandboxed_command` function takes the `gaol::profile::Profile` and the `SerializedProfile`. Although the environment variable mechanism shown above is temporarily disabled in the provided code snippets, the *intention* is for the parent process (`claudia`'s backend) to set up the environment for the child process (`claude`). The child process, if it supports this model (like `gaol`'s `ChildSandbox::activate()`), would read these environment variables upon startup and activate the sandbox *within itself* before executing the main task. + +Alternatively, `gaol` also supports launching the child process directly from the sandboxed parent using `Sandbox::start()`. The provided code attempts this first but falls back due to current `gaol` library limitations regarding getting the child process handle back. + +The `src-tauri/src/sandbox/platform.rs` file defines what kind of sandboxing capabilities are available and supported on the current operating system (Linux, macOS, FreeBSD have some support). + +```rust +// src-tauri/src/sandbox/platform.rs (Simplified) +// ... imports ... + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlatformCapabilities { + pub os: String, + pub sandboxing_supported: bool, // Is sandboxing generally possible? + // ... details about specific operation support ... +} + +pub fn get_platform_capabilities() -> PlatformCapabilities { /* ... detects OS and returns capabilities ... */ } +pub fn is_sandboxing_available() -> bool { /* ... checks if OS is supported ... */ } +``` + +This is used by the UI (via the `get_platform_capabilities` command) to inform the user if sandboxing is fully supported or if there are limitations on their platform. + +In summary, sandboxing in `claudia` works by: +1. Allowing users to set high-level permissions (read/write/network) on Agents via the UI. +2. Storing detailed, reusable sandbox Profiles and Rules in the backend database. +3. Combining Agent permissions with Profile rules in the backend to create a specific set of restrictions for a given process run. +4. Using system-level sandboxing features (via the `gaol` library and potentially environment variables) to apply these restrictions when launching the `claude` process. +5. Logging any attempts by the sandboxed process to violate these rules. + +This multi-layered approach provides both ease of use (Agent toggles) and flexibility (detailed rules in Profiles), significantly improving security when running AI-generated instructions or code. + +## Conclusion + +In this chapter, we explored **Sandboxing**, `claudia`'s security system. We learned why running external processes requires security measures and how sandboxing provides a protective barrier to limit what the `claude` process can access or do. + +We saw how you control sandboxing primarily through Agent permissions in the UI, enabling or disabling file read, file write, and network access. We then dived into the backend to understand how these simple toggles are combined with detailed Sandbox Profile rules to build a concrete `gaol::profile::Profile`. This profile is then used to launch the `claude` binary within a restricted environment enforced by the operating system, with potential violations being logged. + +Understanding sandboxing is key to securely leveraging the power of Claude Code, especially when it interacts with your local file system. + +In the next chapter, we'll learn how `claudia` handles the continuous stream of output from the `claude` binary to update the UI in real-time: [Streamed Output Processing](07_streamed_output_processing_.md). + +[Next Chapter: Streamed Output Processing](07_streamed_output_processing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/sandbox.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/executor.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/mod.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/platform.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/sandbox/profile.rs), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentSandboxSettings.tsx) + +# Chapter 7: Streamed Output Processing + +Welcome back to the `claudia` tutorial! In our previous chapters, we've learned about organizing your work with [Session/Project Management](01_session_project_management_.md), defining specialized assistants with [Agents](02_agents_.md), how the [Frontend UI Components](03_frontend_ui_components_.md) create the user interface, how [Tauri Commands](04_tauri_commands_.md) connect the frontend and backend, how `claudia` interacts with the `claude` command-line tool in [Claude CLI Interaction](05_claude_cli_interaction_.md), and how [Sandboxing](06_sandboxing_.md) keeps things secure. + +Now, let's look at how `claudia` handles the constant flow of information coming *from* the `claude` binary while it's running. This is the concept of **Streamed Output Processing**. + +## The Problem: Real-time Updates + +Imagine you ask Claude Code to perform a complex task, like analyzing your codebase or generating a long piece of documentation. This process can take time. The `claude` command-line tool doesn't just wait until it's completely finished and then dump all the results at once. Instead, it often sends its output piece by piece: a thought process here, a tool call there, a chunk of generated text, and finally, a result message. + +As a user of `claudia`'s graphical interface, you don't want to stare at a frozen screen waiting for everything to finish. You want to see what Claude is doing *right now*, as it's happening. You want a live view of its progress. + +This is the problem that Streamed Output Processing solves. `claudia` needs to capture this real-time, piece-by-piece output from the `claude` process and display it to you instantly. + +Think of it like watching a live news feed or a chat application. Messages appear as they are sent, not all bundled up and delivered at the very end. + +## What is Streamed Output Processing? + +Streamed Output Processing in `claudia` refers to the entire system that: + +1. **Captures** the output from the running `claude` process *as it is generated*. +2. **Receives** this output in the backend, often as a stream of data. +3. **Parses** this data (which is typically in a specific format called JSONL) line by line. +4. **Transforms** each parsed piece into a structured message that the frontend understands. +5. **Sends** these structured messages from the backend to the frontend immediately. +6. **Displays** these messages in the user interface as they arrive, providing a live, dynamic view. + +The core idea is that the output is treated as a *stream* – a continuous flow of data arriving over time – rather than a single large block of data at the end. + +## How it Looks in the UI + +When you execute an Agent or run an interactive session in `claudia`, the main part of the screen fills up with messages as they come in. + +You'll see different types of messages appear: + +* Initial system messages (showing session info, tools available). +* Assistant messages (Claude's thoughts, text, tool calls). +* User messages (your prompts, tool results sent back to Claude). +* Result messages (indicating the overall success or failure of a step). + +Each of these appears in the UI as soon as `claudia` receives the corresponding piece of output from the `claude` process. + +In the frontend code (like `src/components/AgentExecution.tsx` or `src/components/ClaudeCodeSession.tsx`), there's a state variable, typically an array, that holds all the messages displayed. When a new piece of output arrives, this array is updated, and React automatically re-renders the list to include the new message. + +For example, in `AgentExecution.tsx`, you'll find code like this managing the displayed messages: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... + +interface AgentExecutionProps { + // ... props ... +} + +export interface ClaudeStreamMessage { + type: "system" | "assistant" | "user" | "result"; + // ... other fields based on the JSONL structure ... +} + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + // State to hold the list of messages displayed in the UI + const [messages, setMessages] = useState([]); + // ... other state variables ... + + // ... handleExecute function ... + + // When a new message arrives (handled by an event listener, shown below): + const handleNewMessage = (newMessage: ClaudeStreamMessage) => { + setMessages(prev => [...prev, newMessage]); // Add the new message to the array + }; + + // ... render function ... + // The rendering logic maps over the `messages` array to display each one + // using the StreamMessage component + /* + return ( + // ... layout ... +
+ {messages.map((message, index) => ( + // Render each message + ))} +
+ // ... rest of component ... + ); + */ +}; +// ... rest of file ... +``` + +This state update (`setMessages`) is the frontend's way of saying, "Hey React, something new arrived, please update the list!" + +## How it Works: The Data Flow + +The communication happens in several steps, involving the `claude` binary, the operating system's pipes, the `claudia` backend (Rust), the Tauri framework, and the `claudia` frontend (TypeScript/React). + +1. **`claude` writes output:** The `claude` process executes your request. When it has a piece of output to share (like a tool call or a chunk of text), it writes it to its standard output (stdout). +2. **OS captures output:** Because `claudia`'s backend spawned `claude` with piped stdout ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), the operating system redirects `claude`'s stdout into a temporary buffer or pipe that the `claudia` backend can read from. +3. **Backend reads line by line:** The `claudia` backend continuously reads from this pipe. It's specifically looking for newline characters to know when a complete line (a complete JSONL entry) has arrived. +4. **Backend emits event:** As soon as the backend reads a complete line, it takes the raw string data and emits it as a Tauri event. These events have a specific name (like `"agent-output"` or `"claude-output"`) that the frontend is listening for. +5. **Tauri delivers event:** The Tauri framework acts as the messenger, efficiently delivering the event and its data payload from the backend Rust process to the frontend JavaScript process. +6. **Frontend receives event:** The frontend has registered event listeners using Tauri's event API. When an event with the matching name arrives, the registered callback function is executed. +7. **Frontend processes and updates:** The callback function receives the raw output line. It parses the JSONL string into a JavaScript object and updates the component's state (`messages` array). +8. **UI re-renders:** React detects the state change and updates only the necessary parts of the UI to display the new message. + +Here is a simplified sequence diagram for this process: + +```mermaid +sequenceDiagram + participant ClaudeCLI as claude binary + participant OS as OS Pipe + participant Backend as Backend Commands (Rust) + participant Tauri as Tauri Core + participant Frontend as Frontend UI (TS/React) + + ClaudeCLI->>OS: Write line (JSONL) to stdout + OS-->>Backend: Data available in pipe + Backend->>Backend: Read line from pipe + Backend->>Tauri: Emit event "claude-output" with line data + Tauri->>Frontend: Deliver event + Frontend->>Frontend: Receive event in listener + Frontend->>Frontend: Parse JSONL line to message object + Frontend->>Frontend: Update state (add message to list) + Frontend->>Frontend: UI re-renders + Frontend->>User: Display new message in UI +``` + +This flow repeats every time `claude` outputs a new line, providing the smooth, real-time updates you see in the `claudia` interface. + +## Diving into the Code + +Let's look at the relevant code snippets from both the backend (Rust) and the frontend (TypeScript). + +### Backend: Reading and Emitting + +As seen in [Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md), the backend uses `tokio` to handle the asynchronous reading of the process's standard output. It spawns a task that reads line by line and emits events. + +Here's a simplified look at the part of `src-tauri/src/commands/claude.rs` (or similar module) that does this: + +```rust +// src-tauri/src/commands/claude.rs (Simplified) +// ... imports ... +use tokio::io::{AsyncBufReadExt, BufReader}; +use tauri::{AppHandle, Manager}; +use tokio::process::Command; // Assuming command is already built + +/// Helper function to spawn Claude process and handle streaming +async fn spawn_claude_process(app: AppHandle, mut cmd: Command) -> Result<(), String> { + // ... Configure stdout/stderr pipes ... + cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn Claude: {}", e))?; + + let stdout = child.stdout.take().ok_or("Failed to get stdout")?; + let stdout_reader = BufReader::new(stdout); + + // Spawn a task to read stdout line by line and emit events + let app_handle_stdout = app.clone(); // Clone handle for the async task + tokio::spawn(async move { + let mut lines = stdout_reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + // Log or process the raw line + log::debug!("Claude stdout line: {}", line); + // Emit the line as an event to the frontend + let _ = app_handle_stdout.emit("claude-output", &line); // <-- Emitting the event! + } + log::info!("Finished reading Claude stdout."); + }); + + // ... Similar task for stderr ... + // ... Task to wait for process exit and emit completion event ... + + Ok(()) +} + +// Example Tauri command calling the helper +/* +#[tauri::command] +pub async fn execute_claude_code(app: AppHandle, project_path: String, prompt: String, model: String) -> Result<(), String> { + // ... build the Command object 'cmd' ... + spawn_claude_process(app, cmd).await // Calls the streaming helper +} +*/ +``` + +The crucial part here is the `tokio::spawn` block that reads lines (`lines.next_line().await`) and, for each line, calls `app_handle_stdout.emit("claude-output", &line)`. This sends the raw JSONL line string to the frontend via the Tauri event system. The `"claude-output"` string is the event name. + +### Frontend: Listening and Processing + +In the frontend (TypeScript), the component that displays the output (like `AgentExecution.tsx` or `ClaudeCodeSession.tsx`) needs to set up listeners for these events when it loads and clean them up when it unmounts. + +Here's a simplified look at the event listener setup in `AgentExecution.tsx`: + +```typescript +// src/components/AgentExecution.tsx (Simplified) +// ... imports ... +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +// ... ClaudeStreamMessage type ... + +export const AgentExecution: React.FC = ({ + agent, + onBack, + className, +}) => { + const [messages, setMessages] = useState([]); + const [rawJsonlOutput, setRawJsonlOutput] = useState([]); // Keep raw output too + // ... other state variables ... + + const unlistenRefs = useRef([]); // Ref to store unlisten functions + + useEffect(() => { + // Set up event listeners when the component mounts or execution starts + let outputUnlisten: UnlistenFn | undefined; + let errorUnlisten: UnlistenFn | undefined; + let completeUnlisten: UnlistenFn | undefined; + + const setupListeners = async () => { + try { + // Listen for lines from stdout + outputUnlisten = await listen("agent-output", (event) => { // <-- Listening for the event! + try { + // The event payload is the raw JSONL line string + const rawLine = event.payload; + setRawJsonlOutput(prev => [...prev, rawLine]); // Store raw line + + // Parse the JSONL string into a JavaScript object + const message = JSON.parse(rawLine) as ClaudeStreamMessage; + + // Update the messages state, triggering a UI re-render + setMessages(prev => [...prev, message]); // <-- Updating state! + + } catch (err) { + console.error("Failed to process Claude output line:", err, event.payload); + // Handle parsing errors if necessary + } + }); + + // Listen for stderr lines (errors) + errorUnlisten = await listen("agent-error", (event) => { + console.error("Claude stderr:", event.payload); + // You might want to display these errors in the UI too + }); + + // Listen for the process completion event + completeUnlisten = await listen("agent-complete", (event) => { + console.log("Claude process complete:", event.payload); + // Update UI state (e.g., hide loading indicator) + // ... update isRunning state ... + }); + + // Store unlisten functions so we can clean them up later + unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten]; + + } catch (err) { + console.error("Failed to set up event listeners:", err); + // Handle listener setup errors + } + }; + + setupListeners(); + + // Clean up listeners when the component unmounts + return () => { + unlistenRefs.current.forEach(unlisten => unlisten()); + }; + }, []); // Empty dependency array means setup runs once on mount + + // ... render function ... +}; +// ... rest of file ... +``` + +This `useEffect` hook sets up the listener using `listen("agent-output", ...)`. The callback function receives the event, accesses the raw JSONL string via `event.payload`, parses it with `JSON.parse`, and then updates the `messages` state using `setMessages`. This sequence is the core of the streamed output processing on the frontend. The `useRef` and the cleanup function in the `useEffect` are standard React patterns for managing resources (like event listeners) that persist across renders but need to be cleaned up when the component is no longer needed. + +The parsed `message` object is then passed down to the `StreamMessage` component (referenced in the provided code snippet for `src/components/StreamMessage.tsx`) which knows how to interpret the different `type` and `subtype` fields (like "assistant", "tool_use", "tool_result", "result") and render them with appropriate icons, formatting, and potentially syntax highlighting (using libraries like `react-markdown` and `react-syntax-highlighter`) or custom widgets ([ToolWidgets.tsx]). + +## Conclusion + +In this chapter, we explored **Streamed Output Processing**, understanding how `claudia` handles the real-time flow of information from the running `claude` command-line tool. We learned that `claude` sends output piece by piece in JSONL format, and that `claudia`'s backend captures this stream, reads it line by line, and immediately emits each line as a Tauri event to the frontend. + +On the frontend, we saw how components use `listen` to subscribe to these events, parse the JSONL payload into structured message objects, and update their state to display the new information dynamically. This entire process ensures that the `claudia` UI provides a responsive, live view of the AI's progress and actions during interactive sessions and Agent runs. + +Understanding streamed output is key to seeing how `claudia` provides its core real-time chat and execution experience on top of a command-line binary. + +In the next chapter, we'll look at how `claudia` keeps track of multiple potentially running processes, like Agent runs or direct sessions: [Process Registry](08_process_registry_.md). + +[Next Chapter: Process Registry](08_process_registry_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/AgentExecution.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ClaudeCodeSession.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/StreamMessage.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/ToolWidgets.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/types/enhanced-messages.ts) + +# Chapter 8: Process Registry + +Welcome back to the `claudia` tutorial! In our last chapter, [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md), we learned how `claudia` captures and displays the output from the `claude` command-line tool in real-time as it's running. + +Now, let's talk about something that happens just *before* that output starts streaming: launching the `claude` tool itself. When you click "Execute" for an Agent or start a new session, `claudia` doesn't just run the command and wait; it starts the `claude` binary as a separate **process** that runs in the background. + +What if you run multiple agents? What if you start a session and then switch to look at something else while it's running? How does `claudia` keep track of all these separate `claude` processes? How does it know which process is which? And how can it show you their status or let you stop them if needed? + +This is where the **Process Registry** comes in. + +## What is the Process Registry? + +Think of the Process Registry as `claudia`'s internal "Task Manager" specifically for the `claude` processes it starts. It's a system within the `claudia` backend (the Rust code) that keeps a list of all the `claude` processes that are currently running. + +For each running process, the registry stores important information, such as: + +* A unique identifier for this specific "run" (like the `run_id` we saw for Agent Runs in [Chapter 2: Agents](02_agents_.md)). +* The **Process ID (PID)** assigned by the operating system. This is like the process's unique phone number that the operating system uses to identify it. +* The current **status** (like "running", "completed", "failed", "cancelled"). +* Information about *what* is being run (like which Agent, the task description, the project path). +* A reference to the process itself, allowing `claudia` to interact with it (like sending a signal to stop it). +* A temporary buffer to hold the most recent output, allowing quick access to live status without reading the entire JSONL file every time. + +The Process Registry allows `claudia` to monitor these background processes, provide access to their live output streams (as discussed in [Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)), and offer controls like stopping a running task. + +## The Use Case: Managing Running Sessions + +The most visible use case for the Process Registry in `claudia` is the "Running Sessions" screen. This screen lists all the Agent runs or interactive sessions that `claudia` has started and are still considered "active" (running or perhaps recently finished but not yet fully cleaned up). + +Here's a simplified look at the frontend component responsible for this, `RunningSessionsView.tsx`: + +```typescript +// src/components/RunningSessionsView.tsx (Simplified) +import { useState, useEffect } from 'react'; +// ... other imports ... +import { api } from '@/lib/api'; // Import API helper +import type { AgentRun } from '@/lib/api'; // Import data type + +export function RunningSessionsView({ /* ... props ... */ }) { + const [runningSessions, setRunningSessions] = useState([]); // State to hold list + const [loading, setLoading] = useState(true); + // ... other state ... + + // Function to fetch the list of running sessions + const loadRunningSessions = async () => { + try { + // Call the backend command to get running sessions + const sessions = await api.listRunningAgentSessions(); + setRunningSessions(sessions); // Update state with the list + } catch (error) { + console.error('Failed to load running sessions:', error); + // ... handle error ... + } finally { + setLoading(false); + } + }; + + // Function to stop a session + const killSession = async (runId: number, agentName: string) => { + try { + // Call the backend command to kill a session + const success = await api.killAgentSession(runId); + if (success) { + console.log(`${agentName} session stopped.`); + // Refresh the list after killing + await loadRunningSessions(); + } else { + console.warn('Session may have already finished'); + } + } catch (error) { + console.error('Failed to kill session:', error); + // ... handle error ... + } + }; + + useEffect(() => { + loadRunningSessions(); // Load sessions when component mounts + + // Set up auto-refresh + const interval = setInterval(() => { + loadRunningSessions(); + }, 5000); // Refresh every 5 seconds + + return () => clearInterval(interval); // Clean up interval + }, []); + + if (loading) { + return

Loading running sessions...

; // Loading indicator + } + + return ( +
+

Running Agent Sessions

+ {runningSessions.length === 0 ? ( +

No agent sessions are currently running

+ ) : ( +
+ {/* Map over the runningSessions list to display each one */} + {runningSessions.map((session) => ( +
{/* Card or similar display */} +

{session.agent_name}

+

Status: {session.status}

+

PID: {session.pid}

+ {/* ... other details like task, project path, duration ... */} + + {/* Buttons to interact with the session */} + {/* Set state to open viewer */} + +
+ ))} +
+ )} + + {/* Session Output Viewer component (shown when selectedSession is not null) */} + {selectedSession && ( + setSelectedSession(null)} + /> + )} +
+ ); +} +``` + +This component demonstrates how the frontend relies on the backend's Process Registry: +1. It calls `api.listRunningAgentSessions()` to get the current list. +2. It displays information for each running process, including the PID and status. +3. It provides "Stop" buttons that call `api.killAgentSession(runId)`, requesting the backend to terminate the corresponding process. +4. It provides a "View Output" button that, when clicked, might fetch the live output buffer from the registry (using a command like `api.getLiveSessionOutput(runId)`) before potentially switching to file-based streaming ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)). +5. It automatically refreshes this list periodically by calling `loadRunningSessions` again. + +## How it Works: Under the Hood + +The Process Registry is implemented in the Rust backend, primarily in the `src-tauri/src/process/registry.rs` file. + +Here's a simplified look at what happens step-by-step: + +1. **Process Spawned:** When a backend command like `execute_agent` or `execute_claude_code` needs to launch the `claude` binary ([Chapter 5: Claude CLI Interaction](05_claude_cli_interaction_.md)), it prepares the command and then calls `child.spawn()`. +2. **Registration:** Immediately after `child.spawn()` successfully starts the process, the backend extracts the **PID** from the returned `Child` object. It then takes the `run_id` (generated when the Agent run record was created in the database), the PID, and other relevant info (Agent name, task, project path) and calls a method on the `ProcessRegistry` instance, typically `registry.register_process(...)`. +3. **Registry Storage:** The `ProcessRegistry` stores this information in an in-memory data structure, like a `HashMap`, where the key is the `run_id` and the value is an object containing the `ProcessInfo` and the actual `Child` handle. It also initializes a buffer for live output for this specific run. +4. **Output Appending:** As the streaming output processing ([Chapter 7: Streamed Output Processing](07_streamed_output_processing_.md)) reads lines from the process's stdout/stderr pipes, it also appends these lines to the live output buffer associated with this run_id in the Process Registry using `registry.append_live_output(run_id, line)`. +5. **Listing Processes:** When the frontend calls `list_running_agent_sessions` (which maps to a backend command like `list_running_sessions`), the backend accesses the `ProcessRegistry` and asks it for the list of currently registered processes (`registry.get_running_processes()`). The registry returns the stored `ProcessInfo` for each active entry in its map. +6. **Viewing Live Output:** When the frontend calls `get_live_session_output(runId)`, the backend asks the registry for the live output buffer associated with that `runId` (`registry.get_live_output(runId)`), and returns it to the frontend. +7. **Killing Process:** When the frontend calls `kill_agent_session(runId)`, the backend first tells the `ProcessRegistry` to attempt to terminate the process (`registry.kill_process(runId)`). The registry uses the stored `Child` handle or PID to send a termination signal to the operating system. After attempting the kill, the backend also updates the database record for that run to mark its status as 'cancelled'. +8. **Cleanup:** Periodically, `claudia` runs a cleanup task (`cleanup_finished_processes`) that checks the status of processes currently in the registry. If a process has exited (e.g., finished naturally or was killed), the registry removes its entry (`registry.unregister_process(runId)`). This also helps keep the database status accurate. + +Here's a simple sequence diagram showing the core interactions: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI + participant Backend as Backend Commands + participant Registry as Process Registry + participant OS as Operating System + + User->>Frontend: Open Running Sessions View + Frontend->>Backend: Call list_running_sessions() + Backend->>Registry: get_running_processes() + Registry-->>Backend: Return List + Backend-->>Frontend: Return List (mapped from ProcessInfo) + Frontend->>User: Display List + + User->>Frontend: Click Stop Button (for runId) + Frontend->>Backend: Call kill_agent_session(runId) + Backend->>Registry: kill_process(runId) + Registry->>OS: Send terminate signal (using PID/Handle) + OS-->>Registry: Confirmation/Status + Registry-->>Backend: Return success/failure + Backend->>Backend: Update AgentRun status in DB + Backend-->>Frontend: Return confirmation + Frontend->>Frontend: Refresh list / Update UI +``` + +This diagram illustrates how the frontend relies on backend commands to query and manage the processes tracked by the Process Registry. + +## Diving into the Backend Code + +The core implementation of the Process Registry is found in `src-tauri/src/process/registry.rs`. + +First, let's look at the `ProcessInfo` struct, which holds the basic details about a running process: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Information about a running agent process +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessInfo { + pub run_id: i64, // Matches the agent_runs database ID + pub agent_id: i64, // Which agent started this run + pub agent_name: String, // Agent's name + pub pid: u32, // Operating System Process ID + pub started_at: DateTime, // When it started + pub project_path: String, // Where it's running + pub task: String, // The task given + pub model: String, // The model used +} +``` + +The `ProcessRegistry` struct itself is simple; it just holds the map and uses `Arc>` for thread-safe access because multiple parts of the backend might need to interact with it concurrently. + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tokio::process::Child; // Need the process handle itself + +/// Information about a running process with handle +pub struct ProcessHandle { + pub info: ProcessInfo, + pub child: Arc>>, // The handle to the child process + pub live_output: Arc>, // Buffer for live output +} + +/// Registry for tracking active agent processes +pub struct ProcessRegistry { + // Map from run_id to the ProcessHandle + processes: Arc>>, +} + +impl ProcessRegistry { + pub fn new() -> Self { + Self { + processes: Arc::new(Mutex::new(HashMap::new())), + } + } + + // ... methods like register_process, unregister_process, get_running_processes, kill_process, append_live_output, get_live_output ... +} + +// Tauri State wrapper for the registry +pub struct ProcessRegistryState(pub Arc); +// ... Default impl ... +``` + +When a process is spawned, the `execute_agent` command (in `src-tauri/src/commands/agents.rs`) calls `registry.register_process`: + +```rust +// src-tauri/src/commands/agents.rs (Simplified) +// ... imports ... +// Assuming 'registry' is the State +// Assuming 'child' is the tokio::process::Child from cmd.spawn()... +// Assuming 'run_id', 'agent_id', etc., are defined... + +// Register the process in the registry +registry.0.register_process( + run_id, + agent_id, + agent.name.clone(), // Agent name + pid, // Process ID + project_path.clone(), + task.clone(), + execution_model.clone(), + child, // Pass the child handle +).map_err(|e| format!("Failed to register process: {}", e))?; + +info!("📋 Registered process in registry"); + +// ... rest of the async task waiting for process to finish ... +``` + +The `register_process` method in the `ProcessRegistry` then locks the internal map and inserts the new entry: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Register a new running process +pub fn register_process( + &self, + run_id: i64, + agent_id: i64, + agent_name: String, + pid: u32, + project_path: String, + task: String, + model: String, + child: Child, // Receives the child handle +) -> Result<(), String> { + let mut processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + let process_info = ProcessInfo { + run_id, agent_id, agent_name, pid, + started_at: Utc::now(), + project_path, task, model, + }; + + let process_handle = ProcessHandle { + info: process_info, + child: Arc::new(Mutex::new(Some(child))), // Store the handle + live_output: Arc::new(Mutex::new(String::new())), // Init output buffer + }; + + processes.insert(run_id, process_handle); // Insert into the map + Ok(()) +} +``` + +Listing running processes involves locking the map and collecting the `ProcessInfo` from each `ProcessHandle`: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... + +/// Get all running processes +pub fn get_running_processes(&self) -> Result, String> { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + // Iterate through the map's values (ProcessHandle), clone the info field, collect into a Vec + Ok(processes.values().map(|handle| handle.info.clone()).collect()) +} +``` + +Killing a process involves looking up the `ProcessHandle` by `run_id`, accessing the stored `Child` handle, and calling its `kill` method: + +```rust +// src-tauri/src/process/registry.rs (Simplified) +// ... in impl ProcessRegistry ... +use tokio::process::Child; + +/// Kill a running process +pub async fn kill_process(&self, run_id: i64) -> Result { + let processes = self.processes.lock().map_err(|e| e.to_string())?; // Lock the map + + if let Some(handle) = processes.get(&run_id) { + let child_arc = handle.child.clone(); + drop(processes); // IMPORTANT: Release the lock before calling async kill() + + let mut child_guard = child_arc.lock().map_err(|e| e.to_string())?; // Lock the child handle + if let Some(ref mut child) = child_guard.as_mut() { + match child.kill().await { // Call the async kill method + Ok(_) => { + *child_guard = None; // Clear the handle after killing + Ok(true) + } + Err(e) => Err(format!("Failed to kill process: {}", e)), + } + } else { + Ok(false) // Process was already killed or completed + } + } else { + Ok(false) // Process not found in registry + } +} +``` + +Note that the `kill_agent_session` Tauri command ([src-tauri/src/commands/agents.rs]) first calls `registry.kill_process` to try terminating the *actual* OS process via the `Child` handle, and *then* updates the database status. This ensures the UI accurately reflects the state even if the process doesn't immediately exit after the signal. + +The `cleanup_finished_processes` command (also in `src-tauri/src/commands/agents.rs`) periodically checks all processes currently in the registry using `registry.is_process_running()` and, if they are no longer running, updates their status in the database and removes them from the registry. + +This Process Registry provides the backend's central point for managing and interacting with all the separate `claude` instances that `claudia` is running, enabling features like the "Running Sessions" view and the ability to stop tasks. + +## Conclusion + +In this chapter, we introduced the **Process Registry**, `claudia`'s internal system for tracking the `claude` command-line tool processes it launches in the background. We learned that it stores essential information like PID, status, and associated run details, allowing `claudia` to monitor and control these separate tasks. + +We saw how the Process Registry is used to power features like the "Running Sessions" view in the UI, enabling users to see what's currently executing, view live output, and stop processes. We also delved into the backend implementation, seeing how processes are registered upon spawning, how the registry stores their handles, and how backend commands interact with the registry to list, kill, and manage these running tasks. + +Understanding the Process Registry is key to seeing how `claudia` manages concurrency and provides visibility and control over the AI tasks running on your system. + +In the next chapter, we'll explore **Checkpointing**, a feature that allows Claude Code to save and restore its state, enabling longer, more complex interactions across multiple runs. + +[Next Chapter: Checkpointing](09_checkpointing_.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/agents.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/process/registry.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/RunningSessionsView.tsx) + +# Chapter 9: Checkpointing + +Welcome back to the `claudia` tutorial! In our previous chapter, [Chapter 8: Process Registry](08_process_registry_.md), we learned how `claudia` keeps track of and manages the individual `claude` processes it launches. This allows the application to handle multiple running tasks simultaneously and provides a view of what's currently active. + +Now, let's talk about preserving the state of those tasks over time, even after they finish or the application closes. This is the powerful concept of **Checkpointing**. + +## The Problem: Sessions Are Temporary + +Imagine you're working with Claude Code on a complex feature development within a project. You have a long conversation, make several changes to files, get some code snippets, debug an issue, and maybe even use tools to run tests. This interaction might span hours or even days across multiple `claude` runs. + +Each run of `claude` is a session ([Chapter 1: Session/Project Management](01_session_project_management_.md)), and the CLI automatically saves the message history for that session. But what about the state of your project files? What if you want to go back to how the files looked *before* Claude made a specific set of changes? What if you want to experiment with a different approach, but keep the option to return to the current state? + +The basic session history saves the *conversation*, but it doesn't version control your *project files*. This is where checkpoints become essential. + +Think of it like writing a book. The message history is like your rough draft – a linear flow of words. But sometimes you want to save a specific version (e.g., "finished Chapter 5"), experiment with rewriting a scene, and maybe decide later to revert to that saved version or start a new version branched from it. Checkpointing provides this capability for your AI-assisted coding sessions. + +## What is Checkpointing? + +Checkpointing in `claudia` is a system for creating save points of your entire working state for a specific Claude Code session. A checkpoint captures two main things at a particular moment: + +1. **The complete message history** up to that point in the session. +2. **Snapshots of your project files** that have changed since the last checkpoint (or are being tracked). + +When you create a checkpoint, `claudia` records the session's conversation history and saves copies of the relevant files in a special location. This lets you revisit that exact moment later. + +**In simpler terms:** + +* A Checkpoint is a snapshot of your conversation *and* your project files at a specific point in time. +* You can create checkpoints manually whenever you want to save a significant state (like "After implementing Login feature"). +* `claudia` can also create checkpoints automatically based on certain events (like after a tool makes changes to files). +* Checkpoints are organized in a **Timeline**, showing the history of your session like a branching tree (similar to how git commits work). +* You can **Restore** a checkpoint to revert your message history and project files to that saved state. +* You can **Fork** from a checkpoint to start a new conversation branch from a previous state. +* You can **Diff** between checkpoints to see exactly which files were changed and what the changes were. + +## Key Concepts in Checkpointing + +Let's break down the core ideas behind `claudia`'s checkpointing system: + +| Concept | Description | Analogy | +| :----------------- | :--------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | +| **Checkpoint** | A specific save point containing session messages and file snapshots. | Saving your game progress. | +| **Timeline** | The chronological history of checkpoints for a session, shown as a tree structure reflecting branching (forks). | A Git history tree or a family tree. | +| **File Snapshot** | A saved copy of a project file's content and metadata at a specific checkpoint. Only saves changes efficiently. | Saving individual changed files in a commit. | +| **Restoring** | Reverting the current session messages and project files to the state captured in a chosen checkpoint. | Loading a previous save game. | +| **Forking** | Creating a new session branch starting from a specific checkpoint. | Branching in Git or creating an alternate story. | +| **Automatic Checkpoints** | Checkpoints created by `claudia` based on predefined rules (e.g., after certain actions). | Auto-save feature in software. | +| **Checkpoint Strategy** | The specific rule defining when automatic checkpoints are created (Per Prompt, Per Tool Use, Smart). | Different auto-save frequencies/triggers. | +| **Diffing** | Comparing two checkpoints to see the differences in file content and token usage. | `git diff` command. | + +## Using Checkpointing in the UI + +You interact with checkpointing primarily within a specific session view (like `ClaudeCodeSession.tsx`), typically via a dedicated section or side panel. + +The `TimelineNavigator.tsx` component is the central piece of the UI for browsing and interacting with checkpoints: + +```typescript +// src/components/TimelineNavigator.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { GitBranch, Save, RotateCcw, GitFork, Diff } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { api, type Checkpoint, type TimelineNode, type SessionTimeline, type CheckpointDiff } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * Visual timeline navigator for checkpoint management + */ +export const TimelineNavigator: React.FC = ({ + sessionId, + projectId, + projectPath, + currentMessageIndex, + onCheckpointSelect, // Callback for selecting a checkpoint (e.g., for Diff) + onFork, // Callback for triggering a fork + refreshVersion = 0, // Prop to force reload + className +}) => { + const [timeline, setTimeline] = useState(null); // State for the timeline data + const [selectedCheckpoint, setSelectedCheckpoint] = useState(null); // State for the currently selected checkpoint (for diffing, etc.) + const [showCreateDialog, setShowCreateDialog] = useState(false); // State for the "Create Checkpoint" dialog + const [checkpointDescription, setCheckpointDescription] = useState(""); // State for the description input + const [isLoading, setIsLoading] = useState(false); + // ... other state for diff dialog, errors, etc. ... + + // Effect to load the timeline when the component mounts or needs refreshing + useEffect(() => { + loadTimeline(); + }, [sessionId, projectId, projectPath, refreshVersion]); // Dependencies + + // Function to load timeline data from backend + const loadTimeline = async () => { + try { + setIsLoading(true); + // Call backend API to get the timeline + const timelineData = await api.getSessionTimeline(sessionId, projectId, projectPath); + setTimeline(timelineData); // Update state + // ... logic to auto-expand current branch ... + } catch (err) { + console.error("Failed to load timeline:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle manual checkpoint creation + const handleCreateCheckpoint = async () => { + try { + setIsLoading(true); + // Call backend API to create a checkpoint + await api.createCheckpoint( + sessionId, + projectId, + projectPath, + currentMessageIndex, // Pass current message count + checkpointDescription || undefined // Pass optional description + ); + setCheckpointDescription(""); // Clear input + setShowCreateDialog(false); // Close dialog + await loadTimeline(); // Reload timeline to show the new checkpoint + } catch (err) { + console.error("Failed to create checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle restoring a checkpoint + const handleRestoreCheckpoint = async (checkpoint: Checkpoint) => { + // ... confirmation logic ... + try { + setIsLoading(true); + // Call backend API to restore the checkpoint + await api.restoreCheckpoint(checkpoint.id, sessionId, projectId, projectPath); + await loadTimeline(); // Reload timeline + // Notify parent component or session view about the restore + // This might trigger reloading the message history from the checkpoint + onCheckpointSelect(checkpoint); + } catch (err) { + console.error("Failed to restore checkpoint:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + // Function to handle forking (delegates to parent component via callback) + const handleFork = async (checkpoint: Checkpoint) => { + // This component doesn't *create* the new session, it tells the parent + // session view to initiate a fork from this checkpoint ID + onFork(checkpoint.id); + }; + + // Function to handle comparing checkpoints + const handleCompare = async (checkpoint: Checkpoint) => { + if (!selectedCheckpoint) { + // If no checkpoint is selected for comparison, select this one + setSelectedCheckpoint(checkpoint); + // You might update UI to show this checkpoint is selected for compare + return; + } + // If a checkpoint is already selected, perform the comparison + try { + setIsLoading(true); + const diffData = await api.getCheckpointDiff( + selectedCheckpoint.id, // The first selected checkpoint + checkpoint.id, // The checkpoint being compared against + sessionId, projectId // Session/Project context + ); + // ... show diffData in a dialog ... + setDiff(diffData); + // ... open diff dialog ... + } catch (err) { + console.error("Failed to get diff:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + + // Recursive function to render the timeline tree structure + const renderTimelineNode = (node: TimelineNode, depth: number = 0) => { + // ... rendering logic for node, its children, and buttons ... + // Each node displays checkpoint info and buttons for Restore, Fork, Diff + const isCurrent = timeline?.currentCheckpointId === node.checkpoint.id; + const isSelected = selectedCheckpoint?.id === node.checkpoint.id; // For compare selection + + + return ( +
+ {/* UI representation of the checkpoint */} + setSelectedCheckpoint(node.checkpoint)} // Select for compare/info + > + + {/* Display checkpoint ID, timestamp, description, metadata (tokens, files) */} +

{node.checkpoint.id.slice(0, 8)}...

+

{node.checkpoint.timestamp}

+

{node.checkpoint.description}

+ {node.checkpoint.metadata.totalTokens} tokens + {node.checkpoint.metadata.fileChanges} files changed + + {/* Action Buttons */} + + + +
+
+ + {/* Recursively render children */} + {/* ... Conditional rendering based on expanded state ... */} +
+ {node.children.map((child) => renderTimelineNode(child, depth + 1))} +
+
+ ); + }; + + return ( +
+ {/* ... Warning message ... */} + {/* Header with "Checkpoint" button */} +
+
+ +

Timeline

+ {/* Display total checkpoints badge */} +
+ +
+ + {/* Error display */} + {/* ... */} + + {/* Render the timeline tree starting from the root node */} + {timeline?.rootNode ? ( +
+ {renderTimelineNode(timeline.rootNode)} +
+ ) : ( + // ... Loading/empty state ... + )} + + {/* Create checkpoint dialog */} + + + + Create Checkpoint + {/* ... Dialog description and input for description ... */} + +
+
+ + setCheckpointDescription(e.target.value)} /> +
+
+ + {/* ... Cancel and Create buttons calling handleCreateCheckpoint ... */} + +
+
+ + {/* Diff dialog (not shown here, but would display diff state) */} + {/* ... Dialog for showing diff results ... */} +
+ ); +}; +``` + +This component displays the timeline tree structure, fetched from the backend using `api.getSessionTimeline`. Each node in the tree represents a checkpoint (`TimelineNode` contains a `Checkpoint` struct). The component provides buttons to trigger actions like creating a manual checkpoint (`handleCreateCheckpoint`), restoring a checkpoint (`handleRestoreCheckpoint`), forking (`handleFork`), and comparing checkpoints (`handleCompare`). These actions call corresponding backend API functions via `src/lib/api.ts`. + +You can also configure automatic checkpointing and cleanup using the `CheckpointSettings.tsx` component: + +```typescript +// src/components/CheckpointSettings.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Settings, Save, Trash2, HardDrive } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { SelectComponent } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { api, type CheckpointStrategy } from "@/lib/api"; // Import types and API + +// ... component props interface ... + +/** + * CheckpointSettings component for managing checkpoint configuration + */ +export const CheckpointSettings: React.FC = ({ + sessionId, + projectId, + projectPath, + onClose, + className, +}) => { + const [autoCheckpointEnabled, setAutoCheckpointEnabled] = useState(true); + const [checkpointStrategy, setCheckpointStrategy] = useState("smart"); + const [totalCheckpoints, setTotalCheckpoints] = useState(0); + const [keepCount, setKeepCount] = useState(10); // State for cleanup setting + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + // ... error/success states ... + + const strategyOptions: SelectOption[] = [ + { value: "manual", label: "Manual Only" }, + { value: "per_prompt", label: "After Each Prompt" }, + { value: "per_tool_use", label: "After Tool Use" }, + { value: "smart", label: "Smart (Recommended)" }, + ]; + + // Load settings when component mounts + useEffect(() => { + loadSettings(); + }, [sessionId, projectId, projectPath]); + + const loadSettings = async () => { + try { + setIsLoading(true); + // Call backend API to get settings + const settings = await api.getCheckpointSettings(sessionId, projectId, projectPath); + setAutoCheckpointEnabled(settings.auto_checkpoint_enabled); + setCheckpointStrategy(settings.checkpoint_strategy); + setTotalCheckpoints(settings.total_checkpoints); // Get total count for cleanup info + } catch (err) { + console.error("Failed to load checkpoint settings:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + const handleSaveSettings = async () => { + try { + setIsSaving(true); + // Call backend API to update settings + await api.updateCheckpointSettings( + sessionId, + projectId, + projectPath, + autoCheckpointEnabled, + checkpointStrategy + ); + // ... show success message ... + } catch (err) { + console.error("Failed to save checkpoint settings:", err); + // ... set error state ... + } finally { + setIsSaving(false); + } + }; + + const handleCleanup = async () => { + // ... confirmation ... + try { + setIsLoading(true); + // Call backend API to cleanup + const removed = await api.cleanupOldCheckpoints( + sessionId, + projectId, + projectPath, + keepCount // Pass how many recent checkpoints to keep + ); + // ... show success message ... + await loadSettings(); // Refresh count + } catch (err) { + console.error("Failed to cleanup checkpoints:", err); + // ... set error state ... + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* ... Experimental Warning ... */} + {/* Header */} +
+ {/* ... Title and icon ... */} + {onClose && } +
+ + {/* Error/Success messages */} + {/* ... */} + +
+ {/* Auto-checkpoint toggle */} +
+
+ +

Automatically create checkpoints

+
+ +
+ + {/* Checkpoint strategy select */} +
+ + setCheckpointStrategy(value as CheckpointStrategy)} + options={strategyOptions} + disabled={isLoading || !autoCheckpointEnabled} // Disable if auto-checkpoint is off + /> + {/* ... Strategy description text ... */} +
+ + {/* Save button */} + +
+ + {/* Storage Management Section */} +
+
+ {/* ... "Storage Management" title and icon ... */} +

Total checkpoints: {totalCheckpoints}

{/* Display count */} +
+ {/* Cleanup settings */} +
+ +
+ setKeepCount(parseInt(e.target.value) || 10)} disabled={isLoading} className="flex-1"/> + +
+ {/* ... Cleanup description text ... */} +
+
+ + ); +}; +``` + +This component allows you to toggle automatic checkpoints, select a strategy (Manual, Per Prompt, Per Tool Use, Smart), set how many recent checkpoints to keep, and trigger a cleanup. These actions are handled by backend commands called via `api`. + +## How it Works: Under the Hood (Backend) + +The checkpointing logic resides in the `src-tauri/src/checkpoint/` module. This module contains several key parts: + +1. **`checkpoint::mod.rs`**: Defines the main data structures (`Checkpoint`, `FileSnapshot`, `SessionTimeline`, `TimelineNode`, `CheckpointStrategy`, etc.) and utility structs (`CheckpointPaths`, `CheckpointDiff`). +2. **`checkpoint::storage.rs`**: Handles reading from and writing to disk. It manages saving/loading checkpoint metadata, messages, and file snapshots. It uses content-addressable storage for file contents to save space. +3. **`checkpoint::manager.rs`**: The core logic for managing a *single session*'s checkpoints. It tracks file changes (`FileTracker`), keeps the current message history (`current_messages`), interacts with `CheckpointStorage` for saving/loading, manages the session's `Timeline`, and handles operations like creating, restoring, and forking. +4. **`checkpoint::state.rs`**: A stateful manager (similar to the Process Registry) that holds `CheckpointManager` instances for *all active sessions* in memory. This prevents needing to recreate managers for each command call. + +Checkpoint data is stored within the `~/.claude` directory, specifically within the project's timeline directory: + +`~/.claude/projects//.timelines//` + +Inside this session timeline directory, you'll find: +* `timeline.json`: Stores the `SessionTimeline` structure (the tree metadata). +* `checkpoints/`: A directory containing subdirectories for each checkpoint ID. Each checkpoint directory (`checkpoints//`) holds `metadata.json` and `messages.jsonl` (the compressed messages). +* `files/`: A directory containing file snapshots, organized into a `content_pool/` (actual compressed file contents, stored by hash) and `refs/` (references from each checkpoint back to the content pool, stored as small JSON files). + +### The `CheckpointState` + +Just like the Process Registry manages active processes, the `CheckpointState` manages active `CheckpointManager` instances. When a session starts or is loaded in the UI, the frontend calls a backend command which then uses `CheckpointState::get_or_create_manager` to get the manager for that session. + +```rust +// src-tauri/src/checkpoint/state.rs (Simplified) +// ... imports ... +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; // For thread-safe async access + +use super::manager::CheckpointManager; + +/// Manages checkpoint managers for active sessions +#[derive(Default, Clone)] +pub struct CheckpointState { + /// Map of session_id to CheckpointManager + managers: Arc>>>, // Use RwLock for concurrent reads/writes + claude_dir: Arc>>, // Claude dir path needed for storage initialization +} + +impl CheckpointState { + // ... new(), set_claude_dir(), remove_manager(), clear_all() methods ... + + /// Gets or creates a CheckpointManager for a session + pub async fn get_or_create_manager( + &self, + session_id: String, + project_id: String, + project_path: PathBuf, + ) -> Result> { + let mut managers = self.managers.write().await; // Lock for writing + + // Check if manager already exists + if let Some(manager) = managers.get(&session_id) { + return Ok(Arc::clone(manager)); // Return existing manager (Arc::clone increases ref count) + } + + // ... get claude_dir ... + + // Create new manager if it doesn't exist + let manager = CheckpointManager::new( + project_id, + session_id.clone(), + project_path, + claude_dir, + ).await?; // CheckpointManager::new handles loading/init storage + + let manager_arc = Arc::new(manager); + managers.insert(session_id, Arc::clone(&manager_arc)); // Store new manager + + Ok(manager_arc) + } + + // ... get_manager(), list_active_sessions() methods ... +} +``` + +This structure ensures that the heavy work of loading the timeline and setting up file tracking only happens once per session when it's first accessed, not for every single checkpoint-related command. + +### Creating a Checkpoint Flow + +When the frontend requests to create a checkpoint (manually or automatically), the backend command retrieves the session's `CheckpointManager` from the `CheckpointState` and calls `manager.create_checkpoint(...)`. + +Here's a simplified look at what happens inside `CheckpointManager::create_checkpoint`: + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... new(), track_message(), track_file_modification(), etc. ... + + /// Create a checkpoint + pub async fn create_checkpoint( + &self, + description: Option, + parent_checkpoint_id: Option, // Optional parent ID for explicit forks + ) -> Result { + let messages = self.current_messages.read().await; // Get current messages + let message_index = messages.len().saturating_sub(1); + + // ... Extract metadata (prompt, tokens, etc.) from messages ... + + // Ensure all files in the project are tracked before snapshotting + // This discovers new files and adds them to the file tracker + let mut all_files = Vec::new(); + let _ = collect_files(&self.project_path, &self.project_path, &mut all_files); + for rel in all_files { + if let Some(p) = rel.to_str() { + let _ = self.track_file_modification(p).await; // Adds/updates tracker state + } + } + + // Generate a unique ID for the new checkpoint + let checkpoint_id = storage::CheckpointStorage::generate_checkpoint_id(); + + // Create file snapshots based on the *current* state of tracked files + // This reads the content of files marked as modified by track_file_modification + let file_snapshots = self.create_file_snapshots(&checkpoint_id).await?; + + // Build the Checkpoint metadata struct + let checkpoint = Checkpoint { + id: checkpoint_id.clone(), + session_id: self.session_id.clone(), + project_id: self.project_id.clone(), + message_index, + timestamp: Utc::now(), + description, + parent_checkpoint_id: parent_checkpoint_id.or_else(|| self.timeline.read().await.current_checkpoint_id.clone()), // Link to current parent or explicit parent + // ... include extracted metadata ... + }; + + // Save the checkpoint using the storage layer + let messages_content = messages.join("\n"); + let result = self.storage.save_checkpoint( + &self.project_id, + &self.session_id, + &checkpoint, + file_snapshots, // Pass the actual snapshots + &messages_content, // Pass the message content + )?; + + // ... Reload timeline from disk to incorporate new node ... + // ... Update current_checkpoint_id in in-memory timeline ... + // ... Reset is_modified flag in the file tracker ... + + Ok(result) + } + + // Helper to create FileSnapshots from the FileTracker state + async fn create_file_snapshots(&self, checkpoint_id: &str) -> Result> { + let tracker = self.file_tracker.read().await; + let mut snapshots = Vec::new(); + + for (rel_path, state) in &tracker.tracked_files { + // Only snapshot files marked as modified or deleted + if !state.is_modified && state.exists { // Only include if modified OR was deleted + continue; // Skip if not modified AND still exists + } + if state.is_modified || !state.exists { // Snapshot if modified or is now deleted + // ... read file content, calculate hash, get metadata ... + let (content, exists, permissions, size, current_hash) = { /* ... */ }; + + snapshots.push(FileSnapshot { + checkpoint_id: checkpoint_id.to_string(), + file_path: rel_path.clone(), + content, // Content will be empty for deleted files + hash: current_hash, // Hash will be empty for deleted files + is_deleted: !exists, + permissions, + size, + }); + } + } + Ok(snapshots) + } + + // ... other methods ... +} +``` + +The `create_checkpoint` function coordinates the process: it reads current messages, identifies changed files using the `FileTracker`, generates file snapshots by reading changed file contents, creates the checkpoint metadata, saves everything to disk via `CheckpointStorage`, and updates the timeline. + +The `FileTracker` keeps a list of files that have been referenced (either by the user or by tool outputs). The `track_file_modification` method is called whenever a file might have changed (e.g., mentioned in an edit tool output). It checks the file's current state (existence, hash, modification time) and marks it as `is_modified` if it differs from the last known state. + +The `CheckpointStorage::save_checkpoint` method handles the actual disk writing, including compressing messages and file contents and managing the content-addressable storage for file snapshots (`save_file_snapshot`). + +```rust +// src-tauri/src/checkpoint/storage.rs (Simplified) +// ... imports ... + +impl CheckpointStorage { + // ... new(), init_storage(), load_checkpoint(), etc. ... + + /// Save a checkpoint to disk + pub fn save_checkpoint(/* ... arguments ... */) -> Result { + // ... create directories ... + // ... save metadata.json ... + // ... save compressed messages.jsonl ... + + // Save file snapshots (calling save_file_snapshot for each) + let mut files_processed = 0; + for snapshot in &file_snapshots { + if self.save_file_snapshot(&paths, snapshot).is_ok() { // Calls helper + files_processed += 1; + } + } + + // Update timeline file on disk + self.update_timeline_with_checkpoint(/* ... */)?; + + // ... return result ... + Ok(CheckpointResult { /* ... */ }) + } + + /// Save a single file snapshot using content-addressable storage + fn save_file_snapshot(&self, paths: &CheckpointPaths, snapshot: &FileSnapshot) -> Result<()> { + // Directory where actual file content is stored by hash + let content_pool_dir = paths.files_dir.join("content_pool"); + fs::create_dir_all(&content_pool_dir)?; + + // Path to the content file based on its hash + let content_file = content_pool_dir.join(&snapshot.hash); + + // Only write content if the file doesn't exist (avoids duplicates) + if !content_file.exists() && !snapshot.is_deleted { + // Compress and save file content + let compressed_content = encode_all(snapshot.content.as_bytes(), self.compression_level) + .context("Failed to compress file content")?; + fs::write(&content_file, compressed_content)?; + } + + // Create a reference file for this checkpoint's view of the file + let checkpoint_refs_dir = paths.files_dir.join("refs").join(&snapshot.checkpoint_id); + fs::create_dir_all(&checkpoint_refs_dir)?; + + // Save a small JSON file containing metadata and a pointer (hash) to the content pool + let ref_metadata = serde_json::json!({ + "path": snapshot.file_path, + "hash": snapshot.hash, + "is_deleted": snapshot.is_deleted, + "permissions": snapshot.permissions, + "size": snapshot.size, + }); + let safe_filename = snapshot.file_path.to_string_lossy().replace('/', "_").replace('\\', "_"); + let ref_path = checkpoint_refs_dir.join(format!("{}.json", safe_filename)); + fs::write(&ref_path, serde_json::to_string_pretty(&ref_metadata)?)?; + + Ok(()) + } + + // ... update_timeline_with_checkpoint() and other methods ... +} +``` + +This snippet shows how `save_file_snapshot` stores the *actual* file content in a `content_pool` directory, named by the file's hash. This means if the same file content appears in multiple checkpoints, it's only stored once on disk. Then, in a `refs` directory specific to the checkpoint, a small file is saved that just contains the file's metadata and a pointer (the hash) back to the content pool. + +Here is a simplified sequence diagram for creating a manual checkpoint: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (TimelineNavigator.tsx) + participant Backend as Backend Commands (claude.rs) + participant CheckpointState as CheckpointState (state.rs) + participant CheckpointManager as CheckpointManager (manager.rs) + participant CheckpointStorage as CheckpointStorage (storage.rs) + participant Filesystem as Filesystem + + User->>Frontend: Clicks "Checkpoint" button + Frontend->>Backend: Call create_checkpoint(...) + Backend->>CheckpointState: get_or_create_manager(session_id, ...) + CheckpointState->>CheckpointState: Look up manager in map + alt Manager exists + CheckpointState-->>Backend: Return existing manager + else Manager does not exist + CheckpointState->>CheckpointManager: Create new Manager() + CheckpointManager->>CheckpointStorage: init_storage(...) + CheckpointStorage->>Filesystem: Create directories, load timeline.json + Filesystem-->>CheckpointStorage: Return timeline data / Success + CheckpointStorage-->>CheckpointManager: Success + CheckpointManager-->>CheckpointState: Return new manager + CheckpointState->>CheckpointState: Store new manager in map + CheckpointState-->>Backend: Return new manager + end + Backend->>CheckpointManager: create_checkpoint(description, ...) + CheckpointManager->>CheckpointManager: Read current messages + CheckpointManager->>Filesystem: Walk project directory + Filesystem-->>CheckpointManager: List of files + loop For each project file + CheckpointManager->>Filesystem: Read file content & metadata + Filesystem-->>CheckpointManager: File data + CheckpointManager->>CheckpointManager: Track file state (hash, modified) + end + CheckpointManager->>CheckpointStorage: save_checkpoint(checkpoint, snapshots, messages) + CheckpointStorage->>Filesystem: Write metadata.json, messages.jsonl (compressed) + loop For each modified file + CheckpointStorage->>Filesystem: Check if hash exists in content_pool + alt Hash exists + CheckpointStorage->>Filesystem: Skip writing content + else Hash does not exist + CheckpointStorage->>Filesystem: Write compressed file content to content_pool (by hash) + end + CheckpointStorage->>Filesystem: Write reference file (metadata + hash) to refs/ + end + CheckpointStorage->>Filesystem: Update timeline.json + Filesystem-->>CheckpointStorage: Success + CheckpointStorage-->>CheckpointManager: Return success/result + CheckpointManager-->>Backend: Return success/result + Backend-->>Frontend: Resolve Promise + Frontend->>Frontend: Call loadTimeline() to refresh UI + Frontend->>User: Display new checkpoint in timeline +``` + +This diagram illustrates the flow from the user clicking a button to the backend coordinating with the manager, which in turn uses the storage layer to read and write data to the filesystem, resulting in a new checkpoint entry and updated timeline on disk. + +### Restoring a Checkpoint Flow + +Restoring a checkpoint works in reverse. When the frontend calls `api.restoreCheckpoint(checkpointId, ...)`, the backend finds the `CheckpointManager` and calls `manager.restore_checkpoint(checkpointId)`. + +```rust +// src-tauri/src/checkpoint/manager.rs (Simplified) +// ... imports ... + +impl CheckpointManager { + // ... create_checkpoint() etc. ... + + /// Restore a checkpoint + pub async fn restore_checkpoint(&self, checkpoint_id: &str) -> Result { + // Load checkpoint data using the storage layer + let (checkpoint, file_snapshots, messages) = self.storage.load_checkpoint( + &self.project_id, + &self.session_id, + checkpoint_id, + )?; + + // Get list of all files currently in the project directory + let mut current_files = Vec::new(); + let _ = collect_all_project_files(&self.project_path, &self.project_path, &mut current_files); + + // Determine which files need to be deleted (exist now, but not in snapshot as non-deleted) + let mut checkpoint_files_set = std::collections::HashSet::new(); + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + checkpoint_files_set.insert(snapshot.file_path.clone()); + } + } + + // Delete files not present (as non-deleted) in the checkpoint + for current_file in current_files { + if !checkpoint_files_set.contains(¤t_file) { + let full_path = self.project_path.join(¤t_file); + // ... attempt fs::remove_file(&full_path) ... + log::info!("Deleted file not in checkpoint: {:?}", current_file); + } + } + // ... attempt to remove empty directories ... + + + // Restore/overwrite files from snapshots + let mut files_processed = 0; + for snapshot in &file_snapshots { + // This helper handles creating parent dirs, writing content, setting permissions, or deleting + match self.restore_file_snapshot(snapshot).await { // Calls helper + Ok(_) => { /* ... */ }, + Err(e) => { /* ... collect warnings ... */ }, + } + files_processed += 1; + } + + // Update in-memory messages buffer + let mut current_messages = self.current_messages.write().await; + current_messages.clear(); + for line in messages.lines() { + current_messages.push(line.to_string()); + } + + // Update the current_checkpoint_id in the in-memory timeline + let mut timeline = self.timeline.write().await; + timeline.current_checkpoint_id = Some(checkpoint_id.to_string()); + + // Reset the file tracker state to match the restored checkpoint + let mut tracker = self.file_tracker.write().await; + tracker.tracked_files.clear(); // Clear old state + for snapshot in &file_snapshots { + if !snapshot.is_deleted { + tracker.tracked_files.insert( + snapshot.file_path.clone(), + FileState { + last_hash: snapshot.hash.clone(), + is_modified: false, // Assume clean state after restore + last_modified: Utc::now(), // Or snapshot timestamp if available? + exists: true, + } + ); + } + } + + + Ok(CheckpointResult { /* ... checkpoint, files_processed, warnings ... */ }) + } + + // Helper to restore a single file from its snapshot data + async fn restore_file_snapshot(&self, snapshot: &FileSnapshot) -> Result<()> { + let full_path = self.project_path.join(&snapshot.file_path); + + if snapshot.is_deleted { + // If snapshot indicates deleted, remove the file if it exists + if full_path.exists() { + fs::remove_file(&full_path).context("Failed to delete file")?; + } + } else { + // If snapshot exists, create parent directories and write content + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).context("Failed to create parent directories")?; + } + fs::write(&full_path, &snapshot.content).context("Failed to write file")?; + + // Restore permissions (Unix only) + #[cfg(unix)] + if let Some(mode) = snapshot.permissions { + use std::os::unix::fs::PermissionsExt; + let permissions = std::fs::Permissions::from_mode(mode); + fs::set_permissions(&full_path, permissions).context("Failed to set file permissions")?; + } + } + Ok(()) + } + + // ... other methods ... +} +``` + +The `restore_checkpoint` function reads the checkpoint data from disk using `CheckpointStorage::load_checkpoint`. It then gets a list of the *current* files in the project directory. By comparing the current files with the files present in the checkpoint snapshot, it identifies which files need to be deleted. It iterates through the snapshots, using `restore_file_snapshot` to either delete files or write their content back to the project directory, recreating parent directories and setting permissions as needed. Finally, it updates the in-memory message list and the current checkpoint pointer in the timeline manager. + +This process effectively reverts the project directory and the session's state to match the chosen checkpoint. + +### Forking + +Forking is implemented by first restoring the session to the chosen checkpoint and then immediately creating a *new* checkpoint from that restored state. The key is that the new checkpoint explicitly sets its `parent_checkpoint_id` to the checkpoint it forked *from*, causing the timeline to branch. + +### Automatic Checkpointing + +Automatic checkpointing is controlled by the `auto_checkpoint_enabled` flag and the `checkpoint_strategy` setting stored in the `SessionTimeline`. When a new message arrives in the session (handled by the streaming output processing, [Chapter 7]), the `CheckpointManager::should_auto_checkpoint` method is called. This checks the strategy. For example, if the strategy is `PerPrompt`, it checks if the message is a user prompt. If the strategy is `Smart`, it checks if the message indicates a potentially destructive tool use (like `write`, `edit`, `bash`). If `should_auto_checkpoint` returns `true`, the backend triggers the `create_checkpoint` flow described above. + +### Cleanup + +The `Cleanup` feature in the `CheckpointSettings.tsx` component calls a backend command that uses `CheckpointStorage::cleanup_old_checkpoints`. This function loads the timeline, sorts checkpoints chronologically, identifies checkpoints older than the `keep_count`, and removes their metadata and references from disk. Crucially, it then calls `CheckpointStorage::garbage_collect_content` to find any actual file content in the `content_pool` directory that is *no longer referenced by any remaining checkpoints* and deletes that orphaned content to free up disk space. + +## Conclusion + +In this chapter, we delved into **Checkpointing**, a powerful feature in `claudia` that provides version control for your Claude Code sessions. We learned that checkpoints save snapshots of both your session's message history and the state of your project files, organized into a visual timeline. + +We explored how you can use the UI to create manual checkpoints, restore to previous states, fork off new branches of work, view differences between checkpoints, and configure automatic checkpointing and cleanup settings. + +Under the hood, we saw how the backend uses a `CheckpointManager` per session, coordinates with `CheckpointStorage` for reading and writing to disk, tracks file changes using a `FileTracker`, and uses a content-addressable storage mechanism for file snapshots to save disk space. We walked through the steps involved in creating and restoring checkpoints, including managing file changes and updating the session state. + +Understanding checkpointing empowers you to use Claude Code for more complex and iterative tasks with confidence, knowing you can always revert to a previous state or explore different paths. + +In the next and final chapter, we will explore **MCP (Model Context Protocol)**, the standardized format Claude Code uses for exchanging information with tools and other components, which plays a role in enabling features like checkpointing and tool execution. + +[Next Chapter: MCP (Model Context Protocol)](10_mcp__model_context_protocol__.md) + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/manager.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/mod.rs), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/state.rs), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/checkpoint/storage.rs), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/CheckpointSettings.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/TimelineNavigator.tsx) +# Chapter 10: MCP (Model Context Protocol) + +Welcome to the final chapter of the `claudia` tutorial! We've covered a lot, from managing your work with [Session/Project Management](01_session_project_management_.md) and defining specialized [Agents](02_agents_.md), to understanding how the [Frontend UI Components](03_frontend_ui_components_.md) are built and how they talk to the backend using [Tauri Commands](04_tauri_commands_.md). We've seen how `claudia` interacts with the core [Claude CLI Interaction](05_claude_cli_interaction_.md), how [Sandboxing](06_sandboxing_.md) keeps your environment secure, how [Streamed Output Processing](07_streamed_output_processing_.md) provides real-time feedback, and how the [Process Registry](08_process_registry_.md) tracks running tasks. Finally, we explored [Checkpointing](09_checkpointing_.md) for versioning your sessions. + +Now, let's look at a feature that allows `claudia` (specifically, the `claude` CLI it controls) to go beyond just interacting with Anthropic's standard Claude API: **MCP (Model Context Protocol)**. + +## The Problem: Connecting to Different AI Sources + +By default, the `claude` CLI is primarily designed to connect to Anthropic's Claude API endpoints (like the ones that power Sonnet, Opus, etc.). But what if you want to use a different AI model? Perhaps a smaller model running locally on your machine, a specialized AI tool you built, or an internal AI service within your company? + +These other AI sources might have different ways of communicating. You need a standard way for `claudia` (or rather, the `claude` CLI it manages) to talk to *any* AI service that can process prompts, use tools, and respond, regardless of who built it or how it runs. + +This is the problem MCP solves. It provides a standardized "language" or "interface" that allows `claude` to communicate with any external program or service that "speaks" MCP. + +Imagine `claudia` is a smart home hub. It needs to talk to various devices – lights, thermostats, speakers – made by different companies. Instead of needing a unique connection method for every single brand, they all agree to use a standard protocol (like Wi-Fi and a common API). MCP is that standard protocol for AI model servers. + +## What is MCP (Model Context Protocol)? + +MCP stands for **Model Context Protocol**. It's a standard protocol used by the `claude` CLI to exchange information with external programs or services that act as AI models or tools. + +When you configure an "MCP Server" in `claude` (and thus in `claudia`), you're telling `claude` about an external AI source that it can connect to using the MCP standard. + +This abstraction layer manages: + +1. **Defining Servers:** Telling `claude` about external MCP sources by giving them a name and specifying how to connect (e.g., run a specific command, connect to a URL). +2. **Listing Servers:** Seeing which MCP servers are configured. +3. **Interacting:** When a session or Agent is configured to use a specific MCP server, the `claude` CLI connects to that server (instead of the default Anthropic API) and uses the MCP to send prompts and receive responses. + +This capability extends `claudia`'s potential far beyond just Anthropic's hosted models, enabling connections to a variety of AI models or services that implement the MCP standard. + +## Key Concepts + +Here are the main ideas behind MCP in `claudia` (and `claude`): + +| Concept | Description | Analogy | +| :---------------- | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | +| **MCP Server** | An external program or service that speaks the MCP standard and can act as an AI model or provide tools. | A smart device (light, speaker) in a smart home system. | +| **Transport** | How `claude` connects to the MCP Server. Common types are `stdio` (running the server as a command-line process) or `sse` (connecting to a network URL via Server-Sent Events). | How the hub talks to the device (e.g., Wi-Fi, Bluetooth). | +| **Scope** | Where the MCP server configuration is stored. Affects who can see/use it: `user` (all projects), `project` (via `.mcp.json` in the project directory), `local` (only this `claudia` instance's settings, usually linked to a project). | Where you save the device setup (e.g., globally in the app, specific to one room setup). | +| **MCP Configuration** | The details needed to connect to a server: name, transport type, command/URL, environment variables, scope. | The device's settings (name, type, how to connect, what room it's in). | + +## Using MCP in the UI + +`claudia` provides a dedicated section to manage MCP servers. You'll typically find this under "Settings" or a similar menu item. + +The `MCPManager.tsx` component is the main view for this: + +```typescript +// src/components/MCPManager.tsx (Simplified) +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; +// ... other imports like api, MCPServerList, MCPAddServer, MCPImportExport ... + +export const MCPManager: React.FC = ({ onBack, className }) => { + const [activeTab, setActiveTab] = useState("servers"); // State for the active tab + const [servers, setServers] = useState([]); // State for the list of servers + const [loading, setLoading] = useState(true); + // ... error/toast state ... + + // Load servers when the component mounts + useEffect(() => { + loadServers(); + }, []); + + // Function to load servers from the backend + const loadServers = async () => { + try { + setLoading(true); + // Call the backend command to list servers + const serverList = await api.mcpList(); + setServers(serverList); // Update state + } catch (err) { + console.error("Failed to load MCP servers:", err); + // ... set error state ... + } finally { + setLoading(false); + } + }; + + // Callbacks for child components (Add, List, Import) + const handleServerAdded = () => { + loadServers(); // Refresh the list after adding + setActiveTab("servers"); // Switch back to the list view + // ... show success toast ... + }; + + const handleServerRemoved = (name: string) => { + setServers(prev => prev.filter(s => s.name !== name)); // Remove server from state + // ... show success toast ... + }; + + const handleImportCompleted = (imported: number, failed: number) => { + loadServers(); // Refresh after import + // ... show import result toast ... + }; + + return ( +
{/* Layout container */} + {/* Header with Back button */} +
+ +

MCP Servers

+
+ + {/* Tabs for navigating sections */} + + + Servers + Add Server + Import/Export + + + {/* Server List Tab Content */} + + {/* Using a Card component */} + + + + + {/* Add Server Tab Content */} + + {/* Using a Card component */} + + + + + {/* Import/Export Tab Content */} + + {/* Using a Card component */} + + + + + + {/* ... Toast notifications ... */} +
+ ); +}; +``` + +This main component uses tabs to organize the different MCP management tasks: +* **Servers:** Shows a list of configured servers using the `MCPServerList` component. +* **Add Server:** Provides a form to manually add a new server using the `MCPAddServer` component. +* **Import/Export:** Contains options to import servers (e.g., from a JSON file or Claude Desktop config) or potentially export them, using the `MCPImportExport` component. + +The `MCPServerList.tsx` component simply takes the list of `MCPServer` objects and displays them, grouped by scope (User, Project, Local). It provides buttons to remove or test the connection for each server, calling the relevant `onServerRemoved` or backend test command. + +The `MCPAddServer.tsx` component presents a form where you can enter the details of a new server: name, select the transport type (Stdio or SSE), provide the command or URL, add environment variables, and choose the scope. When you click "Add", it calls the backend `api.mcpAdd` command. + +```typescript +// src/components/MCPAddServer.tsx (Simplified) +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { SelectComponent } from "@/components/ui/select"; +// ... other imports like api ... + +export const MCPAddServer: React.FC = ({ onServerAdded, onError }) => { + const [transport, setTransport] = useState<"stdio" | "sse">("stdio"); + const [serverName, setServerName] = useState(""); + const [commandOrUrl, setCommandOrUrl] = useState(""); + const [scope, setScope] = useState("local"); + // ... state for args, env vars, saving indicator ... + + const handleAddServer = async () => { + if (!serverName.trim() || !commandOrUrl.trim()) { + onError("Name and Command/URL are required"); + return; + } + + try { + // ... set saving state ... + + // Prepare arguments based on transport type + const command = transport === "stdio" ? commandOrUrl : undefined; + const url = transport === "sse" ? commandOrUrl : undefined; + const args = transport === "stdio" ? commandOrUrl.split(/\s+/).slice(1) : []; // Simplified arg parsing + const env = {}; // Simplified env vars + + // Call the backend API command + const result = await api.mcpAdd( + serverName, + transport, + command, + args, + env, + url, + scope + ); + + if (result.success) { + // Reset form and notify parent + setServerName(""); + setCommandOrUrl(""); + setScope("local"); + // ... reset args/env ... + onServerAdded(); + } else { + onError(result.message); // Show error from backend + } + } catch (error) { + onError("Failed to add server"); + console.error("Failed to add MCP server:", error); + } finally { + // ... unset saving state ... + } + }; + + return ( +
+

Add MCP Server

+ setTransport(v as "stdio" | "sse")}> + + Stdio + SSE + + {/* ... Form fields based on transport type (Name, Command/URL, Scope, Env) ... */} + + +
+ ); +}; +``` + +This component collects user input and passes it to the `api.mcpAdd` function, which is a wrapper around the backend Tauri command. + +Once an MCP server is configured, it can potentially be selected as the "model" for an Agent run or an interactive session, although the integration point for selecting MCP servers specifically during session execution might be evolving or limited in the current `claudia` UI compared to standard Anthropic models. The core mechanism is that the `claude` CLI itself is told *which* configured MCP server to use for a task via command-line arguments, rather than connecting directly to Anthropic. + +## How it Works: Under the Hood (Backend) + +The MCP management in `claudia`'s backend (Rust) doesn't re-implement the MCP standard or manage external processes/connections directly for all servers. Instead, it primarily acts as a wrapper around the **`claude mcp`** subcommand provided by the `claude` CLI itself. + +When you use the MCP management features in `claudia`'s UI: + +1. **Frontend Calls Command:** The frontend calls a Tauri command like `mcp_add`, `mcp_list`, or `mcp_remove` ([Chapter 4: Tauri Commands]). +2. **Backend Calls `claude mcp`:** The backend command receives the request and constructs the appropriate command-line arguments for the `claude mcp` subcommand (e.g., `claude mcp add`, `claude mcp list`, `claude mcp remove`). +3. **Backend Spawns Process:** The backend spawns the `claude` binary as a child process, executing it with the prepared `mcp` arguments ([Chapter 5: Claude CLI Interaction]). +4. **`claude` CLI Handles Logic:** The `claude` CLI process receives the `mcp` command and performs the requested action: + * `claude mcp add`: Parses the provided configuration (name, transport, command/URL, scope) and saves it to its own configuration file (usually `~/.claude/mcp.json` for user/local scope, or writes to `.mcp.json` in the project path for project scope). + * `claude mcp list`: Reads its configuration files and prints the list of configured servers to standard output in a specific text format. + * `claude mcp remove`: Removes the specified server from its configuration files. +5. **Backend Captures Output/Status:** `claudia`'s backend captures the standard output and standard error of the `claude mcp` process ([Chapter 7: Streamed Output Processing], though for simple `mcp` commands it's usually just capturing the final output). +6. **Backend Returns Result:** The backend processes the captured output (e.g., parses the list for `mcp list`, checks for success/failure messages for `mcp add`/`remove`) and returns the result back to the frontend. + +For managing project-scoped servers via `.mcp.json`, the backend also contains specific commands (`mcp_read_project_config`, `mcp_save_project_config`) that read and write the `.mcp.json` file directly using Rust's filesystem functions and JSON parsing. This is an alternative way to manage project-specific MCP configurations that doesn't strictly go through the `claude mcp` CLI commands. + +Here's a sequence diagram showing the flow for adding an MCP server using the `mcp_add` command: + +```mermaid +sequenceDiagram + participant Frontend as Claudia UI (MCPAddServer.tsx) + participant Backend as Backend Commands (mcp.rs) + participant OS as Operating System + participant ClaudeCLI as claude binary + + User->>Frontend: Fill form & click "Add Server" + Frontend->>Backend: Call mcp_add(name, transport, command, ...) + Backend->>Backend: Construct arguments for "claude mcp add" + Backend->>OS: Spawn process (claude mcp add ...) + OS-->>ClaudeCLI: Start claude binary + ClaudeCLI->>ClaudeCLI: Parse args, update MCP config file (~/.claude/mcp.json or .mcp.json) + ClaudeCLI-->>OS: Process finishes (exit code 0 on success) + OS-->>Backend: Process status & captured output/error + Backend->>Backend: Check status, parse output for result message + Backend-->>Frontend: Return AddServerResult { success, message } + Frontend->>Frontend: Handle result (show toast, refresh list) + Frontend->>User: User sees confirmation/error +``` + +This diagram shows that for server *management* operations (add, list, remove), `claudia` acts as a GUI frontend to the `claude mcp` command-line interface. + +When a session or Agent is configured to *use* one of these registered MCP servers for its AI interactions, the `claude` binary (launched by `claudia` as described in [Chapter 5: Claude CLI Interaction]) is invoked with arguments telling it *which* server to connect to (e.g., `--model mcp:my-server`). The `claude` binary then uses the configuration it previously saved to establish communication with the specified external MCP server using the correct transport (stdio or sse) and protocol. `claudia`'s role during this phase is primarily launching and monitoring the `claude` process, and streaming its output, as covered in previous chapters. + +## Diving into the Backend Code + +Let's look at some snippets from `src-tauri/src/commands/mcp.rs`. + +The helper function `execute_claude_mcp_command` is central to wrapping the CLI calls: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use tauri::AppHandle; +use anyhow::{Context, Result}; +use std::process::Command; +use log::info; + +/// Executes a claude mcp command +fn execute_claude_mcp_command(app_handle: &AppHandle, args: Vec<&str>) -> Result { + info!("Executing claude mcp command with args: {:?}", args); + + // Find the claude binary path (logic from Chapter 5) + let claude_path = super::claude::find_claude_binary(app_handle)?; + + // Create a command with inherited environment (helper from Chapter 5) + let mut cmd = super::claude::create_command_with_env(&claude_path); + + cmd.arg("mcp"); // Add the 'mcp' subcommand + for arg in args { + cmd.arg(arg); // Add specific arguments (add, list, remove, get, serve, test-connection, etc.) + } + + // Run the command and capture output + let output = cmd.output() + .context("Failed to execute claude mcp command")?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) // Return stdout on success + } else { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + Err(anyhow::anyhow!("Command failed: {}", stderr)) // Return stderr as error + } +} +``` + +This function simply prepares and runs the `claude mcp ...` command and handles returning the result or error message based on the exit status. + +Now, let's see how `mcp_add` uses this helper: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... + +/// Adds a new MCP server +#[tauri::command] +pub async fn mcp_add( + app: AppHandle, + name: String, + transport: String, + command: Option, + args: Vec, + env: HashMap, + url: Option, + scope: String, +) -> Result { + info!("Adding MCP server: {} with transport: {}", name, transport); + + let mut cmd_args = vec!["add"]; // The 'add' subcommand argument + + // Add arguments for scope, transport, env, name, command/url + // These match the expected arguments for 'claude mcp add' + cmd_args.push("-s"); + cmd_args.push(&scope); + + if transport == "sse" { + cmd_args.push("--transport"); + cmd_args.push("sse"); + } + + for (key, value) in env.iter() { + cmd_args.push("-e"); + cmd_args.push(&format!("{}={}", key, value)); // Format env vars correctly + } + + cmd_args.push(&name); // The server name + + if transport == "stdio" { + if let Some(cmd_str) = &command { + // Handle commands with spaces/args by adding "--" separator if needed + cmd_args.push("--"); + cmd_args.push(cmd_str); + for arg in &args { + cmd_args.push(arg); + } + } else { /* ... error handling ... */ } + } else if transport == "sse" { + if let Some(url_str) = &url { + cmd_args.push(url_str); // The URL for SSE + } else { /* ... error handling ... */ } + } else { /* ... error handling ... */ } + + // Execute the command using the helper + match execute_claude_mcp_command(&app, cmd_args) { + Ok(output) => { + // Parse the output message from claude mcp add + Ok(AddServerResult { + success: true, + message: output.trim().to_string(), + server_name: Some(name), + }) + } + Err(e) => { + // Handle errors from the command execution + Ok(AddServerResult { + success: false, + message: e.to_string(), + server_name: None, + }) + } + } +} +``` + +This command function demonstrates how it builds the `cmd_args` vector, carefully adding the correct flags and values expected by the `claude mcp add` command. It then passes these arguments to `execute_claude_mcp_command` and formats the result into the `AddServerResult` struct for the frontend. + +The `mcp_list` command is similar, executing `claude mcp list` and then parsing the text output (which can be complex, as noted in the code comments) to build the `Vec` structure returned to the frontend. + +Direct file access for `.mcp.json` (project scope) looks like this: + +```rust +// src-tauri/src/commands/mcp.rs (Simplified) +// ... imports ... +use std::path::PathBuf; +use std::fs; +use serde::{Serialize, Deserialize}; + +// Structs mirroring the .mcp.json structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPProjectConfig { + #[serde(rename = "mcpServers")] + pub mcp_servers: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MCPServerConfig { + pub command: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub env: HashMap, +} + + +/// Reads .mcp.json from the current project +#[tauri::command] +pub async fn mcp_read_project_config(project_path: String) -> Result { + log::info!("Reading .mcp.json from project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + if !mcp_json_path.exists() { + // Return empty config if file doesn't exist + return Ok(MCPProjectConfig { mcp_servers: HashMap::new() }); + } + + match fs::read_to_string(&mcp_json_path) { // Read the file content + Ok(content) => { + match serde_json::from_str::(&content) { // Parse JSON + Ok(config) => Ok(config), + Err(e) => { + log::error!("Failed to parse .mcp.json: {}", e); + Err(format!("Failed to parse .mcp.json: {}", e)) + } + } + } + Err(e) => { + log::error!("Failed to read .mcp.json: {}", e); + Err(format!("Failed to read .mcp.json: {}", e)) + } + } +} + +/// Saves .mcp.json to the current project +#[tauri::command] +pub async fn mcp_save_project_config( + project_path: String, + config: MCPProjectConfig, +) -> Result { + log::info!("Saving .mcp.json to project: {}", project_path); + + let mcp_json_path = PathBuf::from(&project_path).join(".mcp.json"); + + let json_content = serde_json::to_string_pretty(&config) // Serialize config to JSON + .map_err(|e| format!("Failed to serialize config: {}", e))?; + + fs::write(&mcp_json_path, json_content) // Write to the file + .map_err(|e| format!("Failed to write .mcp.json: {}", e))?; + + Ok("Project MCP configuration saved".to_string()) +} +``` + +These commands directly interact with the `.mcp.json` file in the project directory, allowing the UI to edit project-specific configurations without necessarily going through the `claude mcp` command for every change, although `claude` itself will still read this file when run within that project. + +## Conclusion + +In this final chapter, we explored **MCP (Model Context Protocol)**, the standard that allows the `claude` CLI to communicate with external AI model servers running outside the main Claude API. We learned that `claudia` leverages the `claude mcp` subcommand to manage configurations for these external servers, supporting different transport methods (stdio, sse) and scopes (user, project, local). + +We saw how the `claudia` UI provides dedicated sections to list, add, and import MCP servers, and how these actions map to backend Tauri commands. We then looked under the hood to understand that `claudia`'s backend primarily acts as a wrapper, executing `claude mcp` commands to let the `claude` CLI handle the actual configuration management and, during session execution, the communication with the external MCP servers. `claudia` also provides direct file-based management for project-scoped `.mcp.json` configurations. + +Understanding MCP highlights how `claudia` builds a flexible interface on top of `claude`, enabling connections to a potentially diverse ecosystem of AI tools and models that implement this protocol. This extends `claudia`'s capabilities beyond simply interacting with Anthropic's hosted services. + +This concludes our tutorial on the core concepts behind the `claudia` project. We hope this journey through its various components has provided you with a solid understanding of how this application works! + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src-tauri/src/commands/mcp.rs), [[2]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPAddServer.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPImportExport.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPManager.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abe0891b0b6e0f5516343bd86ed590bdc8e479b3/src/components/MCPServerList.tsx) diff --git a/Claudia-docs/claudia-1_tab_management_.md b/Claudia-docs/claudia-1_tab_management_.md new file mode 100644 index 00000000..fdb00f39 --- /dev/null +++ b/Claudia-docs/claudia-1_tab_management_.md @@ -0,0 +1,409 @@ +# Chapter 1: Tab Management + +Welcome to Claudia! This tutorial will guide you through the core concepts that make Claudia a powerful and organized tool for your coding projects. We'll start with something you're probably already very familiar with: tabs! + +## What is Tab Management? + +Think about how you use a web browser like Chrome or Firefox. You probably have many tabs open at once, right? One for your email, another for a news article, maybe a few for different research pages. You can easily switch between them, open new ones, and close old ones. This helps you keep multiple tasks organized without having to open many separate browser windows. + +In Claudia, we use the exact same idea! Instead of browser pages, our tabs hold different parts of the application. You might have: + +* A **chat session** where you're asking Claude (our AI assistant) to write code. +* A **project list** to see all your ongoing coding projects. +* A **settings page** to customize Claudia's behavior. +* An **agent execution** view, showing what an AI agent is doing. + +This system of tabs is what we call "Tab Management." It's designed to help you stay organized and switch between different workflows seamlessly. + +## Why is Tab Management Important? + +Imagine you're deeply engrossed in a Claude Code chat session, working on a complex problem. Suddenly, you realize you need to check the overall list of your projects. Without tabs, you might have to leave your current chat, go to a different screen, find your project, and then try to remember where you left off in your chat. That's disruptive! + +With Tab Management, it's easy: you just click on the "CC Projects" tab or open a new one, browse what you need, and then click back to your "Claude Chat" tab. Your chat session is still there, exactly as you left it. It's like having multiple workspaces open at the same time, right at your fingertips. + +## Key Concepts of Claudia's Tab System + +To understand how this works, let's look at a few core ideas: + +1. **Tab:** Each "box" at the top of the Claudia window represents a tab. It's a container for a specific view or activity. +2. **Active Tab:** Only one tab can be "active" at a time. This is the tab whose content you currently see and interact with. It's usually highlighted. +3. **Tab Types:** Not all tabs are the same! A tab can be of different "types" depending on what it's showing: + * `chat`: For interactive Claude Code sessions. + * `projects`: To view your project list and their sessions. + * `settings`: For application settings. + * `agent`: To monitor AI agent runs. + * ...and more! Each type has its own content. +4. **Tab Lifecycle:** Tabs are not static. You can: + * **Create** new tabs. + * **Switch** between existing tabs. + * **Update** a tab's content (e.g., changing its title or showing new data). + * **Close** tabs when you're done with them. + +## How to Use Tabs in Claudia + +Let's see how you'd use these concepts in practice. + +### Opening a New Tab + +You'll often start by wanting to create a new Claude Code chat session or browse your projects. + +**In the User Interface:** + +Look for the `+` button, usually on the right side of your tab bar. Clicking this button will typically open a new "CC Projects" tab, from which you can then start a new Claude Code session. + +**Using Code (for developers):** + +If you were building a new feature in Claudia and wanted to open a new chat tab, you might use a special tool called a "hook" (we'll learn more about hooks later!) called `useTabState`. + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function MyComponent() { + const { createChatTab } = useTabState(); + + const handleNewChatClick = () => { + // This function creates a new chat tab + createChatTab(); + }; + + return ( + + ); +} +``` +This small snippet, when part of a larger component, means that when `handleNewChatClick` is activated (for example, by clicking a button), a brand new Claude Code chat tab will appear and become the active tab. + +Similarly, to open the "CC Projects" view in a tab (if it's not already open): + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function TopbarMenu() { + const { createProjectsTab } = useTabState(); + + const openProjects = () => { + // This will open or switch to the CC Projects tab + createProjectsTab(); + }; + + // ... rest of your component +} +``` +Calling `createProjectsTab()` checks if a "CC Projects" tab already exists. If it does, it simply switches to it; otherwise, it creates a new one. This ensures you don't end up with many identical "CC Projects" tabs. + +### Switching Between Tabs + +Once you have multiple tabs, you'll want to move between them. + +**In the User Interface:** + +* **Clicking:** Simply click on the title of the tab you want to switch to in the tab bar. +* **Keyboard Shortcuts:** + * `Ctrl + Tab` (Windows/Linux) or `Cmd + Tab` (macOS): Switches to the next tab. + * `Ctrl + Shift + Tab` (Windows/Linux) or `Cmd + Shift + Tab` (macOS): Switches to the previous tab. + * `Ctrl + 1` through `Ctrl + 9` (or `Cmd + 1` through `Cmd + 9`): Switches directly to the tab at that number position (e.g., `Ctrl + 1` for the first tab). + +**Using Code:** + +If you need to programmatically switch to a specific tab, for instance, after an operation completes in another part of the app: + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function SessionList({ sessionIdToFocus }) { + const { switchToTab } = useTabState(); + + const handleSessionOpen = (tabId: string) => { + // This will make the tab with `tabId` the active one + switchToTab(tabId); + }; + + // ... imagine logic that gets a tabId and calls handleSessionOpen +} +``` +The `switchToTab` function takes the unique `id` of a tab and makes it the active one, bringing its content to the front. + +### Closing Tabs + +When you're done with a task, you can close its tab. + +**In the User Interface:** + +* **Click the 'X':** Each tab has a small `X` icon next to its title. Click it to close the tab. +* **Keyboard Shortcut:** `Ctrl + W` (Windows/Linux) or `Cmd + W` (macOS): Closes the currently active tab. + +**Using Code:** + +```typescript +import { useTabState } from "@/hooks/useTabState"; + +function TabItem({ tabId }) { + const { closeTab } = useTabState(); + + const handleCloseClick = () => { + // This will close the tab with `tabId` + closeTab(tabId); + }; + + // ... render the 'X' button with onClick={handleCloseClick} +} +``` +The `closeTab` function handles the removal of a tab. If a tab has unsaved changes, it might even ask for confirmation before closing (though this part is not shown in the simple example). + +## Under the Hood: How Tab Management Works + +Now, let's peek behind the curtain and see how Claudia manages all these tabs. + +### The Flow of a New Tab + +When you, for example, click the `+` button to create a new "CC Projects" tab, here's a simplified sequence of what happens: + +```mermaid +sequenceDiagram + participant User + participant App UI (TabManager) + participant useTabState Hook + participant TabContext Store + participant App UI (TabContent) + + User->>App UI (TabManager): Clicks "+" button + App UI (TabManager)->>useTabState Hook: Calls createProjectsTab() + useTabState Hook->>TabContext Store: Calls addTab({type: 'projects', ...}) + Note over TabContext Store: Generates unique ID, adds tab to list, sets as active + TabContext Store-->>useTabState Hook: Returns new tab ID + useTabState Hook->>TabContext Store: Calls setActiveTab(newTabId) + TabContext Store->>App UI (TabManager): Notifies tabs list changed + TabContext Store->>App UI (TabContent): Notifies active tab changed + App UI (TabManager)->>App UI (TabManager): Renders new tab button in tab bar + App UI (TabContent)->>App UI (TabContent): Renders "CC Projects" content +``` + +### The Core Components + +Claudia's tab management is built using a few key pieces: + +1. **`TabContext` (The Brain):** + * **File:** `src/contexts/TabContext.tsx` + * **Purpose:** This is the central "data store" for all tab-related information. It holds the list of all open tabs, which tab is currently active, and the functions to add, remove, and update tabs. + * **Analogy:** Imagine a clipboard or a big whiteboard where all the tabs' information is kept. Any part of the app can look at or change this whiteboard's contents through specific rules. + + Here's a simplified look at how `TabContext` defines a tab and manages its state: + + ```typescript + // src/contexts/TabContext.tsx (simplified) + export interface Tab { + id: string; // Unique identifier for the tab + type: 'chat' | 'agent' | 'projects' | 'settings'; // What kind of content it holds + title: string; // Text shown on the tab button + sessionId?: string; // Optional: specific to chat tabs + status: 'active' | 'idle' | 'running'; // Tab's current state + hasUnsavedChanges: boolean; // Does it need saving? + // ... more properties + } + + export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [tabs, setTabs] = useState([]); + const [activeTabId, setActiveTabId] = useState(null); + + // Effect to create a default "CC Projects" tab when the app starts + useEffect(() => { + const defaultTab: Tab = { + id: `tab-${Date.now()}-...`, // unique ID + type: 'projects', + title: 'CC Projects', + status: 'idle', + hasUnsavedChanges: false, + order: 0, + createdAt: new Date(), + updatedAt: new Date() + }; + setTabs([defaultTab]); + setActiveTabId(defaultTab.id); + }, []); // [] means it runs only once when the component mounts + + // Functions like addTab, removeTab, updateTab, setActiveTab are defined here + // ... + return ( + + {children} + + ); + }; + ``` + The `TabProvider` wraps the entire application (you can see this in `src/App.tsx`), making the `TabContext` available to all parts of the app. It also ensures that Claudia always starts with a "CC Projects" tab. + +2. **`useTabState` (The Helper Hook):** + * **File:** `src/hooks/useTabState.ts` + * **Purpose:** This is a special "hook" that makes it super easy for any part of Claudia's user interface to talk to the `TabContext`. Instead of directly messing with `TabContext`, components use `useTabState` to create tabs, switch them, or get information about them. + * **Analogy:** If `TabContext` is the big whiteboard, `useTabState` is like your personal assistant who reads from and writes on the whiteboard for you, so you don't have to get your hands dirty. + + Here's how `useTabState` provides handy functions: + + ```typescript + // src/hooks/useTabState.ts (simplified) + import { useTabContext } from '@/contexts/TabContext'; + + export const useTabState = () => { + const { + tabs, activeTabId, addTab, removeTab, updateTab, setActiveTab + } = useTabContext(); + + // Example: A helper function to create a chat tab + const createChatTab = useCallback((projectId?: string, title?: string): string => { + const tabTitle = title || `Chat ${tabs.length + 1}`; + return addTab({ // Calls addTab from TabContext + type: 'chat', + title: tabTitle, + sessionId: projectId, + status: 'idle', + hasUnsavedChanges: false, + }); + }, [addTab, tabs.length]); // Dependencies for useCallback + + // Example: A helper function to close a tab + const closeTab = useCallback(async (id: string, force: boolean = false): Promise => { + // ... logic to check for unsaved changes ... + removeTab(id); // Calls removeTab from TabContext + return true; + }, [removeTab]); + + // Returns all the useful functions and state for components + return { + tabs, activeTabId, createChatTab, closeTab, + switchToTab: setActiveTab, // Renaming setActiveTab for clarity + // ... more functions + }; + }; + ``` + +3. **`TabManager` (The Tab Bar UI):** + * **File:** `src/components/TabManager.tsx` + * **Purpose:** This component is responsible for drawing the visual tab bar at the top of the application. It shows each tab's title, icon, close button, and highlights the active tab. It also handles the `+` button for creating new tabs. + * **Analogy:** This is the physical row of tabs on your web browser's window frame. + + ```typescript + // src/components/TabManager.tsx (simplified) + import { useTabState } from '@/hooks/useTabState'; + import { X, Plus, Folder } from 'lucide-react'; // Icons + + export const TabManager: React.FC = () => { + const { + tabs, activeTabId, createProjectsTab, closeTab, switchToTab, canAddTab + } = useTabState(); // Uses our helper hook! + + const handleNewTab = () => { + if (canAddTab()) { + createProjectsTab(); // Calls the function from useTabState + } + }; + + return ( +
+ {/* Loop through all 'tabs' to render each one */} + {tabs.map((tab) => ( +
switchToTab(tab.id)} // Switches tab on click + > + {/* Example icon */} + {tab.title} + +
+ ))} + +
+ ); + }; + ``` + This component directly uses `useTabState` to get the list of tabs, know which one is active, and call functions like `createProjectsTab`, `closeTab`, and `switchToTab` when the user interacts with the UI. + +4. **`TabContent` (The View Area):** + * **File:** `src/components/TabContent.tsx` + * **Purpose:** This component is where the actual content of the active tab is displayed. It looks at the `activeTabId` from `useTabState` and then renders the correct component for that tab's `type` (e.g., `ClaudeCodeSession` for a `chat` tab, `ProjectList` for a `projects` tab). + * **Analogy:** This is the main part of your web browser window where the actual web page content appears. + + ```typescript + // src/components/TabContent.tsx (simplified) + import React, { Suspense, lazy } from 'react'; + import { useTabState } from '@/hooks/useTabState'; + import { Loader2 } from 'lucide-react'; + + // Lazily load components for performance + const ClaudeCodeSession = lazy(() => import('@/components/ClaudeCodeSession').then(m => ({ default: m.ClaudeCodeSession }))); + const ProjectList = lazy(() => import('@/components/ProjectList').then(m => ({ default: m.ProjectList }))); + const Settings = lazy(() => import('@/components/Settings').then(m => ({ default: m.Settings }))); + + interface TabPanelProps { + tab: Tab; + isActive: boolean; + } + + const TabPanel: React.FC = ({ tab, isActive }) => { + // Determines which component to show based on tab.type + const renderContent = () => { + switch (tab.type) { + case 'projects': + // ProjectList needs data, so it fetches it internally or gets it from context + return ; + case 'chat': + return ; + case 'settings': + return ; + // ... other tab types + default: + return
Unknown tab type: {tab.type}
; + } + }; + + return ( +
+ }> + {renderContent()} + +
+ ); + }; + + export const TabContent: React.FC = () => { + const { tabs, activeTabId } = useTabState(); // Gets tabs and activeTabId + + return ( +
+ {/* Render a TabPanel for each tab, but only show the active one */} + {tabs.map((tab) => ( + + ))} + {tabs.length === 0 && ( +
+

No tabs open. Click the + button to start a new chat.

+
+ )} +
+ ); + }; + ``` + `TabContent` ensures that only the content for the `activeTabId` is visible, effectively "switching" the view when you click on a different tab. + +In summary, `TabContext` manages the data, `useTabState` provides an easy way for components to interact with that data, `TabManager` displays the tab bar, and `TabContent` displays the content of the currently selected tab. + +## Conclusion + +You've now learned about Claudia's powerful Tab Management system! You understand why it's crucial for keeping your workflows organized, how to interact with tabs via the UI and (if you're a developer) through code, and the main components working behind the scenes. This multi-tab interface allows you to effortlessly navigate between your coding projects, AI chat sessions, agent executions, and application settings, providing a smooth and efficient user experience. + +In the next chapter, we'll dive deeper into one of the most exciting features you can use within these tabs: [Claude Code Session](02_claude_code_session_.md). You'll learn how to start interacting with Claude and manage your AI-powered coding sessions. + +--- + +Generated by [AI Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge). **References**: [[1]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/App.tsx), [[2]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/components/App.cleaned.tsx), [[3]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/components/TabContent.tsx), [[4]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/components/TabManager.tsx), [[5]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/contexts/TabContext.tsx), [[6]](https://github.com/getAsterisk/claudia/blob/abc73231946ee446d94453be20c51e88fa15b9ef/src/hooks/useTabState.ts) +```` \ No newline at end of file diff --git a/Claudia-docs/claudia-2_claude_code_session_.md b/Claudia-docs/claudia-2_claude_code_session_.md new file mode 100644 index 00000000..ad9d6b23 --- /dev/null +++ b/Claudia-docs/claudia-2_claude_code_session_.md @@ -0,0 +1,633 @@ +# Chapter 2: Claude Code Session + +In the last chapter, you learned how Claudia uses [Tab Management](01_tab_management_.md) to keep your workspace organized. You discovered that each tab can hold different parts of the application, including a "chat" tab. Now, let's dive into what that "chat" tab truly represents: the **Claude Code Session**. + +## What is a Claude Code Session? + +Imagine you're working on a coding project and you need a super-smart assistant right by your side, who can not only chat with you but also understand your code, make changes to files, and even run commands in your project's terminal. That's exactly what a Claude Code Session is! + +It's a specialized, interactive chat window built into Claudia. Instead of just talking, you're collaborating with Claude (our AI assistant) directly on your code. You give it instructions, Claude responds, and its actions—like editing files, running tests, or listing directory contents—are displayed right there in the conversation. + +**The main problem it solves:** How do you effectively integrate an advanced AI into your everyday coding workflow? A Claude Code Session provides that seamless interface, making AI-assisted development feel natural and intuitive. + +**Think of it like this:** You're sitting next to a highly skilled co-worker. You tell them what you want to achieve, they might ask clarifying questions, then they show you the code they've written, perhaps explaining their choices. If it's not quite right, you give more feedback, and they adjust. The Claude Code Session is your digital version of this collaborative experience. + +### A Common Scenario: Fixing a Bug + +Let's say you're working on a web application, and you've identified a bug in a specific file. Here's how a Claude Code Session helps: + +1. **You:** "Hey Claude, I have a bug in `src/utils/dateFormatter.ts`. It's incorrectly formatting dates for users in the Asia/Tokyo timezone. Can you take a look and fix it?" +2. **Claude:** (Reads the file, analyzes the code, possibly runs a test) "Okay, I see the issue. It looks like the `Intl.DateTimeFormat` options are not correctly handling the `timeZone` property. I'll propose an edit." (Displays a suggested code change) +3. **You:** "Looks good. Apply that change." +4. **Claude:** (Applies the change, confirms) "Done. Would you like me to run the tests to verify the fix?" +5. **You:** "Yes, please." + +This back-and-forth, with Claude directly interacting with your project, is the core of a Claude Code Session. + +## Key Concepts of a Claude Code Session + +To get the most out of your coding partnership with Claude, it's helpful to understand the main ideas behind a session: + +1. **Interactive Conversation (Chat Interface):** + * It's a live chat. You type messages (prompts), and Claude sends messages back. + * The entire conversation history is visible, so you can always see the context of your interaction. + +2. **Coding Environment & Context:** + * Unlike a general chatbot, Claude in a session is always aware of your **project path**. This means it knows which files and folders it can see and work with. + * Its responses and actions are tailored to the code within that specific project. + +3. **Input Prompt:** + * This is the text box at the bottom of the session where you type your instructions, questions, or feedback for Claude. + * It's your primary way to guide the AI. + +4. **Streaming Output:** + * Claude's responses don't just appear all at once. They "stream" in real-time, character by character. This makes the interaction feel very dynamic, as if Claude is typing live. + * You'll see not just text, but also visual indicators of its actions (like running commands or making edits). + +5. **Tool Uses:** + * Claude isn't magic; it uses specific "tools" to interact with your project. These tools are like mini-programs or functions that let Claude: + * `read`: Read the content of a file. + * `write`: Create or modify a file. + * `edit`: Apply specific changes to a file (like adding or deleting lines). + * `bash`: Run commands in your terminal (e.g., `npm test`, `ls`). + * `ls`: List files and directories. + * ...and many more! + * When Claude uses a tool, you'll see a clear display showing which tool it used and what its input was. This transparency helps you understand what Claude is doing. + +6. **Session Continuation & Resumption:** + * One of the best features is that your sessions are saved! You can close Claudia, or switch to a different tab, and when you return to a Claude Code Session tab, the entire conversation and project context is restored exactly as you left it. + * This allows you to take breaks, work on multiple tasks, and easily pick up where you left off. + +## How to Use a Claude Code Session in Claudia + +Let's walk through starting and interacting with a Claude Code Session. + +### 1. Starting a New Session + +You'll typically start a new session from the "CC Projects" tab. + +**In the User Interface:** + +1. If you don't have a "CC Projects" tab open, click the `+` button in the tab bar (as learned in [Tab Management](01_tab_management_.md)). +2. In the "CC Projects" view, you'll see a button like "Start new Claude Code session" or you can click on an existing project to resume a session within it. +3. Once a new Claude Code Session tab opens, the first thing you'll need to do is **select your project directory**. This tells Claude where your code lives! + + You'll see an input field for "Project Directory" and a folder icon. Click the folder icon to browse your computer and select the root folder of your coding project. + + ![Select Project Path](https://i.imgur.com/example-select-path.png) + + Once selected, the path will appear in the input field. + +### 2. Giving Your First Prompt + +With your project path set, you're ready to talk to Claude! + +At the bottom of the session, you'll see the **prompt input area**. This is where you type your instructions. + +```typescript +// src/components/FloatingPromptInput.tsx (simplified) + +interface FloatingPromptInputProps { + onSend: (prompt: string, model: "sonnet" | "opus") => void; + // ... other props +} + +const FloatingPromptInputInner = (props: FloatingPromptInputProps, ref: React.Ref) => { + const [prompt, setPrompt] = useState(""); + // ... other state and refs + + const handleSend = () => { + if (prompt.trim()) { + // Calls the onSend function passed from the parent component + props.onSend(prompt.trim(), selectedModel); + setPrompt(""); // Clear the input after sending + } + }; + + return ( +
+ {/* Model Picker, Thinking Mode Picker */} +
+