i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
use crate::storage::DeveloperProfile;
use crate::semantic::CodeEmbedding;
use serde::{Deserialize, Serialize};

/// Context provided to AI for personalized responses
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AIContext {
    pub profile: Option<DeveloperProfile>,
    pub relevant_code: Vec<CodeSnippet>,
    pub conversation_history: Vec<Message>,
    pub metadata: ContextMetadata,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeSnippet {
    pub content: String,
    pub language: String,
    pub source_file: String,
    pub relevance_score: f32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
    pub role: MessageRole,
    pub content: String,
    pub timestamp: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageRole {
    User,
    Assistant,
    System,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContextMetadata {
    pub query_type: Option<String>,
    pub technologies: Vec<String>,
    pub repositories: Vec<String>,
}

impl AIContext {
    pub fn new() -> Self {
        Self {
            profile: None,
            relevant_code: Vec::new(),
            conversation_history: Vec::new(),
            metadata: ContextMetadata::default(),
        }
    }

    pub fn with_profile(mut self, profile: DeveloperProfile) -> Self {
        self.profile = Some(profile);
        self
    }

    pub fn with_code(mut self, embeddings: &[CodeEmbedding], top_k: usize) -> Self {
        self.relevant_code = embeddings.iter()
            .take(top_k)
            .map(|e| CodeSnippet {
                content: e.content.clone(),
                language: e.metadata.language.clone(),
                source_file: e.metadata.source_file.clone(),
                relevance_score: 1.0, // Would be set from search score
            })
            .collect();
        self
    }

    pub fn add_message(&mut self, role: MessageRole, content: String) {
        self.conversation_history.push(Message {
            role,
            content,
            timestamp: chrono::Utc::now(),
        });
    }

    /// Format context as a prompt prefix
    pub fn format_context(&self) -> String {
        let mut context = String::new();

        // Add developer profile context
        if let Some(profile) = &self.profile {
            context.push_str("## Developer Context\n");
            context.push_str(&format!("Primary Languages: {}\n", 
                profile.github.primary_languages.iter()
                    .take(3)
                    .map(|(l, _)| l.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            ));
            
            if !profile.coding_patterns.preferred_languages.is_empty() {
                context.push_str(&format!("Preferred Languages: {}\n",
                    profile.coding_patterns.preferred_languages.join(", ")
                ));
            }
            context.push('\n');
        }

        // Add relevant code context
        if !self.relevant_code.is_empty() {
            context.push_str("## Relevant Code Examples\n\n");
            for (i, snippet) in self.relevant_code.iter().enumerate() {
                context.push_str(&format!("### Example {} ({} - {})\n",
                    i + 1,
                    snippet.language,
                    snippet.source_file
                ));
                context.push_str(&format!("```{}\\n{}\\n```\\n\\n",
                    snippet.language.to_lowercase(),
                    snippet.content
                ));
            }
        }

        context
    }

    /// Get recent conversation history formatted
    pub fn format_history(&self, max_messages: usize) -> String {
        let start = self.conversation_history.len().saturating_sub(max_messages);
        
        self.conversation_history[start..]
            .iter()
            .map(|m| format!("{}: {}", format_role(&m.role), m.content))
            .collect::<Vec<_>>()
            .join("\n")
    }
}

fn format_role(role: &MessageRole) -> &'static str {
    match role {
        MessageRole::User => "User",
        MessageRole::Assistant => "Assistant",
        MessageRole::System => "System",
    }
}