feat: add debug logging system and improve TUI conversation history

- Add comprehensive debug logging system with file output
- Implement ScrollView component for better scrolling behavior
- Enhance conversation history with proper text wrapping
- Add browser debugging capabilities
- Improve ephemeral context handling
- Fix message duplication issues
- Add test scripts for debugging

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
James Peter
2025-08-13 21:08:18 +10:00
co-authored by Hanzo Dev
parent 227890cfd7
commit 4c879abb35
28 changed files with 1362 additions and 169 deletions
+31
View File
@@ -14,3 +14,34 @@ Completion/build step
- Do not run additional format/lint/test commands on completion (e.g., `just fmt`, `just fix`, `cargo test`) unless explicitly requested for a specific task.
When making individual changes prefer running tests on individual files or projects first if asked, but otherwise rely on `./build-fast.sh` at the end.
## Command Execution Architecture
The command execution flow in Codex follows an event-driven pattern:
1. **Core Layer** (`codex-core/src/codex.rs`):
- `on_exec_command_begin()` initiates command execution
- Creates `EventMsg::ExecCommandBegin` events with command details
2. **TUI Layer** (`codex-tui/src/chatwidget.rs`):
- `handle_codex_event()` processes execution events
- Manages `RunningCommand` state for active commands
- Creates `HistoryCell::Exec` for UI rendering
3. **History Cell** (`codex-tui/src/history_cell.rs`):
- `new_active_exec_command()` - Creates cell for running command
- `new_completed_exec_command()` - Updates with final output
- Handles syntax highlighting via `ParsedCommand`
This architecture separates concerns between execution logic (core), UI state management (chatwidget), and rendering (history_cell).
## Ephemeral Context Injection
Codex automatically injects fresh context at the end of each model request:
- **Location**: Ephemeral items are appended as the last items in the request via `get_formatted_input()`
- **Content**: System status (cwd, git branch, reasoning level), browser info (URL, type), screenshots
- **Generation**: `build_turn_ephemeral_items()` creates fresh context for each retry attempt
- **Storage**: Stored in `Prompt.ephemeral_items` field, ensuring they're regenerated per-request
This design ensures the model always receives current environment context without complex refresh logic or state management.
@@ -59,7 +59,7 @@ impl BrowserInjector {
.unwrap_or_else(|_| "about:blank".to_string());
let system_hint = format!(
"A fresh screenshot of the active page ({}) is attached; use browser.* tools to navigate or capture more.",
"A fresh screenshot of the active page ({}) is attached; use browser_* tools to navigate or capture more.",
current_url
);
+30
View File
@@ -507,6 +507,18 @@ impl BrowserManager {
self.config.try_read().map(|c| c.enabled).unwrap_or(false)
}
/// Get a description of the browser connection type
pub async fn get_browser_type(&self) -> String {
let config = self.config.read().await;
if config.connect_ws.is_some() || config.connect_port.is_some() {
"CDP-connected to user's Chrome browser".to_string()
} else if config.headless {
"internal headless Chrome browser".to_string()
} else {
"internal Chrome browser (headed mode)".to_string()
}
}
pub async fn set_enabled(&self, enabled: bool) -> Result<()> {
let mut config = self.config.write().await;
config.enabled = enabled;
@@ -898,6 +910,24 @@ impl BrowserManager {
page.execute_javascript(code).await
}
/// Scroll the page by the given delta in pixels
pub async fn scroll_by(&self, dx: f64, dy: f64) -> Result<()> {
let page = self.get_or_create_page().await?;
page.scroll_by(dx, dy).await
}
/// Navigate browser history backward one entry
pub async fn history_back(&self) -> Result<()> {
let page = self.get_or_create_page().await?;
page.go_back().await
}
/// Navigate browser history forward one entry
pub async fn history_forward(&self) -> Result<()> {
let page = self.get_or_create_page().await?;
page.go_forward().await
}
/// Set a callback to be called when navigation occurs
pub async fn set_navigation_callback<F>(&self, callback: F)
where
+28 -2
View File
@@ -354,9 +354,11 @@ impl Page {
/// Type text into the currently focused element
pub async fn type_text(&self, text: &str) -> Result<()> {
debug!("Typing text: {}", text);
// Replace em dashes with regular dashes
let processed_text = text.replace('—', " - ");
debug!("Typing text: {}", processed_text);
for ch in text.chars() {
for ch in processed_text.chars() {
let params = DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::Char)
.text(ch.to_string())
@@ -544,6 +546,30 @@ impl Page {
Ok(result_value)
}
/// Scroll the page by the given delta in pixels
pub async fn scroll_by(&self, dx: f64, dy: f64) -> Result<()> {
debug!("Scrolling by ({}, {})", dx, dy);
let js = format!(
"(function() {{ window.scrollBy({dx}, {dy}); return {{ x: window.scrollX, y: window.scrollY }}; }})()"
);
let _ = self.execute_javascript(&js).await?;
Ok(())
}
/// Navigate browser history backward one entry
pub async fn go_back(&self) -> Result<()> {
debug!("History back");
let _ = self.execute_javascript("history.back();").await?;
Ok(())
}
/// Navigate browser history forward one entry
pub async fn go_forward(&self) -> Result<()> {
debug!("History forward");
let _ = self.execute_javascript("history.forward();").await?;
Ok(())
}
}
#[derive(Debug, Clone)]
+95
View File
@@ -0,0 +1,95 @@
In this environment, you are running as `coder` and your name is Coder. Coder is a fork Codex CLI, an open source project led by OpenAI.
Coder is a fast, community-driven fork focused on key developer ergonomics: Browser control, multi-agent flows, live theming, and on-the-fly reasoning control - all while staying compatible with upstream.
# Testing
With your additional browser tools you can validate web UIs visually. For code that generates a web interface, test with browser tools after changes and use visual judgment to improve UX and aesthetics.
# Tools
## Shell tools (via shell)
You have access to CLI tools through the shell function. Use it for any command-line work (e.g., git, builds, tests, codegen). apply_patch is one of these CLI helpers and must be invoked via shell to edit files safely and atomically.
{"command":["git","status"]}
{"command":["rg","-n","--glob","**/package.json","^\\s*\\\"(name|scripts)\\\""],"workdir":"./repo"}
{"command":["fd","-H","-I","-t","f"],"workdir":"./src","timeout":10000}
{"command":["sh","-lc","git log --since='14 days ago' --stat"]}
{"command":["apply_patch","*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n"]}
Editing files with apply_patch (via shell):
Use apply_patch for all file modifications; it validates the patch envelope and applies changes predictably.
## `apply_patch` (via shell)
apply_patch consumes a simple, file-oriented diff wrapped in a clear envelope:
### Formal shape
Patch := Begin { FileOp } End
Begin := "*** Begin Patch" NEWLINE
End := "*** End Patch" NEWLINE
FileOp := AddFile | DeleteFile | UpdateFile
AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE }
DeleteFile := "*** Delete File: " path NEWLINE
UpdateFile := "*** Update File: " path NEWLINE [ "*** Move to: " newPath NEWLINE ] { Hunk }
Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ]
HunkLine := (" " | "-" | "+") text NEWLINE
## Browser tools
Use the browser tools to open a live page, interact with it, and harvest results. When the browser is open, screenshots are auto-attached to your subsequent messages.
The browser will either be an internal headless browser, or a CPD connection to the user's active Chrome browser. Your screenshots will be 1024×768 which exactly matches the viewport.
## Agent tools
Your agents are like having a team of expert peers at your disposal at any time. Use them for non-trivial work in these types of situations;
### Solve
If you get stuck on a problem and your first attempt has failed, race your agents to find the solution in read_only mode. Research shows that the fastest agent is frequently the most accurate.
Example;
run_agent {
"task": "Find and fix flaky test `test_refund_flow` in services/payments. Produce: (1) root cause, (2) minimal unified diff, (3) commands to reproduce and verify. Do not change business logic.",
"context": "Monorepo; CI fails intermittently on macOS since a1b2c3. Stripe sandbox used in tests.",
"files": ["services/payments", "services/payments/tests", "ci/config.yml"],
"model": ["claude","gemini","codex"],
"output": "Root cause + minimal patch + reproducible verification",
"read_only": true
}
wait_for_agent {"batch_id":"<batch_id>","return_all":false} // will return as soon as first agent is complete
get_agent_result {"agent_id":"<first_agent_id>"}
// Repeat and create new agents if needed until problem is resolved
cancel_agent {"batch_id":"<batch>"} // stop running agents ONLY once the task is tested and confirmed solved. Even if you think you have solved the problem it may be good to check the output of other agents
### Plan
When starting a complicated task without an obvious direction, you can find multiple perspectives then merge into one plan with rationale.
Example;
run_agent {
"task": "Draft a phased rollout plan for feature flags across apps/web and services. Include migration steps, rollback, monitoring, ownership, and two safe defaults.",
"context": "apps/web (Next.js), services/api (Rust Axum). Existing env flags in `config/flags`. Goal: progressive delivery.",
"files": ["apps/web/package.json", "services/api/Cargo.toml", "config/flags"],
"model": ["claude","gemini","codex"],
"output": "Numbered phases + risk matrix + observability plan",
"read_only": true
}
wait_for_agent { "batch_id": "<batch_id>", "return_all": true } // wait for all
### Code
Implement multiple tasks at once, or see how multiple other peers would build a solution.
Example;
run_agent {
"task": "Implement JWT middleware (RS256) with key rotation and unit/integration tests. Preserve existing OAuth flows. Provide README usage snippet.",
"context": "Service: services/api (Rust Axum). Secrets via env. CI: `cargo test --all`.",
"files": ["services/api", "services/api/src", "services/api/Cargo.toml"],
"model": ["claude","gemini","codex"],
"output": "Middleware + passing tests + README snippet",
"read_only": false // Allow changes - will launch every agent in a separate worktree
}
wait_for_agent {"batch_id":"<batch_id>","return_all":true,"timeout_seconds": 600 } // Long timeout or you can do separate work and check back later.
+5 -5
View File
@@ -629,9 +629,9 @@ pub fn create_run_agent_tool() -> OpenAiTool {
let mut properties = BTreeMap::new();
properties.insert(
"agent".to_string(),
"task".to_string(),
JsonSchema::String {
description: Some("The agent prompt - what to perform (required)".to_string()),
description: Some("The task prompt - what to perform (required)".to_string()),
},
);
@@ -680,11 +680,11 @@ pub fn create_run_agent_tool() -> OpenAiTool {
OpenAiTool::Function(ResponsesApiTool {
name: "run_agent".to_string(),
description: "Start a complex AI agent asynchronously. Returns a agent ID immediately to check status and retrieve results.".to_string(),
description: "Start a complex AI task asynchronously. Returns a agent ID immediately to check status and retrieve results.".to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["agent".to_string()]),
required: Some(vec!["task".to_string()]),
additional_properties: Some(false),
},
})
@@ -861,7 +861,7 @@ pub fn create_list_agents_tool() -> OpenAiTool {
// Parameter structs for handlers
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunAgentParams {
pub agent: String,
pub task: String,
pub model: Option<serde_json::Value>, // Can be string or array
pub context: Option<String>,
pub output: Option<String>,
+29 -3
View File
@@ -19,6 +19,7 @@ use crate::ModelProviderInfo;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::client_common::ResponseStream;
use crate::debug_logger::DebugLogger;
use crate::error::CodexErr;
use crate::error::Result;
use crate::model_family::ModelFamily;
@@ -27,6 +28,7 @@ use crate::models::ReasoningItemContent;
use crate::models::ResponseItem;
use crate::openai_tools::create_tools_json_for_chat_completions_api;
use crate::util::backoff;
use std::sync::{Arc, Mutex};
/// Implementation for the classic Chat Completions API.
pub(crate) async fn stream_chat_completions(
@@ -34,6 +36,7 @@ pub(crate) async fn stream_chat_completions(
model_family: &ModelFamily,
client: &reqwest::Client,
provider: &ModelProviderInfo,
debug_logger: &Arc<Mutex<DebugLogger>>,
) -> Result<ResponseStream> {
// Build messages array
let mut messages = Vec::<serde_json::Value>::new();
@@ -58,10 +61,11 @@ pub(crate) async fn stream_chat_completions(
ContentItem::InputText { text } | ContentItem::OutputText { text } => {
parts.push(json!({ "type": "text", "text": text }));
}
ContentItem::InputImage { image_url } => {
ContentItem::InputImage { image_url, detail } => {
parts.push(json!({
"type": "image_url",
"image_url": { "url": image_url }
"image_url": { "url": image_url },
"detail": detail.clone().unwrap_or_else(|| "auto".to_string())
}));
}
}
@@ -142,12 +146,18 @@ pub(crate) async fn stream_chat_completions(
"tools": tools_json,
});
let endpoint = provider.get_full_url(&None);
debug!(
"POST to {}: {}",
provider.get_full_url(&None),
endpoint,
serde_json::to_string_pretty(&payload).unwrap_or_default()
);
// Log the request if debug is enabled
if let Ok(logger) = debug_logger.lock() {
let _ = logger.log_request(&endpoint, &payload);
}
let mut attempt = 0;
let max_retries = provider.request_max_retries();
loop {
@@ -163,6 +173,14 @@ pub(crate) async fn stream_chat_completions(
match res {
Ok(resp) if resp.status().is_success() => {
// Log successful response initiation (streaming)
if let Ok(logger) = debug_logger.lock() {
let _ = logger.log_response(&endpoint, &serde_json::json!({
"status": "success",
"type": "stream_initiated",
"status_code": resp.status().as_u16()
}));
}
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent>>(1600);
let stream = resp.bytes_stream().map_err(CodexErr::Reqwest);
tokio::spawn(process_chat_sse(
@@ -176,6 +194,10 @@ pub(crate) async fn stream_chat_completions(
let status = res.status();
if !(status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()) {
let body = (res.text().await).unwrap_or_default();
// Log error response
if let Ok(logger) = debug_logger.lock() {
let _ = logger.log_error(&endpoint, &format!("Status: {}, Body: {}", status, body));
}
return Err(CodexErr::UnexpectedStatus(status, body));
}
@@ -196,6 +218,10 @@ pub(crate) async fn stream_chat_completions(
}
Err(e) => {
if attempt > max_retries {
// Log network error
if let Ok(logger) = debug_logger.lock() {
let _ = logger.log_error(&endpoint, &format!("Network error: {}", e));
}
return Err(e.into());
}
let delay = backoff(attempt);
+39 -2
View File
@@ -40,6 +40,8 @@ use crate::openai_tools::create_tools_json_for_responses_api;
use crate::protocol::TokenUsage;
use crate::util::backoff;
use std::sync::Arc;
use std::sync::Mutex;
use crate::debug_logger::DebugLogger;
#[derive(Debug, Deserialize)]
struct ErrorResponse {
@@ -60,6 +62,7 @@ pub struct ModelClient {
session_id: Uuid,
effort: ReasoningEffortConfig,
summary: ReasoningSummaryConfig,
debug_logger: Arc<Mutex<DebugLogger>>,
}
impl ModelClient {
@@ -70,6 +73,7 @@ impl ModelClient {
effort: ReasoningEffortConfig,
summary: ReasoningSummaryConfig,
session_id: Uuid,
debug_logger: Arc<Mutex<DebugLogger>>,
) -> Self {
Self {
config,
@@ -79,9 +83,15 @@ impl ModelClient {
session_id,
effort,
summary,
debug_logger,
}
}
/// Get the reasoning effort configuration
pub fn get_reasoning_effort(&self) -> ReasoningEffortConfig {
self.effort
}
/// Dispatches to either the Responses or Chat implementation depending on
/// the provider config. Public callers always invoke `stream()` – the
/// specialised helpers are private to avoid accidental misuse.
@@ -95,6 +105,7 @@ impl ModelClient {
&self.config.model_family,
&self.client,
&self.provider,
&self.debug_logger,
)
.await?;
@@ -164,7 +175,7 @@ impl ModelClient {
input: &input_with_instructions,
tools: &tools_json,
tool_choice: "auto",
parallel_tool_calls: false,
parallel_tool_calls: true,
reasoning,
store,
stream: true,
@@ -175,12 +186,18 @@ impl ModelClient {
let mut attempt = 0;
let max_retries = self.provider.request_max_retries();
let endpoint = self.provider.get_full_url(&auth);
trace!(
"POST to {}: {}",
self.provider.get_full_url(&auth),
endpoint,
serde_json::to_string(&payload)?
);
// Log the request if debug is enabled
if let Ok(logger) = self.debug_logger.lock() {
let _ = logger.log_request(&endpoint, &serde_json::to_value(&payload)?);
}
loop {
attempt += 1;
@@ -225,6 +242,18 @@ impl ModelClient {
match res {
Ok(resp) if resp.status().is_success() => {
// Log successful response initiation (streaming)
if let Ok(logger) = self.debug_logger.lock() {
let _ = logger.log_response(&endpoint, &serde_json::json!({
"status": "success",
"type": "stream_initiated",
"status_code": resp.status().as_u16(),
"request_id": resp.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
}));
}
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent>>(1600);
// spawn task to process SSE
@@ -257,6 +286,10 @@ impl ModelClient {
if !(status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()) {
// Surface the error body to callers. Use `unwrap_or_default` per Clippy.
let body = res.text().await.unwrap_or_default();
// Log error response
if let Ok(logger) = self.debug_logger.lock() {
let _ = logger.log_error(&endpoint, &format!("Status: {}, Body: {}", status, body));
}
return Err(CodexErr::UnexpectedStatus(status, body));
}
@@ -291,6 +324,10 @@ impl ModelClient {
}
Err(e) => {
if attempt > max_retries {
// Log network error
if let Ok(logger) = self.debug_logger.lock() {
let _ = logger.log_error(&endpoint, &format!("Network error: {}", e));
}
return Err(e.into());
}
let delay = backoff(attempt);
+15 -1
View File
@@ -23,6 +23,9 @@ use tokio::sync::mpsc;
/// with this content.
const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md");
/// Additional prompt for Coder. Can not edit Codex instructions.
const ADDITIONAL_INSTRUCTIONS: &str = include_str!("../prompt_coder.md");
/// wraps environment context message in a tag for the model to parse more easily.
const ENVIRONMENT_CONTEXT_START: &str = "<environment_context>\n\n";
const ENVIRONMENT_CONTEXT_END: &str = "\n\n</environment_context>";
@@ -83,6 +86,10 @@ pub struct Prompt {
/// external MCP servers.
pub tools: Vec<OpenAiTool>,
/// Ephemeral items to be added at the end of the input
/// These are generated fresh for each request
pub ephemeral_items: Vec<ResponseItem>,
/// Optional override for the built-in BASE_INSTRUCTIONS.
pub base_instructions_override: Option<String>,
}
@@ -113,7 +120,12 @@ impl Prompt {
}
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
let mut input_with_instructions = Vec::with_capacity(self.input.len() + 2);
let mut input_with_instructions = Vec::with_capacity(self.input.len() + self.ephemeral_items.len() + 3);
input_with_instructions.push(ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText { text: ADDITIONAL_INSTRUCTIONS.to_string() }],
});
if let Some(ec) = self.get_formatted_environment_context() {
input_with_instructions.push(ResponseItem::Message {
id: None,
@@ -129,6 +141,8 @@ impl Prompt {
});
}
input_with_instructions.extend(self.input.clone());
// Add ephemeral items at the end so they're fresh for each request
input_with_instructions.extend(self.ephemeral_items.clone());
input_with_instructions
}
}
+304 -103
View File
@@ -65,6 +65,7 @@ async fn build_turn_ephemeral_items(sess: &Session) -> Vec<ResponseItem> {
// Collect environment context
let cwd = sess.cwd.to_string_lossy().to_string();
let branch = get_git_branch(&sess.cwd).unwrap_or_else(|| "unknown".to_string());
let reasoning_effort = sess.client.get_reasoning_effort();
// Prepare browser context + optional screenshot
let mut browser_text = None::<String>;
@@ -72,45 +73,70 @@ async fn build_turn_ephemeral_items(sess: &Session) -> Vec<ResponseItem> {
if let Some(browser_manager) = codex_browser::global::get_browser_manager().await {
if browser_manager.is_enabled().await {
// Capture screenshot and discover URL
if let Some((screenshot_path, url)) = capture_browser_screenshot(sess).await {
if let Ok(bytes) = std::fs::read(&screenshot_path) {
let mime = mime_guess::from_path(&screenshot_path)
.first()
.map(|m| m.to_string())
.unwrap_or_else(|| "image/png".to_string());
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
screenshot_content = Some(ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
});
// Get current URL and browser info
let url = browser_manager
.get_current_url()
.await
.unwrap_or_else(|| "unknown".to_string());
// Try to get a tab title if available
let title = match browser_manager.get_or_create_page().await {
Ok(page) => page.get_title().await,
Err(_) => None,
};
// Get browser type description
let browser_type = browser_manager.get_browser_type().await;
// Try to capture screenshot
let screenshot_status = match capture_browser_screenshot(sess).await {
Ok((screenshot_path, _url)) => {
if let Ok(bytes) = std::fs::read(&screenshot_path) {
let mime = mime_guess::from_path(&screenshot_path)
.first()
.map(|m| m.to_string())
.unwrap_or_else(|| "image/png".to_string());
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
screenshot_content = Some(ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
detail: Some("high".to_string()),
});
""
} else {
" [Screenshot file read failed]"
}
}
Err(err_msg) => {
// Include error message so LLM knows screenshot failed
format!(" [Screenshot unavailable: {}]", err_msg).leak()
}
};
// Try to get a tab title if available
let title = match browser_manager.get_or_create_page().await {
Ok(page) => page.get_title().await,
Err(_) => None,
};
let status_line = if let Some(t) = title {
format!(
"Browser is open at {} — {}. You can interact with it using browser.* tools.",
url, t
)
} else {
format!(
"Browser is open at {}. You can interact with it using browser.* tools.",
url
)
};
browser_text = Some(status_line);
}
let status_line = if let Some(t) = title {
format!(
"Browser is open at {} — {} ({}){}. You can interact with it using browser_* tools.",
url, t, browser_type, screenshot_status
)
} else {
format!(
"Browser is open at {} ({}){}. You can interact with it using browser_* tools.",
url, browser_type, screenshot_status
)
};
browser_text = Some(status_line);
}
}
// Build a single ephemeral message conveying env + browser context + screenshot
let mut content: Vec<ContentItem> = Vec::new();
content.push(ContentItem::InputText {
text: format!("[EPHEMERAL:turn_context] cwd: {}, branch: {}", cwd, branch),
text: format!(r#"== System Status ==
This ephemeral message is added to the current turn to provide context about your environment. You only see the current version of it.
Use this message only as context and work from the previous user or assistant message.
cwd: {}
branch: {}
reasoning: {:?}
"#, cwd, branch, reasoning_effort),
});
if let Some(text) = browser_text {
content.push(ContentItem::InputText { text });
@@ -122,7 +148,6 @@ async fn build_turn_ephemeral_items(sess: &Session) -> Vec<ResponseItem> {
if !content.is_empty() {
jar.items.push(ResponseItem::Message {
id: None,
// Use "user" for broad compatibility across providers.
role: "user".to_string(),
content,
});
@@ -1047,6 +1072,18 @@ async fn submission_loop(
}
};
// Create debug logger based on config
let debug_logger = match crate::debug_logger::DebugLogger::new(config.debug) {
Ok(logger) => std::sync::Arc::new(std::sync::Mutex::new(logger)),
Err(e) => {
warn!("Failed to create debug logger: {}", e);
// Create a disabled logger as fallback
std::sync::Arc::new(std::sync::Mutex::new(
crate::debug_logger::DebugLogger::new(false).unwrap()
))
}
};
let client = ModelClient::new(
config.clone(),
auth.clone(),
@@ -1054,6 +1091,7 @@ async fn submission_loop(
model_reasoning_effort,
model_reasoning_summary,
session_id,
debug_logger,
);
// abort any current running session and clone its state
@@ -1364,7 +1402,7 @@ async fn run_agent(sess: Arc<Session>, sub_id: String, input: Vec<InputItem>) {
// Note that pending_input would be something like a message the user
// submitted through the UI while the model was running. Though the UI
// may support this, the model might not.
let mut pending_input = sess
let pending_input = sess
.get_pending_input()
.into_iter()
.map(ResponseItem::from)
@@ -1379,10 +1417,6 @@ async fn run_agent(sess: Arc<Session>, sub_id: String, input: Vec<InputItem>) {
sess.record_conversation_items(&pending_input).await;
}
// Append ephemeral per-turn context (env + browser status + optional screenshot)
let ephemeral_items = build_turn_ephemeral_items(&sess).await;
pending_input.extend(ephemeral_items);
// Construct the input that we will send to the model. When using the
// Chat completions API (or ZDR clients), the model needs the full
// conversation history on each turn. The rollout file, however, should
@@ -1491,8 +1525,26 @@ async fn run_agent(sess: Arc<Session>, sub_id: String, input: Vec<InputItem>) {
// Only attempt to take the lock if there is something to record.
if !items_to_record_in_conversation_history.is_empty() {
sess.record_conversation_items(&items_to_record_in_conversation_history)
.await;
// To ensure streamed assistant deltas (already appended into
// history during OutputTextDelta) merge cleanly with the final
// assistant message, record assistant messages first. This
// preserves merge-adjacency even if the provider delivered
// other output items (e.g., reasoning) before the final
// assistant message.
let mut assistants = Vec::new();
let mut others = Vec::new();
for item in items_to_record_in_conversation_history.drain(..) {
match &item {
ResponseItem::Message { role, .. } if role == "assistant" => {
assistants.push(item)
}
_ => others.push(item),
}
}
// Maintain relative order within each group while ensuring
// assistants are recorded first for proper merge behaviour.
assistants.extend(others.into_iter());
sess.record_conversation_items(&assistants).await;
}
// If there are responses, add them to pending input for the next iteration
@@ -1550,21 +1602,25 @@ async fn run_turn(
Some(sess.mcp_connection_manager.list_all_tools()),
);
let prompt = Prompt {
input,
user_instructions: sess.user_instructions.clone(),
store: !sess.disable_response_storage,
tools,
base_instructions_override: sess.base_instructions.clone(),
environment_context: Some(EnvironmentContext {
cwd: sess.cwd.clone(),
approval_policy: sess.approval_policy,
sandbox_policy: sess.sandbox_policy.clone(),
}),
};
let mut retries = 0;
loop {
// Build ephemeral items fresh for each attempt
let ephemeral_items = build_turn_ephemeral_items(sess).await;
let prompt = Prompt {
input: input.clone(),
user_instructions: sess.user_instructions.clone(),
store: !sess.disable_response_storage,
tools: tools.clone(),
base_instructions_override: sess.base_instructions.clone(),
environment_context: Some(EnvironmentContext {
cwd: sess.cwd.clone(),
approval_policy: sess.approval_policy,
sandbox_policy: sess.sandbox_policy.clone(),
}),
ephemeral_items,
};
match try_run_turn(sess, turn_diff_tracker, &sub_id, &prompt).await {
Ok(output) => return Ok(output),
Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted),
@@ -1785,19 +1841,22 @@ async fn run_compact_agent(
let turn_input: Vec<ResponseItem> =
sess.turn_input_with_history(vec![initial_input_for_turn.clone().into()]);
let prompt = Prompt {
input: turn_input,
user_instructions: None,
store: !sess.disable_response_storage,
environment_context: None,
tools: Vec::new(),
base_instructions_override: Some(compact_instructions.clone()),
};
let max_retries = sess.client.get_provider().stream_max_retries();
let mut retries = 0;
loop {
// Build ephemeral items fresh for each attempt
let ephemeral_items = build_turn_ephemeral_items(&sess).await;
let prompt = Prompt {
input: turn_input.clone(),
user_instructions: None,
store: !sess.disable_response_storage,
environment_context: None,
tools: Vec::new(),
base_instructions_override: Some(compact_instructions.clone()),
ephemeral_items,
};
let attempt_result = drain_to_completed(&sess, &sub_id, &prompt).await;
match attempt_result {
@@ -2006,6 +2065,8 @@ async fn handle_function_call(
"browser_type" => handle_browser_type(sess, arguments, sub_id, call_id).await,
"browser_key" => handle_browser_key(sess, arguments, sub_id, call_id).await,
"browser_javascript" => handle_browser_javascript(sess, arguments, sub_id, call_id).await,
"browser_scroll" => handle_browser_scroll(sess, arguments, sub_id, call_id).await,
"browser_history" => handle_browser_history(sess, arguments, sub_id, call_id).await,
_ => {
match sess.mcp_connection_manager.parse_tool_name(&name) {
Some((server, tool_name)) => {
@@ -2130,7 +2191,7 @@ async fn handle_run_agent(
let agent_id = manager
.create_agent_with_config(
model,
params.agent.clone(),
params.task.clone(),
params.context.clone(),
params.output.clone(),
params.files.clone().unwrap_or_default(),
@@ -2145,7 +2206,7 @@ async fn handle_run_agent(
let agent_id = manager
.create_agent(
model,
params.agent.clone(),
params.task.clone(),
params.context.clone(),
params.output.clone(),
params.files.clone().unwrap_or_default(),
@@ -3033,41 +3094,42 @@ async fn drain_to_completed(sess: &Session, sub_id: &str, prompt: &Prompt) -> Co
}
/// Capture a screenshot from the browser and store it for the next model request
async fn capture_browser_screenshot(_sess: &Session) -> Option<(PathBuf, String)> {
let browser_manager = codex_browser::global::get_browser_manager().await;
if let Some(browser_manager) = browser_manager {
if browser_manager.is_enabled().await {
// Get current URL first
let url = browser_manager
.get_current_url()
.await
.unwrap_or_else(|| "Browser".to_string());
tracing::debug!("Attempting to capture screenshot at URL: {}", url);
async fn capture_browser_screenshot(_sess: &Session) -> Result<(PathBuf, String), String> {
let browser_manager = codex_browser::global::get_browser_manager().await
.ok_or_else(|| "No browser manager available".to_string())?;
if !browser_manager.is_enabled().await {
return Err("Browser manager is not enabled".to_string());
}
match browser_manager.capture_screenshot().await {
Ok(screenshots) => {
if let Some(first_screenshot) = screenshots.first() {
tracing::info!(
"Captured browser screenshot: {} at URL: {}",
first_screenshot.display(),
url
);
return Some((first_screenshot.clone(), url));
} else {
tracing::warn!("Screenshot capture returned empty results at URL: {}", url);
}
}
Err(e) => {
tracing::warn!("Failed to capture browser screenshot at URL {}: {}", url, e);
}
// Get current URL first
let url = browser_manager
.get_current_url()
.await
.unwrap_or_else(|| "Browser".to_string());
tracing::debug!("Attempting to capture screenshot at URL: {}", url);
match browser_manager.capture_screenshot().await {
Ok(screenshots) => {
if let Some(first_screenshot) = screenshots.first() {
tracing::info!(
"Captured browser screenshot: {} at URL: {}",
first_screenshot.display(),
url
);
Ok((first_screenshot.clone(), url))
} else {
let msg = format!("Screenshot capture returned empty results at URL: {}", url);
tracing::warn!("{}", msg);
Err(msg)
}
} else {
tracing::debug!("Browser manager is not enabled, skipping screenshot capture");
}
Err(e) => {
let msg = format!("Failed to capture screenshot at {}: {}", url, e);
tracing::warn!("{}", msg);
Err(msg)
}
} else {
tracing::debug!("No browser manager available for screenshot capture");
}
None
}
/// Send agent status update event to the TUI
@@ -3167,6 +3229,7 @@ fn consume_pending_screenshots(sess: &Session) -> Vec<ResponseInputItem> {
ContentItem::InputText { text: metadata },
ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
detail: Some("high".to_string()),
},
],
}
@@ -3222,15 +3285,18 @@ async fn handle_browser_open(
Ok(_) => {
tracing::info!("Browser navigation to {} completed successfully", url);
// Capture screenshot after navigation
if let Some((screenshot_path, updated_url)) =
capture_browser_screenshot(sess).await
{
tracing::info!(
"Screenshot captured after navigation: {} at URL: {}",
screenshot_path.display(),
updated_url
);
add_pending_screenshot(sess, screenshot_path, updated_url);
match capture_browser_screenshot(sess).await {
Ok((screenshot_path, updated_url)) => {
tracing::info!(
"Screenshot captured after navigation: {} at URL: {}",
screenshot_path.display(),
updated_url
);
add_pending_screenshot(sess, screenshot_path, updated_url);
}
Err(e) => {
tracing::warn!("Screenshot capture failed after navigation: {}", e);
}
} else {
tracing::warn!(
"Failed to capture screenshot after navigation to {}",
@@ -3642,3 +3708,138 @@ async fn handle_browser_javascript(
}
}
}
async fn handle_browser_scroll(
sess: &Session,
arguments: String,
_sub_id: String,
call_id: String,
) -> ResponseInputItem {
let browser_manager = get_browser_manager_for_session(sess).await;
if let Some(browser_manager) = browser_manager {
let args: Result<Value, _> = serde_json::from_str(&arguments);
match args {
Ok(json) => {
let dx = json.get("dx").and_then(|v| v.as_f64()).unwrap_or(0.0);
let dy = json.get("dy").and_then(|v| v.as_f64()).unwrap_or(0.0);
match browser_manager.scroll_by(dx, dy).await {
Ok(_) => {
if let Some((screenshot_path, url)) = capture_browser_screenshot(sess).await
{
add_pending_screenshot(sess, screenshot_path, url);
}
ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("Scrolled by ({}, {})", dx, dy),
success: Some(true),
},
}
}
Err(e) => ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("Failed to scroll: {}", e),
success: Some(false),
},
},
}
}
Err(e) => ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("Failed to parse browser_scroll arguments: {}", e),
success: Some(false),
},
},
}
} else {
ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: "Browser is not initialized. Use browser_open to start the browser.".to_string(),
success: Some(false),
},
}
}
}
async fn handle_browser_history(
sess: &Session,
arguments: String,
_sub_id: String,
call_id: String,
) -> ResponseInputItem {
let browser_manager = get_browser_manager_for_session(sess).await;
if let Some(browser_manager) = browser_manager {
let args: Result<Value, _> = serde_json::from_str(&arguments);
match args {
Ok(json) => {
let direction = json
.get("direction")
.and_then(|v| v.as_str())
.unwrap_or("");
if direction != "back" && direction != "forward" {
return ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!(
"Unsupported direction: {} (expected 'back' or 'forward')",
direction
),
success: Some(false),
},
};
}
let action_res = if direction == "back" {
browser_manager.history_back().await
} else {
browser_manager.history_forward().await
};
match action_res {
Ok(_) => {
if let Some((screenshot_path, url)) = capture_browser_screenshot(sess).await
{
add_pending_screenshot(sess, screenshot_path, url);
}
ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("History {} triggered", direction),
success: Some(true),
},
}
}
Err(e) => ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("Failed to navigate history: {}", e),
success: Some(false),
},
},
}
}
Err(e) => ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("Failed to parse browser_history arguments: {}", e),
success: Some(false),
},
},
}
} else {
ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: "Browser is not initialized. Use browser_open to start the browser.".to_string(),
success: Some(false),
},
}
}
}
+6
View File
@@ -163,6 +163,9 @@ pub struct Config {
/// The value for the `originator` header included with Responses API requests.
pub internal_originator: Option<String>,
/// Enable debug logging of LLM requests and responses
pub debug: bool,
}
impl Config {
@@ -494,6 +497,7 @@ pub struct ConfigOverrides {
pub include_plan_tool: Option<bool>,
pub disable_response_storage: Option<bool>,
pub show_raw_agent_reasoning: Option<bool>,
pub debug: Option<bool>,
}
impl Config {
@@ -519,6 +523,7 @@ impl Config {
include_plan_tool,
disable_response_storage,
show_raw_agent_reasoning,
debug,
} = overrides;
let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
@@ -673,6 +678,7 @@ impl Config {
experimental_resume,
include_plan_tool: include_plan_tool.unwrap_or(false),
internal_originator: cfg.internal_originator,
debug: debug.unwrap_or(false),
};
Ok(config)
}
+81 -18
View File
@@ -28,24 +28,45 @@ impl ConversationHistory {
continue;
}
// Merge adjacent assistant messages into a single history entry.
// This prevents duplicates when a partial assistant message was
// streamed into history earlier in the turn and the final full
// message is recorded at turn end.
match (&*item, self.items.last_mut()) {
(
ResponseItem::Message {
role: new_role,
content: new_content,
..
},
Some(ResponseItem::Message {
role: last_role,
content: last_content,
..
}),
) if new_role == "assistant" && last_role == "assistant" => {
append_text_content(last_content, new_content);
// De-duplicate assistant messages more robustly by leveraging IDs
// when available and falling back to adjacency/prefix merging.
match &*item {
ResponseItem::Message {
id: new_id,
role: new_role,
content: new_content,
} if new_role == "assistant" => {
// 1) If this assistant message has an ID, try to find an existing
// assistant entry with the same ID anywhere in history and
// replace its text with the new content.
if let Some(id) = new_id.as_ref() {
if let Some(idx) = find_assistant_index_by_id(&self.items, id) {
if let ResponseItem::Message { id: existing_id, content, .. } = &mut self.items[idx] {
// Ensure the existing entry carries the id
if existing_id.is_none() {
*existing_id = Some(id.clone());
}
replace_or_append_text(content, new_content);
continue;
}
}
}
// 2) Otherwise, attempt to merge with the immediately previous
// assistant message (streamed deltas case).
if let Some(ResponseItem::Message { id: last_id, role: last_role, content: last_content }) = self.items.last_mut() {
if last_role == "assistant" {
// If new item has an ID, propagate it to the existing entry
if last_id.is_none() {
if let Some(id) = new_id.clone() { *last_id = Some(id); }
}
replace_or_append_text(last_content, new_content);
continue;
}
}
// 3) No merge opportunity – push as a new assistant message.
self.items.push(item.clone());
}
_ => {
self.items.push(item.clone());
@@ -144,6 +165,48 @@ fn append_text_delta(content: &mut Vec<crate::models::ContentItem>, delta: &str)
}
}
/// Concatenate all OutputText segments into a single string.
fn collect_text_content(content: &Vec<crate::models::ContentItem>) -> String {
let mut out = String::new();
for c in content {
if let crate::models::ContentItem::OutputText { text } = c {
out.push_str(text);
}
}
out
}
/// Replace the text content with a single OutputText item containing `text`.
fn replace_text_content(content: &mut Vec<crate::models::ContentItem>, text: &str) {
content.clear();
content.push(crate::models::ContentItem::OutputText {
text: text.to_string(),
});
}
/// Merge strategy: if the new text starts with the old, replace; otherwise append deltas.
fn replace_or_append_text(existing: &mut Vec<crate::models::ContentItem>, new_content: &Vec<crate::models::ContentItem>) {
let prev = collect_text_content(existing);
let new_full = collect_text_content(new_content);
if !prev.is_empty() && new_full.starts_with(&prev) {
replace_text_content(existing, &new_full);
} else {
append_text_content(existing, new_content);
}
}
fn find_assistant_index_by_id(items: &Vec<ResponseItem>, id: &str) -> Option<usize> {
// Search from the end (most recent first) for better performance in practice.
for (idx, it) in items.iter().enumerate().rev() {
if let ResponseItem::Message { id: Some(existing), role, .. } = it {
if role == "assistant" && existing == id {
return Some(idx);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
+153
View File
@@ -0,0 +1,153 @@
use std::fs;
use std::path::PathBuf;
use chrono::Local;
use serde_json::Value;
pub struct DebugLogger {
enabled: bool,
log_dir: PathBuf,
}
impl DebugLogger {
pub fn new(enabled: bool) -> Result<Self, std::io::Error> {
if !enabled {
return Ok(Self {
enabled: false,
log_dir: PathBuf::new(),
});
}
let log_dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".codex")
.join("debug_logs");
fs::create_dir_all(&log_dir)?;
Ok(Self {
enabled,
log_dir,
})
}
pub fn log_request(&self, endpoint: &str, payload: &Value) -> Result<(), std::io::Error> {
if !self.enabled {
return Ok(());
}
let timestamp = Local::now();
let filename = format!(
"{}_request_{}.json",
timestamp.format("%Y%m%d_%H%M%S%.3f"),
sanitize_endpoint(endpoint)
);
let file_path = self.log_dir.join(filename);
let log_entry = serde_json::json!({
"timestamp": timestamp.to_rfc3339(),
"type": "request",
"endpoint": endpoint,
"payload": payload
});
let formatted = serde_json::to_string_pretty(&log_entry)?;
fs::write(file_path, formatted)?;
Ok(())
}
pub fn log_response(&self, endpoint: &str, response: &Value) -> Result<(), std::io::Error> {
if !self.enabled {
return Ok(());
}
let timestamp = Local::now();
let filename = format!(
"{}_response_{}.json",
timestamp.format("%Y%m%d_%H%M%S%.3f"),
sanitize_endpoint(endpoint)
);
let file_path = self.log_dir.join(filename);
let log_entry = serde_json::json!({
"timestamp": timestamp.to_rfc3339(),
"type": "response",
"endpoint": endpoint,
"response": response
});
let formatted = serde_json::to_string_pretty(&log_entry)?;
fs::write(file_path, formatted)?;
Ok(())
}
pub fn log_stream_chunk(&self, endpoint: &str, chunk: &str) -> Result<(), std::io::Error> {
if !self.enabled {
return Ok(());
}
let timestamp = Local::now();
let filename = format!(
"{}_stream_{}.txt",
timestamp.format("%Y%m%d_%H%M%S%.3f"),
sanitize_endpoint(endpoint)
);
let file_path = self.log_dir.join(filename);
let log_entry = format!(
"=== Stream Chunk at {} ===\nEndpoint: {}\n\n{}\n",
timestamp.to_rfc3339(),
endpoint,
chunk
);
fs::write(file_path, log_entry)?;
Ok(())
}
pub fn log_error(&self, endpoint: &str, error: &str) -> Result<(), std::io::Error> {
if !self.enabled {
return Ok(());
}
let timestamp = Local::now();
let filename = format!(
"{}_error_{}.txt",
timestamp.format("%Y%m%d_%H%M%S%.3f"),
sanitize_endpoint(endpoint)
);
let file_path = self.log_dir.join(filename);
let log_entry = format!(
"=== Error at {} ===\nEndpoint: {}\n\n{}\n",
timestamp.to_rfc3339(),
endpoint,
error
);
fs::write(file_path, log_entry)?;
Ok(())
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
fn sanitize_endpoint(endpoint: &str) -> String {
endpoint
.replace('/', "_")
.replace('\\', "_")
.replace(':', "_")
.replace(' ', "_")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-')
.collect()
}
+1
View File
@@ -12,6 +12,7 @@ mod chat_completions;
mod client;
mod client_common;
pub mod codex;
pub mod debug_logger;
pub mod slash_commands;
pub use codex::Codex;
pub use codex::CodexSpawnOk;
+4 -2
View File
@@ -30,7 +30,7 @@ pub enum ResponseInputItem {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentItem {
InputText { text: String },
InputImage { image_url: String },
InputImage { image_url: String, detail: Option<String> },
OutputText { text: String },
}
@@ -154,7 +154,7 @@ impl From<Vec<InputItem>> for ResponseInputItem {
content_items.push(ContentItem::InputText { text });
}
InputItem::Image { image_url } => {
content_items.push(ContentItem::InputImage { image_url });
content_items.push(ContentItem::InputImage { image_url, detail: None });
}
InputItem::LocalImage { path } => match std::fs::read(&path) {
Ok(bytes) => {
@@ -165,6 +165,7 @@ impl From<Vec<InputItem>> for ResponseInputItem {
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
content_items.push(ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
detail: None,
});
}
Err(err) => {
@@ -199,6 +200,7 @@ impl From<Vec<InputItem>> for ResponseInputItem {
tracing::info!("Created ephemeral image data URL with mime: {}", mime);
content_items.push(ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
detail: Some("high".to_string()),
});
}
Err(err) => {
+50
View File
@@ -469,6 +469,8 @@ pub(crate) fn get_openai_tools(
tools.push(create_browser_type_tool());
tools.push(create_browser_key_tool());
tools.push(create_browser_javascript_tool());
tools.push(create_browser_scroll_tool());
tools.push(create_browser_history_tool());
// Add agent management tools for calling external LLMs asynchronously
tools.push(create_run_agent_tool());
@@ -996,3 +998,51 @@ fn create_browser_javascript_tool() -> OpenAiTool {
},
})
}
fn create_browser_scroll_tool() -> OpenAiTool {
let mut properties = BTreeMap::new();
properties.insert(
"dx".to_string(),
JsonSchema::Number {
description: Some("Horizontal scroll delta in pixels (positive = right)".to_string()),
},
);
properties.insert(
"dy".to_string(),
JsonSchema::Number {
description: Some("Vertical scroll delta in pixels (positive = down)".to_string()),
},
);
OpenAiTool::Function(ResponsesApiTool {
name: "browser_scroll".to_string(),
description: "Scrolls the page by the specified pixel deltas.".to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec![]),
additional_properties: Some(false),
},
})
}
fn create_browser_history_tool() -> OpenAiTool {
let mut properties = BTreeMap::new();
properties.insert(
"direction".to_string(),
JsonSchema::String {
description: Some("History direction: 'back' or 'forward'".to_string()),
},
);
OpenAiTool::Function(ResponsesApiTool {
name: "browser_history".to_string(),
description: "Navigates browser history backward or forward.".to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["direction".to_string()]),
additional_properties: Some(false),
},
})
}
+4
View File
@@ -48,6 +48,10 @@ pub struct Cli {
/// Tell the agent to use the specified directory as its working root.
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Enable debug logging of all LLM requests and responses to files.
#[clap(long = "debug", short = 'd', default_value_t = false)]
pub debug: bool,
/// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", default_value_t = false)]
+2
View File
@@ -42,6 +42,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
full_auto,
dangerously_bypass_approvals_and_sandbox,
cwd,
debug,
skip_git_repo_check,
color,
last_message_file,
@@ -149,6 +150,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
include_plan_tool: None,
disable_response_storage: oss.then_some(true),
show_raw_agent_reasoning: oss.then_some(true),
debug: Some(debug),
};
// Parse `-c` overrides.
let cli_kv_overrides = match config_overrides.parse_overrides() {
@@ -162,6 +162,7 @@ impl CodexToolCallParam {
include_plan_tool,
disable_response_storage: None,
show_raw_agent_reasoning: None,
debug: None,
};
let cli_overrides = cli_overrides
@@ -61,6 +61,7 @@ pub(crate) async fn handle_create_conversation(
include_plan_tool: None,
disable_response_storage: None,
show_raw_agent_reasoning: None,
debug: None,
};
let cfg: CodexConfig = match CodexConfig::load_with_cli_overrides(cli_overrides, overrides) {
+5
View File
@@ -58,6 +58,9 @@ pub(crate) struct App<'a> {
pending_redraw: Arc<AtomicBool>,
enhanced_keys_supported: bool,
/// Debug flag for logging LLM requests/responses
_debug: bool,
}
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
@@ -76,6 +79,7 @@ impl App<'_> {
initial_prompt: Option<String>,
initial_images: Vec<std::path::PathBuf>,
show_trust_screen: bool,
debug: bool,
) -> Self {
let (app_event_tx, app_event_rx) = channel();
let app_event_tx = AppEventSender::new(app_event_tx);
@@ -169,6 +173,7 @@ impl App<'_> {
file_search,
pending_redraw,
enhanced_keys_supported,
_debug: debug,
}
}
+118 -25
View File
@@ -89,6 +89,10 @@ pub(crate) struct ChatWidget<'a> {
answer_buffer: String,
// Cache of the last finalized assistant message to suppress immediate duplicates
last_assistant_message: Option<String>,
// Track the ID of the current streaming message to prevent duplicates
current_stream_message_id: Option<String>,
// Track the ID of the current streaming reasoning to prevent duplicates
current_stream_reasoning_id: Option<String>,
running_commands: HashMap<String, RunningCommand>,
live_builder: RowBuilder,
current_stream: Option<StreamKind>,
@@ -357,8 +361,7 @@ impl ChatWidget<'_> {
StreamKind::Reasoning => {
// Always add space above reasoning (unless history is completely empty)
if !self.history_cells.is_empty() {
self.history_cells
.push(HistoryCell::new_text_line(RLine::from("")));
self.add_to_history(HistoryCell::new_text_line(RLine::from("")));
}
self.stream_header_emitted = true;
self.request_redraw();
@@ -386,8 +389,7 @@ impl ChatWidget<'_> {
};
if needs_space {
self.history_cells
.push(HistoryCell::new_text_line(RLine::from("")));
self.add_to_history(HistoryCell::new_text_line(RLine::from("")));
}
// Add GPT-5 header with secondary color
@@ -395,15 +397,14 @@ impl ChatWidget<'_> {
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Span;
self.history_cells
.push(HistoryCell::new_styled_text_line(RLine::from(
Span::styled(
formatted_model,
Style::default()
.fg(crate::colors::secondary())
.add_modifier(Modifier::BOLD),
),
)));
self.add_to_history(HistoryCell::new_styled_text_line(RLine::from(
Span::styled(
formatted_model,
Style::default()
.fg(crate::colors::secondary())
.add_modifier(Modifier::BOLD),
),
)));
self.stream_header_emitted = true;
self.request_redraw();
@@ -495,6 +496,8 @@ impl ChatWidget<'_> {
content_buffer: String::new(),
answer_buffer: String::new(),
last_assistant_message: None,
current_stream_message_id: None,
current_stream_reasoning_id: None,
running_commands: HashMap::new(),
// Use max width to disable wrapping during streaming
// Text will be properly wrapped when displayed based on terminal width
@@ -974,6 +977,10 @@ impl ChatWidget<'_> {
}
pub(crate) fn handle_codex_event(&mut self, event: Event) {
tracing::info!(
"handle_codex_event({})",
serde_json::to_string_pretty(&event).unwrap_or_default()
);
let Event { id, msg } = event;
match msg {
EventMsg::SessionConfigured(event) => {
@@ -996,7 +1003,23 @@ impl ChatWidget<'_> {
self.request_redraw();
}
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
// Check if we've already processed this message (duplicate detection)
// Check if this is the final message for a stream we've been processing
if let Some(stream_id) = &self.current_stream_message_id {
if stream_id == &id {
// This is the final message for the current stream
// Just finalize without adding duplicate content
if self.current_stream == Some(StreamKind::Answer) {
self.finalize_stream(StreamKind::Answer);
self.last_assistant_message = Some(message.clone());
self.answer_buffer.clear();
self.current_stream_message_id = None;
}
self.request_redraw();
return;
}
}
// Fallback: Check if we've already processed this message (duplicate detection)
let message_trimmed = message.trim();
if !message_trimmed.is_empty() {
if let Some(prev) = &self.last_assistant_message {
@@ -1015,6 +1038,7 @@ impl ChatWidget<'_> {
self.finalize_stream(StreamKind::Answer);
self.last_assistant_message = Some(message.clone());
self.answer_buffer.clear();
self.current_stream_message_id = None;
}
self.request_redraw();
return;
@@ -1035,11 +1059,14 @@ impl ChatWidget<'_> {
self.last_assistant_message = Some(message.clone());
}
self.answer_buffer.clear();
self.current_stream_message_id = None;
}
self.request_redraw();
}
EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => {
self.begin_stream(StreamKind::Answer);
// Track the message ID for this stream
self.current_stream_message_id = Some(id.clone());
// Update status to show we're generating a response
if self.bottom_pane.is_task_running() {
self.bottom_pane
@@ -1053,6 +1080,8 @@ impl ChatWidget<'_> {
// Stream CoT into the live pane; keep input visible and commit
// overflow rows incrementally to scrollback.
self.begin_stream(StreamKind::Reasoning);
// Track the reasoning ID for this stream
self.current_stream_reasoning_id = Some(id.clone());
// Update status to show we're thinking/reasoning
if self.bottom_pane.is_task_running() {
self.bottom_pane.update_status_text("reasoning".to_string());
@@ -1062,6 +1091,35 @@ impl ChatWidget<'_> {
self.request_redraw();
}
EventMsg::AgentReasoning(AgentReasoningEvent { text }) => {
// Check if this is the final message for a stream we've been processing
if let Some(stream_id) = &self.current_stream_reasoning_id {
if stream_id == &id {
// This is the final message for the current stream
// Just finalize without adding duplicate content
if self.current_stream == Some(StreamKind::Reasoning) {
self.finalize_stream(StreamKind::Reasoning);
self.reasoning_buffer.clear();
self.current_stream_reasoning_id = None;
}
self.request_redraw();
return;
}
}
// Fallback: Check if we've already streamed this content via deltas
let text_trimmed = text.trim();
if !self.reasoning_buffer.is_empty() && self.reasoning_buffer.trim() == text_trimmed {
// We've already streamed this content via deltas
// Just finalize the stream without adding duplicate content
if self.current_stream == Some(StreamKind::Reasoning) {
self.finalize_stream(StreamKind::Reasoning);
self.reasoning_buffer.clear();
self.current_stream_reasoning_id = None;
}
self.request_redraw();
return;
}
// Final reasoning: if no deltas were streamed, render the final text.
if self.current_stream != Some(StreamKind::Reasoning) && !text.is_empty() {
self.begin_stream(StreamKind::Reasoning);
@@ -1070,6 +1128,8 @@ impl ChatWidget<'_> {
// Only finalize if we actually had a stream
if self.current_stream == Some(StreamKind::Reasoning) {
self.finalize_stream(StreamKind::Reasoning);
self.reasoning_buffer.clear();
self.current_stream_reasoning_id = None;
}
self.request_redraw();
}
@@ -1078,6 +1138,8 @@ impl ChatWidget<'_> {
}) => {
// Treat raw reasoning content the same as summarized reasoning for UI flow.
self.begin_stream(StreamKind::Reasoning);
// Track the reasoning ID for this stream
self.current_stream_reasoning_id = Some(id.clone());
// Update status to show we're thinking/reasoning
if self.bottom_pane.is_task_running() {
self.bottom_pane.update_status_text("reasoning".to_string());
@@ -1087,6 +1149,35 @@ impl ChatWidget<'_> {
self.request_redraw();
}
EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text }) => {
// Check if this is the final message for a stream we've been processing
if let Some(stream_id) = &self.current_stream_reasoning_id {
if stream_id == &id {
// This is the final message for the current stream
// Just finalize without adding duplicate content
if self.current_stream == Some(StreamKind::Reasoning) {
self.finalize_stream(StreamKind::Reasoning);
self.reasoning_buffer.clear();
self.current_stream_reasoning_id = None;
}
self.request_redraw();
return;
}
}
// Fallback: Check if we've already streamed this content via deltas
let text_trimmed = text.trim();
if !self.reasoning_buffer.is_empty() && self.reasoning_buffer.trim() == text_trimmed {
// We've already streamed this content via deltas
// Just finalize the stream without adding duplicate content
if self.current_stream == Some(StreamKind::Reasoning) {
self.finalize_stream(StreamKind::Reasoning);
self.reasoning_buffer.clear();
self.current_stream_reasoning_id = None;
}
self.request_redraw();
return;
}
// Final raw reasoning: if no deltas were streamed, render the final text.
if self.current_stream != Some(StreamKind::Reasoning) && !text.is_empty() {
self.begin_stream(StreamKind::Reasoning);
@@ -1095,6 +1186,8 @@ impl ChatWidget<'_> {
// Only finalize if we actually had a stream
if self.current_stream == Some(StreamKind::Reasoning) {
self.finalize_stream(StreamKind::Reasoning);
self.reasoning_buffer.clear();
self.current_stream_reasoning_id = None;
}
self.request_redraw();
}
@@ -3134,10 +3227,9 @@ impl ChatWidget<'_> {
for line in lines {
// Use dimmed reasoning for thinking to get both markdown and dimming
if matches!(self.current_stream, Some(StreamKind::Reasoning)) {
self.history_cells
.push(HistoryCell::new_dimmed_reasoning_line(line));
self.add_to_history(HistoryCell::new_dimmed_reasoning_line(line));
} else {
self.history_cells.push(HistoryCell::new_text_line(line));
self.add_to_history(HistoryCell::new_text_line(line));
}
}
// Auto-follow if near bottom so streaming stays visible
@@ -3187,10 +3279,9 @@ impl ChatWidget<'_> {
for line in lines {
// Use dimmed reasoning for thinking to get both markdown and dimming
if matches!(self.current_stream, Some(StreamKind::Reasoning)) {
self.history_cells
.push(HistoryCell::new_dimmed_reasoning_line(line));
self.add_to_history(HistoryCell::new_dimmed_reasoning_line(line));
} else {
self.history_cells.push(HistoryCell::new_text_line(line));
self.add_to_history(HistoryCell::new_text_line(line));
}
}
// Auto-follow if near bottom when finalizing a stream
@@ -3354,13 +3445,14 @@ impl WidgetRef for &ChatWidget<'_> {
(start_y, 0u16) // No scrolling needed
} else {
// Content overflows - calculate scroll position
// scroll_offset of 0 = show newest at bottom
// scroll_offset > 0 = scroll up to see older content
// scroll_offset is measured from the bottom (0 = bottom/newest)
// Convert to distance from the top for rendering math.
let max_scroll = total_height.saturating_sub(content_area.height);
// Update cache and clamp for display only
self.last_max_scroll.set(max_scroll);
let clamped_scroll_offset = self.scroll_offset.min(max_scroll);
(content_area.y, clamped_scroll_offset)
let scroll_from_top = max_scroll.saturating_sub(clamped_scroll_offset);
(content_area.y, scroll_from_top)
};
// Render the scrollable content
@@ -3400,8 +3492,9 @@ impl WidgetRef for &ChatWidget<'_> {
height: visible_height,
};
// Render the item
item.render_ref(item_area, buf);
// Render only the visible window of the item using vertical skip
let skip_rows = skip_top;
item.render_with_skip(item_area, buf, skip_rows);
screen_y += visible_height;
}
+4
View File
@@ -59,6 +59,10 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Enable debug logging of all LLM requests and responses to files.
#[clap(long = "debug", short = 'd', default_value_t = false)]
pub debug: bool,
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
}
+130 -5
View File
@@ -1,6 +1,7 @@
use crate::diff_render::create_diff_summary;
use crate::exec_command::relativize_to_home;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::scroll_view::ScrollView;
use crate::slash_command::SlashCommand;
use crate::text_block::TextBlock;
use crate::text_formatting::format_and_truncate_tool_result;
@@ -319,7 +320,11 @@ impl HistoryCell {
HistoryCell::BackgroundEvent { view: _ } => {
// For background events (LLM responses), use proper word wrapping
let processed_lines = self.get_processed_lines(width);
processed_lines.len() as u16
Paragraph::new(Text::from(processed_lines))
.wrap(Wrap { trim: false })
.line_count(width)
.try_into()
.unwrap_or(0)
}
HistoryCell::StyledText { view } => {
// For styled text, respect wrapping to the available width
@@ -330,9 +335,13 @@ impl HistoryCell {
.unwrap_or(0)
}
HistoryCell::DimmedReasoning { view: _ } => {
// For dimmed reasoning, use dimmed markdown processing
// For dimmed reasoning, use dimmed markdown processing with wrapping
let processed_lines = self.get_processed_lines(width);
processed_lines.len() as u16
Paragraph::new(Text::from(processed_lines))
.wrap(Wrap { trim: false })
.line_count(width)
.try_into()
.unwrap_or(0)
}
_ => Paragraph::new(Text::from(self.plain_lines()))
.wrap(Wrap { trim: false })
@@ -438,15 +447,27 @@ impl HistoryCell {
) -> Vec<Line<'static>> {
match parsed.is_empty() {
true => HistoryCell::new_exec_command_generic(command, output),
false => HistoryCell::new_parsed_command(parsed, output),
false => HistoryCell::new_parsed_command(command, parsed, output),
}
}
fn new_parsed_command(
command: &[String],
parsed_commands: &[ParsedCommand],
output: Option<&CommandOutput>,
) -> Vec<Line<'static>> {
let mut lines: Vec<Line> = vec![Line::from("⚙︎ Working")];
let mut lines: Vec<Line> = Vec::new();
if output.is_some() {
// Completed: show the command that ran
let cmd_text = shlex_join_safe(command);
lines.push(Line::from(vec![
"⚡ Ran command ".magenta(),
cmd_text.into(),
]));
} else {
// In progress
lines.push(Line::from("⚙︎ Working"));
}
for (i, parsed) in parsed_commands.iter().enumerate() {
let text = match parsed {
@@ -1218,6 +1239,110 @@ impl WidgetRef for &HistoryCell {
}
}
impl HistoryCell {
/// Render this history cell with a vertical skip of `skip_top` rows.
/// This is used by the outer scroller to render only the visible window
/// of a potentially tall cell (e.g., large diffs).
pub(crate) fn render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_top: u16) {
match self {
// Animated items ignore vertical skip due to custom rendering.
HistoryCell::AnimatedWelcome { .. } => self.render_ref(area, buf),
HistoryCell::BackgroundEvent { .. }
| HistoryCell::StyledText { .. }
| HistoryCell::WelcomeMessage { .. }
| HistoryCell::DimmedReasoning { .. }
| HistoryCell::GitDiffOutput { .. }
| HistoryCell::ReasoningOutput { .. }
| HistoryCell::StatusOutput { .. }
| HistoryCell::PromptsOutput { .. }
| HistoryCell::ErrorEvent { .. }
| HistoryCell::SessionInfo { .. }
| HistoryCell::PendingPatch { .. }
| HistoryCell::PlanUpdate { .. }
| HistoryCell::PatchApplyResult { .. }
| HistoryCell::CompletedMcpToolCall { .. }
| HistoryCell::ActiveMcpToolCall { .. } => {
// Use the same processed/plain lines as desired_height/render_ref but apply a vertical scroll.
let lines = match self {
HistoryCell::BackgroundEvent { .. }
| HistoryCell::DimmedReasoning { .. } => self.get_processed_lines(area.width),
HistoryCell::StyledText { view }
| HistoryCell::WelcomeMessage { view }
| HistoryCell::GitDiffOutput { view }
| HistoryCell::ReasoningOutput { view }
| HistoryCell::StatusOutput { view }
| HistoryCell::PromptsOutput { view }
| HistoryCell::ErrorEvent { view }
| HistoryCell::SessionInfo { view }
| HistoryCell::PendingPatch { view }
| HistoryCell::PlanUpdate { view }
| HistoryCell::PatchApplyResult { view }
| HistoryCell::CompletedMcpToolCall { view }
| HistoryCell::ActiveMcpToolCall { view } => view.lines.clone(),
_ => self.plain_lines(),
};
Paragraph::new(Text::from(lines))
.wrap(Wrap { trim: false })
.scroll((skip_top, 0))
.style(
Style::default()
.fg(crate::colors::text())
.bg(crate::colors::background()),
)
.render(area, buf);
}
HistoryCell::UserPrompt { view } => {
// Keep the left border and apply scroll inside the text area.
for y in area.top()..area.bottom() {
if area.left() < area.right() {
buf[(area.left(), y)]
.set_char('│')
.set_fg(crate::colors::border_focused());
}
}
let text_area = Rect {
x: area.x + 2,
y: area.y,
width: area.width.saturating_sub(2),
height: area.height,
};
Paragraph::new(Text::from(
view.lines.iter().map(line_to_static).collect::<Vec<_>>(),
))
.wrap(Wrap { trim: false })
.scroll((skip_top, 0))
.style(Style::default().bg(crate::colors::background()))
.render(text_area, buf);
}
HistoryCell::Exec(_) | HistoryCell::CompletedMcpToolCallWithImageOutput { .. } => {
// Use ScrollView for complex widgets that don't support native scrolling
let content_height = self.desired_height(area.width);
// Create a wrapper that implements Widget
#[derive(Clone)]
struct CellRenderer<'a> {
cell: &'a HistoryCell,
}
impl<'a> Widget for CellRenderer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.cell.render_ref(area, buf);
}
}
// Apply ScrollView with the vertical skip
let scroll_view = ScrollView::new(
CellRenderer { cell: self },
content_height as usize,
)
.scroll_y(skip_top as usize);
scroll_view.render(area, buf);
}
}
}
}
fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
let args_str = invocation
.arguments
+4 -2
View File
@@ -42,6 +42,7 @@ pub mod live_wrap;
mod log_layer;
mod markdown;
pub mod onboarding;
mod scroll_view;
mod shimmer;
mod slash_command;
mod terminal_info;
@@ -112,6 +113,7 @@ pub async fn run_main(
include_plan_tool: Some(true),
disable_response_storage: cli.oss.then_some(true),
show_raw_agent_reasoning: cli.oss.then_some(true),
debug: Some(cli.debug),
};
// Parse `-c` overrides from the CLI.
@@ -269,8 +271,8 @@ fn run_ratatui_app(
let mut terminal = tui::init(&config)?;
terminal.clear()?;
let Cli { prompt, images, .. } = cli;
let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen);
let Cli { prompt, images, debug, .. } = cli;
let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen, debug);
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
{
+137
View File
@@ -0,0 +1,137 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Block, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget},
style::Style,
};
/// A generic scrollable viewport for any Widget.
/// You must provide the content height (in rows) for the current width.
#[derive(Clone)]
pub struct ScrollView<W> {
inner: W,
/// total logical content size
pub content_height: usize,
pub content_width: Option<usize>, // if you can measure it; otherwise None
/// scroll offsets
pub scroll_y: usize,
pub scroll_x: usize,
/// optional decorations
pub block: Option<Block<'static>>,
pub show_scrollbar: bool,
pub scrollbar_style: Style,
}
impl<W> ScrollView<W> {
pub fn new(inner: W, content_height: usize) -> Self {
Self {
inner,
content_height,
content_width: None,
scroll_y: 0,
scroll_x: 0,
block: None,
show_scrollbar: false, // We handle scrollbar separately in ChatWidget
scrollbar_style: Style::default(),
}
}
#[allow(dead_code)]
pub fn block(mut self, block: Block<'static>) -> Self {
self.block = Some(block);
self
}
pub fn scroll_y(mut self, y: usize) -> Self {
self.scroll_y = y;
self
}
#[allow(dead_code)]
pub fn scroll_x(mut self, x: usize) -> Self {
self.scroll_x = x;
self
}
#[allow(dead_code)]
pub fn show_scrollbar(mut self, yes: bool) -> Self {
self.show_scrollbar = yes;
self
}
#[allow(dead_code)]
pub fn scrollbar_style(mut self, style: Style) -> Self {
self.scrollbar_style = style;
self
}
}
impl<W> Widget for ScrollView<W>
where
W: Widget + Clone, // render takes ownership; Clone keeps ergonomics
{
fn render(self, area: Rect, buf: &mut Buffer) {
// Apply outer block (optional)
let mut inner_area = area;
if let Some(block) = &self.block {
block.clone().render(area, buf);
inner_area = block.inner(area);
if inner_area.height == 0 || inner_area.width == 0 {
return;
}
}
let viewport_h = inner_area.height as usize;
let viewport_w = inner_area.width as usize;
// Clamp scroll positions
let max_y = self.content_height.saturating_sub(viewport_h);
let sy = self.scroll_y.min(max_y);
let sx = self.scroll_x.min(self.content_width.unwrap_or(0).saturating_sub(viewport_w));
// Offscreen render: pretend we have a huge height = content_height
// (Width = viewport width so we don't waste memory horizontally)
let virtual_h = self.content_height.max(viewport_h) as u16;
let virtual_area = Rect {
x: 0,
y: 0,
width: inner_area.width,
height: virtual_h,
};
// Create an offscreen buffer and render the child into it
let mut off = Buffer::empty(virtual_area);
self.inner.clone().render(virtual_area, &mut off);
// Blit the visible window [sy..sy+viewport_h] and [sx..sx+viewport_w] into real buffer
for row in 0..viewport_h.min(self.content_height.saturating_sub(sy)) {
for col in 0..viewport_w {
let src_x = sx + col;
// If content_width is unknown, assume child draws within viewport width
if let Some(cw) = self.content_width {
if src_x >= cw {
continue;
}
}
let src_y = sy + row;
if let Some(src_cell) = off.cell((src_x as u16, src_y as u16)) {
let dst_x = inner_area.x + col as u16;
let dst_y = inner_area.y + row as u16;
if let Some(dst_cell) = buf.cell_mut((dst_x, dst_y)) {
*dst_cell = src_cell.clone();
}
}
}
}
// Optional scrollbar on the right
if self.show_scrollbar && self.content_height > viewport_h {
let mut state = ScrollbarState::new(self.content_height)
.position(sy)
.viewport_content_length(viewport_h);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.style(self.scrollbar_style)
.render(inner_area, buf, &mut state);
}
}
}
Executable
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# Test the debug flag
echo "Testing debug flag..."
# Check if debug logs directory gets created
DEBUG_LOG_DIR="$HOME/.codex/debug_logs"
# Remove old debug logs if they exist
if [ -d "$DEBUG_LOG_DIR" ]; then
echo "Removing old debug logs..."
rm -rf "$DEBUG_LOG_DIR"
fi
# Run coder with debug flag in exec mode (non-interactive)
echo "Running coder with --debug flag..."
./target/dev-fast/coder exec --debug "What is 2+2?" 2>&1 | head -20
# Check if debug logs were created
if [ -d "$DEBUG_LOG_DIR" ]; then
echo ""
echo "Debug logs directory created: $DEBUG_LOG_DIR"
echo "Contents:"
ls -la "$DEBUG_LOG_DIR" 2>/dev/null | head -10
# Show a sample log file
echo ""
echo "Sample log file:"
FIRST_LOG=$(ls "$DEBUG_LOG_DIR" 2>/dev/null | head -1)
if [ -n "$FIRST_LOG" ]; then
echo "Contents of $FIRST_LOG:"
head -20 "$DEBUG_LOG_DIR/$FIRST_LOG"
fi
else
echo "Debug logs directory NOT created"
fi
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Simple test for debug logging
echo "Testing debug logging..."
# Set the debug log directory
DEBUG_LOG_DIR="$HOME/.codex/debug_logs"
# Clean up any existing debug logs
if [ -d "$DEBUG_LOG_DIR" ]; then
echo "Cleaning up old debug logs..."
rm -rf "$DEBUG_LOG_DIR"
fi
# Run coder exec with debug flag
cd /Users/zemaj/www/just-every/coder
echo "Running: ./codex-rs/target/dev-fast/coder exec --debug 'What is 2+2?'"
OUTPUT=$(./codex-rs/target/dev-fast/coder exec --debug "What is 2+2?" 2>&1)
echo "Command output:"
echo "$OUTPUT" | head -20
# Check if debug logs were created
echo ""
if [ -d "$DEBUG_LOG_DIR" ]; then
echo "✅ Debug logs directory created: $DEBUG_LOG_DIR"
# Count log files
LOG_COUNT=$(find "$DEBUG_LOG_DIR" -type f | wc -l)
echo "Number of log files created: $LOG_COUNT"
# List log files
echo ""
echo "Log files:"
ls -la "$DEBUG_LOG_DIR" | tail -n +2 | head -10
# Show a sample request log
echo ""
REQUEST_LOG=$(find "$DEBUG_LOG_DIR" -name "*request*.json" | head -1)
if [ -n "$REQUEST_LOG" ]; then
echo "Sample request log (first 30 lines of $(basename "$REQUEST_LOG")):"
cat "$REQUEST_LOG" | head -30
fi
else
echo "❌ Debug logs directory NOT created - debug logging may not be working"
fi