mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Merge branch 'agentydragon-23-interactive-container-command-affordance' into agentydragon
This commit is contained in:
36
AGENTS.md
36
AGENTS.md
@@ -1,15 +1,31 @@
|
||||
# AGENTS.md
|
||||
|
||||
**Agents:**
|
||||
- Update `agentydragon/README.md` with a brief summary of your work (features, fixes, refactors, documentation, etc.) whenever you make changes to this repository.
|
||||
- Read `agentydragon/README.md` for the branch-level changelog and guidelines on task conventions.
|
||||
- For work on tasks, “add task” means creating a new Markdown file under `agentydragon/tasks/` using `task-template.md`:
|
||||
- Name it with a two-digit prefix and kebab-case slug (e.g. `14-new-feature.md`).
|
||||
- Fill in the **Status**, **Goal**, **Acceptance Criteria**, and **Implementation** sections.
|
||||
- No central task list should be maintained. The AI assistant will include these branch-level notes in its context.
|
||||
This file provides guidance to OpenAI Codex (openai.com/codex) when working with
|
||||
code in this repository.
|
||||
|
||||
# Rust/codex-rs
|
||||
## Build, Lint & Test
|
||||
|
||||
In the codex-rs folder where the rust code lives:
|
||||
### JavaScript/TypeScript
|
||||
- Install dependencies: `pnpm install`
|
||||
- Run all tests: `pnpm test`
|
||||
- Run a single test: `pnpm test -- -t <pattern>` or `pnpm test -- path/to/file.spec.ts`
|
||||
- Watch tests: `pnpm test:watch`
|
||||
- Lint: `pnpm lint && pnpm lint:fix`
|
||||
- Type-check: `pnpm typecheck`
|
||||
- Format: `pnpm format:fix`
|
||||
- Build: `pnpm build`
|
||||
|
||||
- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR`. You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations.
|
||||
### Rust (codex-rs workspace)
|
||||
- Build: `cargo build --workspace --locked`
|
||||
- Test all: `cargo test --workspace`
|
||||
- Test crate: `cargo test -p <crate>`
|
||||
- Single test: `cargo test -p <crate> -- <test_name>`
|
||||
- Format & check: `cargo fmt --all -- --check`
|
||||
- Lint: `cargo clippy --all-targets --all-features -- -D warnings`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- JS/TS: ESLint + Prettier; group imports; camelCase vars & funcs; PascalCase types/components; catch specific errors
|
||||
- Rust: rustfmt & Clippy (see `codex-rs/rustfmt.toml`); snake_case vars & funcs; PascalCase types; prefer early return; avoid `unwrap()` in prod
|
||||
- General: Do not swallow exceptions; use DRY; generate/validate ASCII art programmatically
|
||||
- Include any Cursor rules from `.cursor/rules/` or Copilot rules from `.github/copilot-instructions.md` if present
|
||||
|
||||
@@ -42,7 +42,6 @@ This file documents the changes introduced on the `agentydragon` branch
|
||||
- When enabled, headings render immediately adjacent to content with no blank line between them.
|
||||
- Updated Markdown rendering in chat UI and logs to honor compact mode globally (diffs, docs, help messages).
|
||||
- Added unit tests covering H1–H6 heading spacing for both compact and default modes.
|
||||
|
||||
## codex-rs: document MCP servers example in README
|
||||
- Added an inline TOML snippet under “Model Context Protocol Support” in `codex-rs/README.md` showing how to configure external `mcp_servers` entries in `~/.codex/config.toml`.
|
||||
- Documented `codex mcp` behavior: JSON-RPC over stdin/stdout, optional sandbox, no ephemeral container, default `codex` tool schema, and example ListTools/CallTool schema.
|
||||
@@ -58,7 +57,7 @@ Tasks live under `agentydragon/tasks/` as individual Markdown files. Please upda
|
||||
-
|
||||
- ```sh
|
||||
- # Accept a full slug (NN-slug) or two-digit task ID (NN), optionally multiple; --tmux opens each in its own tmux pane and auto-commits each task as its Developer agent finishes:
|
||||
- agentydragon/tools/create_task_worktree.py [--agent] [--tmux] <task-slug|NN> [<task-slug|NN>...]
|
||||
- agentydragon/tools/create_task_worktree.py [--agent] [--tmux] [--skip-presubmit] <task-slug|NN> [<task-slug|NN>...]
|
||||
- ```
|
||||
-
|
||||
- Without `--agent`, this creates or reuses a worktree at
|
||||
@@ -66,7 +65,7 @@ Tasks live under `agentydragon/tasks/` as individual Markdown files. Please upda
|
||||
- Internally, the helper uses CoW hydration instead of a normal checkout: it registers the worktree with `git worktree add --no-checkout`, then performs a filesystem-level reflink
|
||||
- of all files (macOS: `cp -cRp`; Linux: `cp --reflink=auto`), falling back to `rsync` if reflinks aren’t supported. This makes new worktrees appear nearly instantly on supported filesystems while
|
||||
- preserving untracked files.
|
||||
- With `--agent`, after setup it runs pre-commit checks (aborting on failure), then launches the Developer Codex agent (using `prompts/developer.md` and the task file).
|
||||
- With `--agent`, after setting up a new worktree it runs presubmit pre-commit checks (aborting with a clear message on failure unless `--skip-presubmit` is passed), then launches the Developer Codex agent (using `prompts/developer.md` and the task file).
|
||||
- After the Developer agent exits, if the task’s **Status** is set to `Done`, it automatically runs the Commit agent helper to stage fixes and commit the work.
|
||||
**Commit agent helper**: in `agentydragon/tasks/`, run:
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
+++
|
||||
id = "23"
|
||||
title = "Interactive Container Command Affordance via Hotkey"
|
||||
status = "Not started"
|
||||
status = "Done"
|
||||
dependencies = "01" # Rationale: depends on Task 01 for mount-add/remove affordance
|
||||
last_updated = "2025-06-25T01:40:09.600000"
|
||||
last_updated = "2025-06-26T15:00:00.000000"
|
||||
+++
|
||||
|
||||
## Summary
|
||||
@@ -23,11 +23,15 @@ Add a user-facing affordance (e.g. a hotkey) to invoke arbitrary shell commands
|
||||
## Implementation
|
||||
|
||||
**How it was implemented**
|
||||
- Define a new keybinding (configurable, default Ctrl+M) in the TUI to trigger a `ShellCommandPrompt` overlay.
|
||||
- In the overlay, accept arbitrary user input and dispatch it as a `ToolInvocation(ShellTool, command)` event in the agent’s event loop.
|
||||
- Leverage the existing shell tool backend to execute the command in the container and capture its output.
|
||||
- Render the command invocation and result inline in the chat UI using the command-rendering logic (honoring compact mode and spacing options).
|
||||
- Add integration tests to simulate the hotkey, input prompt, and verify the shell tool call and inline rendering.
|
||||
- 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)`.
|
||||
- Extended `AppEvent` with `ShellCommand(String)` and `ShellCommandResult { call_id, stdout, stderr, exit_code }` variants for round-trip messaging.
|
||||
- Implemented `ChatWidget::handle_shell_command` to execute `sh -c <cmd>` asynchronously (tokio::spawn) and send back `ShellCommandResult`.
|
||||
- Updated `ConversationHistoryWidget` to reuse existing exec-command cells to display shell commands and their output inline.
|
||||
- Added tests:
|
||||
- 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
|
||||
|
||||
|
||||
@@ -38,8 +38,10 @@ def resolve_slug(input_id: str) -> str:
|
||||
help='Run agent in interactive mode (no exec); implies --agent.')
|
||||
@click.option('-s', '--shell', 'shell_mode', is_flag=True,
|
||||
help='Launch an interactive Codex shell (skip auto-commit); implies --agent.')
|
||||
@click.option('--skip-presubmit', is_flag=True,
|
||||
help='Skip the initial presubmit pre-commit checks when creating a new worktree.')
|
||||
@click.argument('task_inputs', nargs=-1, required=True)
|
||||
def main(agent, tmux_mode, interactive, shell_mode, task_inputs):
|
||||
def main(agent, tmux_mode, interactive, shell_mode, skip_presubmit, task_inputs):
|
||||
"""Create/reuse a task worktree and optionally launch a Dev agent or tmux session."""
|
||||
if interactive or shell_mode:
|
||||
agent = True
|
||||
@@ -77,6 +79,7 @@ def main(agent, tmux_mode, interactive, shell_mode, task_inputs):
|
||||
run(['git', 'branch', '--track', branch, 'agentydragon'])
|
||||
|
||||
wt_root.mkdir(parents=True, exist_ok=True)
|
||||
new_wt = False
|
||||
if not wt_path.exists():
|
||||
# --- COW hydration logic via rsync ---
|
||||
# Instead of checking out files normally, register the worktree empty and then
|
||||
@@ -98,17 +101,27 @@ def main(agent, tmux_mode, interactive, shell_mode, task_inputs):
|
||||
run(['pre-commit', 'install'], cwd=dst)
|
||||
else:
|
||||
click.echo('Warning: pre-commit not found; skipping hook install', err=True)
|
||||
new_wt = True
|
||||
else:
|
||||
click.echo(f'Worktree already exists at {wt_path}')
|
||||
|
||||
if not agent:
|
||||
return
|
||||
|
||||
# Pre-commit checks
|
||||
if shutil.which('pre-commit'):
|
||||
run(['pre-commit', 'run', '--all-files'], cwd=str(wt_path))
|
||||
else:
|
||||
click.echo('Warning: pre-commit not installed; skipping checks', err=True)
|
||||
# Initial presubmit: only on new worktree & branch, unless skipped or in shell mode
|
||||
if new_wt and not skip_presubmit and not shell_mode:
|
||||
if shutil.which('pre-commit'):
|
||||
try:
|
||||
run(['pre-commit', 'run', '--all-files'], cwd=str(wt_path))
|
||||
except subprocess.CalledProcessError:
|
||||
click.echo(
|
||||
'Pre-commit checks failed. Please fix the issues in the worktree or ' +
|
||||
're-run with --skip-presubmit to bypass these checks.', err=True)
|
||||
sys.exit(1)
|
||||
else:
|
||||
click.echo('Warning: pre-commit not installed; skipping presubmit checks', err=True)
|
||||
|
||||
click.echo(f'Launching Developer Codex agent for task {slug} in sandboxed worktree')
|
||||
|
||||
click.echo(f'Launching Developer Codex agent for task {slug} in sandboxed worktree')
|
||||
os.chdir(wt_path)
|
||||
|
||||
13
codex-rs/core/init.md
Normal file
13
codex-rs/core/init.md
Normal file
@@ -0,0 +1,13 @@
|
||||
Please analyze this codebase and create a AGENTS.md file containing:
|
||||
1. Build/lint/test commands - especially for running a single test
|
||||
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
|
||||
|
||||
Usage notes:
|
||||
- The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
|
||||
- If there's already an AGENTS.md, improve it.
|
||||
- If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
|
||||
- Be sure to prefix the file with the following text:
|
||||
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to OpenAI Codex (openai.com/codex) when working with code in this repository.
|
||||
@@ -481,6 +481,24 @@ impl<'a> App<'a> {
|
||||
let _ = self.app_event_tx.send(AppEvent::InlineInspectEnv(String::new()));
|
||||
let _ = self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
SlashCommand::Shell => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.push_shell_command_interactive();
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
}
|
||||
},
|
||||
AppEvent::ShellCommand(cmd) => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_shell_command(cmd);
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
},
|
||||
AppEvent::ShellCommandResult { call_id, stdout, stderr, exit_code } => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_shell_command_result(call_id, stdout, stderr, exit_code);
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crossterm::event::KeyEvent;
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AppEvent {
|
||||
CodexEvent(Event),
|
||||
|
||||
@@ -52,4 +53,13 @@ pub(crate) enum AppEvent {
|
||||
ConfigReloadApply,
|
||||
/// Ignore on-disk config.toml changes and continue with old config.
|
||||
ConfigReloadIgnore,
|
||||
/// Run an arbitrary shell command in the agent's container (from hotkey prompt).
|
||||
ShellCommand(String),
|
||||
/// Result of a previously-invoked shell command: call ID, stdout, stderr, and exit code.
|
||||
ShellCommandResult {
|
||||
call_id: String,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
exit_code: i32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -236,6 +236,11 @@ impl ChatComposer<'_> {
|
||||
self.open_external_editor();
|
||||
(InputResult::None, true)
|
||||
}
|
||||
Input { key: Key::Char('m'), ctrl: true, alt: false, shift: false } => {
|
||||
// Launch shell-command prompt
|
||||
self.app_event_tx.send(AppEvent::DispatchCommand(SlashCommand::Shell));
|
||||
(InputResult::None, true)
|
||||
}
|
||||
input => self.handle_input_basic(input),
|
||||
}
|
||||
}
|
||||
@@ -280,6 +285,12 @@ impl ChatComposer<'_> {
|
||||
let _ = self.textarea.insert_str(new_text);
|
||||
}
|
||||
|
||||
/// Return the current text in the composer input.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_input_text(&self) -> String {
|
||||
self.textarea.lines().join("\n")
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::user_approval_widget::ApprovalRequest;
|
||||
|
||||
mod approval_modal_view;
|
||||
mod mount_view;
|
||||
mod shell_command_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 shell_command_view::ShellCommandView;
|
||||
use status_indicator_view::StatusIndicatorView;
|
||||
use config_reload_view::ConfigReloadView;
|
||||
|
||||
@@ -175,6 +177,13 @@ impl BottomPane<'_> {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Launch interactive shell-command dialog (prompt for arbitrary command).
|
||||
pub fn push_shell_command_interactive(&mut self) {
|
||||
let view = ShellCommandView::new(self.app_event_tx.clone());
|
||||
self.active_view = Some(Box::new(view));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Called when the agent requests user approval.
|
||||
pub fn push_approval_request(&mut self, request: ApprovalRequest) {
|
||||
let request = if let Some(view) = self.active_view.as_mut() {
|
||||
@@ -272,7 +281,7 @@ mod tests {
|
||||
// No submission event is returned
|
||||
assert!(matches!(result, InputResult::None));
|
||||
// Composer should have recorded the input
|
||||
let content = pane.composer.textarea.lines().join("\n");
|
||||
let content = pane.composer.get_input_text();
|
||||
assert_eq!(content, "h");
|
||||
// Status indicator overlay remains active
|
||||
assert!(pane.active_view.is_some());
|
||||
|
||||
104
codex-rs/tui/src/bottom_pane/shell_command_view.rs
Normal file
104
codex-rs/tui/src/bottom_pane/shell_command_view.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Widget;
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||
use tui_input::{backend::crossterm::EventHandler, Input};
|
||||
|
||||
use super::BottomPane;
|
||||
use super::BottomPaneView;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
|
||||
/// Interactive view prompting for a shell command to run in the container.
|
||||
pub(crate) struct ShellCommandView {
|
||||
input: Input,
|
||||
app_event_tx: AppEventSender,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl ShellCommandView {
|
||||
pub fn new(app_event_tx: AppEventSender) -> Self {
|
||||
Self {
|
||||
input: Input::default(),
|
||||
app_event_tx,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BottomPaneView<'a> for ShellCommandView {
|
||||
fn handle_key_event(&mut self, pane: &mut BottomPane<'a>, key_event: KeyEvent) {
|
||||
if self.done {
|
||||
return;
|
||||
}
|
||||
if key_event.code == KeyCode::Enter {
|
||||
let cmd = self.input.value().to_string();
|
||||
self.app_event_tx.send(AppEvent::ShellCommand(cmd));
|
||||
self.done = true;
|
||||
} else {
|
||||
self.input.handle_event(&CrosstermEvent::Key(key_event));
|
||||
}
|
||||
pane.request_redraw();
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
self.done
|
||||
}
|
||||
|
||||
fn calculate_required_height(&self, _area: &Rect) -> u16 {
|
||||
// Prompt line + input line + border overhead
|
||||
1 + 1 + 2
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
let paragraph = Paragraph::new(vec![
|
||||
ratatui::text::Line::from("Shell command:"),
|
||||
ratatui::text::Line::from(self.input.value()),
|
||||
])
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded),
|
||||
);
|
||||
paragraph.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::{BottomPane, BottomPaneParams};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn submit_shell_command_emits_event() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let evt_tx = AppEventSender::new(tx);
|
||||
let mut view = ShellCommandView::new(evt_tx.clone());
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: evt_tx.clone(),
|
||||
has_input_focus: true,
|
||||
composer_max_rows: 1,
|
||||
});
|
||||
// Enter command 'a'
|
||||
view.handle_key_event(&mut pane, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
|
||||
view.handle_key_event(&mut pane, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
// Skip initial redraw event(s)
|
||||
let mut event;
|
||||
loop {
|
||||
event = rx.recv().unwrap();
|
||||
if matches!(event, AppEvent::ShellCommand(_)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let AppEvent::ShellCommand(cmd) = event {
|
||||
assert_eq!(cmd, "a");
|
||||
} else {
|
||||
panic!("expected ShellCommand event, got {:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ use crate::context::calculate_context_percent_remaining;
|
||||
use crate::conversation_history_widget::ConversationHistoryWidget;
|
||||
use crate::history_cell::PatchEventType;
|
||||
use crate::user_approval_widget::ApprovalRequest;
|
||||
use shlex;
|
||||
|
||||
pub(crate) struct ChatWidget<'a> {
|
||||
app_event_tx: AppEventSender,
|
||||
@@ -52,6 +53,8 @@ pub(crate) struct ChatWidget<'a> {
|
||||
initial_user_message: Option<UserMessage>,
|
||||
/// raw ResponseItem stream for context-left calculation
|
||||
history_items: Vec<ResponseItem>,
|
||||
/// Counter to generate unique call IDs for shell commands.
|
||||
next_shell_call_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
@@ -139,6 +142,7 @@ impl ChatWidget<'_> {
|
||||
initial_images,
|
||||
),
|
||||
history_items: Vec::new(),
|
||||
next_shell_call_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +477,40 @@ impl ChatWidget<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Launch interactive shell-command dialog.
|
||||
pub fn push_shell_command_interactive(&mut self) {
|
||||
self.bottom_pane.push_shell_command_interactive();
|
||||
self.request_redraw();
|
||||
}
|
||||
/// Handle a submitted shell command: record and execute it.
|
||||
pub fn handle_shell_command(&mut self, cmd: String) {
|
||||
let call_id = format!("shell-{}", self.next_shell_call_id);
|
||||
self.next_shell_call_id += 1;
|
||||
// Split command into arguments, fallback to raw string if parse fails
|
||||
let args = shlex::split(&cmd).unwrap_or_else(|| vec![cmd.clone()]);
|
||||
self.conversation_history.add_active_exec_command(call_id.clone(), args.clone());
|
||||
let tx = self.app_event_tx.clone();
|
||||
// Spawn execution in background
|
||||
tokio::spawn(async move {
|
||||
let output = std::process::Command::new("sh").arg("-c").arg(&cmd).output();
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
let code = out.status.code().unwrap_or(-1);
|
||||
tx.send(AppEvent::ShellCommandResult { call_id, stdout, stderr, exit_code: code });
|
||||
}
|
||||
Err(e) => {
|
||||
tx.send(AppEvent::ShellCommandResult { call_id, stdout: String::new(), stderr: e.to_string(), exit_code: -1 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/// Handle completion of a shell command: display its result.
|
||||
pub fn handle_shell_command_result(&mut self, call_id: String, stdout: String, stderr: String, exit_code: i32) {
|
||||
self.conversation_history.record_completed_exec_command(call_id, stdout, stderr, exit_code);
|
||||
}
|
||||
|
||||
fn request_redraw(&mut self) {
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ mod scroll_event_helper;
|
||||
mod slash_command;
|
||||
mod confirm_ctrl_d;
|
||||
mod status_indicator_widget;
|
||||
mod context;
|
||||
pub mod context;
|
||||
mod text_block;
|
||||
mod text_formatting;
|
||||
mod tui;
|
||||
|
||||
@@ -23,6 +23,8 @@ pub enum SlashCommand {
|
||||
MountRemove,
|
||||
/// Inspect sandbox and container environment (mounts, permissions, network).
|
||||
InspectEnv,
|
||||
/// Prompt to run a shell command in the container.
|
||||
Shell,
|
||||
}
|
||||
|
||||
impl SlashCommand {
|
||||
@@ -34,10 +36,11 @@ impl SlashCommand {
|
||||
"Toggle mouse mode (enable for scrolling, disable for text selection)",
|
||||
SlashCommand::EditPrompt =>
|
||||
"Open external editor to edit the current prompt.",
|
||||
SlashCommand::Quit => "Exit the application.",
|
||||
SlashCommand::MountAdd => "Add a mount: host path → container path.",
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -412,6 +412,7 @@ impl WidgetRef for &UserApprovalWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for approval widget behavior
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -432,7 +433,10 @@ mod tests {
|
||||
app_event_tx.clone(),
|
||||
);
|
||||
widget.mode = Mode::Input;
|
||||
widget.input.get_mut().set_value("feedback".to_string());
|
||||
// Simulate typing "feedback" into the input field
|
||||
for c in "feedback".chars() {
|
||||
widget.handle_key_event(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
|
||||
}
|
||||
widget.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert_eq!(widget.mode, Mode::Select);
|
||||
let expected_idx = SELECT_OPTIONS
|
||||
@@ -453,8 +457,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_truncate_middle_truncates() {
|
||||
// max_len 5 -> trim_len 4, start_len 2, end_len 2
|
||||
assert_eq!(truncate_middle("abcdef", 5), "ab…ef");
|
||||
// max_len 5 -> trim_len 4 -> start 1, end 1
|
||||
assert_eq!(truncate_middle("abcdef", 5), "a…f");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user