tui: encode queued resume paths losslessly

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-15 23:32:52 -07:00
co-authored by Codex
parent 5ebd3486a8
commit 51d1e061f8
2 changed files with 95 additions and 10 deletions
+70 -10
View File
@@ -29,6 +29,7 @@ use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
@@ -38,6 +39,16 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use base64::Engine;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::ffi::OsStringExt;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
#[cfg(windows)]
use std::os::windows::ffi::OsStringExt;
use self::realtime::PendingSteerCompareKey;
use crate::app_event::RealtimeAudioDeviceKind;
use crate::app_event::WindowsSandboxEnableMode;
@@ -5327,7 +5338,9 @@ impl ChatWidget {
);
}
};
if !(args.len() == 1 || (args.len() == 3 && args[1] == "--path")) {
if !(args.len() == 1
|| (args.len() == 3 && matches!(args[1].as_str(), "--path" | "--path-base64")))
{
return self.restore_invalid_inline_slash_command(
cmd,
args_message,
@@ -5337,11 +5350,44 @@ impl ChatWidget {
match ThreadId::from_string(&args[0]) {
Ok(thread_id) => {
if let Some(path) = args.get(2) {
let path = match args[1].as_str() {
"--path" => PathBuf::from(path),
"--path-base64" => {
let Ok(bytes) =
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(path)
else {
return self.restore_invalid_inline_slash_command(
cmd,
args_message,
"Invalid encoded rollout path for /resume.".to_string(),
);
};
#[cfg(unix)]
let path = PathBuf::from(OsString::from_vec(bytes));
#[cfg(windows)]
let path = {
let mut chunks = bytes.chunks_exact(2);
if !chunks.remainder().is_empty() {
return self.restore_invalid_inline_slash_command(
cmd,
args_message,
"Invalid encoded rollout path for /resume."
.to_string(),
);
}
let wide = chunks
.by_ref()
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect::<Vec<_>>();
PathBuf::from(OsString::from_wide(&wide))
};
path
}
_ => unreachable!("validated resume path flag"),
};
self.app_event_tx.send(AppEvent::ResumeSessionTarget(
crate::resume_picker::SessionTarget {
path: PathBuf::from(path),
thread_id,
},
crate::resume_picker::SessionTarget { path, thread_id },
));
} else {
self.app_event_tx.send(AppEvent::ResumeSession(thread_id));
@@ -5720,13 +5766,27 @@ impl ChatWidget {
pub(crate) fn resume_selection_draft(
target_session: &crate::resume_picker::SessionTarget,
) -> UserMessage {
let (path_flag, path_value) = if let Some(path) = target_session.path.to_str() {
("--path".to_string(), path.to_string())
} else {
#[cfg(unix)]
let bytes = target_session.path.as_os_str().as_bytes().to_vec();
#[cfg(windows)]
let bytes = target_session
.path
.as_os_str()
.encode_wide()
.flat_map(u16::to_le_bytes)
.collect::<Vec<_>>();
(
"--path-base64".to_string(),
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes),
)
};
SlashCommandInvocation::with_args(
SlashCommand::Resume,
[
target_session.thread_id.to_string(),
"--path".to_string(),
target_session.path.display().to_string(),
],
[target_session.thread_id.to_string(), path_flag, path_value],
)
.into_user_message()
}
+25
View File
@@ -129,6 +129,10 @@ use pretty_assertions::assert_eq;
use serial_test::serial;
use std::collections::BTreeMap;
use std::collections::HashSet;
#[cfg(unix)]
use std::ffi::OsString;
#[cfg(unix)]
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use tempfile::tempdir;
@@ -5736,6 +5740,27 @@ async fn queued_resume_picker_selection_replays_exact_targets_in_order() {
assert_matches!(rx.try_recv(), Ok(AppEvent::ResumeSessionTarget(target)) if target == second);
}
#[cfg(unix)]
#[tokio::test]
async fn queued_resume_picker_selection_preserves_non_utf8_path() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
chat.bottom_pane.set_task_running(true);
let target = crate::resume_picker::SessionTarget {
path: PathBuf::from(OsString::from_vec(b"/tmp/non-utf8-\xff.rollout".to_vec())),
thread_id: ThreadId::new(),
};
let draft = ChatWidget::resume_selection_draft(&target);
assert!(draft.text.contains("--path-base64"));
chat.handle_serialized_slash_command(draft);
chat.bottom_pane.set_task_running(false);
chat.drain_queued_inputs_until_blocked();
assert_matches!(rx.try_recv(), Ok(AppEvent::ResumeSessionTarget(replayed)) if replayed == target);
}
#[tokio::test]
async fn collaboration_modes_defaults_to_code_on_startup() {
let codex_home = tempdir().expect("tempdir");