mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
Fix formatting and build issues
- Applied cargo fmt to fix code formatting - Fixed trailing whitespace issues - Updated colors to use theme system - Fixed merge-related syntax errors - Recreated build-fast.sh for monorepo structure
This commit is contained in:
+3
-3
@@ -2,7 +2,7 @@
|
||||
# Fast build script for local development - optimized for speed
|
||||
set -euo pipefail
|
||||
|
||||
cd codex-rs
|
||||
# No need to cd, we're already in the Rust project root
|
||||
|
||||
# Use dev-fast profile by default for quick iteration
|
||||
# Can override with: PROFILE=release ./build-fast.sh
|
||||
@@ -37,13 +37,13 @@ if [ $? -eq 0 ]; then
|
||||
ln -sf "../${PROFILE}/coder" "./target/release/coder"
|
||||
|
||||
# Update the symlink in codex-cli/bin for npm wrapper
|
||||
CODEX_CLI_BIN="../codex-cli/bin/coder-aarch64-apple-darwin"
|
||||
CODEX_CLI_BIN="./codex-cli/bin/coder-aarch64-apple-darwin"
|
||||
mkdir -p "$(dirname "$CODEX_CLI_BIN")"
|
||||
if [ -e "$CODEX_CLI_BIN" ]; then
|
||||
rm -f "$CODEX_CLI_BIN"
|
||||
fi
|
||||
# Create relative symlink from codex-cli/bin to the actual binary
|
||||
ln -sf "../../codex-rs/target/${PROFILE}/coder" "$CODEX_CLI_BIN"
|
||||
ln -sf "../../target/${PROFILE}/coder" "$CODEX_CLI_BIN"
|
||||
|
||||
echo "✅ Symlinks updated"
|
||||
echo ""
|
||||
|
||||
@@ -234,10 +234,7 @@ impl BrowserManager {
|
||||
info!("Connecting to Chrome via WebSocket...");
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let (browser, mut handler) = Browser::connect(ws).await?;
|
||||
info!(
|
||||
"Connected to Chrome in {:?}",
|
||||
connect_start.elapsed()
|
||||
);
|
||||
info!("Connected to Chrome in {:?}", connect_start.elapsed());
|
||||
|
||||
let task =
|
||||
tokio::spawn(
|
||||
@@ -260,12 +257,12 @@ impl BrowserManager {
|
||||
}
|
||||
} else {
|
||||
return Err(BrowserError::CdpError(
|
||||
"No Chrome instance found with debug port".to_string()
|
||||
"No Chrome instance found with debug port".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(BrowserError::CdpError(
|
||||
"No CDP port configured for Chrome connection".to_string()
|
||||
"No CDP port configured for Chrome connection".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -400,8 +397,11 @@ impl BrowserManager {
|
||||
.arg("--disable-background-timer-throttling")
|
||||
// Redirect Chrome logging to a file to keep terminal clean
|
||||
.arg("--enable-logging")
|
||||
.arg("--log-level=1") // 0 = INFO, 1 = WARNING, 2 = ERROR, 3 = FATAL (1 to reduce verbosity)
|
||||
.arg(format!("--log-file={}/coder-chrome.log", std::env::temp_dir().display()))
|
||||
.arg("--log-level=1") // 0 = INFO, 1 = WARNING, 2 = ERROR, 3 = FATAL (1 to reduce verbosity)
|
||||
.arg(format!(
|
||||
"--log-file={}/coder-chrome.log",
|
||||
std::env::temp_dir().display()
|
||||
))
|
||||
// Suppress console output
|
||||
.arg("--silent-launch")
|
||||
// Set a longer timeout for CDP requests (60 seconds instead of default 30)
|
||||
@@ -491,11 +491,9 @@ impl BrowserManager {
|
||||
let mut page_guard = self.page.lock().await;
|
||||
if let Some(page) = page_guard.as_ref() {
|
||||
// Verify the page is still responsive
|
||||
let check_result = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
page.get_current_url()
|
||||
).await;
|
||||
|
||||
let check_result =
|
||||
tokio::time::timeout(Duration::from_secs(2), page.get_current_url()).await;
|
||||
|
||||
match check_result {
|
||||
Ok(Ok(_)) => {
|
||||
// Page is responsive
|
||||
@@ -769,15 +767,13 @@ impl BrowserManager {
|
||||
|
||||
async fn ensure_browser(&self) -> Result<()> {
|
||||
let mut browser_guard = self.browser.lock().await;
|
||||
|
||||
|
||||
// Check if we have a browser instance
|
||||
if let Some(browser) = browser_guard.as_ref() {
|
||||
// Try to verify it's still connected with a simple operation
|
||||
let check_result = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
browser.version()
|
||||
).await;
|
||||
|
||||
let check_result =
|
||||
tokio::time::timeout(Duration::from_secs(2), browser.version()).await;
|
||||
|
||||
match check_result {
|
||||
Ok(Ok(_)) => {
|
||||
// Browser is responsive
|
||||
@@ -793,7 +789,7 @@ impl BrowserManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Need to start or restart the browser
|
||||
drop(browser_guard);
|
||||
info!("Starting/restarting browser connection...");
|
||||
@@ -1000,33 +996,33 @@ impl BrowserManager {
|
||||
};
|
||||
|
||||
// Get current URL with timeout
|
||||
let current_url = match tokio::time::timeout(
|
||||
Duration::from_secs(3),
|
||||
page.get_current_url()
|
||||
).await {
|
||||
Ok(Ok(url)) => url,
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
warn!("Failed to get current URL, using default");
|
||||
"about:blank".to_string()
|
||||
}
|
||||
};
|
||||
let current_url =
|
||||
match tokio::time::timeout(Duration::from_secs(3), page.get_current_url()).await {
|
||||
Ok(Ok(url)) => url,
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
warn!("Failed to get current URL, using default");
|
||||
"about:blank".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
// Capture screenshots with timeout
|
||||
let screenshot_result = tokio::time::timeout(
|
||||
Duration::from_secs(15), // Allow up to 15 seconds for screenshot
|
||||
page.screenshot(mode)
|
||||
).await;
|
||||
|
||||
page.screenshot(mode),
|
||||
)
|
||||
.await;
|
||||
|
||||
let screenshots = match screenshot_result {
|
||||
Ok(Ok(shots)) => shots,
|
||||
Ok(Err(e)) => {
|
||||
return Err(BrowserError::ScreenshotError(
|
||||
format!("Screenshot capture failed: {}", e)
|
||||
));
|
||||
return Err(BrowserError::ScreenshotError(format!(
|
||||
"Screenshot capture failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(BrowserError::ScreenshotError(
|
||||
"Screenshot capture timed out after 15 seconds".to_string()
|
||||
"Screenshot capture timed out after 15 seconds".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
@@ -1060,19 +1056,19 @@ impl BrowserManager {
|
||||
let page = self.get_or_create_page().await?;
|
||||
page.move_mouse(x, y).await
|
||||
}
|
||||
|
||||
|
||||
/// Move the mouse by relative offset from current position
|
||||
pub async fn move_mouse_relative(&self, dx: f64, dy: f64) -> Result<(f64, f64)> {
|
||||
let page = self.get_or_create_page().await?;
|
||||
page.move_mouse_relative(dx, dy).await
|
||||
}
|
||||
|
||||
|
||||
/// Click at the specified coordinates
|
||||
pub async fn click(&self, x: f64, y: f64) -> Result<()> {
|
||||
let page = self.get_or_create_page().await?;
|
||||
page.click(x, y).await
|
||||
}
|
||||
|
||||
|
||||
/// Click at the current mouse position
|
||||
pub async fn click_at_current(&self) -> Result<(f64, f64)> {
|
||||
let page = self.get_or_create_page().await?;
|
||||
|
||||
+121
-95
@@ -11,10 +11,10 @@ use chromiumoxide::cdp::browser_protocol::input::DispatchMouseEventType;
|
||||
// Import MouseButton (New)
|
||||
use chromiumoxide::cdp::browser_protocol::input::MouseButton;
|
||||
// Import AddScriptToEvaluateOnNewDocumentParams (New)
|
||||
use base64::Engine as _;
|
||||
use chromiumoxide::cdp::browser_protocol::page::AddScriptToEvaluateOnNewDocumentParams;
|
||||
use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat;
|
||||
use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotParams;
|
||||
use base64::Engine as _;
|
||||
use chromiumoxide::page::Page as CdpPage;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -62,7 +62,7 @@ impl Page {
|
||||
let cdp_page_clone = page.cdp_page.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::inject_tab_interception_script(&cdp_page_clone).await {
|
||||
warn!("Failed to inject navigation interception script: {}", e);
|
||||
warn!("Failed to inject navigation interception script: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -175,56 +175,63 @@ impl Page {
|
||||
) -> Result<chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotReturns> {
|
||||
// First try with from_surface(false) to avoid flashing, with timeout
|
||||
let params = params_builder.clone().from_surface(false).build();
|
||||
|
||||
|
||||
let first_attempt = tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(8),
|
||||
self.cdp_page.execute(params)
|
||||
).await;
|
||||
|
||||
self.cdp_page.execute(params),
|
||||
)
|
||||
.await;
|
||||
|
||||
match first_attempt {
|
||||
Ok(Ok(resp)) => Ok(resp.result),
|
||||
Ok(Err(e)) => {
|
||||
// Log the initial failure
|
||||
debug!("Screenshot with from_surface(false) failed: {}. Retrying with from_surface(true)...", e);
|
||||
|
||||
debug!(
|
||||
"Screenshot with from_surface(false) failed: {}. Retrying with from_surface(true)...",
|
||||
e
|
||||
);
|
||||
|
||||
// Retry with from_surface(true) - this may cause flashing but will work when window is not visible
|
||||
let retry_params = params_builder.from_surface(true).build();
|
||||
let retry_attempt = tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(8),
|
||||
self.cdp_page.execute(retry_params)
|
||||
).await;
|
||||
|
||||
self.cdp_page.execute(retry_params),
|
||||
)
|
||||
.await;
|
||||
|
||||
match retry_attempt {
|
||||
Ok(Ok(resp)) => Ok(resp.result),
|
||||
Ok(Err(retry_err)) => {
|
||||
debug!("Screenshot retry with from_surface(true) also failed: {}", retry_err);
|
||||
debug!(
|
||||
"Screenshot retry with from_surface(true) also failed: {}",
|
||||
retry_err
|
||||
);
|
||||
Err(retry_err.into())
|
||||
}
|
||||
Err(_) => {
|
||||
Err(BrowserError::ScreenshotError(
|
||||
"Screenshot retry timed out after 8 seconds".to_string()
|
||||
))
|
||||
}
|
||||
Err(_) => Err(BrowserError::ScreenshotError(
|
||||
"Screenshot retry timed out after 8 seconds".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// First attempt timed out, try with from_surface(true)
|
||||
debug!("Screenshot with from_surface(false) timed out. Retrying with from_surface(true)...");
|
||||
|
||||
debug!(
|
||||
"Screenshot with from_surface(false) timed out. Retrying with from_surface(true)..."
|
||||
);
|
||||
|
||||
let retry_params = params_builder.from_surface(true).build();
|
||||
let retry_attempt = tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(8),
|
||||
self.cdp_page.execute(retry_params)
|
||||
).await;
|
||||
|
||||
self.cdp_page.execute(retry_params),
|
||||
)
|
||||
.await;
|
||||
|
||||
match retry_attempt {
|
||||
Ok(Ok(resp)) => Ok(resp.result),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(_) => {
|
||||
Err(BrowserError::ScreenshotError(
|
||||
"Screenshot timed out after 8 seconds on both attempts".to_string()
|
||||
))
|
||||
}
|
||||
Err(_) => Err(BrowserError::ScreenshotError(
|
||||
"Screenshot timed out after 8 seconds on both attempts".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,23 +244,24 @@ impl Page {
|
||||
|
||||
/// (NEW) Injects a virtual cursor element into the page at the current coordinates.
|
||||
pub async fn inject_virtual_cursor(&self) -> Result<()> {
|
||||
let cursor = self.cursor_state.lock().await.clone();
|
||||
let cursor_x = cursor.x;
|
||||
let cursor_y = cursor.y;
|
||||
let cursor = self.cursor_state.lock().await.clone();
|
||||
let cursor_x = cursor.x;
|
||||
let cursor_y = cursor.y;
|
||||
|
||||
// Creates (once) and updates a two-part virtual cursor:
|
||||
// - Arrow (instant follow)
|
||||
// - Badge (slight lag / offset for a nicer feel)
|
||||
//
|
||||
// Tunables inside script:
|
||||
// ARROW_SIZE_PX => overall design width (scales arrow)
|
||||
// BADGE_SIZE_PX => overall design width (scales badge)
|
||||
// TIP_X/TIP_Y => pixel nudge so the arrow tip lands exactly on (x,y)
|
||||
// BADGE_OFF_* => relative offset of the trailing badge
|
||||
// LAG => how much the badge trails; lower = looser, higher = tighter
|
||||
//
|
||||
// To remove later: evaluate `window.__vc?.destroy()`
|
||||
let script = format!(r#"
|
||||
// Creates (once) and updates a two-part virtual cursor:
|
||||
// - Arrow (instant follow)
|
||||
// - Badge (slight lag / offset for a nicer feel)
|
||||
//
|
||||
// Tunables inside script:
|
||||
// ARROW_SIZE_PX => overall design width (scales arrow)
|
||||
// BADGE_SIZE_PX => overall design width (scales badge)
|
||||
// TIP_X/TIP_Y => pixel nudge so the arrow tip lands exactly on (x,y)
|
||||
// BADGE_OFF_* => relative offset of the trailing badge
|
||||
// LAG => how much the badge trails; lower = looser, higher = tighter
|
||||
//
|
||||
// To remove later: evaluate `window.__vc?.destroy()`
|
||||
let script = format!(
|
||||
r#"
|
||||
(function(x, y) {{
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const VIEW_W = 40, VIEW_H = 30; // original viewBox of your assets
|
||||
@@ -511,11 +519,14 @@ impl Page {
|
||||
window.__vc.moveTo(x, y);
|
||||
}}
|
||||
}})({cursor_x}, {cursor_y});
|
||||
"#, cursor_x=cursor_x, cursor_y=cursor_y);
|
||||
"#,
|
||||
cursor_x = cursor_x,
|
||||
cursor_y = cursor_y
|
||||
);
|
||||
|
||||
self.cdp_page.evaluate(script).await?;
|
||||
Ok(())
|
||||
}
|
||||
self.cdp_page.evaluate(script).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// (NEW) Ensures an editable element is focused before typing. (Ported from TS implementation)
|
||||
async fn ensure_editable_focused(&self) -> Result<bool> {
|
||||
@@ -524,7 +535,8 @@ impl Page {
|
||||
let cursor_y = cursor.y;
|
||||
|
||||
// The JS logic from browser_session.ts
|
||||
let script = format!(r#"
|
||||
let script = format!(
|
||||
r#"
|
||||
(function(cursorX, cursorY) {{
|
||||
const editable = el => el && (
|
||||
// Refined list: exclude input types not meant for direct typing
|
||||
@@ -576,12 +588,13 @@ impl Page {
|
||||
}}
|
||||
return false;
|
||||
}})({cursor_x}, {cursor_y})
|
||||
"#, cursor_x=cursor_x, cursor_y=cursor_y);
|
||||
"#,
|
||||
cursor_x = cursor_x,
|
||||
cursor_y = cursor_y
|
||||
);
|
||||
|
||||
let result = self.cdp_page.evaluate(script).await?;
|
||||
let focused = result.value()
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
let focused = result.value().and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
|
||||
Ok(focused)
|
||||
}
|
||||
@@ -594,7 +607,7 @@ impl Page {
|
||||
// Navigate to the URL with retry on timeout
|
||||
let max_retries = 3;
|
||||
let mut last_error = None;
|
||||
|
||||
|
||||
for attempt in 1..=max_retries {
|
||||
match self.cdp_page.goto(url).await {
|
||||
Ok(_) => {
|
||||
@@ -604,9 +617,12 @@ impl Page {
|
||||
Err(e) => {
|
||||
let error_str = e.to_string();
|
||||
if error_str.contains("Request timed out") || error_str.contains("timeout") {
|
||||
warn!("Navigation timeout on attempt {}/{}: {}", attempt, max_retries, error_str);
|
||||
warn!(
|
||||
"Navigation timeout on attempt {}/{}: {}",
|
||||
attempt, max_retries, error_str
|
||||
);
|
||||
last_error = Some(e);
|
||||
|
||||
|
||||
if attempt < max_retries {
|
||||
// Wait before retry, increasing delay each time
|
||||
let delay_ms = 1000 * attempt as u64;
|
||||
@@ -621,12 +637,13 @@ impl Page {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we exhausted retries, return the last error
|
||||
if let Some(e) = last_error {
|
||||
return Err(BrowserError::CdpError(
|
||||
format!("Navigation failed after {} retries: {}", max_retries, e)
|
||||
));
|
||||
return Err(BrowserError::CdpError(format!(
|
||||
"Navigation failed after {} retries: {}",
|
||||
max_retries, e
|
||||
)));
|
||||
}
|
||||
|
||||
// Wait according to the strategy
|
||||
@@ -701,7 +718,7 @@ impl Page {
|
||||
warn!("Failed to inject virtual cursor: {}", e);
|
||||
// Continue with screenshot even if cursor injection fails
|
||||
}
|
||||
|
||||
|
||||
// Allow a brief moment for the cursor SVG to render
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||
|
||||
@@ -715,7 +732,6 @@ impl Page {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn screenshot_viewport(&self) -> Result<Vec<Screenshot>> {
|
||||
// Safe viewport capture: do not change device metrics or viewport.
|
||||
// Measure CSS viewport size via JS and capture a clipped image
|
||||
@@ -735,18 +751,20 @@ impl Page {
|
||||
.await
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
|
||||
let doc_w = probe
|
||||
.get("w")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
let doc_h = probe
|
||||
.get("h")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
let doc_w = probe.get("w").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
|
||||
let doc_h = probe.get("h").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
|
||||
|
||||
// Fall back to configured viewport if probe failed
|
||||
let vw = if doc_w > 0 { doc_w } else { self.config.viewport.width };
|
||||
let vh = if doc_h > 0 { doc_h } else { self.config.viewport.height };
|
||||
let vw = if doc_w > 0 {
|
||||
doc_w
|
||||
} else {
|
||||
self.config.viewport.width
|
||||
};
|
||||
let vh = if doc_h > 0 {
|
||||
doc_h
|
||||
} else {
|
||||
self.config.viewport.height
|
||||
};
|
||||
|
||||
// Clamp to configured maximums to keep images small for the LLM
|
||||
let target_w = vw.min(self.config.viewport.width);
|
||||
@@ -816,7 +834,9 @@ impl Page {
|
||||
let data_b64: &str = resp.data.as_ref();
|
||||
let data = base64::engine::general_purpose::STANDARD
|
||||
.decode(data_b64.as_bytes())
|
||||
.map_err(|e| BrowserError::ScreenshotError(format!("base64 decode failed: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
BrowserError::ScreenshotError(format!("base64 decode failed: {}", e))
|
||||
})?;
|
||||
shots.push(Screenshot {
|
||||
data,
|
||||
width: vw,
|
||||
@@ -846,15 +866,15 @@ impl Page {
|
||||
ImageFormat::Webp => CaptureScreenshotFormat::Webp,
|
||||
};
|
||||
|
||||
let params_builder = CaptureScreenshotParams::builder()
|
||||
.format(format)
|
||||
.clip(chromiumoxide::cdp::browser_protocol::page::Viewport {
|
||||
let params_builder = CaptureScreenshotParams::builder().format(format).clip(
|
||||
chromiumoxide::cdp::browser_protocol::page::Viewport {
|
||||
x: region.x as f64,
|
||||
y: region.y as f64,
|
||||
width: region.width as f64,
|
||||
height: region.height as f64,
|
||||
scale: 1.0,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let resp = self.capture_screenshot_with_retry(params_builder).await?;
|
||||
let data_b64: &str = resp.data.as_ref();
|
||||
@@ -908,7 +928,9 @@ impl Page {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_viewport(&self, _viewport: ViewportConfig) -> Result<()> { Ok(()) }
|
||||
pub async fn update_viewport(&self, _viewport: ViewportConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Move the mouse by relative offset from current position
|
||||
pub async fn move_mouse_relative(&self, dx: f64, dy: f64) -> Result<(f64, f64)> {
|
||||
@@ -917,19 +939,21 @@ impl Page {
|
||||
let current_x = cursor.x;
|
||||
let current_y = cursor.y;
|
||||
drop(cursor);
|
||||
|
||||
|
||||
// Calculate new position
|
||||
let new_x = current_x + dx;
|
||||
let new_y = current_y + dy;
|
||||
|
||||
debug!("Moving mouse relatively by ({}, {}) from ({}, {}) to ({}, {})",
|
||||
dx, dy, current_x, current_y, new_x, new_y);
|
||||
|
||||
|
||||
debug!(
|
||||
"Moving mouse relatively by ({}, {}) from ({}, {}) to ({}, {})",
|
||||
dx, dy, current_x, current_y, new_x, new_y
|
||||
);
|
||||
|
||||
// Use absolute move with the calculated position
|
||||
self.move_mouse(new_x, new_y).await?;
|
||||
Ok((new_x, new_y))
|
||||
}
|
||||
|
||||
|
||||
// (NEW) Move the mouse to the specified coordinates
|
||||
pub async fn move_mouse(&self, x: f64, y: f64) -> Result<()> {
|
||||
debug!("Moving mouse to ({}, {})", x, y);
|
||||
@@ -957,7 +981,8 @@ impl Page {
|
||||
|
||||
// First check if cursor exists, if not inject it
|
||||
let check_script = "typeof window.__vc !== 'undefined'";
|
||||
let cursor_exists = self.cdp_page
|
||||
let cursor_exists = self
|
||||
.cdp_page
|
||||
.evaluate(check_script)
|
||||
.await
|
||||
.ok()
|
||||
@@ -972,7 +997,8 @@ impl Page {
|
||||
}
|
||||
|
||||
// Update cursor position
|
||||
let _ = self.cdp_page
|
||||
let _ = self
|
||||
.cdp_page
|
||||
.evaluate(format!(
|
||||
"window.__vc && window.__vc.update({:.0}, {:.0});",
|
||||
move_x, move_y
|
||||
@@ -1044,7 +1070,7 @@ impl Page {
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
|
||||
// Start the click animation
|
||||
let _ = self.cdp_page.evaluate(animation_script).await;
|
||||
|
||||
@@ -1134,7 +1160,7 @@ impl Page {
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
|
||||
// Start the click animation
|
||||
let _ = self.cdp_page.evaluate(animation_script).await;
|
||||
|
||||
@@ -1174,12 +1200,12 @@ impl Page {
|
||||
// Replace em dashes with regular dashes
|
||||
let processed_text = text.replace('—', " - ");
|
||||
debug!("Typing text: {}", processed_text);
|
||||
|
||||
|
||||
// Ensure an editable element is focused first
|
||||
self.ensure_editable_focused().await?;
|
||||
|
||||
let text_len = processed_text.len();
|
||||
|
||||
|
||||
if text_len < 20 {
|
||||
// Short text: character-by-character with natural delays
|
||||
for ch in processed_text.chars() {
|
||||
@@ -1189,7 +1215,7 @@ impl Page {
|
||||
.build()
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
self.cdp_page.execute(params).await?;
|
||||
|
||||
|
||||
// Natural typing delay with slight randomization
|
||||
let delay = 50 + (rand::random::<u64>() % 30);
|
||||
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||||
@@ -1202,7 +1228,7 @@ impl Page {
|
||||
.chunks(chunk_size)
|
||||
.map(|chunk| chunk.iter().collect())
|
||||
.collect();
|
||||
|
||||
|
||||
for chunk in chunks {
|
||||
// Type each character in the chunk
|
||||
for ch in chunk.chars() {
|
||||
@@ -1213,7 +1239,7 @@ impl Page {
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
self.cdp_page.execute(params).await?;
|
||||
}
|
||||
|
||||
|
||||
// Delay between chunks
|
||||
let delay = 100 + (rand::random::<u64>() % 50);
|
||||
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||||
@@ -1226,7 +1252,7 @@ impl Page {
|
||||
.chunks(chunk_size)
|
||||
.map(|chunk| chunk.iter().collect())
|
||||
.collect();
|
||||
|
||||
|
||||
for (i, chunk) in chunks.iter().enumerate() {
|
||||
// Type each character in the chunk
|
||||
for ch in chunk.chars() {
|
||||
@@ -1237,7 +1263,7 @@ impl Page {
|
||||
.map_err(|e| BrowserError::CdpError(e))?;
|
||||
self.cdp_page.execute(params).await?;
|
||||
}
|
||||
|
||||
|
||||
// Add occasional longer pauses to simulate thinking
|
||||
if i % 5 == 0 && i > 0 {
|
||||
let delay = 300 + (rand::random::<u64>() % 200);
|
||||
@@ -1248,7 +1274,7 @@ impl Page {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fast build script for local development - optimized for speed
|
||||
set -euo pipefail
|
||||
|
||||
# Use dev-fast profile by default for quick iteration
|
||||
# Can override with: PROFILE=release ./build-fast.sh
|
||||
PROFILE="${PROFILE:-dev-fast}"
|
||||
|
||||
# Determine the correct binary path based on profile
|
||||
if [ "$PROFILE" = "dev-fast" ]; then
|
||||
BIN_PATH="./target/dev-fast/coder"
|
||||
elif [ "$PROFILE" = "dev" ]; then
|
||||
BIN_PATH="./target/debug/coder"
|
||||
else
|
||||
BIN_PATH="./target/${PROFILE}/coder"
|
||||
fi
|
||||
|
||||
echo "Building coder binary (${PROFILE} mode)..."
|
||||
|
||||
# Build for native target (no --target flag) for maximum speed
|
||||
# This reuses the host stdlib and normal cache
|
||||
cargo build --profile "${PROFILE}" --bin coder
|
||||
|
||||
# Check if build succeeded
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Build successful!"
|
||||
echo "Binary location: ${BIN_PATH}"
|
||||
|
||||
# Keep old symlink locations working for compatibility
|
||||
# Create symlink in target/release for npm wrapper expectations
|
||||
mkdir -p ./target/release
|
||||
if [ -e "./target/release/coder" ]; then
|
||||
rm -f ./target/release/coder
|
||||
fi
|
||||
ln -sf "../${PROFILE}/coder" "./target/release/coder"
|
||||
|
||||
# Update the symlink in codex-cli/bin for npm wrapper
|
||||
CODEX_CLI_BIN="./codex-cli/bin/coder-aarch64-apple-darwin"
|
||||
mkdir -p "$(dirname "$CODEX_CLI_BIN")"
|
||||
if [ -e "$CODEX_CLI_BIN" ]; then
|
||||
rm -f "$CODEX_CLI_BIN"
|
||||
fi
|
||||
# Create relative symlink from codex-cli/bin to the actual binary
|
||||
ln -sf "../../target/${PROFILE}/coder" "$CODEX_CLI_BIN"
|
||||
|
||||
echo "✅ Symlinks updated"
|
||||
echo ""
|
||||
echo "You can now run: coder"
|
||||
echo "Binary size: $(du -h ${BIN_PATH} | cut -f1)"
|
||||
echo ""
|
||||
echo "Build profile: ${PROFILE}"
|
||||
if [ "$PROFILE" = "dev-fast" ]; then
|
||||
echo " → Optimized for fast iteration (incremental builds enabled)"
|
||||
echo " → For production build, use: PROFILE=release-prod ./build-fast.sh"
|
||||
fi
|
||||
else
|
||||
echo "❌ Build failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1 @@
|
||||
../../target/dev-fast/coder
|
||||
@@ -52,7 +52,9 @@ pub(crate) async fn stream_chat_completions(
|
||||
// If the message contains any images, we must use the
|
||||
// multi-modal array form supported by Chat Completions:
|
||||
// [{ type: "text", text: "..." }, { type: "image_url", image_url: { url: "data:..." } }]
|
||||
let contains_image = content.iter().any(|c| matches!(c, ContentItem::InputImage { .. }));
|
||||
let contains_image = content
|
||||
.iter()
|
||||
.any(|c| matches!(c, ContentItem::InputImage { .. }));
|
||||
|
||||
if contains_image {
|
||||
let mut parts = Vec::<serde_json::Value>::new();
|
||||
@@ -78,7 +80,8 @@ pub(crate) async fn stream_chat_completions(
|
||||
let mut text = String::new();
|
||||
for c in content {
|
||||
match c {
|
||||
ContentItem::InputText { text: t } | ContentItem::OutputText { text: t } => {
|
||||
ContentItem::InputText { text: t }
|
||||
| ContentItem::OutputText { text: t } => {
|
||||
text.push_str(t);
|
||||
}
|
||||
_ => {}
|
||||
@@ -155,7 +158,9 @@ pub(crate) async fn stream_chat_completions(
|
||||
|
||||
// Start logging the request and get a request_id to track the response
|
||||
let request_id = if let Ok(logger) = debug_logger.lock() {
|
||||
logger.start_request_log(&endpoint, &payload).unwrap_or_default()
|
||||
logger
|
||||
.start_request_log(&endpoint, &payload)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -177,10 +182,14 @@ pub(crate) async fn stream_chat_completions(
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
// Log successful response initiation
|
||||
if let Ok(logger) = debug_logger.lock() {
|
||||
let _ = logger.append_response_event(&request_id, "stream_initiated", &serde_json::json!({
|
||||
"status": "success",
|
||||
"status_code": resp.status().as_u16()
|
||||
}));
|
||||
let _ = logger.append_response_event(
|
||||
&request_id,
|
||||
"stream_initiated",
|
||||
&serde_json::json!({
|
||||
"status": "success",
|
||||
"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);
|
||||
@@ -201,10 +210,14 @@ pub(crate) async fn stream_chat_completions(
|
||||
let body = (res.text().await).unwrap_or_default();
|
||||
// Log error response
|
||||
if let Ok(logger) = debug_logger.lock() {
|
||||
let _ = logger.append_response_event(&request_id, "error", &serde_json::json!({
|
||||
"status": status.as_u16(),
|
||||
"body": body
|
||||
}));
|
||||
let _ = logger.append_response_event(
|
||||
&request_id,
|
||||
"error",
|
||||
&serde_json::json!({
|
||||
"status": status.as_u16(),
|
||||
"body": body
|
||||
}),
|
||||
);
|
||||
let _ = logger.end_request_log(&request_id);
|
||||
}
|
||||
return Err(CodexErr::UnexpectedStatus(status, body));
|
||||
@@ -229,9 +242,13 @@ pub(crate) async fn stream_chat_completions(
|
||||
if attempt > max_retries {
|
||||
// Log network error
|
||||
if let Ok(logger) = debug_logger.lock() {
|
||||
let _ = logger.append_response_event(&request_id, "network_error", &serde_json::json!({
|
||||
"error": e.to_string()
|
||||
}));
|
||||
let _ = logger.append_response_event(
|
||||
&request_id,
|
||||
"network_error",
|
||||
&serde_json::json!({
|
||||
"error": e.to_string()
|
||||
}),
|
||||
);
|
||||
let _ = logger.end_request_log(&request_id);
|
||||
}
|
||||
return Err(e.into());
|
||||
@@ -355,7 +372,7 @@ async fn process_chat_sse<S>(
|
||||
Err(_) => continue,
|
||||
};
|
||||
trace!("chat_completions received SSE chunk: {chunk:?}");
|
||||
|
||||
|
||||
// Log the SSE chunk to debug log
|
||||
if let Ok(logger) = debug_logger.lock() {
|
||||
let _ = logger.append_response_event(&request_id, "sse_event", &chunk);
|
||||
@@ -373,7 +390,7 @@ async fn process_chat_sse<S>(
|
||||
if let Some(item_id) = choice.get("item_id").and_then(|id| id.as_str()) {
|
||||
current_item_id = Some(item_id.to_string());
|
||||
}
|
||||
|
||||
|
||||
// Handle assistant content tokens as streaming deltas.
|
||||
if let Some(content) = choice
|
||||
.get("delta")
|
||||
@@ -525,7 +542,7 @@ async fn process_chat_sse<S>(
|
||||
if let Ok(logger) = debug_logger.lock() {
|
||||
let _ = logger.end_request_log(&request_id);
|
||||
}
|
||||
|
||||
|
||||
return; // End processing for this SSE stream.
|
||||
}
|
||||
}
|
||||
@@ -593,7 +610,8 @@ where
|
||||
// seen any deltas; otherwise, deltas already built the
|
||||
// cumulative text and this would duplicate it.
|
||||
if this.cumulative.is_empty() {
|
||||
if let crate::models::ResponseItem::Message { content, id, .. } = &item {
|
||||
if let crate::models::ResponseItem::Message { content, id, .. } = &item
|
||||
{
|
||||
// Capture the item_id if present
|
||||
if let Some(item_id) = id {
|
||||
this.cumulative_item_id = Some(item_id.clone());
|
||||
@@ -610,7 +628,7 @@ where
|
||||
// Swallow assistant message here; emit on Completed.
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Also capture item_id from Reasoning items
|
||||
if let crate::models::ResponseItem::Reasoning { id, .. } = &item {
|
||||
if !id.is_empty() {
|
||||
@@ -691,7 +709,10 @@ where
|
||||
}
|
||||
if matches!(this.mode, AggregateMode::Streaming) {
|
||||
// In streaming mode, also forward the delta immediately.
|
||||
return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta { delta, item_id })));
|
||||
return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta {
|
||||
delta,
|
||||
item_id,
|
||||
})));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -705,7 +726,10 @@ where
|
||||
}
|
||||
if matches!(this.mode, AggregateMode::Streaming) {
|
||||
// In streaming mode, also forward the delta immediately.
|
||||
return Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta { delta, item_id })));
|
||||
return Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta {
|
||||
delta,
|
||||
item_id,
|
||||
})));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
+52
-33
@@ -31,6 +31,7 @@ use crate::client_common::create_reasoning_param_for_request;
|
||||
use crate::config::Config;
|
||||
use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use crate::debug_logger::DebugLogger;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result;
|
||||
use crate::error::UsageLimitReachedError;
|
||||
@@ -44,7 +45,6 @@ use crate::user_agent::get_codex_user_agent;
|
||||
use crate::util::backoff;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use crate::debug_logger::DebugLogger;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ErrorResponse {
|
||||
@@ -192,15 +192,13 @@ impl ModelClient {
|
||||
let max_retries = self.provider.request_max_retries();
|
||||
|
||||
let endpoint = self.provider.get_full_url(&auth);
|
||||
trace!(
|
||||
"POST to {}: {}",
|
||||
endpoint,
|
||||
serde_json::to_string(&payload)?
|
||||
);
|
||||
trace!("POST to {}: {}", endpoint, serde_json::to_string(&payload)?);
|
||||
|
||||
// Start logging the request and get a request_id to track the response
|
||||
let request_id = if let Ok(logger) = self.debug_logger.lock() {
|
||||
logger.start_request_log(&endpoint, &serde_json::to_value(&payload)?).unwrap_or_default()
|
||||
logger
|
||||
.start_request_log(&endpoint, &serde_json::to_value(&payload)?)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -252,14 +250,18 @@ impl ModelClient {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
// Log successful response initiation
|
||||
if let Ok(logger) = self.debug_logger.lock() {
|
||||
let _ = logger.append_response_event(&request_id, "stream_initiated", &serde_json::json!({
|
||||
"status": "success",
|
||||
"status_code": resp.status().as_u16(),
|
||||
"x_request_id": resp.headers()
|
||||
.get("x-request-id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or_default()
|
||||
}));
|
||||
let _ = logger.append_response_event(
|
||||
&request_id,
|
||||
"stream_initiated",
|
||||
&serde_json::json!({
|
||||
"status": "success",
|
||||
"status_code": resp.status().as_u16(),
|
||||
"x_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);
|
||||
|
||||
@@ -299,10 +301,14 @@ impl ModelClient {
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
// Log error response
|
||||
if let Ok(logger) = self.debug_logger.lock() {
|
||||
let _ = logger.append_response_event(&request_id, "error", &serde_json::json!({
|
||||
"status": status.as_u16(),
|
||||
"body": body
|
||||
}));
|
||||
let _ = logger.append_response_event(
|
||||
&request_id,
|
||||
"error",
|
||||
&serde_json::json!({
|
||||
"status": status.as_u16(),
|
||||
"body": body
|
||||
}),
|
||||
);
|
||||
let _ = logger.end_request_log(&request_id);
|
||||
}
|
||||
return Err(CodexErr::UnexpectedStatus(status, body));
|
||||
@@ -424,6 +430,7 @@ async fn process_sse<S>(
|
||||
// If the stream stays completely silent for an extended period treat it as disconnected.
|
||||
// The response id returned from the "complete" message.
|
||||
let mut response_completed: Option<ResponseCompleted> = None;
|
||||
let mut response_error: Option<CodexErr> = None;
|
||||
// Track the current item_id to include with delta events
|
||||
let mut current_item_id: Option<String> = None;
|
||||
|
||||
@@ -523,9 +530,9 @@ async fn process_sse<S>(
|
||||
} else {
|
||||
// Check within the parsed item structure
|
||||
match &item {
|
||||
ResponseItem::Message { id, .. } |
|
||||
ResponseItem::FunctionCall { id, .. } |
|
||||
ResponseItem::LocalShellCall { id, .. } => {
|
||||
ResponseItem::Message { id, .. }
|
||||
| ResponseItem::FunctionCall { id, .. }
|
||||
| ResponseItem::LocalShellCall { id, .. } => {
|
||||
if let Some(item_id) = id {
|
||||
current_item_id = Some(item_id.clone());
|
||||
}
|
||||
@@ -544,9 +551,9 @@ async fn process_sse<S>(
|
||||
}
|
||||
"response.output_text.delta" => {
|
||||
if let Some(delta) = event.delta {
|
||||
let event = ResponseEvent::OutputTextDelta {
|
||||
delta,
|
||||
item_id: current_item_id.clone()
|
||||
let event = ResponseEvent::OutputTextDelta {
|
||||
delta,
|
||||
item_id: current_item_id.clone(),
|
||||
};
|
||||
if tx_event.send(Ok(event)).await.is_err() {
|
||||
return;
|
||||
@@ -555,9 +562,9 @@ async fn process_sse<S>(
|
||||
}
|
||||
"response.reasoning_summary_text.delta" => {
|
||||
if let Some(delta) = event.delta {
|
||||
let event = ResponseEvent::ReasoningSummaryDelta {
|
||||
delta,
|
||||
item_id: current_item_id.clone()
|
||||
let event = ResponseEvent::ReasoningSummaryDelta {
|
||||
delta,
|
||||
item_id: current_item_id.clone(),
|
||||
};
|
||||
if tx_event.send(Ok(event)).await.is_err() {
|
||||
return;
|
||||
@@ -566,9 +573,9 @@ async fn process_sse<S>(
|
||||
}
|
||||
"response.reasoning_text.delta" => {
|
||||
if let Some(delta) = event.delta {
|
||||
let event = ResponseEvent::ReasoningContentDelta {
|
||||
delta,
|
||||
item_id: current_item_id.clone()
|
||||
let event = ResponseEvent::ReasoningContentDelta {
|
||||
delta,
|
||||
item_id: current_item_id.clone(),
|
||||
};
|
||||
if tx_event.send(Ok(event)).await.is_err() {
|
||||
return;
|
||||
@@ -731,7 +738,13 @@ mod tests {
|
||||
let stream = ReaderStream::new(reader).map_err(CodexErr::Io);
|
||||
let (tx, mut rx) = mpsc::channel::<Result<ResponseEvent>>(16);
|
||||
let debug_logger = Arc::new(Mutex::new(DebugLogger::new(false).unwrap()));
|
||||
tokio::spawn(process_sse(stream, tx, provider.stream_idle_timeout(), debug_logger, String::new()));
|
||||
tokio::spawn(process_sse(
|
||||
stream,
|
||||
tx,
|
||||
provider.stream_idle_timeout(),
|
||||
debug_logger,
|
||||
String::new(),
|
||||
));
|
||||
|
||||
let mut events = Vec::new();
|
||||
while let Some(ev) = rx.recv().await {
|
||||
@@ -762,7 +775,13 @@ mod tests {
|
||||
let (tx, mut rx) = mpsc::channel::<Result<ResponseEvent>>(8);
|
||||
let stream = ReaderStream::new(std::io::Cursor::new(body)).map_err(CodexErr::Io);
|
||||
let debug_logger = Arc::new(Mutex::new(DebugLogger::new(false).unwrap()));
|
||||
tokio::spawn(process_sse(stream, tx, provider.stream_idle_timeout(), debug_logger, String::new()));
|
||||
tokio::spawn(process_sse(
|
||||
stream,
|
||||
tx,
|
||||
provider.stream_idle_timeout(),
|
||||
debug_logger,
|
||||
String::new(),
|
||||
));
|
||||
|
||||
let mut out = Vec::new();
|
||||
while let Some(ev) = rx.recv().await {
|
||||
|
||||
@@ -120,11 +120,14 @@ impl Prompt {
|
||||
}
|
||||
|
||||
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
|
||||
let mut input_with_instructions = Vec::with_capacity(self.input.len() + self.status_items.len() + 3);
|
||||
let mut input_with_instructions =
|
||||
Vec::with_capacity(self.input.len() + self.status_items.len() + 3);
|
||||
input_with_instructions.push(ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
content: vec![ContentItem::InputText { text: ADDITIONAL_INSTRUCTIONS.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 {
|
||||
@@ -147,7 +150,10 @@ impl Prompt {
|
||||
ResponseItem::FunctionCallOutput { call_id, .. } => {
|
||||
if !seen_call_ids.insert(call_id.clone()) {
|
||||
// Skip duplicate function call output
|
||||
tracing::debug!("Filtering duplicate FunctionCallOutput with call_id: {} from input", call_id);
|
||||
tracing::debug!(
|
||||
"Filtering duplicate FunctionCallOutput with call_id: {} from input",
|
||||
call_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -155,7 +161,7 @@ impl Prompt {
|
||||
}
|
||||
input_with_instructions.push(item.clone());
|
||||
}
|
||||
|
||||
|
||||
// Add status items at the end so they're fresh for each request
|
||||
input_with_instructions.extend(self.status_items.clone());
|
||||
input_with_instructions
|
||||
@@ -170,9 +176,18 @@ pub enum ResponseEvent {
|
||||
response_id: String,
|
||||
token_usage: Option<TokenUsage>,
|
||||
},
|
||||
OutputTextDelta { delta: String, item_id: Option<String> },
|
||||
ReasoningSummaryDelta { delta: String, item_id: Option<String> },
|
||||
ReasoningContentDelta { delta: String, item_id: Option<String> },
|
||||
OutputTextDelta {
|
||||
delta: String,
|
||||
item_id: Option<String>,
|
||||
},
|
||||
ReasoningSummaryDelta {
|
||||
delta: String,
|
||||
item_id: Option<String>,
|
||||
},
|
||||
ReasoningContentDelta {
|
||||
delta: String,
|
||||
item_id: Option<String>,
|
||||
},
|
||||
ReasoningSummaryPartAdded,
|
||||
}
|
||||
|
||||
|
||||
+391
-334
File diff suppressed because it is too large
Load Diff
@@ -163,7 +163,7 @@ 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,
|
||||
}
|
||||
|
||||
@@ -42,7 +42,12 @@ impl ConversationHistory {
|
||||
// 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] {
|
||||
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());
|
||||
@@ -55,11 +60,18 @@ impl ConversationHistory {
|
||||
|
||||
// 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 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); }
|
||||
if let Some(id) = new_id.clone() {
|
||||
*last_id = Some(id);
|
||||
}
|
||||
}
|
||||
replace_or_append_text(last_content, new_content);
|
||||
continue;
|
||||
@@ -74,11 +86,17 @@ impl ConversationHistory {
|
||||
let already_exists = self.items.iter().any(|existing| {
|
||||
matches!(existing, ResponseItem::FunctionCallOutput { call_id: existing_id, .. } if existing_id == call_id)
|
||||
});
|
||||
|
||||
|
||||
if already_exists {
|
||||
debug!("Skipping duplicate FunctionCallOutput for call_id: {} (already in history)", call_id);
|
||||
debug!(
|
||||
"Skipping duplicate FunctionCallOutput for call_id: {} (already in history)",
|
||||
call_id
|
||||
);
|
||||
} else {
|
||||
debug!("Recording FunctionCallOutput to history for call_id: {}", call_id);
|
||||
debug!(
|
||||
"Recording FunctionCallOutput to history for call_id: {}",
|
||||
call_id
|
||||
);
|
||||
self.items.push(item.clone());
|
||||
}
|
||||
}
|
||||
@@ -87,9 +105,12 @@ impl ConversationHistory {
|
||||
let already_exists = self.items.iter().any(|existing| {
|
||||
matches!(existing, ResponseItem::FunctionCall { call_id: existing_id, .. } if existing_id == call_id)
|
||||
});
|
||||
|
||||
|
||||
if already_exists {
|
||||
debug!("Skipping duplicate FunctionCall for call_id: {} (already in history)", call_id);
|
||||
debug!(
|
||||
"Skipping duplicate FunctionCall for call_id: {} (already in history)",
|
||||
call_id
|
||||
);
|
||||
} else {
|
||||
self.items.push(item.clone());
|
||||
}
|
||||
@@ -101,7 +122,6 @@ impl ConversationHistory {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn keep_last_messages(&mut self, n: usize) {
|
||||
if n == 0 {
|
||||
self.items.clear();
|
||||
@@ -192,7 +212,10 @@ fn replace_text_content(content: &mut Vec<crate::models::ContentItem>, text: &st
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
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) {
|
||||
@@ -205,7 +228,12 @@ fn replace_or_append_text(existing: &mut Vec<crate::models::ContentItem>, new_co
|
||||
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 let ResponseItem::Message {
|
||||
id: Some(existing),
|
||||
role,
|
||||
..
|
||||
} = it
|
||||
{
|
||||
if role == "assistant" && existing == id {
|
||||
return Some(idx);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use chrono::Local;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
struct StreamInfo {
|
||||
@@ -43,7 +43,11 @@ impl DebugLogger {
|
||||
}
|
||||
|
||||
/// Start a new request/response log file and return the request ID
|
||||
pub fn start_request_log(&self, endpoint: &str, payload: &Value) -> Result<String, std::io::Error> {
|
||||
pub fn start_request_log(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
payload: &Value,
|
||||
) -> Result<String, std::io::Error> {
|
||||
if !self.enabled {
|
||||
return Ok(String::new());
|
||||
}
|
||||
@@ -51,7 +55,7 @@ impl DebugLogger {
|
||||
let timestamp = Local::now();
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
let request_id_short = &request_id[..8]; // Use first 8 chars of UUID for brevity
|
||||
|
||||
|
||||
// Create request file with pretty-printed JSON
|
||||
let request_filename = format!(
|
||||
"{}_{}_request.json",
|
||||
@@ -59,7 +63,7 @@ impl DebugLogger {
|
||||
request_id_short
|
||||
);
|
||||
let request_file_path = self.log_dir.join(request_filename);
|
||||
|
||||
|
||||
// Create request object with metadata
|
||||
let request_entry = serde_json::json!({
|
||||
"timestamp": timestamp.to_rfc3339(),
|
||||
@@ -82,17 +86,25 @@ impl DebugLogger {
|
||||
|
||||
// Store the stream info for this request_id
|
||||
if let Ok(mut streams) = self.active_streams.lock() {
|
||||
streams.insert(request_id.clone(), StreamInfo {
|
||||
response_file: response_file_path,
|
||||
events: Vec::new(),
|
||||
});
|
||||
streams.insert(
|
||||
request_id.clone(),
|
||||
StreamInfo {
|
||||
response_file: response_file_path,
|
||||
events: Vec::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(request_id)
|
||||
}
|
||||
|
||||
/// Append a response event to the in-memory event list
|
||||
pub fn append_response_event(&self, request_id: &str, event_type: &str, data: &Value) -> Result<(), std::io::Error> {
|
||||
pub fn append_response_event(
|
||||
&self,
|
||||
request_id: &str,
|
||||
event_type: &str,
|
||||
data: &Value,
|
||||
) -> Result<(), std::io::Error> {
|
||||
if !self.enabled || request_id.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -143,13 +155,10 @@ impl DebugLogger {
|
||||
}
|
||||
|
||||
let timestamp = Local::now();
|
||||
let filename = format!(
|
||||
"{}_request.json",
|
||||
timestamp.format("%Y%m%d_%H%M%S%.3f")
|
||||
);
|
||||
let filename = format!("{}_request.json", timestamp.format("%Y%m%d_%H%M%S%.3f"));
|
||||
|
||||
let file_path = self.log_dir.join(filename);
|
||||
|
||||
|
||||
let log_entry = serde_json::json!({
|
||||
"timestamp": timestamp.to_rfc3339(),
|
||||
"type": "request",
|
||||
@@ -169,16 +178,13 @@ impl DebugLogger {
|
||||
}
|
||||
|
||||
let timestamp = Local::now();
|
||||
let filename = format!(
|
||||
"{}_response.json",
|
||||
timestamp.format("%Y%m%d_%H%M%S%.3f")
|
||||
);
|
||||
let filename = format!("{}_response.json", timestamp.format("%Y%m%d_%H%M%S%.3f"));
|
||||
|
||||
let file_path = self.log_dir.join(filename);
|
||||
|
||||
|
||||
let log_entry = serde_json::json!({
|
||||
"timestamp": timestamp.to_rfc3339(),
|
||||
"type": "response",
|
||||
"type": "response",
|
||||
"endpoint": endpoint,
|
||||
"response": response
|
||||
});
|
||||
@@ -195,13 +201,10 @@ impl DebugLogger {
|
||||
}
|
||||
|
||||
let timestamp = Local::now();
|
||||
let filename = format!(
|
||||
"{}_stream.txt",
|
||||
timestamp.format("%Y%m%d_%H%M%S%.3f")
|
||||
);
|
||||
let filename = format!("{}_stream.txt", timestamp.format("%Y%m%d_%H%M%S%.3f"));
|
||||
|
||||
let file_path = self.log_dir.join(filename);
|
||||
|
||||
|
||||
let log_entry = format!(
|
||||
"=== Stream Chunk at {} ===\nEndpoint: {}\n\n{}\n",
|
||||
timestamp.to_rfc3339(),
|
||||
@@ -220,13 +223,10 @@ impl DebugLogger {
|
||||
}
|
||||
|
||||
let timestamp = Local::now();
|
||||
let filename = format!(
|
||||
"{}_error.txt",
|
||||
timestamp.format("%Y%m%d_%H%M%S%.3f")
|
||||
);
|
||||
let filename = format!("{}_error.txt", timestamp.format("%Y%m%d_%H%M%S%.3f"));
|
||||
|
||||
let file_path = self.log_dir.join(filename);
|
||||
|
||||
|
||||
let log_entry = format!(
|
||||
"=== Error at {} ===\nEndpoint: {}\n\n{}\n",
|
||||
timestamp.to_rfc3339(),
|
||||
@@ -250,13 +250,10 @@ impl DebugLogger {
|
||||
}
|
||||
|
||||
let timestamp = Local::now();
|
||||
let filename = format!(
|
||||
"{}_sse.json",
|
||||
timestamp.format("%Y%m%d_%H%M%S%.3f")
|
||||
);
|
||||
let filename = format!("{}_sse.json", timestamp.format("%Y%m%d_%H%M%S%.3f"));
|
||||
|
||||
let file_path = self.log_dir.join(filename);
|
||||
|
||||
|
||||
let log_entry = serde_json::json!({
|
||||
"timestamp": timestamp.to_rfc3339(),
|
||||
"type": "sse_event",
|
||||
@@ -269,4 +266,4 @@ impl DebugLogger {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,34 +27,29 @@ pub fn compute_image_hash<P: AsRef<Path>>(path: P) -> anyhow::Result<(Vec<u8>, V
|
||||
let img = ImageReader::open(path)?.decode()?;
|
||||
let phash = phash_256(&img);
|
||||
let dhash = dhash_256(&img);
|
||||
|
||||
|
||||
Ok((phash.as_bytes().to_vec(), dhash.as_bytes().to_vec()))
|
||||
}
|
||||
|
||||
/// Compare image hashes to determine if images are similar
|
||||
pub fn are_hashes_similar(
|
||||
phash1: &[u8],
|
||||
dhash1: &[u8],
|
||||
phash2: &[u8],
|
||||
dhash2: &[u8],
|
||||
) -> bool {
|
||||
pub fn are_hashes_similar(phash1: &[u8], dhash1: &[u8], phash2: &[u8], dhash2: &[u8]) -> bool {
|
||||
if phash1.len() != 32 || dhash1.len() != 32 || phash2.len() != 32 || dhash2.len() != 32 {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Count differing bits (Hamming distance)
|
||||
let phash_dist = phash1
|
||||
.iter()
|
||||
.zip(phash2.iter())
|
||||
.map(|(a, b)| (a ^ b).count_ones() as u32)
|
||||
.sum::<u32>();
|
||||
|
||||
|
||||
let dhash_dist = dhash1
|
||||
.iter()
|
||||
.zip(dhash2.iter())
|
||||
.map(|(a, b)| (a ^ b).count_ones() as u32)
|
||||
.sum::<u32>();
|
||||
|
||||
|
||||
// 256 bits → ~5% tolerance (≈13 bits)
|
||||
phash_dist <= 13 && dhash_dist <= 13
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,16 @@ pub enum ResponseInputItem {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ContentItem {
|
||||
InputText { text: String },
|
||||
InputImage { image_url: String, detail: Option<String> },
|
||||
OutputText { text: String },
|
||||
InputText {
|
||||
text: String,
|
||||
},
|
||||
InputImage {
|
||||
image_url: String,
|
||||
detail: Option<String>,
|
||||
},
|
||||
OutputText {
|
||||
text: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -164,7 +171,10 @@ impl From<Vec<InputItem>> for ResponseInputItem {
|
||||
content_items.push(ContentItem::InputText { text });
|
||||
}
|
||||
InputItem::Image { image_url } => {
|
||||
content_items.push(ContentItem::InputImage { image_url, detail: None });
|
||||
content_items.push(ContentItem::InputImage {
|
||||
image_url,
|
||||
detail: None,
|
||||
});
|
||||
}
|
||||
InputItem::LocalImage { path } => match std::fs::read(&path) {
|
||||
Ok(bytes) => {
|
||||
|
||||
@@ -928,25 +928,35 @@ fn create_browser_move_tool() -> OpenAiTool {
|
||||
properties.insert(
|
||||
"x".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("The absolute X coordinate to move the mouse to (use with y)".to_string()),
|
||||
description: Some(
|
||||
"The absolute X coordinate to move the mouse to (use with y)".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
"y".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("The absolute Y coordinate to move the mouse to (use with x)".to_string()),
|
||||
description: Some(
|
||||
"The absolute Y coordinate to move the mouse to (use with x)".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
"dx".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("Relative (+/-) X movement in CSS pixels from current mouse position (use with dy)".to_string()),
|
||||
description: Some(
|
||||
"Relative (+/-) X movement in CSS pixels from current mouse position (use with dy)"
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
"dy".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("Relative (+/-) Y movement in CSS pixels from current mouse position (use with dx)".to_string()),
|
||||
description: Some(
|
||||
"Relative (+/-) Y movement in CSS pixels from current mouse position (use with dx)"
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ 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,
|
||||
|
||||
@@ -378,6 +378,7 @@ fn derive_config_from_params(
|
||||
include_plan_tool,
|
||||
disable_response_storage: None,
|
||||
show_raw_agent_reasoning: None,
|
||||
debug: None,
|
||||
};
|
||||
|
||||
let cli_overrides = cli_overrides
|
||||
|
||||
+142
-141
@@ -60,7 +60,7 @@ pub(crate) struct App<'a> {
|
||||
pending_redraw: Arc<AtomicBool>,
|
||||
|
||||
enhanced_keys_supported: bool,
|
||||
|
||||
|
||||
/// Debug flag for logging LLM requests/responses
|
||||
_debug: bool,
|
||||
|
||||
@@ -368,156 +368,157 @@ impl App<'_> {
|
||||
command_text.clone()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
match command {
|
||||
SlashCommand::New => {
|
||||
// User accepted – switch to chat view.
|
||||
let new_widget = Box::new(ChatWidget::new(
|
||||
self.config.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
None,
|
||||
Vec::new(),
|
||||
self.enhanced_keys_supported,
|
||||
));
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
SlashCommand::Init => {
|
||||
// Guard: do not run if a task is active.
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
const INIT_PROMPT: &str = include_str!("../prompt_for_init_command.md");
|
||||
widget.submit_text_message(INIT_PROMPT.to_string());
|
||||
SlashCommand::New => {
|
||||
// User accepted – switch to chat view.
|
||||
let new_widget = Box::new(ChatWidget::new(
|
||||
self.config.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
None,
|
||||
Vec::new(),
|
||||
self.enhanced_keys_supported,
|
||||
));
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
}
|
||||
SlashCommand::Compact => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.clear_token_usage();
|
||||
self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
|
||||
SlashCommand::Init => {
|
||||
// Guard: do not run if a task is active.
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
const INIT_PROMPT: &str =
|
||||
include_str!("../prompt_for_init_command.md");
|
||||
widget.submit_text_message(INIT_PROMPT.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Quit => {
|
||||
break;
|
||||
}
|
||||
SlashCommand::Logout => {
|
||||
if let Err(e) = codex_login::logout(&self.config.codex_home) {
|
||||
tracing::error!("failed to logout: {e}");
|
||||
SlashCommand::Compact => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.clear_token_usage();
|
||||
self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
SlashCommand::Diff => {
|
||||
let (is_git_repo, diff_text) = match get_git_diff() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
let msg = format!("Failed to compute diff: {e}");
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.add_diff_output(msg);
|
||||
SlashCommand::Quit => {
|
||||
break;
|
||||
}
|
||||
SlashCommand::Logout => {
|
||||
if let Err(e) = codex_login::logout(&self.config.codex_home) {
|
||||
tracing::error!("failed to logout: {e}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
SlashCommand::Diff => {
|
||||
let (is_git_repo, diff_text) = match get_git_diff() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
let msg = format!("Failed to compute diff: {e}");
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.add_diff_output(msg);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
let text = if is_git_repo {
|
||||
diff_text
|
||||
} else {
|
||||
"`/diff` — _not inside a git repository_".to_string()
|
||||
};
|
||||
widget.add_diff_output(text);
|
||||
}
|
||||
}
|
||||
SlashCommand::Mention => {
|
||||
// The mention feature is handled differently in our fork
|
||||
// For now, just add @ to the composer
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.insert_str("@");
|
||||
}
|
||||
}
|
||||
SlashCommand::Status => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.add_status_output();
|
||||
}
|
||||
}
|
||||
SlashCommand::Reasoning => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_reasoning_command(command_args);
|
||||
}
|
||||
}
|
||||
SlashCommand::Theme => {
|
||||
// Theme selection is handled in submit_user_message
|
||||
// This case is here for completeness
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.show_theme_selection();
|
||||
}
|
||||
}
|
||||
SlashCommand::Prompts => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.add_prompts_output();
|
||||
}
|
||||
}
|
||||
// Prompt-expanding commands should have been handled in submit_user_message
|
||||
// but add a fallback just in case
|
||||
SlashCommand::Plan | SlashCommand::Solve | SlashCommand::Code => {
|
||||
// These should have been expanded already, but handle them anyway
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
let expanded = command.expand_prompt(&command_text);
|
||||
if let Some(prompt) = expanded {
|
||||
widget.submit_text_message(prompt);
|
||||
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
let text = if is_git_repo {
|
||||
diff_text
|
||||
} else {
|
||||
"`/diff` — _not inside a git repository_".to_string()
|
||||
};
|
||||
widget.add_diff_output(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Browser => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_browser_command(command_args);
|
||||
SlashCommand::Mention => {
|
||||
// The mention feature is handled differently in our fork
|
||||
// For now, just add @ to the composer
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.insert_str("@");
|
||||
}
|
||||
}
|
||||
SlashCommand::Status => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.add_status_output();
|
||||
}
|
||||
}
|
||||
SlashCommand::Reasoning => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_reasoning_command(command_args);
|
||||
}
|
||||
}
|
||||
SlashCommand::Theme => {
|
||||
// Theme selection is handled in submit_user_message
|
||||
// This case is here for completeness
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.show_theme_selection();
|
||||
}
|
||||
}
|
||||
SlashCommand::Prompts => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.add_prompts_output();
|
||||
}
|
||||
}
|
||||
// Prompt-expanding commands should have been handled in submit_user_message
|
||||
// but add a fallback just in case
|
||||
SlashCommand::Plan | SlashCommand::Solve | SlashCommand::Code => {
|
||||
// These should have been expanded already, but handle them anyway
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
let expanded = command.expand_prompt(&command_text);
|
||||
if let Some(prompt) = expanded {
|
||||
widget.submit_text_message(prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Browser => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.handle_browser_command(command_args);
|
||||
}
|
||||
}
|
||||
SlashCommand::Chrome => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
// Call the dedicated chrome command handler
|
||||
widget.handle_chrome_command(command_args);
|
||||
}
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
SlashCommand::TestApproval => {
|
||||
use codex_core::protocol::EventMsg;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_core::protocol::FileChange;
|
||||
|
||||
self.app_event_tx.send(AppEvent::CodexEvent(Event {
|
||||
id: "1".to_string(),
|
||||
// msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
// call_id: "1".to_string(),
|
||||
// command: vec!["git".into(), "apply".into()],
|
||||
// cwd: self.config.cwd.clone(),
|
||||
// reason: Some("test".to_string()),
|
||||
// }),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "1".to_string(),
|
||||
changes: HashMap::from([
|
||||
(
|
||||
PathBuf::from("/tmp/test.txt"),
|
||||
FileChange::Add {
|
||||
content: "test".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
PathBuf::from("/tmp/test2.txt"),
|
||||
FileChange::Update {
|
||||
unified_diff: "+test\n-test2".to_string(),
|
||||
move_path: None,
|
||||
},
|
||||
),
|
||||
]),
|
||||
reason: None,
|
||||
grant_root: Some(PathBuf::from("/tmp")),
|
||||
},
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
SlashCommand::Chrome => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
// Call the dedicated chrome command handler
|
||||
widget.handle_chrome_command(command_args);
|
||||
}
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
SlashCommand::TestApproval => {
|
||||
use codex_core::protocol::EventMsg;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_core::protocol::FileChange;
|
||||
|
||||
self.app_event_tx.send(AppEvent::CodexEvent(Event {
|
||||
id: "1".to_string(),
|
||||
// msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
// call_id: "1".to_string(),
|
||||
// command: vec!["git".into(), "apply".into()],
|
||||
// cwd: self.config.cwd.clone(),
|
||||
// reason: Some("test".to_string()),
|
||||
// }),
|
||||
msg: EventMsg::ApplyPatchApprovalRequest(
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: "1".to_string(),
|
||||
changes: HashMap::from([
|
||||
(
|
||||
PathBuf::from("/tmp/test.txt"),
|
||||
FileChange::Add {
|
||||
content: "test".to_string(),
|
||||
},
|
||||
),
|
||||
(
|
||||
PathBuf::from("/tmp/test2.txt"),
|
||||
FileChange::Update {
|
||||
unified_diff: "+test\n-test2".to_string(),
|
||||
move_path: None,
|
||||
},
|
||||
),
|
||||
]),
|
||||
reason: None,
|
||||
grant_root: Some(PathBuf::from("/tmp")),
|
||||
},
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
AppEvent::PrepareAgents => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.prepare_agents();
|
||||
|
||||
@@ -78,10 +78,10 @@ pub(crate) enum AppEvent {
|
||||
/// Onboarding: result of login_with_chatgpt.
|
||||
OnboardingAuthComplete(Result<(), String>),
|
||||
OnboardingComplete(ChatWidgetArgs),
|
||||
|
||||
|
||||
/// Show Chrome launch options dialog
|
||||
ShowChromeOptions(Option<u16>),
|
||||
|
||||
|
||||
/// Chrome launch option selected by user
|
||||
ChromeLaunchOptionSelected(ChromeLaunchOption, Option<u16>),
|
||||
}
|
||||
|
||||
@@ -979,8 +979,10 @@ impl WidgetRef for &ChatComposer {
|
||||
|
||||
// Mix bits so low bits aren't parity-biased
|
||||
fn mix(mut x: u64) -> u64 {
|
||||
x ^= x >> 30; x = x.wrapping_mul(0xbf58476d1ce4e5b9);
|
||||
x ^= x >> 27; x = x.wrapping_mul(0x94d049bb133111eb);
|
||||
x ^= x >> 30;
|
||||
x = x.wrapping_mul(0xbf58476d1ce4e5b9);
|
||||
x ^= x >> 27;
|
||||
x = x.wrapping_mul(0x94d049bb133111eb);
|
||||
x ^ (x >> 31)
|
||||
}
|
||||
|
||||
@@ -992,7 +994,7 @@ impl WidgetRef for &ChatComposer {
|
||||
let selected_spinner: &[char] = if r < 2 {
|
||||
&['✨']
|
||||
} else if r < 101 {
|
||||
&['✧','✦','✧']
|
||||
&['✧', '✦', '✧']
|
||||
} else {
|
||||
match r % 3 {
|
||||
0 => &['✶'],
|
||||
@@ -1004,7 +1006,8 @@ impl WidgetRef for &ChatComposer {
|
||||
let frame_idx = (SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() / 150) as usize;
|
||||
.as_millis()
|
||||
/ 150) as usize;
|
||||
|
||||
let spinner = selected_spinner[frame_idx % selected_spinner.len()];
|
||||
|
||||
|
||||
@@ -86,19 +86,21 @@ impl ChromeSelectionView {
|
||||
fn confirm_selection(&mut self) {
|
||||
let options = Self::get_options();
|
||||
let selected = options[self.selected_index].0;
|
||||
|
||||
|
||||
// Send the selected option event
|
||||
self.app_event_tx
|
||||
.send(AppEvent::ChromeLaunchOptionSelected(selected, self.port));
|
||||
|
||||
|
||||
self.is_complete = true;
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {
|
||||
// Send cancel event
|
||||
self.app_event_tx
|
||||
.send(AppEvent::ChromeLaunchOptionSelected(ChromeLaunchOption::Cancel, self.port));
|
||||
|
||||
self.app_event_tx.send(AppEvent::ChromeLaunchOptionSelected(
|
||||
ChromeLaunchOption::Cancel,
|
||||
self.port,
|
||||
));
|
||||
|
||||
self.is_complete = true;
|
||||
}
|
||||
}
|
||||
@@ -142,14 +144,14 @@ impl<'a> BottomPaneView<'a> for ChromeSelectionView {
|
||||
block.render(area, buf);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
|
||||
// Add header
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
"Chrome is already running or CDP connection failed",
|
||||
Style::default().fg(crate::colors::warning()).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
"Chrome is already running or CDP connection failed",
|
||||
Style::default()
|
||||
.fg(crate::colors::warning())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)]));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from("Select an option:"));
|
||||
lines.push(Line::from(""));
|
||||
@@ -158,37 +160,29 @@ impl<'a> BottomPaneView<'a> for ChromeSelectionView {
|
||||
let options = Self::get_options();
|
||||
for (i, (_, label, description)) in options.iter().enumerate() {
|
||||
let is_selected = i == self.selected_index;
|
||||
|
||||
|
||||
if is_selected {
|
||||
// Highlighted option
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
format!("▶ {}", label),
|
||||
Style::default()
|
||||
.fg(crate::colors::success())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {}", description),
|
||||
Style::default().fg(crate::colors::secondary()),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!("▶ {}", label),
|
||||
Style::default()
|
||||
.fg(crate::colors::success())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!(" {}", description),
|
||||
Style::default().fg(crate::colors::secondary()),
|
||||
)]));
|
||||
} else {
|
||||
// Non-selected option
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {}", label),
|
||||
Style::default().fg(crate::colors::text()),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {}", description),
|
||||
Style::default().fg(crate::colors::text_dim()),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!(" {}", label),
|
||||
Style::default().fg(crate::colors::text()),
|
||||
)]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!(" {}", description),
|
||||
Style::default().fg(crate::colors::text_dim()),
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,12 +202,11 @@ impl<'a> BottomPaneView<'a> for ChromeSelectionView {
|
||||
),
|
||||
]));
|
||||
|
||||
let paragraph = Paragraph::new(lines)
|
||||
.alignment(Alignment::Left);
|
||||
let paragraph = Paragraph::new(lines).alignment(Alignment::Left);
|
||||
paragraph.render(inner, buf);
|
||||
}
|
||||
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
20 // Fixed height for the selection dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ impl BottomPane<'_> {
|
||||
self.status_view_active = false;
|
||||
self.request_redraw()
|
||||
}
|
||||
|
||||
|
||||
/// Show the Chrome launch options UI
|
||||
pub fn show_chrome_selection(&mut self, port: Option<u16>) {
|
||||
use chrome_selection_view::ChromeSelectionView;
|
||||
|
||||
+168
-122
@@ -5,9 +5,9 @@ use std::sync::Mutex;
|
||||
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_types::ReasoningEffort;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::parse_command::ParsedCommand;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
@@ -19,6 +19,8 @@ use codex_core::protocol::AgentStatusUpdateEvent;
|
||||
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_core::protocol::BackgroundEventEvent;
|
||||
use codex_core::protocol::BrowserScreenshotUpdateEvent;
|
||||
use codex_core::protocol::CustomToolCallBeginEvent;
|
||||
use codex_core::protocol::CustomToolCallEndEvent;
|
||||
use codex_core::protocol::ErrorEvent;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
@@ -26,8 +28,6 @@ use codex_core::protocol::ExecApprovalRequestEvent;
|
||||
use codex_core::protocol::ExecCommandBeginEvent;
|
||||
use codex_core::protocol::ExecCommandEndEvent;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::CustomToolCallBeginEvent;
|
||||
use codex_core::protocol::CustomToolCallEndEvent;
|
||||
use codex_core::protocol::McpToolCallBeginEvent;
|
||||
use codex_core::protocol::McpToolCallEndEvent;
|
||||
use codex_core::protocol::Op;
|
||||
@@ -432,7 +432,10 @@ impl ChatWidget<'_> {
|
||||
tokio::spawn(async move {
|
||||
// Use ConversationManager to properly handle authentication
|
||||
let conversation_manager = ConversationManager::default();
|
||||
let new_conversation = match conversation_manager.new_conversation(config_for_agent_loop).await {
|
||||
let new_conversation = match conversation_manager
|
||||
.new_conversation(config_for_agent_loop)
|
||||
.await
|
||||
{
|
||||
Ok(conv) => conv,
|
||||
Err(e) => {
|
||||
// TODO: surface this error to the user.
|
||||
@@ -680,13 +683,17 @@ impl ChatWidget<'_> {
|
||||
// Check for duplicate text lines to prevent adding the same content twice
|
||||
if let Some(last_cell) = self.history_cells.last() {
|
||||
// If both are text lines with identical content, skip adding the duplicate
|
||||
if let (HistoryCell::StyledText { view: last_view }, HistoryCell::StyledText { view: new_view }) = (last_cell, &cell) {
|
||||
if let (
|
||||
HistoryCell::StyledText { view: last_view },
|
||||
HistoryCell::StyledText { view: new_view },
|
||||
) = (last_cell, &cell)
|
||||
{
|
||||
if last_view.lines == new_view.lines {
|
||||
return; // Skip duplicate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If this is the first user prompt, start fade-out animation for AnimatedWelcome
|
||||
if matches!(cell, HistoryCell::UserPrompt { .. }) {
|
||||
let has_existing_user_prompts = self
|
||||
@@ -820,9 +827,10 @@ impl ChatWidget<'_> {
|
||||
// Add timeout to screenshot capture
|
||||
let capture_result = tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(10),
|
||||
browser_manager.capture_screenshot_with_url()
|
||||
).await;
|
||||
|
||||
browser_manager.capture_screenshot_with_url(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match capture_result {
|
||||
Ok(Ok((screenshot_paths, url))) => {
|
||||
tracing::info!(
|
||||
@@ -892,9 +900,12 @@ impl ChatWidget<'_> {
|
||||
"Screenshot capture timed out on attempt {} - browser may be disconnected",
|
||||
attempts
|
||||
);
|
||||
|
||||
|
||||
if attempts >= max_attempts {
|
||||
tracing::error!("Giving up after {} timeout attempts", max_attempts);
|
||||
tracing::error!(
|
||||
"Giving up after {} timeout attempts",
|
||||
max_attempts
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
// Try to reconnect the browser before next attempt
|
||||
@@ -902,13 +913,13 @@ impl ChatWidget<'_> {
|
||||
if let Err(e) = browser_manager.close().await {
|
||||
tracing::warn!("Error closing browser: {}", e);
|
||||
}
|
||||
|
||||
|
||||
// Wait a bit before reconnection
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
|
||||
// Browser will auto-reconnect on next capture attempt
|
||||
tracing::info!("Will retry screenshot capture after reconnection");
|
||||
|
||||
|
||||
// Exponential backoff
|
||||
let wait_time =
|
||||
std::time::Duration::from_millis(1000 * (1 << (attempts - 1)));
|
||||
@@ -1057,7 +1068,7 @@ impl ChatWidget<'_> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback: Check if we've already processed this message (duplicate detection)
|
||||
let message_trimmed = message.trim();
|
||||
if !message_trimmed.is_empty() {
|
||||
@@ -1068,9 +1079,11 @@ impl ChatWidget<'_> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Also check against the current answer_buffer
|
||||
if !self.answer_buffer.is_empty() && self.answer_buffer.trim() == message_trimmed {
|
||||
if !self.answer_buffer.is_empty()
|
||||
&& self.answer_buffer.trim() == message_trimmed
|
||||
{
|
||||
// We've already streamed this content via deltas
|
||||
// Just finalize the stream without adding duplicate content
|
||||
if self.current_stream == Some(StreamKind::Answer) {
|
||||
@@ -1083,7 +1096,7 @@ impl ChatWidget<'_> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// AgentMessage: if no deltas were streamed, render the final text once.
|
||||
if self.current_stream != Some(StreamKind::Answer) && !message.is_empty() {
|
||||
self.begin_stream(StreamKind::Answer);
|
||||
@@ -1144,10 +1157,11 @@ impl ChatWidget<'_> {
|
||||
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 {
|
||||
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) {
|
||||
@@ -1158,7 +1172,7 @@ impl ChatWidget<'_> {
|
||||
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);
|
||||
@@ -1202,10 +1216,11 @@ impl ChatWidget<'_> {
|
||||
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 {
|
||||
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) {
|
||||
@@ -1216,7 +1231,7 @@ impl ChatWidget<'_> {
|
||||
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);
|
||||
@@ -1262,7 +1277,7 @@ impl ChatWidget<'_> {
|
||||
self.bottom_pane.set_task_running(false);
|
||||
self.bottom_pane.clear_live_ring();
|
||||
// Reset with max width to disable wrapping
|
||||
self.live_builder = RowBuilder::new(usize::MAX);
|
||||
self.live_builder = RowBuilder::new(usize::MAX);
|
||||
self.current_stream = None;
|
||||
self.stream_header_emitted = false;
|
||||
self.answer_buffer.clear();
|
||||
@@ -1412,8 +1427,7 @@ impl ChatWidget<'_> {
|
||||
}) => {
|
||||
self.finalize_active_stream();
|
||||
self.add_to_history(HistoryCell::new_active_custom_tool_call(
|
||||
tool_name,
|
||||
parameters,
|
||||
tool_name, parameters,
|
||||
));
|
||||
}
|
||||
EventMsg::CustomToolCallEnd(CustomToolCallEndEvent {
|
||||
@@ -1424,11 +1438,7 @@ impl ChatWidget<'_> {
|
||||
result,
|
||||
}) => {
|
||||
self.add_to_history(HistoryCell::new_completed_custom_tool_call(
|
||||
80,
|
||||
tool_name,
|
||||
parameters,
|
||||
duration,
|
||||
result,
|
||||
80, tool_name, parameters, duration, result,
|
||||
));
|
||||
}
|
||||
EventMsg::GetHistoryEntryResponse(event) => {
|
||||
@@ -1850,12 +1860,16 @@ impl ChatWidget<'_> {
|
||||
pub(crate) fn show_chrome_options(&mut self, port: Option<u16>) {
|
||||
self.bottom_pane.show_chrome_selection(port);
|
||||
}
|
||||
|
||||
pub(crate) fn handle_chrome_launch_option(&mut self, option: crate::bottom_pane::chrome_selection_view::ChromeLaunchOption, port: Option<u16>) {
|
||||
|
||||
pub(crate) fn handle_chrome_launch_option(
|
||||
&mut self,
|
||||
option: crate::bottom_pane::chrome_selection_view::ChromeLaunchOption,
|
||||
port: Option<u16>,
|
||||
) {
|
||||
use crate::bottom_pane::chrome_selection_view::ChromeLaunchOption;
|
||||
|
||||
|
||||
let launch_port = port.unwrap_or(9222);
|
||||
|
||||
|
||||
match option {
|
||||
ChromeLaunchOption::CloseAndUseProfile => {
|
||||
// Kill existing Chrome and launch with user profile
|
||||
@@ -1903,15 +1917,17 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn launch_chrome_with_profile(&mut self, port: u16) {
|
||||
use std::process::Stdio;
|
||||
use ratatui::text::Line;
|
||||
|
||||
use std::process::Stdio;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let log_path = format!("{}/coder-chrome.log", std::env::temp_dir().display());
|
||||
let mut cmd = std::process::Command::new("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
||||
let mut cmd = std::process::Command::new(
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
);
|
||||
cmd.arg(format!("--remote-debugging-port={}", port))
|
||||
.arg("--no-first-run")
|
||||
.arg("--no-default-browser-check")
|
||||
@@ -1931,7 +1947,7 @@ impl ChatWidget<'_> {
|
||||
.stdin(Stdio::null());
|
||||
let _ = cmd.spawn();
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let log_path = format!("{}/coder-chrome.log", std::env::temp_dir().display());
|
||||
@@ -1955,17 +1971,19 @@ impl ChatWidget<'_> {
|
||||
.stdin(Stdio::null());
|
||||
let _ = cmd.spawn();
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let log_path = format!("{}\\coder-chrome.log", std::env::temp_dir().display());
|
||||
let chrome_paths = vec![
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
||||
format!("{}\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
|
||||
std::env::var("USERPROFILE").unwrap_or_default()),
|
||||
format!(
|
||||
"{}\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
|
||||
std::env::var("USERPROFILE").unwrap_or_default()
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
for chrome_path in chrome_paths {
|
||||
if std::path::Path::new(&chrome_path).exists() {
|
||||
let mut cmd = std::process::Command::new(&chrome_path);
|
||||
@@ -1991,27 +2009,27 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add status message
|
||||
self.add_to_history(HistoryCell::BackgroundEvent {
|
||||
view: TextBlock::new(vec![Line::from("✅ Chrome launched with user profile")]),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
fn connect_to_chrome_after_launch(&mut self, port: u16) {
|
||||
// Wait a moment for Chrome to start, then reuse the existing connection logic
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let latest_screenshot = self.latest_browser_screenshot.clone();
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Wait for Chrome to fully start
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
|
||||
// Now try to connect using the shared CDP connection logic
|
||||
ChatWidget::connect_to_cdp_chrome(Some(port), latest_screenshot, app_event_tx).await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// Shared CDP connection logic used by both /chrome command and Chrome launch options
|
||||
async fn connect_to_cdp_chrome(
|
||||
port: Option<u16>,
|
||||
@@ -2034,52 +2052,62 @@ impl ChatWidget<'_> {
|
||||
match browser_manager.connect_to_chrome_only().await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Connected to Chrome via CDP");
|
||||
|
||||
|
||||
// Send success message
|
||||
let success_msg = if let Some(p) = port {
|
||||
format!("✅ Connected to Chrome on port {}", p)
|
||||
} else {
|
||||
"✅ Connected to Chrome (auto-detected port)".to_string()
|
||||
};
|
||||
|
||||
|
||||
// Set up navigation callback
|
||||
let latest_screenshot_callback = latest_screenshot.clone();
|
||||
let app_event_tx_callback = app_event_tx.clone();
|
||||
|
||||
browser_manager.set_navigation_callback(move |url| {
|
||||
tracing::info!("CDP Navigation callback triggered for URL: {}", url);
|
||||
let latest_screenshot_inner = latest_screenshot_callback.clone();
|
||||
let app_event_tx_inner = app_event_tx_callback.clone();
|
||||
let url_inner = url.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
let browser_manager_inner = ChatWidget::get_browser_manager().await;
|
||||
match browser_manager_inner.capture_screenshot_with_url().await {
|
||||
Ok((paths, _)) => {
|
||||
if let Some(first_path) = paths.first() {
|
||||
tracing::info!("Auto-captured CDP screenshot: {}", first_path.display());
|
||||
|
||||
if let Ok(mut latest) = latest_screenshot_inner.lock() {
|
||||
*latest = Some((first_path.clone(), url_inner.clone()));
|
||||
|
||||
browser_manager
|
||||
.set_navigation_callback(move |url| {
|
||||
tracing::info!("CDP Navigation callback triggered for URL: {}", url);
|
||||
let latest_screenshot_inner = latest_screenshot_callback.clone();
|
||||
let app_event_tx_inner = app_event_tx_callback.clone();
|
||||
let url_inner = url.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
let browser_manager_inner = ChatWidget::get_browser_manager().await;
|
||||
match browser_manager_inner.capture_screenshot_with_url().await {
|
||||
Ok((paths, _)) => {
|
||||
if let Some(first_path) = paths.first() {
|
||||
tracing::info!(
|
||||
"Auto-captured CDP screenshot: {}",
|
||||
first_path.display()
|
||||
);
|
||||
|
||||
if let Ok(mut latest) = latest_screenshot_inner.lock() {
|
||||
*latest = Some((first_path.clone(), url_inner.clone()));
|
||||
}
|
||||
|
||||
use codex_core::protocol::{
|
||||
BrowserScreenshotUpdateEvent, Event, EventMsg,
|
||||
};
|
||||
let _ =
|
||||
app_event_tx_inner.send(AppEvent::CodexEvent(Event {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
msg: EventMsg::BrowserScreenshotUpdate(
|
||||
BrowserScreenshotUpdateEvent {
|
||||
screenshot_path: first_path.clone(),
|
||||
url: url_inner,
|
||||
},
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
use codex_core::protocol::{BrowserScreenshotUpdateEvent, EventMsg, Event};
|
||||
let _ = app_event_tx_inner.send(AppEvent::CodexEvent(Event {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
msg: EventMsg::BrowserScreenshotUpdate(BrowserScreenshotUpdateEvent {
|
||||
screenshot_path: first_path.clone(),
|
||||
url: url_inner,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to auto-capture CDP screenshot: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to auto-capture CDP screenshot: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}).await;
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
// Set as global manager
|
||||
codex_browser::global::set_global_browser_manager(browser_manager.clone()).await;
|
||||
@@ -2089,22 +2117,29 @@ impl ChatWidget<'_> {
|
||||
match browser_manager.capture_screenshot_with_url().await {
|
||||
Ok((paths, url)) => {
|
||||
if let Some(first_path) = paths.first() {
|
||||
tracing::info!("Initial CDP screenshot captured: {}", first_path.display());
|
||||
|
||||
tracing::info!(
|
||||
"Initial CDP screenshot captured: {}",
|
||||
first_path.display()
|
||||
);
|
||||
|
||||
if let Ok(mut latest) = latest_screenshot.lock() {
|
||||
*latest = Some((
|
||||
first_path.clone(),
|
||||
url.clone().unwrap_or_else(|| "Chrome".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
use codex_core::protocol::{BrowserScreenshotUpdateEvent, EventMsg, Event};
|
||||
|
||||
use codex_core::protocol::{
|
||||
BrowserScreenshotUpdateEvent, Event, EventMsg,
|
||||
};
|
||||
let _ = app_event_tx.send(AppEvent::CodexEvent(Event {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
msg: EventMsg::BrowserScreenshotUpdate(BrowserScreenshotUpdateEvent {
|
||||
screenshot_path: first_path.clone(),
|
||||
url: url.unwrap_or_else(|| "Chrome".to_string()),
|
||||
}),
|
||||
msg: EventMsg::BrowserScreenshotUpdate(
|
||||
BrowserScreenshotUpdateEvent {
|
||||
screenshot_path: first_path.clone(),
|
||||
url: url.unwrap_or_else(|| "Chrome".to_string()),
|
||||
},
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -2112,9 +2147,9 @@ impl ChatWidget<'_> {
|
||||
tracing::error!("Failed to capture initial CDP screenshot: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Send success status to chat
|
||||
use codex_core::protocol::{EventMsg, BackgroundEventEvent, Event};
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = app_event_tx.send(AppEvent::CodexEvent(Event {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
|
||||
@@ -2124,10 +2159,10 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to connect to Chrome: {}", e);
|
||||
|
||||
|
||||
// Send error message only - don't show dialog again since we're already
|
||||
// in the post-launch connection attempt
|
||||
use codex_core::protocol::{BackgroundEventEvent, EventMsg, Event};
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = app_event_tx.send(AppEvent::CodexEvent(Event {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
|
||||
@@ -2137,18 +2172,20 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn launch_chrome_with_temp_profile(&mut self, port: u16) {
|
||||
use std::process::Stdio;
|
||||
use ratatui::text::Line;
|
||||
|
||||
use std::process::Stdio;
|
||||
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let profile_dir = temp_dir.join(format!("coder-chrome-temp-{}", port));
|
||||
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let log_path = format!("{}/coder-chrome.log", std::env::temp_dir().display());
|
||||
let mut cmd = std::process::Command::new("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
|
||||
let mut cmd = std::process::Command::new(
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
);
|
||||
cmd.arg(format!("--remote-debugging-port={}", port))
|
||||
.arg(format!("--user-data-dir={}", profile_dir.display()))
|
||||
.arg("--no-first-run")
|
||||
@@ -2169,7 +2206,7 @@ impl ChatWidget<'_> {
|
||||
.stdin(Stdio::null());
|
||||
let _ = cmd.spawn();
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let log_path = format!("{}/coder-chrome.log", std::env::temp_dir().display());
|
||||
@@ -2194,17 +2231,19 @@ impl ChatWidget<'_> {
|
||||
.stdin(Stdio::null());
|
||||
let _ = cmd.spawn();
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let log_path = format!("{}\\coder-chrome.log", std::env::temp_dir().display());
|
||||
let chrome_paths = vec![
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
||||
format!("{}\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
|
||||
std::env::var("USERPROFILE").unwrap_or_default()),
|
||||
format!(
|
||||
"{}\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
|
||||
std::env::var("USERPROFILE").unwrap_or_default()
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
for chrome_path in chrome_paths {
|
||||
if std::path::Path::new(&chrome_path).exists() {
|
||||
let mut cmd = std::process::Command::new(&chrome_path);
|
||||
@@ -2231,17 +2270,20 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add status message
|
||||
self.add_to_history(HistoryCell::BackgroundEvent {
|
||||
view: TextBlock::new(vec![Line::from(format!("✅ Chrome launched with temporary profile at {}", profile_dir.display()))]),
|
||||
view: TextBlock::new(vec![Line::from(format!(
|
||||
"✅ Chrome launched with temporary profile at {}",
|
||||
profile_dir.display()
|
||||
))]),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn handle_browser_command(&mut self, command_text: String) {
|
||||
// Parse the browser subcommand
|
||||
let trimmed = command_text.trim();
|
||||
|
||||
|
||||
// Handle the case where just "/browser" was typed
|
||||
if trimmed.is_empty() {
|
||||
let response = "Browser commands:\n• /browser <url> - Open URL in internal browser\n• /browser off - Disable browser mode\n• /browser status - Show current status\n• /browser fullpage [on|off] - Toggle full-page mode\n• /browser config <key> <value> - Update configuration\n\nUse /chrome [port] to connect to external Chrome browser";
|
||||
@@ -2254,7 +2296,7 @@ impl ChatWidget<'_> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let parts: Vec<&str> = trimmed.split_whitespace().collect();
|
||||
let response = if !parts.is_empty() {
|
||||
let first_arg = parts[0];
|
||||
@@ -2277,7 +2319,7 @@ impl ChatWidget<'_> {
|
||||
let latest_screenshot = self.latest_browser_screenshot.clone();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
let url_for_goto = full_url.clone();
|
||||
|
||||
|
||||
// Add status message
|
||||
let status_msg = format!("🌐 Opening internal browser: {}", full_url);
|
||||
self.add_to_history(HistoryCell::BackgroundEvent {
|
||||
@@ -2430,9 +2472,9 @@ impl ChatWidget<'_> {
|
||||
result.url,
|
||||
result.title
|
||||
);
|
||||
|
||||
|
||||
// Send success message to chat
|
||||
use codex_core::protocol::{EventMsg, BackgroundEventEvent};
|
||||
use codex_core::protocol::{BackgroundEventEvent, EventMsg};
|
||||
let _ = app_event_tx.send(AppEvent::CodexEvent(Event {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
msg: EventMsg::BackgroundEvent(BackgroundEventEvent {
|
||||
@@ -2619,7 +2661,7 @@ impl ChatWidget<'_> {
|
||||
pub(crate) fn handle_chrome_command(&mut self, command_text: String) {
|
||||
// Parse the chrome command arguments
|
||||
let parts: Vec<&str> = command_text.trim().split_whitespace().collect();
|
||||
|
||||
|
||||
// Check if it's a status command
|
||||
if !parts.is_empty() && parts[0] == "status" {
|
||||
// Get status from BrowserManager - same as /browser status
|
||||
@@ -2632,7 +2674,7 @@ impl ChatWidget<'_> {
|
||||
let status = status_rx
|
||||
.recv()
|
||||
.unwrap_or_else(|_| "Failed to get browser status.".to_string());
|
||||
|
||||
|
||||
// Add the response to the UI
|
||||
let lines = status
|
||||
.lines()
|
||||
@@ -2643,7 +2685,7 @@ impl ChatWidget<'_> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Extract port if provided (number as first argument)
|
||||
let port = if !parts.is_empty() {
|
||||
parts[0].parse::<u16>().ok()
|
||||
@@ -2657,7 +2699,10 @@ impl ChatWidget<'_> {
|
||||
let launch_port = port.unwrap_or(9222);
|
||||
|
||||
// Add status message to chat
|
||||
let status_msg = format!("🔗 Connecting to Chrome DevTools Protocol (port: {})...", port_display);
|
||||
let status_msg = format!(
|
||||
"🔗 Connecting to Chrome DevTools Protocol (port: {})...",
|
||||
port_display
|
||||
);
|
||||
self.add_to_history(HistoryCell::BackgroundEvent {
|
||||
view: TextBlock::new(vec![Line::from(status_msg)]),
|
||||
});
|
||||
@@ -3582,12 +3627,12 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
let mut total_height = 0u16;
|
||||
let mut item_heights = Vec::new();
|
||||
let spacing = 1u16; // Add 1 line of spacing between each history cell
|
||||
|
||||
|
||||
for (idx, item) in all_content.iter().enumerate() {
|
||||
let h = item.desired_height(content_area.width);
|
||||
item_heights.push(h);
|
||||
total_height += h;
|
||||
|
||||
|
||||
// Add spacing after each item except the last one
|
||||
if idx < all_content.len() - 1 {
|
||||
total_height += spacing;
|
||||
@@ -3699,13 +3744,14 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
}
|
||||
|
||||
content_y += item_height;
|
||||
|
||||
|
||||
// Add spacing after this item (except for the last item)
|
||||
if idx < all_content.len() - 1 {
|
||||
content_y += spacing;
|
||||
// Also advance screen_y by the visible portion of the spacing
|
||||
if content_y > scroll_pos && screen_y < content_area.y + content_area.height {
|
||||
screen_y += spacing.min((content_area.y + content_area.height).saturating_sub(screen_y));
|
||||
screen_y += spacing
|
||||
.min((content_area.y + content_area.height).saturating_sub(screen_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,9 +167,7 @@ pub(crate) fn create_diff_summary(
|
||||
fn render_patch_details(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
|
||||
let mut out: Vec<RtLine<'static>> = Vec::new();
|
||||
// Use terminal width or a reasonable fallback
|
||||
let term_cols: usize = terminal::size()
|
||||
.map(|(w, _)| w as usize)
|
||||
.unwrap_or(120);
|
||||
let term_cols: usize = terminal::size().map(|(w, _)| w as usize).unwrap_or(120);
|
||||
|
||||
for (index, (path, change)) in changes.iter().enumerate() {
|
||||
let is_first_file = index == 0;
|
||||
@@ -424,10 +422,14 @@ mod tests {
|
||||
// Call the wrapping function directly so we can precisely control the width
|
||||
// Use a fixed width for testing wrapping behavior
|
||||
const TEST_WRAP_WIDTH: usize = 80;
|
||||
let lines =
|
||||
push_wrapped_diff_line(1, DiffLineType::Insert, long_line, TEST_WRAP_WIDTH);
|
||||
let lines = push_wrapped_diff_line(1, DiffLineType::Insert, long_line, TEST_WRAP_WIDTH);
|
||||
|
||||
// Render into a small terminal to capture the visual layout
|
||||
snapshot_lines("wrap_behavior_insert", lines, (TEST_WRAP_WIDTH + 10) as u16, 8);
|
||||
snapshot_lines(
|
||||
"wrap_behavior_insert",
|
||||
lines,
|
||||
(TEST_WRAP_WIDTH + 10) as u16,
|
||||
8,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ use image::ImageReader;
|
||||
use mcp_types::EmbeddedResourceResource;
|
||||
use mcp_types::ResourceLink;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::widgets::Paragraph;
|
||||
@@ -503,7 +502,10 @@ impl HistoryCell {
|
||||
let prefix = if j == 0 { first_prefix } else { " " };
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(prefix, Style::default().add_modifier(Modifier::DIM)),
|
||||
Span::styled(line_text.to_string(), Style::default().fg(Color::LightBlue)),
|
||||
Span::styled(
|
||||
line_text.to_string(),
|
||||
Style::default().fg(crate::colors::info()),
|
||||
),
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -556,9 +558,9 @@ impl HistoryCell {
|
||||
parameters: Option<serde_json::Value>,
|
||||
) -> Self {
|
||||
let title_line = Line::from(vec![tool_name.bold(), " running...".dim()]);
|
||||
|
||||
|
||||
let mut lines: Vec<Line> = vec![title_line];
|
||||
|
||||
|
||||
// Add parameters if present
|
||||
if let Some(params) = parameters {
|
||||
if let Ok(formatted) = serde_json::to_string_pretty(¶ms) {
|
||||
@@ -692,7 +694,9 @@ impl HistoryCell {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
"Error: ",
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(crate::colors::error())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(e),
|
||||
]));
|
||||
@@ -715,7 +719,7 @@ impl HistoryCell {
|
||||
let duration_str = format_duration(duration);
|
||||
let success = result.is_ok();
|
||||
let status_str = if success { "success" } else { "failed" };
|
||||
|
||||
|
||||
let title_line = Line::from(vec![
|
||||
tool_name.bold(),
|
||||
" ".into(),
|
||||
@@ -733,7 +737,8 @@ impl HistoryCell {
|
||||
// Add result
|
||||
match result {
|
||||
Ok(msg) => {
|
||||
let truncated = format_and_truncate_tool_result(&msg, TOOL_CALL_MAX_LINES, num_cols);
|
||||
let truncated =
|
||||
format_and_truncate_tool_result(&msg, TOOL_CALL_MAX_LINES, num_cols);
|
||||
for line in truncated.lines() {
|
||||
lines.push(Line::from(format!("> {}", line)));
|
||||
}
|
||||
@@ -965,7 +970,7 @@ impl HistoryCell {
|
||||
if filled > 0 {
|
||||
header.push(Span::styled(
|
||||
"█".repeat(filled),
|
||||
Style::default().fg(Color::Green),
|
||||
Style::default().fg(crate::colors::success()),
|
||||
));
|
||||
}
|
||||
if empty > 0 {
|
||||
@@ -996,7 +1001,7 @@ impl HistoryCell {
|
||||
for (idx, PlanItemArg { step, status }) in plan.into_iter().enumerate() {
|
||||
let (box_span, text_span) = match status {
|
||||
StepStatus::Completed => (
|
||||
Span::styled("✔", Style::default().fg(Color::Green)),
|
||||
Span::styled("✔", Style::default().fg(crate::colors::success())),
|
||||
Span::styled(
|
||||
step,
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT | Modifier::DIM),
|
||||
@@ -1007,7 +1012,7 @@ impl HistoryCell {
|
||||
Span::styled(
|
||||
step,
|
||||
Style::default()
|
||||
.fg(Color::Blue)
|
||||
.fg(crate::colors::info())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
),
|
||||
@@ -1345,8 +1350,9 @@ impl HistoryCell {
|
||||
| HistoryCell::CompletedCustomToolCall { .. } => {
|
||||
// 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::BackgroundEvent { .. } | HistoryCell::DimmedReasoning { .. } => {
|
||||
self.get_processed_lines(area.width)
|
||||
}
|
||||
HistoryCell::StyledText { view }
|
||||
| HistoryCell::WelcomeMessage { view }
|
||||
| HistoryCell::GitDiffOutput { view }
|
||||
@@ -1400,26 +1406,24 @@ impl HistoryCell {
|
||||
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);
|
||||
|
||||
let scroll_view =
|
||||
ScrollView::new(CellRenderer { cell: self }, content_height as usize)
|
||||
.scroll_y(skip_top as usize);
|
||||
|
||||
scroll_view.render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1437,7 +1441,10 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
||||
.unwrap_or_default();
|
||||
|
||||
let invocation_spans = vec![
|
||||
Span::styled(invocation.server.clone(), Style::default().fg(Color::Blue)),
|
||||
Span::styled(
|
||||
invocation.server.clone(),
|
||||
Style::default().fg(crate::colors::info()),
|
||||
),
|
||||
Span::raw("."),
|
||||
Span::styled(invocation.tool.clone(), Style::default().fg(Color::Blue)),
|
||||
Span::raw("("),
|
||||
|
||||
@@ -462,7 +462,8 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
actual,
|
||||
expected,
|
||||
"Actual: {}",
|
||||
String::from_utf8_lossy(&actual)
|
||||
);
|
||||
@@ -486,13 +487,33 @@ mod tests {
|
||||
assert_eq!(wrapped.len(), 2);
|
||||
assert_eq!(wrapped[0].spans.len(), 2);
|
||||
assert_eq!(wrapped[0].spans[0].content, "hello ");
|
||||
assert!(!wrapped[0].spans[0].style.add_modifier.contains(Modifier::BOLD));
|
||||
assert!(
|
||||
!wrapped[0].spans[0]
|
||||
.style
|
||||
.add_modifier
|
||||
.contains(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(wrapped[0].spans[1].content, "wo");
|
||||
assert!(wrapped[0].spans[1].style.add_modifier.contains(Modifier::BOLD));
|
||||
assert!(
|
||||
wrapped[0].spans[1]
|
||||
.style
|
||||
.add_modifier
|
||||
.contains(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(wrapped[1].spans.len(), 2);
|
||||
assert_eq!(wrapped[1].spans[0].content, "rld");
|
||||
assert!(wrapped[1].spans[0].style.add_modifier.contains(Modifier::BOLD));
|
||||
assert!(
|
||||
wrapped[1].spans[0]
|
||||
.style
|
||||
.add_modifier
|
||||
.contains(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(wrapped[1].spans[1].content, " foo");
|
||||
assert!(!wrapped[1].spans[1].style.add_modifier.contains(Modifier::BOLD));
|
||||
assert!(
|
||||
!wrapped[1].spans[1]
|
||||
.style
|
||||
.add_modifier
|
||||
.contains(Modifier::BOLD)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -285,8 +285,19 @@ fn run_ratatui_app(
|
||||
// Initialize high-fidelity session event logging if enabled.
|
||||
session_log::maybe_init(&config);
|
||||
|
||||
let Cli { prompt, images, debug, .. } = cli;
|
||||
let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen, debug);
|
||||
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.
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Widget;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
@@ -116,7 +115,7 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
if let Some(error) = &self.error {
|
||||
lines.push(Line::from(format!(" {error}")).fg(Color::Red));
|
||||
lines.push(Line::from(format!(" {error}")).fg(crate::colors::error()));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{Block, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget},
|
||||
style::Style,
|
||||
widgets::{Block, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
/// A generic scrollable viewport for any Widget.
|
||||
@@ -87,7 +87,9 @@ where
|
||||
// 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));
|
||||
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)
|
||||
@@ -134,4 +136,4 @@ where
|
||||
.render(inner_area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,9 @@ pub(crate) fn shimmer_spans(text: &str, frame_idx: usize) -> Vec<Span<'static>>
|
||||
.fg(Color::Rgb(level, level, level))
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
color_for_level(level)
|
||||
Style::default()
|
||||
.fg(color_for_level(level))
|
||||
.add_modifier(Modifier::BOLD)
|
||||
};
|
||||
spans.push(Span::styled(ch.to_string(), style));
|
||||
}
|
||||
@@ -74,12 +76,11 @@ pub(crate) fn shimmer_spans(text: &str, frame_idx: usize) -> Vec<Span<'static>>
|
||||
}
|
||||
|
||||
fn color_for_level(level: u8) -> Color {
|
||||
// For shimmer effect, we need to use grayscale colors
|
||||
// Theme colors wouldn't work well here as they're not grayscale
|
||||
// For shimmer effect, we map brightness levels to theme colors
|
||||
if level < 128 {
|
||||
Color::DarkGray
|
||||
crate::colors::text_dim()
|
||||
} else if level < 192 {
|
||||
Color::Gray
|
||||
crate::colors::text()
|
||||
} else {
|
||||
crate::colors::text_bright()
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ impl UserApprovalWidget<'_> {
|
||||
cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM);
|
||||
let mut contents: Vec<Line> = vec![
|
||||
Line::from(vec![
|
||||
"? ".fg(Color::Blue),
|
||||
"? ".fg(crate::colors::info()),
|
||||
"Codex wants to run ".bold(),
|
||||
cmd_span,
|
||||
]),
|
||||
@@ -245,7 +245,7 @@ impl UserApprovalWidget<'_> {
|
||||
match decision {
|
||||
ReviewDecision::Approved => {
|
||||
lines.push(Line::from(vec![
|
||||
"✔ ".fg(Color::Green),
|
||||
"✔ ".fg(crate::colors::success()),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
" codex to run ".into(),
|
||||
@@ -256,7 +256,7 @@ impl UserApprovalWidget<'_> {
|
||||
}
|
||||
ReviewDecision::ApprovedForSession => {
|
||||
lines.push(Line::from(vec![
|
||||
"✔ ".fg(Color::Green),
|
||||
"✔ ".fg(crate::colors::success()),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
" codex to run ".into(),
|
||||
@@ -267,7 +267,7 @@ impl UserApprovalWidget<'_> {
|
||||
}
|
||||
ReviewDecision::Denied => {
|
||||
lines.push(Line::from(vec![
|
||||
"✗ ".fg(Color::Red),
|
||||
"✗ ".fg(crate::colors::error()),
|
||||
"You ".into(),
|
||||
"did not approve".bold(),
|
||||
" codex to run ".into(),
|
||||
@@ -276,7 +276,7 @@ impl UserApprovalWidget<'_> {
|
||||
}
|
||||
ReviewDecision::Abort => {
|
||||
lines.push(Line::from(vec![
|
||||
"✗ ".fg(Color::Red),
|
||||
"✗ ".fg(crate::colors::error()),
|
||||
"You ".into(),
|
||||
"canceled".bold(),
|
||||
" the request to run ".into(),
|
||||
@@ -340,7 +340,7 @@ impl WidgetRef for &UserApprovalWidget<'_> {
|
||||
let style = if idx == self.selected_option {
|
||||
Style::new()
|
||||
.bg(crate::colors::light_blue())
|
||||
.fg(Color::Black)
|
||||
.fg(crate::colors::background())
|
||||
} else {
|
||||
Style::new().add_modifier(Modifier::DIM)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user