mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
Compare commits
8 Commits
rust-v.0.0
...
rust-v0.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f7a54501c | ||
|
|
2f1d96e77d | ||
|
|
84aaefa102 | ||
|
|
c432d9ef81 | ||
|
|
4746ee900f | ||
|
|
f2ed46ceca | ||
|
|
e42dacbdc8 | ||
|
|
5122fe647f |
14
.github/workflows/rust-release.yml
vendored
14
.github/workflows/rust-release.yml
vendored
@@ -9,14 +9,14 @@ name: rust-release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "rust-v.*.*.*"
|
||||
- "rust-v*.*.*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TAG_REGEX: '^rust-v\.[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
TAG_REGEX: '^rust-v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
|
||||
jobs:
|
||||
tag-check:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|| { echo "❌ Tag '${GITHUB_REF_NAME}' != ${TAG_REGEX}"; exit 1; }
|
||||
|
||||
# 2. Extract versions
|
||||
tag_ver="${GITHUB_REF_NAME#rust-v.}"
|
||||
tag_ver="${GITHUB_REF_NAME#rust-v}"
|
||||
cargo_ver="$(grep -m1 '^version' codex-rs/Cargo.toml \
|
||||
| sed -E 's/version *= *"([^"]+)".*/\1/')"
|
||||
|
||||
@@ -106,15 +106,17 @@ jobs:
|
||||
cp target/${{ matrix.target }}/release/codex-exec "$dest/codex-exec-${{ matrix.target }}"
|
||||
cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}"
|
||||
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }} || ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
name: Stage Linux-only artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
dest="dist/${{ matrix.target }}"
|
||||
cp target/${{ matrix.target }}/release/codex-linux-sandbox "$dest/codex-linux-sandbox-${{ matrix.target }}"
|
||||
|
||||
- name: Compress artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
dest="dist/${{ matrix.target }}"
|
||||
zstd -T0 -19 --rm "$dest"/*
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
@@ -141,10 +143,10 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
files: dist/**
|
||||
# TODO(ragona): I'm going to leave these as prerelease/draft for now.
|
||||
# TODO(ragona): I'm going to leave these as draft for now.
|
||||
# It gives us 1) clarity that these are not yet a stable version, and
|
||||
# 2) allows a human step to review the release before publishing the draft.
|
||||
prerelease: true
|
||||
prerelease: false
|
||||
draft: true
|
||||
|
||||
- uses: facebook/dotslash-publish-release@v2
|
||||
|
||||
@@ -640,7 +640,7 @@ To publish a new version of the CLI, run the release scripts defined in `codex-c
|
||||
3. Bump the version and `CLI_VERSION` to current datetime: `pnpm release:version`
|
||||
4. Commit the version bump (with DCO sign-off):
|
||||
```bash
|
||||
git add codex-cli/src/utils/session.ts codex-cli/package.json
|
||||
git add codex-cli/package.json
|
||||
git commit -s -m "chore(release): codex-cli v$(node -p \"require('./codex-cli/package.json').version\")"
|
||||
```
|
||||
5. Copy README, build, and publish to npm: `pnpm release`
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"build": "node build.mjs",
|
||||
"build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js",
|
||||
"release:readme": "cp ../README.md ./README.md",
|
||||
"release:version": "TS=$(date +%y%m%d%H%M) && sed -E -i'' -e \"s/\\\"0\\.1\\.[0-9]{10}\\\"/\\\"0.1.${TS}\\\"/g\" package.json src/utils/session.ts",
|
||||
"release:version": "TS=$(date +%y%m%d%H%M) && sed -E -i'' -e \"s/\\\"0\\.1\\.[0-9]{10}\\\"/\\\"0.1.${TS}\\\"/g\" package.json",
|
||||
"release:build-and-publish": "pnpm run build && npm publish",
|
||||
"release": "pnpm run release:readme && pnpm run release:version && pnpm install && pnpm run release:build-and-publish"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const CLI_VERSION = "0.1.2504251709"; // Must be in sync with package.json.
|
||||
// Node ESM supports JSON imports behind an assertion. TypeScript's
|
||||
// `resolveJsonModule` takes care of the typings.
|
||||
import pkg from "../../package.json" assert { type: "json" };
|
||||
|
||||
// Read the version directly from package.json.
|
||||
export const CLI_VERSION: string = (pkg as { version: string }).version;
|
||||
export const ORIGIN = "codex_cli_ts";
|
||||
|
||||
export type TerminalChatSession = {
|
||||
|
||||
19
codex-rs/Cargo.lock
generated
19
codex-rs/Cargo.lock
generated
@@ -469,13 +469,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codex-cli"
|
||||
version = "0.0.2504291954"
|
||||
version = "0.0.2504301132"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-core",
|
||||
"codex-exec",
|
||||
"codex-repl",
|
||||
"codex-tui",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -524,7 +523,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codex-exec"
|
||||
version = "0.0.2504291954"
|
||||
version = "0.0.2504301132"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -557,20 +556,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-repl"
|
||||
version = "0.0.2504291954"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-core",
|
||||
"owo-colors 4.2.0",
|
||||
"rand",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-tui"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -7,12 +7,11 @@ members = [
|
||||
"core",
|
||||
"exec",
|
||||
"execpolicy",
|
||||
"repl",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.2504291954"
|
||||
version = "0.0.2504301132"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
||||
@@ -19,5 +19,4 @@ This folder is the root of a Cargo workspace. It contains quite a bit of experim
|
||||
- [`core/`](./core) contains the business logic for Codex. Ultimately, we hope this to be a library crate that is generally useful for building other Rust/native applications that use Codex.
|
||||
- [`exec/`](./exec) "headless" CLI for use in automation.
|
||||
- [`tui/`](./tui) CLI that launches a fullscreen TUI built with [Ratatui](https://ratatui.rs/).
|
||||
- [`repl/`](./repl) CLI that launches a lightweight REPL similar to the Python or Node.js REPL.
|
||||
- [`cli/`](./cli) CLI multitool that provides the aforementioned CLIs via subcommands.
|
||||
|
||||
@@ -20,7 +20,6 @@ anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
codex-core = { path = "../core" }
|
||||
codex-exec = { path = "../exec" }
|
||||
codex-repl = { path = "../repl" }
|
||||
codex-tui = { path = "../tui" }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = [
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_cli::seatbelt;
|
||||
use codex_cli::LandlockCommand;
|
||||
use codex_cli::SeatbeltCommand;
|
||||
use codex_exec::Cli as ExecCli;
|
||||
use codex_repl::Cli as ReplCli;
|
||||
use codex_tui::Cli as TuiCli;
|
||||
|
||||
use crate::proto::ProtoCli;
|
||||
@@ -34,10 +33,6 @@ enum Subcommand {
|
||||
#[clap(visible_alias = "e")]
|
||||
Exec(ExecCli),
|
||||
|
||||
/// Run the REPL.
|
||||
#[clap(visible_alias = "r")]
|
||||
Repl(ReplCli),
|
||||
|
||||
/// Run the Protocol stream via stdin/stdout
|
||||
#[clap(visible_alias = "p")]
|
||||
Proto(ProtoCli),
|
||||
@@ -75,9 +70,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
Some(Subcommand::Exec(exec_cli)) => {
|
||||
codex_exec::run_main(exec_cli).await?;
|
||||
}
|
||||
Some(Subcommand::Repl(repl_cli)) => {
|
||||
codex_repl::run_main(repl_cli).await?;
|
||||
}
|
||||
Some(Subcommand::Proto(proto_cli)) => {
|
||||
proto::run_main(proto_cli).await?;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ install:
|
||||
tui *args:
|
||||
cargo run --bin codex -- tui {{args}}
|
||||
|
||||
# Run the REPL app
|
||||
repl *args:
|
||||
cargo run --bin codex -- repl {{args}}
|
||||
|
||||
# Run the Proto app
|
||||
proto *args:
|
||||
cargo run --bin codex -- proto {{args}}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
name = "codex-repl"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "codex-repl"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "codex_repl"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
codex-core = { path = "../core", features = ["cli"] }
|
||||
owo-colors = "4.2.0"
|
||||
rand = "0.9"
|
||||
tokio = { version = "1", features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
tracing = { version = "0.1.41", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
@@ -1,65 +0,0 @@
|
||||
use clap::ArgAction;
|
||||
use clap::Parser;
|
||||
use codex_core::ApprovalModeCliArg;
|
||||
use codex_core::SandboxPermissionOption;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Command‑line arguments.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
author,
|
||||
version,
|
||||
about = "Interactive Codex CLI that streams all agent actions."
|
||||
)]
|
||||
pub struct Cli {
|
||||
/// User prompt to start the session.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Override the default model from ~/.codex/config.toml.
|
||||
#[arg(short, long)]
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Optional images to attach to the prompt.
|
||||
#[arg(long, value_name = "FILE")]
|
||||
pub images: Vec<PathBuf>,
|
||||
|
||||
/// Increase verbosity (-v info, -vv debug, -vvv trace).
|
||||
///
|
||||
/// The flag may be passed up to three times. Without any -v the CLI only prints warnings and errors.
|
||||
#[arg(short, long, action = ArgAction::Count)]
|
||||
pub verbose: u8,
|
||||
|
||||
/// Don't use colored ansi output for verbose logging
|
||||
#[arg(long)]
|
||||
pub no_ansi: bool,
|
||||
|
||||
/// Configure when the model requires human approval before executing a command.
|
||||
#[arg(long = "ask-for-approval", short = 'a')]
|
||||
pub approval_policy: Option<ApprovalModeCliArg>,
|
||||
|
||||
/// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, network-disabled sandbox that can write to cwd and TMPDIR)
|
||||
#[arg(long = "full-auto", default_value_t = false)]
|
||||
pub full_auto: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub sandbox: SandboxPermissionOption,
|
||||
|
||||
/// Allow running Codex outside a Git repository. By default the CLI
|
||||
/// aborts early when the current working directory is **not** inside a
|
||||
/// Git repo because most agents rely on `git` for interacting with the
|
||||
/// code‑base. Pass this flag if you really know what you are doing.
|
||||
#[arg(long, action = ArgAction::SetTrue, default_value_t = false)]
|
||||
pub allow_no_git_exec: bool,
|
||||
|
||||
/// Disable server‑side response storage (sends the full conversation context with every request)
|
||||
#[arg(long = "disable-response-storage", default_value_t = false)]
|
||||
pub disable_response_storage: bool,
|
||||
|
||||
/// Record submissions into file as JSON
|
||||
#[arg(short = 'S', long)]
|
||||
pub record_submissions: Option<PathBuf>,
|
||||
|
||||
/// Record events into file as JSON
|
||||
#[arg(short = 'E', long)]
|
||||
pub record_events: Option<PathBuf>,
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
use std::io::stdin;
|
||||
use std::io::stdout;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::protocol;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::util::is_inside_git_repo;
|
||||
use codex_core::util::notify_on_sigint;
|
||||
use codex_core::Codex;
|
||||
use owo_colors::OwoColorize;
|
||||
use owo_colors::Style;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::io::Lines;
|
||||
use tokio::io::Stdin;
|
||||
use tokio::sync::Notify;
|
||||
use tracing::debug;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod cli;
|
||||
pub use cli::Cli;
|
||||
|
||||
/// Initialize the global logger once at startup based on the `--verbose` flag.
|
||||
fn init_logger(verbose: u8, allow_ansi: bool) {
|
||||
// Map -v occurrences to explicit log levels:
|
||||
// 0 → warn (default)
|
||||
// 1 → info
|
||||
// 2 → debug
|
||||
// ≥3 → trace
|
||||
|
||||
let default_level = match verbose {
|
||||
0 => "warn",
|
||||
1 => "info",
|
||||
2 => "codex=debug",
|
||||
_ => "codex=trace",
|
||||
};
|
||||
|
||||
// Only initialize the logger once – repeated calls are ignored. `try_init` will return an
|
||||
// error if another crate (like tests) initialized it first, which we can safely ignore.
|
||||
// By default `tracing_subscriber::fmt()` writes formatted logs to stderr. That is fine when
|
||||
// running the CLI manually but in our smoke tests we capture **stdout** (via `assert_cmd`) and
|
||||
// ignore stderr. As a result none of the `tracing::info!` banners or warnings show up in the
|
||||
// recorded output making it much harder to debug live runs.
|
||||
|
||||
// Switch the logger's writer to stdout so both human runs and the integration tests see the
|
||||
// same stream. Disable ANSI colors because the binary already prints plain text and color
|
||||
// escape codes make predicate matching brittle.
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new(default_level))
|
||||
.unwrap(),
|
||||
)
|
||||
.with_ansi(allow_ansi)
|
||||
.with_writer(std::io::stdout)
|
||||
.try_init();
|
||||
}
|
||||
|
||||
pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
let ctrl_c = notify_on_sigint();
|
||||
|
||||
// Abort early when the user runs Codex outside a Git repository unless
|
||||
// they explicitly acknowledged the risks with `--allow-no-git-exec`.
|
||||
if !cli.allow_no_git_exec && !is_inside_git_repo() {
|
||||
eprintln!(
|
||||
"We recommend running codex inside a git repository. \
|
||||
If you understand the risks, you can proceed with \
|
||||
`--allow-no-git-exec`."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Initialize logging before any other work so early errors are captured.
|
||||
init_logger(cli.verbose, !cli.no_ansi);
|
||||
|
||||
let (sandbox_policy, approval_policy) = if cli.full_auto {
|
||||
(
|
||||
Some(SandboxPolicy::new_full_auto_policy()),
|
||||
Some(AskForApproval::OnFailure),
|
||||
)
|
||||
} else {
|
||||
let sandbox_policy = cli.sandbox.permissions.clone().map(Into::into);
|
||||
(sandbox_policy, cli.approval_policy.map(Into::into))
|
||||
};
|
||||
|
||||
// Load config file and apply CLI overrides (model & approval policy)
|
||||
let overrides = ConfigOverrides {
|
||||
model: cli.model.clone(),
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
disable_response_storage: if cli.disable_response_storage {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
let config = Config::load_with_overrides(overrides)?;
|
||||
|
||||
codex_main(cli, config, ctrl_c).await
|
||||
}
|
||||
|
||||
async fn codex_main(cli: Cli, cfg: Config, ctrl_c: Arc<Notify>) -> anyhow::Result<()> {
|
||||
let mut builder = Codex::builder();
|
||||
if let Some(path) = cli.record_submissions {
|
||||
builder = builder.record_submissions(path);
|
||||
}
|
||||
if let Some(path) = cli.record_events {
|
||||
builder = builder.record_events(path);
|
||||
}
|
||||
let codex = builder.spawn(Arc::clone(&ctrl_c))?;
|
||||
let init_id = random_id();
|
||||
let init = protocol::Submission {
|
||||
id: init_id.clone(),
|
||||
op: protocol::Op::ConfigureSession {
|
||||
model: cfg.model,
|
||||
instructions: cfg.instructions,
|
||||
approval_policy: cfg.approval_policy,
|
||||
sandbox_policy: cfg.sandbox_policy,
|
||||
disable_response_storage: cfg.disable_response_storage,
|
||||
},
|
||||
};
|
||||
|
||||
out(
|
||||
"initializing session",
|
||||
MessagePriority::BackgroundEvent,
|
||||
MessageActor::User,
|
||||
);
|
||||
codex.submit(init).await?;
|
||||
|
||||
// init
|
||||
loop {
|
||||
out(
|
||||
"waiting for session initialization",
|
||||
MessagePriority::BackgroundEvent,
|
||||
MessageActor::User,
|
||||
);
|
||||
let event = codex.next_event().await?;
|
||||
if event.id == init_id {
|
||||
if let protocol::EventMsg::Error { message } = event.msg {
|
||||
anyhow::bail!("Error during initialization: {message}");
|
||||
} else {
|
||||
out(
|
||||
"session initialized",
|
||||
MessagePriority::BackgroundEvent,
|
||||
MessageActor::User,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// run loop
|
||||
let mut reader = InputReader::new(ctrl_c.clone());
|
||||
loop {
|
||||
let text = match &cli.prompt {
|
||||
Some(input) => input.clone(),
|
||||
None => match reader.request_input().await? {
|
||||
Some(input) => input,
|
||||
None => {
|
||||
// ctrl + d
|
||||
println!();
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
if text.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Interpret certain single‑word commands as immediate termination requests.
|
||||
let trimmed = text.trim();
|
||||
if trimmed == "q" {
|
||||
// Exit gracefully.
|
||||
println!("Exiting…");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let sub = protocol::Submission {
|
||||
id: random_id(),
|
||||
op: protocol::Op::UserInput {
|
||||
items: vec![protocol::InputItem::Text { text }],
|
||||
},
|
||||
};
|
||||
|
||||
out(
|
||||
"sending request to model",
|
||||
MessagePriority::TaskProgress,
|
||||
MessageActor::User,
|
||||
);
|
||||
codex.submit(sub).await?;
|
||||
|
||||
// Wait for agent events **or** user interrupts (Ctrl+C).
|
||||
'inner: loop {
|
||||
// Listen for either the next agent event **or** a SIGINT notification. Using
|
||||
// `tokio::select!` allows the user to cancel a long‑running request that would
|
||||
// otherwise leave the CLI stuck waiting for a server response.
|
||||
let event = {
|
||||
let interrupted = ctrl_c.notified();
|
||||
tokio::select! {
|
||||
_ = interrupted => {
|
||||
// Forward an interrupt to the agent so it can abort any in‑flight task.
|
||||
let _ = codex
|
||||
.submit(protocol::Submission {
|
||||
id: random_id(),
|
||||
op: protocol::Op::Interrupt,
|
||||
})
|
||||
.await;
|
||||
|
||||
// Exit the inner loop and return to the main input prompt. The agent
|
||||
// will emit a `TurnInterrupted` (Error) event which is drained later.
|
||||
break 'inner;
|
||||
}
|
||||
res = codex.next_event() => res?
|
||||
}
|
||||
};
|
||||
|
||||
debug!(?event, "Got event");
|
||||
let id = event.id;
|
||||
match event.msg {
|
||||
protocol::EventMsg::Error { message } => {
|
||||
println!("Error: {message}");
|
||||
break 'inner;
|
||||
}
|
||||
protocol::EventMsg::TaskComplete => break 'inner,
|
||||
protocol::EventMsg::AgentMessage { message } => {
|
||||
out(&message, MessagePriority::UserMessage, MessageActor::Agent)
|
||||
}
|
||||
protocol::EventMsg::SessionConfigured { model } => {
|
||||
debug!(model, "Session initialized");
|
||||
}
|
||||
protocol::EventMsg::ExecApprovalRequest {
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
} => {
|
||||
let reason_str = reason
|
||||
.as_deref()
|
||||
.map(|r| format!(" [{r}]"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let prompt = format!(
|
||||
"approve command in {} {}{} (y/N): ",
|
||||
cwd.display(),
|
||||
command.join(" "),
|
||||
reason_str
|
||||
);
|
||||
let decision = request_user_approval2(prompt)?;
|
||||
let sub = protocol::Submission {
|
||||
id: random_id(),
|
||||
op: protocol::Op::ExecApproval { id, decision },
|
||||
};
|
||||
out(
|
||||
"submitting command approval",
|
||||
MessagePriority::TaskProgress,
|
||||
MessageActor::User,
|
||||
);
|
||||
codex.submit(sub).await?;
|
||||
}
|
||||
protocol::EventMsg::ApplyPatchApprovalRequest {
|
||||
changes,
|
||||
reason: _,
|
||||
grant_root: _,
|
||||
} => {
|
||||
let file_list = changes
|
||||
.keys()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let request =
|
||||
format!("approve apply_patch that will touch? {file_list} (y/N): ");
|
||||
let decision = request_user_approval2(request)?;
|
||||
let sub = protocol::Submission {
|
||||
id: random_id(),
|
||||
op: protocol::Op::PatchApproval { id, decision },
|
||||
};
|
||||
out(
|
||||
"submitting patch approval",
|
||||
MessagePriority::UserMessage,
|
||||
MessageActor::Agent,
|
||||
);
|
||||
codex.submit(sub).await?;
|
||||
}
|
||||
protocol::EventMsg::ExecCommandBegin {
|
||||
command,
|
||||
cwd,
|
||||
call_id: _,
|
||||
} => {
|
||||
out(
|
||||
&format!("running command: '{}' in '{}'", command.join(" "), cwd),
|
||||
MessagePriority::BackgroundEvent,
|
||||
MessageActor::Agent,
|
||||
);
|
||||
}
|
||||
protocol::EventMsg::ExecCommandEnd {
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code,
|
||||
call_id: _,
|
||||
} => {
|
||||
let msg = if exit_code == 0 {
|
||||
"command completed (exit 0)".to_string()
|
||||
} else {
|
||||
// Prefer stderr but fall back to stdout if empty.
|
||||
let err_snippet = if !stderr.trim().is_empty() {
|
||||
stderr.trim()
|
||||
} else {
|
||||
stdout.trim()
|
||||
};
|
||||
format!("command failed (exit {exit_code}): {err_snippet}")
|
||||
};
|
||||
out(&msg, MessagePriority::BackgroundEvent, MessageActor::Agent);
|
||||
out(
|
||||
"sending results to model",
|
||||
MessagePriority::TaskProgress,
|
||||
MessageActor::Agent,
|
||||
);
|
||||
}
|
||||
protocol::EventMsg::PatchApplyBegin { changes, .. } => {
|
||||
// Emit PatchApplyBegin so the front‑end can show progress.
|
||||
let summary = changes
|
||||
.iter()
|
||||
.map(|(path, change)| match change {
|
||||
FileChange::Add { .. } => format!("A {}", path.display()),
|
||||
FileChange::Delete => format!("D {}", path.display()),
|
||||
FileChange::Update { .. } => format!("M {}", path.display()),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
out(
|
||||
&format!("applying patch: {summary}"),
|
||||
MessagePriority::BackgroundEvent,
|
||||
MessageActor::Agent,
|
||||
);
|
||||
}
|
||||
protocol::EventMsg::PatchApplyEnd { success, .. } => {
|
||||
let status = if success { "success" } else { "failed" };
|
||||
out(
|
||||
&format!("patch application {status}"),
|
||||
MessagePriority::BackgroundEvent,
|
||||
MessageActor::Agent,
|
||||
);
|
||||
out(
|
||||
"sending results to model",
|
||||
MessagePriority::TaskProgress,
|
||||
MessageActor::Agent,
|
||||
);
|
||||
}
|
||||
// Broad fallback; if the CLI is unaware of an event type, it will just
|
||||
// print it as a generic BackgroundEvent.
|
||||
e => {
|
||||
out(
|
||||
&format!("event: {e:?}"),
|
||||
MessagePriority::BackgroundEvent,
|
||||
MessageActor::Agent,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn random_id() -> String {
|
||||
let id: u64 = rand::random();
|
||||
id.to_string()
|
||||
}
|
||||
|
||||
fn request_user_approval2(request: String) -> anyhow::Result<protocol::ReviewDecision> {
|
||||
println!("{}", request);
|
||||
|
||||
let mut line = String::new();
|
||||
stdin().read_line(&mut line)?;
|
||||
let answer = line.trim().to_ascii_lowercase();
|
||||
let is_accepted = answer == "y" || answer == "yes";
|
||||
let decision = if is_accepted {
|
||||
protocol::ReviewDecision::Approved
|
||||
} else {
|
||||
protocol::ReviewDecision::Denied
|
||||
};
|
||||
Ok(decision)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum MessagePriority {
|
||||
BackgroundEvent,
|
||||
TaskProgress,
|
||||
UserMessage,
|
||||
}
|
||||
enum MessageActor {
|
||||
Agent,
|
||||
User,
|
||||
}
|
||||
|
||||
impl From<MessageActor> for String {
|
||||
fn from(actor: MessageActor) -> Self {
|
||||
match actor {
|
||||
MessageActor::Agent => "codex".to_string(),
|
||||
MessageActor::User => "user".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn out(msg: &str, priority: MessagePriority, actor: MessageActor) {
|
||||
let actor: String = actor.into();
|
||||
let style = match priority {
|
||||
MessagePriority::BackgroundEvent => Style::new().fg_rgb::<127, 127, 127>(),
|
||||
MessagePriority::TaskProgress => Style::new().fg_rgb::<200, 200, 200>(),
|
||||
MessagePriority::UserMessage => Style::new().white(),
|
||||
};
|
||||
|
||||
println!("{}> {}", actor.bold(), msg.style(style));
|
||||
}
|
||||
|
||||
struct InputReader {
|
||||
reader: Lines<BufReader<Stdin>>,
|
||||
ctrl_c: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl InputReader {
|
||||
pub fn new(ctrl_c: Arc<Notify>) -> Self {
|
||||
Self {
|
||||
reader: BufReader::new(tokio::io::stdin()).lines(),
|
||||
ctrl_c,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_input(&mut self) -> std::io::Result<Option<String>> {
|
||||
print!("user> ");
|
||||
stdout().flush()?;
|
||||
let interrupted = self.ctrl_c.notified();
|
||||
tokio::select! {
|
||||
line = self.reader.next_line() => {
|
||||
match line? {
|
||||
Some(input) => Ok(Some(input.trim().to_string())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
_ = interrupted => {
|
||||
println!();
|
||||
Ok(Some(String::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use clap::Parser;
|
||||
use codex_repl::run_main;
|
||||
use codex_repl::Cli;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
run_main(cli).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user