├── .gitignore ├── crates ├── code_assistant │ ├── src │ │ ├── utils │ │ │ ├── mod.rs │ │ │ └── content.rs │ │ ├── mcp │ │ │ ├── mod.rs │ │ │ ├── server.rs │ │ │ └── resources.rs │ │ ├── tests │ │ │ ├── mod.rs │ │ │ ├── utils.rs │ │ │ └── sandbox_tests.rs │ │ ├── ui │ │ │ ├── terminal │ │ │ │ ├── mod.rs │ │ │ │ └── state.rs │ │ │ ├── gpui │ │ │ │ ├── assets.rs │ │ │ │ ├── content_renderer.rs │ │ │ │ └── simple_renderers.rs │ │ │ ├── mod.rs │ │ │ └── streaming │ │ │ │ └── mod.rs │ │ ├── agent │ │ │ └── mod.rs │ │ ├── app │ │ │ ├── terminal.rs │ │ │ ├── server.rs │ │ │ └── mod.rs │ │ ├── tools │ │ │ ├── core │ │ │ │ ├── result.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── spec.rs │ │ │ │ ├── render.rs │ │ │ │ ├── tool.rs │ │ │ │ └── dyn_tool.rs │ │ │ ├── mod.rs │ │ │ ├── impls │ │ │ │ └── mod.rs │ │ │ └── types.rs │ │ ├── acp │ │ │ └── mod.rs │ │ ├── permissions │ │ │ └── mod.rs │ │ ├── session │ │ │ └── mod.rs │ │ ├── main.rs │ │ └── logging.rs │ ├── assets │ │ └── icons │ │ │ ├── generic_minimize.svg │ │ │ ├── generic_maximize.svg │ │ │ ├── plus.svg │ │ │ ├── close.svg │ │ │ ├── generic_restore.svg │ │ │ ├── pencil.svg │ │ │ ├── check.svg │ │ │ ├── ellipsis.svg │ │ │ ├── arrow_right.svg │ │ │ ├── chevron_left.svg │ │ │ ├── chevron_up.svg │ │ │ ├── arrow_left.svg │ │ │ ├── chevron_down.svg │ │ │ ├── chevron_right.svg │ │ │ ├── chevron_down_small.svg │ │ │ ├── arrow_down.svg │ │ │ ├── theme_dark.svg │ │ │ ├── chevron_up_down.svg │ │ │ ├── code.svg │ │ │ ├── arrow_up.svg │ │ │ ├── library.svg │ │ │ ├── return.svg │ │ │ ├── check_circle.svg │ │ │ ├── rotate_ccw.svg │ │ │ ├── rotate_cw.svg │ │ │ ├── generic_close.svg │ │ │ ├── code-xml.svg │ │ │ ├── file_icons │ │ │ ├── elixir.svg │ │ │ ├── font.svg │ │ │ ├── file.svg │ │ │ ├── kotlin.svg │ │ │ ├── magnifying_glass.svg │ │ │ ├── css.svg │ │ │ ├── html.svg │ │ │ ├── project.svg │ │ │ ├── video.svg │ │ │ ├── toml.svg │ │ │ ├── hash.svg │ │ │ ├── folder.svg │ │ │ ├── vue.svg │ │ │ ├── graphql.svg │ │ │ ├── git.svg │ │ │ ├── code.svg │ │ │ ├── package.svg │ │ │ ├── archive.svg │ │ │ ├── prisma.svg │ │ │ ├── lock.svg │ │ │ ├── camera.svg │ │ │ ├── dart.svg │ │ │ ├── phoenix.svg │ │ │ ├── tcl.svg │ │ │ ├── database.svg │ │ │ ├── coffeescript.svg │ │ │ ├── erlang.svg │ │ │ ├── folder_open.svg │ │ │ ├── java.svg │ │ │ ├── image.svg │ │ │ ├── docker.svg │ │ │ ├── notebook.svg │ │ │ ├── ruby.svg │ │ │ ├── audio.svg │ │ │ ├── eslint.svg │ │ │ ├── info.svg │ │ │ ├── book.svg │ │ │ ├── prettier.svg │ │ │ ├── conversations.svg │ │ │ ├── fsharp.svg │ │ │ ├── bun.svg │ │ │ ├── c.svg │ │ │ ├── cpp.svg │ │ │ ├── heroku.svg │ │ │ ├── haskell.svg │ │ │ ├── terraform.svg │ │ │ ├── python.svg │ │ │ ├── r.svg │ │ │ ├── astro.svg │ │ │ ├── scala.svg │ │ │ ├── swift.svg │ │ │ ├── elm.svg │ │ │ ├── nim.svg │ │ │ ├── go.svg │ │ │ ├── typescript.svg │ │ │ ├── ocaml.svg │ │ │ ├── rust.svg │ │ │ ├── javascript.svg │ │ │ └── lua.svg │ │ │ ├── menu.svg │ │ │ ├── circle_stop.svg │ │ │ ├── ellipsis_vertical.svg │ │ │ ├── list_tree.svg │ │ │ ├── search_code.svg │ │ │ ├── delete.svg │ │ │ ├── file_generic.svg │ │ │ ├── panel_left_open.svg │ │ │ ├── panel_left_close.svg │ │ │ ├── panel_right_close.svg │ │ │ ├── panel_right_open.svg │ │ │ ├── maximize.svg │ │ │ ├── minimize.svg │ │ │ ├── file_code.svg │ │ │ ├── braces.svg │ │ │ ├── magnifying_glass.svg │ │ │ ├── reveal.svg │ │ │ ├── expand_vertical.svg │ │ │ ├── trash.svg │ │ │ ├── send.svg │ │ │ ├── square-pen.svg │ │ │ ├── settings_alt.svg │ │ │ ├── file_tree.svg │ │ │ ├── theme_light.svg │ │ │ ├── stop.svg │ │ │ ├── ai_google.svg │ │ │ ├── ai_anthropic.svg │ │ │ ├── caret_up.svg │ │ │ ├── caret_down.svg │ │ │ ├── text_snippet.svg │ │ │ ├── arrow_circle.svg │ │ │ ├── exit.svg │ │ │ ├── history_rerun.svg │ │ │ ├── brain.svg │ │ │ ├── terminal.svg │ │ │ ├── LICENSES │ │ │ ├── ai_mistral.svg │ │ │ ├── rerun.svg │ │ │ ├── person.svg │ │ │ ├── ai_open_router.svg │ │ │ ├── message_bubbles.svg │ │ │ ├── replace_next.svg │ │ │ ├── ai_sap.svg │ │ │ ├── replace_all.svg │ │ │ ├── settings.svg │ │ │ ├── link.svg │ │ │ ├── ai_open_ai.svg │ │ │ ├── ai_cerebras.svg │ │ │ ├── replace.svg │ │ │ └── ai_groq.svg │ ├── resources │ │ ├── system_prompts │ │ │ ├── mapping.json │ │ │ └── claude.md │ │ ├── tool_use_intro.md │ │ └── compaction_prompt.md │ └── Cargo.toml ├── sandbox │ ├── src │ │ ├── seatbelt_network_policy.sbpl │ │ ├── context.rs │ │ ├── seatbelt_base_policy.sbpl │ │ └── seatbelt.rs │ └── Cargo.toml ├── fs_explorer │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── web │ ├── src │ │ ├── lib.rs │ │ └── tests.rs │ └── Cargo.toml ├── command_executor │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── llm │ ├── Cargo.toml │ └── src │ ├── openrouter.rs │ ├── aicore │ ├── types.rs │ └── mod.rs │ ├── groq.rs │ ├── auth.rs │ ├── lib.rs │ └── streaming.rs ├── Cargo.toml ├── .github └── workflows │ └── build.yml ├── LICENSE ├── models.example.json └── docs └── agent-client-protocol └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /sessions 3 | mcp-config.json 4 | .code-assistant.state.json 5 | /.code-assistant-chats 6 | tarpaulin-report.html 7 | .DS_Store 8 | code-assistant 9 | -------------------------------------------------------------------------------- /crates/code_assistant/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod content; 2 | mod writer; 3 | 4 | #[cfg(test)] 5 | pub use writer::MockWriter; 6 | pub use writer::{MessageWriter, StdoutWriter}; 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/code_assistant", "crates/command_executor", "crates/fs_explorer", "crates/llm", "crates/sandbox", "crates/web"] 3 | 4 | resolver = "2" 5 | -------------------------------------------------------------------------------- /crates/code_assistant/src/mcp/mod.rs: -------------------------------------------------------------------------------- 1 | mod handler; 2 | mod resources; 3 | mod server; 4 | mod types; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | 9 | pub use server::MCPServer; 10 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod format_on_save_tests; 2 | pub mod gitignore_tests; 3 | pub mod integration_tests; 4 | pub mod mocks; 5 | pub mod sandbox_tests; 6 | pub mod utils; 7 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/generic_minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/sandbox/src/seatbelt_network_policy.sbpl: -------------------------------------------------------------------------------- 1 | (allow network-outbound 2 | (remote tcp "*:0-65535") 3 | (remote udp "*:0-65535") 4 | (remote tcp6 "*:0-65535") 5 | (remote udp6 "*:0-65535")) 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/generic_maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/fs_explorer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod encoding; 2 | mod explorer; 3 | pub mod file_updater; 4 | pub mod types; 5 | 6 | pub use explorer::{Explorer, is_path_gitignored}; 7 | pub use file_updater::*; 8 | pub use types::*; 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/sandbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sandbox" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.215", features = ["derive"] } 8 | thiserror = "1.0.69" 9 | tempfile = "3.13.0" 10 | -------------------------------------------------------------------------------- /crates/web/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod perplexity; 3 | #[cfg(test)] 4 | mod tests; 5 | pub use client::{PageMetadata, WebClient, WebPage, WebSearchResult}; 6 | pub use perplexity::{PerplexityCitation, PerplexityClient, PerplexityMessage, PerplexityResponse}; 7 | -------------------------------------------------------------------------------- /crates/code_assistant/src/ui/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod commands; 3 | pub mod input; 4 | pub mod message; 5 | pub mod renderer; 6 | pub mod state; 7 | pub mod tool_widget; 8 | pub mod ui; 9 | 10 | pub use app::TerminalTuiApp as TerminalApp; 11 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/generic_restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ellipsis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/chevron_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/chevron_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/chevron_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/chevron_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/chevron_down_small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/src/agent/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests; 3 | 4 | pub mod persistence; 5 | pub mod runner; 6 | pub mod types; 7 | 8 | pub use crate::types::ToolSyntax; 9 | // pub use persistence::FileStatePersistence; 10 | pub use runner::{Agent, AgentComponents}; 11 | pub use types::ToolExecution; 12 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/theme_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/chevron_up_down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/code.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/library.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/return.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/command_executor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command_executor" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "1.0.95" 8 | async-trait = "0.1.85" 9 | tokio = { version = "1.40.0", features = ["macros", "process", "io-util"] } 10 | sandbox = { path = "../sandbox" } 11 | tracing = "0.1" 12 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/check_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/rotate_ccw.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/rotate_cw.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/src/app/terminal.rs: -------------------------------------------------------------------------------- 1 | use super::AgentRunConfig; 2 | use crate::ui::terminal::TerminalApp; 3 | use anyhow::Result; 4 | 5 | pub async fn run(config: AgentRunConfig) -> Result<()> { 6 | // Use the new terminal UI implementation 7 | let terminal_app = TerminalApp::new(); 8 | terminal_app.run(&config).await 9 | } 10 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/generic_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/core/result.rs: -------------------------------------------------------------------------------- 1 | /// Trait for determining whether a tool execution was successful 2 | pub trait ToolResult: Send + Sync + 'static { 3 | /// Returns whether the tool execution was successful 4 | /// This is used for status reporting and can affect how the result is displayed 5 | fn is_success(&self) -> bool; 6 | } 7 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/code-xml.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/elixir.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/circle_stop.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ellipsis_vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/resources/system_prompts/mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_prompt": "default.md", 3 | "prompts": [ 4 | { 5 | "file": "claude.md", 6 | "model_substrings": [ 7 | "claude" 8 | ] 9 | }, 10 | { 11 | "file": "codex.md", 12 | "model_substrings": [ 13 | "codex" 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/list_tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/search_code.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_generic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/panel_left_open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/src/acp/mod.rs: -------------------------------------------------------------------------------- 1 | mod agent; 2 | pub mod error_handling; 3 | mod explorer; 4 | mod terminal_executor; 5 | mod types; 6 | mod ui; 7 | 8 | pub use agent::{set_acp_client_connection, ACPAgentImpl}; 9 | pub use explorer::{register_fs_worker, AcpProjectManager}; 10 | pub use terminal_executor::{register_terminal_worker, ACPTerminalCommandExecutor}; 11 | pub use ui::ACPUserUI; 12 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/panel_left_close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/panel_right_close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/panel_right_open.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/src/app/server.rs: -------------------------------------------------------------------------------- 1 | use crate::logging::setup_logging; 2 | use crate::mcp::MCPServer; 3 | use anyhow::Result; 4 | 5 | pub async fn run(verbose: bool) -> Result<()> { 6 | // Setup logging based on verbose flag 7 | setup_logging(if verbose { 1 } else { 0 }, false); 8 | 9 | // Initialize server 10 | let mut server = MCPServer::new()?; 11 | server.run().await 12 | } 13 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/kotlin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/resources/tool_use_intro.md: -------------------------------------------------------------------------------- 1 | TOOL USE 2 | 3 | You have access to a set of tools. You can use multiple tools per turn, as long as one tool would not depend on the output of a previous tool you generated in the same turn. You will receive the result of the tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use. 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_code.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/braces.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/magnifying_glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/reveal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/expand_vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/magnifying_glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/css.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/square-pen.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/core/mod.rs: -------------------------------------------------------------------------------- 1 | // Core tools implementation 2 | pub mod dyn_tool; 3 | pub mod registry; 4 | pub mod render; 5 | pub mod result; 6 | pub mod spec; 7 | pub mod tool; 8 | 9 | // Re-export all core components for easier imports 10 | pub use dyn_tool::AnyOutput; 11 | pub use registry::ToolRegistry; 12 | pub use render::{Render, ResourcesTracker}; 13 | pub use result::ToolResult; 14 | pub use spec::{ToolScope, ToolSpec}; 15 | pub use tool::{Tool, ToolContext}; 16 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/html.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/project.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/settings_alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/theme_light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/resources/compaction_prompt.md: -------------------------------------------------------------------------------- 1 | 2 | The conversation history is nearing the model's context window limit. Provide a thorough summary that allows resuming the task without the earlier messages. Include: 3 | - The current objectives or tasks. 4 | - Key actions taken so far and their outcomes. 5 | - Important files, commands, or decisions that matter for continuing. 6 | - Outstanding questions or follow-up work that still needs attention. 7 | Respond with plain text only. 8 | 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/toml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/hash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/fs_explorer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fs_explorer" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "1.0.95" 8 | async-trait = "0.1.85" 9 | command_executor = { path = "../command_executor" } 10 | content_inspector = "0.2.4" 11 | encoding_rs = "0.8.35" 12 | glob = "0.3.1" 13 | ignore = "0.4.23" 14 | regex = "1.11.1" 15 | serde = { version = "1.0.215", features = ["derive"] } 16 | tracing = "0.1.40" 17 | path-clean = "1.0.1" 18 | 19 | [dev-dependencies] 20 | tempfile = "3.13.0" 21 | tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } 22 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/vue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_anthropic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/caret_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/caret_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /crates/web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web" 3 | version = "0.1.19" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0" 8 | async-trait = "0.1" 9 | chromiumoxide = { version = "0.5", features = ["tokio-runtime"] } 10 | futures = "0.3" 11 | htmd = "0.1.6" 12 | percent-encoding = "2.3" 13 | regex = "1.12" 14 | reqwest = { version = "0.11", features = ["json", "stream"] } 15 | scraper = "0.18" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | tempfile = "3.23" 19 | tokio = { version = "1.48", features = ["full"] } 20 | url = "2.5" 21 | 22 | [dev-dependencies] 23 | axum = "0.7" 24 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/graphql.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/git.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/text_snippet.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/package.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tests/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::tools::{ParserRegistry, ToolRequest}; 2 | use crate::types::ToolSyntax; 3 | use anyhow::Result; 4 | 5 | /// Parse tool requests from LLM response and return both requests and truncated response after first tool 6 | /// This is a wrapper that defaults to XML parsing for backward compatibility 7 | pub fn parse_and_truncate_llm_response( 8 | response: &llm::LLMResponse, 9 | request_id: u64, 10 | ) -> Result<(Vec, llm::LLMResponse)> { 11 | // Default to XML parser for backward compatibility with existing tests 12 | let parser = ParserRegistry::get(ToolSyntax::Xml); 13 | parser.extract_requests(response, request_id, 0) 14 | } 15 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/prisma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acp; 2 | pub mod gpui; 3 | pub mod server; 4 | pub mod terminal; 5 | 6 | use crate::types::ToolSyntax; 7 | use sandbox::SandboxPolicy; 8 | 9 | use std::path::PathBuf; 10 | 11 | /// Configuration for running the agent in either terminal or GPUI mode 12 | #[derive(Debug, Clone)] 13 | pub struct AgentRunConfig { 14 | pub path: PathBuf, 15 | pub task: Option, 16 | pub continue_task: bool, 17 | pub model: String, 18 | pub tool_syntax: ToolSyntax, 19 | pub use_diff_format: bool, 20 | pub record: Option, 21 | pub playback: Option, 22 | pub fast_playback: bool, 23 | pub sandbox_policy: SandboxPolicy, 24 | } 25 | -------------------------------------------------------------------------------- /crates/llm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "llm" 3 | version = "0.1.19" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0" 8 | async-trait = "0.1" 9 | serde = { version = "1.0", features = ["derive"] } 10 | serde_json = "1.0" 11 | reqwest = { version = "0.11", features = ["json", "stream"] } 12 | tokio = { version = "1.48", features = ["full"] } 13 | futures = "0.3" 14 | tracing = "0.1" 15 | oauth2 = "4.4" 16 | base64 = "0.21" 17 | keyring = "2.3" 18 | chrono = { version = "0.4", features = ["serde"] } 19 | tempfile = "3.23" 20 | thiserror = "1.0" 21 | regex = "1.12" 22 | dirs = "5.0" 23 | clap = { version = "4.5", features = ["derive"] } 24 | 25 | [dev-dependencies] 26 | axum = "0.7" 27 | bytes = "1.10" 28 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/dart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/phoenix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/tcl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/database.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | // Original tools implementation 2 | mod parse; 3 | mod types; 4 | 5 | // Parser registry for different tool syntaxes 6 | pub mod parser_registry; 7 | 8 | // System message generation 9 | pub mod system_message; 10 | 11 | // Tool use filtering system 12 | pub mod tool_use_filter; 13 | 14 | // Tool formatter system 15 | pub mod formatter; 16 | 17 | // New trait-based tools implementation 18 | pub mod core; 19 | pub mod impls; 20 | 21 | #[cfg(test)] 22 | mod tests; 23 | 24 | pub use parse::{parse_caret_tool_invocations, parse_xml_tool_invocations}; 25 | pub use parser_registry::ParserRegistry; 26 | pub use system_message::generate_system_message; 27 | pub use types::{AnnotatedToolDefinition, ParseError, ToolRequest}; 28 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/coffeescript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/erlang.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/arrow_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/folder_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/history_rerun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/brain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | # - name: Install dependencies 19 | # run: | 20 | # sudo apt-get update 21 | # sudo apt-get install -y libxcb1-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-dev 22 | - name: Check formatting 23 | run: cargo fmt --all -- --check 24 | - name: Lint 25 | run: cargo clippy --locked --all-targets --all-features -- -D warnings 26 | - name: Build 27 | run: cargo build --locked --verbose 28 | - name: Run tests 29 | run: cargo test --locked --verbose 30 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/java.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/docker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/notebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/ruby.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/code_assistant/src/ui/gpui/assets.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use gpui::{AssetSource, Result, SharedString}; 3 | use rust_embed::RustEmbed; 4 | 5 | #[derive(RustEmbed)] 6 | #[folder = "assets"] 7 | #[include = "icons/**/*"] 8 | #[exclude = "*.DS_Store"] 9 | pub struct Assets; 10 | 11 | impl AssetSource for Assets { 12 | fn load(&self, path: &str) -> Result>> { 13 | Self::get(path) 14 | .map(|f| Some(f.data)) 15 | .ok_or_else(|| anyhow!("could not find asset at path \"{path}\"")) 16 | } 17 | 18 | fn list(&self, path: &str) -> Result> { 19 | Ok(Self::iter() 20 | .filter_map(|p| { 21 | if p.starts_with(path) { 22 | Some(p.into()) 23 | } else { 24 | None 25 | } 26 | }) 27 | .collect()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/LICENSES: -------------------------------------------------------------------------------- 1 | Lucide License 2 | 3 | ISC License 4 | 5 | Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. 6 | 7 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 10 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/eslint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_mistral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/rerun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/book.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/web/src/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | use super::WebClient; 3 | 4 | #[tokio::test] 5 | async fn test_web_search() { 6 | let client = WebClient::new().await.unwrap(); 7 | let results = client.search("rust programming", 1).await.unwrap(); 8 | 9 | println!("\nSearch Results:"); 10 | for (i, result) in results.iter().enumerate() { 11 | println!("\n{}. {}", i + 1, result.title); 12 | println!(" URL: {}", result.url); 13 | println!(" Snippet: {}", result.snippet); 14 | } 15 | 16 | assert!(!results.is_empty()); 17 | assert!(!results[0].url.is_empty()); 18 | assert!(!results[0].title.is_empty()); 19 | assert!(!results[0].snippet.is_empty()); 20 | } 21 | 22 | #[tokio::test] 23 | async fn test_web_fetch() { 24 | let client = WebClient::new().await.unwrap(); 25 | let page = client.fetch("https://www.rust-lang.org").await.unwrap(); 26 | 27 | println!("\nContent: {}", page.content); 28 | 29 | assert!(!page.content.is_empty()); 30 | assert!(page.content.contains("Rust")); 31 | } 32 | -------------------------------------------------------------------------------- /crates/code_assistant/src/permissions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acp; 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use std::path::Path; 6 | 7 | pub use acp::AcpPermissionMediator; 8 | 9 | /// Context about why permission is being requested. 10 | #[derive(Debug)] 11 | pub enum PermissionRequestReason<'a> { 12 | ExecuteCommand { 13 | command_line: &'a str, 14 | working_dir: Option<&'a Path>, 15 | }, 16 | } 17 | 18 | /// Request payload passed to a [`PermissionMediator`]. 19 | #[derive(Debug)] 20 | pub struct PermissionRequest<'a> { 21 | pub tool_id: Option<&'a str>, 22 | pub tool_name: &'a str, 23 | pub reason: PermissionRequestReason<'a>, 24 | } 25 | 26 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 27 | pub enum PermissionDecision { 28 | GrantedOnce, 29 | GrantedSession, 30 | Denied, 31 | } 32 | 33 | #[async_trait] 34 | pub trait PermissionMediator: Send + Sync { 35 | async fn request_permission( 36 | &self, 37 | request: PermissionRequest<'_>, 38 | ) -> Result; 39 | } 40 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/prettier.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/impls/mod.rs: -------------------------------------------------------------------------------- 1 | // Tool implementations 2 | pub mod delete_files; 3 | pub mod edit; 4 | pub mod execute_command; 5 | pub mod glob_files; 6 | pub mod list_files; 7 | pub mod list_projects; 8 | pub mod name_session; 9 | pub mod perplexity_ask; 10 | pub mod read_files; 11 | pub mod replace_in_file; 12 | pub mod search_files; 13 | pub mod update_plan; 14 | pub mod web_fetch; 15 | pub mod web_search; 16 | pub mod write_file; 17 | 18 | // Re-export all tools for registration 19 | pub use delete_files::DeleteFilesTool; 20 | pub use edit::EditTool; 21 | pub use execute_command::ExecuteCommandTool; 22 | pub use glob_files::GlobFilesTool; 23 | pub use list_files::ListFilesTool; 24 | pub use list_projects::ListProjectsTool; 25 | pub use name_session::NameSessionTool; 26 | pub use perplexity_ask::PerplexityAskTool; 27 | pub use read_files::ReadFilesTool; 28 | pub use replace_in_file::ReplaceInFileTool; 29 | pub use search_files::SearchFilesTool; 30 | pub use update_plan::UpdatePlanTool; 31 | pub use web_fetch::WebFetchTool; 32 | pub use web_search::WebSearchTool; 33 | pub use write_file::WriteFileTool; 34 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_open_router.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/message_bubbles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Stephan Aßmus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/conversations.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/fsharp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/bun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /crates/llm/src/openrouter.rs: -------------------------------------------------------------------------------- 1 | use super::openai::OpenAIClient; 2 | use crate::{types::*, LLMProvider, StreamingCallback}; 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | 6 | pub struct OpenRouterClient { 7 | inner: OpenAIClient, 8 | } 9 | 10 | impl OpenRouterClient { 11 | pub fn default_base_url() -> String { 12 | "https://openrouter.ai/api/v1".to_string() 13 | } 14 | 15 | pub fn new(api_key: String, model: String, base_url: String) -> Self { 16 | Self { 17 | inner: OpenAIClient::new(api_key, model, base_url), 18 | } 19 | } 20 | 21 | /// Set custom model configuration to be merged into API requests 22 | pub fn with_custom_config(mut self, custom_config: serde_json::Value) -> Self { 23 | self.inner = self.inner.with_custom_config(custom_config); 24 | self 25 | } 26 | } 27 | 28 | #[async_trait] 29 | impl LLMProvider for OpenRouterClient { 30 | async fn send_message( 31 | &mut self, 32 | request: LLMRequest, 33 | streaming_callback: Option<&StreamingCallback>, 34 | ) -> Result { 35 | // Delegate to inner OpenAI client since the APIs are compatible 36 | self.inner.send_message(request, streaming_callback).await 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/code_assistant/src/utils/content.rs: -------------------------------------------------------------------------------- 1 | use crate::persistence::DraftAttachment; 2 | use llm::ContentBlock; 3 | use std::time::SystemTime; 4 | 5 | pub fn content_blocks_from(message: &str, attachments: &[DraftAttachment]) -> Vec { 6 | let mut blocks = Vec::new(); 7 | 8 | if !message.is_empty() { 9 | blocks.push(ContentBlock::new_text(message.to_owned())); 10 | } 11 | 12 | for attachment in attachments { 13 | match attachment { 14 | DraftAttachment::Image { content, mime_type } => { 15 | blocks.push(ContentBlock::Image { 16 | media_type: mime_type.clone(), 17 | data: content.clone(), 18 | start_time: Some(SystemTime::now()), 19 | end_time: None, 20 | }); 21 | } 22 | DraftAttachment::Text { content } => { 23 | blocks.push(ContentBlock::new_text(content.clone())); 24 | } 25 | DraftAttachment::File { 26 | content, filename, .. 27 | } => { 28 | blocks.push(ContentBlock::new_text(format!( 29 | "File: {filename}\n{content}" 30 | ))); 31 | } 32 | } 33 | } 34 | 35 | blocks 36 | } 37 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/core/spec.rs: -------------------------------------------------------------------------------- 1 | /// Define available modes for tools 2 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 3 | pub enum ToolScope { 4 | /// Tool can be used in the MCP server 5 | McpServer, 6 | /// Tool can be used in the message history agent 7 | Agent, 8 | /// Tool can be used in the agent when configured for diff blocks format 9 | AgentWithDiffBlocks, 10 | } 11 | 12 | /// Specification for a tool, including metadata 13 | #[derive(Clone)] 14 | pub struct ToolSpec { 15 | /// Unique name of the tool 16 | pub name: &'static str, 17 | /// Detailed description of what the tool does 18 | pub description: &'static str, 19 | /// JSON Schema for the tool's parameters 20 | pub parameters_schema: serde_json::Value, 21 | /// Optional annotations for LLM-specific instructions 22 | pub annotations: Option, 23 | /// Which execution modes this tool supports 24 | pub supported_scopes: &'static [ToolScope], 25 | /// Whether this tool should be hidden from UI display 26 | pub hidden: bool, 27 | /// Optional template for generating dynamic titles from parameters 28 | /// Use {parameter_name} placeholders, e.g. "Reading {paths}" or "Searching for '{regex}'" 29 | pub title_template: Option<&'static str>, 30 | } 31 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/cpp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/heroku.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/core/render.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | /// Responsible for formatting tool outputs for display 4 | pub trait Render: Send + Sync + 'static { 5 | /// Generate a short status message for display in action history 6 | fn status(&self) -> String; 7 | 8 | /// Format the detailed output, with awareness of other tool results 9 | /// The resources_tracker helps detect and handle redundant output 10 | fn render(&self, resources_tracker: &mut ResourcesTracker) -> String; 11 | } 12 | 13 | /// Tracks resources that have been included in tool outputs to prevent redundant display 14 | pub struct ResourcesTracker { 15 | /// Set of already rendered resource identifiers 16 | rendered_resources: HashSet, 17 | } 18 | 19 | impl ResourcesTracker { 20 | /// Create a new empty resources tracker 21 | pub fn new() -> Self { 22 | Self { 23 | rendered_resources: HashSet::new(), 24 | } 25 | } 26 | 27 | /// Check if a resource has already been rendered 28 | pub fn is_rendered(&self, resource_id: &str) -> bool { 29 | self.rendered_resources.contains(resource_id) 30 | } 31 | 32 | /// Mark a resource as rendered to prevent duplicate display 33 | pub fn mark_rendered(&mut self, resource_id: String) { 34 | self.rendered_resources.insert(resource_id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/haskell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/terraform.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/code_assistant/src/ui/gpui/content_renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::gpui::parameter_renderers::ParameterRenderer; 2 | use gpui::{div, px, rgba, Element, FontWeight, ParentElement, Styled}; 3 | 4 | /// Renderer for the "content" parameter of the "write_file" tool 5 | pub struct ContentRenderer; 6 | 7 | impl ParameterRenderer for ContentRenderer { 8 | fn supported_parameters(&self) -> Vec<(String, String)> { 9 | vec![("write_file".to_string(), "content".to_string())] 10 | } 11 | 12 | fn render( 13 | &self, 14 | _tool_name: &str, 15 | _param_name: &str, 16 | param_value: &str, 17 | theme: &gpui_component::theme::Theme, 18 | ) -> gpui::AnyElement { 19 | // Container for the content - no parameter name shown 20 | div() 21 | .rounded_md() 22 | .bg(if theme.is_dark() { 23 | rgba(0x0A0A0AFF) // Darker background in Dark Mode 24 | } else { 25 | rgba(0xEAEAEAFF) // Lighter background in Light Mode 26 | }) 27 | .p_2() 28 | .text_size(px(15.)) 29 | .font_weight(FontWeight(500.0)) 30 | .child(param_value.to_string()) 31 | .into_any() 32 | } 33 | 34 | fn is_full_width(&self, _tool_name: &str, _param_name: &str) -> bool { 35 | true // Content parameter is always full-width 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/python.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crates/code_assistant/src/mcp/server.rs: -------------------------------------------------------------------------------- 1 | use crate::mcp::handler::MessageHandler; 2 | use anyhow::Result; 3 | use tokio::io::{stdin, AsyncBufReadExt, BufReader}; 4 | use tracing::{debug, error, trace}; 5 | 6 | pub struct MCPServer { 7 | handler: MessageHandler, 8 | } 9 | 10 | impl MCPServer { 11 | pub fn new() -> Result { 12 | Ok(Self { 13 | handler: MessageHandler::new(tokio::io::stdout())?, 14 | }) 15 | } 16 | 17 | pub async fn run(&mut self) -> Result<()> { 18 | debug!("Starting MCP server using stdio transport"); 19 | 20 | let stdin = stdin(); 21 | let mut reader = BufReader::new(stdin); 22 | 23 | let mut line = String::new(); 24 | while let Ok(n) = reader.read_line(&mut line).await { 25 | if n == 0 { 26 | break; // EOF 27 | } 28 | 29 | let trimmed = line.trim(); 30 | trace!("Received message: {}", trimmed); 31 | 32 | // Process the message 33 | match self.handler.handle_message(trimmed).await { 34 | Ok(()) => { 35 | trace!("Message processed successfully"); 36 | } 37 | Err(e) => { 38 | error!("Error handling message: {}", e); 39 | } 40 | } 41 | 42 | line.clear(); 43 | } 44 | 45 | debug!("MCP server shutting down"); 46 | Ok(()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/code_assistant/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod backend; 2 | pub mod gpui; 3 | pub mod streaming; 4 | pub mod terminal; 5 | pub mod ui_events; 6 | use async_trait::async_trait; 7 | pub use streaming::DisplayFragment; 8 | use thiserror::Error; 9 | pub use ui_events::UiEvent; 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq)] 12 | pub enum ToolStatus { 13 | Pending, // Default status when a tool appears in the stream 14 | Running, // Tool is currently being executed 15 | Success, // Execution was successful 16 | Error, // Error during execution 17 | } 18 | 19 | #[derive(Error, Debug)] 20 | pub enum UIError { 21 | #[error("IO error: {0}")] 22 | IOError(#[from] std::io::Error), 23 | } 24 | 25 | #[async_trait] 26 | pub trait UserInterface: Send + Sync { 27 | /// Send an event to the UI 28 | async fn send_event(&self, event: UiEvent) -> Result<(), UIError>; 29 | 30 | /// Display a streaming fragment with specific type information 31 | fn display_fragment(&self, fragment: &DisplayFragment) -> Result<(), UIError>; 32 | 33 | /// Check if streaming should continue 34 | fn should_streaming_continue(&self) -> bool; 35 | 36 | /// Notify the UI about rate limiting and countdown 37 | fn notify_rate_limit(&self, seconds_remaining: u64); 38 | 39 | /// Clear rate limit notification 40 | fn clear_rate_limit(&self); 41 | 42 | /// Downcast to Any for accessing concrete type methods 43 | #[allow(dead_code)] 44 | fn as_any(&self) -> &dyn std::any::Any; 45 | } 46 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/r.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/astro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/llm/src/aicore/types.rs: -------------------------------------------------------------------------------- 1 | //! Types for AI Core provider configuration 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Specifies which vendor API type to use for an AI Core deployment 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] 7 | #[serde(rename_all = "lowercase")] 8 | pub enum AiCoreApiType { 9 | /// Anthropic Claude API (Bedrock-style invoke/converse endpoints) 10 | #[default] 11 | Anthropic, 12 | /// OpenAI Chat Completions API 13 | OpenAI, 14 | /// Google Vertex AI / Gemini API 15 | Vertex, 16 | } 17 | 18 | impl std::fmt::Display for AiCoreApiType { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | match self { 21 | AiCoreApiType::Anthropic => write!(f, "anthropic"), 22 | AiCoreApiType::OpenAI => write!(f, "openai"), 23 | AiCoreApiType::Vertex => write!(f, "vertex"), 24 | } 25 | } 26 | } 27 | 28 | impl std::str::FromStr for AiCoreApiType { 29 | type Err = anyhow::Error; 30 | 31 | fn from_str(s: &str) -> Result { 32 | match s.to_lowercase().as_str() { 33 | "anthropic" => Ok(AiCoreApiType::Anthropic), 34 | "openai" => Ok(AiCoreApiType::OpenAI), 35 | "vertex" => Ok(AiCoreApiType::Vertex), 36 | _ => Err(anyhow::anyhow!( 37 | "Unknown AI Core API type: '{}'. Expected one of: anthropic, openai, vertex", 38 | s 39 | )), 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/replace_next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_sap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/scala.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/replace_all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/swift.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/sandbox/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::{Path, PathBuf}; 3 | use std::sync::{Arc, RwLock}; 4 | 5 | /// Tracks canonical sandbox roots that are allowed for a session. 6 | #[derive(Clone, Default)] 7 | pub struct SandboxContext { 8 | roots: Arc>>, 9 | } 10 | 11 | impl SandboxContext { 12 | pub fn new() -> Self { 13 | Self::default() 14 | } 15 | 16 | /// Registers a root path, canonicalizing it and avoiding duplicates. 17 | pub fn register_root>(&self, path: P) -> io::Result { 18 | let canonical = path 19 | .as_ref() 20 | .canonicalize() 21 | .unwrap_or_else(|_| path.as_ref().to_path_buf()); 22 | 23 | let mut roots = self.roots.write().expect("sandbox context poisoned"); 24 | if !roots 25 | .iter() 26 | .any(|existing| existing.starts_with(&canonical) || canonical.starts_with(existing)) 27 | { 28 | roots.push(canonical.clone()); 29 | } 30 | Ok(canonical) 31 | } 32 | 33 | /// Returns the currently registered roots. 34 | pub fn roots(&self) -> Vec { 35 | self.roots.read().expect("sandbox context poisoned").clone() 36 | } 37 | 38 | /// Returns whether the candidate path is inside any registered root. 39 | pub fn is_path_allowed>(&self, candidate: P) -> bool { 40 | let candidate = candidate.as_ref(); 41 | self.roots 42 | .read() 43 | .expect("sandbox context poisoned") 44 | .iter() 45 | .any(|root| candidate.starts_with(root)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/elm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/nim.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/src/ui/gpui/simple_renderers.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::gpui::parameter_renderers::ParameterRenderer; 2 | use gpui::{div, px, IntoElement, ParentElement, Styled}; 3 | 4 | /// Renderer for parameters that shouldn't show their parameter name 5 | pub struct SimpleParameterRenderer { 6 | /// The list of tool+parameter combinations that should use this renderer 7 | supported_combinations: Vec<(String, String)>, 8 | /// Whether this parameter should be rendered with full width 9 | full_width: bool, 10 | } 11 | 12 | impl SimpleParameterRenderer { 13 | /// Create a new simple parameter renderer with specified combinations 14 | pub fn new(combinations: Vec<(String, String)>, full_width: bool) -> Self { 15 | Self { 16 | supported_combinations: combinations, 17 | full_width, 18 | } 19 | } 20 | } 21 | 22 | impl ParameterRenderer for SimpleParameterRenderer { 23 | fn supported_parameters(&self) -> Vec<(String, String)> { 24 | self.supported_combinations.clone() 25 | } 26 | 27 | fn render( 28 | &self, 29 | _tool_name: &str, 30 | _param_name: &str, 31 | param_value: &str, 32 | theme: &gpui_component::theme::Theme, 33 | ) -> gpui::AnyElement { 34 | div() 35 | .rounded_md() 36 | .px_2() 37 | .py_1() 38 | .text_size(px(13.)) 39 | .bg(crate::ui::gpui::theme::colors::tool_parameter_bg(theme)) 40 | .text_color(crate::ui::gpui::theme::colors::tool_parameter_value(theme)) 41 | .child(param_value.to_string()) 42 | .into_any_element() 43 | } 44 | 45 | fn is_full_width(&self, _tool_name: &str, _param_name: &str) -> bool { 46 | self.full_width 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/code_assistant/src/session/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::agent::ToolExecution; 2 | use crate::persistence::SessionModelConfig; 3 | use crate::types::{PlanState, ToolSyntax}; 4 | use llm::Message; 5 | use sandbox::SandboxPolicy; 6 | use serde::{Deserialize, Serialize}; 7 | use std::path::PathBuf; 8 | 9 | // New session management architecture 10 | pub mod instance; 11 | pub mod manager; 12 | 13 | // Main session manager 14 | pub use manager::SessionManager; 15 | 16 | /// Static configuration stored with each session. 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct SessionConfig { 19 | #[serde(default, skip_serializing_if = "Option::is_none")] 20 | pub init_path: Option, 21 | #[serde(default)] 22 | pub initial_project: String, 23 | #[serde(default = "default_tool_syntax")] 24 | pub tool_syntax: ToolSyntax, 25 | #[serde(default)] 26 | pub use_diff_blocks: bool, 27 | #[serde(default)] 28 | pub sandbox_policy: SandboxPolicy, 29 | } 30 | 31 | fn default_tool_syntax() -> ToolSyntax { 32 | ToolSyntax::Native 33 | } 34 | 35 | impl Default for SessionConfig { 36 | fn default() -> Self { 37 | Self { 38 | init_path: None, 39 | initial_project: String::new(), 40 | tool_syntax: default_tool_syntax(), 41 | use_diff_blocks: false, 42 | sandbox_policy: SandboxPolicy::DangerFullAccess, 43 | } 44 | } 45 | } 46 | 47 | /// State data needed to restore an agent session 48 | #[derive(Debug, Clone)] 49 | pub struct SessionState { 50 | pub session_id: String, 51 | pub name: String, 52 | pub messages: Vec, 53 | pub tool_executions: Vec, 54 | pub plan: PlanState, 55 | pub config: SessionConfig, 56 | pub next_request_id: Option, 57 | pub model_config: Option, 58 | } 59 | -------------------------------------------------------------------------------- /crates/code_assistant/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "code-assistant" 3 | version = "0.1.19" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | command_executor = { path = "../command_executor" } 8 | fs_explorer = { path = "../fs_explorer" } 9 | llm = { path = "../llm" } 10 | sandbox = { path = "../sandbox" } 11 | web = { path = "../web" } 12 | 13 | glob = "0.3" 14 | ignore = "0.4" 15 | walkdir = "2.5" 16 | percent-encoding = "2.3" 17 | tokio = { version = "1.48", features = ["full"] } 18 | tempfile = "3.23" 19 | 20 | # Terminal UI 21 | rustyline = "12.0.0" 22 | crossterm = "0.27.0" 23 | unicode-width = "0.1" 24 | ratatui = "0.29" 25 | tui-textarea = "0.7" 26 | tui-markdown = "0.3.6" 27 | 28 | # GPUI related 29 | gpui = "0.2.2" 30 | gpui-component = "0.4.0" 31 | smallvec = "1.15" 32 | rust-embed = { version = "8.9", features = ["include-exclude"] } 33 | 34 | # JSON (de)serialization 35 | serde = { version = "1.0", features = ["derive"] } 36 | serde_json = "1.0" 37 | 38 | # Error handling 39 | anyhow = "1.0" 40 | thiserror = "1.0" 41 | regex = "1.12" 42 | 43 | # Logging 44 | tracing = "0.1" 45 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 46 | 47 | # CLI 48 | clap = { version = "4.5", features = ["derive"] } 49 | 50 | async-trait = "0.1" 51 | dotenv = "0.15" 52 | dirs = "5.0" 53 | md5 = "0.7.0" 54 | 55 | # Date and time handling 56 | chrono = { version = "0.4", features = ["serde"] } 57 | 58 | # File content inspection 59 | unicode-segmentation = "1.12.0" 60 | rand = "0.8.5" 61 | 62 | # Diff visualization 63 | similar = { version = "2.7.0", features = ["inline"] } 64 | async-channel = "2.5.0" 65 | 66 | # Base64 encoding for images 67 | base64 = "0.22" 68 | 69 | # Image processing 70 | image = "0.25" 71 | 72 | # Agent Client Protocol 73 | agent-client-protocol = { version = "0.7", features = ["unstable"] } 74 | tokio-util = { version = "0.7", features = ["compat"] } 75 | 76 | [dev-dependencies] 77 | axum = "0.7" 78 | bytes = "1.10" 79 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/go.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/typescript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/src/mcp/resources.rs: -------------------------------------------------------------------------------- 1 | use super::types::{Resource, ResourceContent}; 2 | use fs_explorer::FileTreeEntry; 3 | use std::collections::HashSet; 4 | 5 | pub struct ResourceManager { 6 | file_tree: Option, 7 | subscriptions: HashSet, 8 | } 9 | 10 | impl ResourceManager { 11 | pub fn new() -> Self { 12 | Self { 13 | file_tree: None, 14 | subscriptions: HashSet::new(), 15 | } 16 | } 17 | 18 | /// Lists all available resources 19 | pub fn list_resources(&self) -> Vec { 20 | let mut resources = Vec::new(); 21 | // Add file tree resource if available 22 | if self.file_tree.is_some() { 23 | resources.push(Resource { 24 | uri: "tree:///".to_string(), 25 | name: "Repository Structure".to_string(), 26 | description: Some("The repository file tree structure".to_string()), 27 | mime_type: Some("text/plain".to_string()), 28 | }); 29 | } 30 | resources 31 | } 32 | 33 | /// Reads a specific resource content 34 | pub fn read_resource(&self, uri: &str) -> Option { 35 | match uri { 36 | "tree:///" => self.file_tree.as_ref().map(|t| ResourceContent { 37 | uri: uri.to_string(), 38 | mime_type: Some("text/plain".to_string()), 39 | text: Some(t.to_string()), 40 | }), 41 | _ => None, 42 | } 43 | } 44 | 45 | /// Subscribes to a resource 46 | pub fn subscribe(&mut self, uri: &str) { 47 | self.subscriptions.insert(uri.to_string()); 48 | } 49 | 50 | /// Unsubscribes from a resource 51 | pub fn unsubscribe(&mut self, uri: &str) { 52 | self.subscriptions.remove(uri); 53 | } 54 | 55 | /// Checks if a resource is subscribed 56 | pub fn is_subscribed(&self, uri: &str) -> bool { 57 | self.subscriptions.contains(uri) 58 | } 59 | 60 | /// Updates the file tree 61 | #[allow(dead_code)] 62 | pub fn update_file_tree(&mut self, tree: FileTreeEntry) { 63 | self.file_tree = Some(tree); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/ocaml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/llm/src/groq.rs: -------------------------------------------------------------------------------- 1 | use super::openai::{ApiKeyAuth, OpenAIClient, RequestCustomizer}; 2 | use crate::{types::*, LLMProvider, StreamingCallback}; 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | 6 | /// Custom request customizer for Groq API that removes prompt_cache_key 7 | pub struct GroqRequestCustomizer; 8 | 9 | impl RequestCustomizer for GroqRequestCustomizer { 10 | fn customize_request(&self, request: &mut serde_json::Value) -> Result<()> { 11 | // Remove prompt_cache_key field if it exists 12 | if let Some(obj) = request.as_object_mut() { 13 | obj.remove("prompt_cache_key"); 14 | } 15 | Ok(()) 16 | } 17 | 18 | fn get_additional_headers(&self) -> Vec<(String, String)> { 19 | vec![("Content-Type".to_string(), "application/json".to_string())] 20 | } 21 | 22 | fn customize_url(&self, base_url: &str, _streaming: bool) -> String { 23 | format!("{base_url}/chat/completions") 24 | } 25 | } 26 | 27 | pub struct GroqClient { 28 | inner: OpenAIClient, 29 | } 30 | 31 | impl GroqClient { 32 | pub fn default_base_url() -> String { 33 | "https://api.groq.com/openai/v1".to_string() 34 | } 35 | 36 | pub fn new(api_key: String, model: String, base_url: String) -> Self { 37 | Self { 38 | inner: OpenAIClient::with_customization( 39 | model, 40 | base_url, 41 | Box::new(ApiKeyAuth::new(api_key)), 42 | Box::new(GroqRequestCustomizer), 43 | ), 44 | } 45 | } 46 | 47 | /// Set custom model configuration to be merged into API requests 48 | pub fn with_custom_config(mut self, custom_config: serde_json::Value) -> Self { 49 | self.inner = self.inner.with_custom_config(custom_config); 50 | self 51 | } 52 | } 53 | 54 | #[async_trait] 55 | impl LLMProvider for GroqClient { 56 | async fn send_message( 57 | &mut self, 58 | request: LLMRequest, 59 | streaming_callback: Option<&StreamingCallback>, 60 | ) -> Result { 61 | // Delegate to inner OpenAI client since the APIs are compatible 62 | self.inner.send_message(request, streaming_callback).await 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/core/tool.rs: -------------------------------------------------------------------------------- 1 | use super::render::Render; 2 | use super::result::ToolResult; 3 | use super::spec::ToolSpec; 4 | use crate::permissions::PermissionMediator; 5 | use crate::types::PlanState; 6 | use anyhow::{anyhow, Result}; 7 | use command_executor::CommandExecutor; 8 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 9 | 10 | /// Context provided to tools during execution 11 | pub struct ToolContext<'a> { 12 | /// Project manager for accessing files 13 | pub project_manager: &'a dyn crate::config::ProjectManager, 14 | /// Command executor for running shell commands 15 | pub command_executor: &'a dyn CommandExecutor, 16 | /// Optional plan state reference for plan-related tools 17 | pub plan: Option<&'a mut PlanState>, 18 | /// Optional UI instance for streaming output 19 | pub ui: Option<&'a dyn crate::ui::UserInterface>, 20 | /// Optional current tool ID for streaming output 21 | pub tool_id: Option, 22 | /// Optional permission handler for potentially sensitive operations 23 | pub permission_handler: Option<&'a dyn PermissionMediator>, 24 | } 25 | 26 | /// Core trait for tools, defining the execution interface 27 | #[async_trait::async_trait] 28 | pub trait Tool: Send + Sync + 'static { 29 | /// Input type for this tool, must be deserializable from JSON 30 | type Input: DeserializeOwned + Serialize + Send; 31 | 32 | /// Output type for this tool, must implement Render, ToolResult and Serialize/Deserialize 33 | type Output: Render + ToolResult + Serialize + for<'de> Deserialize<'de> + Send + Sync; 34 | 35 | /// Get the metadata for this tool 36 | fn spec(&self) -> ToolSpec; 37 | 38 | /// Execute the tool with the given context and input 39 | /// The input may be modified during execution (e.g., for format-on-save) 40 | async fn execute<'a>( 41 | &self, 42 | context: &mut ToolContext<'a>, 43 | input: &mut Self::Input, 44 | ) -> Result; 45 | 46 | /// Deserialize a JSON value into this tool's output type 47 | fn deserialize_output(&self, json: serde_json::Value) -> Result { 48 | serde_json::from_value(json).map_err(|e| anyhow!("Failed to deserialize output: {e}")) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/rust.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /crates/code_assistant/resources/system_prompts/claude.md: -------------------------------------------------------------------------------- 1 | You are a software engineering assistant helping users with coding tasks. 2 | 3 | You accomplish tasks in these phases: 4 | - **Plan**: Break down the task into small, verifiable steps. Use the planning tool for complex tasks. 5 | - **Inform**: Gather relevant information using appropriate tools. 6 | - **Work**: Complete the task based on your plan and collected information. 7 | - **Validate**: Verify completion by running tests or build commands. 8 | - **Review**: Look for opportunities to improve the code. 9 | 10 | You may return to any previous phase as needed. 11 | 12 | # Plan tool 13 | 14 | When using the planning tool: 15 | - Skip it for straightforward tasks (roughly the easiest 25%). 16 | - Do not make single-step plans. 17 | - Update the plan after completing each sub-task. 18 | 19 | # Output style 20 | 21 | - Be concise unless the situation requires elaboration. 22 | - Use markdown for structure. 23 | - Provide brief summaries when done; don't claim issues are resolved without verification. 24 | - Clearly state when something could not be fully implemented. 25 | - Never use emojis. 26 | - Never create documentation files unless explicitly requested. 27 | 28 | ==== 29 | 30 | {{syntax}} 31 | 32 | {{tools}} 33 | 34 | # Tool use 35 | 36 | - Prefer specialized tools over shell commands (e.g., `list_files` over `ls`, `search_files` over `grep`). 37 | - Use one tool at a time; let each result inform the next action. 38 | - For targeted edits use `edit`; for new files or major rewrites use `write_file`. 39 | - After code changes, consider searching for affected files you haven't seen yet. 40 | 41 | # Git safety 42 | 43 | - Never revert changes you didn't make unless explicitly requested. 44 | - If you notice unexpected changes in files you're working on, stop and ask the user. 45 | - Avoid destructive commands like `git reset --hard` or `git checkout --` without user approval. 46 | 47 | # Long-running processes 48 | 49 | When running dev servers, watchers, or long-running tests, always background them: 50 | 51 | ```bash 52 | command > /path/to/log 2>&1 & 53 | ``` 54 | 55 | Never run blocking commands in the foreground. 56 | 57 | ==== 58 | 59 | When referencing files, use inline code with optional line numbers: `src/app.ts:42` 60 | -------------------------------------------------------------------------------- /crates/llm/src/aicore/mod.rs: -------------------------------------------------------------------------------- 1 | //! AI Core provider module 2 | //! 3 | //! AI Core acts as a proxy service that can route to different backend vendors. 4 | //! This module provides support for multiple vendor API types: 5 | //! - Anthropic (Claude models via Bedrock-style API) 6 | //! - OpenAI (Chat Completions API) 7 | //! - Vertex (Google Gemini API) 8 | 9 | mod anthropic; 10 | mod openai; 11 | mod types; 12 | mod vertex; 13 | 14 | pub use anthropic::AiCoreAnthropicClient; 15 | pub use openai::AiCoreOpenAIClient; 16 | pub use types::AiCoreApiType; 17 | pub use vertex::AiCoreVertexClient; 18 | 19 | use crate::auth::TokenManager; 20 | use crate::LLMProvider; 21 | use std::path::Path; 22 | use std::sync::Arc; 23 | 24 | /// Create an AI Core client based on the API type 25 | pub fn create_aicore_client( 26 | api_type: AiCoreApiType, 27 | token_manager: Arc, 28 | base_url: String, 29 | model_id: String, 30 | ) -> Box { 31 | match api_type { 32 | AiCoreApiType::Anthropic => Box::new(AiCoreAnthropicClient::new(token_manager, base_url)), 33 | AiCoreApiType::OpenAI => { 34 | Box::new(AiCoreOpenAIClient::new(token_manager, base_url, model_id)) 35 | } 36 | AiCoreApiType::Vertex => { 37 | Box::new(AiCoreVertexClient::new(token_manager, base_url, model_id)) 38 | } 39 | } 40 | } 41 | 42 | /// Create an AI Core client with recording capability 43 | pub fn create_aicore_client_with_recorder>( 44 | api_type: AiCoreApiType, 45 | token_manager: Arc, 46 | base_url: String, 47 | model_id: String, 48 | recording_path: P, 49 | ) -> Box { 50 | match api_type { 51 | AiCoreApiType::Anthropic => Box::new(AiCoreAnthropicClient::new_with_recorder( 52 | token_manager, 53 | base_url, 54 | recording_path, 55 | )), 56 | AiCoreApiType::OpenAI => Box::new(AiCoreOpenAIClient::new_with_recorder( 57 | token_manager, 58 | base_url, 59 | model_id, 60 | recording_path, 61 | )), 62 | AiCoreApiType::Vertex => Box::new(AiCoreVertexClient::new_with_recorder( 63 | token_manager, 64 | base_url, 65 | model_id, 66 | recording_path, 67 | )), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_open_ai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_cerebras.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/javascript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /crates/sandbox/src/seatbelt_base_policy.sbpl: -------------------------------------------------------------------------------- 1 | (version 1) 2 | 3 | ; Allow everything by default; we'll layer on top with specific denies. 4 | (allow default) 5 | 6 | ; Allow manipulating processes (needed for shells) 7 | (allow process*) 8 | 9 | ; Allow common syscalls used by standard tools 10 | (allow sysctl-read) 11 | 12 | ; Allow working with standard file descriptors and termios 13 | (allow file-ioctl (literal "/dev/tty")) 14 | (allow signal (target self)) 15 | 16 | ; Allow interacting with /dev/null, /dev/zero, /dev/tty 17 | (allow file-read-data (literal "/dev/null")) 18 | (allow file-read-data (literal "/dev/zero")) 19 | (allow file-read-data (literal "/dev/tty")) 20 | (allow file-write-data (literal "/dev/tty")) 21 | (allow file-write-data (literal "/dev/null")) 22 | 23 | ; Allow reading basic system files 24 | (allow file-read-metadata) 25 | (allow file-read-data (regex #"^/usr/")) 26 | (allow file-read-data (regex #"^/System/")) 27 | (allow file-read-data (regex #"^/private/etc/")) 28 | (allow file-read-data (regex #"^/Library/")) 29 | (allow file-read-data (literal "/dev/dtracehelper")) 30 | (allow file-read-data (literal "/dev/dtracehelper_dof")) 31 | (allow file-read-data (literal "/etc/hosts")) 32 | (allow file-read-data (literal "/etc/resolv.conf")) 33 | (allow file-read-data (literal "/etc/localtime")) 34 | (allow file-read-data (literal "/etc/services")) 35 | (allow file-read-data (literal "/etc/protocols")) 36 | (allow file-read-data (literal "/etc/networks")) 37 | (allow file-read-data (literal "/etc/proftpd.conf")) 38 | (allow file-read-data (literal "/etc/syslog.conf")) 39 | (allow file-read-data (literal "/etc/spwd.db")) 40 | (allow file-read-data (literal "/etc/hosts.equiv")) 41 | (allow file-read-data (literal "/etc/hosts.lpd")) 42 | (allow file-read-data (literal "/etc/hosts.secu")) 43 | 44 | ; Allow DNS lookups via mDNSResponder / resolver configuration 45 | (allow mach-lookup (global-name "com.apple.system.logger")) 46 | (allow mach-lookup (global-name "com.apple.system.notification_center")) 47 | (allow mach-lookup (global-name "com.apple.system.opendirectoryd.api")) 48 | (allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo")) 49 | (allow mach-lookup (global-name "com.apple.system.opendirectoryd.membership")) 50 | (allow mach-lookup (global-name "com.apple.system.coreservices.usernotification")) 51 | (allow mach-lookup (global-name "com.apple.CoreServices.coreservicesd")) 52 | (allow mach-lookup (global-name "com.apple.SecurityServer")) 53 | (allow mach-lookup (global-name "com.apple.trustd")) 54 | (allow mach-lookup (global-name "com.apple.notifyd.matching")) 55 | (allow mach-lookup (global-name "com.apple.bsd.dirhelper")) 56 | (allow mach-lookup (global-name "com.apple.autofsd")) 57 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/file_icons/lua.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /crates/sandbox/src/seatbelt.rs: -------------------------------------------------------------------------------- 1 | use crate::{SandboxPolicy, WritableRoot}; 2 | use std::io::Write; 3 | use std::path::{Path, PathBuf}; 4 | use tempfile::{NamedTempFile, TempPath}; 5 | 6 | const SEATBELT_EXEC: &str = "/usr/bin/sandbox-exec"; 7 | const BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl"); 8 | const NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl"); 9 | 10 | pub struct SeatbeltInvocation { 11 | pub executable: PathBuf, 12 | pub args: Vec, 13 | pub policy_path: TempPath, 14 | } 15 | 16 | pub fn build_invocation( 17 | command: Vec, 18 | policy: &SandboxPolicy, 19 | cwd: &Path, 20 | ) -> std::io::Result { 21 | let mut temp_policy = NamedTempFile::new()?; 22 | let policy_text = policy_text(policy, cwd); 23 | temp_policy.write_all(policy_text.as_bytes())?; 24 | let temp_path = temp_policy.into_temp_path(); 25 | 26 | let mut args = Vec::new(); 27 | args.push("-f".to_string()); 28 | args.push(temp_path.to_string_lossy().to_string()); 29 | args.push("--".to_string()); 30 | args.extend(command); 31 | 32 | Ok(SeatbeltInvocation { 33 | executable: PathBuf::from(SEATBELT_EXEC), 34 | args, 35 | policy_path: temp_path, 36 | }) 37 | } 38 | 39 | fn policy_text(policy: &SandboxPolicy, cwd: &Path) -> String { 40 | let mut text = String::from(BASE_POLICY); 41 | 42 | text.push_str("(deny file-write*)\n"); 43 | 44 | let writable_roots: Vec = policy.get_writable_roots_with_cwd(cwd); 45 | 46 | for root in writable_roots.iter() { 47 | let path = canonical_string(&root.root); 48 | text.push_str(&format!("(allow file-write* (subpath \"{path}\"))\n")); 49 | for ro in root.read_only_subpaths.iter() { 50 | let ro_path = canonical_string(ro); 51 | text.push_str(&format!("(deny file-write* (subpath \"{ro_path}\"))\n")); 52 | } 53 | } 54 | 55 | // Always allow writes to TMPDIR when it's already permitted by sandbox policy generation 56 | if let Some(tmpdir) = std::env::var_os("TMPDIR") { 57 | let tmp_path = PathBuf::from(tmpdir); 58 | if writable_roots 59 | .iter() 60 | .any(|wr| tmp_path.starts_with(&wr.root)) 61 | { 62 | text.push_str(&format!( 63 | "(allow file-write* (subpath \"{}\"))\n", 64 | canonical_string(&tmp_path) 65 | )); 66 | } 67 | } 68 | 69 | if policy.has_full_network_access() { 70 | text.push_str(NETWORK_POLICY); 71 | } 72 | 73 | text 74 | } 75 | 76 | fn canonical_string(path: &Path) -> String { 77 | path.canonicalize() 78 | .unwrap_or_else(|_| path.to_path_buf()) 79 | .to_string_lossy() 80 | .into_owned() 81 | } 82 | -------------------------------------------------------------------------------- /models.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Claude Sonnet 4.5": { 3 | "provider": "anthropic-main", 4 | "id": "claude-sonnet-4-5", 5 | "context_token_limit": 200000, 6 | "config": { 7 | "max_tokens": 32768, 8 | "thinking": { 9 | "type": "enabled", 10 | "budget_tokens": 8192 11 | } 12 | } 13 | }, 14 | "GPT-5-Codex (low)": { 15 | "provider": "openai-responses", 16 | "id": "gpt-5-codex", 17 | "context_token_limit": 128000, 18 | "config": { 19 | "temperature": 0.7, 20 | "reasoning": { 21 | "effort": "low", 22 | "summary": "detailed" 23 | } 24 | } 25 | }, 26 | "GPT-5-Codex (medium)": { 27 | "provider": "openai-responses", 28 | "id": "gpt-5-codex", 29 | "context_token_limit": 128000, 30 | "config": { 31 | "temperature": 0.7, 32 | "reasoning": { 33 | "effort": "medium", 34 | "summary": "detailed" 35 | } 36 | } 37 | }, 38 | "GPT-4.1": { 39 | "provider": "openai-main", 40 | "id": "gpt-4.1", 41 | "context_token_limit": 128000, 42 | "config": { 43 | "temperature": 0.8, 44 | "max_tokens": 4096 45 | } 46 | }, 47 | "Llama 3.2 8B": { 48 | "provider": "ollama-local", 49 | "id": "llama3.3:8b", 50 | "context_token_limit": 16384, 51 | "config": { 52 | "options": { 53 | "num_ctx": 16384, 54 | "temperature": 0.8 55 | } 56 | } 57 | }, 58 | "Qwen 3 Coder 480B": { 59 | "provider": "groq-main", 60 | "id": "qwen-3-coder-480b", 61 | "context_token_limit": 32768, 62 | "config": { 63 | "temperature": 0.7, 64 | "top_p": 0.8 65 | } 66 | }, 67 | "Moonshot Kimi K2": { 68 | "provider": "groq-main", 69 | "id": "moonshotai/kimi-k2-instruct", 70 | "context_token_limit": 32768, 71 | "config": { 72 | "temperature": 0.6 73 | } 74 | }, 75 | "Cerebras GPT OSS 120B": { 76 | "provider": "cerebras-main", 77 | "id": "gpt-oss-120b", 78 | "context_token_limit": 16384, 79 | "config": { 80 | "temperature": 0.7 81 | } 82 | }, 83 | "Mistral Devstral Medium": { 84 | "provider": "mistral-main", 85 | "id": "devstral-medium-2507", 86 | "context_token_limit": 200000, 87 | "config": { 88 | "temperature": 0.7 89 | } 90 | }, 91 | "Gemini 2.5 Pro": { 92 | "provider": "vertex-main", 93 | "id": "gemini-2.5-pro-preview-06-05", 94 | "context_token_limit": 200000, 95 | "config": { 96 | "generation_config": { 97 | "temperature": 0.8 98 | } 99 | } 100 | }, 101 | "AI Core Claude Sonnet 4": { 102 | "provider": "ai-core-dev", 103 | "id": "claude-sonnet-4", 104 | "context_token_limit": 200000, 105 | "config": {} 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/replace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /crates/llm/src/auth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use base64::engine::{general_purpose, Engine}; 3 | use std::sync::Arc; 4 | use std::time::{Duration, SystemTime}; 5 | use tokio::sync::RwLock; 6 | 7 | pub struct TokenManager { 8 | client_id: String, 9 | client_secret: String, 10 | token_url: String, 11 | current_token: RwLock>, 12 | } 13 | 14 | #[derive(serde::Deserialize)] 15 | struct TokenResponse { 16 | access_token: String, 17 | expires_in: u64, 18 | } 19 | 20 | struct TokenInfo { 21 | token: String, 22 | expires_at: SystemTime, 23 | } 24 | 25 | impl TokenManager { 26 | pub async fn new( 27 | client_id: String, 28 | client_secret: String, 29 | token_url: String, 30 | ) -> Result> { 31 | tracing::debug!("Creating new TokenManager..."); 32 | 33 | let manager = Arc::new(Self { 34 | client_id, 35 | client_secret, 36 | token_url, 37 | current_token: RwLock::new(None), 38 | }); 39 | 40 | // Fetch initial token 41 | manager.refresh_token().await?; 42 | 43 | Ok(manager) 44 | } 45 | 46 | pub async fn get_valid_token(&self) -> Result { 47 | // Check if we have a valid token 48 | if let Some(token_info) = self.current_token.read().await.as_ref() { 49 | if SystemTime::now() < token_info.expires_at { 50 | return Ok(token_info.token.clone()); 51 | } 52 | } 53 | 54 | // If not, we need to fetch a new one 55 | self.refresh_token().await 56 | } 57 | 58 | async fn refresh_token(&self) -> Result { 59 | tracing::debug!("Requesting new access token..."); 60 | 61 | let client = reqwest::Client::new(); 62 | let auth = 63 | general_purpose::STANDARD.encode(format!("{}:{}", self.client_id, self.client_secret)); 64 | 65 | let res = client 66 | .post(&self.token_url) 67 | .header("Authorization", format!("Basic {auth}")) 68 | .header("Content-Type", "application/x-www-form-urlencoded") 69 | .body("grant_type=client_credentials") 70 | .send() 71 | .await?; 72 | 73 | let status = res.status(); 74 | if !status.is_success() { 75 | let error_text = res.text().await?; 76 | anyhow::bail!("Token request failed: {status} - {error_text}"); 77 | } 78 | 79 | let token_response = res.json::().await?; 80 | 81 | // Set expiry slightly before actual expiry to ensure we don't use expired tokens 82 | let expires_at = SystemTime::now() + Duration::from_secs(token_response.expires_in - 60); 83 | 84 | let token_info = TokenInfo { 85 | token: token_response.access_token.clone(), 86 | expires_at, 87 | }; 88 | 89 | *self.current_token.write().await = Some(token_info); 90 | 91 | Ok(token_response.access_token) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/llm/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! LLM integration module providing abstraction over different LLM providers 2 | //! 3 | //! This module implements: 4 | //! - Common interface for LLM interactions via the LLMProvider trait 5 | //! - Support for multiple providers (Anthropic, OpenAI, Ollama, Vertex, AI Core) 6 | //! - Message streaming capabilities 7 | //! - Provider-specific implementations and optimizations 8 | //! - Shared types and utilities for LLM interactions 9 | //! - Recording capabilities for debugging and testing 10 | 11 | #[cfg(test)] 12 | mod tests; 13 | 14 | mod utils; 15 | 16 | pub mod aicore; 17 | pub mod anthropic; 18 | pub mod auth; 19 | pub mod cerebras; 20 | pub mod config_merge; 21 | pub mod display; 22 | pub mod factory; 23 | pub mod groq; 24 | pub mod mistralai; 25 | pub mod ollama; 26 | pub mod openai; 27 | pub mod openai_responses; 28 | pub mod openrouter; 29 | pub mod provider_config; 30 | pub mod recording; 31 | pub mod streaming; 32 | pub mod types; 33 | pub mod vertex; 34 | 35 | pub use aicore::{ 36 | create_aicore_client, create_aicore_client_with_recorder, AiCoreAnthropicClient, AiCoreApiType, 37 | AiCoreOpenAIClient, AiCoreVertexClient, 38 | }; 39 | pub use anthropic::AnthropicClient; 40 | pub use cerebras::CerebrasClient; 41 | pub use groq::GroqClient; 42 | pub use mistralai::MistralAiClient; 43 | pub use ollama::OllamaClient; 44 | pub use openai::OpenAIClient; 45 | pub use openai_responses::OpenAIResponsesClient; 46 | pub use openrouter::OpenRouterClient; 47 | pub use types::*; 48 | pub use vertex::VertexClient; 49 | 50 | use anyhow::Result; 51 | use async_trait::async_trait; 52 | 53 | /// Structure to represent different types of streaming content from LLMs 54 | #[derive(Debug, Clone)] 55 | pub enum StreamingChunk { 56 | /// Regular text content 57 | Text(String), 58 | /// Content identified as "thinking" (supported by some models) 59 | Thinking(String), 60 | /// JSON input for tool calls with optional metadata 61 | InputJson { 62 | content: String, 63 | tool_name: Option, 64 | tool_id: Option, 65 | }, 66 | /// Rate limit notification with countdown in seconds 67 | RateLimit { seconds_remaining: u64 }, 68 | /// Clear rate limit notification 69 | RateLimitClear, 70 | /// Indicates that streaming from the LLM has completed 71 | StreamingComplete, 72 | /// OpenAI reasoning summary started a new item 73 | ReasoningSummaryStart, 74 | /// OpenAI reasoning summary delta for the current item 75 | ReasoningSummaryDelta(String), 76 | /// Indicates reasoning block is complete 77 | ReasoningComplete, 78 | } 79 | 80 | pub type StreamingCallback = Box Result<()> + Send + Sync>; 81 | 82 | /// Trait for different LLM provider implementations 83 | #[async_trait] 84 | pub trait LLMProvider: Send + Sync { 85 | /// Sends a request to the LLM service 86 | async fn send_message( 87 | &mut self, 88 | request: LLMRequest, 89 | streaming_callback: Option<&StreamingCallback>, 90 | ) -> Result; 91 | } 92 | -------------------------------------------------------------------------------- /crates/llm/src/streaming.rs: -------------------------------------------------------------------------------- 1 | //! Common streaming infrastructure for LLM providers 2 | //! 3 | //! This module provides shared abstractions for handling streaming responses 4 | //! from LLM providers, supporting both real HTTP responses and recorded 5 | //! playback with identical processing logic. 6 | 7 | use crate::recording::RecordedChunk; 8 | use anyhow::Result; 9 | use async_trait::async_trait; 10 | use reqwest::Response; 11 | use std::time::{Duration, Instant}; 12 | 13 | /// Trait for streaming chunk sources (real HTTP response or recorded playback) 14 | /// 15 | /// This abstraction allows providers to use the same streaming processing logic 16 | /// for both live HTTP responses and recorded playback, ensuring identical behavior. 17 | #[async_trait] 18 | pub trait ChunkStream: Send { 19 | async fn next_chunk(&mut self) -> Result>>; 20 | } 21 | 22 | /// Real HTTP response chunk stream 23 | pub struct HttpChunkStream { 24 | pub response: Response, 25 | } 26 | 27 | impl HttpChunkStream { 28 | pub fn new(response: Response) -> Self { 29 | Self { response } 30 | } 31 | } 32 | 33 | #[async_trait] 34 | impl ChunkStream for HttpChunkStream { 35 | async fn next_chunk(&mut self) -> Result>> { 36 | match self.response.chunk().await { 37 | Ok(Some(chunk)) => Ok(Some(chunk.to_vec())), 38 | Ok(None) => Ok(None), 39 | Err(e) => Err(anyhow::anyhow!("HTTP chunk error: {e}")), 40 | } 41 | } 42 | } 43 | 44 | /// Recorded chunk stream for playback 45 | pub struct PlaybackChunkStream { 46 | chunks: Vec, 47 | current_index: usize, 48 | start_time: Instant, 49 | fast_mode: bool, 50 | } 51 | 52 | impl PlaybackChunkStream { 53 | pub fn new(chunks: Vec, fast_mode: bool) -> Self { 54 | Self { 55 | chunks, 56 | current_index: 0, 57 | start_time: Instant::now(), 58 | fast_mode, 59 | } 60 | } 61 | } 62 | 63 | #[async_trait] 64 | impl ChunkStream for PlaybackChunkStream { 65 | async fn next_chunk(&mut self) -> Result>> { 66 | if self.current_index >= self.chunks.len() { 67 | return Ok(None); 68 | } 69 | 70 | let chunk = &self.chunks[self.current_index]; 71 | 72 | // Handle timing - either fast playback or respect original timing 73 | if !self.fast_mode { 74 | let elapsed = self.start_time.elapsed(); 75 | let expected_time = Duration::from_millis(chunk.timestamp_ms); 76 | 77 | if elapsed < expected_time { 78 | let sleep_duration = expected_time - elapsed; 79 | tokio::time::sleep(sleep_duration).await; 80 | } 81 | } else { 82 | // Fast playback - small delay to simulate streaming 83 | tokio::time::sleep(Duration::from_millis(17)).await; // ~60fps 84 | } 85 | 86 | let line = format!("{}\n", chunk.data); 87 | self.current_index += 1; 88 | 89 | Ok(Some(line.into_bytes())) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/command_executor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::path::{Path, PathBuf}; 3 | 4 | mod default_executor; 5 | mod sandboxed_executor; 6 | pub use default_executor::DefaultCommandExecutor; 7 | pub use sandboxed_executor::SandboxedCommandExecutor; 8 | 9 | #[derive(Clone)] 10 | pub struct CommandOutput { 11 | pub success: bool, 12 | pub output: String, 13 | } 14 | 15 | #[derive(Clone, Debug, Default)] 16 | pub struct SandboxCommandRequest { 17 | pub writable_roots: Vec, 18 | pub read_only: bool, 19 | pub bypass_sandbox: bool, 20 | } 21 | 22 | /// Callback trait for streaming command output 23 | pub trait StreamingCallback: Send + Sync { 24 | fn on_output_chunk(&self, chunk: &str) -> Result<()>; 25 | 26 | fn on_terminal_attached(&self, _terminal_id: &str) -> Result<()> { 27 | Ok(()) 28 | } 29 | } 30 | 31 | #[async_trait::async_trait] 32 | pub trait CommandExecutor: Send + Sync { 33 | async fn execute( 34 | &self, 35 | command_line: &str, 36 | working_dir: Option<&PathBuf>, 37 | sandbox_request: Option<&SandboxCommandRequest>, 38 | ) -> Result; 39 | 40 | /// Execute command with streaming output callback 41 | async fn execute_streaming( 42 | &self, 43 | command_line: &str, 44 | working_dir: Option<&PathBuf>, 45 | callback: Option<&dyn StreamingCallback>, 46 | sandbox_request: Option<&SandboxCommandRequest>, 47 | ) -> Result; 48 | } 49 | 50 | /// Quote a path for the current platform so spaces and special chars are preserved when passed 51 | /// through the shell. This is a best-effort helper; it does not aim to be a full shell-quoting lib. 52 | pub fn shell_quote_path(path: &Path) -> String { 53 | #[cfg(target_family = "unix")] 54 | { 55 | let s = path.to_string_lossy(); 56 | // Only quote if whitespace is present; basic behavior for tests 57 | if s.chars().any(|c| c.is_whitespace()) { 58 | let escaped = s.replace('\'', "'\\''"); 59 | format!("'{escaped}'") 60 | } else { 61 | s.to_string() 62 | } 63 | } 64 | 65 | #[cfg(target_family = "windows")] 66 | { 67 | let s = path.to_string_lossy(); 68 | if s.chars().any(|c| c.is_whitespace()) { 69 | // Surround with double quotes and escape internal quotes by doubling them 70 | let escaped = s.replace('"', "\"\""); 71 | format!("\"{escaped}\"") 72 | } else { 73 | s.to_string() 74 | } 75 | } 76 | } 77 | 78 | /// Build a formatter command line from a template. If the template contains the {path} placeholder, 79 | /// it will be replaced with the (quoted) relative path. If not present, the template is returned as-is. 80 | pub fn build_format_command(template: &str, relative_path: &Path) -> String { 81 | if template.contains("{path}") { 82 | let quoted = shell_quote_path(relative_path); 83 | template.replace("{path}", "ed) 84 | } else { 85 | template.to_string() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/code_assistant/assets/icons/ai_groq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/agent-client-protocol/README.md: -------------------------------------------------------------------------------- 1 | # Agent Client Protocol (ACP) Documentation 2 | 3 | This directory contains documentation about the Agent Client Protocol (ACP) from the creators of Zed. 4 | 5 | ## Overview 6 | 7 | The Agent Client Protocol standardizes communication between code editors (IDEs, text-editors, etc.) and coding agents (programs that use generative AI to autonomously modify code). 8 | 9 | ## Why ACP? 10 | 11 | AI coding agents and editors are tightly coupled but interoperability isn't the default. Each editor must build custom integrations for every agent they want to support, and agents must implement editor-specific APIs to reach users. This creates several problems: 12 | 13 | - **Integration overhead**: Every new agent-editor combination requires custom work 14 | - **Limited compatibility**: Agents work with only a subset of available editors 15 | - **Developer lock-in**: Choosing an agent often means accepting their available interfaces 16 | 17 | ACP solves this by providing a standardized protocol for agent-editor communication, similar to how the Language Server Protocol (LSP) standardized language server integration. 18 | 19 | ## Key Concepts 20 | 21 | - **Agents**: Programs that use generative AI to autonomously modify code. They typically run as subprocesses of the Client. 22 | - **Clients**: Code editors (IDEs, text editors) that provide the interface between users and agents. They manage the environment, handle user interactions, and control access to resources. 23 | - **Sessions**: Independent conversation contexts with their own history and state 24 | - **Communication**: JSON-RPC 2.0 over stdio 25 | - **Content Format**: Markdown for user-readable text 26 | 27 | ## Documentation Structure 28 | 29 | 1. [Introduction](./01-introduction.md) - Basic concepts and protocol overview 30 | 2. [Protocol Overview](./02-protocol-overview.md) - Communication model and message flow 31 | 3. [Initialization](./03-initialization.md) - Version negotiation and capability exchange 32 | 4. [Session Setup](./04-session-setup.md) - Creating and loading sessions 33 | 5. [Prompt Turn](./05-prompt-turn.md) - The complete lifecycle of a user prompt 34 | 6. [Tool Calls](./06-tool-calls.md) - How agents execute operations 35 | 7. [Content Types](./07-content-types.md) - Different content block types 36 | 8. [File System](./08-file-system.md) - Reading and writing files 37 | 9. [Terminals](./09-terminals.md) - Executing shell commands 38 | 10. [Schema Reference](./10-schema.md) - Complete type definitions 39 | 11. [Rust Implementation](./11-rust-implementation.md) - Using the Rust crate 40 | 41 | ## Resources 42 | 43 | - Official Website: https://agentclientprotocol.com 44 | - Rust Crate: https://crates.io/crates/agent-client-protocol 45 | - Documentation: https://docs.rs/agent-client-protocol 46 | - GitHub Examples: https://github.com/zed-industries/agent-client-protocol 47 | 48 | ## Implementation Notes 49 | 50 | The protocol is still under development but complete enough to build interesting user experiences. Agents that implement ACP work with any compatible editor, and editors that support ACP gain access to the entire ecosystem of ACP-compatible agents. 51 | -------------------------------------------------------------------------------- /crates/code_assistant/src/ui/terminal/state.rs: -------------------------------------------------------------------------------- 1 | use crate::persistence::ChatMetadata; 2 | use crate::session::instance::SessionActivityState; 3 | use crate::types::PlanState; 4 | use sandbox::SandboxPolicy; 5 | use std::collections::HashMap; 6 | 7 | pub struct AppState { 8 | pub plan: Option, 9 | pub plan_expanded: bool, 10 | pub plan_dirty: bool, 11 | pub sessions: Vec, 12 | pub current_session_id: Option, 13 | pub activity_state: Option, 14 | pub session_activity_states: HashMap, 15 | pub pending_message: Option, 16 | pub tool_statuses: HashMap, 17 | pub current_model: Option, 18 | pub info_message: Option, 19 | pub current_sandbox_policy: Option, 20 | } 21 | 22 | impl AppState { 23 | pub fn new() -> Self { 24 | Self { 25 | plan: None, 26 | plan_expanded: false, 27 | plan_dirty: true, 28 | sessions: Vec::new(), 29 | current_session_id: None, 30 | activity_state: None, 31 | session_activity_states: HashMap::new(), 32 | pending_message: None, 33 | tool_statuses: HashMap::new(), 34 | current_model: None, 35 | info_message: None, 36 | current_sandbox_policy: None, 37 | } 38 | } 39 | 40 | pub fn update_sessions(&mut self, sessions: Vec) { 41 | self.sessions = sessions; 42 | } 43 | 44 | pub fn update_activity_state(&mut self, activity_state: Option) { 45 | self.activity_state = activity_state; 46 | } 47 | 48 | pub fn update_pending_message(&mut self, message: Option) { 49 | self.pending_message = message; 50 | } 51 | 52 | pub fn update_session_activity_state( 53 | &mut self, 54 | session_id: String, 55 | activity_state: SessionActivityState, 56 | ) { 57 | self.session_activity_states 58 | .insert(session_id, activity_state); 59 | } 60 | 61 | pub fn update_current_model(&mut self, model: Option) { 62 | self.current_model = model; 63 | } 64 | 65 | pub fn update_sandbox_policy(&mut self, policy: Option) { 66 | self.current_sandbox_policy = policy; 67 | } 68 | 69 | pub fn set_info_message(&mut self, message: Option) { 70 | self.info_message = message; 71 | } 72 | 73 | pub fn set_plan(&mut self, plan: Option) { 74 | if let Some(ref plan_state) = plan { 75 | tracing::debug!( 76 | "AppState::set_plan with {} entries (expanded: {})", 77 | plan_state.entries.len(), 78 | self.plan_expanded 79 | ); 80 | } else { 81 | tracing::debug!("AppState::set_plan clearing plan state"); 82 | } 83 | self.plan = plan; 84 | self.plan_dirty = true; 85 | } 86 | 87 | pub fn toggle_plan_expanded(&mut self) -> bool { 88 | self.plan_expanded = !self.plan_expanded; 89 | self.plan_expanded 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/types.rs: -------------------------------------------------------------------------------- 1 | use crate::tools::core::{Render, ResourcesTracker, ToolResult}; 2 | use llm::ToolDefinition; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | 6 | /// Enhanced version of the base ToolDefinition with additional metadata fields 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct AnnotatedToolDefinition { 9 | pub name: String, 10 | pub description: String, 11 | pub parameters: serde_json::Value, 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub annotations: Option, 14 | } 15 | 16 | impl AnnotatedToolDefinition { 17 | /// Convert to a basic ToolDefinition (without annotations) for LLM providers 18 | pub fn to_tool_definition(&self) -> ToolDefinition { 19 | ToolDefinition { 20 | name: self.name.clone(), 21 | description: self.description.clone(), 22 | parameters: self.parameters.clone(), 23 | } 24 | } 25 | 26 | /// Convert a vector of AnnotatedToolDefinition to a vector of ToolDefinition 27 | pub fn to_tool_definitions(tools: Vec) -> Vec { 28 | tools.into_iter().map(|t| t.to_tool_definition()).collect() 29 | } 30 | } 31 | 32 | /// Represents a tool request from the LLM, derived from ContentBlock::ToolUse 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub struct ToolRequest { 35 | pub id: String, 36 | pub name: String, 37 | pub input: Value, 38 | /// Start position of the tool block in the original text (for custom syntaxes) 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub start_offset: Option, 41 | /// End position of the tool block in the original text (for custom syntaxes) 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub end_offset: Option, 44 | } 45 | 46 | impl From<&llm::ContentBlock> for ToolRequest { 47 | fn from(block: &llm::ContentBlock) -> Self { 48 | if let llm::ContentBlock::ToolUse { 49 | id, name, input, .. 50 | } = block 51 | { 52 | Self { 53 | id: id.clone(), 54 | name: name.clone(), 55 | input: input.clone(), 56 | start_offset: None, 57 | end_offset: None, 58 | } 59 | } else { 60 | panic!("Cannot convert non-ToolUse ContentBlock to ToolRequest") 61 | } 62 | } 63 | } 64 | 65 | /// Represents a parse error that occurred when processing tool blocks 66 | /// This allows parse errors to be treated like regular tool results 67 | #[derive(Debug, Clone, Serialize, Deserialize)] 68 | pub struct ParseError { 69 | pub error_message: String, 70 | } 71 | 72 | impl ParseError { 73 | pub fn new(error_message: String) -> Self { 74 | Self { error_message } 75 | } 76 | } 77 | 78 | impl Render for ParseError { 79 | fn render(&self, _tracker: &mut ResourcesTracker) -> String { 80 | self.error_message.clone() 81 | } 82 | 83 | fn status(&self) -> String { 84 | "Parse Error".to_string() 85 | } 86 | } 87 | 88 | impl ToolResult for ParseError { 89 | fn is_success(&self) -> bool { 90 | false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/code_assistant/src/ui/streaming/mod.rs: -------------------------------------------------------------------------------- 1 | //! Streaming processor for handling chunks from LLM providers 2 | 3 | use crate::agent::ToolSyntax; 4 | use crate::ui::UIError; 5 | use crate::ui::UserInterface; 6 | use llm::{Message, StreamingChunk}; 7 | use std::sync::Arc; 8 | 9 | mod caret_processor; 10 | mod json_processor; 11 | mod xml_processor; 12 | 13 | #[cfg(test)] 14 | mod caret_processor_tests; 15 | #[cfg(test)] 16 | mod json_processor_tests; 17 | #[cfg(test)] 18 | mod test_utils; 19 | #[cfg(test)] 20 | mod xml_processor_tests; 21 | 22 | /// Fragments for display in UI components 23 | #[derive(Debug, Clone, PartialEq)] 24 | pub enum DisplayFragment { 25 | /// Regular plain text 26 | PlainText(String), 27 | /// Thinking text (shown differently) 28 | ThinkingText(String), 29 | /// Image content 30 | Image { media_type: String, data: String }, 31 | /// Tool invocation start 32 | ToolName { name: String, id: String }, 33 | /// Parameter for a tool 34 | ToolParameter { 35 | name: String, 36 | value: String, 37 | tool_id: String, 38 | }, 39 | /// End of a tool invocation 40 | ToolEnd { id: String }, 41 | /// Streaming tool output chunk 42 | ToolOutput { tool_id: String, chunk: String }, 43 | /// Tool attached a terminal on the client side 44 | ToolTerminal { 45 | tool_id: String, 46 | terminal_id: String, 47 | }, 48 | /// OpenAI reasoning summary started a new item 49 | ReasoningSummaryStart, 50 | /// OpenAI reasoning summary delta for the current item 51 | ReasoningSummaryDelta(String), 52 | /// Mark reasoning as completed 53 | ReasoningComplete, 54 | /// Divider indicating the conversation was compacted, with expandable summary text 55 | CompactionDivider { summary: String }, 56 | } 57 | 58 | /// Common trait for stream processors 59 | pub trait StreamProcessorTrait: Send + Sync { 60 | /// Create a new stream processor with the given UI and request context 61 | fn new(ui: Arc, request_id: u64) -> Self 62 | where 63 | Self: Sized; 64 | 65 | /// Process a streaming chunk and send display fragments to the UI 66 | fn process(&mut self, chunk: &StreamingChunk) -> Result<(), UIError>; 67 | 68 | /// Extract display fragments from a stored message without sending to UI 69 | /// Used for session loading to reconstruct fragment sequences 70 | fn extract_fragments_from_message( 71 | &mut self, 72 | message: &Message, 73 | ) -> Result, UIError>; 74 | } 75 | 76 | // Export the concrete implementations 77 | pub use caret_processor::CaretStreamProcessor; 78 | pub use json_processor::JsonStreamProcessor; 79 | pub use xml_processor::XmlStreamProcessor; 80 | 81 | /// Factory function to create the appropriate processor based on tool syntax 82 | pub fn create_stream_processor( 83 | tool_syntax: ToolSyntax, 84 | ui: Arc, 85 | request_id: u64, 86 | ) -> Box { 87 | match tool_syntax { 88 | ToolSyntax::Xml => Box::new(XmlStreamProcessor::new(ui, request_id)), 89 | ToolSyntax::Native => Box::new(JsonStreamProcessor::new(ui, request_id)), 90 | ToolSyntax::Caret => Box::new(CaretStreamProcessor::new(ui, request_id)), 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/code_assistant/src/main.rs: -------------------------------------------------------------------------------- 1 | mod acp; 2 | mod agent; 3 | mod app; 4 | mod cli; 5 | mod config; 6 | mod logging; 7 | mod mcp; 8 | mod permissions; 9 | mod persistence; 10 | mod session; 11 | mod tools; 12 | mod types; 13 | mod ui; 14 | mod utils; 15 | 16 | #[cfg(test)] 17 | mod tests; 18 | 19 | use crate::cli::{Args, Mode}; 20 | use crate::logging::setup_logging; 21 | use anyhow::Result; 22 | 23 | #[tokio::main] 24 | async fn main() -> Result<()> { 25 | let args = Args::parse(); 26 | 27 | // Handle list commands first 28 | if args.handle_list_commands()? { 29 | return Ok(()); 30 | } 31 | 32 | match args.mode { 33 | Some(Mode::Server { verbose }) => app::server::run(verbose).await, 34 | Some(Mode::Acp { 35 | verbose, 36 | path, 37 | model, 38 | tool_syntax, 39 | use_diff_format, 40 | sandbox_mode, 41 | sandbox_network, 42 | }) => { 43 | // Ensure the path exists and is a directory 44 | if !path.is_dir() { 45 | anyhow::bail!("Path '{}' is not a directory", path.display()); 46 | } 47 | 48 | let model_name = Args::resolve_model_name(model)?; 49 | 50 | let config = app::AgentRunConfig { 51 | path, 52 | task: None, 53 | continue_task: false, 54 | model: model_name.clone(), 55 | tool_syntax, 56 | use_diff_format, 57 | record: None, 58 | playback: None, 59 | fast_playback: false, 60 | sandbox_policy: sandbox_mode.to_policy(sandbox_network), 61 | }; 62 | 63 | app::acp::run(verbose, config).await 64 | } 65 | None => { 66 | if args.ui { 67 | // GPUI mode - use stderr to keep stdout clean 68 | setup_logging(args.verbose, false); 69 | } else { 70 | // Terminal UI mode - log to file to prevent UI interference 71 | logging::setup_logging_for_terminal_ui(args.verbose); 72 | } 73 | 74 | // Ensure the path exists and is a directory 75 | if !args.path.is_dir() { 76 | anyhow::bail!("Path '{}' is not a directory", args.path.display()); 77 | } 78 | 79 | let model_name = args.get_model_name()?; 80 | let sandbox_policy = args.sandbox_policy(); 81 | 82 | let config = app::AgentRunConfig { 83 | path: args.path, 84 | task: args.task, 85 | continue_task: args.continue_task, 86 | model: model_name, 87 | tool_syntax: args.tool_syntax, 88 | use_diff_format: args.use_diff_format, 89 | record: args.record, 90 | playback: args.playback, 91 | fast_playback: args.fast_playback, 92 | sandbox_policy, 93 | }; 94 | 95 | if args.ui { 96 | app::gpui::run(config) 97 | } else { 98 | app::terminal::run(config).await 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/code_assistant/src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | use std::io; 3 | use tracing_subscriber::fmt::SubscriberBuilder; 4 | 5 | pub fn setup_logging(verbose_level: u8, to_stdout: bool) { 6 | setup_logging_with_file(verbose_level, to_stdout, None); 7 | } 8 | 9 | pub fn setup_logging_for_terminal_ui(verbose_level: u8) { 10 | // For terminal UI, log to a file to prevent interference with the UI 11 | let log_file_path = dirs::cache_dir() 12 | .unwrap_or_else(std::env::temp_dir) 13 | .join("code-assistant") 14 | .join("terminal-ui.log"); 15 | 16 | // Create directory if it doesn't exist 17 | if let Some(parent) = log_file_path.parent() { 18 | let _ = std::fs::create_dir_all(parent); 19 | } 20 | 21 | setup_logging_with_file(verbose_level, false, Some(log_file_path)); 22 | } 23 | 24 | fn setup_logging_with_file( 25 | verbose_level: u8, 26 | to_stdout: bool, 27 | log_file: Option, 28 | ) { 29 | let filter = if std::env::var("RUST_LOG").is_ok() { 30 | // Use RUST_LOG if set 31 | tracing_subscriber::EnvFilter::from_default_env() 32 | } else { 33 | // Map verbosity count to filters 34 | let filter_str = match verbose_level { 35 | 0 => "warn,code_assistant=info,llm=info,web=info", 36 | 1 => "info,code_assistant=debug,llm=debug,web=debug", 37 | _ => "debug,code_assistant=trace,llm=trace,web=trace", 38 | }; 39 | tracing_subscriber::EnvFilter::new(filter_str) 40 | }; 41 | 42 | let subscriber = tracing_subscriber::fmt() 43 | .with_env_filter(filter) 44 | .with_target(false) 45 | .with_thread_ids(false) 46 | .with_file(true) 47 | .with_line_number(true) 48 | .with_level(true); 49 | 50 | // Choose output destination 51 | if let Some(log_file_path) = log_file { 52 | // Log to file (for terminal UI) 53 | let file = OpenOptions::new() 54 | .create(true) 55 | .append(true) 56 | .open(&log_file_path) 57 | .unwrap_or_else(|_| { 58 | eprintln!( 59 | "Warning: Could not open log file {log_file_path:?}, falling back to stderr" 60 | ); 61 | std::fs::File::create("/dev/null").unwrap_or_else(|_| { 62 | // On Windows, use NUL device 63 | std::fs::File::create("NUL").unwrap_or_else(|_| { 64 | panic!("Could not create log file or null device"); 65 | }) 66 | }) 67 | }); 68 | 69 | subscriber 70 | .with_writer(move || { 71 | Box::new(file.try_clone().expect("Failed to clone file handle")) 72 | as Box 73 | }) 74 | .init(); 75 | } else { 76 | // For server mode, write only to stderr to keep stdout clean for JSON-RPC 77 | let subscriber: SubscriberBuilder<_, _, _, fn() -> Box> = if to_stdout 78 | { 79 | subscriber.with_writer(|| Box::new(std::io::stdout()) as Box) 80 | } else { 81 | subscriber.with_writer(|| Box::new(std::io::stderr()) as Box) 82 | }; 83 | 84 | subscriber.init(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tests/sandbox_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | mod macos_sandbox_tests { 3 | use command_executor::{CommandExecutor, DefaultCommandExecutor, SandboxedCommandExecutor}; 4 | use sandbox::SandboxPolicy; 5 | use std::path::Path; 6 | use tempfile::tempdir; 7 | 8 | fn executor_with_policy(policy: SandboxPolicy) -> SandboxedCommandExecutor { 9 | SandboxedCommandExecutor::new(Box::new(DefaultCommandExecutor), policy, None, None) 10 | } 11 | 12 | fn workspace_policy(root: &Path) -> SandboxPolicy { 13 | SandboxPolicy::WorkspaceWrite { 14 | writable_roots: vec![root.to_path_buf()], 15 | network_access: false, 16 | exclude_tmpdir_env_var: true, 17 | exclude_slash_tmp: true, 18 | } 19 | } 20 | 21 | #[tokio::test] 22 | async fn read_only_policy_blocks_writes() { 23 | let temp = tempdir().expect("tempdir"); 24 | let working_dir = temp.path().to_path_buf(); 25 | 26 | let executor = executor_with_policy(SandboxPolicy::ReadOnly); 27 | let result = executor 28 | .execute("echo blocked > denied.txt", Some(&working_dir), None) 29 | .await 30 | .expect("command result"); 31 | assert!( 32 | !result.success, 33 | "read-only sandbox should deny file writes (output: {})", 34 | result.output 35 | ); 36 | assert!( 37 | !working_dir.join("denied.txt").exists(), 38 | "file should not be created" 39 | ); 40 | } 41 | 42 | #[tokio::test] 43 | async fn workspace_write_allows_within_root() { 44 | let temp = tempdir().expect("tempdir"); 45 | let working_dir = temp.path().to_path_buf(); 46 | 47 | let policy = workspace_policy(&working_dir); 48 | let executor = executor_with_policy(policy); 49 | let result = executor 50 | .execute("echo allowed > ok.txt", Some(&working_dir), None) 51 | .await 52 | .expect("command result"); 53 | assert!( 54 | result.success, 55 | "workspace write policy should allow writes (output: {})", 56 | result.output 57 | ); 58 | assert!( 59 | working_dir.join("ok.txt").exists(), 60 | "file should be created" 61 | ); 62 | } 63 | 64 | #[tokio::test] 65 | async fn workspace_write_blocks_outside_root() { 66 | let temp_parent = tempdir().expect("tempdir"); 67 | let working_dir = temp_parent.path().join("project"); 68 | std::fs::create_dir_all(&working_dir).expect("create project dir"); 69 | 70 | let policy = workspace_policy(&working_dir); 71 | let executor = executor_with_policy(policy); 72 | let result = executor 73 | .execute("echo nope > ../outside.txt", Some(&working_dir), None) 74 | .await 75 | .expect("command result"); 76 | assert!( 77 | !result.success, 78 | "workspace write policy should block writes outside root (output: {})", 79 | result.output 80 | ); 81 | assert!( 82 | !temp_parent.path().join("outside.txt").exists(), 83 | "outside file should not be created" 84 | ); 85 | } 86 | } 87 | 88 | #[cfg(not(target_os = "macos"))] 89 | mod sandbox_tests_placeholder { 90 | #[test] 91 | fn sandbox_tests_skip_on_non_macos() { 92 | // Seatbelt enforcement is macOS-specific, so these tests are skipped elsewhere. 93 | assert!(true); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/code_assistant/src/tools/core/dyn_tool.rs: -------------------------------------------------------------------------------- 1 | use super::render::Render; 2 | use super::result::ToolResult; 3 | use super::spec::ToolSpec; 4 | use super::tool::{Tool, ToolContext}; 5 | use crate::types::ToolError; 6 | use anyhow::Result; 7 | use serde::de::DeserializeOwned; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_json::Value; 10 | 11 | /// Type-erased tool output that can be rendered and determined for success 12 | pub trait AnyOutput: Send + Sync { 13 | /// Get a reference to the output as a Render trait object 14 | fn as_render(&self) -> &dyn Render; 15 | 16 | /// Determine if the tool execution was successful 17 | fn is_success(&self) -> bool; 18 | 19 | /// Serialize this output to a JSON value 20 | #[allow(dead_code)] 21 | fn to_json(&self) -> Result; 22 | } 23 | 24 | /// Automatically implemented for all types that implement both Render, ToolResult and Serialize 25 | impl AnyOutput for T { 26 | fn as_render(&self) -> &dyn Render { 27 | self 28 | } 29 | 30 | fn is_success(&self) -> bool { 31 | ToolResult::is_success(self) 32 | } 33 | 34 | fn to_json(&self) -> Result { 35 | serde_json::to_value(self).map_err(|e| anyhow::anyhow!("Failed to serialize output: {e}")) 36 | } 37 | } 38 | 39 | /// Type-erased tool interface for storing heterogeneous tools in collections 40 | #[async_trait::async_trait] 41 | pub trait DynTool: Send + Sync + 'static { 42 | /// Get the static metadata for this tool 43 | fn spec(&self) -> ToolSpec; 44 | 45 | /// Invoke the tool with JSON parameters and get a type-erased output 46 | async fn invoke<'a>( 47 | &self, 48 | context: &mut ToolContext<'a>, 49 | params: &mut Value, 50 | ) -> Result>; 51 | 52 | /// Deserialize a JSON value into this tool's output type 53 | fn deserialize_output(&self, json: Value) -> Result>; 54 | } 55 | 56 | /// Automatic implementation of DynTool for any type that implements Tool 57 | #[async_trait::async_trait] 58 | impl DynTool for T 59 | where 60 | T: Tool, 61 | T::Input: DeserializeOwned, 62 | T::Output: Render + ToolResult + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static, 63 | { 64 | fn spec(&self) -> ToolSpec { 65 | Tool::spec(self) 66 | } 67 | 68 | async fn invoke<'a>( 69 | &self, 70 | context: &mut ToolContext<'a>, 71 | params: &mut Value, 72 | ) -> Result> { 73 | // Deserialize input 74 | let mut input: T::Input = serde_json::from_value(params.clone()).map_err(|e| { 75 | // Convert Serde error to ToolError::ParseError 76 | ToolError::ParseError(format!("Failed to parse parameters: {e}")) 77 | })?; 78 | 79 | // Execute the tool 80 | let output = self.execute(context, &mut input).await?; 81 | 82 | // Serialize the potentially updated input back to JSON 83 | *params = serde_json::to_value(input) 84 | .map_err(|e| anyhow::anyhow!("Failed to serialize updated input: {e}"))?; 85 | 86 | // Box the output as AnyOutput 87 | Ok(Box::new(output) as Box) 88 | } 89 | 90 | fn deserialize_output(&self, json: Value) -> Result> { 91 | // Use the tool's deserialize_output method 92 | let output = Tool::deserialize_output(self, json)?; 93 | 94 | // Box the output as AnyOutput 95 | Ok(Box::new(output) as Box) 96 | } 97 | } 98 | --------------------------------------------------------------------------------