llm 1.3.8

A Rust library unifying multiple LLM backends.
Documentation
use chrono::Utc;

use crate::conversation::ConversationId;
use crate::history::SnapshotId;
use crate::runtime::{BacktrackOverlayState, OverlayState, PickerItem, PickerState};

use super::AppController;

impl AppController {
    pub fn record_snapshot(&mut self) -> bool {
        let Some(conv) = self.state.active_conversation().cloned() else {
            return false;
        };
        self.state.backtrack.record(&conv).is_some()
    }

    pub fn open_backtrack(&mut self) -> bool {
        let conv_id = match self.state.active_conversation_id() {
            Some(id) => id,
            None => return false,
        };
        if let Some(conv) = self.state.active_conversation().cloned() {
            self.state.backtrack.record(&conv);
        }
        let entries = self.state.backtrack.list(conv_id);
        if entries.is_empty() {
            self.set_status(crate::runtime::AppStatus::Error(
                "no snapshots available".to_string(),
            ));
            return false;
        }
        self.state.overlay = OverlayState::Backtrack(BacktrackOverlayState::new(entries));
        true
    }

    pub fn restore_snapshot(&mut self, snapshot_id: SnapshotId) -> bool {
        let conv_id = match self.state.active_conversation_id() {
            Some(id) => id,
            None => return false,
        };
        let snapshot = match self.state.backtrack.get(conv_id, snapshot_id) {
            Some(snapshot) => snapshot.clone(),
            None => return false,
        };
        let mut conv = snapshot.conversation.clone();
        let parent = conv.id;
        let now = Utc::now();
        conv.id = ConversationId::new();
        conv.parent_id = Some(parent);
        conv.created_at = now;
        conv.updated_at = now;
        conv.title = format!("Branch of {}", conv.title);
        conv.dirty = true;
        self.state.conversations.add(conv);
        self.state.scroll.reset();
        self.record_snapshot();
        true
    }

    pub fn open_branches(&mut self) -> bool {
        let items = self
            .state
            .conversations
            .list()
            .filter(|conv| conv.parent_id.is_some())
            .map(|conv| PickerItem {
                id: conv.id.to_string(),
                label: conv.title.clone(),
                meta: conv.parent_id.map(|id| id.to_string()),
                badges: vec!["branch".to_string()],
            })
            .collect();
        self.state.overlay = OverlayState::ConversationPicker(PickerState::new("Branches", items));
        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::AppConfig;
    use crate::conversation::{ConversationMessage, MessageKind, MessageRole};
    use crate::persistence::JsonConversationStore;
    use crate::runtime::{AppState, StreamManager};
    use crate::terminal::TerminalCapabilities;
    use crate::tools::{ToolContext, ToolRegistry};
    use crate::{config::ConfigPaths, provider::ProviderRegistry};

    fn build_controller() -> AppController {
        let config = AppConfig::default();
        let registry = ProviderRegistry::from_config(&config.providers);
        let store = JsonConversationStore::new(std::env::temp_dir().join("llm-test-conv"));
        let terminal_caps = TerminalCapabilities::detect();
        let state = AppState::new(config.clone(), registry, store, terminal_caps);
        let (tx, _rx) = tokio::sync::mpsc::channel(1);
        let stream_manager = StreamManager::new(tx.clone());
        let tool_registry = ToolRegistry::from_config(&config.tools);
        let tool_context = ToolContext::new(".".to_string())
            .with_allowed_paths(config.tools.allowed_paths.clone())
            .with_timeout(config.tools.timeout_ms);
        let config_paths =
            ConfigPaths::resolve(Some(std::env::temp_dir().join("llm-test-config.toml"))).unwrap();
        let params = crate::runtime::controller::AppControllerParams {
            state,
            stream_manager,
            event_sender: tx,
            tool_registry,
            tool_context,
            config_paths,
        };
        AppController::new(params)
    }

    #[test]
    fn restores_snapshot_as_branch() {
        let mut controller = build_controller();
        controller
            .state
            .conversations
            .new_conversation("openai".into(), None, None);
        let conv_id = controller.state.active_conversation_id().unwrap();
        if let Some(conv) = controller.state.active_conversation_mut() {
            conv.push_message(ConversationMessage::new(
                MessageRole::User,
                MessageKind::Text("hello".to_string()),
            ));
        }
        let conv = controller.state.active_conversation().cloned().unwrap();
        let snapshot = controller.state.backtrack.record(&conv).unwrap();
        assert!(controller.restore_snapshot(snapshot.id));
        let new_id = controller.state.active_conversation_id().unwrap();
        assert_ne!(new_id, conv_id);
        let new_conv = controller.state.active_conversation().unwrap();
        assert_eq!(new_conv.parent_id, Some(conv_id));
    }
}