magi-code 0.80.2

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

/// A full conversation projection, not the bounded subagent preview. Its state
/// survives hiding and uses the same card, selection, and prompt geometry.
#[derive(Debug)]
pub(crate) struct TranscriptModal {
    pub(crate) state: MissionControlState,
    pub(crate) visible: bool,
    pub(crate) active: bool,
    pub(crate) unread: bool,
}

impl TranscriptModal {
    pub(crate) fn new(mut state: MissionControlState) -> Self {
        state.transcript_only_layout = true;
        state.welcome_render_cache.get_mut().take();
        state.focus_prompt();
        Self {
            state,
            visible: true,
            active: false,
            unread: false,
        }
    }

    fn modal_area(area: Rect) -> Rect {
        crate::tui::modal_container::modal_area(area, 60, 60)
    }

    pub(crate) fn content_area(area: Rect) -> Rect {
        Self::modal_area(area).inner(Margin {
            horizontal: 1,
            vertical: 1,
        })
    }

    pub(super) fn draw(&self, frame: &mut Frame<'_>) {
        if !self.visible {
            if self.active || self.unread {
                let label = match (self.active, self.unread) {
                    (true, true) => " /side running • unread ",
                    (true, false) => " /side running ",
                    _ => " /side unread ",
                };
                let area = frame.area();
                let width = (label.len() as u16).min(area.width);
                frame.render_widget(
                    Paragraph::new(label).style(self.state.theme.app()),
                    Rect::new(area.right().saturating_sub(width), area.y, width, 1),
                );
            }
            return;
        }
        let area = Self::modal_area(frame.area());
        let content = crate::tui::modal_container::draw_modal_container(
            frame,
            area,
            "Side",
            "[Alt+C] cancel · /reset",
            self.state.theme,
            Some("[Esc] close"),
        );
        super::draw_in_area(frame, content, &self.state);
    }
}

impl MissionControlState {
    pub(crate) fn focused_conversation_mut(&mut self) -> &mut Self {
        if self
            .conversation_modal
            .as_ref()
            .is_some_and(|modal| modal.visible)
        {
            &mut self
                .conversation_modal
                .as_mut()
                .expect("visible modal")
                .state
        } else {
            self
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::{Terminal, backend::TestBackend};

    #[test]
    fn transcript_modal_is_compact_centered_and_uses_full_width_for_hit_testing() {
        let terminal = Rect::new(3, 2, 100, 40);
        assert_eq!(
            TranscriptModal::modal_area(terminal),
            Rect::new(23, 10, 60, 24)
        );
        let content = TranscriptModal::content_area(terminal);
        assert_eq!(content, Rect::new(24, 11, 58, 22));
        let modal = TranscriptModal::new(MissionControlState::default());
        let layout = layout_for(content, &modal.state);
        assert_eq!(layout.transcript.x, content.x);
        assert_eq!(layout.transcript.width, content.width);
        assert_eq!(layout.prompt.bottom(), content.bottom());
        assert_eq!(
            layout.pane_at(content.right() - 1, content.y),
            Some(TuiPane::Transcript)
        );
        assert_eq!(
            layout.pane_at(content.right() - 1, content.bottom() - 1),
            Some(TuiPane::Prompt)
        );
        assert_eq!(layout.pane_at(content.x - 1, content.y), None);
        for (width, height) in [(0, 0), (1, 1), (5, 3), (20, 6), (39, 11)] {
            let area = Rect::new(0, 0, width, height);
            let modal = TranscriptModal::modal_area(area);
            assert_eq!(modal.intersection(area), modal);
            assert!(u32::from(modal.width) * 100 <= u32::from(width) * 80);
            assert!(u32::from(modal.height) * 100 <= u32::from(height) * 80);
        }
    }

    #[test]
    fn transcript_modal_shell_separates_close_from_worker_cancel() {
        let modal = TranscriptModal::new(MissionControlState::default());
        let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap();
        terminal.draw(|frame| modal.draw(frame)).unwrap();
        let area = TranscriptModal::modal_area(Rect::new(0, 0, 100, 30));
        let buffer = terminal.backend().buffer();
        let row = |y| {
            (area.x..area.right())
                .map(|x| buffer[(x, y)].symbol())
                .collect::<String>()
        };
        assert!(row(area.y).starts_with("┌Side"));
        assert!(row(area.y).contains("[Esc] close"));
        assert!(!row(area.y).contains("cancel"));
        assert!(row(area.bottom() - 1).contains("[Alt+C] cancel · /reset"));
    }

    #[test]
    fn transcript_modal_keeps_panes_absent_after_shortcuts_reset_and_reopen() {
        let area = Rect::new(0, 0, 160, 60);
        let primary = MissionControlState::default();
        let primary_layout = layout_for(area, &primary);
        assert!(primary_layout.activity_tree.is_some());
        assert!(primary_layout.right_column_rail.is_some());
        let mut modal = TranscriptModal::new(MissionControlState::default());
        for tab in [RightColumnTab::Activity, RightColumnTab::Summary] {
            modal.state.select_right_column_tab(tab);
            modal.state.toggle_activity_column();
            modal.state.focus_activity_layout();
            modal.state.focus_activity_tree();
            modal.state.focus_activity_detail();
            modal.state.focus_summary();
            assert!(modal.state.is_prompt_focused());
            modal.state.focus_next_primary_pane();
            assert!(modal.state.is_transcript_focused());
            modal.state.focus_next_primary_pane();
            assert!(modal.state.is_prompt_focused());
            modal.state.focus_previous_primary_pane();
            assert!(modal.state.is_transcript_focused());
            modal.state.focus_previous_primary_pane();
            assert!(modal.state.is_prompt_focused());
            modal.state.reset_for_new_session();
            let layout = layout_for(TranscriptModal::content_area(area), &modal.state);
            assert!(layout.activity_tree.is_none());
            assert!(layout.detail.is_none());
            assert!(layout.summary.is_none());
            assert!(layout.right_column_rail.is_none());
            assert_eq!(layout.right_column_tab_area(tab), None);
        }
    }

    #[test]
    fn transcript_modal_uses_exact_normal_card_and_prompt_projection() {
        let mut ui = MissionControlState::default();
        ui.apply_output_event(&crate::output::OutputEvent::Diagnostic {
            level: "info".into(),
            message: "Independent side history".into(),
        });
        ui.set_prompt_text("side draft", 10);
        ui.focus_prompt();
        let modal = TranscriptModal::new(ui);
        let mut actual = Terminal::new(TestBackend::new(100, 30)).unwrap();
        actual.draw(|frame| modal.draw(frame)).unwrap();
        let rendered: String = actual
            .backend()
            .buffer()
            .content
            .iter()
            .map(|cell| cell.symbol())
            .collect();
        assert!(rendered.contains("Session:"), "{rendered}");
        assert!(!rendered.contains("[Alt-A]"), "{rendered}");
        assert!(!rendered.contains("Show/Hide Panel"), "{rendered}");
        let mut expected = Terminal::new(TestBackend::new(100, 30)).unwrap();
        expected
            .draw(|frame| {
                super::super::draw_in_area(
                    frame,
                    TranscriptModal::content_area(frame.area()),
                    &modal.state,
                )
            })
            .unwrap();
        let area = TranscriptModal::content_area(Rect::new(0, 0, 100, 30));
        for y in area.y..area.bottom() {
            for x in area.x..area.right() {
                assert_eq!(
                    actual.backend().buffer()[(x, y)],
                    expected.backend().buffer()[(x, y)]
                );
            }
        }
    }

    #[test]
    fn transcript_modal_handles_tiny_terminals_and_hidden_indicator() {
        for (width, height) in [(1, 1), (5, 3), (20, 6), (80, 24)] {
            let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
            let mut modal = TranscriptModal::new(MissionControlState::default());
            terminal.draw(|frame| modal.draw(frame)).unwrap();
            modal.visible = false;
            modal.active = true;
            modal.unread = true;
            terminal.draw(|frame| modal.draw(frame)).unwrap();
            if width >= 80 {
                let text: String = terminal
                    .backend()
                    .buffer()
                    .content
                    .iter()
                    .map(|cell| cell.symbol())
                    .collect();
                assert!(text.contains("/side running"));
                assert!(text.contains("unread"));
            }
        }
    }
}