coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Core functionality for CoderLib
//!
//! This module contains the main CoderLib struct and core types that form
//! the foundation of the library.

pub mod agent;
pub mod session;
pub mod config;
pub mod error;

use std::sync::Arc;
use tokio::sync::broadcast;
use uuid::Uuid;

pub use agent::Agent;
pub use config::CoderLibConfig;
pub use error::CoderLibError;
pub use session::{Session, SessionManager};

use crate::integration::HostIntegration;
use crate::lsp::{
    LspManager, LspService, LspDiagnostic, Position, Range, Location,
    CompletionItem, Hover, CodeAction, SymbolInformation
};

/// Main CoderLib instance that provides the public API
pub struct CoderLib {
    agent: Agent,
    session_manager: SessionManager,
    config: CoderLibConfig,
    lsp_manager: Option<LspManager>,
    host_integration: Option<Arc<dyn HostIntegration>>,
}

/// Request for AI coding assistance
#[derive(Debug, Clone)]
pub struct CodeRequest {
    /// Session ID for this request
    pub session_id: String,
    /// The prompt/content for the AI
    pub content: String,
    /// File attachments (if any)
    pub attachments: Vec<Attachment>,
    /// Optional model override
    pub model: Option<String>,
    /// Context about the current editing state
    pub context: RequestContext,
}

/// File attachment for a code request
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Attachment {
    /// File path
    pub path: std::path::PathBuf,
    /// MIME type
    pub mime_type: String,
    /// File content
    pub content: Vec<u8>,
}

/// Context information about the current editing state
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct RequestContext {
    /// Currently active file
    pub current_file: Option<std::path::PathBuf>,
    /// Cursor position in the current file
    pub cursor_position: Option<Position>,
    /// Current text selection
    pub selection: Option<Range>,
    /// Project root directory
    pub project_root: Option<std::path::PathBuf>,
    /// List of currently open files
    pub open_files: Vec<std::path::PathBuf>,
}



/// Response from AI processing
#[derive(Debug, Clone)]
pub struct CodeResponse {
    /// Session ID
    pub session_id: String,
    /// Response content
    pub content: String,
    /// Whether this is the final response
    pub is_complete: bool,
    /// Any tool calls made during processing
    pub tool_calls: Vec<ToolCall>,
    /// Token usage information
    pub token_usage: Option<TokenUsage>,
}

/// Information about a tool call made by the AI
#[derive(Debug, Clone)]
pub struct ToolCall {
    /// Unique ID for this tool call
    pub id: String,
    /// Name of the tool
    pub name: String,
    /// Tool parameters as JSON
    pub parameters: serde_json::Value,
    /// Tool execution result
    pub result: Option<String>,
}

/// Token usage statistics
#[derive(Debug, Clone, Default)]
pub struct TokenUsage {
    /// Input tokens used
    pub input_tokens: u32,
    /// Output tokens generated
    pub output_tokens: u32,
    /// Total tokens (input + output + cache)
    pub total_tokens: u32,
    /// Cache creation tokens (if applicable)
    pub cache_creation_tokens: u32,
    /// Cache read tokens (if applicable)
    pub cache_read_tokens: u32,
}

impl TokenUsage {
    /// Create a new TokenUsage instance
    pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
        let total_tokens = input_tokens + output_tokens;
        Self {
            input_tokens,
            output_tokens,
            total_tokens,
            cache_creation_tokens: 0,
            cache_read_tokens: 0,
        }
    }

    /// Get the total number of tokens used
    pub fn total(&self) -> u32 {
        self.input_tokens + self.output_tokens + self.cache_creation_tokens + self.cache_read_tokens
    }

    /// Update total tokens to match the sum of all components
    pub fn update_total(&mut self) {
        self.total_tokens = self.input_tokens + self.output_tokens + self.cache_creation_tokens + self.cache_read_tokens;
    }
}

impl Default for CodeRequest {
    fn default() -> Self {
        Self {
            session_id: Uuid::new_v4().to_string(),
            content: String::new(),
            attachments: Vec::new(),
            model: None,
            context: RequestContext::default(),
        }
    }
}

impl CoderLib {
    /// Create a new CoderLib instance
    pub async fn new(config: CoderLibConfig) -> Result<Self, CoderLibError> {
        let storage = config.create_storage().await?;
        let session_manager = SessionManager::new(storage.clone());
        let agent = Agent::new(config.clone(), storage).await?;

        // Initialize LSP manager if enabled
        let lsp_manager = if config.lsp.enabled {
            let mut manager = LspManager::new(config.lsp.clone());
            // Initialize with current directory as workspace root
            let workspace_root = std::env::current_dir().ok();
            if let Err(e) = manager.initialize(workspace_root).await {
                tracing::warn!("Failed to initialize LSP manager: {}", e);
                None
            } else {
                Some(manager)
            }
        } else {
            None
        };

        Ok(Self {
            agent,
            session_manager,
            config,
            lsp_manager,
            host_integration: None,
        })
    }

    /// Set the host integration for callbacks and permissions
    pub fn set_host_integration(&mut self, host: Arc<dyn HostIntegration>) {
        self.host_integration = Some(host.clone());
        self.agent.set_host_integration(host);
    }

    /// Process a coding request and return a stream of responses
    pub async fn process_request(
        &self,
        request: CodeRequest,
    ) -> Result<broadcast::Receiver<CodeResponse>, CoderLibError> {
        self.agent.process_request(request).await
    }

    /// Create a new coding session
    pub async fn create_session(&self, title: Option<String>) -> Result<String, CoderLibError> {
        self.session_manager.create_session(title).await.map_err(CoderLibError::Session)
    }

    /// Get session information
    pub async fn get_session(&self, session_id: &str) -> Result<Option<Session>, CoderLibError> {
        self.session_manager.get_session(session_id).await.map_err(CoderLibError::Session)
    }

    /// List all sessions
    pub async fn list_sessions(&self) -> Result<Vec<Session>, CoderLibError> {
        self.session_manager.list_sessions().await.map_err(CoderLibError::Session)
    }

    /// Cancel an ongoing request
    pub async fn cancel_request(&self, session_id: &str) -> Result<(), CoderLibError> {
        self.agent.cancel_request(session_id).await
    }

    /// Get available AI models
    pub fn available_models(&self) -> Vec<crate::llm::Model> {
        self.agent.available_models()
    }

    /// Update configuration at runtime
    pub async fn update_config(&mut self, config: CoderLibConfig) -> Result<(), CoderLibError> {
        self.config = config.clone();
        self.agent.update_config(config).await
    }

    // LSP-specific methods

    /// Get diagnostics for a file (if LSP is available)
    pub async fn get_diagnostics(&self, file_path: &std::path::Path) -> Result<Vec<LspDiagnostic>, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.get_diagnostics(file_path).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(Vec::new())
        }
    }

    /// Notify LSP that a file was opened
    pub async fn lsp_did_open(&self, file_path: &std::path::Path, content: &str) -> Result<(), CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.did_open_file(file_path, content).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(())
        }
    }

    /// Notify LSP that a file was changed
    pub async fn lsp_did_change(&self, file_path: &std::path::Path, content: &str) -> Result<(), CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.did_change_file(file_path, content).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(())
        }
    }

    /// Notify LSP that a file was closed
    pub async fn lsp_did_close(&self, file_path: &std::path::Path) -> Result<(), CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.did_close_file(file_path).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(())
        }
    }

    /// Check if LSP support is available for a file
    pub fn lsp_supports_file(&self, file_path: &std::path::Path) -> bool {
        if let Some(lsp) = &self.lsp_manager {
            lsp.supports_file(file_path)
        } else {
            false
        }
    }

    /// Get list of active LSP servers
    pub async fn lsp_active_servers(&self) -> Vec<String> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.active_servers().await
        } else {
            Vec::new()
        }
    }

    /// Get code completions at a position
    pub async fn lsp_get_completions(&self, file_path: &std::path::Path, position: Position) -> Result<Vec<CompletionItem>, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.get_completions(file_path, position).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(Vec::new())
        }
    }

    /// Go to definition of symbol at position
    pub async fn lsp_goto_definition(&self, file_path: &std::path::Path, position: Position) -> Result<Vec<Location>, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.goto_definition(file_path, position).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(Vec::new())
        }
    }

    /// Get hover information at position
    pub async fn lsp_get_hover(&self, file_path: &std::path::Path, position: Position) -> Result<Option<Hover>, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.get_hover(file_path, position).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(None)
        }
    }

    /// Find references to symbol at position
    pub async fn lsp_find_references(&self, file_path: &std::path::Path, position: Position, include_declaration: bool) -> Result<Vec<Location>, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.find_references(file_path, position, include_declaration).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(Vec::new())
        }
    }

    /// Get code actions for a range
    pub async fn lsp_get_code_actions(&self, file_path: &std::path::Path, range: Range) -> Result<Vec<CodeAction>, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.get_code_actions(file_path, range).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(Vec::new())
        }
    }

    /// Get document symbols
    pub async fn lsp_get_document_symbols(&self, file_path: &std::path::Path) -> Result<Vec<SymbolInformation>, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.get_document_symbols(file_path).await.map_err(CoderLibError::Lsp)
        } else {
            Ok(Vec::new())
        }
    }

    /// Format document
    pub async fn lsp_format_document(&self, file_path: &std::path::Path) -> Result<String, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.format_document(file_path).await.map_err(CoderLibError::Lsp)
        } else {
            Err(CoderLibError::Lsp(crate::lsp::LspError::ServerNotFound("LSP not available".to_string())))
        }
    }

    /// Format range in document
    pub async fn lsp_format_range(&self, file_path: &std::path::Path, range: Range) -> Result<String, CoderLibError> {
        if let Some(lsp) = &self.lsp_manager {
            lsp.format_range(file_path, range).await.map_err(CoderLibError::Lsp)
        } else {
            Err(CoderLibError::Lsp(crate::lsp::LspError::ServerNotFound("LSP not available".to_string())))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_position_creation() {
        let pos = Position { line: 10, character: 5 };
        assert_eq!(pos.line, 10);
        assert_eq!(pos.character, 5);
    }

    #[test]
    fn test_range_creation() {
        let range = Range {
            start: Position { line: 1, character: 0 },
            end: Position { line: 1, character: 10 },
        };
        assert_eq!(range.start.line, 1);
        assert_eq!(range.end.character, 10);
    }

    #[test]
    fn test_code_request_default() {
        let request = CodeRequest::default();
        assert!(!request.session_id.is_empty());
        assert!(request.content.is_empty());
        assert!(request.attachments.is_empty());
    }
}