Compare commits

...

3 Commits

Author SHA1 Message Date
Codex + Matthew Zeng
6084fcf0f3 Fix sandbox multiprocessing test on Python 3.14 2026-01-16 12:30:50 -08:00
Anton Panasenko
e893e83eb9 feat: /fork the current session instead of opening session picker (#9385)
Implemented /fork to fork the current session directly (no picker),
handling it via a new ForkCurrentSession app event in both tui and tui2.
Updated slash command descriptions/tooltips and adjusted the fork tests
accordingly. Removed the unused in-session fork picker event.
2026-01-16 11:28:52 -08:00
viyatb-oai
f89a40a849 chore: upgrade to Rust 1.92.0 (#8860)
**Summary**
- Upgrade Rust toolchain used by CI to 1.92.0.
- Address new clippy `derivable_impls` warnings by deriving `Default`
for enums across protocol, core, backend openapi models, and
windows-sandbox setup.
- Tidy up related test/config behavior (originator header handling, env
override cleanup) and remove a now-unused assignment in TUI/TUI2 render
layout.

**Testing**
- `just fmt`
- `just fix -p codex-tui`
- `just fix -p codex-tui2`
- `just fix -p codex-windows-sandbox`
- `cargo test -p codex-tui`
- `cargo test -p codex-tui2`
- `cargo test -p codex-windows-sandbox`
- `cargo test -p codex-core --test all`
- `cargo test -p codex-app-server --test all`
- `cargo test -p codex-mcp-server --test all`
- `cargo test --all-features`
2026-01-16 11:12:52 -08:00
27 changed files with 152 additions and 187 deletions

View File

@@ -59,7 +59,7 @@ jobs:
working-directory: codex-rs
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
- uses: dtolnay/rust-toolchain@1.92
with:
components: rustfmt
- name: cargo fmt
@@ -77,7 +77,7 @@ jobs:
working-directory: codex-rs
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
- uses: dtolnay/rust-toolchain@1.92
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: cargo-shear
@@ -177,7 +177,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
- uses: dtolnay/rust-toolchain@1.92
with:
targets: ${{ matrix.target }}
components: clippy
@@ -416,7 +416,7 @@ jobs:
- name: Install DotSlash
uses: facebook/install-dotslash@v2
- uses: dtolnay/rust-toolchain@1.90
- uses: dtolnay/rust-toolchain@1.92
with:
targets: ${{ matrix.target }}

View File

@@ -80,7 +80,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
- uses: dtolnay/rust-toolchain@1.92
with:
targets: ${{ matrix.target }}

View File

@@ -24,7 +24,7 @@ jobs:
node-version: 22
cache: pnpm
- uses: dtolnay/rust-toolchain@1.90
- uses: dtolnay/rust-toolchain@1.92
- name: build codex
run: cargo build --bin codex

View File

@@ -93,7 +93,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
- uses: dtolnay/rust-toolchain@1.92
with:
targets: ${{ matrix.target }}

View File

@@ -42,9 +42,12 @@ impl RateLimitStatusPayload {
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[derive(
Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Default,
)]
pub enum PlanType {
#[serde(rename = "guest")]
#[default]
Guest,
#[serde(rename = "free")]
Free,
@@ -71,9 +74,3 @@ pub enum PlanType {
#[serde(rename = "edu")]
Edu,
}
impl Default for PlanType {
fn default() -> PlanType {
Self::Guest
}
}

View File

@@ -423,10 +423,11 @@ impl Default for Notifications {
/// Terminals generally encode both mouse wheels and trackpads as the same "scroll up/down" mouse
/// button events, without a magnitude. This setting controls whether Codex uses a heuristic to
/// infer wheel vs trackpad per stream, or forces a specific behavior.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum ScrollInputMode {
/// Infer wheel vs trackpad behavior per scroll stream.
#[default]
Auto,
/// Always treat scroll events as mouse-wheel input (fixed lines per tick).
Wheel,
@@ -434,12 +435,6 @@ pub enum ScrollInputMode {
Trackpad,
}
impl Default for ScrollInputMode {
fn default() -> Self {
Self::Auto
}
}
/// Collection of settings that are specific to the TUI.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]

View File

@@ -340,6 +340,7 @@ mod detect_shell_type_tests {
#[cfg(unix)]
mod tests {
use super::*;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
@@ -350,7 +351,7 @@ mod tests {
let shell_path = zsh_shell.shell_path;
assert_eq!(shell_path, PathBuf::from("/bin/zsh"));
assert_eq!(shell_path, Path::new("/bin/zsh"));
}
#[test]
@@ -360,7 +361,7 @@ mod tests {
let shell_path = zsh_shell.shell_path;
assert_eq!(shell_path, PathBuf::from("/bin/zsh"));
assert_eq!(shell_path, Path::new("/bin/zsh"));
}
#[test]
@@ -369,9 +370,9 @@ mod tests {
let shell_path = bash_shell.shell_path;
assert!(
shell_path == PathBuf::from("/bin/bash")
|| shell_path == PathBuf::from("/usr/bin/bash")
|| shell_path == PathBuf::from("/usr/local/bin/bash"),
shell_path == Path::new("/bin/bash")
|| shell_path == Path::new("/usr/bin/bash")
|| shell_path == Path::new("/usr/local/bin/bash"),
"shell path: {shell_path:?}",
);
}
@@ -381,7 +382,7 @@ mod tests {
let sh_shell = get_shell(ShellType::Sh, None).unwrap();
let shell_path = sh_shell.shell_path;
assert!(
shell_path == PathBuf::from("/bin/sh") || shell_path == PathBuf::from("/usr/bin/sh"),
shell_path == Path::new("/bin/sh") || shell_path == Path::new("/usr/bin/sh"),
"shell path: {shell_path:?}",
);
}

View File

@@ -40,9 +40,10 @@ struct ReadFileArgs {
indentation: Option<IndentationArgs>,
}
#[derive(Deserialize)]
#[derive(Deserialize, Default)]
#[serde(rename_all = "snake_case")]
enum ReadMode {
#[default]
Slice,
Indentation,
}
@@ -461,12 +462,6 @@ mod defaults {
}
}
impl Default for ReadMode {
fn default() -> Self {
Self::Slice
}
}
pub fn offset() -> usize {
1
}

View File

@@ -77,8 +77,17 @@ async fn python_multiprocessing_lock_works_under_sandbox() {
};
let python_code = r#"import multiprocessing
import sys
from multiprocessing import Lock, Process
# Python 3.14 defaults to forkserver on some Linux distros, which can
# be blocked by the sandbox. Force fork to keep the test stable.
if sys.platform.startswith("linux"):
try:
multiprocessing.set_start_method("fork")
except RuntimeError:
pass
def f(lock):
with lock:
print("Lock acquired in child process")

View File

@@ -1656,21 +1656,18 @@ pub struct ReviewLineRange {
pub end: u32,
}
#[derive(Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[derive(
Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum ExecCommandSource {
#[default]
Agent,
UserShell,
UnifiedExecStartup,
UnifiedExecInteraction,
}
impl Default for ExecCommandSource {
fn default() -> Self {
Self::Agent
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecCommandBeginEvent {
/// Identifier so this can be paired with the ExecCommandEnd event.

View File

@@ -1,3 +1,3 @@
[toolchain]
channel = "1.90.0"
channel = "1.92.0"
components = ["clippy", "rustfmt", "rust-src"]

View File

@@ -179,7 +179,7 @@ if (-not (Ensure-Command 'cargo')) {
Write-Host "==> Configuring Rust toolchain per rust-toolchain.toml" -ForegroundColor Cyan
# Pin to the workspace toolchain and install components
$toolchain = '1.90.0'
$toolchain = '1.92.0'
& rustup toolchain install $toolchain --profile minimal | Out-Host
& rustup default $toolchain | Out-Host
& rustup component add clippy rustfmt rust-src --toolchain $toolchain | Out-Host

View File

@@ -758,73 +758,61 @@ impl App {
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
tui.frame_requester().schedule_frame();
}
AppEvent::OpenForkPicker => {
match crate::resume_picker::run_fork_picker(
tui,
&self.config.codex_home,
&self.config.model_provider_id,
false,
)
.await?
{
SessionSelection::Fork(path) => {
let summary = session_summary(
self.chat_widget.token_usage(),
self.chat_widget.thread_id(),
);
match self
.server
.fork_thread(usize::MAX, self.config.clone(), path.clone())
.await
{
Ok(forked) => {
self.shutdown_current_thread().await;
let init = crate::chatwidget::ChatWidgetInit {
config: self.config.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
initial_images: Vec::new(),
enhanced_keys_supported: self.enhanced_keys_supported,
auth_manager: self.auth_manager.clone(),
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model: Some(self.current_model.clone()),
};
self.chat_widget = ChatWidget::new_from_existing(
init,
forked.thread,
forked.session_configured,
);
self.current_model = model_info.slug.clone();
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().into()];
if let Some(command) = summary.resume_command {
let spans = vec![
"To continue this session, run ".into(),
command.cyan(),
];
lines.push(spans.into());
}
self.chat_widget.add_plain_history_lines(lines);
AppEvent::ForkCurrentSession => {
let summary =
session_summary(self.chat_widget.token_usage(), self.chat_widget.thread_id());
if let Some(path) = self.chat_widget.rollout_path() {
match self
.server
.fork_thread(usize::MAX, self.config.clone(), path.clone())
.await
{
Ok(forked) => {
self.shutdown_current_thread().await;
let init = crate::chatwidget::ChatWidgetInit {
config: self.config.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
initial_images: Vec::new(),
enhanced_keys_supported: self.enhanced_keys_supported,
auth_manager: self.auth_manager.clone(),
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model: Some(self.current_model.clone()),
};
self.chat_widget = ChatWidget::new_from_existing(
init,
forked.thread,
forked.session_configured,
);
self.current_model = model_info.slug.clone();
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().into()];
if let Some(command) = summary.resume_command {
let spans = vec![
"To continue this session, run ".into(),
command.cyan(),
];
lines.push(spans.into());
}
}
Err(err) => {
let path_display = path.display();
self.chat_widget.add_error_message(format!(
"Failed to fork session from {path_display}: {err}"
));
self.chat_widget.add_plain_history_lines(lines);
}
}
Err(err) => {
let path_display = path.display();
self.chat_widget.add_error_message(format!(
"Failed to fork current session from {path_display}: {err}"
));
}
}
SessionSelection::Exit
| SessionSelection::StartFresh
| SessionSelection::Resume(_) => {}
} else {
self.chat_widget
.add_error_message("Current session is not ready to fork yet.".to_string());
}
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
tui.frame_requester().schedule_frame();
}
AppEvent::InsertHistoryCell(cell) => {

View File

@@ -53,8 +53,8 @@ pub(crate) enum AppEvent {
/// Open the resume picker inside the running TUI session.
OpenResumePicker,
/// Open the fork picker inside the running TUI session.
OpenForkPicker,
/// Fork the current session into a new thread.
ForkCurrentSession,
/// Request to exit the application.
///

View File

@@ -1994,7 +1994,7 @@ impl ChatWidget {
self.app_event_tx.send(AppEvent::OpenResumePicker);
}
SlashCommand::Fork => {
self.app_event_tx.send(AppEvent::OpenForkPicker);
self.app_event_tx.send(AppEvent::ForkCurrentSession);
}
SlashCommand::Init => {
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);

View File

@@ -1532,12 +1532,12 @@ async fn slash_resume_opens_picker() {
}
#[tokio::test]
async fn slash_fork_opens_picker() {
async fn slash_fork_requests_current_fork() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
chat.dispatch_command(SlashCommand::Fork);
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenForkPicker));
assert_matches!(rx.try_recv(), Ok(AppEvent::ForkCurrentSession));
}
#[tokio::test]

View File

@@ -275,7 +275,6 @@ impl<'a> FlexRenderable<'a> {
};
let child_size = child.desired_height(area.width).min(max_child_extent);
child_sizes[i] = child_size;
allocated_size += child_size;
allocated_flex_space += child_size;
}
}

View File

@@ -48,7 +48,7 @@ impl SlashCommand {
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
SlashCommand::Review => "review my current changes and find issues",
SlashCommand::Resume => "resume a saved chat",
SlashCommand::Fork => "fork a saved chat",
SlashCommand::Fork => "fork the current chat",
// SlashCommand::Undo => "ask Codex to undo a turn",
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
SlashCommand::Diff => "show git diff (including untracked files)",

View File

@@ -6,7 +6,7 @@ Use /approvals to control when Codex asks for confirmation.
Run /review to get a code review of your current changes.
Use /skills to list available skills or ask Codex to use one.
Use /status to see the current model, approvals, and token usage.
Use /fork to branch a saved chat into a new thread.
Use /fork to branch the current chat into a new thread.
Use /init to create an AGENTS.md with project-specific guidance.
Use /mcp to list configured MCP tools.
You can run any shell command from Codex using `!` (e.g. `!ls`)

View File

@@ -1531,72 +1531,62 @@ impl App {
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
tui.frame_requester().schedule_frame();
}
AppEvent::OpenForkPicker => {
match crate::resume_picker::run_fork_picker(
tui,
&self.config.codex_home,
&self.config.model_provider_id,
false,
)
.await?
{
SessionSelection::Fork(path) => {
let summary = session_summary(
self.chat_widget.token_usage(),
self.chat_widget.conversation_id(),
);
match self
.server
.fork_thread(usize::MAX, self.config.clone(), path.clone())
.await
{
Ok(forked) => {
self.shutdown_current_conversation().await;
let init = crate::chatwidget::ChatWidgetInit {
config: self.config.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
initial_images: Vec::new(),
enhanced_keys_supported: self.enhanced_keys_supported,
auth_manager: self.auth_manager.clone(),
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model: Some(self.current_model.clone()),
};
self.chat_widget = ChatWidget::new_from_existing(
init,
forked.thread,
forked.session_configured,
);
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().into()];
if let Some(command) = summary.resume_command {
let spans = vec![
"To continue this session, run ".into(),
command.cyan(),
];
lines.push(spans.into());
}
self.chat_widget.add_plain_history_lines(lines);
AppEvent::ForkCurrentSession => {
let summary = session_summary(
self.chat_widget.token_usage(),
self.chat_widget.conversation_id(),
);
if let Some(path) = self.chat_widget.rollout_path() {
match self
.server
.fork_thread(usize::MAX, self.config.clone(), path.clone())
.await
{
Ok(forked) => {
self.shutdown_current_conversation().await;
let init = crate::chatwidget::ChatWidgetInit {
config: self.config.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
initial_images: Vec::new(),
enhanced_keys_supported: self.enhanced_keys_supported,
auth_manager: self.auth_manager.clone(),
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model: Some(self.current_model.clone()),
};
self.chat_widget = ChatWidget::new_from_existing(
init,
forked.thread,
forked.session_configured,
);
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().into()];
if let Some(command) = summary.resume_command {
let spans = vec![
"To continue this session, run ".into(),
command.cyan(),
];
lines.push(spans.into());
}
}
Err(err) => {
let path_display = path.display();
self.chat_widget.add_error_message(format!(
"Failed to fork session from {path_display}: {err}"
));
self.chat_widget.add_plain_history_lines(lines);
}
}
Err(err) => {
let path_display = path.display();
self.chat_widget.add_error_message(format!(
"Failed to fork current session from {path_display}: {err}"
));
}
}
SessionSelection::Exit
| SessionSelection::StartFresh
| SessionSelection::Resume(_) => {}
} else {
self.chat_widget
.add_error_message("Current session is not ready to fork yet.".to_string());
}
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
tui.frame_requester().schedule_frame();
}
AppEvent::InsertHistoryCell(cell) => {

View File

@@ -47,8 +47,8 @@ pub(crate) enum AppEvent {
/// Open the resume picker inside the running TUI session.
OpenResumePicker,
/// Open the fork picker inside the running TUI session.
OpenForkPicker,
/// Fork the current session into a new thread.
ForkCurrentSession,
/// Request to exit the application.
///

View File

@@ -1769,7 +1769,7 @@ impl ChatWidget {
self.app_event_tx.send(AppEvent::OpenResumePicker);
}
SlashCommand::Fork => {
self.app_event_tx.send(AppEvent::OpenForkPicker);
self.app_event_tx.send(AppEvent::ForkCurrentSession);
}
SlashCommand::Init => {
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);

View File

@@ -1338,12 +1338,12 @@ async fn slash_resume_opens_picker() {
}
#[tokio::test]
async fn slash_fork_opens_picker() {
async fn slash_fork_requests_current_fork() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
chat.dispatch_command(SlashCommand::Fork);
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenForkPicker));
assert_matches!(rx.try_recv(), Ok(AppEvent::ForkCurrentSession));
}
#[tokio::test]

View File

@@ -275,7 +275,6 @@ impl<'a> FlexRenderable<'a> {
};
let child_size = child.desired_height(area.width).min(max_child_extent);
child_sizes[i] = child_size;
allocated_size += child_size;
allocated_flex_space += child_size;
}
}

View File

@@ -46,7 +46,7 @@ impl SlashCommand {
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
SlashCommand::Review => "review my current changes and find issues",
SlashCommand::Resume => "resume a saved chat",
SlashCommand::Fork => "fork a saved chat",
SlashCommand::Fork => "fork the current chat",
// SlashCommand::Undo => "ask Codex to undo a turn",
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
SlashCommand::Diff => "show git diff (including untracked files)",

View File

@@ -6,7 +6,7 @@ Use /approvals to control when Codex asks for confirmation.
Run /review to get a code review of your current changes.
Use /skills to list available skills or ask Codex to use one.
Use /status to see the current model, approvals, and token usage.
Use /fork to branch a saved chat into a new thread.
Use /fork to branch the current chat into a new thread.
Use /init to create an AGENTS.md with project-specific guidance.
Use /mcp to list configured MCP tools.
You can run any shell command from Codex using `!` (e.g. `!ls`)

View File

@@ -76,19 +76,14 @@ struct Payload {
refresh_only: bool,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
enum SetupMode {
#[default]
Full,
ReadAclsOnly,
}
impl Default for SetupMode {
fn default() -> Self {
Self::Full
}
}
fn log_line(log: &mut File, msg: &str) -> Result<()> {
let ts = chrono::Utc::now().to_rfc3339();
writeln!(log, "[{ts}] {msg}")?;