aicx 0.6.6

Operator CLI + MCP server: canonical corpus first, optional semantic index second (Claude Code, Codex, Gemini)
Documentation
use std::fs;

use crate::intents::{self, IntentDisplayFilters, IntentRecord, IntentSortOrder};

#[derive(Debug)]
pub struct IntentsScreen {
    pub records: Vec<IntentRecord>,
    pub visible: Vec<IntentRecord>,
    pub selected: usize,
    pub query: String,
    pub project: Option<String>,
    pub agent: Option<String>,
    pub hours: u64,
    pub preview: String,
    pub status: String,
}

impl IntentsScreen {
    pub fn load(project: Option<String>, hours: u64, agent: Option<String>) -> Self {
        let project_filter = project.clone().unwrap_or_else(|| "aicx".to_string());
        let config = intents::IntentsConfig {
            project: project_filter.clone(),
            hours,
            strict: false,
            kind_filter: None,
            frame_kind: None,
        };

        match intents::extract_intents(&config) {
            Ok(records) => {
                let mut screen = Self {
                    records,
                    visible: Vec::new(),
                    selected: 0,
                    query: String::new(),
                    project,
                    agent,
                    hours,
                    preview: String::new(),
                    status: String::new(),
                };
                screen.apply_filters();
                screen.status = format!(
                    "{} intents loaded for project filter '{}'",
                    screen.visible.len(),
                    project_filter
                );
                screen
            }
            Err(error) => Self {
                records: Vec::new(),
                visible: Vec::new(),
                selected: 0,
                query: String::new(),
                project,
                agent,
                hours,
                preview: String::new(),
                status: format!("intent load failed: {error}"),
            },
        }
    }

    pub fn move_selection(&mut self, delta: isize) {
        if self.visible.is_empty() {
            return;
        }
        if delta < 0 {
            self.selected = self.selected.saturating_sub(delta.unsigned_abs());
        } else {
            self.selected = self
                .selected
                .saturating_add(delta as usize)
                .min(self.visible.len() - 1);
        }
    }

    pub fn apply_query(&mut self, query: String) {
        self.query = query.trim().to_string();
        self.apply_filters();
    }

    pub fn open_selected(&mut self) {
        let Some(record) = self.visible.get(self.selected) else {
            self.preview = "No intent selected.".to_string();
            return;
        };
        self.preview = match fs::read_to_string(&record.source_chunk) {
            Ok(raw) => raw.lines().take(80).collect::<Vec<_>>().join("\n"),
            Err(error) => format!("Failed to open {}: {error}", record.source_chunk),
        };
    }

    pub fn selected_preview(&self) -> String {
        if !self.preview.is_empty() {
            return self.preview.clone();
        }
        self.visible
            .get(self.selected)
            .map(|record| {
                format!(
                    "{} | {} | {}\n{}\n\nsource: {}",
                    record.date,
                    record.agent,
                    record.kind.heading(),
                    record.summary,
                    record.source_chunk
                )
            })
            .unwrap_or_else(|| "No intents visible.".to_string())
    }

    pub fn cycle_project_filter(&mut self) {
        self.project = match self.project.as_deref() {
            None => Some("aicx".to_string()),
            Some("aicx") => Some("memex".to_string()),
            Some(_) => None,
        };
        *self = Self::load(self.project.clone(), self.hours, self.agent.clone());
    }

    pub fn cycle_agent_filter(&mut self) {
        self.agent = match self.agent.as_deref() {
            None => Some("codex".to_string()),
            Some("codex") => Some("claude".to_string()),
            Some(_) => None,
        };
        self.apply_filters();
    }

    pub fn cycle_hours(&mut self) {
        self.hours = match self.hours {
            48 => 168,
            168 => 720,
            _ => 48,
        };
        *self = Self::load(self.project.clone(), self.hours, self.agent.clone());
    }

    fn apply_filters(&mut self) {
        let filters = IntentDisplayFilters {
            agent: self.agent.clone(),
            sort: Some(IntentSortOrder::Newest),
            limit: Some(200),
            ..IntentDisplayFilters::default()
        };
        let mut visible = intents::apply_display_filters(self.records.clone(), &filters);
        if !self.query.is_empty() {
            let needle = self.query.to_ascii_lowercase();
            visible.retain(|record| {
                format!(
                    "{} {} {} {} {}",
                    record.summary,
                    record.context.clone().unwrap_or_default(),
                    record.agent,
                    record.project,
                    record.source_chunk
                )
                .to_ascii_lowercase()
                .contains(&needle)
            });
        }
        self.visible = visible;
        self.selected = 0;
        self.preview.clear();
        self.status = format!("{} intents visible", self.visible.len());
    }
}