Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
96929aa
Image in WSL do not work.
Waxime64 Oct 24, 2025
e579ca2
Remove Warning
Waxime64 Oct 24, 2025
7409720
Merge branch 'main' into wsl-image
Waxime64 Oct 24, 2025
2c17985
--check
Waxime64 Oct 24, 2025
311c233
Merge branch 'wsl-image' of https://github.com/Waxime64/codex into ws…
Waxime64 Oct 25, 2025
e9c9db1
--check
Waxime64 Oct 25, 2025
b5101d9
Merge branch 'main' into wsl-image
Waxime64 Oct 25, 2025
7e03b6f
Merge branch 'main' into wsl-image
Waxime64 Oct 25, 2025
eaff4d7
Remove temp file save, use direct base64 data
Waxime64 Oct 26, 2025
d2dc716
Merge branch 'main' into wsl-image
Waxime64 Oct 26, 2025
f9ede60
Force UTF-8 output to avoid encoding issues between powershell.exe (U…
Waxime64 Oct 26, 2025
6961c0e
Merge branch 'wsl-image' of https://github.com/Waxime64/codex into ws…
Waxime64 Oct 26, 2025
efa8921
Merge branch 'main' into wsl-image
Waxime64 Oct 26, 2025
3e7e221
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
7661e36
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
b81896d
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
c2cf60f
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
8dd3b86
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
80eb392
Merge conflict
Waxime64 Oct 27, 2025
080a2cf
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
38a5e0e
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
fefcf89
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
af35c01
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
eb0c809
Merge branch 'main' into wsl-image
Waxime64 Oct 27, 2025
aa2e008
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
c312418
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
b8e524e
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
eaf0b13
`if` statement can be collapsed
Waxime64 Oct 28, 2025
9a2c1b2
correction for Format / etc
Waxime64 Oct 28, 2025
29f0a7a
correction for Format / etc
Waxime64 Oct 28, 2025
0812e7e
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
d3a7713
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
fe152ed
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
5440dbf
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
4ab25f1
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
9a41dce
Add validation WSL + remove Ctrl+Shift+V
Waxime64 Oct 28, 2025
16a62af
Merge branch 'wsl-image' of https://github.com/Waxime64/codex into ws…
Waxime64 Oct 28, 2025
e434358
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
cca6f5a
Add cache + security under is_running_under_wsl
Waxime64 Oct 28, 2025
31991fb
Split use PasteImageError::{ClipboardUnavailable, NoImage};
Waxime64 Oct 28, 2025
1e5a603
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
6320526
Re-Add // --- Image attachment tests ---
Waxime64 Oct 28, 2025
9c52469
Re-Add // --- Image attachment tests ---
Waxime64 Oct 28, 2025
e049a6f
Text not present if original file
Waxime64 Oct 28, 2025
3f854e1
Remove some change for simplify code review
Waxime64 Oct 28, 2025
9d01c97
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
1b4c198
Reset test test_partial_placeholder_deletion
Waxime64 Oct 28, 2025
36277f3
Merge branch 'main' into wsl-image
Waxime64 Oct 28, 2025
5cadb39
Merge branch 'main' into wsl-image
Waxime64 Oct 29, 2025
cff5b9b
Merge branch 'main' into wsl-image
Waxime64 Oct 29, 2025
319385c
Merge branch 'main' into wsl-image
Waxime64 Oct 29, 2025
017195f
Merge branch 'wsl-image' of https://github.com/Waxime64/codex into ws…
Waxime64 Oct 29, 2025
5504192
Merge branch 'wsl-image' of https://github.com/Waxime64/codex into ws…
Waxime64 Oct 29, 2025
7f58b3a
Merge branch 'wsl-image' of https://github.com/Waxime64/codex into ws…
Waxime64 Oct 29, 2025
28c0319
Merge branch 'wsl-image' of https://github.com/Waxime64/codex into ws…
Waxime64 Oct 29, 2025
d545564
Solve Format / etc
Waxime64 Oct 29, 2025
b8ac9b0
Solve Format / etc
Waxime64 Oct 29, 2025
be1dda1
Merge branch 'main' into wsl-image
Waxime64 Oct 29, 2025
2caf765
Merge branch 'main' into wsl-image
Waxime64 Oct 29, 2025
9f26fde
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
79ab219
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
1caf9b3
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
c297a91
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
99e3b1b
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
0794446
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
abd5979
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
aeda1bf
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
f965a05
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ pub mod models;
pub mod num_format;
pub mod parse_command;
pub mod plan_tool;
pub mod platform;
pub mod protocol;
pub mod user_input;
93 changes: 56 additions & 37 deletions codex-rs/protocol/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use serde::Serialize;
use serde::ser::Serializer;
use ts_rs::TS;

use crate::platform;
use crate::user_input::UserInput;
use codex_git::GhostCommit;
use codex_utils_image::error::ImageProcessingError;
Expand Down Expand Up @@ -242,49 +243,67 @@ impl From<Vec<UserInput>> for ResponseInputItem {
.map(|c| match c {
UserInput::Text { text } => ContentItem::InputText { text },
UserInput::Image { image_url } => ContentItem::InputImage { image_url },
UserInput::LocalImage { path } => match load_and_resize_to_fit(&path) {
Ok(image) => ContentItem::InputImage {
image_url: image.into_data_url(),
},
Err(err) => {
tracing::warn!("Failed to resize image {}: {}", path.display(), err);
if matches!(&err, ImageProcessingError::Read { .. }) {
local_image_error_placeholder(&path, &err)
} else {
match std::fs::read(&path) {
Ok(bytes) => {
let Some(mime_guess) = mime_guess::from_path(&path).first()
else {
return local_image_error_placeholder(
&path,
"unsupported MIME type (unknown)",
);
};
let mime = mime_guess.essence_str().to_owned();
if !mime.starts_with("image/") {
return local_image_error_placeholder(
&path,
format!("unsupported MIME type `{mime}`"),
);
UserInput::LocalImage { path } => {
let mapped: Option<std::path::PathBuf> = if platform::is_running_under_wsl()
{
let win_path_str = path.to_string_lossy();
platform::try_map_windows_drive_to_wsl_path(&win_path_str)
.filter(|p| p.exists())
} else {
None
};

let effective_path: &std::path::Path = mapped.as_deref().unwrap_or(&path);

match load_and_resize_to_fit(effective_path) {
Ok(image) => ContentItem::InputImage {
image_url: image.into_data_url(),
},
Err(err) => {
tracing::warn!(
"Failed to resize image {}: {}",
effective_path.display(),
err
);
if matches!(&err, ImageProcessingError::Read { .. }) {
local_image_error_placeholder(effective_path, &err)
} else {
match std::fs::read(effective_path) {
Ok(bytes) => {
let Some(mime_guess) =
mime_guess::from_path(effective_path).first()
else {
return local_image_error_placeholder(
effective_path,
"unsupported MIME type (unknown)",
);
};
let mime = mime_guess.essence_str().to_owned();
if !mime.starts_with("image/") {
return local_image_error_placeholder(
effective_path,
format!("unsupported MIME type `{mime}`"),
);
}
let encoded = base64::engine::general_purpose::STANDARD
.encode(bytes);
ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
}
}
let encoded =
base64::engine::general_purpose::STANDARD.encode(bytes);
ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
Err(read_err) => {
tracing::warn!(
"Skipping image {} – could not read file: {}",
effective_path.display(),
read_err
);
local_image_error_placeholder(effective_path, &read_err)
}
}
Err(read_err) => {
tracing::warn!(
"Skipping image {} – could not read file: {}",
path.display(),
read_err
);
local_image_error_placeholder(&path, &read_err)
}
}
}
}
},
}
})
.collect::<Vec<ContentItem>>(),
}
Expand Down
38 changes: 38 additions & 0 deletions codex-rs/protocol/src/platform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::path::PathBuf;
use std::sync::OnceLock;

/// Return true when running under WSL (heuristics: WSL_DISTRO_NAME or /proc/version contains "microsoft").
pub fn is_running_under_wsl() -> bool {
static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| {
// Strong signals: env vars set by WSL when interop is available
let has_wsl_env = std::env::var_os("WSL_INTEROP").is_some()
|| std::env::var_os("WSL_DISTRO_NAME").is_some();

// Kernel hint: often contains "microsoft" under WSL1/2
let kernel_mentions_ms = std::fs::read_to_string("/proc/sys/kernel/osrelease")
.or_else(|_| std::fs::read_to_string("/proc/version"))
.map(|s| s.to_ascii_lowercase().contains("microsoft"))
.unwrap_or(false);

// Be conservative: require both a WSL env var AND a microsoft kernel hint
has_wsl_env && kernel_mentions_ms
})
}

/// Map a Windows drive-letter path (e.g. `C:\Users\Alice\file.png`) to a
/// WSL path (`/mnt/c/Users/Alice/file.png`). Returns `None` if the input
/// doesn't look like a drive-letter path.
pub fn try_map_windows_drive_to_wsl_path(win_path: &str) -> Option<PathBuf> {
let s = win_path.trim();
let mut chars = s.chars();
let drive = chars.next()?;
let colon = chars.next()?;
if !drive.is_ascii_alphabetic() || colon != ':' {
return None;
}
let rest = chars.as_str().trim_start_matches(['\\', '/']);
let drive_lower = drive.to_ascii_lowercase();
let mapped = format!("/mnt/{}/{}", drive_lower, rest.replace('\\', "/"));
Some(PathBuf::from(mapped))
}
103 changes: 103 additions & 0 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use crate::bottom_pane::prompt_args::prompt_has_numeric_placeholders;
use crate::slash_command::SlashCommand;
use crate::slash_command::built_in_slash_commands;
use crate::style::user_message_style;
use base64::Engine;
use codex_protocol::custom_prompts::CustomPrompt;
use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX;

Expand All @@ -46,10 +47,13 @@ use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::textarea::TextArea;
use crate::bottom_pane::textarea::TextAreaState;
use crate::clipboard_paste::normalize_pasted_path;
use crate::clipboard_paste::paste_image_to_temp_png;
use crate::clipboard_paste::pasted_image_format;
use crate::history_cell;
use crate::ui_consts::LIVE_PREFIX_COLS;
use codex_file_search::FileMatch;
use codex_protocol::platform::is_running_under_wsl;
use codex_protocol::platform::try_map_windows_drive_to_wsl_path;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
Expand Down Expand Up @@ -271,10 +275,70 @@ impl ChatComposer {
}

pub fn handle_paste_image_path(&mut self, pasted: String) -> bool {
// Support data: URLs (base64) pasted from some terminals/clients.
// If the pasted text is a data URL, decode it into a project-local
// ./.codex/tmp file and attach that file as an image.
let pasted_trim = pasted.trim();
if pasted_trim.starts_with("data:") {
if let Some(comma) = pasted_trim.find(',') {
let header = &pasted_trim[5..comma]; // after "data:"
let b64 = &pasted_trim[comma + 1..];
if header.contains("base64")
&& let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(b64)
{
// Try to determine extension from mime type
let ext = if header.contains("image/png") {
"png"
} else if header.contains("image/jpeg") || header.contains("image/jpg") {
"jpg"
} else {
"png"
};

if let Ok(cwd) = std::env::current_dir() {
let tmp_dir = cwd.join(".codex").join("tmp");
if std::fs::create_dir_all(&tmp_dir).is_ok() {
let uniq = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|d| d.as_millis().to_string())
.unwrap_or_else(|| "0".to_string());
let dest = tmp_dir.join(format!("pasted-{uniq}.{ext}"));
if std::fs::write(&dest, &decoded).is_ok()
&& let Ok((w, h)) = image::image_dimensions(&dest)
{
tracing::info!("OK (data URL pasted): {}", dest.display());
let format_label = pasted_image_format(&dest).label();
self.attach_image(dest, w, h, format_label);
return true;
}
}
}
// Fallthrough: if project-local write failed, try a system tempfile
if let Ok(tmp) = tempfile::Builder::new()
.suffix(&format!(".{ext}"))
.tempfile()
&& std::fs::write(tmp.path(), &decoded).is_ok()
&& let Ok((w, h)) = image::image_dimensions(tmp.path())
&& let Ok((_f, pathbuf)) = tmp.keep()
{
let format_label = pasted_image_format(&pathbuf).label();
self.attach_image(pathbuf, w, h, format_label);
return true;
}
}
}
return false;
}

let Some(path_buf) = normalize_pasted_path(&pasted) else {
return false;
};

// Try to read image dimensions for the normalized path. If that fails,
// attempt a WSL-style mapping for pasted Windows drive-letter paths
// (e.g. C:\Users\...) → /mnt/c/... which occurs when pasting from
// Windows into a WSL terminal.
match image::image_dimensions(&path_buf) {
Ok((w, h)) => {
tracing::info!("OK: {pasted}");
Expand All @@ -283,6 +347,16 @@ impl ChatComposer {
true
}
Err(err) => {
// Attempt simple Windows drive → /mnt mapping (only if it looks like a Windows path).
if let Some(mapped) = try_map_windows_drive_to_wsl_path(&path_buf.to_string_lossy())
&& let Ok((w, h)) = image::image_dimensions(&mapped)
{
tracing::info!("OK (WSL mapped): {}", mapped.display());
let format_label = pasted_image_format(&mapped).label();
self.attach_image(mapped, w, h, format_label);
return true;
}

tracing::info!("ERR: {err}");
false
}
Expand Down Expand Up @@ -871,6 +945,35 @@ impl ChatComposer {
if self.handle_shortcut_overlay_key(&key_event) {
return (InputResult::None, true);
}
// Support an explicit keyboard shortcut to paste image from clipboard
// using the native clipboard reader (arboard) or Windows PowerShell
// fallback when running under WSL. This avoids relying on the terminal
// to forward Ctrl+V as text. Shortcut: Ctrl+Alt+V.
if let KeyEvent {
code: KeyCode::Char('v'),
modifiers,
..
} = key_event
&& modifiers.contains(KeyModifiers::CONTROL)
&& modifiers.contains(KeyModifiers::ALT)
&& is_running_under_wsl()
{
match paste_image_to_temp_png() {
Ok((path, info)) => {
let format_label = pasted_image_format(&path).label();
self.attach_image(path, info.width, info.height, format_label);
return (InputResult::None, true);
}
Err(e) => {
tracing::warn!("paste_image_to_temp_png failed: {}", e.to_string());
let msg = format!("Failed to paste image from clipboard: {e}");
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::new_info_event(msg, None),
)));
return (InputResult::None, true);
}
}
}
if key_event.code == KeyCode::Esc {
if self.is_empty() {
let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running);
Expand Down
46 changes: 38 additions & 8 deletions codex-rs/tui/src/bottom_pane/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::render::line_utils::prefix_lines;
use crate::ui_consts::FOOTER_INDENT_COLS;
use codex_protocol::platform::is_running_under_wsl;
use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
Expand Down Expand Up @@ -89,10 +90,14 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
]);
vec![line]
}
FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState {
use_shift_enter_hint: props.use_shift_enter_hint,
esc_backtrack_hint: props.esc_backtrack_hint,
}),
FooterMode::ShortcutOverlay => {
let state = ShortcutsState {
use_shift_enter_hint: props.use_shift_enter_hint,
esc_backtrack_hint: props.esc_backtrack_hint,
is_wsl: is_running_under_wsl(),
};
shortcut_overlay_lines(state)
}
FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)],
FooterMode::ContextOnly => vec![context_window_line(props.context_window_percent)],
}
Expand All @@ -107,6 +112,7 @@ struct CtrlCReminderState {
struct ShortcutsState {
use_shift_enter_hint: bool,
esc_backtrack_hint: bool,
is_wsl: bool,
}

fn ctrl_c_reminder_line(state: CtrlCReminderState) -> Line<'static> {
Expand Down Expand Up @@ -254,6 +260,7 @@ enum DisplayCondition {
Always,
WhenShiftEnterHint,
WhenNotShiftEnterHint,
WhenUnderWSL,
}

impl DisplayCondition {
Expand All @@ -262,6 +269,7 @@ impl DisplayCondition {
DisplayCondition::Always => true,
DisplayCondition::WhenShiftEnterHint => state.use_shift_enter_hint,
DisplayCondition::WhenNotShiftEnterHint => !state.use_shift_enter_hint,
DisplayCondition::WhenUnderWSL => state.is_wsl,
}
}
}
Expand All @@ -280,6 +288,18 @@ impl ShortcutDescriptor {

fn overlay_entry(&self, state: ShortcutsState) -> Option<Line<'static>> {
let binding = self.binding_for(state)?;
// Special-case paste-image: when running under WSL prefer showing
// the explicit "ctrl + alt + v" hint (terminals often intercept
// plain Ctrl+V). We render a custom label instead of relying on a
// KeyBinding that combines modifiers (which cannot be created in a
// const context due to bitflags not being const-friendly).
if matches!(self.id, ShortcutId::PasteImage) && state.is_wsl {
let mut line = Line::from(vec![self.prefix.into()]);
line.push_span("ctrl + alt + v".dim());
line.push_span(self.label);
return Some(line);
}

let mut line = Line::from(vec![self.prefix.into(), binding.key.into()]);
match self.id {
ShortcutId::EditPrevious => {
Expand Down Expand Up @@ -335,10 +355,20 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
},
ShortcutDescriptor {
id: ShortcutId::PasteImage,
bindings: &[ShortcutBinding {
key: key_hint::ctrl(KeyCode::Char('v')),
condition: DisplayCondition::Always,
}],
// Show Ctrl+Alt+V when running under WSL (terminals often intercept plain
// Ctrl+V); otherwise fall back to Ctrl+V.
bindings: &[
ShortcutBinding {
// Use a plain 'v' binding here; overlay_entry will render the
// full "ctrl + alt + v" label when state.is_wsl is true.
key: key_hint::plain(KeyCode::Char('v')),
condition: DisplayCondition::WhenUnderWSL,
},
ShortcutBinding {
key: key_hint::ctrl(KeyCode::Char('v')),
condition: DisplayCondition::Always,
},
],
prefix: "",
label: " to paste images",
},
Expand Down
Loading