Skip to content
Merged
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
143 changes: 80 additions & 63 deletions codi-rs/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,75 +112,26 @@ impl Agent {
self.system_prompt = prompt.into();
}

/// Get tool definitions if tools are enabled and supported.
fn get_tool_definitions(&self) -> Option<Vec<ToolDefinition>> {
if self.config.use_tools && self.provider.supports_tool_use() {
Some(self.tool_registry.definitions())
} else {
None
}
/// Get the conversation summary if available.
pub fn conversation_summary(&self) -> Option<&str> {
self.state.conversation_summary.as_deref()
}

/// Build the system context including any conversation summary.
fn build_system_context(&self) -> String {
let mut context = self.system_prompt.clone();

if let Some(ref summary) = self.state.conversation_summary {
context.push_str("\n\n## Previous Conversation Summary\n");
context.push_str(summary);
}

context
/// Get the number of messages in the conversation.
pub fn message_count(&self) -> usize {
self.state.messages.len()
}

/// Estimate the current token count of all messages.
/// Uses the standard approximation of ~4 characters per token.
/// Leverages `running_char_count` to avoid re-serializing every message each iteration.
fn estimate_tokens(&self) -> usize {
let mut total_chars: usize = self.state.running_char_count;

// Add system prompt + summary (not cached — these are cheap to measure)
total_chars += self.system_prompt.len();
if let Some(ref summary) = self.state.conversation_summary {
total_chars += summary.len();
}

total_chars / 4
/// Force context compaction to reduce token usage.
/// Returns the number of messages that were summarized.
pub fn compact_context(&mut self) -> usize {
let msg_count_before = self.state.messages.len();
self.compact_context_internal();
msg_count_before.saturating_sub(self.state.messages.len())
}

/// Count the characters in a message's content.
fn message_char_count(&self, msg: &Message) -> usize {
match &msg.content {
crate::types::MessageContent::Text(s) => s.len(),
crate::types::MessageContent::Blocks(blocks) => {
blocks.iter().map(|b| {
let mut n = 0;
if let Some(ref t) = b.text { n += t.len(); }
if let Some(ref name) = b.name { n += name.len(); }
if let Some(ref input) = b.input {
n += input.to_string().len();
}
if let Some(ref content) = b.content { n += content.len(); }
n
}).sum()
}
}
}

/// Truncate a string to at most `max_chars` characters, appending "..." if truncated.
/// Safe for multi-byte UTF-8 (truncates at char boundary).
fn truncate_str(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars).collect();
format!("{}...", truncated)
}
}

/// Compact the conversation context when it exceeds the token limit.
/// Keeps the system prompt and recent messages, summarizes older ones.
fn compact_context(&mut self) {
/// Internal implementation of context compaction.
fn compact_context_internal(&mut self) {
// Notify that compaction is starting
if let Some(ref on_compaction) = self.callbacks.on_compaction {
on_compaction(true);
Expand Down Expand Up @@ -252,6 +203,72 @@ impl Agent {
}
}

/// Get tool definitions if tools are enabled and supported.
fn get_tool_definitions(&self) -> Option<Vec<ToolDefinition>> {
if self.config.use_tools && self.provider.supports_tool_use() {
Some(self.tool_registry.definitions())
} else {
None
}
}

/// Build the system context including any conversation summary.
fn build_system_context(&self) -> String {
let mut context = self.system_prompt.clone();

if let Some(ref summary) = self.state.conversation_summary {
context.push_str("\n\n## Previous Conversation Summary\n");
context.push_str(summary);
}

context
}

/// Estimate the current token count of all messages.
/// Uses the standard approximation of ~4 characters per token.
/// Leverages `running_char_count` to avoid re-serializing every message each iteration.
fn estimate_tokens(&self) -> usize {
let mut total_chars: usize = self.state.running_char_count;

// Add system prompt + summary (not cached — these are cheap to measure)
total_chars += self.system_prompt.len();
if let Some(ref summary) = self.state.conversation_summary {
total_chars += summary.len();
}

total_chars / 4
}

/// Count the characters in a message's content.
fn message_char_count(&self, msg: &Message) -> usize {
match &msg.content {
crate::types::MessageContent::Text(s) => s.len(),
crate::types::MessageContent::Blocks(blocks) => {
blocks.iter().map(|b| {
let mut n = 0;
if let Some(ref t) = b.text { n += t.len(); }
if let Some(ref name) = b.name { n += name.len(); }
if let Some(ref input) = b.input {
n += input.to_string().len();
}
if let Some(ref content) = b.content { n += content.len(); }
n
}).sum()
}
}
}

/// Truncate a string to at most `max_chars` characters, appending "..." if truncated.
/// Safe for multi-byte UTF-8 (truncates at char boundary).
fn truncate_str(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars).collect();
format!("{}...", truncated)
}
}

/// Check whether a tool call needs confirmation and, if so, ask the user.
///
/// Returns `None` when no confirmation is needed (tool is auto-approved and
Expand Down
70 changes: 4 additions & 66 deletions codi-rs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use codi::agent::AgentConfig;
use codi::config::{self, CliOptions};
use codi::providers::{create_provider_from_config, ProviderType};
use codi::tools::ToolRegistry;
use codi::tui::{App, build_system_prompt_from_config, run as run_tui};
use codi::tui::{App, build_system_prompt_from_config};
use codi::tui::terminal_ui::run_terminal_repl;

/// Codi version string.
const VERSION: &str = env!("CARGO_PKG_VERSION");
Expand Down Expand Up @@ -227,11 +228,6 @@ async fn main() -> anyhow::Result<()> {
let workspace_root = std::env::current_dir()?;
let config = config::load_config(&workspace_root, cli_options)?;

// Print startup message in non-quiet mode
if !cli.quiet {
print_startup_message(&config);
}

// Handle non-interactive mode
if let Some(prompt) = cli.prompt {
return handle_prompt(&config, &prompt, cli.output_format, cli.quiet, cli.yes).await;
Expand All @@ -241,20 +237,6 @@ async fn main() -> anyhow::Result<()> {
run_repl(&config, cli.yes).await
}

fn print_startup_message(config: &config::ResolvedConfig) {
println!(
"{} {} - Your AI coding wingman",
"codi".cyan().bold(),
format!("v{}", VERSION ).dimmed()
);
println!(
"Provider: {} | Model: {}",
config.provider.green(),
config.model.as_deref().unwrap_or("default").yellow()
);
println!();
}

async fn handle_command(command: Commands) -> anyhow::Result<()> {
match command {
Commands::Config { action } => {
Expand Down Expand Up @@ -514,50 +496,6 @@ async fn handle_prompt(
}

async fn run_repl(config: &config::ResolvedConfig, auto_approve: bool) -> anyhow::Result<()> {
// Create provider from configuration
let provider = create_provider_from_config(config)?;

// Create TUI app with project path, set config before provider
let mut app = App::with_project_path(std::env::current_dir()?);

// Set config and auto_approve flag, then set provider (which uses stored config)
app.set_config(config.clone());
app.set_auto_approve(auto_approve);
app.set_provider(provider);

// Load session if specified
if let Some(ref session_name) = config.default_session {
if let Err(e) = load_session(&mut app, session_name).await {
eprintln!("{} Failed to load session '{}': {}", "⚠".yellow(), session_name, e);
}
}

// Auto-index symbol index in background
let project_path_str = std::env::current_dir()?.to_string_lossy().to_string();
tokio::spawn(async move {
match codi::symbol_index::SymbolIndexService::new(&project_path_str).await {
Ok(service) => {
if let Err(e) = service.build(false).await {
tracing::warn!("Symbol index build failed: {}", e);
} else {
tracing::info!("Symbol index built successfully");
}
}
Err(e) => tracing::debug!("Symbol index init skipped: {}", e),
}
});

// Run TUI
match run_tui(&mut app).await {
Ok(_) => Ok(()),
Err(e) => Err(anyhow::anyhow!("TUI error: {}", e)),
}
}

async fn load_session(app: &mut App, session_name: &str) -> anyhow::Result<()> {
// Try to load by exact name first, then by fuzzy match
if let Err(_) = app.load_session(session_name).await {
return Err(anyhow::anyhow!("Session not found: {}", session_name));
}
Ok(())
// Use new terminal-style REPL instead of full-screen TUI
run_terminal_repl(config, auto_approve).await
}
Loading