//! `/expand` -- re-print the last collapsed block, fully expanded (minimal mode). //! //! In the scrollback-native minimal mode (`grok --minimal`) finalized blocks are //! printed once into the terminal's native scrollback, with reasoning collapsed //! and large tool output truncated (design decision K9). Committed terminal text //! can't be mutated, so "expanding" one is an honest re-print of the same block //! in full below the conversation (K10). `/expand` is the slash-command twin of //! the `Ctrl+E` chord; both walk backwards through the most-recently committed //! folded blocks. use crate::app::actions::Action; use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand}; /// Re-print the last collapsed/truncated block, fully expanded (minimal mode). pub struct ExpandCommand; impl SlashCommand for ExpandCommand { fn name(&self) -> &str { "expand" } fn description(&self) -> &str { "Re-print the last collapsed block, fully expanded (minimal mode)" } fn session_scoped(&self) -> bool { true } fn usage(&self) -> &str { "/expand" } fn run(&self, ctx: &mut CommandExecCtx, _args: &str) -> CommandResult { // Expansion is meaningful only in minimal mode — the full-TUI scrollback // pane folds/unfolds blocks in place (the `e` / `Ctrl+E` chords) and has // no print-once committed history to re-print. if !ctx.screen_mode.is_minimal() { return CommandResult::Message( "/expand is only available in minimal mode (--minimal)".to_string(), ); } if ctx.session_id.is_none() { return CommandResult::Error("No active session".to_string()); } CommandResult::Action(Action::MinimalExpandLast) } } #[cfg(test)] mod tests { use super::*; use crate::acp::model_state::ModelState; use crate::app::bundle::BundleState; use crate::settings::PagerLocalSnapshot; static DEFAULT_BUNDLE_STATE: BundleState = BundleState { has_cache: false, version: String::new(), personas: Vec::new(), roles: Vec::new(), agents: Vec::new(), skills: Vec::new(), persona_details: Vec::new(), role_details: Vec::new(), }; fn ctx<'a>( models: &'a ModelState, session_id: Option<&'a agent_client_protocol::SessionId>, screen_mode: crate::app::ScreenMode, ) -> CommandExecCtx<'a> { CommandExecCtx { models, session_id, bundle_state: &DEFAULT_BUNDLE_STATE, screen_mode, pager_state: PagerLocalSnapshot::default(), } } #[test] fn minimal_with_session_dispatches_expand_action() { let models = ModelState::default(); let sid = agent_client_protocol::SessionId::from("s1".to_string()); let mut c = ctx(&models, Some(&sid), crate::app::ScreenMode::Minimal); assert!(matches!( ExpandCommand.run(&mut c, ""), CommandResult::Action(Action::MinimalExpandLast) )); } #[test] fn non_minimal_returns_message() { let models = ModelState::default(); let sid = agent_client_protocol::SessionId::from("s1".to_string()); let mut c = ctx(&models, Some(&sid), crate::app::ScreenMode::Fullscreen); match ExpandCommand.run(&mut c, "") { CommandResult::Message(msg) => assert!(msg.contains("minimal")), other => panic!("expected Message, got {other:?}"), } } #[test] fn minimal_without_session_errors() { let models = ModelState::default(); let mut c = ctx(&models, None, crate::app::ScreenMode::Minimal); match ExpandCommand.run(&mut c, "") { CommandResult::Error(msg) => assert!(msg.contains("No active session")), other => panic!("expected Error, got {other:?}"), } } }