diff --git a/agentydragon/tasks/35-tui-inspect-env-integration.md b/agentydragon/tasks/35-tui-inspect-env-integration.md index 5df212a7e7..4df84de893 100644 --- a/agentydragon/tasks/35-tui-inspect-env-integration.md +++ b/agentydragon/tasks/35-tui-inspect-env-integration.md @@ -1,9 +1,9 @@ +++ id = "35" title = "TUI Integration for Inspect-Env Command" -status = "Not started" +status = "Done" dependencies = "10" # Rationale: depends on Task 10 for container state inspection -last_updated = "2025-06-25T04:45:29Z" +last_updated = "2025-06-25T11:38:19Z" +++ > *This task is specific to codex-rs.* @@ -32,7 +32,7 @@ Add an `/inspect-env` slash-command in the TUI that invokes the existing `codex - Add `InlineInspectEnv` variant to `AppEvent` enum to represent inline slash-command invocation. - Update dispatch logic in `App::run` to spawn a background thread on `InlineInspectEnv` that runs `codex inspect-env`, reads its stdout line-by-line, and sends each line as `AppEvent::LatestLog`, then triggers a redraw. - Wire up `/inspect-env` to dispatch `InlineInspectEnv` in the slash-command handling. -- Add unit tests in the TUI crate to verify `built_in_slash_commands()` includes `inspect-env` mapping and description. +- Add unit tests in the TUI crate to verify `built_in_slash_commands()` includes `inspect-env` mapping and description, and tests for the command-popup filter to ensure `InspectEnv` is listed when `/inspect-env` is entered. **How it works** When the user enters `/inspect-env`, the TUI parser recognizes the command and emits `AppEvent::InlineInspectEnv`. The main event loop handles this event by spawning a thread that invokes the external `codex inspect-env` command, captures its output line-by-line, and forwards each line into the TUI log pane via `AppEvent::LatestLog`. A redraw is scheduled once the inspection completes. diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e620cde5f4..85b9c1bc1d 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -478,8 +478,11 @@ impl<'a> App<'a> { } } SlashCommand::InspectEnv => { + // Activate inspect-env view and initiate output streaming + if let AppState::Chat { widget } = &mut self.app_state { + widget.push_inspect_env(); + } let _ = self.app_event_tx.send(AppEvent::InlineInspectEnv(String::new())); - let _ = self.app_event_tx.send(AppEvent::Redraw); } }, } diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 0dcb98865c..5ec09b5c93 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -29,6 +29,29 @@ pub(crate) struct CommandPopup { selected_idx: Option, } +#[cfg(test)] +mod tests { + use super::*; + use crate::slash_command::SlashCommand; + + #[test] + fn filter_inspect_env_in_command_popup() { + let mut popup = CommandPopup::new(); + popup.on_composer_text_change("/inspect-env".to_string()); + let filtered: Vec<&SlashCommand> = popup.filtered_commands(); + // Ensure InspectEnv command is among filtered results + assert!(filtered.contains(&&SlashCommand::InspectEnv)); + } + + #[test] + fn select_inspect_env_as_selected_command() { + let mut popup = CommandPopup::new(); + popup.on_composer_text_change("/inspect-env".to_string()); + popup.selected_idx = Some(0); + assert_eq!(popup.selected_command(), Some(&SlashCommand::InspectEnv)); + } +} + impl CommandPopup { pub(crate) fn new() -> Self { Self { diff --git a/codex-rs/tui/src/bottom_pane/inspect_env_view.rs b/codex-rs/tui/src/bottom_pane/inspect_env_view.rs new file mode 100644 index 0000000000..5fac6e639b --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/inspect_env_view.rs @@ -0,0 +1,85 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::{Block, Borders, BorderType, Paragraph}; +use ratatui::prelude::Widget; + +use super::{BottomPane, BottomPaneView}; +use super::bottom_pane_view::ConditionalUpdate; + +/// View for displaying the output of `codex inspect-env` in the bottom pane. +pub(crate) struct InspectEnvView { + lines: Vec, + done: bool, +} + +impl InspectEnvView { + /// Create a new inspect-env view. + pub fn new() -> Self { + Self { lines: Vec::new(), done: false } + } +} + +impl<'a> BottomPaneView<'a> for InspectEnvView { + fn update_status_text(&mut self, text: String) -> ConditionalUpdate { + self.lines.push(text); + ConditionalUpdate::NeedsRedraw + } + + fn handle_key_event(&mut self, pane: &mut BottomPane<'a>, key_event: KeyEvent) { + if key_event.code == KeyCode::Enter || key_event.code == KeyCode::Esc { + self.done = true; + } + pane.request_redraw(); + } + + fn is_complete(&self) -> bool { + self.done + } + + fn calculate_required_height(&self, area: &Rect) -> u16 { + area.height + } + + fn render(&self, area: Rect, buf: &mut Buffer) { + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title("Inspect Env (Enter/Esc to close)"); + let text = self.lines.join("\n"); + Paragraph::new(text).block(block).render(area, buf); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; + + #[test] + fn update_status_text_appends_lines() { + let mut view = InspectEnvView::new(); + assert!(view.lines.is_empty()); + view.update_status_text("foo".to_string()); + view.update_status_text("bar".to_string()); + assert_eq!(view.lines, vec!["foo".to_string(), "bar".to_string()]); + } + + #[test] + fn render_includes_lines() { + let mut view = InspectEnvView::new(); + view.update_status_text("line1".to_string()); + view.update_status_text("line2".to_string()); + let area = Rect { x: 0, y: 0, width: 10, height: 3 }; + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); + // Collect all cell symbols into a flat string and verify the lines are present + let content: String = buf + .content() + .iter() + .fold(String::new(), |mut acc, cell| { acc.push_str(cell.symbol()); acc }); + assert!(content.contains("line1")); + assert!(content.contains("line2")); + } +} diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 446fbb11be..78f7601be7 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -13,6 +13,7 @@ use crate::user_approval_widget::ApprovalRequest; mod approval_modal_view; mod mount_view; +mod inspect_env_view; mod bottom_pane_view; mod chat_composer; mod chat_composer_history; @@ -25,6 +26,7 @@ pub(crate) use chat_composer::InputResult; use approval_modal_view::ApprovalModalView; use mount_view::{MountAddView, MountRemoveView}; +use inspect_env_view::InspectEnvView; use status_indicator_view::StatusIndicatorView; use config_reload_view::ConfigReloadView; @@ -161,6 +163,13 @@ impl BottomPane<'_> { self.request_redraw(); } + /// Launch inspect-env output view. + pub fn push_inspect_env(&mut self) { + let view = InspectEnvView::new(); + self.active_view = Some(Box::new(view)); + self.request_redraw(); + } + /// Launch interactive mount-remove dialog (container path). pub fn push_mount_remove_interactive(&mut self) { let view = MountRemoveView::new(self.app_event_tx.clone()); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a5714efd3e..38a02de54d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -463,6 +463,12 @@ impl ChatWidget<'_> { self.request_redraw(); } + /// Launch inspect-env output view. + pub fn push_inspect_env(&mut self) { + self.bottom_pane.push_inspect_env(); + self.request_redraw(); + } + /// Update the running config and reconstruct bottom pane settings. pub fn update_config(&mut self, config: Config) { self.config = config.clone();