mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
Merge branch 'agentydragon-23-interactive-container-command-affordance' into agentydragon
This commit is contained in:
@@ -48,6 +48,12 @@ This file documents the changes introduced on the `agentydragon` branch
|
||||
|
||||
## Documentation tasks
|
||||
|
||||
## codex-rs/tui: interactive shell-command affordance via hotkey
|
||||
- Bound `Ctrl+M` to open a ShellCommandView overlay for arbitrary container shell input.
|
||||
- Toggled shell-command mode with `Ctrl+M` to enter or exit prompt, with styled border in shell mode.
|
||||
- Executed commands asynchronously (`sh -c`) and recorded outputs inline in conversation history.
|
||||
- Added unit tests for ShellCommandView event emission and shell-mode toggling behavior.
|
||||
|
||||
Tasks live under `agentydragon/tasks/` as individual Markdown files. Please update each task’s **Status** and **Implementation** sections in place rather than maintaining a static list here.
|
||||
|
||||
### Branch & Worktree Workflow
|
||||
|
||||
@@ -3,7 +3,7 @@ id = "23"
|
||||
title = "Interactive Container Command Affordance via Hotkey"
|
||||
status = "Done"
|
||||
dependencies = "01" # Rationale: depends on Task 01 for mount-add/remove affordance
|
||||
last_updated = "2025-06-26T15:00:00.000000"
|
||||
last_updated = "2025-06-30T12:00:00.000001"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
@@ -22,7 +22,7 @@ Add a user-facing affordance (e.g. a hotkey) to invoke arbitrary shell commands
|
||||
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
**How it was implemented**
|
||||
- Added a new slash command `Shell` and updated dispatch logic in `app.rs` to push a shell-command view.
|
||||
- Bound `Ctrl+M` in `ChatComposer` to dispatch `SlashCommand::Shell` for hotkey-driven shell prompt.
|
||||
- Created `ShellCommandView` (bottom pane overlay) to capture arbitrary user input and emit `AppEvent::ShellCommand(cmd)`.
|
||||
@@ -33,6 +33,7 @@ Add a user-facing affordance (e.g. a hotkey) to invoke arbitrary shell commands
|
||||
- Unit test in `shell_command_view.rs` asserting correct event emission (skipping redraws).
|
||||
- Integration test in `chat_composer.rs` asserting `Ctrl+M` opens the shell prompt view and allows input.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- This feature aids debugging and inspection without leaving the agent workflow.
|
||||
|
||||
@@ -499,7 +499,7 @@ impl<'a> App<'a> {
|
||||
widget.handle_shell_command_result(call_id, stdout, stderr, exit_code);
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal.clear()?;
|
||||
|
||||
@@ -25,6 +25,7 @@ const MIN_TEXTAREA_ROWS: usize = 1;
|
||||
const BORDER_LINES: u16 = 2;
|
||||
|
||||
/// Result returned when the user interacts with the text area.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InputResult {
|
||||
Submitted(String),
|
||||
None,
|
||||
@@ -39,6 +40,39 @@ pub(crate) struct ChatComposer<'a> {
|
||||
max_rows: usize,
|
||||
/// Last computed context-left percentage
|
||||
context_left_percent: f64,
|
||||
/// Whether the composer is in shell-command mode (Ctrl+M toggles).
|
||||
shell_mode: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn ctrl_m_dispatches_shell_command() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let evt_tx = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(true, evt_tx.clone(), 1);
|
||||
// Initial shell_mode should be false.
|
||||
assert!(!composer.shell_mode);
|
||||
// Simulate Ctrl+M key event.
|
||||
let key_event = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
|
||||
let (res, needs_redraw) = composer.handle_key_event(key_event);
|
||||
assert!(needs_redraw);
|
||||
assert_eq!(res, InputResult::None);
|
||||
// shell_mode should have toggled to true.
|
||||
assert!(composer.shell_mode);
|
||||
// Verify DispatchCommand(Shell) event was sent.
|
||||
match rx.recv().unwrap() {
|
||||
AppEvent::DispatchCommand(cmd) => assert_eq!(cmd, SlashCommand::Shell),
|
||||
other => panic!("Expected DispatchCommand(Shell), got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatComposer<'_> {
|
||||
@@ -54,6 +88,7 @@ impl ChatComposer<'_> {
|
||||
history: ChatComposerHistory::new(),
|
||||
max_rows,
|
||||
context_left_percent: 100.0,
|
||||
shell_mode: false,
|
||||
};
|
||||
this.update_border(has_input_focus);
|
||||
this
|
||||
@@ -232,7 +267,8 @@ impl ChatComposer<'_> {
|
||||
(InputResult::None, true)
|
||||
}
|
||||
Input { key: Key::Char('m'), ctrl: true, alt: false, shift: false } => {
|
||||
// Launch shell-command prompt
|
||||
// Toggle shell-command mode and prompt/exit accordingly
|
||||
self.shell_mode = !self.shell_mode;
|
||||
self.app_event_tx.send(AppEvent::DispatchCommand(SlashCommand::Shell));
|
||||
(InputResult::None, true)
|
||||
}
|
||||
@@ -286,6 +322,12 @@ impl ChatComposer<'_> {
|
||||
self.textarea.lines().join("\n")
|
||||
}
|
||||
|
||||
/// Returns true if the composer is in shell-command mode.
|
||||
#[allow(dead_code)]
|
||||
pub fn is_shell_mode(&self) -> bool {
|
||||
self.shell_mode
|
||||
}
|
||||
|
||||
/// Synchronize `self.command_popup` with the current text in the
|
||||
/// textarea. This must be called after every modification that can change
|
||||
/// the text so the popup is shown/updated/hidden as appropriate.
|
||||
@@ -337,7 +379,15 @@ impl ChatComposer<'_> {
|
||||
border_style: Style,
|
||||
}
|
||||
|
||||
let bs = if has_focus {
|
||||
let bs = if self.shell_mode {
|
||||
BlockState {
|
||||
right_title: Line::from(
|
||||
"Shell mode – Enter to run | Ctrl+M to exit shell mode",
|
||||
)
|
||||
.alignment(Alignment::Right),
|
||||
border_style: Style::default().fg(Color::Red),
|
||||
}
|
||||
} else if has_focus {
|
||||
BlockState {
|
||||
right_title: Line::from("Enter to send | Ctrl+D to quit | Ctrl+J for newline")
|
||||
.alignment(Alignment::Right),
|
||||
|
||||
@@ -257,9 +257,9 @@ impl WidgetRef for &BottomPane<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::mpsc;
|
||||
use crate::app_event::AppEvent;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
/// Construct a BottomPane with default parameters for testing.
|
||||
fn make_pane() -> BottomPane<'static> {
|
||||
@@ -303,6 +303,17 @@ mod tests {
|
||||
assert!(pane.active_view.as_mut().unwrap().should_hide_when_task_is_done());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_m_toggles_shell_mode() {
|
||||
let mut pane = make_pane();
|
||||
assert!(!pane.composer.is_shell_mode());
|
||||
let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
|
||||
pane.handle_key_event(key);
|
||||
assert!(pane.composer.is_shell_mode());
|
||||
pane.handle_key_event(key);
|
||||
assert!(!pane.composer.is_shell_mode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_status_indicator_after_task_complete() {
|
||||
let mut pane = make_pane();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
|
||||
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Widget;
|
||||
@@ -29,6 +29,12 @@ impl ShellCommandView {
|
||||
|
||||
impl<'a> BottomPaneView<'a> for ShellCommandView {
|
||||
fn handle_key_event(&mut self, pane: &mut BottomPane<'a>, key_event: KeyEvent) {
|
||||
// Exit shell prompt on Ctrl+M
|
||||
if let KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::CONTROL, .. } = key_event {
|
||||
self.done = true;
|
||||
pane.request_redraw();
|
||||
return;
|
||||
}
|
||||
if self.done {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl SlashCommand {
|
||||
SlashCommand::MountRemove => "Remove a mount by container path.",
|
||||
SlashCommand::InspectEnv => "Inspect sandbox and container environment (mounts, permissions, network)",
|
||||
SlashCommand::Shell => "Run a shell command in the container.",
|
||||
SlashCommand::Quit => "Quit",
|
||||
SlashCommand::Quit => "Exit the application.",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user