Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions crates/code_assistant/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ edition = "2021"
[features]
default = ["document-conversion"]
document-conversion = ["transmutation", "fs_explorer/document-conversion"]
# Enable the GPUI desktop UI. Requires Xcode / Metal on macOS.
# Without this flag the binary is TUI-only and compiles without Metal tools.
gpui-ui = ["dep:gpui", "dep:gpui_platform", "dep:gpui-component", "dep:terminal", "dep:terminal_view"]

[dependencies]
command_executor = { path = "../command_executor" }
fs_explorer = { path = "../fs_explorer" }
git = { path = "../git" }
llm = { path = "../llm" }
sandbox = { path = "../sandbox" }
terminal = { path = "../terminal" }
terminal_view = { path = "../terminal_view" }
terminal = { path = "../terminal", optional = true }
terminal_view = { path = "../terminal_view", optional = true }
web = { path = "../web" }

glob = "0.3"
Expand Down Expand Up @@ -42,10 +45,10 @@ textwrap = "0.16"
tui-markdown = "=0.3.6"
derive_more = { version = "2", features = ["is_variant"] }

# GPUI related
gpui = "0.2.2"
gpui_platform = { version = "0.1", features = ["font-kit"] }
gpui-component = "0.5.2"
# GPUI related — only needed when the "gpui" feature is enabled
gpui = { version = "0.2.2", features = ["test-support"], optional = true }
gpui_platform = { version = "0.1", features = ["font-kit"], optional = true }
gpui-component = { version = "0.5.2", optional = true }
smallvec = "1.15"
rust-embed = { version = "8.9", features = ["include-exclude"] }

Expand Down Expand Up @@ -110,6 +113,5 @@ tokio-util = { version = "0.7", features = ["compat"] }
core-text = "=21.0.0"

[dev-dependencies]
gpui = { version = "0.2.2", features = ["test-support"] }
Comment thread
stippi marked this conversation as resolved.
axum = "0.7"
bytes = "1.10"
1 change: 1 addition & 0 deletions crates/code_assistant/src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod acp;
#[cfg(feature = "gpui-ui")]
pub mod gpui;
pub mod server;
pub mod terminal;
Expand Down
13 changes: 8 additions & 5 deletions crates/code_assistant/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,14 @@ impl Args {
);
}

// Check persisted default model from settings
let settings = crate::ui::gpui::shared::settings::UiSettings::load();
if let Some(ref default_model) = settings.default_model {
if config.get_model(default_model).is_some() {
return Ok(default_model.clone());
// Check persisted default model from settings (only available in GPUI builds)
#[cfg(feature = "gpui-ui")]
{
let settings = crate::ui::gpui::shared::settings::UiSettings::load();
if let Some(ref default_model) = settings.default_model {
if config.get_model(default_model).is_some() {
return Ok(default_model.clone());
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions crates/code_assistant/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ async fn main() -> Result<()> {
None => {
if args.ui {
// GPUI mode - use stderr to keep stdout clean
#[cfg(not(feature = "gpui-ui"))]
anyhow::bail!(
"This binary was compiled without the 'gpui' feature. \
Rebuild with `cargo build --features gpui` to enable the desktop UI."
);
#[cfg(feature = "gpui-ui")]
setup_logging(args.verbose, false);
} else {
// Terminal UI mode - log to file to prevent UI interference
Expand All @@ -96,6 +102,9 @@ async fn main() -> Result<()> {
// In GUI mode, allow starting without a valid model config
// (the settings screen will guide the user through setup).
let model_name = if args.ui {
#[cfg(not(feature = "gpui-ui"))]
unreachable!("--ui requires the gpui feature");
#[cfg(feature = "gpui-ui")]
args.get_model_name().unwrap_or_default()
} else {
args.get_model_name()?
Expand All @@ -116,6 +125,9 @@ async fn main() -> Result<()> {
};

if args.ui {
#[cfg(not(feature = "gpui-ui"))]
unreachable!("--ui requires the gpui feature");
#[cfg(feature = "gpui-ui")]
app::gpui::run(config)
} else {
app::terminal::run(config).await
Expand Down
5 changes: 5 additions & 0 deletions crates/code_assistant/src/ui/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use crate::config::{save_project, DefaultProjectManager};
use crate::persistence::{ChatMetadata, DraftAttachment, SessionModelConfig};
use crate::session::SessionManager;
use crate::types::Project;
#[cfg(feature = "gpui-ui")]
use crate::ui::gpui::terminal::executor::GpuiTerminalCommandExecutor;
use crate::ui::UserInterface;
use crate::utils::content::content_blocks_from;
use command_executor::DefaultCommandExecutor;
use llm::factory::create_llm_client_from_model;
use llm::provider_config::ConfigurationSystem;
use sandbox::SandboxPolicy;
Expand Down Expand Up @@ -649,7 +651,10 @@ async fn handle_send_user_message(
// Start the agent (message already added)
let result = {
let project_manager = Box::new(DefaultProjectManager::new());
#[cfg(feature = "gpui-ui")]
let command_executor = Box::new(GpuiTerminalCommandExecutor::new(session_id.to_string()));
#[cfg(not(feature = "gpui-ui"))]
let command_executor = Box::new(DefaultCommandExecutor);
let user_interface = ui.clone();

// Check if session has stored model config, otherwise use global config
Expand Down
1 change: 1 addition & 0 deletions crates/code_assistant/src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod backend;
#[cfg(feature = "gpui-ui")]
pub mod gpui;
pub mod streaming;
pub mod terminal;
Expand Down
85 changes: 71 additions & 14 deletions crates/code_assistant/src/ui/terminal/commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
use anyhow::Result;
use llm::provider_config::ConfigurationSystem;

/// Static descriptor for a slash command, used for autocomplete and help display.
pub struct SlashCommand {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub description: &'static str,
}

/// All registered slash commands.
///
/// This slice is the single source of truth for command discovery. Every entry here
/// corresponds to a match arm in `CommandProcessor::process_command`. Keeping them
/// in sync is intentional: adding a command requires updating both places.
pub fn all_commands() -> &'static [SlashCommand] {
&[
SlashCommand {
name: "help",
aliases: &["h"],
description: "Show available commands",
},
SlashCommand {
name: "model",
aliases: &["m"],
description: "List available models or switch to one: /model <name>",
},
SlashCommand {
name: "provider",
aliases: &["p"],
description: "List available LLM providers",
},
SlashCommand {
name: "current",
aliases: &["c"],
description: "Show the currently active model",
},
SlashCommand {
name: "plan",
aliases: &[],
description: "Toggle the plan view panel",
},
SlashCommand {
name: "clear",
aliases: &[],
description: "Clear the conversation context",
},
SlashCommand {
name: "compact",
aliases: &[],
description: "Summarize and compact the conversation context",
},
]
}

/// Result of processing a slash command
#[derive(Debug, Clone)]
pub enum CommandResult {
Expand Down Expand Up @@ -84,20 +136,25 @@ impl CommandProcessor {
}

fn get_help_text(&self) -> String {
concat!(
"Available commands:\n",
"/help, /h - Show this help\n",
"/model, /m - List available models\n",
"/model <name> - Switch to model\n",
"/provider, /p - List available providers\n",
"/current, /c - Show current model\n",
"/plan - Toggle plan view\n",
"\n",
"Examples:\n",
"/model Claude Sonnet 4.5\n",
"/model GPT-5",
)
.to_string()
let mut text = String::from("Available commands:\n");
for cmd in all_commands() {
if cmd.aliases.is_empty() {
text.push_str(&format!(" /{:<16} {}\n", cmd.name, cmd.description));
} else {
let alias_list = cmd
.aliases
.iter()
.map(|a| format!("/{a}"))
.collect::<Vec<_>>()
.join(", ");
text.push_str(&format!(
" /{}, {:<12} {}\n",
cmd.name, alias_list, cmd.description
));
}
}
text.push_str("\nExamples:\n /model Claude Sonnet 4.5\n /model GPT-5");
text
}

/// Get formatted list of available models
Expand Down
15 changes: 13 additions & 2 deletions crates/code_assistant/src/ui/ui_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ use crate::ui::{DisplayFragment, ToolStatus};
use sandbox::SandboxPolicy;
use std::path::PathBuf;

// When the gpui-ui feature is enabled, StyledLine comes from the `terminal` crate
// (which captures ANSI-coloured output from the interactive terminal tool).
// Without gpui-ui the field is always `None`, so we use a zero-sized stub that
// satisfies the type checker without pulling in the Metal dependency chain.
#[cfg(feature = "gpui-ui")]
pub use terminal::StyledLine;

#[cfg(not(feature = "gpui-ui"))]
#[derive(Debug, Clone)]
pub struct StyledLine;

/// Role of a message in the conversation.
#[derive(Debug, Clone, PartialEq)]
pub enum MessageRole {
Expand Down Expand Up @@ -34,7 +45,7 @@ pub struct ToolResultData {
pub message: Option<String>,
pub output: Option<String>,
/// Styled terminal output with ANSI color information preserved.
pub styled_output: Option<Vec<terminal::StyledLine>>,
pub styled_output: Option<Vec<StyledLine>>,
/// Duration of the tool execution in seconds, computed from persisted ContentBlock timestamps.
pub duration_seconds: Option<f64>,
/// Image data from tools that produce visual output (e.g. view_images).
Expand Down Expand Up @@ -79,7 +90,7 @@ pub enum UiEvent {
message: Option<String>,
output: Option<String>,
/// Styled terminal output with ANSI color information preserved.
styled_output: Option<Vec<terminal::StyledLine>>,
styled_output: Option<Vec<StyledLine>>,
/// Execution duration in seconds, set from ContentBlock timestamps on completion.
duration_seconds: Option<f64>,
/// Image data from tools that produce visual output (e.g. view_images).
Expand Down