diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index a7152d1462..15e185e56d 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -74,7 +74,7 @@ pub enum HistoryPersistence { } /// Collection of settings that are specific to the TUI. -#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub struct Tui { /// By default, mouse capture is enabled in the TUI so that it is possible /// to scroll the conversation history with a mouse. This comes at the cost @@ -87,7 +87,27 @@ pub struct Tui { /// the mouse is not possible, though the keyboard shortcuts e.g. `b` and /// `space` still work. This allows the user to select text in the TUI /// using the mouse without needing to hold down a modifier key. + #[serde(default)] pub disable_mouse_capture: bool, + + /// Maximum number of visible lines in the chat input composer before scrolling. + /// The composer will expand up to this many lines; additional content will enable + /// an internal scrollbar. + #[serde(default = "default_composer_max_rows")] + pub composer_max_rows: usize, +} + +fn default_composer_max_rows() -> usize { + 10 +} + +impl Default for Tui { + fn default() -> Self { + Self { + disable_mouse_capture: Default::default(), + composer_max_rows: default_composer_max_rows(), + } + } } #[derive(Deserialize, Debug, Clone, PartialEq, Default)] diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index ff61b5c941..8600b9e0c5 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -9,8 +9,7 @@ use crate::scroll_event_helper::ScrollEventHelper; use crate::slash_command::SlashCommand; use crate::tui; use codex_core::config::Config; -use codex_core::protocol::Event; -use codex_core::protocol::Op; +use codex_core::protocol::{Event, EventMsg, Op, SessionConfiguredEvent}; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -20,6 +19,8 @@ use std::path::PathBuf; use std::sync::mpsc::Receiver; use std::sync::mpsc::channel; +use uuid::Uuid; + /// Top-level application state: which full-screen view is currently active. #[allow(clippy::large_enum_variant)] enum AppState<'a> { @@ -46,6 +47,9 @@ pub(crate) struct App<'a> { /// Stored parameters needed to instantiate the ChatWidget later, e.g., /// after dismissing the Git-repo warning. chat_args: Option, + + /// Session ID reported by the backend; used for resuming the session. + session_id: Option, } /// Aggregate parameters needed to create a `ChatWidget`, as creation may be @@ -162,6 +166,7 @@ impl<'a> App<'a> { app_state, config, chat_args, + session_id: None, } } @@ -171,6 +176,11 @@ impl<'a> App<'a> { self.app_event_tx.clone() } + /// Returns the session ID assigned by the backend for this session, if available. + pub fn session_id(&self) -> Option { + self.session_id + } + pub(crate) fn run( &mut self, terminal: &mut tui::Tui, @@ -316,6 +326,10 @@ impl<'a> App<'a> { } fn dispatch_codex_event(&mut self, event: Event) { + // Capture session ID when the session is initially configured + if let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) = &event.msg { + self.session_id = Some(*session_id); + } match &mut self.app_state { AppState::Chat { widget } => widget.handle_codex_event(event), AppState::Login { .. } | AppState::GitWarning { .. } => {} diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 1218f76ec7..e610b96e2d 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -35,10 +35,12 @@ pub(crate) struct ChatComposer<'a> { command_popup: Option, app_event_tx: AppEventSender, history: ChatComposerHistory, + /// Maximum number of visible lines in the chat input composer. + max_rows: usize, } impl ChatComposer<'_> { - pub fn new(has_input_focus: bool, app_event_tx: AppEventSender) -> Self { + pub fn new(has_input_focus: bool, app_event_tx: AppEventSender, max_rows: usize) -> Self { let mut textarea = TextArea::default(); textarea.set_placeholder_text("send a message"); textarea.set_cursor_line_style(ratatui::style::Style::default()); @@ -48,6 +50,7 @@ impl ChatComposer<'_> { command_popup: None, app_event_tx, history: ChatComposerHistory::new(), + max_rows, }; this.update_border(has_input_focus); this @@ -249,7 +252,12 @@ impl ChatComposer<'_> { } pub fn calculate_required_height(&self, area: &Rect) -> u16 { - let rows = self.textarea.lines().len().max(MIN_TEXTAREA_ROWS); + let rows = self + .textarea + .lines() + .len() + .max(MIN_TEXTAREA_ROWS) + .min(self.max_rows); let num_popup_rows = if let Some(popup) = &self.command_popup { popup.calculate_required_height(area) } else { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index c654581ccd..330c38aeaf 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -41,12 +41,18 @@ pub(crate) struct BottomPane<'a> { pub(crate) struct BottomPaneParams { pub(crate) app_event_tx: AppEventSender, pub(crate) has_input_focus: bool, + /// Maximum number of visible lines in the chat input composer. + pub(crate) composer_max_rows: usize, } impl BottomPane<'_> { pub fn new(params: BottomPaneParams) -> Self { Self { - composer: ChatComposer::new(params.has_input_focus, params.app_event_tx.clone()), + composer: ChatComposer::new( + params.has_input_focus, + params.app_event_tx.clone(), + params.composer_max_rows, + ), active_view: None, app_event_tx: params.app_event_tx, has_input_focus: params.has_input_focus, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index bd5197c73b..5811d74927 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -124,6 +124,7 @@ impl ChatWidget<'_> { bottom_pane: BottomPane::new(BottomPaneParams { app_event_tx, has_input_focus: true, + composer_max_rows: config.tui.composer_max_rows, }), input_focus: InputFocus::BottomPane, config, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5f3e2d69b5..e594ad7788 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -197,6 +197,11 @@ fn run_ratatui_app( let app_result = app.run(&mut terminal, &mut mouse_capture); restore(); + // On exit, display a command that can be used to resume this session + #[allow(clippy::print_stderr)] + if let Some(session_id) = app.session_id() { + eprintln!("Resume this session with: codex session {session_id}"); + } app_result }