As stated in `codex-rs/README.md`: Today, Codex CLI is written in TypeScript and requires Node.js 22+ to run it. For a number of users, this runtime requirement inhibits adoption: they would be better served by a standalone executable. As maintainers, we want Codex to run efficiently in a wide range of environments with minimal overhead. We also want to take advantage of operating system-specific APIs to provide better sandboxing, where possible. To that end, we are moving forward with a Rust implementation of Codex CLI contained in this folder, which has the following benefits: - The CLI compiles to small, standalone, platform-specific binaries. - Can make direct, native calls to [seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and [landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in order to support sandboxing on Linux. - No runtime garbage collection, resulting in lower memory consumption and better, more predictable performance. Currently, the Rust implementation is materially behind the TypeScript implementation in functionality, so continue to use the TypeScript implmentation for the time being. We will publish native executables via GitHub Releases as soon as we feel the Rust version is usable.
272 lines
9.3 KiB
Rust
272 lines
9.3 KiB
Rust
use codex_ansi_escape::ansi_escape_line;
|
||
use codex_core::protocol::FileChange;
|
||
use ratatui::prelude::*;
|
||
use ratatui::style::Color;
|
||
use ratatui::style::Modifier;
|
||
use ratatui::style::Style;
|
||
use ratatui::text::Line as RtLine;
|
||
use ratatui::text::Span as RtSpan;
|
||
use std::collections::HashMap;
|
||
use std::path::PathBuf;
|
||
use std::time::Duration;
|
||
use std::time::Instant;
|
||
|
||
use crate::exec_command::escape_command;
|
||
|
||
pub(crate) struct CommandOutput {
|
||
pub(crate) exit_code: i32,
|
||
pub(crate) stdout: String,
|
||
pub(crate) stderr: String,
|
||
pub(crate) duration: Duration,
|
||
}
|
||
|
||
pub(crate) enum PatchEventType {
|
||
ApprovalRequest,
|
||
ApplyBegin { auto_approved: bool },
|
||
}
|
||
|
||
/// Represents an event to display in the conversation history. Returns its
|
||
/// `Vec<Line<'static>>` representation to make it easier to display in a
|
||
/// scrollable list.
|
||
pub(crate) enum HistoryCell {
|
||
/// Message from the user.
|
||
UserPrompt { lines: Vec<Line<'static>> },
|
||
|
||
/// Message from the agent.
|
||
AgentMessage { lines: Vec<Line<'static>> },
|
||
|
||
/// An exec tool call that has not finished yet.
|
||
ActiveExecCommand {
|
||
call_id: String,
|
||
/// The shell command, escaped and formatted.
|
||
command: String,
|
||
start: Instant,
|
||
lines: Vec<Line<'static>>,
|
||
},
|
||
|
||
/// Completed exec tool call.
|
||
CompletedExecCommand { lines: Vec<Line<'static>> },
|
||
|
||
/// Background event
|
||
BackgroundEvent { lines: Vec<Line<'static>> },
|
||
|
||
/// Info describing the newly‑initialized session.
|
||
SessionInfo { lines: Vec<Line<'static>> },
|
||
|
||
/// A pending code patch that is awaiting user approval. Mirrors the
|
||
/// behaviour of `ActiveExecCommand` so the user sees *what* patch the
|
||
/// model wants to apply before being prompted to approve or deny it.
|
||
PendingPatch {
|
||
/// Identifier so that a future `PatchApplyEnd` can update the entry
|
||
/// with the final status (not yet implemented).
|
||
lines: Vec<Line<'static>>,
|
||
},
|
||
}
|
||
|
||
impl HistoryCell {
|
||
pub(crate) fn new_user_prompt(message: String) -> Self {
|
||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||
lines.push(Line::from("user".cyan().bold()));
|
||
lines.extend(message.lines().map(|l| Line::from(l.to_string())));
|
||
lines.push(Line::from(""));
|
||
|
||
HistoryCell::UserPrompt { lines }
|
||
}
|
||
|
||
pub(crate) fn new_agent_message(message: String) -> Self {
|
||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||
lines.push(Line::from("codex".magenta().bold()));
|
||
lines.extend(message.lines().map(|l| Line::from(l.to_string())));
|
||
lines.push(Line::from(""));
|
||
|
||
HistoryCell::AgentMessage { lines }
|
||
}
|
||
|
||
pub(crate) fn new_active_exec_command(call_id: String, command: Vec<String>) -> Self {
|
||
let command_escaped = escape_command(&command);
|
||
let start = Instant::now();
|
||
|
||
let lines: Vec<Line<'static>> = vec![
|
||
Line::from(vec!["command".magenta(), " running...".dim()]),
|
||
Line::from(format!("$ {command_escaped}")),
|
||
Line::from(""),
|
||
];
|
||
|
||
HistoryCell::ActiveExecCommand {
|
||
call_id,
|
||
command: command_escaped,
|
||
start,
|
||
lines,
|
||
}
|
||
}
|
||
|
||
pub(crate) fn new_completed_exec_command(command: String, output: CommandOutput) -> Self {
|
||
let CommandOutput {
|
||
exit_code,
|
||
stdout,
|
||
stderr,
|
||
duration,
|
||
} = output;
|
||
|
||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||
|
||
// Title depends on whether we have output yet.
|
||
let title_line = Line::from(vec![
|
||
"command".magenta(),
|
||
format!(" (code: {}, duration: {:?})", exit_code, duration).dim(),
|
||
]);
|
||
lines.push(title_line);
|
||
|
||
const MAX_LINES: usize = 5;
|
||
|
||
let src = if exit_code == 0 { stdout } else { stderr };
|
||
|
||
lines.push(Line::from(format!("$ {command}")));
|
||
let mut lines_iter = src.lines();
|
||
for raw in lines_iter.by_ref().take(MAX_LINES) {
|
||
lines.push(ansi_escape_line(raw).dim());
|
||
}
|
||
let remaining = lines_iter.count();
|
||
if remaining > 0 {
|
||
lines.push(Line::from(format!("... {} additional lines", remaining)).dim());
|
||
}
|
||
lines.push(Line::from(""));
|
||
|
||
HistoryCell::CompletedExecCommand { lines }
|
||
}
|
||
|
||
pub(crate) fn new_background_event(message: String) -> Self {
|
||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||
lines.push(Line::from("event".dim()));
|
||
lines.extend(message.lines().map(|l| Line::from(l.to_string()).dim()));
|
||
lines.push(Line::from(""));
|
||
HistoryCell::BackgroundEvent { lines }
|
||
}
|
||
|
||
pub(crate) fn new_session_info(
|
||
model: String,
|
||
cwd: std::path::PathBuf,
|
||
approval_policy: codex_core::protocol::AskForApproval,
|
||
) -> Self {
|
||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||
|
||
lines.push(Line::from("codex session:".magenta().bold()));
|
||
lines.push(Line::from(vec!["↳ model: ".bold(), model.into()]));
|
||
lines.push(Line::from(vec![
|
||
"↳ cwd: ".bold(),
|
||
cwd.display().to_string().into(),
|
||
]));
|
||
lines.push(Line::from(vec![
|
||
"↳ approval: ".bold(),
|
||
format!("{:?}", approval_policy).into(),
|
||
]));
|
||
lines.push(Line::from(""));
|
||
|
||
HistoryCell::SessionInfo { lines }
|
||
}
|
||
|
||
/// Create a new `PendingPatch` cell that lists the file‑level summary of
|
||
/// a proposed patch. The summary lines should already be formatted (e.g.
|
||
/// "A path/to/file.rs").
|
||
pub(crate) fn new_patch_event(
|
||
event_type: PatchEventType,
|
||
changes: HashMap<PathBuf, FileChange>,
|
||
) -> Self {
|
||
let title = match event_type {
|
||
PatchEventType::ApprovalRequest => "proposed patch",
|
||
PatchEventType::ApplyBegin {
|
||
auto_approved: true,
|
||
} => "applying patch",
|
||
PatchEventType::ApplyBegin {
|
||
auto_approved: false,
|
||
} => {
|
||
let lines = vec![Line::from("patch applied".magenta().bold())];
|
||
return Self::PendingPatch { lines };
|
||
}
|
||
};
|
||
|
||
let summary_lines = create_diff_summary(changes);
|
||
|
||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||
|
||
// Header similar to the command formatter so patches are visually
|
||
// distinct while still fitting the overall colour scheme.
|
||
lines.push(Line::from(title.magenta().bold()));
|
||
|
||
for line in summary_lines {
|
||
if line.starts_with('+') {
|
||
lines.push(line.green().into());
|
||
} else if line.starts_with('-') {
|
||
lines.push(line.red().into());
|
||
} else if let Some(space_idx) = line.find(' ') {
|
||
let kind_owned = line[..space_idx].to_string();
|
||
let rest_owned = line[space_idx + 1..].to_string();
|
||
|
||
let style_for = |fg: Color| Style::default().fg(fg).add_modifier(Modifier::BOLD);
|
||
|
||
let styled_kind = match kind_owned.as_str() {
|
||
"A" => RtSpan::styled(kind_owned.clone(), style_for(Color::Green)),
|
||
"D" => RtSpan::styled(kind_owned.clone(), style_for(Color::Red)),
|
||
"M" => RtSpan::styled(kind_owned.clone(), style_for(Color::Yellow)),
|
||
"R" | "C" => RtSpan::styled(kind_owned.clone(), style_for(Color::Cyan)),
|
||
_ => RtSpan::raw(kind_owned.clone()),
|
||
};
|
||
|
||
let styled_line =
|
||
RtLine::from(vec![styled_kind, RtSpan::raw(" "), RtSpan::raw(rest_owned)]);
|
||
lines.push(styled_line);
|
||
} else {
|
||
lines.push(Line::from(line));
|
||
}
|
||
}
|
||
|
||
lines.push(Line::from(""));
|
||
|
||
HistoryCell::PendingPatch { lines }
|
||
}
|
||
|
||
pub(crate) fn lines(&self) -> &Vec<Line<'static>> {
|
||
match self {
|
||
HistoryCell::UserPrompt { lines, .. }
|
||
| HistoryCell::AgentMessage { lines, .. }
|
||
| HistoryCell::BackgroundEvent { lines, .. }
|
||
| HistoryCell::SessionInfo { lines, .. }
|
||
| HistoryCell::ActiveExecCommand { lines, .. }
|
||
| HistoryCell::CompletedExecCommand { lines, .. }
|
||
| HistoryCell::PendingPatch { lines, .. } => lines,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn create_diff_summary(changes: HashMap<PathBuf, FileChange>) -> Vec<String> {
|
||
// Build a concise, human‑readable summary list similar to the
|
||
// `git status` short format so the user can reason about the
|
||
// patch without scrolling.
|
||
let mut summaries: Vec<String> = Vec::new();
|
||
for (path, change) in &changes {
|
||
use codex_core::protocol::FileChange::*;
|
||
match change {
|
||
Add { content } => {
|
||
let added = content.lines().count();
|
||
summaries.push(format!("A {} (+{added})", path.display()));
|
||
}
|
||
Delete => {
|
||
summaries.push(format!("D {}", path.display()));
|
||
}
|
||
Update {
|
||
unified_diff,
|
||
move_path,
|
||
} => {
|
||
if let Some(new_path) = move_path {
|
||
summaries.push(format!("R {} → {}", path.display(), new_path.display(),));
|
||
} else {
|
||
summaries.push(format!("M {}", path.display(),));
|
||
}
|
||
summaries.extend(unified_diff.lines().map(|s| s.to_string()));
|
||
}
|
||
}
|
||
}
|
||
|
||
summaries
|
||
}
|