rho-coding-agent 0.23.0

A lightweight agent harness inspired by Pi
Documentation
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::DefaultTerminal;

use super::{
    questionnaire_notice_text, App, ComposerMode, Entry, QuestionAnswerRequest,
    QuestionnaireComposer,
};

impl App {
    pub(super) fn handle_questionnaire_key(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
        if !matches!(self.composer, ComposerMode::Questionnaire(_)) {
            return Ok(false);
        }

        match (key.modifiers, key.code) {
            (KeyModifiers::CONTROL, KeyCode::Char('c')) => {
                if self.ctrl_c_streak == 0 {
                    if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                        questionnaire.clear_active_answer();
                    }
                    self.status = "answer cleared; press ctrl-c again to cancel".into();
                    self.ctrl_c_streak = 1;
                } else {
                    self.cancel_questionnaire_answer();
                }
                self.paste_burst.clear();
                Ok(true)
            }
            (KeyModifiers::ALT, KeyCode::Up) | (_, KeyCode::BackTab) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_to_previous_field();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (KeyModifiers::ALT, KeyCode::Down) | (_, KeyCode::Tab) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_to_next_field();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (KeyModifiers::ALT, KeyCode::Backspace) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.delete_previous_word();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (KeyModifiers::NONE, KeyCode::Enter) => {
                self.submit_questionnaire_answer()?;
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Esc) => {
                self.cancel_questionnaire_answer();
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Up) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_active_choice_previous();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Down) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_active_choice_next();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Backspace) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.backspace();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Delete) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.delete();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (KeyModifiers::ALT, KeyCode::Left) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_text_cursor_previous_word();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (KeyModifiers::ALT, KeyCode::Right) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_text_cursor_next_word();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Left) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_cursor_left();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Right) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_cursor_right();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Home) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_cursor_home();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::End) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.move_cursor_end();
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (KeyModifiers::CONTROL, KeyCode::Char('j')) | (KeyModifiers::ALT, KeyCode::Enter) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.insert_char('\n');
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (modifiers, KeyCode::Enter) if modifiers.contains(KeyModifiers::SHIFT) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.insert_char('\n');
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (_, KeyCode::Char(' ')) => {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    if questionnaire.active_text_entry_active() {
                        questionnaire.insert_char(' ');
                    } else {
                        questionnaire.toggle_active_choice();
                    }
                }
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            (modifiers, KeyCode::Char(ch))
                if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
            {
                if let ComposerMode::Questionnaire(questionnaire) = &mut self.composer {
                    questionnaire.insert_char(ch);
                }
                self.ctrl_c_streak = 0;
                Ok(true)
            }
            _ => {
                self.paste_burst.clear();
                self.ctrl_c_streak = 0;
                Ok(true)
            }
        }
    }

    fn submit_questionnaire_answer(&mut self) -> anyhow::Result<()> {
        if let Some(display) = self.prepare_questionnaire_answer()? {
            self.insert_entry(&Entry::User(display));
        }
        Ok(())
    }

    fn prepare_questionnaire_answer(&mut self) -> anyhow::Result<Option<String>> {
        let ComposerMode::Questionnaire(mut questionnaire) =
            std::mem::replace(&mut self.composer, ComposerMode::Input)
        else {
            return Ok(None);
        };
        match questionnaire.submit() {
            Ok(submitted) => {
                let display = submitted.display;
                self.input.clear();
                self.paste_segments.clear();
                self.input_cursor = 0;
                self.command_palette_dismissed = false;
                self.clamp_command_selection();
                self.status = "answers submitted".into();
                Ok(Some(display))
            }
            Err(error) => {
                self.composer = ComposerMode::Questionnaire(questionnaire);
                self.status = error;
                Ok(None)
            }
        }
    }

    fn cancel_questionnaire_answer(&mut self) {
        let ComposerMode::Questionnaire(mut questionnaire) =
            std::mem::replace(&mut self.composer, ComposerMode::Input)
        else {
            return;
        };
        questionnaire.cancel_by_user();
        self.ctrl_c_streak = 0;
        self.input.clear();
        self.paste_segments.clear();
        self.input_cursor = 0;
        self.command_palette_dismissed = false;
        self.clamp_command_selection();
        self.status = "answer cancelled".into();
    }

    pub(super) fn open_questionnaire(
        &mut self,
        request: QuestionAnswerRequest,
        terminal: &mut DefaultTerminal,
    ) -> std::io::Result<()> {
        self.finish_streams(terminal)?;
        self.input.clear();
        self.paste_segments.clear();
        self.input_cursor = 0;
        self.command_palette_dismissed = false;
        self.clamp_command_selection();
        self.insert_entry(&Entry::Notice(questionnaire_notice_text(&request.request)));
        self.composer = ComposerMode::Questionnaire(QuestionnaireComposer::new(
            request.request,
            request.response,
        ));
        self.status = "waiting for your answers".into();
        Ok(())
    }
}