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", 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"] }
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
45 changes: 45 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 @@ -110,6 +112,21 @@ pub enum BackendEvent {
session_id: String,
},

/// Clear the conversation context (messages) for a session.
///
/// The session itself is kept alive; only the message history is wiped.
/// The UI transcript is cleared via `UiEvent::ClearMessages`.
ClearContext {
session_id: String,
},

/// Compact (summarise) conversation context for a session.
///
/// Not yet implemented — emits an informational message instead.
CompactContext {
session_id: String,
},

/// Incremental session refresh triggered by the file watcher.
/// Compares the on-disk state with the in-memory state and emits only
/// the delta (new messages appended by an external process).
Expand Down Expand Up @@ -391,6 +408,31 @@ pub async fn handle_backend_events(
handle_refresh_session(&multi_session_manager, &session_id, &ui).await
}

BackendEvent::ClearContext { session_id } => {
let mut manager = multi_session_manager.lock().await;
if let Some(session) = manager.get_session_mut(&session_id) {
let chat = &mut session.session;
chat.message_nodes.clear();
chat.active_path.clear();
chat.next_node_id = 1;
chat.messages.clear();
chat.plan = Default::default();
}
let _ = ui.send_event(crate::ui::UiEvent::ClearMessages).await;
None
}

BackendEvent::CompactContext { session_id: _ } => {
let _ = ui
.send_event(crate::ui::UiEvent::DisplayError {
message:
"Compact is not yet implemented. Use /clear to reset context."
.to_string(),
})
.await;
None
}

BackendEvent::UpdateDefaultModel { model_name } => {
debug!("Updating default model name to: {}", model_name);
let mut manager = multi_session_manager.lock().await;
Expand Down Expand Up @@ -649,7 +691,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
22 changes: 22 additions & 0 deletions crates/code_assistant/src/ui/terminal/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,28 @@ async fn event_loop(
renderer_guard.set_plan_expanded(expanded);
renderer_guard.set_overlay_active(overlay_active);
}
KeyEventResult::ClearContext => {
let current_session_id = {
let state = app_state.lock().await;
state.current_session_id.clone()
};
if let Some(session_id) = current_session_id {
let _ = backend_event_tx
.send(BackendEvent::ClearContext { session_id })
.await;
}
}
KeyEventResult::CompactContext => {
let current_session_id = {
let state = app_state.lock().await;
state.current_session_id.clone()
};
if let Some(session_id) = current_session_id {
let _ = backend_event_tx
.send(BackendEvent::CompactContext { session_id })
.await;
}
}
}
needs_redraw = true;
}
Expand Down
91 changes: 77 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 All @@ -20,6 +72,10 @@ pub enum CommandResult {
InvalidCommand(String),
/// Toggle plan rendering mode
TogglePlan,
/// Clear conversation context
ClearContext,
/// Compact (summarise) conversation context
CompactContext,
}

/// Process slash commands in terminal UI
Expand Down Expand Up @@ -52,6 +108,8 @@ impl CommandProcessor {
"provider" | "p" => self.process_provider_command(&parts[1..]),
"current" | "c" => CommandResult::ShowCurrentModel,
"plan" => CommandResult::TogglePlan,
"clear" => CommandResult::ClearContext,
"compact" => CommandResult::CompactContext,
_ => CommandResult::InvalidCommand(format!("Unknown command: /{}", parts[0])),
}
}
Expand Down Expand Up @@ -84,20 +142,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
6 changes: 6 additions & 0 deletions crates/code_assistant/src/ui/terminal/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub enum KeyEventResult {
ShowCurrentModel,
/// Toggle plan rendering mode
TogglePlan,
/// Clear conversation context
ClearContext,
/// Compact (summarise) conversation context
CompactContext,
}

/// Manages the input area using the custom TextArea widget
Expand Down Expand Up @@ -128,6 +132,8 @@ impl InputManager {
}
CommandResult::ShowCurrentModel => KeyEventResult::ShowCurrentModel,
CommandResult::TogglePlan => KeyEventResult::TogglePlan,
CommandResult::ClearContext => KeyEventResult::ClearContext,
CommandResult::CompactContext => KeyEventResult::CompactContext,
CommandResult::InvalidCommand(error) => {
KeyEventResult::ShowInfo(format!("Error: {error}"))
}
Expand Down
Loading