Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 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
a8069b0
Merge branch 'main' into wsl-image
Waxime64 Oct 30, 2025
89dc836
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
f7d3526
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
2c58b4d
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
51d4f59
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
88ef96b
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
7ffb551
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
0c87014
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
ed58f93
Merge branch 'dh--env-context-windows' into wsl-image + update code
Oct 31, 2025
3dc57ce
Merge branch 'main' into wsl-image
Waxime64 Oct 31, 2025
86e3097
Delete codex-rs/protocol/src/platform.rs
Waxime64 Oct 31, 2025
d90b29e
Merge branch 'main' into wsl-image
Waxime64 Nov 1, 2025
aa545f8
Merge branch 'main' into wsl-image
Waxime64 Nov 2, 2025
d856417
Merge branch 'main' into wsl-image
Waxime64 Nov 3, 2025
14790cf
Merge branch 'main' into wsl-image
Waxime64 Nov 3, 2025
1f8bee2
Merge branch 'main' into wsl-image
Waxime64 Nov 4, 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
105 changes: 105 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_core::environment_context::get_operating_system_info;
use codex_core::environment_context::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,37 @@ 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)
&& get_operating_system_info()
.and_then(|info| info.is_likely_windows_subsystem_for_linux)
.unwrap_or(false)
{
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
49 changes: 41 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_core::environment_context::get_operating_system_info;
use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
Expand Down Expand Up @@ -89,10 +90,17 @@ 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 is_wsl = get_operating_system_info()
.and_then(|info| info.is_likely_windows_subsystem_for_linux)
.unwrap_or(false);
let state = ShortcutsState {
use_shift_enter_hint: props.use_shift_enter_hint,
esc_backtrack_hint: props.esc_backtrack_hint,
is_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 +115,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 +263,7 @@ enum DisplayCondition {
Always,
WhenShiftEnterHint,
WhenNotShiftEnterHint,
WhenUnderWSL,
}

impl DisplayCondition {
Expand All @@ -262,6 +272,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 +291,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 +358,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
103 changes: 90 additions & 13 deletions codex-rs/tui/src/clipboard_paste.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use codex_core::environment_context::get_operating_system_info;
use codex_core::environment_context::try_map_windows_drive_to_wsl_path;
use std::path::Path;
use std::path::PathBuf;
use tempfile::Builder;
Expand Down Expand Up @@ -119,19 +121,94 @@ pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageErro
/// Convenience: write to a temp file and return its path + info.
#[cfg(not(target_os = "android"))]
pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
let (png, info) = paste_image_as_png()?;
// Create a unique temporary file with a .png suffix to avoid collisions.
let tmp = Builder::new()
.prefix("codex-clipboard-")
.suffix(".png")
.tempfile()
.map_err(|e| PasteImageError::IoError(e.to_string()))?;
std::fs::write(tmp.path(), &png).map_err(|e| PasteImageError::IoError(e.to_string()))?;
// Persist the file (so it remains after the handle is dropped) and return its PathBuf.
let (_file, path) = tmp
.keep()
.map_err(|e| PasteImageError::IoError(e.error.to_string()))?;
Ok((path, info))
// First attempt: read image from system clipboard via arboard (native paths or image data).
match paste_image_as_png() {
Ok((png, info)) => {
// Create a unique temporary file with a .png suffix to avoid collisions.
let tmp = Builder::new()
.prefix("codex-clipboard-")
.suffix(".png")
.tempfile()
.map_err(|e| PasteImageError::IoError(e.to_string()))?;
std::fs::write(tmp.path(), &png)
.map_err(|e| PasteImageError::IoError(e.to_string()))?;
// Persist the file (so it remains after the handle is dropped) and return its PathBuf.
let (_file, path) = tmp
.keep()
.map_err(|e| PasteImageError::IoError(e.error.to_string()))?;
Ok((path, info))
}
Err(e) => {
// If clipboard is unavailable (common under WSL because arboard cannot access
// the Windows clipboard), attempt a WSL fallback that calls PowerShell on the
// Windows side to write the clipboard image to a temporary file, then return
// the corresponding WSL path.
use PasteImageError::ClipboardUnavailable;
use PasteImageError::NoImage;

let is_wsl = get_operating_system_info()
.and_then(|info| info.is_likely_windows_subsystem_for_linux)
.unwrap_or(false);

if is_wsl && matches!(&e, ClipboardUnavailable(_) | NoImage(_)) {
tracing::debug!("attempting Windows PowerShell clipboard fallback");
if let Some(win_path) = try_dump_windows_clipboard_image() {
tracing::debug!("powershell produced path: {}", win_path);
if let Some(mapped_path) = try_map_windows_drive_to_wsl_path(&win_path)
&& let Ok((w, h)) = image::image_dimensions(&mapped_path)
{
// Return the mapped path directly without copying.
// The file will be read and base64-encoded during serialization.
return Ok((
mapped_path,
PastedImageInfo {
width: w,
height: h,
encoded_format: EncodedImageFormat::Png,
},
));
}
}
}
// If we reach here, fall through to returning the original error.
Err(e)
}
}
}

/// Try to call a Windows PowerShell command (several common names) to save the
/// clipboard image to a temporary PNG and return the Windows path to that file.
/// Returns None if no command succeeded or no image was present.
fn try_dump_windows_clipboard_image() -> Option<String> {
// Powershell script: save image from clipboard to a temp png and print the path.
// Force UTF-8 output to avoid encoding issues between powershell.exe (UTF-16LE default)
// and pwsh (UTF-8 default).
let script = r#"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $img = Get-Clipboard -Format Image; if ($img -ne $null) { $p=[System.IO.Path]::GetTempFileName(); $p = [System.IO.Path]::ChangeExtension($p,'png'); $img.Save($p,[System.Drawing.Imaging.ImageFormat]::Png); Write-Output $p } else { exit 1 }"#;

for cmd in ["powershell.exe", "pwsh", "powershell"] {
match std::process::Command::new(cmd)
.args(["-NoProfile", "-Command", script])
.output()
{
// Executing PowerShell command
Ok(output) => {
if output.status.success() {
// Decode as UTF-8 (forced by the script above).
let win_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !win_path.is_empty() {
tracing::debug!("{} saved clipboard image to {}", cmd, win_path);
return Some(win_path);
}
} else {
tracing::debug!("{} returned non-zero status", cmd);
}
}
Err(err) => {
tracing::debug!("{} not executable: {}", cmd, err);
}
}
}
None
}

#[cfg(target_os = "android")]
Expand Down
Loading