aicx 0.6.6

Operator CLI + MCP server: canonical corpus first, optional semantic index second (Claude Code, Codex, Gemini)
Documentation
use crossterm::event::{KeyCode, KeyModifiers};

use crate::wizard::screens::{
    corpus::CorpusScreen, doctor::DoctorScreen, intents::IntentsScreen, store::StoreScreen,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Screen {
    Corpus,
    Doctor,
    Intents,
    Store,
}

impl Screen {
    pub fn title(self) -> &'static str {
        match self {
            Self::Corpus => "Corpus",
            Self::Doctor => "Doctor",
            Self::Intents => "Intents",
            Self::Store => "Store",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Confirmation {
    DoctorFix,
    DoctorFixBuckets,
}

impl Confirmation {
    pub fn command(&self) -> &'static str {
        match self {
            Self::DoctorFix => "aicx doctor --fix",
            Self::DoctorFixBuckets => "aicx doctor --fix-buckets",
        }
    }
}

pub struct App {
    pub active: Screen,
    pub corpus: CorpusScreen,
    pub doctor: DoctorScreen,
    pub intents: IntentsScreen,
    pub store: StoreScreen,
    pub should_quit: bool,
    pub show_help: bool,
    pub search_mode: bool,
    pub search_input: String,
    pub confirmation: Option<Confirmation>,
    pub status: String,
}

impl App {
    pub fn new() -> Self {
        let mut app = Self {
            active: Screen::Corpus,
            corpus: CorpusScreen::load(),
            doctor: DoctorScreen::default(),
            intents: IntentsScreen::load(None, 168, None),
            store: StoreScreen::default(),
            should_quit: false,
            show_help: false,
            search_mode: false,
            search_input: String::new(),
            confirmation: None,
            status: "ready".to_string(),
        };
        app.status = app.corpus.status_line();
        app
    }

    pub fn corpus_stats(&self) -> String {
        self.corpus.stats_line()
    }

    pub fn tick(&mut self) {
        self.store.poll();
    }

    pub fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) {
        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
            if self.store.cancel() {
                self.status = self.store.status.clone();
            } else {
                self.should_quit = true;
            }
            return;
        }

        self.handle_key(key.code);
    }

    pub fn handle_key(&mut self, key: KeyCode) {
        if self.handle_confirmation_key(key) {
            return;
        }

        if self.show_help {
            match key {
                KeyCode::Esc | KeyCode::Char('?') | KeyCode::Char('q') => self.show_help = false,
                _ => {}
            }
            return;
        }

        if self.search_mode {
            self.handle_search_key(key);
            return;
        }

        match key {
            KeyCode::Char('q') => {
                if self.store.is_running() {
                    self.status = "store is running; press Ctrl+C to cancel or wait".to_string();
                } else {
                    self.should_quit = true;
                }
            }
            KeyCode::Char('?') => self.show_help = true,
            KeyCode::Esc => {
                self.confirmation = None;
                self.search_mode = false;
            }
            KeyCode::Char('1') => self.switch(Screen::Corpus),
            KeyCode::Char('2') => self.switch(Screen::Doctor),
            KeyCode::Char('3') => self.switch(Screen::Intents),
            KeyCode::Char('4') => self.switch(Screen::Store),
            KeyCode::Char('/') => {
                self.search_mode = true;
                self.search_input = match self.active {
                    Screen::Corpus => self.corpus.search.clone(),
                    Screen::Intents => self.intents.query.clone(),
                    _ => String::new(),
                };
            }
            KeyCode::Up | KeyCode::Char('k') => self.move_selection(-1),
            KeyCode::Down | KeyCode::Char('j') => self.move_selection(1),
            KeyCode::Left | KeyCode::Char('h') => self.corpus.move_column(-1),
            KeyCode::Right | KeyCode::Char('l') => self.corpus.move_column(1),
            KeyCode::Enter => self.activate_selected(),
            KeyCode::Char('f') if self.active == Screen::Doctor => {
                self.confirmation = Some(Confirmation::DoctorFix);
            }
            KeyCode::Char('b') if self.active == Screen::Doctor => {
                self.confirmation = Some(Confirmation::DoctorFixBuckets);
            }
            KeyCode::Char('r') if self.active == Screen::Doctor => {
                self.doctor.refresh(false);
                self.status = self.doctor.status.clone();
            }
            KeyCode::Char('s') if self.active == Screen::Store => {
                self.store.start();
                self.status = self.store.status.clone();
            }
            KeyCode::Char('t') if self.active == Screen::Store => {
                self.store.cycle_hours();
                self.status = self.store.status.clone();
            }
            KeyCode::Char('p') if self.active == Screen::Intents => {
                self.intents.cycle_project_filter();
                self.status = self.intents.status.clone();
            }
            KeyCode::Char('a') if self.active == Screen::Intents => {
                self.intents.cycle_agent_filter();
                self.status = self.intents.status.clone();
            }
            KeyCode::Char('t') if self.active == Screen::Intents => {
                self.intents.cycle_hours();
                self.status = self.intents.status.clone();
            }
            _ => {}
        }
    }

    fn switch(&mut self, screen: Screen) {
        self.active = screen;
        self.status = match screen {
            Screen::Corpus => self.corpus.status_line(),
            Screen::Doctor => {
                if !self.doctor.loaded {
                    self.doctor.refresh(false);
                }
                self.doctor.status.clone()
            }
            Screen::Intents => self.intents.status.clone(),
            Screen::Store => self.store.status.clone(),
        };
    }

    fn move_selection(&mut self, delta: isize) {
        match self.active {
            Screen::Corpus => self.corpus.move_selection(delta),
            Screen::Doctor => self.doctor.move_selection(delta),
            Screen::Intents => self.intents.move_selection(delta),
            Screen::Store => self.store.move_log(delta),
        }
    }

    fn activate_selected(&mut self) {
        match self.active {
            Screen::Corpus => self.status = self.corpus.status_line(),
            Screen::Doctor => {
                self.doctor.refresh(false);
                self.status = self.doctor.status.clone();
            }
            Screen::Intents => self.intents.open_selected(),
            Screen::Store => self.store.start(),
        }
    }

    fn handle_search_key(&mut self, key: KeyCode) {
        match key {
            KeyCode::Esc => {
                self.search_mode = false;
                self.search_input.clear();
            }
            KeyCode::Enter => {
                match self.active {
                    Screen::Corpus => self.corpus.apply_search(self.search_input.clone()),
                    Screen::Intents => self.intents.apply_query(self.search_input.clone()),
                    _ => {}
                }
                self.search_mode = false;
                self.status = format!("filter: {}", self.search_input);
            }
            KeyCode::Backspace => {
                self.search_input.pop();
            }
            KeyCode::Char(c) => self.search_input.push(c),
            _ => {}
        }
    }

    fn handle_confirmation_key(&mut self, key: KeyCode) -> bool {
        let Some(action) = self.confirmation.clone() else {
            return false;
        };

        match key {
            KeyCode::Char('y') | KeyCode::Enter => {
                match action {
                    Confirmation::DoctorFix => self.doctor.refresh(true),
                    Confirmation::DoctorFixBuckets => self.doctor.fix_buckets(),
                }
                self.status = self.doctor.status.clone();
                self.confirmation = None;
                true
            }
            KeyCode::Char('n') | KeyCode::Esc => {
                self.confirmation = None;
                self.status = "action cancelled".to_string();
                true
            }
            _ => true,
        }
    }
}

impl Default for App {
    fn default() -> Self {
        Self::new()
    }
}