mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
Fix terminal query and improve image rendering initialization
- Query terminal capabilities at TUI startup using Picker::from_query_stdio() - Store terminal info (picker and font size) and pass through initialization - Fix unstable let-chains syntax in mcp-server - Add missing std::env import in chatwidget.rs - Add missing FilterType import for image resizing - Update all ChatWidget creation calls to pass terminal_info - Improve font size detection by querying once at startup instead of during render This fixes the issue where from_query_stdio() couldn't work during rendering because stdio was already in use by the TUI. Now we query capabilities once at startup when it's safe to do so. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
+4
-3
@@ -2,7 +2,8 @@
|
||||
# Fast build script for local development - optimized for speed
|
||||
set -euo pipefail
|
||||
|
||||
# No need to cd, we're already in the Rust project root
|
||||
# Change to the Rust project root directory
|
||||
cd codex-rs
|
||||
|
||||
# Use dev-fast profile by default for quick iteration
|
||||
# Can override with: PROFILE=release ./build-fast.sh
|
||||
@@ -37,13 +38,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 "../../target/${PROFILE}/coder" "$CODEX_CLI_BIN"
|
||||
ln -sf "../../codex-rs/target/${PROFILE}/coder" "$CODEX_CLI_BIN"
|
||||
|
||||
echo "✅ Symlinks updated"
|
||||
echo ""
|
||||
|
||||
@@ -482,9 +482,9 @@ impl Page {
|
||||
|
||||
// Motion configuration (distance-based durations)
|
||||
const MOTION = {{
|
||||
pxPerSec: 1400, // base speed
|
||||
min: 500, // ms clamp
|
||||
max: 2000, // ms clamp
|
||||
pxPerSec: 800, // base speed
|
||||
min: 600, // ms clamp
|
||||
max: 3000, // ms clamp
|
||||
easing: 'cubic-bezier(0.25, 1, 0.5, 1)', // easeOutQuart-ish
|
||||
badgeScale: 1.25, // badge longer than arrow
|
||||
badgeDelay: 100, // ms
|
||||
@@ -556,13 +556,13 @@ impl Page {
|
||||
// --- Hover-to-dim (distance to tip) ---
|
||||
root.style.opacity = '1';
|
||||
root.style.transition = 'opacity 160ms ease-out';
|
||||
const HOVER = {{ opacity: 0.2, radius: 12, enabled: true }};
|
||||
const HOVER = {{ opacity: 0.2, offset: 20, radius: 55, enabled: true }};
|
||||
|
||||
let _mx = 0, _my = 0, _rafHover = 0, _dimmed = false;
|
||||
function hoverTick() {{
|
||||
_rafHover = 0;
|
||||
const tipX = state.arrowX + TIP_X;
|
||||
const tipY = state.arrowY + TIP_Y;
|
||||
const tipX = state.arrowX + TIP_X + HOVER.offset;
|
||||
const tipY = state.arrowY + TIP_Y + HOVER.offset;
|
||||
const dx = _mx - tipX, dy = _my - tipY;
|
||||
const over = (dx*dx + dy*dy) <= (HOVER.radius * HOVER.radius);
|
||||
const shouldDim = HOVER.enabled && over;
|
||||
|
||||
+12
-12
@@ -689,20 +689,20 @@ fn try_parse_retry_after(err: &Error) -> Option<Duration> {
|
||||
|
||||
// parse the Please try again in 1.898s format using regex
|
||||
let re = rate_limit_regex();
|
||||
if let Some(message) = &err.message
|
||||
&& let Some(captures) = re.captures(message)
|
||||
{
|
||||
let seconds = captures.get(1);
|
||||
let unit = captures.get(2);
|
||||
if let Some(message) = &err.message {
|
||||
if let Some(captures) = re.captures(message) {
|
||||
let seconds = captures.get(1);
|
||||
let unit = captures.get(2);
|
||||
|
||||
if let (Some(value), Some(unit)) = (seconds, unit) {
|
||||
let value = value.as_str().parse::<f64>().ok()?;
|
||||
let unit = unit.as_str();
|
||||
if let (Some(value), Some(unit)) = (seconds, unit) {
|
||||
let value = value.as_str().parse::<f64>().ok()?;
|
||||
let unit = unit.as_str();
|
||||
|
||||
if unit == "s" {
|
||||
return Some(Duration::from_secs_f64(value));
|
||||
} else if unit == "ms" {
|
||||
return Some(Duration::from_millis(value as u64));
|
||||
if unit == "s" {
|
||||
return Some(Duration::from_secs_f64(value));
|
||||
} else if unit == "ms" {
|
||||
return Some(Duration::from_millis(value as u64));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,15 +89,15 @@ impl MessageProcessor {
|
||||
}
|
||||
|
||||
pub(crate) async fn process_request(&mut self, request: JSONRPCRequest) {
|
||||
if let Ok(request_json) = serde_json::to_value(request.clone())
|
||||
&& let Ok(codex_request) = serde_json::from_value::<ClientRequest>(request_json)
|
||||
{
|
||||
// If the request is a Codex request, handle it with the Codex
|
||||
// message processor.
|
||||
self.codex_message_processor
|
||||
.process_request(codex_request)
|
||||
.await;
|
||||
return;
|
||||
if let Ok(request_json) = serde_json::to_value(request.clone()) {
|
||||
if let Ok(codex_request) = serde_json::from_value::<ClientRequest>(request_json) {
|
||||
// If the request is a Codex request, handle it with the Codex
|
||||
// message processor.
|
||||
self.codex_message_processor
|
||||
.process_request(codex_request)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Hold on to the ID so we can respond.
|
||||
|
||||
+15
-1
@@ -9,6 +9,7 @@ use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
||||
use crate::should_show_login_screen;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::tui;
|
||||
use crate::tui::TerminalInfo;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::protocol::Event;
|
||||
@@ -66,6 +67,9 @@ pub(crate) struct App<'a> {
|
||||
|
||||
/// Controls the animation thread that sends CommitTick events.
|
||||
commit_anim_running: Arc<AtomicBool>,
|
||||
|
||||
/// Terminal information queried at startup
|
||||
terminal_info: TerminalInfo,
|
||||
}
|
||||
|
||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||
@@ -76,6 +80,7 @@ pub(crate) struct ChatWidgetArgs {
|
||||
initial_prompt: Option<String>,
|
||||
initial_images: Vec<PathBuf>,
|
||||
enhanced_keys_supported: bool,
|
||||
terminal_info: TerminalInfo,
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
@@ -85,6 +90,7 @@ impl App<'_> {
|
||||
initial_images: Vec<std::path::PathBuf>,
|
||||
show_trust_screen: bool,
|
||||
debug: bool,
|
||||
terminal_info: TerminalInfo,
|
||||
) -> Self {
|
||||
let conversation_manager = Arc::new(ConversationManager::default());
|
||||
|
||||
@@ -145,6 +151,7 @@ impl App<'_> {
|
||||
initial_prompt,
|
||||
initial_images,
|
||||
enhanced_keys_supported,
|
||||
terminal_info: terminal_info.clone(),
|
||||
};
|
||||
AppState::Onboarding {
|
||||
screen: OnboardingScreen::new(OnboardingScreenArgs {
|
||||
@@ -163,6 +170,7 @@ impl App<'_> {
|
||||
initial_prompt,
|
||||
initial_images,
|
||||
enhanced_keys_supported,
|
||||
terminal_info.clone(),
|
||||
);
|
||||
AppState::Chat {
|
||||
widget: Box::new(chat_widget),
|
||||
@@ -181,6 +189,7 @@ impl App<'_> {
|
||||
enhanced_keys_supported,
|
||||
_debug: debug,
|
||||
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
||||
terminal_info,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,6 +387,7 @@ impl App<'_> {
|
||||
None,
|
||||
Vec::new(),
|
||||
self.enhanced_keys_supported,
|
||||
self.terminal_info.clone(),
|
||||
));
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
@@ -589,6 +599,7 @@ impl App<'_> {
|
||||
enhanced_keys_supported,
|
||||
initial_images,
|
||||
initial_prompt,
|
||||
terminal_info,
|
||||
}) => {
|
||||
self.app_state = AppState::Chat {
|
||||
widget: Box::new(ChatWidget::new(
|
||||
@@ -597,6 +608,7 @@ impl App<'_> {
|
||||
initial_prompt,
|
||||
initial_images,
|
||||
enhanced_keys_supported,
|
||||
terminal_info,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -638,7 +650,9 @@ impl App<'_> {
|
||||
// subsequent re-init path will still leave the terminal in a good state.
|
||||
// We considered `nix`, but didn't think it was worth pulling in for this one call.
|
||||
unsafe { libc::kill(0, libc::SIGTSTP) };
|
||||
*terminal = tui::init(&self.config)?;
|
||||
let (new_terminal, new_terminal_info) = tui::init(&self.config)?;
|
||||
*terminal = new_terminal;
|
||||
self.terminal_info = new_terminal_info;
|
||||
terminal.clear()?;
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
Ok(())
|
||||
|
||||
@@ -45,6 +45,7 @@ use ratatui::text::Line;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui_image::picker::Picker;
|
||||
use image::imageops::FilterType;
|
||||
use std::cell::RefCell;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
@@ -112,6 +113,9 @@ pub(crate) struct ChatWidget<'a> {
|
||||
|
||||
// Cached cell size (width,height) in pixels
|
||||
cached_cell_size: std::cell::OnceCell<(u16, u16)>,
|
||||
|
||||
// Terminal information from startup
|
||||
terminal_info: crate::tui::TerminalInfo,
|
||||
// Scroll offset from bottom (0 = at bottom, positive = scrolled up)
|
||||
scroll_offset: u16,
|
||||
// Cached max scroll from last render to prevent overscroll artifacts
|
||||
@@ -423,6 +427,7 @@ impl ChatWidget<'_> {
|
||||
initial_prompt: Option<String>,
|
||||
initial_images: Vec<PathBuf>,
|
||||
enhanced_keys_supported: bool,
|
||||
terminal_info: crate::tui::TerminalInfo,
|
||||
) -> Self {
|
||||
let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>();
|
||||
|
||||
@@ -515,8 +520,9 @@ impl ChatWidget<'_> {
|
||||
welcome_shown: false,
|
||||
latest_browser_screenshot: Arc::new(Mutex::new(None)),
|
||||
cached_image_protocol: RefCell::new(None),
|
||||
cached_picker: RefCell::new(None),
|
||||
cached_picker: RefCell::new(terminal_info.picker.clone()),
|
||||
cached_cell_size: std::cell::OnceCell::new(),
|
||||
terminal_info,
|
||||
scroll_offset: 0,
|
||||
last_max_scroll: std::cell::Cell::new(0),
|
||||
active_agents: Vec::new(),
|
||||
@@ -2773,13 +2779,8 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
fn measured_font_size(&self) -> (u16, u16) {
|
||||
let default_guess = if std::env::var("TERM_PROGRAM").unwrap_or_default() == "iTerm.app" {
|
||||
(7, 15)
|
||||
} else {
|
||||
(8, 16)
|
||||
};
|
||||
*self.cached_cell_size.get_or_init(|| {
|
||||
let size = crate::terminal_info::get_cell_size_pixels().unwrap_or(default_guess);
|
||||
let size = self.terminal_info.font_size;
|
||||
|
||||
// HACK: On macOS Retina displays, terminals often report physical pixels
|
||||
// but ratatui-image expects logical pixels. If we detect suspiciously
|
||||
@@ -2949,8 +2950,11 @@ impl ChatWidget<'_> {
|
||||
// picker (Retina 2x workaround preserved)
|
||||
let mut cached_picker = self.cached_picker.borrow_mut();
|
||||
if cached_picker.is_none() {
|
||||
// If we didn't get a picker from terminal query at startup, create one from font size
|
||||
let (fw, fh) = self.measured_font_size();
|
||||
*cached_picker = Some(Picker::from_fontsize((fw * 2, fh * 2)));
|
||||
let p = Picker::from_fontsize((fw, fh));
|
||||
|
||||
*cached_picker = Some(p);
|
||||
}
|
||||
let picker = cached_picker.as_ref().unwrap();
|
||||
|
||||
@@ -3022,7 +3026,7 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
};
|
||||
if needs_recreate {
|
||||
match picker.new_protocol(dyn_img, target, Resize::Fit(None)) {
|
||||
match picker.new_protocol(dyn_img, target, Resize::Fit(Some(FilterType::Lanczos3))) {
|
||||
Ok(protocol) => {
|
||||
*self.cached_image_protocol.borrow_mut() =
|
||||
Some((path.clone(), target, protocol))
|
||||
|
||||
@@ -235,7 +235,7 @@ pub async fn run_main(
|
||||
|
||||
eprintln!(
|
||||
"{} {current_version} -> {latest_version}.",
|
||||
"✨⬆️ Update available!".bold().cyan()
|
||||
"Coder update available!".blue()
|
||||
);
|
||||
|
||||
if managed_by_npm {
|
||||
@@ -279,7 +279,7 @@ fn run_ratatui_app(
|
||||
tracing::error!("panic: {info}");
|
||||
prev_hook(info);
|
||||
}));
|
||||
let mut terminal = tui::init(&config)?;
|
||||
let (mut terminal, terminal_info) = tui::init(&config)?;
|
||||
terminal.clear()?;
|
||||
|
||||
// Initialize high-fidelity session event logging if enabled.
|
||||
@@ -297,6 +297,7 @@ fn run_ratatui_app(
|
||||
images,
|
||||
should_show_trust_screen,
|
||||
debug,
|
||||
terminal_info,
|
||||
);
|
||||
|
||||
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
|
||||
|
||||
+60
-2
@@ -18,12 +18,31 @@ use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::crossterm::execute;
|
||||
use ratatui::crossterm::terminal::disable_raw_mode;
|
||||
use ratatui::crossterm::terminal::enable_raw_mode;
|
||||
use ratatui_image::picker::Picker;
|
||||
|
||||
/// A type alias for the terminal type used in this application
|
||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// Terminal information queried at startup
|
||||
#[derive(Clone)]
|
||||
pub struct TerminalInfo {
|
||||
/// The image picker with detected capabilities
|
||||
pub picker: Option<Picker>,
|
||||
/// Measured font size (width, height) in pixels
|
||||
pub font_size: (u16, u16),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TerminalInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TerminalInfo")
|
||||
.field("picker", &self.picker.is_some())
|
||||
.field("font_size", &self.font_size)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the terminal (full screen mode with alternate screen)
|
||||
pub fn init(config: &Config) -> Result<Tui> {
|
||||
pub fn init(config: &Config) -> Result<(Tui, TerminalInfo)> {
|
||||
// Initialize the theme based on config
|
||||
crate::theme::init_theme(&config.tui.theme);
|
||||
|
||||
@@ -31,6 +50,10 @@ pub fn init(config: &Config) -> Result<Tui> {
|
||||
|
||||
// Enter alternate screen mode for full screen TUI
|
||||
execute!(stdout(), crossterm::terminal::EnterAlternateScreen)?;
|
||||
|
||||
// Query terminal capabilities and font size after entering alternate screen
|
||||
// but before enabling raw mode
|
||||
let terminal_info = query_terminal_info();
|
||||
|
||||
enable_raw_mode()?;
|
||||
// Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated.
|
||||
@@ -66,7 +89,42 @@ pub fn init(config: &Config) -> Result<Tui> {
|
||||
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let tui = Terminal::new(backend)?;
|
||||
Ok(tui)
|
||||
Ok((tui, terminal_info))
|
||||
}
|
||||
|
||||
/// Query terminal capabilities before entering raw mode
|
||||
fn query_terminal_info() -> TerminalInfo {
|
||||
// Try to query using ratatui_image's picker
|
||||
let picker = match Picker::from_query_stdio() {
|
||||
Ok(p) => {
|
||||
tracing::info!("Successfully queried terminal capabilities via Picker");
|
||||
Some(p)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to query terminal via Picker: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Get font size from picker if available, otherwise fall back to terminal_info query
|
||||
let font_size = if let Some(ref p) = picker {
|
||||
// The picker has font size information
|
||||
let (w, h) = p.font_size();
|
||||
tracing::info!("Got font size from Picker: {}x{}", w, h);
|
||||
(w, h)
|
||||
} else {
|
||||
// Fall back to our own terminal query
|
||||
crate::terminal_info::get_cell_size_pixels().unwrap_or_else(|| {
|
||||
tracing::warn!("Failed to get cell size, using defaults");
|
||||
if std::env::var("TERM_PROGRAM").unwrap_or_default() == "iTerm.app" {
|
||||
(7, 15)
|
||||
} else {
|
||||
(8, 16)
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
TerminalInfo { picker, font_size }
|
||||
}
|
||||
|
||||
fn set_panic_hook() {
|
||||
|
||||
Reference in New Issue
Block a user