met 1.0.0-beta-preview

Metrale Engine: pure Rust LLM inference server (HTTP API, scheduler, TUI, CLI).
// SPDX-License-Identifier: MIT OR Apache-2.0

//! 2026-09-26: Key handling for Help ▸ Report Issue: one handler each for Compose, Preview, Done and Failed, and one shared by the two auth phases.
//!
//! Owner: server tui.
//! Invariants:
//! - `HelpState::on_key` does nothing on the Guide sub-tab or in `ReportPhase::Submitting`.

use std::sync::atomic::Ordering;

use crossterm::event::{KeyCode, KeyEvent};

use super::help_state::{ComposerField, HelpState, HelpSub, ReportCtx, ReportPhase};

impl HelpState {
    pub(super) fn on_key(&mut self, key: KeyEvent, ctx: &ReportCtx) {
        if self.sub == HelpSub::Guide {
            return;
        }
        match self.phase {
            ReportPhase::Compose => self.compose_key(key, ctx),
            ReportPhase::Preview => self.preview_key(key, ctx),
            ReportPhase::RequestingCode | ReportPhase::WaitingAuth { .. } => self.auth_key(key),
            // 2026-09-26: Not cancellable, since the POST may already have created the
            // issue. Each request of the submit ends within the agent's 30 s timeout
            // (`report_http.rs`).
            ReportPhase::Submitting => {}
            ReportPhase::Done { .. } => self.done_key(key),
            ReportPhase::Failed { .. } => self.failed_key(key),
        }
    }

    fn compose_key(&mut self, key: KeyEvent, ctx: &ReportCtx) {
        if self.title_editing {
            match key.code {
                // 2026-09-26: Esc keeps the draft title and only leaves editing.
                KeyCode::Esc | KeyCode::Enter => self.title_editing = false,
                KeyCode::Backspace => {
                    self.title.pop();
                }
                KeyCode::Char(c) => self.title.push(c),
                _ => {}
            }
            return;
        }
        if self.body_editing {
            if key.code == KeyCode::Esc {
                self.set_body_editing(false);
            } else {
                self.body.input(key);
            }
            return;
        }
        match key.code {
            KeyCode::Down | KeyCode::Char('j') => {
                self.field = match self.field {
                    ComposerField::Title => ComposerField::Body,
                    _ => ComposerField::Attach,
                }
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.field = match self.field {
                    ComposerField::Attach => ComposerField::Body,
                    _ => ComposerField::Title,
                }
            }
            KeyCode::Enter => match self.field {
                ComposerField::Title => self.title_editing = true,
                ComposerField::Body => self.set_body_editing(true),
                ComposerField::Attach => self.attach_logs = !self.attach_logs,
            },
            KeyCode::Char(' ') if self.field == ComposerField::Attach => {
                self.attach_logs = !self.attach_logs;
            }
            KeyCode::Char('a') => self.attach_logs = !self.attach_logs,
            KeyCode::Char('s') => self.review(ctx),
            _ => {}
        }
    }

    fn preview_key(&mut self, key: KeyEvent, ctx: &ReportCtx) {
        match key.code {
            KeyCode::Down | KeyCode::Char('j') => self.scroll_preview(1),
            KeyCode::Up | KeyCode::Char('k') => self.scroll_preview(-1),
            KeyCode::PageDown => self.scroll_preview(10),
            KeyCode::PageUp => self.scroll_preview(-10),
            KeyCode::Char('g') | KeyCode::Home => self.preview_scroll = 0,
            KeyCode::Char('G') | KeyCode::End => {
                self.preview_scroll = self.preview_scroll_max.get();
            }
            // 2026-09-26: Toggling the log attachment here re-composes the preview.
            KeyCode::Char('a') => {
                self.attach_logs = !self.attach_logs;
                match self.compose(ctx) {
                    Ok(c) => {
                        self.preview = Some(c);
                        self.preview_scroll = 0;
                    }
                    Err(e) => self.say(e, true),
                }
            }
            KeyCode::Char('y') | KeyCode::Enter => {
                if let Some(c) = self.preview.take() {
                    self.proceed(c);
                }
            }
            KeyCode::Esc => self.back_to_compose(),
            _ => {}
        }
    }

    fn auth_key(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Esc => {
                // 2026-09-26: The device-flow thread checks `cancel` every 200 ms while it
                // waits between polls. The draft title and body are kept.
                if let Some(c) = self.cancel.take() {
                    c.store(true, Ordering::Relaxed);
                }
                self.rx = None;
                self.back_to_compose();
            }
            KeyCode::Char('c') => {
                if let ReportPhase::WaitingAuth { user_code, .. } = &self.phase {
                    let said = match super::clipboard::copy(user_code) {
                        Ok(_) => (
                            "code sent to the terminal clipboard (OSC 52)".to_string(),
                            false,
                        ),
                        Err(e) => (e, true),
                    };
                    self.say(said.0, said.1);
                }
            }
            _ => {}
        }
    }

    fn done_key(&mut self, key: KeyEvent) {
        match (key.code, &self.phase) {
            (KeyCode::Char('c'), ReportPhase::Done { url, .. }) if !url.is_empty() => {
                let said = match super::clipboard::copy(url) {
                    Ok(_) => (
                        "link sent to the terminal clipboard (OSC 52)".to_string(),
                        false,
                    ),
                    Err(e) => (e, true),
                };
                self.say(said.0, said.1);
            }
            (KeyCode::Esc | KeyCode::Enter, _) => self.phase = ReportPhase::Compose,
            _ => {}
        }
    }

    fn failed_key(&mut self, key: KeyEvent) {
        match key.code {
            // 2026-09-26: Retry sends the stored `pending` body without re-composing it,
            // requesting a device code first when no tokens are held.
            KeyCode::Char('s') if self.pending.is_some() => self.auth_or_submit(),
            KeyCode::Char('s') | KeyCode::Esc => self.back_to_compose(),
            _ => {}
        }
    }

    pub(super) fn scroll_preview(&mut self, rows: i32) {
        let max = self.preview_scroll_max.get() as i32;
        self.preview_scroll = (self.preview_scroll as i32 + rows).clamp(0, max.max(0)) as usize;
    }
}

impl super::app::App {
    /// 2026-09-26: Route a key into the Help section with the context the composer needs.
    pub(super) fn on_help_key(&mut self, key: KeyEvent) {
        let ctx = ReportCtx {
            model: super::render::live_model_name(self),
            engine_ready: self.progress.ready,
            tee: super::init::tee_file_path(),
        };
        self.help.on_key(key, &ctx);
    }
}