diff --git a/README.md b/README.md index 8901631..22a4c0e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,22 @@ # ChatMe 🤖 -A modern, cross-platform AI chat application built with Tauri, React, and TypeScript. ChatMe supports multiple AI providers with a beautiful, responsive interface and advanced features including voice interaction and intelligent file operations. +A modern, cross-platform AI chat application built with Tauri, React, and TypeScript. ChatMe supports multiple AI providers with a beautiful, responsive interface and advanced features including voice interaction and powerful agent mode with full system access. + +## 🆕 **What's New in v0.4.0** +- **🚀 Enhanced Agent Mode**: Execute terminal commands, launch apps, and manage processes +- **📊 Command Execution Cards**: Interactive, expandable results with timing and copy buttons +- **⌨️ Keyboard Shortcuts**: Ctrl+K to focus, Ctrl+R to repeat last command +- **🎨 Improved UI**: Better dark mode support, polished settings page +- **🔒 Permission System**: Safety checks for dangerous operations +- **💬 Natural AI Flow**: AI explains, executes, and continues in one response ### 🚀 **Key Highlights** +- **🤖 Powerful Agent Mode**: Full system access with terminal commands and app control - **🎤 Voice Interaction**: Speech-to-text input and text-to-speech output with customizable voices -- **🤖 Agentic Mode**: AI-powered file system operations with intelligent command execution - **🎯 Multi-Provider Support**: OpenAI, Google Gemini, Claude, Ollama, and custom APIs - **📱 Mobile-Optimized**: Responsive design that works beautifully on all devices - **🎨 Modern UI**: Custom title bar, dark/light themes, and smooth animations -- **⚡ Real-time Features**: Live streaming responses and interactive file browsing +- **⚡ Real-time Features**: Live streaming responses and interactive command execution ### 📱 **Cross-Platform Features** - **Touch-Friendly Controls**: Large tap targets for accessibility @@ -54,13 +62,16 @@ A modern, cross-platform AI chat application built with Tauri, React, and TypeSc - **Ollama**: Local models (Llama 2, CodeLlama, Mistral, etc.) - **Custom APIs**: Support for Mistral AI, Groq, Together AI, Perplexity, and any OpenAI-compatible API -### 🤖 **Agentic Mode** -- **File System Operations**: Intelligent file browsing, opening, and searching -- **Working Directory Management**: Set and manage the agent's working directory -- **Smart Command Execution**: AI automatically determines and executes appropriate file operations -- **Interactive File Display**: Beautiful card-based UI for file and folder listings -- **One-Click File Access**: Direct file opening with system default applications -- **Real-time Directory Navigation**: Seamless folder exploration and navigation +### 🤖 **Enhanced Agent Mode (NEW!)** +- **Full System Access**: Execute terminal commands, launch applications, and manage files +- **Natural Conversation Flow**: AI explains, executes, and continues conversation in a single response +- **Interactive Command Cards**: Expandable command execution results with timing and copy buttons +- **Terminal Command Execution**: Run npm, git, python, and any shell commands +- **Application Management**: Launch and manage system applications +- **Process Control**: List and terminate running processes +- **File Operations**: Copy, move, delete, rename files and folders +- **Permission System**: Safety checks for dangerous operations +- **Smart Context Awareness**: AI understands your working directory and project context ### 🎤 **Speech Features** - **Speech-to-Text**: Click the microphone button to dictate messages using voice input @@ -93,8 +104,9 @@ A modern, cross-platform AI chat application built with Tauri, React, and TypeSc - **Message Management**: Copy, export, and share conversations - **Chat History**: Persistent chat storage with SQLite - **Markdown Support**: Rich text rendering with syntax highlighting -- **Custom Components**: Enhanced rendering for file listings and special content +- **Custom Components**: Enhanced rendering for file listings and command executions - **Scroll Management**: Intelligent auto-scrolling and manual scroll control +- **Keyboard Shortcuts**: Ctrl+K (focus input), Ctrl+R (repeat last command), Enter (send) ### ⚙️ **Configuration & Settings** - **Provider Management**: Easy setup and switching between AI providers @@ -131,23 +143,54 @@ A modern, cross-platform AI chat application built with Tauri, React, and TypeSc ## 🤖 Agent Mode Guide ### **Enabling Agent Mode** -1. **Toggle the agent switch** in the title bar +1. **Toggle the agent switch** in the settings or title bar 2. **Set working directory** (optional) for file operations -3. **Ask natural questions** about files and folders -4. **Let the AI automatically** execute appropriate commands +3. **Ask natural questions** and the AI will execute commands +4. **Watch commands execute** in real-time with results ### **Agent Capabilities** -- **File Browsing**: "Show me the files in this folder" -- **File Opening**: "Open the README file" -- **Directory Navigation**: "Go to the src folder" -- **File Search**: "Find all TypeScript files" -- **Smart Operations**: AI determines the best action for your request - -### **Interactive File Display** -- **Card-based Layout**: Beautiful grid display of files and folders -- **One-click Actions**: Direct file opening with system applications -- **Visual Icons**: Distinct icons for files and folders -- **Responsive Grid**: Adapts to screen size (1-3 columns) +- **Terminal Commands**: "Run npm install" → Executes the command +- **Application Launch**: "Open Chrome browser" → Launches Chrome +- **File Operations**: "Copy this file to backup folder" → Performs file operation +- **Process Management**: "Show running processes" → Lists all processes +- **Git Operations**: "Check git status" → Runs git commands +- **Build & Deploy**: "Build the project" → Executes build scripts +- **System Info**: "Check Node version" → Runs system commands + +### **Command Execution Display** +- **Expandable Cards**: Click to view command output +- **Execution Time**: See how long commands took +- **Copy Button**: Copy commands with one click +- **Status Indicators**: Success/error/running states +- **Working Directory**: Shows where commands executed + +### **Keyboard Shortcuts** +- **Ctrl+K / Cmd+K**: Focus the input box from anywhere +- **Ctrl+R / Cmd+R**: Repeat the last command +- **Enter**: Send message +- **Shift+Enter**: New line in message + +### **Example Agent Mode Interactions** + +**User**: "Check if npm is installed and show me the version" +**AI**: "I'll check if npm is installed and show you the version. +[Executes: npm --version] +✅ npm version 10.2.5 is installed. This is a recent version compatible with most modern Node.js projects." + +**User**: "Run the development server" +**AI**: "I'll start the development server for you. +[Executes: npm run dev] +The server is now running on http://localhost:1420. You can open this in your browser to see your application." + +**User**: "Show me all TypeScript files in the src folder" +**AI**: "I'll search for all TypeScript files in the src directory. +[Executes: search for *.ts and *.tsx files] +Found 42 TypeScript files in your src folder. The main components are in src/components/ and pages are in src/pages/." + +**User**: "Open Chrome and go to GitHub" +**AI**: "I'll launch Chrome and navigate to GitHub for you. +[Executes: launch Chrome with https://github.com] +Chrome has been opened with GitHub loaded. You can now browse your repositories." ## 🚀 Getting Started diff --git a/images/dark.png b/images/dark.png index f0b2a1d..120e61e 100644 Binary files a/images/dark.png and b/images/dark.png differ diff --git a/images/light.png b/images/light.png index db3e3ea..ea6c349 100644 Binary files a/images/light.png and b/images/light.png differ diff --git a/package-lock.json b/package-lock.json index 46eceff..45fbaf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chatme", - "version": "0.3.2", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chatme", - "version": "0.3.2", + "version": "0.4.0", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-collapsible": "^1.1.12", diff --git a/package.json b/package.json index 44e09eb..cac998e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chatme", "private": true, - "version": "0.3.2", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c5fde43..5c48d76 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -102,11 +102,11 @@ dependencies = [ [[package]] name = "async-io" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if 1.0.3", "concurrent-queue", "futures-io", @@ -115,7 +115,7 @@ dependencies = [ "polling", "rustix", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "async-process" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", @@ -160,9 +160,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", @@ -173,7 +173,7 @@ dependencies = [ "rustix", "signal-hook-registry", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -421,11 +421,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.12" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -463,9 +463,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.36" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ "find-msvc-tools", "shlex", @@ -518,7 +518,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chatme" -version = "0.3.2" +version = "0.4.0" dependencies = [ "anyhow", "chrono", @@ -1097,11 +1097,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" dependencies = [ "serde", + "serde_core", "typeid", ] @@ -1911,9 +1912,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1921,7 +1922,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.0", ] [[package]] @@ -2315,9 +2316,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", @@ -3287,16 +3288,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if 1.0.3", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -3717,9 +3718,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8" dependencies = [ "ring", "rustls-pki-types", @@ -3856,38 +3857,50 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde-untagged" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ "erased-serde", "serde", + "serde_core", "typeid", ] +[[package]] +name = "serde_core" +version = "1.0.223" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56" dependencies = [ "proc-macro2", "quote", @@ -3907,14 +3920,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -4559,7 +4573,7 @@ dependencies = [ "unicode-segmentation", "url", "windows", - "windows-core", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -5694,7 +5708,7 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows-core", + "windows-core 0.61.2", "windows-implement", "windows-interface", ] @@ -5718,7 +5732,7 @@ checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ "thiserror 2.0.16", "windows", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -5784,7 +5798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -5796,7 +5810,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -5808,8 +5822,21 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -5818,7 +5845,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] @@ -5863,7 +5890,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] @@ -5874,8 +5901,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -5887,6 +5914,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -5896,6 +5932,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -6291,7 +6336,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows", - "windows-core", + "windows-core 0.61.2", "windows-version", "x11-dl", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 735648a..f626ab1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chatme" -version = "0.3.2" +version = "0.4.0" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 368b850..5a7eb80 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,6 +11,9 @@ "core:window:allow-close", "core:window:allow-minimize", "core:window:allow-maximize", - "core:window:allow-unmaximize" + "core:window:allow-unmaximize", + "core:event:default", + "core:event:allow-emit", + "core:event:allow-listen" ] } diff --git a/src-tauri/src/agentic.rs b/src-tauri/src/agentic.rs index 14f2ee0..f6b9f23 100644 --- a/src-tauri/src/agentic.rs +++ b/src-tauri/src/agentic.rs @@ -3,7 +3,10 @@ use std::collections::HashMap; use std::sync::{Mutex, Arc}; use anyhow::{Result, anyhow}; use crate::file_operations::{read_directory_contents, search_in_files, read_file_contents, write_file_contents, open_with_default_app}; - +use crate::system_operations::{ + get_installed_applications, launch_application, execute_terminal_command, + perform_file_operation, get_running_processes, kill_process, check_permission_level, + FileSystemOperation, FileOperationType, PermissionLevel}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AgentAction { pub action_type: String, @@ -84,6 +87,12 @@ impl AgentSession { "open_file".to_string(), "change_directory".to_string(), "get_file_info".to_string(), + "launch_application".to_string(), + "get_installed_apps".to_string(), + "execute_command".to_string(), + "file_operation".to_string(), + "get_processes".to_string(), + "kill_process".to_string(), ], } } @@ -237,6 +246,12 @@ impl AgentSession { "search_files" => self.execute_search_files(¶meters).await, "open_file" => self.execute_open_file(¶meters).await, "change_directory" => self.execute_change_directory(¶meters).await, + "launch_application" => self.execute_launch_application(¶meters).await, + "get_installed_apps" => self.execute_get_installed_apps(¶meters).await, + "execute_command" => self.execute_command(¶meters).await, + "file_operation" => self.execute_file_operation(¶meters).await, + "get_processes" => self.execute_get_processes(¶meters).await, + "kill_process" => self.execute_kill_process(¶meters).await, _ => Err(anyhow!("Unknown action type: {}", action_type)), }; @@ -361,4 +376,109 @@ impl AgentSession { Ok(serde_json::Value::String(format!("Changed directory to {}", dir_display))) } + + async fn execute_launch_application(&self, params: &HashMap) -> Result { + let app_path = params.get("path") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Missing required parameter: path"))?; + + let args = params.get("arguments") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect()); + + let pid = launch_application(app_path, args)?; + Ok(serde_json::json!({ + "success": true, + "pid": pid, + "message": format!("Launched application: {}", app_path) + })) + } + + async fn execute_get_installed_apps(&self, _params: &HashMap) -> Result { + let apps = get_installed_applications()?; + Ok(serde_json::to_value(apps)?) + } + + async fn execute_command(&self, params: &HashMap) -> Result { + let command = params.get("command") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Missing required parameter: command"))?; + + let working_dir = params.get("working_directory") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .or_else(|| { + self.current_directory.lock().ok() + .map(|dir| dir.clone()) + }); + + // Check permission level + let permission = check_permission_level("execute_command", params); + if permission.level == PermissionLevel::Dangerous { + return Err(anyhow!("Command requires explicit user permission: {}", command)); + } + + let result = execute_terminal_command(command, working_dir.as_deref())?; + Ok(serde_json::to_value(result)?) + } + + async fn execute_file_operation(&self, params: &HashMap) -> Result { + let operation_type = params.get("operation_type") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Missing required parameter: operation_type"))?; + + let source = params.get("source") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow!("Missing required parameter: source"))?; + + let destination = params.get("destination") + .and_then(|v| v.as_str()) + .map(String::from); + + let recursive = params.get("recursive") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let file_op_type = match operation_type { + "copy" => FileOperationType::Copy, + "move" => FileOperationType::Move, + "delete" => FileOperationType::Delete, + "create_directory" => FileOperationType::CreateDirectory, + "rename" => FileOperationType::Rename, + _ => return Err(anyhow!("Invalid operation type: {}", operation_type)), + }; + + let operation = FileSystemOperation { + operation_type: file_op_type, + source: source.to_string(), + destination, + recursive, + }; + + let result = perform_file_operation(&operation)?; + Ok(serde_json::Value::String(result)) + } + + async fn execute_get_processes(&self, _params: &HashMap) -> Result { + let processes = get_running_processes()?; + Ok(serde_json::to_value(processes)?) + } + + async fn execute_kill_process(&self, params: &HashMap) -> Result { + let pid = params.get("pid") + .and_then(|v| v.as_u64()) + .map(|v| v as u32) + .ok_or_else(|| anyhow!("Missing required parameter: pid"))?; + + // Check permission level + let permission = check_permission_level("kill_process", params); + if permission.level == PermissionLevel::Dangerous { + return Err(anyhow!("Killing process requires explicit user permission: PID {}", pid)); + } + + kill_process(pid)?; + Ok(serde_json::Value::String(format!("Successfully terminated process with PID: {}", pid))) + } } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d0b75af..834c707 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -5,6 +5,11 @@ use crate::file_operations::{ read_file_contents, write_file_contents, DirectoryContents, SearchResult }; use crate::agentic::{AgentSession, AgentAction, AgentCapability}; +use crate::system_operations::{ + launch_application, get_installed_applications, execute_terminal_command, + perform_file_operation, get_running_processes, kill_process, check_permission_level, + FileSystemOperation, FileOperationType, PermissionLevel, AppInfo, CommandResult, ProcessInfo +}; use tauri::{State, Emitter}; use serde_json::json; use std::collections::HashMap; @@ -422,3 +427,189 @@ pub async fn create_or_get_agent_session( Ok(session_clone) } } + +// System Operations Commands with Permission System +#[tauri::command] +pub async fn request_permission( + window: tauri::Window, + operation: String, + parameters: HashMap, +) -> Result { + let permission = check_permission_level(&operation, ¶meters); + + // Emit permission request to frontend + window.emit("permission_request", json!({ + "operation": permission.operation, + "description": permission.description, + "level": permission.level, + "details": permission.details, + })).map_err(|e| e.to_string())?; + + // In a real implementation, you would wait for user response + // For now, we'll return based on permission level + match permission.level { + PermissionLevel::Safe => Ok(true), + PermissionLevel::Moderate => Ok(true), // Should wait for user confirmation + PermissionLevel::Dangerous => Ok(false), // Should require explicit permission + } +} + +#[tauri::command] +pub async fn launch_app( + window: tauri::Window, + app_path: String, + arguments: Option>, + request_permission: bool, +) -> Result { + if request_permission { + let mut params = HashMap::new(); + params.insert("path".to_string(), json!(app_path)); + if let Some(ref args) = arguments { + params.insert("arguments".to_string(), json!(args)); + } + + let permission = check_permission_level("launch_app", ¶ms); + + // Emit permission request and wait for response + window.emit("permission_request", json!({ + "operation": permission.operation, + "description": permission.description, + "level": permission.level, + "details": permission.details, + "callback_id": "launch_app" + })).map_err(|e| e.to_string())?; + + // For now, proceed if not dangerous + if permission.level == PermissionLevel::Dangerous { + return Err("Permission denied: This operation requires explicit user permission".to_string()); + } + } + + launch_application(&app_path, arguments) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn get_installed_apps() -> Result, String> { + get_installed_applications() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn execute_command( + window: tauri::Window, + command: String, + working_directory: Option, + request_permission: bool, +) -> Result { + if request_permission { + let mut params = HashMap::new(); + params.insert("command".to_string(), json!(command)); + if let Some(ref dir) = working_directory { + params.insert("working_directory".to_string(), json!(dir)); + } + + let permission = check_permission_level("execute_command", ¶ms); + + // Emit permission request + window.emit("permission_request", json!({ + "operation": permission.operation, + "description": permission.description, + "level": permission.level, + "details": permission.details, + "callback_id": "execute_command" + })).map_err(|e| e.to_string())?; + + // Block dangerous commands without explicit permission + if permission.level == PermissionLevel::Dangerous { + return Err("Permission denied: This command requires explicit user permission".to_string()); + } + } + + execute_terminal_command(&command, working_directory.as_deref()) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn perform_file_system_operation( + window: tauri::Window, + operation_type: String, + source: String, + destination: Option, + recursive: bool, + request_permission: bool, +) -> Result { + let file_op_type = match operation_type.as_str() { + "copy" => FileOperationType::Copy, + "move" => FileOperationType::Move, + "delete" => FileOperationType::Delete, + "create_directory" => FileOperationType::CreateDirectory, + "rename" => FileOperationType::Rename, + _ => return Err(format!("Invalid operation type: {}", operation_type)), + }; + + if request_permission && matches!(file_op_type, FileOperationType::Delete) { + let mut params = HashMap::new(); + params.insert("path".to_string(), json!(source)); + + let permission = check_permission_level("delete_file", ¶ms); + + window.emit("permission_request", json!({ + "operation": permission.operation, + "description": permission.description, + "level": permission.level, + "details": permission.details, + "callback_id": "file_operation" + })).map_err(|e| e.to_string())?; + + if permission.level == PermissionLevel::Dangerous { + return Err("Permission denied: Deleting system files requires explicit permission".to_string()); + } + } + + let operation = FileSystemOperation { + operation_type: file_op_type, + source, + destination, + recursive, + }; + + perform_file_operation(&operation) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn get_processes() -> Result, String> { + get_running_processes() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn terminate_process( + window: tauri::Window, + pid: u32, + request_permission: bool, +) -> Result { + if request_permission { + let mut params = HashMap::new(); + params.insert("pid".to_string(), json!(pid)); + + let permission = check_permission_level("kill_process", ¶ms); + + window.emit("permission_request", json!({ + "operation": permission.operation, + "description": permission.description, + "level": permission.level, + "details": permission.details, + "callback_id": "kill_process" + })).map_err(|e| e.to_string())?; + + // Always require explicit permission for killing processes + return Err("Permission required: Terminating processes requires explicit user permission".to_string()); + } + + kill_process(pid) + .map_err(|e| e.to_string())?; + + Ok(format!("Successfully terminated process with PID: {}", pid)) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cca0845..5a2c5b7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod database; mod models; mod file_operations; mod agentic; +mod system_operations; use database::Database; use std::collections::HashMap; @@ -49,6 +50,14 @@ pub fn run() { commands::execute_agent_action, commands::get_agent_session, commands::create_or_get_agent_session, + // System operations with permissions + commands::request_permission, + commands::launch_app, + commands::get_installed_apps, + commands::execute_command, + commands::perform_file_system_operation, + commands::get_processes, + commands::terminate_process, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/system_operations.rs b/src-tauri/src/system_operations.rs new file mode 100644 index 0000000..26ea564 --- /dev/null +++ b/src-tauri/src/system_operations.rs @@ -0,0 +1,523 @@ +use anyhow::{Result, anyhow}; +use serde::{Deserialize, Serialize}; +use std::process::{Command, Stdio}; +use std::collections::HashMap; +use std::path::Path; +use std::fs; +use std::io::Read; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppInfo { + pub name: String, + pub path: String, + pub icon: Option, + pub description: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CommandResult { + pub stdout: String, + pub stderr: String, + pub exit_code: i32, + pub success: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FileSystemOperation { + pub operation_type: FileOperationType, + pub source: String, + pub destination: Option, + pub recursive: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum FileOperationType { + Copy, + Move, + Delete, + CreateDirectory, + Rename, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ProcessInfo { + pub pid: u32, + pub name: String, + pub memory_usage: Option, + pub cpu_usage: Option, +} + +// Permission levels for operations +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum PermissionLevel { + Safe, // No permission needed + Moderate, // Requires confirmation + Dangerous, // Requires explicit permission with warning +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OperationPermission { + pub operation: String, + pub description: String, + pub level: PermissionLevel, + pub details: HashMap, +} + +// App launching functions +pub fn launch_application(app_path: &str, args: Option>) -> Result { + let path = Path::new(app_path); + + if !path.exists() { + return Err(anyhow!("Application path does not exist: {}", app_path)); + } + + let mut command = if cfg!(target_os = "windows") { + let mut cmd = Command::new("cmd"); + cmd.args(&["/C", "start", "", app_path]); + if let Some(arguments) = args { + for arg in arguments { + cmd.arg(arg); + } + } + cmd + } else if cfg!(target_os = "macos") { + let mut cmd = Command::new("open"); + cmd.arg(app_path); + if let Some(arguments) = args { + cmd.arg("--args"); + for arg in arguments { + cmd.arg(arg); + } + } + cmd + } else { + // Linux + let mut cmd = Command::new(app_path); + if let Some(arguments) = args { + for arg in arguments { + cmd.arg(arg); + } + } + cmd + }; + + let child = command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + Ok(child.id()) +} + +// Get list of installed applications +pub fn get_installed_applications() -> Result> { + let mut apps = Vec::new(); + + if cfg!(target_os = "windows") { + // Windows: Check Program Files and common locations + let user_profile_app_data = format!("{}\\AppData\\Local", std::env::var("USERPROFILE").unwrap_or_default()); + let program_files = vec![ + "C:\\Program Files", + "C:\\Program Files (x86)", + user_profile_app_data.as_str(), + ]; + + for dir in program_files { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let name = entry.file_name().to_string_lossy().to_string(); + + // Look for .exe files in the directory + if let Ok(exe_entries) = fs::read_dir(&entry.path()) { + for exe_entry in exe_entries.flatten() { + if let Some(ext) = exe_entry.path().extension() { + if ext == "exe" { + apps.push(AppInfo { + name: name.clone(), + path: exe_entry.path().to_string_lossy().to_string(), + icon: None, + description: None, + }); + break; // Take the first .exe found + } + } + } + } + } + } + } + } + } + } else if cfg!(target_os = "macos") { + // macOS: Check Applications folder + if let Ok(entries) = fs::read_dir("/Applications") { + for entry in entries.flatten() { + if let Some(ext) = entry.path().extension() { + if ext == "app" { + let name = entry.file_name().to_string_lossy() + .replace(".app", ""); + apps.push(AppInfo { + name, + path: entry.path().to_string_lossy().to_string(), + icon: None, + description: None, + }); + } + } + } + } + } else { + // Linux: Check common directories + let home_app_dir = format!("{}/.local/share/applications", std::env::var("HOME").unwrap_or_default()); + let app_dirs = vec![ + "/usr/share/applications", + "/usr/local/share/applications", + home_app_dir.as_str(), + ]; + + for dir in app_dirs { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + if let Some(ext) = entry.path().extension() { + if ext == "desktop" { + // Parse .desktop file for app info + if let Ok(mut file) = fs::File::open(entry.path()) { + let mut contents = String::new(); + if file.read_to_string(&mut contents).is_ok() { + let mut name = String::new(); + let mut exec = String::new(); + + for line in contents.lines() { + if line.starts_with("Name=") { + name = line.replace("Name=", ""); + } else if line.starts_with("Exec=") { + exec = line.replace("Exec=", "").split_whitespace().next().unwrap_or("").to_string(); + } + } + + if !name.is_empty() && !exec.is_empty() { + apps.push(AppInfo { + name, + path: exec, + icon: None, + description: None, + }); + } + } + } + } + } + } + } + } + } + + Ok(apps) +} + +// Terminal command execution with safety checks +pub fn execute_terminal_command(command: &str, working_dir: Option<&str>) -> Result { + // Check if command is potentially dangerous + let dangerous_commands = vec![ + "rm -rf /", "format", "del /f", "deltree", + "dd if=/dev/zero", "mkfs", "fdisk" + ]; + + for dangerous in &dangerous_commands { + if command.to_lowercase().contains(dangerous) { + return Err(anyhow!("Command blocked: potentially dangerous operation detected")); + } + } + + let mut cmd = if cfg!(target_os = "windows") { + let mut c = Command::new("cmd"); + c.args(&["/C", command]); + c + } else { + let mut c = Command::new("sh"); + c.args(&["-c", command]); + c + }; + + if let Some(dir) = working_dir { + cmd.current_dir(dir); + } + + let output = cmd + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output()?; + + Ok(CommandResult { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + exit_code: output.status.code().unwrap_or(-1), + success: output.status.success(), + }) +} + +// Enhanced file operations +pub fn perform_file_operation(operation: &FileSystemOperation) -> Result { + let source_path = Path::new(&operation.source); + + match operation.operation_type { + FileOperationType::Copy => { + let dest = operation.destination.as_ref() + .ok_or_else(|| anyhow!("Destination required for copy operation"))?; + + if source_path.is_file() { + fs::copy(&operation.source, dest)?; + } else if source_path.is_dir() && operation.recursive { + copy_dir_recursive(source_path, Path::new(dest))?; + } else { + return Err(anyhow!("Source is a directory but recursive flag is not set")); + } + Ok(format!("Copied {} to {}", operation.source, dest)) + }, + + FileOperationType::Move => { + let dest = operation.destination.as_ref() + .ok_or_else(|| anyhow!("Destination required for move operation"))?; + fs::rename(&operation.source, dest)?; + Ok(format!("Moved {} to {}", operation.source, dest)) + }, + + FileOperationType::Delete => { + if source_path.is_file() { + fs::remove_file(&operation.source)?; + } else if source_path.is_dir() { + if operation.recursive { + fs::remove_dir_all(&operation.source)?; + } else { + fs::remove_dir(&operation.source)?; + } + } + Ok(format!("Deleted {}", operation.source)) + }, + + FileOperationType::CreateDirectory => { + if operation.recursive { + fs::create_dir_all(&operation.source)?; + } else { + fs::create_dir(&operation.source)?; + } + Ok(format!("Created directory {}", operation.source)) + }, + + FileOperationType::Rename => { + let dest = operation.destination.as_ref() + .ok_or_else(|| anyhow!("New name required for rename operation"))?; + fs::rename(&operation.source, dest)?; + Ok(format!("Renamed {} to {}", operation.source, dest)) + }, + } +} + +fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> { + if !dst.exists() { + fs::create_dir_all(dst)?; + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if src_path.is_dir() { + copy_dir_recursive(&src_path, &dst_path)?; + } else { + fs::copy(&src_path, &dst_path)?; + } + } + + Ok(()) +} + +// Get running processes +pub fn get_running_processes() -> Result> { + let mut processes = Vec::new(); + + if cfg!(target_os = "windows") { + let output = Command::new("wmic") + .args(&["process", "get", "ProcessId,Name,WorkingSetSize", "/format:csv"]) + .output()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines().skip(2) { // Skip headers + let parts: Vec<&str> = line.split(',').collect(); + if parts.len() >= 4 { + if let Ok(pid) = parts[2].parse::() { + let memory = parts[3].parse::().ok(); + processes.push(ProcessInfo { + pid, + name: parts[1].to_string(), + memory_usage: memory, + cpu_usage: None, + }); + } + } + } + } else { + // Unix-like systems + let output = Command::new("ps") + .args(&["aux"]) + .output()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines().skip(1) { // Skip header + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 11 { + if let Ok(pid) = parts[1].parse::() { + let cpu = parts[2].parse::().ok(); + let memory = parts[5].parse::().ok(); + processes.push(ProcessInfo { + pid, + name: parts[10].to_string(), + memory_usage: memory, + cpu_usage: cpu, + }); + } + } + } + } + + Ok(processes) +} + +// Kill a process +pub fn kill_process(pid: u32) -> Result<()> { + if cfg!(target_os = "windows") { + Command::new("taskkill") + .args(&["/F", "/PID", &pid.to_string()]) + .output()?; + } else { + Command::new("kill") + .args(&["-9", &pid.to_string()]) + .output()?; + } + + Ok(()) +} + +// Check permission level for an operation +pub fn check_permission_level(operation: &str, params: &HashMap) -> OperationPermission { + let mut details = HashMap::new(); + + match operation { + "execute_command" => { + if let Some(cmd) = params.get("command").and_then(|v| v.as_str()) { + details.insert("command".to_string(), cmd.to_string()); + + // Check for dangerous patterns + let dangerous_patterns = vec![ + "rm -rf", "del /f", "format", "fdisk", "dd if=", + "sudo", "admin", "registry", "regedit" + ]; + + let is_dangerous = dangerous_patterns.iter() + .any(|pattern| cmd.to_lowercase().contains(pattern)); + + OperationPermission { + operation: "Execute Terminal Command".to_string(), + description: format!("Execute command: {}", cmd), + level: if is_dangerous { + PermissionLevel::Dangerous + } else { + PermissionLevel::Moderate + }, + details, + } + } else { + OperationPermission { + operation: "Execute Terminal Command".to_string(), + description: "Execute unknown command".to_string(), + level: PermissionLevel::Dangerous, + details, + } + } + }, + + "launch_app" => { + if let Some(path) = params.get("path").and_then(|v| v.as_str()) { + details.insert("application".to_string(), path.to_string()); + OperationPermission { + operation: "Launch Application".to_string(), + description: format!("Launch application: {}", path), + level: PermissionLevel::Moderate, + details, + } + } else { + OperationPermission { + operation: "Launch Application".to_string(), + description: "Launch unknown application".to_string(), + level: PermissionLevel::Moderate, + details, + } + } + }, + + "delete_file" | "delete_directory" => { + if let Some(path) = params.get("path").and_then(|v| v.as_str()) { + details.insert("path".to_string(), path.to_string()); + + // Check if it's a system directory + let system_dirs = vec![ + "C:\\Windows", "C:\\Program Files", "/usr", "/bin", "/etc", + "/System", "/Library", "/Applications" + ]; + + let is_system = system_dirs.iter() + .any(|dir| path.starts_with(dir)); + + OperationPermission { + operation: "Delete File/Directory".to_string(), + description: format!("Delete: {}", path), + level: if is_system { + PermissionLevel::Dangerous + } else { + PermissionLevel::Moderate + }, + details, + } + } else { + OperationPermission { + operation: "Delete File/Directory".to_string(), + description: "Delete unknown path".to_string(), + level: PermissionLevel::Dangerous, + details, + } + } + }, + + "kill_process" => { + if let Some(pid) = params.get("pid") { + details.insert("pid".to_string(), pid.to_string()); + OperationPermission { + operation: "Kill Process".to_string(), + description: format!("Terminate process with PID: {}", pid), + level: PermissionLevel::Dangerous, + details, + } + } else { + OperationPermission { + operation: "Kill Process".to_string(), + description: "Terminate unknown process".to_string(), + level: PermissionLevel::Dangerous, + details, + } + } + }, + + _ => OperationPermission { + operation: operation.to_string(), + description: "Unknown operation".to_string(), + level: PermissionLevel::Safe, + details, + } + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3178a85..a269a30 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ChatMe", - "version": "0.3.2", + "version": "0.4.0", "identifier": "com.amanpreet.chatme", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index b056cd3..57f1971 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import AppLayout from "./components/app/app-layout"; import SettingsPage from "./pages/settings"; import HomePage from "./pages/home"; import { Toaster } from "./components/ui/sonner"; +import PermissionDialog from "./components/app/permission-dialog"; const router = createBrowserRouter([ { @@ -42,6 +43,7 @@ function App() { + diff --git a/src/components/app/agent-mode.tsx b/src/components/app/agent-mode.tsx index 55d791e..598dbe5 100644 --- a/src/components/app/agent-mode.tsx +++ b/src/components/app/agent-mode.tsx @@ -4,12 +4,15 @@ import { Label } from "../ui/label"; import { Badge } from "../ui/badge"; import { ScrollArea } from "../ui/scroll-area"; import { Switch } from "../ui/switch"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible"; import { toast } from "sonner"; -import { FaRobot, FaFolder } from "react-icons/fa"; +import { FaRobot, FaFolder, FaTerminal, FaRocket, FaCog, FaChevronDown, FaShieldAlt } from "react-icons/fa"; import { useAgent } from "../../contexts/AgentContext"; +import { useState } from "react"; export default function AgentMode() { const { isAgentActive, workingDirectory, setAgentActive, setWorkingDirectory } = useAgent(); + const [expandedSections, setExpandedSections] = useState(["overview"]); const handleWorkingDirectoryChange = (newPath: string) => { setWorkingDirectory(newPath); @@ -70,20 +73,230 @@ export default function AgentMode() { - {/* Instructions */} + {/* Capabilities Overview */} {isAgentActive && (
-
-

How Agent Mode Works

-
-
When agent mode is enabled, simply chat with natural language requests. The AI will automatically understand your intent and perform the appropriate actions.
- -
-
Example requests:
-
• "Search for function getName in all JavaScript files"
-
• "List all files in the src directory"
-
• "Find all TODO comments in the project"
-
The agent will automatically determine and execute the appropriate actions.
+ {/* Overview Section */} + { + setExpandedSections(open + ? [...expandedSections, "overview"] + : expandedSections.filter(s => s !== "overview") + ); + }} + > + +
+ + How Agent Mode Works +
+ +
+ +
+
+
When agent mode is enabled, simply chat with natural language requests. The AI will automatically understand your intent and perform the appropriate actions.
+ +
+
Quick Examples:
+
+
• "Open Chrome browser"
+
• "Run npm install in current directory"
+
• "List all running processes"
+
• "Create a new folder called 'test'"
+
+
+
+
+
+
+ + {/* File Operations */} + { + setExpandedSections(open + ? [...expandedSections, "file-ops"] + : expandedSections.filter(s => s !== "file-ops") + ); + }} + > + +
+ + File & Directory Operations +
+ +
+ +
+
+
+ Enhanced File Explorer +
+ • Copy, move, rename, and delete files/folders
+ • Create new directories
+ • Search files with regex patterns
+ • Read and write file contents
+ • Open files with default applications +
+
+
+ Example: "Copy all .js files to backup folder" +
+
+
+
+
+ + {/* Terminal Commands */} + { + setExpandedSections(open + ? [...expandedSections, "terminal"] + : expandedSections.filter(s => s !== "terminal") + ); + }} + > + +
+ + Terminal Command Execution + With Permission +
+ +
+ +
+
+
+
+ + Permission Required +
+
+ • Execute terminal/shell commands
+ • Run build scripts and automation
+ • Install packages and dependencies
+ • Git operations and version control
+ • System administration tasks +
+
+
+ ⚠️ Dangerous commands are blocked for safety +
+
+ Example: "Run npm test in the project folder" +
+
+
+
+
+ + {/* Application Control */} + { + setExpandedSections(open + ? [...expandedSections, "apps"] + : expandedSections.filter(s => s !== "apps") + ); + }} + > + +
+ + Application Control +
+ +
+ +
+
+
+ Launch & Manage Apps +
+ • Launch installed applications
+ • List all installed apps
+ • Pass arguments to applications
+ • View running processes
+ • Terminate processes (with permission) +
+
+
+ Example: "Open Visual Studio Code with the current folder" +
+
+
+
+
+ + {/* Process Management */} + { + setExpandedSections(open + ? [...expandedSections, "processes"] + : expandedSections.filter(s => s !== "processes") + ); + }} + > + +
+ + Process Management + Advanced +
+ +
+ +
+
+
+
+ + High Permission Required +
+
+ • View all running processes
+ • Monitor CPU and memory usage
+ • Terminate specific processes by PID
+ • Manage system resources +
+
+
+ 🔒 Requires explicit user permission for each action +
+
+ Example: "Show me all Chrome processes" +
+
+
+
+
+ + {/* Security Notice */} +
+
+ +
+

Security First

+

+ All potentially dangerous operations require your explicit permission. + The agent will never execute harmful commands without your approval. +

diff --git a/src/components/app/command-execution.tsx b/src/components/app/command-execution.tsx new file mode 100644 index 0000000..e3dff79 --- /dev/null +++ b/src/components/app/command-execution.tsx @@ -0,0 +1,176 @@ +import { useState } from "react"; +import { ChevronDown, ChevronRight, Terminal, CheckCircle, XCircle, AlertCircle, Copy, Check } from "lucide-react"; +import { cn } from "../../lib/utils"; +import { toast } from "sonner"; + +interface CommandExecutionProps { + command: string; + result?: any; + status: "pending" | "running" | "success" | "error"; + type?: string; + working_directory?: string; + executionTime?: number; // in milliseconds +} + +export default function CommandExecution({ + command, + result, + status, + type = "command", + working_directory, + executionTime +}: CommandExecutionProps) { + const [isExpanded, setIsExpanded] = useState(status === "error"); + const [copied, setCopied] = useState(false); + + const getStatusIcon = () => { + switch (status) { + case "pending": + return ; + case "running": + return
; + case "success": + return ; + case "error": + return ; + } + }; + + const getStatusText = () => { + switch (status) { + case "pending": + return "Pending execution..."; + case "running": + return "Running..."; + case "success": + return executionTime ? `Executed in ${formatExecutionTime(executionTime)}` : "Executed successfully"; + case "error": + return "Execution failed"; + } + }; + + const formatExecutionTime = (ms: number) => { + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; + }; + + const handleCopy = (e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(command).then(() => { + setCopied(true); + toast.success("Command copied to clipboard"); + setTimeout(() => setCopied(false), 2000); + }).catch(() => { + toast.error("Failed to copy command"); + }); + }; + + const formatResult = () => { + if (!result) return null; + + // Handle different types of results + if (type === "command" && result.stdout !== undefined) { + return ( +
+ {result.stdout && ( +
+
Output:
+
+                                {result.stdout}
+                            
+
+ )} + {result.stderr && ( +
+
Errors:
+
+                                {result.stderr}
+                            
+
+ )} + {result.exit_code !== undefined && result.exit_code !== 0 && ( +
+ Exit code: {result.exit_code} +
+ )} +
+ ); + } else if (typeof result === "string") { + return ( +
+                    {result}
+                
+ ); + } else if (typeof result === "object") { + return ( +
+                    {JSON.stringify(result, null, 2)}
+                
+ ); + } + + return null; + }; + + return ( +
+
+ + {getStatusIcon()} + {getStatusText()} +
+ +
+ + {isExpanded && ( +
+ {working_directory && ( +
+ Working directory: {working_directory} +
+ )} + {status === "pending" && ( +
+ Waiting to execute... +
+ )} + {status === "running" && ( +
+ Executing command... +
+ )} + {(status === "success" || status === "error") && formatResult()} +
+ )} +
+ ); +} diff --git a/src/components/app/custom-markdown-renderer.tsx b/src/components/app/custom-markdown-renderer.tsx index f02eb9f..a00bee6 100644 --- a/src/components/app/custom-markdown-renderer.tsx +++ b/src/components/app/custom-markdown-renderer.tsx @@ -3,15 +3,24 @@ import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; import rehypeRaw from 'rehype-raw'; import FileListDisplay from "./file-list-display"; +import CommandExecution from "./command-execution"; interface CustomMarkdownRendererProps { content: string; } export default function CustomMarkdownRenderer({ content }: CustomMarkdownRendererProps) { - // Check if content contains file-list-component + // Check if content contains custom components const fileListRegex = /<\/file-list-component>/g; - const matches = [...content.matchAll(fileListRegex)]; + const commandExecRegex = /<\/command-execution>/g; + + // Find all custom components + const allMatches = [ + ...[...content.matchAll(fileListRegex)].map(m => ({ type: 'file-list', match: m })), + ...[...content.matchAll(commandExecRegex)].map(m => ({ type: 'command', match: m })) + ].sort((a, b) => (a.match.index || 0) - (b.match.index || 0)); + + const matches = allMatches; if (matches.length === 0) { // No custom components, render with regular ReactMarkdown @@ -43,7 +52,8 @@ export default function CustomMarkdownRenderer({ content }: CustomMarkdownRender const parts = []; let lastIndex = 0; - matches.forEach((match, index) => { + matches.forEach((item, index) => { + const match = item.match; // Add text before the component if (match.index! > lastIndex) { const textBefore = content.slice(lastIndex, match.index); @@ -77,7 +87,7 @@ export default function CustomMarkdownRenderer({ content }: CustomMarkdownRender // Add the custom component try { const componentData = JSON.parse(match[1]); - if (componentData.type === 'file-list') { + if (item.type === 'file-list' && componentData.type === 'file-list') { parts.push( ); + } else if (item.type === 'command') { + parts.push( + + ); } } catch (error) { console.error('Error parsing component data:', error); diff --git a/src/components/app/input-box.tsx b/src/components/app/input-box.tsx index 7a158e9..ee29225 100644 --- a/src/components/app/input-box.tsx +++ b/src/components/app/input-box.tsx @@ -1,21 +1,33 @@ -import { useState, useRef, KeyboardEvent, useEffect } from "react"; +import { useState, useRef, KeyboardEvent, useEffect, forwardRef, useImperativeHandle } from "react"; import { Button } from "../ui/button"; import { Textarea } from "../ui/textarea"; -import { FaArrowRight, FaPaperclip, FaMicrophone, FaTimes, FaStop } from "react-icons/fa"; +import { FaArrowRight, FaPaperclip, FaMicrophone, FaTimes, FaStop, FaKeyboard } from "react-icons/fa"; import { useSpeechRecognition } from "../../hooks/use-speech-recognition"; import { toast } from "sonner"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "../ui/tooltip"; interface InputBoxProps { onSendMessage?: (message: string, images?: string[]) => void; disabled?: boolean; } -export default function InputBox({ onSendMessage, disabled = false }: InputBoxProps) { +export interface InputBoxRef { + focus: () => void; + insertText: (text: string) => void; +} + +const InputBox = forwardRef(({ onSendMessage, disabled = false }, ref) => { const [message, setMessage] = useState(""); const [, setIsTyping] = useState(false); const [selectedImages, setSelectedImages] = useState([]); const textareaRef = useRef(null); const fileInputRef = useRef(null); + const [lastCommand, setLastCommand] = useState(""); // Speech recognition setup const { @@ -37,6 +49,16 @@ export default function InputBox({ onSendMessage, disabled = false }: InputBoxPr interimResults: true }); + // Expose methods to parent component + useImperativeHandle(ref, () => ({ + focus: () => { + textareaRef.current?.focus(); + }, + insertText: (text: string) => { + setMessage(prev => prev + text); + } + })); + // Auto-resize textarea when message changes useEffect(() => { if (textareaRef.current) { @@ -45,9 +67,35 @@ export default function InputBox({ onSendMessage, disabled = false }: InputBoxPr } }, [message]); + // Global keyboard shortcuts + useEffect(() => { + const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => { + // Ctrl+K or Cmd+K to focus input + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + textareaRef.current?.focus(); + } + // Ctrl+R or Cmd+R to repeat last command (only when focused) + if ((e.ctrlKey || e.metaKey) && e.key === 'r' && document.activeElement === textareaRef.current) { + e.preventDefault(); + if (lastCommand) { + setMessage(lastCommand); + toast.info("Repeated last command"); + } + } + }; + + window.addEventListener('keydown', handleGlobalKeyDown); + return () => window.removeEventListener('keydown', handleGlobalKeyDown); + }, [lastCommand]); + const handleSend = () => { if ((message.trim() || selectedImages.length > 0) && !disabled) { - onSendMessage?.(message.trim(), selectedImages.length > 0 ? selectedImages : undefined); + const trimmedMessage = message.trim(); + if (trimmedMessage) { + setLastCommand(trimmedMessage); + } + onSendMessage?.(trimmedMessage, selectedImages.length > 0 ? selectedImages : undefined); setMessage(""); setSelectedImages([]); setIsTyping(false); @@ -180,7 +228,7 @@ export default function InputBox({ onSendMessage, disabled = false }: InputBoxPr ? "AI is generating..." : isListening ? "Listening... Speak now or click microphone to stop" - : "Type your message..." + : "Type your message... (Ctrl+K to focus)" } disabled={disabled} className="min-h-[44px] max-h-[120px] resize-none border-none !bg-transparent dark:!bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none disabled:opacity-50 leading-relaxed" @@ -237,7 +285,45 @@ export default function InputBox({ onSendMessage, disabled = false }: InputBoxPr )}
+ + {/* Keyboard shortcuts hint */} +
+ + + + + + +
+
+ Focus input: + Ctrl+K +
+
+ Repeat last: + Ctrl+R +
+
+ Send message: + Enter +
+
+ New line: + Shift+Enter +
+
+
+
+
+
); -} \ No newline at end of file +}); + +InputBox.displayName = 'InputBox'; + +export default InputBox; diff --git a/src/components/app/permission-dialog.tsx b/src/components/app/permission-dialog.tsx new file mode 100644 index 0000000..005ce0d --- /dev/null +++ b/src/components/app/permission-dialog.tsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { invoke } from "@tauri-apps/api/core"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import { Badge } from "../ui/badge"; +import { FaExclamationTriangle, FaInfoCircle, FaShieldAlt } from "react-icons/fa"; + +interface PermissionRequest { + operation: string; + description: string; + level: "Safe" | "Moderate" | "Dangerous"; + details: Record; + callback_id?: string; +} + +export default function PermissionDialog() { + const [permissionRequest, setPermissionRequest] = useState(null); + const [pendingRequests, setPendingRequests] = useState([]); + + useEffect(() => { + // Listen for permission requests from the backend + const unlisten = listen("permission_request", (event) => { + const request = event.payload; + + // If there's no current request, show it immediately + if (!permissionRequest) { + setPermissionRequest(request); + } else { + // Otherwise, queue it + setPendingRequests(prev => [...prev, request]); + } + }); + + return () => { + unlisten.then(fn => fn()); + }; + }, [permissionRequest]); + + // Process next request in queue when current one is handled + useEffect(() => { + if (!permissionRequest && pendingRequests.length > 0) { + const [next, ...remaining] = pendingRequests; + setPermissionRequest(next); + setPendingRequests(remaining); + } + }, [permissionRequest, pendingRequests]); + + const handleResponse = async (approved: boolean) => { + if (permissionRequest?.callback_id) { + try { + // Send response back to backend + await invoke("handle_permission_response", { + callbackId: permissionRequest.callback_id, + approved + }); + } catch (error) { + console.error("Failed to send permission response:", error); + } + } + setPermissionRequest(null); + }; + + if (!permissionRequest) return null; + + const getLevelIcon = () => { + switch (permissionRequest.level) { + case "Safe": + return ; + case "Moderate": + return ; + case "Dangerous": + return ; + } + }; + + const getLevelBadge = () => { + switch (permissionRequest.level) { + case "Safe": + return Safe; + case "Moderate": + return Moderate; + case "Dangerous": + return Dangerous; + } + }; + + return ( + + + + + {getLevelIcon()} + Permission Required + {getLevelBadge()} + + +
+ Operation: {permissionRequest.operation} +
+
+ Description: {permissionRequest.description} +
+ + {Object.keys(permissionRequest.details).length > 0 && ( +
+ Details: + {Object.entries(permissionRequest.details).map(([key, value]) => ( +
+ {key.replace(/_/g, " ")}:{" "} + {value} +
+ ))} +
+ )} + + {permissionRequest.level === "Dangerous" && ( +
+ ⚠️ Warning: +

+ This operation could potentially harm your system or delete important files. + Only approve if you fully understand the consequences. +

+
+ )} + + {pendingRequests.length > 0 && ( +
+ {pendingRequests.length} more permission{pendingRequests.length > 1 ? "s" : ""} pending +
+ )} +
+
+ + handleResponse(false)}> + Deny + + handleResponse(true)} + className={permissionRequest.level === "Dangerous" ? "bg-red-600 hover:bg-red-700" : ""} + > + {permissionRequest.level === "Dangerous" ? "I Understand, Allow" : "Allow"} + + +
+
+ ); +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 71ee0fe..bf7e590 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -34,7 +34,7 @@ function TooltipTrigger({ function TooltipContent({ className, - sideOffset = 0, + sideOffset = 4, children, ...props }: React.ComponentProps) { @@ -44,13 +44,13 @@ function TooltipContent({ data-slot="tooltip-content" sideOffset={sideOffset} className={cn( - "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", + "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className )} {...props} > {children} - + ) diff --git a/src/lib/agent-utils.ts b/src/lib/agent-utils.ts index cf4a450..2a57589 100644 --- a/src/lib/agent-utils.ts +++ b/src/lib/agent-utils.ts @@ -1,6 +1,21 @@ import { invoke } from '@tauri-apps/api/core'; import type { DirectoryContents, SearchResult } from './types'; +// Global session ID for agent operations +let agentSessionId: string | null = null; + +// Initialize or get agent session +async function ensureAgentSession(): Promise { + if (!agentSessionId) { + // Generate a unique session ID + agentSessionId = `agent-session-${Date.now()}-${Math.random().toString(36).substring(7)}`; + + // Create or get the session in the backend + await invoke('create_or_get_agent_session', { sessionId: agentSessionId }); + } + return agentSessionId; +} + // Function to open files/folders with default application export async function openPath(path: string): Promise { try { @@ -14,10 +29,21 @@ export async function openPath(path: string): Promise { // Function to execute Tauri commands based on LLM instructions export async function executeAgentCommand(command: string, params: any): Promise { try { + // Ensure we have an agent session for commands that need it + const sessionId = await ensureAgentSession(); + switch (command) { + // File & Directory Operations case 'get_current_directory': return await invoke('get_current_directory'); + case 'change_directory': + return await invoke('execute_agent_action', { + sessionId: sessionId, + actionType: 'change_directory', + parameters: { path: params.path } + }); + case 'read_directory': return await invoke('read_directory', { directoryPath: params.directoryPath, @@ -27,6 +53,12 @@ export async function executeAgentCommand(command: string, params: any): Promise case 'read_file': return await invoke('read_file', { filePath: params.filePath }); + case 'write_file': + return await invoke('write_file', { + path: params.path, + content: params.content + }); + case 'search_files': return await invoke('search_files', { directoryPath: params.directoryPath, @@ -38,10 +70,72 @@ export async function executeAgentCommand(command: string, params: any): Promise case 'open_file_with_default_app': return await invoke('open_file_with_default_app', { filePath: params.filePath }); + case 'file_operation': + return await invoke('execute_agent_action', { + sessionId: sessionId, + actionType: 'file_operation', + parameters: { + operation_type: params.operation_type, + source: params.source, + destination: params.destination, + recursive: params.recursive || false + } + }); + + // Terminal & Command Execution + case 'execute_command': + // This may trigger permission dialog for dangerous commands + return await invoke('execute_agent_action', { + sessionId: sessionId, + actionType: 'execute_command', + parameters: { + command: params.command, + working_directory: params.working_directory + } + }); + + // Application Management + case 'launch_application': + return await invoke('execute_agent_action', { + sessionId: sessionId, + actionType: 'launch_application', + parameters: { + path: params.path, + arguments: params.arguments || [] + } + }); + + case 'get_installed_apps': + return await invoke('execute_agent_action', { + sessionId: sessionId, + actionType: 'get_installed_apps', + parameters: {} + }); + + // Process Management + case 'get_processes': + return await invoke('execute_agent_action', { + sessionId: sessionId, + actionType: 'get_processes', + parameters: {} + }); + + case 'kill_process': + // This will trigger permission dialog + return await invoke('execute_agent_action', { + sessionId: sessionId, + actionType: 'kill_process', + parameters: { pid: params.pid } + }); + default: throw new Error(`Unknown command: ${command}`); } } catch (error) { + // Check if it's a permission denied error + if (String(error).includes('permission') || String(error).includes('denied')) { + throw new Error(`Permission denied for ${command}. User rejected the operation.`); + } throw new Error(`Failed to execute ${command}: ${error}`); } } @@ -56,39 +150,150 @@ export async function parseAndExecuteCommands(response: string): Promise while ((match = commandPattern.exec(response)) !== null) { try { const commandData = JSON.parse(match[1]); - commands.push(commandData); + commands.push({ data: commandData, originalText: match[0] }); } catch (e) { console.error('Failed to parse command:', match[1]); } } - // Execute commands and replace with results + // Execute commands and replace with expandable components let processedResponse = response; - for (const cmd of commands) { + for (const cmdInfo of commands) { + const cmd = cmdInfo.data; + let commandComponent = ''; + const startTime = Date.now(); + try { const result = await executeAgentCommand(cmd.command, cmd.params || {}); + const executionTime = Date.now() - startTime; - // Format the result based on command type - let formattedResult = ''; - - if (cmd.command === 'read_directory') { - formattedResult = formatDirectoryListing(result, cmd.params?.directoryPath || ''); + // Create command execution component based on type + if (cmd.command === 'execute_command') { + // Get the actual command string + const commandStr = cmd.params?.command || 'Unknown command'; + const workingDir = cmd.params?.working_directory; + + // Parse result if it's from AgentAction + let actualResult = result; + if (result && result.result) { + try { + actualResult = JSON.parse(result.result); + } catch { + actualResult = result.result; + } + } + + const componentData = { + command: commandStr, + result: actualResult, + status: 'success', + type: 'command', + working_directory: workingDir, + executionTime: executionTime + }; + + commandComponent = ``; + + } else if (cmd.command === 'launch_application') { + const appPath = cmd.params?.path || 'Unknown application'; + let resultMessage = 'Application launched'; + + if (result && result.result) { + try { + const res = JSON.parse(result.result); + resultMessage = res.message || resultMessage; + } catch { + resultMessage = result.result || resultMessage; + } + } + + const componentData = { + command: `launch ${appPath}`, + result: resultMessage, + status: 'success', + type: 'application', + executionTime: executionTime + }; + + commandComponent = ``; + + } else if (cmd.command === 'file_operation') { + const operation = cmd.params?.operation_type || 'operation'; + const source = cmd.params?.source || ''; + const dest = cmd.params?.destination || ''; + const commandStr = dest ? `${operation} ${source} → ${dest}` : `${operation} ${source}`; + + const componentData = { + command: commandStr, + result: result?.result || result, + status: 'success', + type: 'file_operation', + executionTime: executionTime + }; + + commandComponent = ``; + + } else if (cmd.command === 'read_directory') { + // Keep directory listing as special component + commandComponent = formatDirectoryListing(result, cmd.params?.directoryPath || ''); + } else if (cmd.command === 'search_files') { - formattedResult = formatSearchResults(result, cmd.params?.pattern || ''); + commandComponent = formatSearchResults(result, cmd.params?.pattern || ''); + } else if (cmd.command === 'read_file') { - formattedResult = `\`\`\`\n${result}\n\`\`\``; + // For file reading, show as code block + commandComponent = `\n\`\`\`\n${result}\n\`\`\`\n`; + + } else if (cmd.command === 'get_processes') { + let processes = result; + if (result && result.result) { + try { + processes = JSON.parse(result.result); + } catch { + processes = result.result; + } + } + commandComponent = formatProcessList(processes); + + } else if (cmd.command === 'get_installed_apps') { + let apps = result; + if (result && result.result) { + try { + apps = JSON.parse(result.result); + } catch { + apps = result.result; + } + } + commandComponent = formatAppsList(apps); + } else { - formattedResult = String(result); + // Default handling for other commands + const componentData = { + command: cmd.command, + result: result?.result || result, + status: 'success', + type: 'general', + executionTime: executionTime + }; + + commandComponent = ``; } - // Replace the command with the result - const commandText = `[EXECUTE:${JSON.stringify(cmd)}]`; - processedResponse = processedResponse.replace(commandText, formattedResult); + // Replace the command with the component + processedResponse = processedResponse.replace(cmdInfo.originalText, commandComponent); } catch (error) { - const commandText = `[EXECUTE:${JSON.stringify(cmd)}]`; - processedResponse = processedResponse.replace(commandText, `Error: ${error}`); + // Create error component + const componentData = { + command: cmd.command, + result: String(error), + status: 'error', + type: cmd.command + }; + + commandComponent = ``; + processedResponse = processedResponse.replace(cmdInfo.originalText, commandComponent); } } @@ -112,6 +317,57 @@ function formatDirectoryListing(result: DirectoryContents, basePath: string): st You can ask me to open any specific file or folder by name!`; } +// Format process list +function formatProcessList(processes: any[]): string { + if (!processes || processes.length === 0) { + return 'No processes found'; + } + + let formatted = `**Running Processes (${processes.length} total):**\n\n`; + formatted += '| PID | Name | Memory | CPU |\n'; + formatted += '|-----|------|--------|-----|\n'; + + // Show first 20 processes + processes.slice(0, 20).forEach(proc => { + const memory = proc.memory_usage ? `${(proc.memory_usage / 1024 / 1024).toFixed(1)} MB` : 'N/A'; + const cpu = proc.cpu_usage !== undefined ? `${proc.cpu_usage.toFixed(1)}%` : 'N/A'; + formatted += `| ${proc.pid} | ${proc.name} | ${memory} | ${cpu} |\n`; + }); + + if (processes.length > 20) { + formatted += `\n*...and ${processes.length - 20} more processes*`; + } + + return formatted; +} + +// Format apps list +function formatAppsList(apps: any[]): string { + if (!apps || apps.length === 0) { + return 'No applications found'; + } + + let formatted = `**Installed Applications (${apps.length} found):**\n\n`; + + // Group by first letter for better organization + const grouped = apps.reduce((acc, app) => { + const firstLetter = app.name[0].toUpperCase(); + if (!acc[firstLetter]) acc[firstLetter] = []; + acc[firstLetter].push(app); + return acc; + }, {} as Record); + + Object.keys(grouped).sort().forEach(letter => { + formatted += `**${letter}:**\n`; + grouped[letter].forEach((app: any) => { + formatted += ` 📦 ${app.name}\n`; + }); + }); + + formatted += `\n*Use 'launch [app name]' to open any application*`; + return formatted; +} + // Format search results function formatSearchResults(results: SearchResult[], pattern: string): string { if (!results || results.length === 0) { @@ -140,56 +396,98 @@ export async function getAvailableAgentTools(): Promise { currentDir = 'Unable to determine current directory'; } - return `You are now in AGENT MODE. You can execute Tauri commands using the following syntax: + return `You are now in AGENT MODE. You have access to powerful system capabilities to execute commands and perform operations. **COMMAND EXECUTION SYNTAX:** -Use this format to execute commands: [EXECUTE:{"command":"command_name","params":{"param1":"value1"}}] +Use this format inline in your responses: [EXECUTE:{"command":"command_name","params":{"param1":"value1"}}] +Commands will be executed and results shown as expandable components in the chat. **CURRENT WORKING DIRECTORY:** ${currentDir} **AVAILABLE COMMANDS:** +**📁 FILE & DIRECTORY OPERATIONS:** + 1. **get_current_directory** - Get current working directory Syntax: [EXECUTE:{"command":"get_current_directory"}] -2. **read_directory** - List files and folders +2. **change_directory** - Change working directory + Syntax: [EXECUTE:{"command":"change_directory","params":{"path":"C:\\\\Users\\\\Documents"}}] + +3. **read_directory** - List files and folders Syntax: [EXECUTE:{"command":"read_directory","params":{"directoryPath":"${currentDir}","recursive":false}}] -3. **read_file** - Read file contents +4. **read_file** - Read file contents Syntax: [EXECUTE:{"command":"read_file","params":{"filePath":"package.json"}}] -4. **search_files** - Search for text in files +5. **write_file** - Write content to a file + Syntax: [EXECUTE:{"command":"write_file","params":{"path":"output.txt","content":"Hello World"}}] + +6. **search_files** - Search for text in files Syntax: [EXECUTE:{"command":"search_files","params":{"directoryPath":"${currentDir}","pattern":"function","recursive":true,"maxResults":20}}] -5. **open_file_with_default_app** - Open file/folder with default app +7. **open_file_with_default_app** - Open file/folder with default app Syntax: [EXECUTE:{"command":"open_file_with_default_app","params":{"filePath":"src/"}}] +8. **file_operation** - Copy/Move/Delete/Rename files and folders + Syntax: [EXECUTE:{"command":"file_operation","params":{"operation_type":"copy","source":"file.txt","destination":"backup.txt"}}] + Operations: copy, move, delete, create_directory, rename + +**💻 TERMINAL & COMMAND EXECUTION:** + +9. **execute_command** - Execute terminal/shell commands + Syntax: [EXECUTE:{"command":"execute_command","params":{"command":"npm install","working_directory":"${currentDir}"}}] + Note: Dangerous commands require user permission + +**🚀 APPLICATION MANAGEMENT:** + +10. **launch_application** - Launch applications + Syntax: [EXECUTE:{"command":"launch_application","params":{"path":"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe","arguments":["--new-window","https://google.com"]}}] + +11. **get_installed_apps** - Get list of installed applications + Syntax: [EXECUTE:{"command":"get_installed_apps"}] + +**⚙️ PROCESS MANAGEMENT:** + +12. **get_processes** - Get list of running processes + Syntax: [EXECUTE:{"command":"get_processes"}] + +13. **kill_process** - Terminate a process by PID + Syntax: [EXECUTE:{"command":"kill_process","params":{"pid":1234}}] + Note: Requires user permission + **EXAMPLE RESPONSES:** -User: "Show me the files in the current directory" -Response: "I'll list the files in the current directory for you. +User: "Run npm install" +Response: "I'll run npm install in the current directory. + +[EXECUTE:{"command":"execute_command","params":{"command":"npm install","working_directory":"${currentDir}"}}]" + +User: "Open Chrome browser" +Response: "I'll launch Chrome for you. -[EXECUTE:{"command":"read_directory","params":{"directoryPath":"${currentDir}","recursive":false}}]" +[EXECUTE:{"command":"launch_application","params":{"path":"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe"}}]" -User: "Open the src folder" -Response: "I'll open the src folder for you. +User: "Copy this file to backup folder" +Response: "I'll copy the file to the backup folder. -[EXECUTE:{"command":"open_file_with_default_app","params":{"filePath":"${currentDir}\\\\src"}}]" +[EXECUTE:{"command":"file_operation","params":{"operation_type":"copy","source":"file.txt","destination":"backup/file.txt","recursive":false}}]" -User: "Search for JavaScript files" -Response: "I'll search for JavaScript files in the current directory. +User: "Show running processes" +Response: "I'll get the list of running processes for you. -[EXECUTE:{"command":"search_files","params":{"directoryPath":"${currentDir}","pattern":".js","recursive":true,"maxResults":20}}]" +[EXECUTE:{"command":"get_processes"}]" **IMPORTANT RULES:** +- You CAN execute terminal commands, launch applications, and perform system operations - Always use the current working directory: ${currentDir} - Use proper Windows path format with escaped backslashes in JSON: "\\\\" -- Always execute the appropriate command when users request file operations +- Always execute the appropriate command when users request operations - Use the exact syntax with proper JSON formatting - Always provide helpful context around the commands - The commands will be automatically executed and replaced with results -- Always be helpful and suggest what users can do next -- When opening files/folders, use full paths relative to current directory`; +- Dangerous operations (like killing processes or risky commands) require user permission +- Always be helpful and suggest what users can do next`; } export async function handleAgentQuery(query: string, _workingDirectory: string): Promise { diff --git a/src/pages/home.tsx b/src/pages/home.tsx index f35daea..ebffe82 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -307,13 +307,37 @@ export default function HomePage() { if (isAgentActive) { const toolsInfo = await getAvailableAgentTools(); messageForLLM = `${content.trim()}\n\n[AGENT MODE ACTIVE] -You are an AI assistant with access to Tauri commands. Execute appropriate commands based on user requests. +You are an AI assistant with FULL SYSTEM ACCESS through Tauri commands. You CAN and SHOULD execute commands, launch applications, manage files, run terminal commands, and perform system operations as requested. ${toolsInfo} Working Directory: ${workingDirectory || 'Use get_current_directory() to find current location'} -Respond with commands and helpful information.`; +IMPORTANT CONVERSATION STYLE: +1. **Explain first**: Start by acknowledging the request and explaining what you're about to do +2. **Execute inline**: Use [EXECUTE:...] commands right in your response where they make sense +3. **Continue naturally**: After showing the results, continue your explanation or provide additional help +4. **Be conversational**: Write as if you're having a natural conversation, not just listing commands + +EXAMPLE RESPONSE PATTERN: +"I'll help you check the npm packages in your project. Let me first see what's in your package.json file. + +[EXECUTE:{"command":"read_file","params":{"filePath":"package.json"}}] + +Based on your package.json, I can see you have [explain findings]. Now let me check if everything is installed: + +[EXECUTE:{"command":"execute_command","params":{"command":"npm list --depth=0"}}] + +Great! Your dependencies are installed. Would you like me to update any packages or run the development server?" + +You have the ability to: +- Execute terminal/shell commands (npm, git, python, etc.) +- Launch applications (Chrome, VS Code, etc.) +- Manage files (copy, move, delete, create) +- Control processes (list, kill) +- And much more! + +Always execute commands naturally within your response. Be helpful, informative, and proactive.`; } // sendAiMessageStreaming creates the user message, so we don't create it separately diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 0dc73c9..195e5a2 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -38,6 +38,7 @@ interface ProviderTemplate { defaultUrl?: string; defaultModels: string[]; popular?: boolean; + color: string; } const providerTemplates: ProviderTemplate[] = [ @@ -47,25 +48,28 @@ const providerTemplates: ProviderTemplate[] = [ icon: , description: "GPT-4, GPT-3.5 and other OpenAI models", defaultUrl: "https://api.openai.com/v1/chat/completions", - defaultModels: ["gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"], - popular: true + defaultModels: ["gpt-4-turbo-preview", "gpt-4", "gpt-3.5-turbo"], + popular: true, + color: "text-green-600" }, { id: "google", name: "Google Gemini", icon: , - description: "Google's Gemini models (supports both original and OpenAI-compatible APIs)", + description: "Google's Gemini models", defaultUrl: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", defaultModels: ["gemini-pro", "gemini-1.5-flash", "gemini-1.5-pro"], - popular: true + popular: true, + color: "text-blue-600" }, { id: "anthropic", name: "Anthropic Claude", icon: , - description: "Claude 3 and other Anthropic models", + description: "Claude 3 Opus, Sonnet, and Haiku", defaultUrl: "https://api.anthropic.com/v1/messages", - defaultModels: ["claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"] + defaultModels: ["claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"], + color: "text-red-600" }, { id: "ollama", @@ -73,14 +77,16 @@ const providerTemplates: ProviderTemplate[] = [ icon: , description: "Local models via Ollama", defaultUrl: "http://localhost:11434", - defaultModels: ["llama2", "codellama", "mistral", "neural-chat"] + defaultModels: ["llama2", "codellama", "mistral", "neural-chat"], + color: "text-gray-600" }, { id: "custom", name: "Custom API", icon: , - description: "OpenAI-compatible APIs (Mistral, Groq, Together AI, etc.)", - defaultModels: ["mistral-large", "mixtral-8x7b", "custom-model"] + description: "OpenAI-compatible APIs", + defaultModels: ["custom-model"], + color: "text-purple-600" } ];