core-agent-ide/codex-rs/debug-client/src/main.rs
Michael Bolin b77fe8fefe
Apply argument comment lint across codex-rs (#14652)
## Why

Once the repo-local lint exists, `codex-rs` needs to follow the
checked-in convention and CI needs to keep it from drifting. This commit
applies the fallback `/*param*/` style consistently across existing
positional literal call sites without changing those APIs.

The longer-term preference is still to avoid APIs that require comments
by choosing clearer parameter types and call shapes. This PR is
intentionally the mechanical follow-through for the places where the
existing signatures stay in place.

After rebasing onto newer `main`, the rollout also had to cover newly
introduced `tui_app_server` call sites. That made it clear the first cut
of the CI job was too expensive for the common path: it was spending
almost as much time installing `cargo-dylint` and re-testing the lint
crate as a representative test job spends running product tests. The CI
update keeps the full workspace enforcement but trims that extra
overhead from ordinary `codex-rs` PRs.

## What changed

- keep a dedicated `argument_comment_lint` job in `rust-ci`
- mechanically annotate remaining opaque positional literals across
`codex-rs` with exact `/*param*/` comments, including the rebased
`tui_app_server` call sites that now fall under the lint
- keep the checked-in style aligned with the lint policy by using
`/*param*/` and leaving string and char literals uncommented
- cache `cargo-dylint`, `dylint-link`, and the relevant Cargo
registry/git metadata in the lint job
- split changed-path detection so the lint crate's own `cargo test` step
runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes
- continue to run the repo wrapper over the `codex-rs` workspace, so
product-code enforcement is unchanged

Most of the code changes in this commit are intentionally mechanical
comment rewrites or insertions driven by the lint itself.

## Verification

- `./tools/argument-comment-lint/run.sh --workspace`
- `cargo test -p codex-tui-app-server -p codex-tui`
- parsed `.github/workflows/rust-ci.yml` locally with PyYAML

---

* -> #14652
* #14651
2026-03-16 16:48:15 -07:00

293 lines
9.3 KiB
Rust

mod client;
mod commands;
mod output;
mod reader;
mod state;
use std::io;
use std::io::BufRead;
use std::sync::mpsc;
use anyhow::Context;
use anyhow::Result;
use clap::ArgAction;
use clap::Parser;
use codex_app_server_protocol::AskForApproval;
use crate::client::AppServerClient;
use crate::client::build_thread_resume_params;
use crate::client::build_thread_start_params;
use crate::commands::InputAction;
use crate::commands::UserCommand;
use crate::commands::parse_input;
use crate::output::Output;
use crate::state::ReaderEvent;
#[derive(Parser)]
#[command(author = "Codex", version, about = "Minimal app-server client")]
struct Cli {
/// Path to the `codex` CLI binary.
#[arg(long, default_value = "codex")]
codex_bin: String,
/// Forwarded to the `codex` CLI as `--config key=value`. Repeatable.
#[arg(short = 'c', long = "config", value_name = "key=value", action = ArgAction::Append)]
config_overrides: Vec<String>,
/// Resume an existing thread instead of starting a new one.
#[arg(long)]
thread_id: Option<String>,
/// Set the approval policy for the thread.
#[arg(long, default_value = "on-request")]
approval_policy: String,
/// Auto-approve command/file-change approvals.
#[arg(long, default_value_t = false)]
auto_approve: bool,
/// Only show final assistant messages and tool calls.
#[arg(long, default_value_t = false)]
final_only: bool,
/// Optional model override when starting/resuming a thread.
#[arg(long)]
model: Option<String>,
/// Optional model provider override when starting/resuming a thread.
#[arg(long)]
model_provider: Option<String>,
/// Optional working directory override when starting/resuming a thread.
#[arg(long)]
cwd: Option<String>,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let output = Output::new();
let approval_policy = parse_approval_policy(&cli.approval_policy)?;
let mut client = AppServerClient::spawn(
&cli.codex_bin,
&cli.config_overrides,
output.clone(),
cli.final_only,
)?;
client.initialize()?;
let thread_id = if let Some(thread_id) = cli.thread_id.as_ref() {
client.resume_thread(build_thread_resume_params(
thread_id.clone(),
approval_policy,
cli.model.clone(),
cli.model_provider.clone(),
cli.cwd.clone(),
))?
} else {
client.start_thread(build_thread_start_params(
approval_policy,
cli.model.clone(),
cli.model_provider.clone(),
cli.cwd.clone(),
))?
};
output
.client_line(&format!("connected to thread {thread_id}"))
.ok();
output.set_prompt(&thread_id);
let (event_tx, event_rx) = mpsc::channel();
client.start_reader(event_tx, cli.auto_approve, cli.final_only)?;
print_help(&output);
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
loop {
drain_events(&event_rx, &output);
let prompt_thread = client
.thread_id()
.unwrap_or_else(|| "no-thread".to_string());
output.prompt(&prompt_thread).ok();
let Some(line) = lines.next() else {
break;
};
let line = line.context("read stdin")?;
match parse_input(&line) {
Ok(None) => continue,
Ok(Some(InputAction::Message(message))) => {
let Some(active_thread) = client.thread_id() else {
output
.client_line("no active thread; use :new or :resume <id>")
.ok();
continue;
};
if let Err(err) = client.send_turn(&active_thread, message) {
output
.client_line(&format!("failed to send turn: {err}"))
.ok();
}
}
Ok(Some(InputAction::Command(command))) => {
if !handle_command(command, &client, &output, approval_policy, &cli) {
break;
}
}
Err(err) => {
output.client_line(&err.message()).ok();
}
}
}
client.shutdown();
Ok(())
}
fn handle_command(
command: UserCommand,
client: &AppServerClient,
output: &Output,
approval_policy: AskForApproval,
cli: &Cli,
) -> bool {
match command {
UserCommand::Help => {
print_help(output);
true
}
UserCommand::Quit => false,
UserCommand::NewThread => {
match client.request_thread_start(build_thread_start_params(
approval_policy,
cli.model.clone(),
cli.model_provider.clone(),
cli.cwd.clone(),
)) {
Ok(request_id) => {
output
.client_line(&format!("requested new thread ({request_id:?})"))
.ok();
}
Err(err) => {
output
.client_line(&format!("failed to start thread: {err}"))
.ok();
}
}
true
}
UserCommand::Resume(thread_id) => {
match client.request_thread_resume(build_thread_resume_params(
thread_id,
approval_policy,
cli.model.clone(),
cli.model_provider.clone(),
cli.cwd.clone(),
)) {
Ok(request_id) => {
output
.client_line(&format!("requested thread resume ({request_id:?})"))
.ok();
}
Err(err) => {
output
.client_line(&format!("failed to resume thread: {err}"))
.ok();
}
}
true
}
UserCommand::Use(thread_id) => {
let known = client.use_thread(thread_id.clone());
output.set_prompt(&thread_id);
if known {
output
.client_line(&format!("switched active thread to {thread_id}"))
.ok();
} else {
output
.client_line(&format!(
"switched active thread to {thread_id} (unknown; use :resume to load)"
))
.ok();
}
true
}
UserCommand::RefreshThread => {
match client.request_thread_list(/*cursor*/ None) {
Ok(request_id) => {
output
.client_line(&format!("requested thread list ({request_id:?})"))
.ok();
}
Err(err) => {
output
.client_line(&format!("failed to list threads: {err}"))
.ok();
}
}
true
}
}
}
fn parse_approval_policy(value: &str) -> Result<AskForApproval> {
match value {
"untrusted" | "unless-trusted" | "unlessTrusted" => Ok(AskForApproval::UnlessTrusted),
"on-failure" | "onFailure" => Ok(AskForApproval::OnFailure),
"on-request" | "onRequest" => Ok(AskForApproval::OnRequest),
"never" => Ok(AskForApproval::Never),
_ => anyhow::bail!(
"unknown approval policy: {value}. Expected one of: untrusted, on-failure, on-request, never"
),
}
}
fn drain_events(event_rx: &mpsc::Receiver<ReaderEvent>, output: &Output) {
while let Ok(event) = event_rx.try_recv() {
match event {
ReaderEvent::ThreadReady { thread_id } => {
output
.client_line(&format!("active thread is now {thread_id}"))
.ok();
output.set_prompt(&thread_id);
}
ReaderEvent::ThreadList {
thread_ids,
next_cursor,
} => {
if thread_ids.is_empty() {
output.client_line("threads: (none)").ok();
} else {
output.client_line("threads:").ok();
for thread_id in thread_ids {
output.client_line(&format!(" {thread_id}")).ok();
}
}
if let Some(next_cursor) = next_cursor {
output
.client_line(&format!(
"more threads available, next cursor: {next_cursor}"
))
.ok();
}
}
}
}
}
fn print_help(output: &Output) {
let _ = output.client_line("commands:");
let _ = output.client_line(" :help show this help");
let _ = output.client_line(" :new start a new thread");
let _ = output.client_line(" :resume <thread-id> resume an existing thread");
let _ = output.client_line(" :use <thread-id> switch the active thread");
let _ = output.client_line(" :refresh-thread list available threads");
let _ = output.client_line(" :quit exit");
let _ = output.client_line("type a message to send it as a new turn");
}