diff --git a/README.md b/README.md index 0b8f48937..a7f4d669f 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,13 @@ Think of Claudia as your command center for Claude Code - bridging the gap betwe ### Getting Started 1. **Launch Claudia**: Open the application after installation -2. **Welcome Screen**: Choose between CC Agents or CC Projects +2. **Welcome Screen**: Choose between CC Agents or Projects 3. **First Time Setup**: Claudia will automatically detect your `~/.claude` directory ### Managing Projects ``` -CC Projects → Select Project → View Sessions → Resume or Start New +Projects → Select Project → View Sessions → Resume or Start New ``` - Click on any project to view its sessions diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5c3951ef9..f907bb50a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -143,7 +143,7 @@ dependencies = [ "image", "log", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", @@ -622,7 +622,8 @@ dependencies = [ "async-trait", "base64 0.22.1", "chrono", - "cocoa", + "cocoa 0.25.0", + "cocoa 0.26.1", "dirs 5.0.1", "env_logger", "futures", @@ -653,6 +654,7 @@ dependencies = [ "uuid", "walkdir", "which", + "window-vibrancy 0.5.3", "zstd", ] @@ -665,6 +667,22 @@ dependencies = [ "error-code", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.26.1" @@ -673,14 +691,28 @@ checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ "bitflags 2.9.1", "block", - "cocoa-foundation", + "cocoa-foundation 0.2.1", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "libc", "objc", ] +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "cocoa-foundation" version = "0.2.1" @@ -690,7 +722,7 @@ dependencies = [ "bitflags 2.9.1", "block", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "objc", ] @@ -780,6 +812,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" @@ -788,11 +833,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -1768,7 +1824,7 @@ dependencies = [ "crossbeam-channel", "keyboard-types", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "once_cell", "serde", "thiserror 2.0.12", @@ -2663,7 +2719,7 @@ dependencies = [ "gtk", "keyboard-types", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "once_cell", @@ -2840,6 +2896,22 @@ dependencies = [ "objc2-exception-helper", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + [[package]] name = "objc2-app-kit" version = "0.3.1" @@ -2851,10 +2923,10 @@ dependencies = [ "libc", "objc2 0.6.1", "objc2-cloud-kit", - "objc2-core-data", + "objc2-core-data 0.3.1", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image", + "objc2-core-image 0.3.1", "objc2-foundation 0.3.1", "objc2-quartz-core 0.3.1", ] @@ -2870,6 +2942,18 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-data" version = "0.3.1" @@ -2905,6 +2989,18 @@ dependencies = [ "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + [[package]] name = "objc2-core-image" version = "0.3.1" @@ -2986,7 +3082,7 @@ checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ "bitflags 2.9.1", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", ] @@ -3035,7 +3131,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.6.1", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", ] @@ -3903,7 +3999,7 @@ dependencies = [ "js-sys", "log", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "raw-window-handle", @@ -4386,7 +4482,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "js-sys", "log", @@ -4570,7 +4666,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", @@ -4586,7 +4682,7 @@ dependencies = [ "ndk-context", "ndk-sys", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "once_cell", "parking_lot", @@ -4654,7 +4750,7 @@ dependencies = [ "mime", "muda", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "objc2-ui-kit", "percent-encoding", @@ -4678,7 +4774,7 @@ dependencies = [ "urlpattern", "webkit2gtk", "webview2-com", - "window-vibrancy", + "window-vibrancy 0.6.0", "windows", ] @@ -4971,7 +5067,7 @@ dependencies = [ "jni", "log", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "once_cell", "percent-encoding", @@ -5398,7 +5494,7 @@ dependencies = [ "libappindicator", "muda", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", @@ -5960,6 +6056,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window-vibrancy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831ad7678290beae36be6f9fad9234139c7f00f3b536347de7745621716be82d" +dependencies = [ + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + [[package]] name = "window-vibrancy" version = "0.6.0" @@ -5967,7 +6077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "raw-window-handle", @@ -6483,7 +6593,7 @@ dependencies = [ "libc", "ndk", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "objc2-ui-kit", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 01efb811a..4eeaf718a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,7 +20,7 @@ crate-type = ["lib", "cdylib", "staticlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] } +tauri = { version = "2", features = [ "macos-private-api", "protocol-asset", "tray-icon", "image-png"] } tauri-plugin-shell = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" @@ -30,6 +30,9 @@ tauri-plugin-notification = "2" tauri-plugin-clipboard-manager = "2" tauri-plugin-global-shortcut = "2" tauri-plugin-http = "2" +window-vibrancy = "0.5" +cocoa = "0.25" +objc = "0.2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 0eeb8f722..5e231dacf 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -49,6 +49,12 @@ "notification:default", "clipboard-manager:default", "global-shortcut:default", - "updater:default" + "updater:default", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-close", + "core:window:allow-is-maximized", + "core:window:allow-start-dragging" ] } diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 272a1cc92..2d1c7e366 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -502,6 +502,20 @@ pub fn create_command_with_env(program: &str) -> Command { } } } + + // Add Homebrew support if the program is in a Homebrew directory + if program.contains("/homebrew/") || program.contains("/opt/homebrew/") { + if let Some(program_dir) = std::path::Path::new(program).parent() { + // Ensure the Homebrew bin directory is in PATH + let current_path = std::env::var("PATH").unwrap_or_default(); + let homebrew_bin_str = program_dir.to_string_lossy(); + if !current_path.contains(&homebrew_bin_str.as_ref()) { + let new_path = format!("{}:{}", homebrew_bin_str, current_path); + debug!("Adding Homebrew bin directory to PATH: {}", homebrew_bin_str); + cmd.env("PATH", new_path); + } + } + } cmd } diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index c1f669d6a..94ad3c55e 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -5,7 +5,7 @@ use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::process::Stdio; use std::sync::Arc; -use std::time::SystemTime; +use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Emitter, Manager}; use tokio::process::{Child, Command}; use tokio::sync::Mutex; @@ -35,6 +35,8 @@ pub struct Project { pub sessions: Vec, /// Unix timestamp when the project directory was created pub created_at: u64, + /// Unix timestamp of the most recent session (if any) + pub most_recent_session: Option, } /// Represents a session with its metadata @@ -260,6 +262,19 @@ fn create_command_with_env(program: &str) -> Command { } } } + + // Add Homebrew support if the program is in a Homebrew directory + if program.contains("/homebrew/") || program.contains("/opt/homebrew/") { + if let Some(program_dir) = std::path::Path::new(program).parent() { + let current_path = std::env::var("PATH").unwrap_or_default(); + let homebrew_bin_str = program_dir.to_string_lossy(); + if !current_path.contains(&homebrew_bin_str.as_ref()) { + let new_path = format!("{}:{}", homebrew_bin_str, current_path); + log::debug!("Adding Homebrew bin directory to PATH: {}", homebrew_bin_str); + tokio_cmd.env("PATH", new_path); + } + } + } tokio_cmd } @@ -284,6 +299,15 @@ fn create_system_command( cmd } +/// Gets the user's home directory path +#[tauri::command] +pub async fn get_home_directory() -> Result { + dirs::home_dir() + .and_then(|path| path.to_str().map(|s| s.to_string())) + .ok_or_else(|| "Could not determine home directory".to_string()) +} + + /// Lists all projects in the ~/.claude/projects directory #[tauri::command] pub async fn list_projects() -> Result, String> { @@ -336,6 +360,8 @@ pub async fn list_projects() -> Result, String> { // List all JSONL files (sessions) in this project directory let mut sessions = Vec::new(); + let mut most_recent_session: Option = None; + if let Ok(session_entries) = fs::read_dir(&path) { for session_entry in session_entries.flatten() { let session_path = session_entry.path(); @@ -345,6 +371,21 @@ pub async fn list_projects() -> Result, String> { if let Some(session_id) = session_path.file_stem().and_then(|s| s.to_str()) { sessions.push(session_id.to_string()); + + // Track the most recent session timestamp + if let Ok(metadata) = fs::metadata(&session_path) { + let modified = metadata + .modified() + .unwrap_or(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + most_recent_session = Some(match most_recent_session { + Some(current) => current.max(modified), + None => modified, + }); + } } } } @@ -355,17 +396,73 @@ pub async fn list_projects() -> Result, String> { path: project_path, sessions, created_at, + most_recent_session, }); } } - // Sort projects by creation time (newest first) - projects.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + // Sort projects by most recent session activity, then by creation time + projects.sort_by(|a, b| { + // First compare by most recent session + match (a.most_recent_session, b.most_recent_session) { + (Some(a_time), Some(b_time)) => b_time.cmp(&a_time), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => b.created_at.cmp(&a.created_at), + } + }); log::info!("Found {} projects", projects.len()); Ok(projects) } +/// Creates a new project for the given directory path +#[tauri::command] +pub async fn create_project(path: String) -> Result { + log::info!("Creating project for path: {}", path); + + // Encode the path to create a project ID + let project_id = path.replace('/', "-"); + + // Get claude directory + let claude_dir = get_claude_dir().map_err(|e| e.to_string())?; + let projects_dir = claude_dir.join("projects"); + + // Create projects directory if it doesn't exist + if !projects_dir.exists() { + fs::create_dir_all(&projects_dir) + .map_err(|e| format!("Failed to create projects directory: {}", e))?; + } + + // Create project directory if it doesn't exist + let project_dir = projects_dir.join(&project_id); + if !project_dir.exists() { + fs::create_dir_all(&project_dir) + .map_err(|e| format!("Failed to create project directory: {}", e))?; + } + + // Get creation time + let metadata = fs::metadata(&project_dir) + .map_err(|e| format!("Failed to read directory metadata: {}", e))?; + + let created_at = metadata + .created() + .or_else(|_| metadata.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + // Return the created project + Ok(Project { + id: project_id, + path, + sessions: Vec::new(), + created_at, + most_recent_session: None, + }) +} + /// Gets sessions for a specific project #[tauri::command] pub async fn get_project_sessions(project_id: String) -> Result, String> { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0589bef75..ffc0212e3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -18,9 +18,9 @@ use commands::agents::{ }; use commands::claude::{ cancel_claude_execution, check_auto_checkpoint, check_claude_version, cleanup_old_checkpoints, - clear_checkpoint_manager, continue_claude_code, create_checkpoint, execute_claude_code, + clear_checkpoint_manager, continue_claude_code, create_checkpoint, create_project, execute_claude_code, find_claude_md_files, fork_from_checkpoint, get_checkpoint_diff, get_checkpoint_settings, - get_checkpoint_state_stats, get_claude_session_output, get_claude_settings, get_project_sessions, + get_checkpoint_state_stats, get_claude_session_output, get_claude_settings, get_home_directory, get_project_sessions, get_recently_modified_files, get_session_timeline, get_system_prompt, list_checkpoints, list_directory_contents, list_projects, list_running_claude_sessions, load_session_history, open_new_session, read_claude_md_file, restore_checkpoint, resume_claude_code, @@ -47,6 +47,10 @@ use process::ProcessRegistryState; use std::sync::Mutex; use tauri::Manager; +#[cfg(target_os = "macos")] +use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; + + fn main() { // Initialize logger env_logger::init(); @@ -136,12 +140,43 @@ fn main() { // Initialize Claude process state app.manage(ClaudeProcessState::default()); + // Apply window vibrancy with rounded corners on macOS + #[cfg(target_os = "macos")] + { + let window = app.get_webview_window("main").unwrap(); + + // Try different vibrancy materials that support rounded corners + let materials = [ + NSVisualEffectMaterial::UnderWindowBackground, + NSVisualEffectMaterial::WindowBackground, + NSVisualEffectMaterial::Popover, + NSVisualEffectMaterial::Menu, + NSVisualEffectMaterial::Sidebar, + ]; + + let mut applied = false; + for material in materials.iter() { + if apply_vibrancy(&window, *material, None, Some(12.0)).is_ok() { + applied = true; + break; + } + } + + if !applied { + // Fallback without rounded corners + apply_vibrancy(&window, NSVisualEffectMaterial::WindowBackground, None, None) + .expect("Failed to apply any window vibrancy"); + } + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ // Claude & Project Management list_projects, + create_project, get_project_sessions, + get_home_directory, get_claude_settings, open_new_session, get_system_prompt, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0530fc2a2..45094bc91 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -10,11 +10,18 @@ "frontendDist": "../dist" }, "app": { + "macOSPrivateApi": true, "windows": [ { "title": "Claudia", "width": 800, - "height": 600 + "height": 600, + "decorations": false, + "transparent": true, + "shadow": true, + "center": true, + "resizable": true, + "alwaysOnTop": false } ], "security": { diff --git a/src/App.tsx b/src/App.tsx index 0d424fb74..f798dbc33 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,15 @@ import { useState, useEffect } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { Plus, Loader2, Bot, FolderCode } from "lucide-react"; +import { motion } from "framer-motion"; +import { Bot, FolderCode } from "lucide-react"; import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api"; import { OutputCacheProvider } from "@/lib/outputCache"; import { TabProvider } from "@/contexts/TabContext"; import { ThemeProvider } from "@/contexts/ThemeContext"; -import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { ProjectList } from "@/components/ProjectList"; +import { FilePicker } from "@/components/FilePicker"; import { SessionList } from "@/components/SessionList"; -import { RunningClaudeSessions } from "@/components/RunningClaudeSessions"; -import { Topbar } from "@/components/Topbar"; +import { CustomTitlebar } from "@/components/CustomTitlebar"; import { MarkdownEditor } from "@/components/MarkdownEditor"; import { ClaudeFileEditor } from "@/components/ClaudeFileEditor"; import { Settings } from "@/components/Settings"; @@ -23,7 +22,6 @@ import { Toast, ToastContainer } from "@/components/ui/toast"; import { ProjectSettings } from '@/components/ProjectSettings'; import { TabManager } from "@/components/TabManager"; import { TabContent } from "@/components/TabContent"; -import { AgentsModal } from "@/components/AgentsModal"; import { useTabState } from "@/hooks/useTabState"; import { AnalyticsConsentBanner } from "@/components/AnalyticsConsent"; import { useAppLifecycle, useTrackEvent } from "@/hooks"; @@ -49,19 +47,20 @@ type View = */ function AppContent() { const [view, setView] = useState("tabs"); - const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab } = useTabState(); + const { createClaudeMdTab, createSettingsTab, createUsageTab, createMCPTab, createAgentsTab } = useTabState(); const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [sessions, setSessions] = useState([]); const [editingClaudeFile, setEditingClaudeFile] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [_error, setError] = useState(null); const [showNFO, setShowNFO] = useState(false); const [showClaudeBinaryDialog, setShowClaudeBinaryDialog] = useState(false); + const [showProjectPicker, setShowProjectPicker] = useState(false); + const [homeDirectory, setHomeDirectory] = useState('/'); const [toast, setToast] = useState<{ message: string; type: "success" | "error" | "info" } | null>(null); const [projectForSettings, setProjectForSettings] = useState(null); const [previousView] = useState("welcome"); - const [showAgentsModal, setShowAgentsModal] = useState(false); // Initialize analytics lifecycle tracking useAppLifecycle(); @@ -183,20 +182,19 @@ function AppContent() { }; /** - * Opens a new Claude Code session in the interactive UI + * Opens the project directory picker */ - const handleNewSession = async () => { - handleViewChange("tabs"); - // The tab system will handle creating a new chat tab + const handleOpenProject = async () => { + // Get home directory before showing picker + const homeDir = await api.getHomeDirectory(); + setHomeDirectory(homeDir); + setShowProjectPicker(true); }; /** - * Returns to project list view + * Opens a new Claude Code session in the interactive UI */ - const handleBack = () => { - setSelectedProject(null); - setSessions([]); - }; + // New session creation is handled by the tab system via titlebar actions /** * Handles editing a CLAUDE.md file from a project @@ -225,10 +223,7 @@ function AppContent() { /** * Handles navigating to hooks configuration */ - const handleProjectSettings = (project: Project) => { - setProjectForSettings(project); - handleViewChange("project-settings"); - }; + // Project settings navigation handled via `projectForSettings` state when needed const renderContent = () => { @@ -239,9 +234,9 @@ function AppContent() {
{/* Welcome Header */}

@@ -254,9 +249,9 @@ function AppContent() {
{/* CC Agents Card */} - {/* CC Projects Card */} + {/* Projects Card */}
-

CC Projects

+

Projects

@@ -306,125 +301,25 @@ function AppContent() { ); case "settings": - return ( -
- handleViewChange("welcome")} /> -
- ); + return handleViewChange("welcome")} />; case "projects": + if (selectedProject) { + return ( + + ); + } return ( -
-
- {/* Header with back button */} - - -
-

CC Projects

-

- Browse your Claude Code sessions -

-
-
- - {/* Error display */} - {error && ( - - {error} - - )} - - {/* Loading state */} - {loading && ( -
- -
- )} - - {/* Content */} - {!loading && ( - - {selectedProject ? ( - - - - ) : ( - - {/* New session button at the top */} - - - - - {/* Running Claude Sessions */} - - - {/* Project list */} - {projects.length > 0 ? ( - - ) : ( -
-

- No projects found in ~/.claude/projects -

-
- )} -
- )} -
- )} -
-
+ ); case "claude-file-editor": @@ -475,16 +370,26 @@ function AppContent() { }; return ( -
- {/* Topbar */} - + {/* Custom Titlebar */} + createAgentsTab()} + onUsageClick={() => createUsageTab()} + onClaudeClick={() => createClaudeMdTab()} + onMCPClick={() => createMCPTab()} + onSettingsClick={() => createSettingsTab()} + onInfoClick={() => setShowNFO(true)} + /> + + {/* Topbar - Commented out since navigation moved to titlebar */} + {/* createClaudeMdTab()} onSettingsClick={() => createSettingsTab()} onUsageClick={() => createUsageTab()} onMCPClick={() => createMCPTab()} onInfoClick={() => setShowNFO(true)} onAgentsClick={() => setShowAgentsModal(true)} - /> + /> */} {/* Analytics Consent Banner */} @@ -497,11 +402,6 @@ function AppContent() { {/* NFO Credits Modal */} {showNFO && setShowNFO(false)} />} - {/* Agents Modal */} - {/* Claude Binary Dialog */} setToast({ message, type: "error" })} /> + + {/* File picker modal for selecting project directory */} + {showProjectPicker && ( +
+
+ { + if (entry.is_directory) { + // Create or open a project for this directory + try { + const project = await api.createProject(entry.path); + setShowProjectPicker(false); + await loadProjects(); + await handleProjectClick(project); + } catch (err) { + console.error('Failed to create project:', err); + setError('Failed to create project for the selected directory.'); + } + } + }} + onClose={() => setShowProjectPicker(false)} + /> +
+
+ )} {/* Toast Container */} @@ -525,6 +451,33 @@ function AppContent() { /> )} + + {/* File picker modal for selecting project directory */} + {showProjectPicker && ( +
+
+ { + if (entry.is_directory) { + // Create or open a project for this directory + try { + const project = await api.createProject(entry.path); + setShowProjectPicker(false); + await loadProjects(); + // Load sessions for the selected project + await handleProjectClick(project); + } catch (err) { + console.error('Failed to create project:', err); + setError('Failed to create project for the selected directory.'); + } + } + }} + onClose={() => setShowProjectPicker(false)} + /> +
+
+ )}
); } diff --git a/src/assets/fonts/inter/Inter.ttf b/src/assets/fonts/inter/Inter.ttf new file mode 100644 index 000000000..e31b51e3e Binary files /dev/null and b/src/assets/fonts/inter/Inter.ttf differ diff --git a/src/components/AgentExecution.tsx b/src/components/AgentExecution.tsx index 9691ec614..41fbb1fd7 100644 --- a/src/components/AgentExecution.tsx +++ b/src/components/AgentExecution.tsx @@ -4,7 +4,6 @@ import { ArrowLeft, Play, StopCircle, - FolderOpen, Terminal, AlertCircle, Loader2, @@ -22,27 +21,33 @@ import { Dialog, DialogContent, DialogDescription, - DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { api, type Agent } from "@/lib/api"; import { cn } from "@/lib/utils"; -import { open } from "@tauri-apps/plugin-dialog"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { StreamMessage } from "./StreamMessage"; import { ExecutionControlBar } from "./ExecutionControlBar"; import { ErrorBoundary } from "./ErrorBoundary"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { AGENT_ICONS } from "./CCAgents"; import { HooksEditor } from "./HooksEditor"; import { useTrackEvent, useComponentMetrics, useFeatureAdoptionTracking } from "@/hooks"; +import { useTabState } from "@/hooks/useTabState"; interface AgentExecutionProps { /** * The agent to execute */ agent: Agent; + /** + * Optional initial project path + */ + projectPath?: string; + /** + * Optional tab ID for updating tab status + */ + tabId?: string; /** * Callback to go back to the agents list */ @@ -78,13 +83,18 @@ export interface ClaudeStreamMessage { */ export const AgentExecution: React.FC = ({ agent, + projectPath: initialProjectPath, + tabId, onBack, className, }) => { - const [projectPath, setProjectPath] = useState(""); + const [projectPath] = useState(initialProjectPath || ""); const [task, setTask] = useState(agent.default_task || ""); const [model, setModel] = useState(agent.model || "sonnet"); const [isRunning, setIsRunning] = useState(false); + + // Get tab state functions + const { updateTabStatus } = useTabState(); const [messages, setMessages] = useState([]); const [rawJsonlOutput, setRawJsonlOutput] = useState([]); const [error, setError] = useState(null); @@ -267,25 +277,7 @@ export const AgentExecution: React.FC = ({ }, [messages]); - const handleSelectPath = async () => { - try { - const selected = await open({ - directory: true, - multiple: false, - title: "Select Project Directory" - }); - - if (selected) { - setProjectPath(selected as string); - setError(null); // Clear any previous errors - } - } catch (err) { - console.error("Failed to select directory:", err); - // More detailed error logging - const errorMessage = err instanceof Error ? err.message : String(err); - setError(`Failed to select directory: ${errorMessage}`); - } - }; + // Project path selection is handled upstream when opening an execution tab const handleOpenHooksDialog = async () => { setIsHooksDialogOpen(true); @@ -294,6 +286,11 @@ export const AgentExecution: React.FC = ({ const handleExecute = async () => { try { setIsRunning(true); + // Update tab status to running + console.log('Setting tab status to running for tab:', tabId); + if (tabId) { + updateTabStatus(tabId, 'running'); + } setExecutionStartTime(Date.now()); setMessages([]); setRawJsonlOutput([]); @@ -351,6 +348,10 @@ export const AgentExecution: React.FC = ({ setExecutionStartTime(null); if (!event.payload) { setError("Agent execution failed"); + // Update tab status to error + if (tabId) { + updateTabStatus(tabId, 'error'); + } // Track both the old event for compatibility and the new error event trackEvent.agentExecuted(agent.name || 'custom', false, agent.name, duration); trackEvent.agentError({ @@ -360,6 +361,10 @@ export const AgentExecution: React.FC = ({ agent_type: agent.name || 'custom' }); } else { + // Update tab status to complete on success + if (tabId) { + updateTabStatus(tabId, 'complete'); + } trackEvent.agentExecuted(agent.name || 'custom', true, agent.name, duration); } }); @@ -368,6 +373,10 @@ export const AgentExecution: React.FC = ({ setIsRunning(false); setExecutionStartTime(null); setError("Agent execution was cancelled"); + // Update tab status to idle when cancelled + if (tabId) { + updateTabStatus(tabId, 'idle'); + } }); unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten, cancelUnlisten]; @@ -376,6 +385,10 @@ export const AgentExecution: React.FC = ({ setIsRunning(false); setExecutionStartTime(null); setRunId(null); + // Update tab status to error + if (tabId) { + updateTabStatus(tabId, 'error'); + } // Show error in messages setMessages(prev => [...prev, { type: "result", @@ -410,6 +423,10 @@ export const AgentExecution: React.FC = ({ // Update UI state setIsRunning(false); setExecutionStartTime(null); + // Update tab status to idle when stopped + if (tabId) { + updateTabStatus(tabId, 'idle'); + } // Clean up listeners unlistenRefs.current.forEach(unlisten => unlisten()); @@ -432,6 +449,10 @@ export const AgentExecution: React.FC = ({ // Still update UI state even if the backend call failed setIsRunning(false); setExecutionStartTime(null); + // Update tab status to idle + if (tabId) { + updateTabStatus(tabId, 'idle'); + } // Show error message setMessages(prev => [...prev, { @@ -539,200 +560,188 @@ export const AgentExecution: React.FC = ({ setCopyPopoverOpen(false); }; - const renderIcon = () => { - const Icon = agent.icon in AGENT_ICONS ? AGENT_ICONS[agent.icon as keyof typeof AGENT_ICONS] : Terminal; - return ; - }; return (
{/* Fixed container that takes full height */} -
- {/* Sticky Header */} -
-
- -
-
- -
-
- {renderIcon()} -
-
-

Execute: {agent.name}

-

- {model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'} -

-
-
-
-
- -
+
+ {/* Header */} +
+
+
+ +
+

{agent.name}

+

+ {isRunning ? 'Running' : messages.length > 0 ? 'Complete' : 'Ready'} • {model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'} +

- +
+
+ {messages.length > 0 && ( + + )} +
- {/* Sticky Configuration */} -
-
+ {/* Configuration Section */} +
+
{/* Error display */} {error && ( - - {error} + + {error} )} - {/* Project Path */} -
- -
- setProjectPath(e.target.value)} - placeholder="Select or enter project path" - disabled={isRunning} - className="flex-1" - /> - - -
-
- {/* Model Selection */} -
- -
- + - +
{/* Task Input */} -
- +
+
+ + {projectPath && ( + + )} +
setTask(e.target.value)} - placeholder="Enter the task for the agent" + placeholder="What would you like the agent to do?" disabled={isRunning} - className="flex-1" + className="flex-1 h-9" onKeyPress={(e) => { if (e.key === "Enter" && !isRunning && projectPath && task.trim()) { handleExecute(); } }} /> - + +
+ {projectPath && ( +

+ Working in: {projectPath.split('/').pop() || projectPath} +

+ )}
@@ -761,7 +770,7 @@ export const AgentExecution: React.FC = ({

Ready to Execute

- Select a project path and enter a task to run the agent + Enter a task to run the agent

)} @@ -823,7 +832,6 @@ export const AgentExecution: React.FC = ({ {/* Modal Header */}
- {renderIcon()}

{agent.name} - Output

{isRunning && (
@@ -903,7 +911,7 @@ export const AgentExecution: React.FC = ({

Ready to Execute

- Select a project path and enter a task to run the agent + Enter a task to run the agent

)} @@ -955,26 +963,34 @@ export const AgentExecution: React.FC = ({ open={isHooksDialogOpen} onOpenChange={setIsHooksDialogOpen} > - - - Configure Hooks - - Configure hooks that run before, during, and after tool executions. Changes are saved immediately. + +
+ Configure Hooks + + Configure hooks that run before, during, and after tool executions - +
- - Project Settings - Local Settings - +
+ + + Project Settings + + + Local Settings + + +
- -
-

- Project hooks are stored in .claude/settings.json and - are committed to version control. -

+ +
+
+

+ Project hooks are stored in .claude/settings.json and + are committed to version control, allowing team members to share configurations. +

+
= ({
- -
-

- Local hooks are stored in .claude/settings.local.json and - are not committed to version control. -

+ +
+
+

+ Local hooks are stored in .claude/settings.local.json and + are not committed to version control, perfect for personal preferences. +

+
{ + const [activeTab, setActiveTab] = useState('agents'); + const [showCreateAgent, setShowCreateAgent] = useState(false); + const [agents, setAgents] = useState([]); + const [runningAgents, setRunningAgents] = useState([]); + const [loading, setLoading] = useState(true); + const [agentToDelete, setAgentToDelete] = useState(null); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); + const [showGitHubBrowser, setShowGitHubBrowser] = useState(false); + const { createAgentTab } = useTabState(); + + // Load agents on mount + useEffect(() => { + loadAgents(); + loadRunningAgents(); + }, []); + + // Refresh running agents periodically + useEffect(() => { + const interval = setInterval(() => { + loadRunningAgents(); + }, 3000); // Refresh every 3 seconds + + return () => clearInterval(interval); + }, []); + + const loadAgents = async () => { + try { + setLoading(true); + const agents = await api.listAgents(); + setAgents(agents); + } catch (error) { + console.error('Failed to load agents:', error); + setToast({ message: 'Failed to load agents', type: 'error' }); + } finally { + setLoading(false); + } + }; + + const loadRunningAgents = async () => { + try { + const runs = await api.listAgentRunsWithMetrics(); + setRunningAgents(runs); + } catch (error) { + console.error('Failed to load running agents:', error); + } + }; + + const handleRunAgent = async (agent: Agent) => { + if (!agent.id) { + setToast({ message: 'Agent ID is missing', type: 'error' }); + return; + } + + // Import the dialog function + const { open } = await import('@tauri-apps/plugin-dialog'); + + try { + // Prompt user to select a project directory + const projectPath = await open({ + directory: true, + multiple: false, + title: `Select project directory for ${agent.name}` + }); + + if (!projectPath) { + // User cancelled + return; + } + + // Dispatch event to open agent execution in a new tab + const tabId = `agent-exec-${agent.id}-${Date.now()}`; + window.dispatchEvent(new CustomEvent('open-agent-execution', { + detail: { agent, tabId, projectPath } + })); + + setToast({ message: `Opening agent: ${agent.name}`, type: 'success' }); + } catch (error) { + console.error('Failed to open agent:', error); + setToast({ message: `Failed to open agent: ${agent.name}`, type: 'error' }); + } + }; + + const handleDeleteAgent = async () => { + if (!agentToDelete || !agentToDelete.id) return; + + try { + await api.deleteAgent(agentToDelete.id); + setToast({ message: `Deleted agent: ${agentToDelete.name}`, type: 'success' }); + setAgents(prev => prev.filter(a => a.id !== agentToDelete.id)); + setShowDeleteDialog(false); + setAgentToDelete(null); + } catch (error) { + console.error('Failed to delete agent:', error); + setToast({ message: `Failed to delete agent: ${agentToDelete.name}`, type: 'error' }); + } + }; + + const handleImportFromFile = async () => { + try { + const selected = await openDialog({ + filters: [ + { name: 'JSON Files', extensions: ['json'] }, + { name: 'All Files', extensions: ['*'] } + ], + multiple: false, + }); + + if (selected) { + const fileContent = await invoke('read_text_file', { path: selected }); + const agentData = JSON.parse(fileContent); + + const importedAgent = await api.importAgent(JSON.stringify(agentData)); + setToast({ message: `Imported agent: ${importedAgent.name}`, type: 'success' }); + loadAgents(); + } + } catch (error) { + console.error('Failed to import agent:', error); + setToast({ message: 'Failed to import agent', type: 'error' }); + } + }; + + const handleExportAgent = async (agent: Agent) => { + try { + const path = await save({ + defaultPath: `${agent.name}.json`, + filters: [ + { name: 'JSON Files', extensions: ['json'] } + ] + }); + + if (path && agent.id) { + const agentData = await api.exportAgent(agent.id); + await invoke('write_text_file', { + path, + contents: agentData + }); + setToast({ message: `Exported agent: ${agent.name}`, type: 'success' }); + } + } catch (error) { + console.error('Failed to export agent:', error); + setToast({ message: 'Failed to export agent', type: 'error' }); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'running': + return ; + case 'completed': + return ; + case 'failed': + return ; + default: + return ; + } + }; + + // Show CreateAgent component if creating + if (showCreateAgent) { + return ( + setShowCreateAgent(false)} + onAgentCreated={() => { + setShowCreateAgent(false); + loadAgents(); // Reload agents after creation + }} + /> + ); + } + + return ( +
+
+ {/* Header */} +
+
+
+

Agents

+

+ Manage your Claude Code agents +

+
+
+ + + + + + + + From File + + setShowGitHubBrowser(true)}> + + From GitHub + + + + + +
+
+
+ + {/* Toast notifications */} + + {toast && ( + + setToast(null)} + /> + + )} + + + {showGitHubBrowser && ( + setShowGitHubBrowser(false)} + onImportSuccess={() => { + loadAgents(); + setShowGitHubBrowser(false); + setToast({ message: 'Agent imported successfully', type: 'success' }); + }} + /> + )} + + + {showDeleteDialog && agentToDelete && ( + setShowDeleteDialog(false)} + > + e.stopPropagation()} + > +

Delete Agent

+

+ Are you sure you want to delete "{agentToDelete.name}"? This action cannot be undone. +

+
+ + +
+
+
+ )} +
+ + {/* Content */} +
+ + + + + Agents ({agents.length}) + + + + History ({runningAgents.length}) + + + + + {loading ? ( +
+ +
+ ) : agents.length === 0 ? ( +
+ +

No Agents Yet

+

+ Create your first agent to get started +

+ +
+ ) : ( +
+ {agents.map((agent) => ( + +
+
+ +

{agent.name}

+
+ + + + + + handleRunAgent(agent)}> + + Run + + handleExportAgent(agent)}> + + Export + + { + setAgentToDelete(agent); + setShowDeleteDialog(true); + }} + className="text-destructive" + > + + Delete + + + +
+ +

+ No description provided +

+ +
+ + v1.0.0 + + +
+
+ ))} +
+ )} +
+ + + {runningAgents.length === 0 ? ( + +
+ +

No Agent History

+

+ Run an agent to see it here +

+
+
+ ) : ( +
+ {runningAgents.map((run) => ( + +
+
+ {getStatusIcon(run.status)} +

{run.agent_name}

+ + {run.status} + +
+ +
+ +
+
+ Started: +

{new Date(run.created_at).toLocaleString()}

+
+
+ Duration: +

{run.metrics?.duration_ms ? `${(run.metrics.duration_ms / 1000).toFixed(1)}s` : run.duration_ms ? `${(run.duration_ms / 1000).toFixed(1)}s` : '—'}

+
+
+ Tokens: +

{run.metrics?.total_tokens ? run.metrics.total_tokens.toLocaleString() : run.total_tokens ? run.total_tokens.toLocaleString() : '—'}

+
+
+ + {run.status === 'failed' && ( +
+ Agent execution failed +
+ )} +
+ ))} +
+ )} +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/AgentsModal.tsx b/src/components/AgentsModal.tsx index a93d2df80..f5eb4063c 100644 --- a/src/components/AgentsModal.tsx +++ b/src/components/AgentsModal.tsx @@ -94,16 +94,35 @@ export const AgentsModal: React.FC = ({ open, onOpenChange }) }; const handleRunAgent = async (agent: Agent) => { - // Create a new agent execution tab - const tabId = `agent-exec-${agent.id}-${Date.now()}`; + // Open directory picker for project path + const { open } = await import('@tauri-apps/plugin-dialog'); - // Close modal - onOpenChange(false); - - // Dispatch event to open agent execution in the new tab - window.dispatchEvent(new CustomEvent('open-agent-execution', { - detail: { agent, tabId } - })); + try { + const projectPath = await open({ + directory: true, + multiple: false, + title: `Select project directory for ${agent.name}` + }); + + if (!projectPath) { + // User cancelled + return; + } + + // Create a new agent execution tab + const tabId = `agent-exec-${agent.id}-${Date.now()}`; + + // Close modal + onOpenChange(false); + + // Dispatch event to open agent execution in the new tab with project path + window.dispatchEvent(new CustomEvent('open-agent-execution', { + detail: { agent, tabId, projectPath } + })); + } catch (error) { + console.error('Failed to run agent:', error); + setToast({ message: `Failed to run agent: ${agent.name}`, type: 'error' }); + } }; const handleDeleteAgent = async (agent: Agent) => { diff --git a/src/components/App.cleaned.tsx b/src/components/App.cleaned.tsx index b37c0179c..a6c7000bd 100644 --- a/src/components/App.cleaned.tsx +++ b/src/components/App.cleaned.tsx @@ -8,6 +8,7 @@ import { Toast, ToastContainer } from "@/components/ui/toast"; import { TabManager } from "@/components/TabManager"; import { TabContent } from "@/components/TabContent"; import { AgentsModal } from "@/components/AgentsModal"; +import { CustomTitlebar } from "@/components/CustomTitlebar"; import { useTabState } from "@/hooks/useTabState"; /** @@ -111,8 +112,17 @@ function AppContent() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="min-h-screen bg-background flex flex-col" + className="min-h-screen bg-background flex flex-col rounded-xl overflow-hidden shadow-2xl border border-border/20" > + {/* Custom Titlebar */} + { + // Open settings tab or modal + window.dispatchEvent(new CustomEvent('create-settings-tab')); + }} + onAgentsClick={() => {}} + /> + {/* Tab-based interface */}
diff --git a/src/components/CCAgents.tsx b/src/components/CCAgents.tsx index 3f272fa2b..0f7078b10 100644 --- a/src/components/CCAgents.tsx +++ b/src/components/CCAgents.tsx @@ -308,8 +308,8 @@ export const CCAgents: React.FC = ({ onBack, className }) => {
-

CC Agents

-

+

CC Agents

+

Manage your Claude Code agents

@@ -355,8 +355,7 @@ export const CCAgents: React.FC = ({ onBack, className }) => { + className="mb-4 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-body-small text-destructive"> {error} )} @@ -381,8 +380,8 @@ export const CCAgents: React.FC = ({ onBack, className }) => { ) : agents.length === 0 ? (
-

No agents yet

-

+

No agents yet

+

Create your first CC Agent to get started

- + Page {currentPage} of {totalPages} - )}
{/* Experimental Feature Warning */} -
-
- -
-

Experimental Feature

-

+

+
+ +
+

Experimental Feature

+

Checkpointing may affect directory structure or cause data loss. Use with caution.

@@ -161,33 +164,36 @@ export const CheckpointSettings: React.FC = ({ {error && (
- - {error} + + {error}
)} {successMessage && ( - {successMessage} + {successMessage} )} -
+ {/* Main Settings Card */} + {/* Auto-checkpoint toggle */}
- -

+ +

Automatically create checkpoints based on the selected strategy

@@ -201,14 +207,14 @@ export const CheckpointSettings: React.FC = ({ {/* Checkpoint strategy */}
- + setCheckpointStrategy(value as CheckpointStrategy)} options={strategyOptions} disabled={isLoading || !autoCheckpointEnabled} /> -

+

{checkpointStrategy === "manual" && "Checkpoints will only be created manually"} {checkpointStrategy === "per_prompt" && "A checkpoint will be created after each user prompt"} {checkpointStrategy === "per_tool_use" && "A checkpoint will be created after each tool use"} @@ -217,39 +223,48 @@ export const CheckpointSettings: React.FC = ({

{/* Save button */} - -
+ + +
-
+ {/* Storage Management Card */} +
- -

- Total checkpoints: {totalCheckpoints} +

+ + +
+

+ Total checkpoints: {totalCheckpoints}

-
{/* Cleanup settings */}
- +
= ({ value={keepCount} onChange={(e) => setKeepCount(parseInt(e.target.value) || 10)} disabled={isLoading} - className="flex-1" + className="flex-1 h-9" /> - + +
-

+

Remove old checkpoints, keeping only the most recent {keepCount}

-
+ ); }; \ No newline at end of file diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index 20d32a00d..d4563e242 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -1,17 +1,13 @@ import React, { useState, useEffect, useRef, useMemo } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { - ArrowLeft, - Terminal, - FolderOpen, Copy, ChevronDown, GitBranch, - Settings, ChevronUp, X, Hash, - Command + Wrench } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -19,7 +15,6 @@ import { Label } from "@/components/ui/label"; import { Popover } from "@/components/ui/popover"; import { api, type Session } from "@/lib/api"; import { cn } from "@/lib/utils"; -import { open } from "@tauri-apps/plugin-dialog"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { StreamMessage } from "./StreamMessage"; import { FloatingPromptInput, type FloatingPromptInputRef } from "./FloatingPromptInput"; @@ -28,12 +23,13 @@ import { TimelineNavigator } from "./TimelineNavigator"; import { CheckpointSettings } from "./CheckpointSettings"; import { SlashCommandsManager } from "./SlashCommandsManager"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { TooltipProvider, TooltipSimple } from "@/components/ui/tooltip-modern"; import { SplitPane } from "@/components/ui/split-pane"; import { WebviewPreview } from "./WebviewPreview"; import type { ClaudeStreamMessage } from "./AgentExecution"; import { useVirtualizer } from "@tanstack/react-virtual"; import { useTrackEvent, useComponentMetrics, useWorkflowTracking } from "@/hooks"; +import { SessionPersistenceService } from "@/services/sessionPersistence"; interface ClaudeCodeSessionProps { /** @@ -60,6 +56,10 @@ interface ClaudeCodeSessionProps { * Callback when streaming state changes */ onStreamingChange?: (isStreaming: boolean, sessionId: string | null) => void; + /** + * Callback when project path changes + */ + onProjectPathChange?: (path: string) => void; } /** @@ -71,12 +71,11 @@ interface ClaudeCodeSessionProps { export const ClaudeCodeSession: React.FC = ({ session, initialProjectPath = "", - onBack, - onProjectSettings, className, onStreamingChange, + onProjectPathChange, }) => { - const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || ""); + const [projectPath] = useState(initialProjectPath || session?.project_path || ""); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -140,6 +139,13 @@ export const ClaudeCodeSession: React.FC = ({ // const aiTracking = useAIInteractionTracking('sonnet'); // Default model const workflowTracking = useWorkflowTracking('claude_session'); + // Call onProjectPathChange when component mounts with initial path + useEffect(() => { + if (onProjectPathChange && projectPath) { + onProjectPathChange(projectPath); + } + }, []); // Only run on mount + // Keep ref in sync with state useEffect(() => { queuedPromptsRef.current = queuedPrompts; @@ -295,6 +301,16 @@ export const ClaudeCodeSession: React.FC = ({ const history = await api.loadSessionHistory(session.id, session.project_id); + // Save session data for restoration + if (history && history.length > 0) { + SessionPersistenceService.saveSession( + session.id, + session.project_id, + session.project_path, + history.length + ); + } + // Convert history to messages format const loadedMessages: ClaudeStreamMessage[] = history.map(entry => ({ ...entry, @@ -306,6 +322,13 @@ export const ClaudeCodeSession: React.FC = ({ // After loading history, we're continuing a conversation setIsFirstPrompt(false); + + // Scroll to bottom after loading history + setTimeout(() => { + if (loadedMessages.length > 0) { + rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); + } + }, 100); } catch (err) { console.error("Failed to load session history:", err); setError("Failed to load session history"); @@ -405,24 +428,7 @@ export const ClaudeCodeSession: React.FC = ({ } }; - const handleSelectPath = async () => { - try { - const selected = await open({ - directory: true, - multiple: false, - title: "Select Project Directory" - }); - - if (selected) { - setProjectPath(selected as string); - setError(null); - } - } catch (err) { - console.error("Failed to select directory:", err); - const errorMessage = err instanceof Error ? err.message : String(err); - setError(`Failed to select directory: ${errorMessage}`); - } - }; + // Project path selection handled by parent tab controls const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => { console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath, claudeSessionId, effectiveSession }); @@ -519,6 +525,14 @@ export const ClaudeCodeSession: React.FC = ({ if (!extractedSessionInfo) { const projectId = projectPath.replace(/[^a-zA-Z0-9]/g, '-'); setExtractedSessionInfo({ sessionId: msg.session_id, projectId }); + + // Save session data for restoration + SessionPersistenceService.saveSession( + msg.session_id, + projectId, + projectPath, + messages.length + ); } // Switch to session-specific listeners @@ -1133,7 +1147,7 @@ export const ClaudeCodeSession: React.FC = ({ }} >
= ({ key={virtualItem.key} data-index={virtualItem.index} ref={(el) => el && rowVirtualizer.measureElement(el)} - initial={{ opacity: 0, y: 20 }} + initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -20 }} + exit={{ opacity: 0, y: -8 }} transition={{ duration: 0.3 }} className="absolute inset-x-4 pb-4" style={{ @@ -1170,8 +1184,9 @@ export const ClaudeCodeSession: React.FC = ({ {/* Loading indicator under the latest message */} {isLoading && (
@@ -1181,9 +1196,10 @@ export const ClaudeCodeSession: React.FC = ({ {/* Error indicator */} {error && ( {error} @@ -1191,36 +1207,7 @@ export const ClaudeCodeSession: React.FC = ({
); - const projectPathInput = !session && ( - - -
- setProjectPath(e.target.value)} - placeholder="/path/to/your/project" - className="flex-1" - disabled={isLoading} - /> - -
-
- ); + const projectPathInput = null; // Removed project path display // If preview is maximized, render only the WebviewPreview in full screen if (showPreview && isPreviewMaximized) { @@ -1228,8 +1215,8 @@ export const ClaudeCodeSession: React.FC = ({ @@ -1247,142 +1234,9 @@ export const ClaudeCodeSession: React.FC = ({ } return ( -
-
- {/* Header */} - -
- -
- -
-

Claude Code Session

-

- {projectPath ? `${projectPath}` : "No project selected"} -

-
-
-
- -
- {projectPath && onProjectSettings && ( - - )} - {projectPath && ( - - )} -
- {showSettings && ( - - )} - - - - - - -

Checkpoint Settings

-
-
-
- {effectiveSession && ( - - - - - - -

Timeline Navigator

-
-
-
- )} - {messages.length > 0 && ( - - - Copy Output - - - } - content={ -
- - -
- } - open={copyPopoverOpen} - onOpenChange={setCopyPopoverOpen} - /> - )} -
-
-
+ +
+
{/* Main Content Area */}
= ({ /> ) : ( // Original layout when no preview -
+
{projectPathInput} {messagesList} @@ -1449,17 +1303,24 @@ export const ClaudeCodeSession: React.FC = ({
Queued Prompts ({queuedPrompts.length})
- + + + + +
{!queuedPromptsCollapsed && queuedPrompts.map((queuedPrompt, index) => (
@@ -1471,14 +1332,19 @@ export const ClaudeCodeSession: React.FC = ({

{queuedPrompt.prompt}

- + + ))}
@@ -1496,59 +1362,71 @@ export const ClaudeCodeSession: React.FC = ({ className="fixed bottom-32 right-6 z-50" >
- -
- + }} + className="px-3 py-2 hover:bg-accent rounded-none" + > + + + + +
+ + + + +
)} @@ -1564,13 +1442,93 @@ export const ClaudeCodeSession: React.FC = ({ isLoading={isLoading} disabled={!projectPath} projectPath={projectPath} + extraMenuItems={ + <> + {effectiveSession && ( + + + + + + )} + {messages.length > 0 && ( + + + + + + } + content={ +
+ + +
+ } + open={copyPopoverOpen} + onOpenChange={setCopyPopoverOpen} + side="top" + align="end" + /> + )} + + + + + + + } />
{/* Token Counter - positioned under the Send button */} {totalTokens > 0 && (
-
+
= ({ initial={{ x: "100%" }} animate={{ x: 0 }} exit={{ x: "100%" }} - transition={{ type: "spring", damping: 20, stiffness: 300 }} + transition={{ duration: 0.2, ease: "easeOut" }} className="fixed right-0 top-0 h-full w-full sm:w-96 bg-background border-l border-border shadow-xl z-30 overflow-hidden" >
@@ -1708,6 +1666,7 @@ export const ClaudeCodeSession: React.FC = ({ )} -
+
+ ); }; diff --git a/src/components/ClaudeVersionSelector.tsx b/src/components/ClaudeVersionSelector.tsx index 763c09213..d78ac370c 100644 --- a/src/components/ClaudeVersionSelector.tsx +++ b/src/components/ClaudeVersionSelector.tsx @@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Label } from "@/components/ui/label"; import { api, type ClaudeInstallation } from "@/lib/api"; import { cn } from "@/lib/utils"; -import { CheckCircle, HardDrive, Settings } from "lucide-react"; +import { CheckCircle, HardDrive, Settings, Terminal, Info } from "lucide-react"; interface ClaudeVersionSelectorProps { /** @@ -33,6 +33,10 @@ interface ClaudeVersionSelectorProps { * Whether save is in progress */ isSaving?: boolean; + /** + * Simplified mode for cleaner UI + */ + simplified?: boolean; } /** @@ -52,6 +56,7 @@ export const ClaudeVersionSelector: React.FC = ({ showSaveButton = false, onSave, isSaving = false, + simplified = false, }) => { const [installations, setInstallations] = useState([]); const [loading, setLoading] = useState(true); @@ -120,15 +125,25 @@ export const ClaudeVersionSelector: React.FC = ({ const getInstallationTypeColor = (installation: ClaudeInstallation) => { switch (installation.installation_type) { case "System": - return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"; + return "default"; case "Custom": - return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300"; + return "secondary"; default: - return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"; + return "outline"; } }; if (loading) { + if (simplified) { + return ( +
+ +
+
+
+
+ ); + } return ( @@ -145,6 +160,19 @@ export const ClaudeVersionSelector: React.FC = ({ } if (error) { + if (simplified) { + return ( +
+ +
+

{error}

+ +
+
+ ); + } return ( @@ -164,6 +192,81 @@ export const ClaudeVersionSelector: React.FC = ({ const systemInstallations = installations.filter(i => i.installation_type === "System"); const customInstallations = installations.filter(i => i.installation_type === "Custom"); + // Simplified mode - more streamlined UI + if (simplified) { + return ( +
+
+
+ +

+ Select which version of Claude to use +

+
+ {selectedInstallation && ( + + {selectedInstallation.installation_type} + + )} +
+ + + + {selectedInstallation && ( +
+ +
+ Path: {selectedInstallation.path} +
+
+ )} +
+ ); + } + + // Original card-based UI return ( @@ -186,19 +289,19 @@ export const ClaudeVersionSelector: React.FC = ({
{getInstallationIcon(selectedInstallation)} {selectedInstallation.path} - + {selectedInstallation.installation_type}
)} - + {systemInstallations.length > 0 && ( <>
System Installations
{systemInstallations.map((installation) => ( - +
{getInstallationIcon(installation)}
@@ -220,7 +323,7 @@ export const ClaudeVersionSelector: React.FC = ({ <>
Custom Installations
{customInstallations.map((installation) => ( - +
{getInstallationIcon(installation)}
@@ -246,7 +349,7 @@ export const ClaudeVersionSelector: React.FC = ({
Selected Installation - + {selectedInstallation.installation_type}
diff --git a/src/components/CreateAgent.tsx b/src/components/CreateAgent.tsx index 3c1ffec87..96861e8e0 100644 --- a/src/components/CreateAgent.tsx +++ b/src/components/CreateAgent.tsx @@ -1,9 +1,10 @@ import React, { useState } from "react"; import { motion } from "framer-motion"; -import { ArrowLeft, Save, Loader2, ChevronDown } from "lucide-react"; +import { ArrowLeft, Save, Loader2, ChevronDown, Zap, AlertCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Card } from "@/components/ui/card"; import { Toast, ToastContainer } from "@/components/ui/toast"; import { api, type Agent } from "@/lib/api"; import { cn } from "@/lib/utils"; @@ -115,91 +116,108 @@ export const CreateAgent: React.FC = ({ }; return ( -
-
+ +
{/* Header */} - -
- -
-

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

-

- {isEditMode ? "Update your Claude Code agent" : "Create a new Claude Code agent"} -

+
+
+
+ + + +
+

+ {isEditMode ? "Edit Agent" : "Create New Agent"} +

+

+ {isEditMode ? "Update your Claude Code agent configuration" : "Configure a new Claude Code agent"} +

+
+ + + +
- - - +
{/* Error display */} {error && ( - {error} + + {error} )} - {/* Form */} -
- - {/* Basic Information */} -
-
-

Basic Information

-
- - {/* Name and Icon */} + {/* Content */} +
+
+ {/* Basic Information */} + +
+

Basic Information

+
- + setName(e.target.value)} placeholder="e.g., Code Assistant" required + className="h-9" />
- -
Agent Icon + setShowIconPicker(true)} - className="h-10 px-3 py-2 bg-background border border-input rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors flex items-center justify-between" + className="h-9 px-3 py-2 bg-background border border-input rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors flex items-center justify-between" >
{(() => { @@ -213,128 +231,127 @@ export const CreateAgent: React.FC = ({ })()}
-
+
{/* Model Selection */} -
- -
- + - +
+
- {/* Default Task */} + {/* Configuration */} + +

Configuration

- + setDefaultTask(e.target.value)} - className="max-w-md" + className="h-9" /> -

+

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

+
- {/* System Prompt Editor */} -
- -

- Define the behavior and capabilities of your CC Agent + {/* System Prompt */} + +

+

System Prompt

+

+ Define the behavior and capabilities of your Claude Code agent

-
- setSystemPrompt(val || "")} - preview="edit" - height={400} - visibleDragbar={false} - /> -
-
- +
+ setSystemPrompt(val || "")} + preview="edit" + height={350} + visibleDragbar={false} + /> +
+ +
- {/* Toast Notification */} - - {toast && ( - setToast(null)} - /> - )} - + {/* Toast Notification */} + + {toast && ( + setToast(null)} + /> + )} + - {/* Icon Picker Dialog */} - { - setSelectedIcon(iconName as AgentIconName); - setShowIconPicker(false); - }} - isOpen={showIconPicker} - onClose={() => setShowIconPicker(false)} - /> -
+ {/* Icon Picker Dialog */} + { + setSelectedIcon(iconName as AgentIconName); + setShowIconPicker(false); + }} + isOpen={showIconPicker} + onClose={() => setShowIconPicker(false)} + /> + ); }; diff --git a/src/components/CustomTitlebar.tsx b/src/components/CustomTitlebar.tsx new file mode 100644 index 000000000..3342959b0 --- /dev/null +++ b/src/components/CustomTitlebar.tsx @@ -0,0 +1,250 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { Settings, Minus, Square, X, Bot, BarChart3, FileText, Network, Info, MoreVertical } from 'lucide-react'; +import { getCurrentWindow } from '@tauri-apps/api/window'; +import { TooltipProvider, TooltipSimple } from '@/components/ui/tooltip-modern'; + +interface CustomTitlebarProps { + onSettingsClick?: () => void; + onAgentsClick?: () => void; + onUsageClick?: () => void; + onClaudeClick?: () => void; + onMCPClick?: () => void; + onInfoClick?: () => void; +} + +export const CustomTitlebar: React.FC = ({ + onSettingsClick, + onAgentsClick, + onUsageClick, + onClaudeClick, + onMCPClick, + onInfoClick +}) => { + const [isHovered, setIsHovered] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleMinimize = async () => { + try { + const window = getCurrentWindow(); + await window.minimize(); + console.log('Window minimized successfully'); + } catch (error) { + console.error('Failed to minimize window:', error); + } + }; + + const handleMaximize = async () => { + try { + const window = getCurrentWindow(); + const isMaximized = await window.isMaximized(); + if (isMaximized) { + await window.unmaximize(); + console.log('Window unmaximized successfully'); + } else { + await window.maximize(); + console.log('Window maximized successfully'); + } + } catch (error) { + console.error('Failed to maximize/unmaximize window:', error); + } + }; + + const handleClose = async () => { + try { + const window = getCurrentWindow(); + await window.close(); + console.log('Window closed successfully'); + } catch (error) { + console.error('Failed to close window:', error); + } + }; + + return ( + +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Left side - macOS Traffic Light buttons */} +
+
+ {/* Close button */} + + + {/* Minimize button */} + + + {/* Maximize button */} + +
+
+ + {/* Center - Title (hidden) */} + {/*
+ {title} +
*/} + + {/* Right side - Navigation icons with improved spacing */} +
+ {/* Primary actions group */} +
+ {onAgentsClick && ( + + + + + + )} + + {onUsageClick && ( + + + + + + )} +
+ + {/* Visual separator */} +
+ + {/* Secondary actions group */} +
+ {onSettingsClick && ( + + + + + + )} + + {/* Dropdown menu for additional options */} +
+ + setIsDropdownOpen(!isDropdownOpen)} + whileTap={{ scale: 0.97 }} + transition={{ duration: 0.15 }} + className="p-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-1" + > + + + + + {isDropdownOpen && ( +
+
+ {onClaudeClick && ( + + )} + + {onMCPClick && ( + + )} + + {onInfoClick && ( + + )} +
+
+ )} +
+
+
+
+ + ); +}; diff --git a/src/components/FloatingPromptInput.tsx b/src/components/FloatingPromptInput.tsx index be3507e4f..c3f5ea28c 100644 --- a/src/components/FloatingPromptInput.tsx +++ b/src/components/FloatingPromptInput.tsx @@ -8,13 +8,17 @@ import { Sparkles, Zap, Square, - Brain + Brain, + Lightbulb, + Cpu, + Rocket, + } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Popover } from "@/components/ui/popover"; import { Textarea } from "@/components/ui/textarea"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { TooltipProvider, TooltipSimple, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip-modern"; import { FilePicker } from "./FilePicker"; import { SlashCommandPicker } from "./SlashCommandPicker"; import { ImagePreview } from "./ImagePreview"; @@ -50,6 +54,10 @@ interface FloatingPromptInputProps { * Callback when cancel is clicked (only during loading) */ onCancel?: () => void; + /** + * Extra menu items to display in the prompt bar + */ + extraMenuItems?: React.ReactNode; } export interface FloatingPromptInputRef { @@ -70,6 +78,9 @@ type ThinkingModeConfig = { description: string; level: number; // 0-4 for visual indicator phrase?: string; // The phrase to append + icon: React.ReactNode; + color: string; + shortName: string; }; const THINKING_MODES: ThinkingModeConfig[] = [ @@ -77,50 +88,71 @@ const THINKING_MODES: ThinkingModeConfig[] = [ id: "auto", name: "Auto", description: "Let Claude decide", - level: 0 + level: 0, + icon: , + color: "text-muted-foreground", + shortName: "A" }, { id: "think", name: "Think", description: "Basic reasoning", level: 1, - phrase: "think" + phrase: "think", + icon: , + color: "text-primary", + shortName: "T" }, { id: "think_hard", name: "Think Hard", description: "Deeper analysis", level: 2, - phrase: "think hard" + phrase: "think hard", + icon: , + color: "text-primary", + shortName: "T+" }, { id: "think_harder", name: "Think Harder", description: "Extensive reasoning", level: 3, - phrase: "think harder" + phrase: "think harder", + icon: , + color: "text-primary", + shortName: "T++" }, { id: "ultrathink", name: "Ultrathink", description: "Maximum computation", level: 4, - phrase: "ultrathink" + phrase: "ultrathink", + icon: , + color: "text-primary", + shortName: "Ultra" } ]; /** * ThinkingModeIndicator component - Shows visual indicator bars for thinking level */ -const ThinkingModeIndicator: React.FC<{ level: number }> = ({ level }) => { +const ThinkingModeIndicator: React.FC<{ level: number; color?: string }> = ({ level, color: _color }) => { + const getBarColor = (barIndex: number) => { + if (barIndex > level) return "bg-muted"; + return "bg-primary"; + }; + return (
{[1, 2, 3, 4].map((i) => (
))} @@ -133,6 +165,8 @@ type Model = { name: string; description: string; icon: React.ReactNode; + shortName: string; + color: string; }; const MODELS: Model[] = [ @@ -140,13 +174,17 @@ const MODELS: Model[] = [ id: "sonnet", name: "Claude 4 Sonnet", description: "Faster, efficient for most tasks", - icon: + icon: , + shortName: "S", + color: "text-primary" }, { id: "opus", name: "Claude 4 Opus", description: "More capable, better for complex tasks", - icon: + icon: , + shortName: "O", + color: "text-primary" } ]; @@ -170,6 +208,7 @@ const FloatingPromptInputInner = ( projectPath, className, onCancel, + extraMenuItems, }: FloatingPromptInputProps, ref: React.Ref, ) => { @@ -190,6 +229,7 @@ const FloatingPromptInputInner = ( const textareaRef = useRef(null); const expandedTextareaRef = useRef(null); const unlistenDragDropRef = useRef<(() => void) | null>(null); + const [textareaHeight, setTextareaHeight] = useState(48); // Expose a method to add images programmatically React.useImperativeHandle( @@ -294,7 +334,16 @@ const FloatingPromptInputInner = ( const imagePaths = extractImagePaths(prompt); console.log('[useEffect] Setting embeddedImages to:', imagePaths); setEmbeddedImages(imagePaths); - }, [prompt, projectPath]); + + // Auto-resize on prompt change (handles paste, programmatic changes, etc.) + if (textareaRef.current && !isExpanded) { + textareaRef.current.style.height = 'auto'; + const scrollHeight = textareaRef.current.scrollHeight; + const newHeight = Math.min(Math.max(scrollHeight, 48), 240); + setTextareaHeight(newHeight); + textareaRef.current.style.height = `${newHeight}px`; + } + }, [prompt, projectPath, isExpanded]); // Set up Tauri drag-drop event listener useEffect(() => { @@ -396,12 +445,24 @@ const FloatingPromptInputInner = ( onSend(finalPrompt, selectedModel); setPrompt(""); setEmbeddedImages([]); + setTextareaHeight(48); // Reset height after sending } }; const handleTextChange = (e: React.ChangeEvent) => { const newValue = e.target.value; const newCursorPosition = e.target.selectionStart || 0; + + // Auto-resize textarea based on content + if (textareaRef.current && !isExpanded) { + // Reset height to auto to get the actual scrollHeight + textareaRef.current.style.height = 'auto'; + const scrollHeight = textareaRef.current.scrollHeight; + // Set min height to 48px and max to 240px (about 10 lines) + const newHeight = Math.min(Math.max(scrollHeight, 48), 240); + setTextareaHeight(newHeight); + textareaRef.current.style.height = `${newHeight}px`; + } // Check if / was just typed at the beginning of input or after whitespace if (newValue.length > prompt.length && newValue[newCursorPosition - 1] === '/') { @@ -611,6 +672,13 @@ const FloatingPromptInputInner = ( return; } + // Add keyboard shortcut for expanding + if (e.key === 'e' && (e.ctrlKey || e.metaKey) && e.shiftKey) { + e.preventDefault(); + setIsExpanded(true); + return; + } + if (e.key === "Enter" && !e.shiftKey && !isExpanded && !showFilePicker && !showSlashCommandPicker) { e.preventDefault(); handleSend(); @@ -714,6 +782,7 @@ const FloatingPromptInputInner = ( const selectedModelData = MODELS.find(m => m.id === selectedModel) || MODELS[0]; return ( + <> {/* Expanded Modal */} @@ -726,22 +795,30 @@ const FloatingPromptInputInner = ( onClick={() => setIsExpanded(false)} > e.stopPropagation()} >

Compose your prompt

- + + + + +
{/* Image previews in expanded mode */} @@ -758,7 +835,7 @@ const FloatingPromptInputInner = ( value={prompt} onChange={handleTextChange} onPaste={handlePaste} - placeholder="Type your prompt here..." + placeholder="Type your message..." className="min-h-[200px] resize-none" disabled={disabled} onDragEnter={handleDrag} @@ -771,31 +848,72 @@ const FloatingPromptInputInner = (
Model: - + setModelPickerOpen(!modelPickerOpen)} + className="gap-2" + > + + {selectedModelData.icon} + + {selectedModelData.name} + + } + content={ +
+ {MODELS.map((model) => ( + + ))} +
+ } + open={modelPickerOpen} + onOpenChange={setModelPickerOpen} + align="start" + side="top" + />
Thinking: - - - + + + + +
@@ -866,7 +992,7 @@ const FloatingPromptInputInner = ( {/* Fixed Position Input Bar */}
-
+
{/* Image previews */} {embeddedImages.length > 0 && ( )} -
-
- {/* Model Picker */} - - {selectedModelData.icon} - {selectedModelData.name} - - - } +
+
+ {/* Model & Thinking Mode Selectors - Left side, fixed at bottom */} +
+ + + + + + + +

{selectedModelData.name}

+

{selectedModelData.description}

+
+ + } content={
{MODELS.map((model) => ( @@ -916,7 +1060,11 @@ const FloatingPromptInputInner = ( selectedModel === model.id && "bg-accent" )} > -
{model.icon}
+
+ + {model.icon} + +
{model.name}
@@ -933,31 +1081,36 @@ const FloatingPromptInputInner = ( side="top" /> - {/* Thinking Mode Picker */} - + - - - -

{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}

-

{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}

-
- - - } + + + + + +

Thinking: {THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}

+

{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}

+
+ + } content={
{THINKING_MODES.map((mode) => ( @@ -973,7 +1126,9 @@ const FloatingPromptInputInner = ( selectedThinkingMode === mode.id && "bg-accent" )} > - + + {mode.icon} +
{mode.name} @@ -993,7 +1148,9 @@ const FloatingPromptInputInner = ( side="top" /> - {/* Prompt Input */} +
+ + {/* Prompt Input - Center */}