stynx-code 3.2.1

stynx-code — interactive AI coding assistant
use std::sync::Arc;

use stynx_code_types::{Conversation, Role};

use super::skills::Skill;
use super::terminal::{BOLD, CYAN, DIM, RESET};

pub fn expand_with_pins(input: &str, pinned_files: &[String]) -> String {
    let mut s = super::command_extras::expand_at_mentions(input);
    for path in pinned_files {
        if let Ok(content) = std::fs::read_to_string(path) {
            s.push_str(&format!("\n\n<file path=\"{path}\">{content}</file>"));
        }
    }
    s
}

pub async fn save_session(repo: &Arc<dyn stynx_code_memory::SessionRepository>, c: &Conversation) {
    if let Err(e) = stynx_code_memory::save_session(repo, c).await {
        tracing::warn!("failed to save session: {e}");
    }
}

pub fn show_skills(skills: &[Skill]) {
    if skills.is_empty() {
        println!("  {DIM}No skills loaded. Add .md files to ~/.claude/skills/ or .claude/skills/{RESET}\n");
    } else {
        println!("  {BOLD}{CYAN}Skills{RESET}");
        for s in skills {
            let hint = s.argument_hint.as_deref().map(|h| format!(" {DIM}{h}{RESET}")).unwrap_or_default();
            println!("  {DIM}/{}{RESET}{hint}  {DIM}{}{RESET}", s.name, s.description);
        }
        println!();
    }
}

pub fn show_files(pinned_files: &[String]) {
    if pinned_files.is_empty() { println!("  {DIM}No files pinned. Use /add <path> to pin files.{RESET}\n"); }
    else { println!("  {DIM}Pinned files:{RESET}"); for f in pinned_files { println!("  {DIM}  · {f}{RESET}"); } println!(); }
}

pub fn handle_add(path: &str, pinned_files: &mut Vec<String>) {
    let path = path.trim().to_string();
    if std::path::Path::new(&path).exists() {
        println!("  {DIM}✓ Added {path} to context{RESET}\n");
        pinned_files.push(path);
    } else {
        println!("  {DIM}✗ File not found: {path}{RESET}\n");
    }
}

pub fn copy_last_response(conversation: &Conversation) {
    use std::io::Write;
    let last_text = conversation.messages.iter().rev()
        .find(|m| m.role == Role::Assistant)
        .map(|m| m.content.iter().filter_map(|b| match b {
            stynx_code_types::ContentBlock::Text { text } => Some(text.as_str()),
            _ => None,
        }).collect::<Vec<_>>().join("\n"));
    if let Some(text) = last_text {
        let mut child = if cfg!(windows) {
            std::process::Command::new("powershell")
                .args(["-NoProfile", "-Command", "Set-Clipboard -Value $input"])
                .stdin(std::process::Stdio::piped())
                .spawn()
        } else {
            std::process::Command::new("sh")
                .arg("-c")
                .arg("xclip -selection clipboard 2>/dev/null || xsel --clipboard 2>/dev/null || pbcopy 2>/dev/null")
                .stdin(std::process::Stdio::piped())
                .spawn()
        };
        match child {
            Ok(ref mut c) => {
                if let Some(ref mut stdin) = c.stdin { let _ = stdin.write_all(text.as_bytes()); }
                let _ = c.wait();
                println!("  {DIM}✓ Copied to clipboard{RESET}\n");
            }
            Err(_) => {
                if cfg!(windows) {
                    println!("  {DIM}✗ Failed to copy (powershell not available){RESET}\n");
                } else {
                    println!("  {DIM}✗ Install xclip or xsel for clipboard support{RESET}\n");
                }
            }
        }
    } else {
        println!("  {DIM}No response to copy.{RESET}\n");
    }
}