Add non-interactive resume filter option (#15339)

## Summary
- add `codex resume --include-non-interactive` to include
non-interactive sessions in the picker and `--last`
- keep current-provider and cwd filtering behavior unchanged
- replace the picker API boolean with a `SessionSourceFilter` enum to
avoid a boolean trap

## Tests
- `cargo test -p codex-cli`
- `cargo test -p codex-tui`
- `just fmt`
- `just fix -p codex-cli`
- `just fix -p codex-tui`
This commit is contained in:
Jeremy Rose
2026-03-25 11:05:07 -07:00
committed by GitHub
Unverified
parent fba3c79885
commit 6b10e186c4
5 changed files with 81 additions and 7 deletions
+19
View File
@@ -208,6 +208,10 @@ struct ResumeCommand {
#[arg(long = "all", default_value_t = false)]
all: bool,
/// Include non-interactive sessions in the resume picker and --last selection.
#[arg(long = "include-non-interactive", default_value_t = false)]
include_non_interactive: bool,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
@@ -691,6 +695,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
session_id,
last,
all,
include_non_interactive,
remote,
config_overrides,
})) => {
@@ -700,6 +705,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
session_id,
last,
all,
include_non_interactive,
config_overrides,
);
let exit_info = run_interactive_tui(
@@ -1169,6 +1175,7 @@ fn finalize_resume_interactive(
session_id: Option<String>,
last: bool,
show_all: bool,
include_non_interactive: bool,
resume_cli: TuiCli,
) -> TuiCli {
// Start with the parsed interactive CLI so resume shares the same
@@ -1178,6 +1185,7 @@ fn finalize_resume_interactive(
interactive.resume_last = last;
interactive.resume_session_id = resume_session_id;
interactive.resume_show_all = show_all;
interactive.resume_include_non_interactive = include_non_interactive;
// Merge resume-scoped flags and overrides with highest precedence.
merge_interactive_cli_flags(&mut interactive, resume_cli);
@@ -1290,6 +1298,7 @@ mod tests {
session_id,
last,
all,
include_non_interactive,
remote: _,
config_overrides: resume_cli,
}) = subcommand.expect("resume present")
@@ -1303,6 +1312,7 @@ mod tests {
session_id,
last,
all,
include_non_interactive,
resume_cli,
)
}
@@ -1498,6 +1508,15 @@ mod tests {
assert!(interactive.resume_show_all);
}
#[test]
fn resume_include_non_interactive_flag_sets_source_filter_override() {
let interactive =
finalize_resume_from_args(["codex", "resume", "--include-non-interactive"].as_ref());
assert!(interactive.resume_picker);
assert!(interactive.resume_include_non_interactive);
}
#[test]
fn resume_merges_option_flags_and_full_auto() {
let interactive = finalize_resume_from_args(
+1
View File
@@ -2819,6 +2819,7 @@ impl App {
tui,
&self.config,
/*show_all*/ false,
crate::resume_picker::SessionSourceFilter::InteractiveOnly,
)
.await?
{
+4
View File
@@ -32,6 +32,10 @@ pub struct Cli {
#[clap(skip)]
pub resume_show_all: bool,
/// Internal: include non-interactive sessions in resume listings.
#[clap(skip)]
pub resume_include_non_interactive: bool,
// Internal controls set by the top-level `codex fork` subcommand.
// These are not exposed as user flags on the base `codex` command.
#[clap(skip)]
+19 -2
View File
@@ -845,12 +845,17 @@ async fn run_ratatui_app(
} else {
Some(config.cwd.as_path())
};
let allowed_sources = if cli.resume_include_non_interactive {
&[][..]
} else {
INTERACTIVE_SESSION_SOURCES.as_slice()
};
match RolloutRecorder::find_latest_thread_path(
&config,
/*page_size*/ 1,
/*cursor*/ None,
ThreadSortKey::UpdatedAt,
INTERACTIVE_SESSION_SOURCES.as_slice(),
allowed_sources,
Some(provider_filter.as_slice()),
&config.model_provider_id,
filter_cwd,
@@ -888,7 +893,19 @@ async fn run_ratatui_app(
_ => resume_picker::SessionSelection::StartFresh,
}
} else if cli.resume_picker {
match resume_picker::run_resume_picker(&mut tui, &config, cli.resume_show_all).await? {
let source_filter = if cli.resume_include_non_interactive {
resume_picker::SessionSourceFilter::IncludeNonInteractive
} else {
resume_picker::SessionSourceFilter::InteractiveOnly
};
match resume_picker::run_resume_picker(
&mut tui,
&config,
cli.resume_show_all,
source_filter,
)
.await?
{
resume_picker::SessionSelection::Exit => {
terminal_restore_guard.restore_silently();
session_log::log_session_end();
+38 -5
View File
@@ -60,6 +60,21 @@ pub enum SessionPickerAction {
Fork,
}
#[derive(Clone, Copy, Debug)]
pub enum SessionSourceFilter {
InteractiveOnly,
IncludeNonInteractive,
}
impl SessionSourceFilter {
fn allowed_sources(self) -> &'static [codex_protocol::protocol::SessionSource] {
match self {
SessionSourceFilter::InteractiveOnly => INTERACTIVE_SESSION_SOURCES.as_slice(),
SessionSourceFilter::IncludeNonInteractive => &[],
}
}
}
impl SessionPickerAction {
fn title(self) -> &'static str {
match self {
@@ -116,15 +131,24 @@ enum BackgroundEvent {
/// new sessions appear during pagination.
///
/// Filtering happens in two layers:
/// 1. Provider and source filtering at the backend (only interactive CLI sessions
/// for the current model provider).
/// 1. Provider and source filtering at the backend (interactive sessions for the
/// current model provider by default, optionally including non-interactive
/// sessions).
/// 2. Working-directory filtering at the picker (unless `--all` is passed).
pub async fn run_resume_picker(
tui: &mut Tui,
config: &Config,
show_all: bool,
source_filter: SessionSourceFilter,
) -> Result<SessionSelection> {
run_session_picker(tui, config, show_all, SessionPickerAction::Resume).await
run_session_picker(
tui,
config,
show_all,
source_filter,
SessionPickerAction::Resume,
)
.await
}
pub async fn run_fork_picker(
@@ -132,13 +156,21 @@ pub async fn run_fork_picker(
config: &Config,
show_all: bool,
) -> Result<SessionSelection> {
run_session_picker(tui, config, show_all, SessionPickerAction::Fork).await
run_session_picker(
tui,
config,
show_all,
SessionSourceFilter::InteractiveOnly,
SessionPickerAction::Fork,
)
.await
}
async fn run_session_picker(
tui: &mut Tui,
config: &Config,
show_all: bool,
source_filter: SessionSourceFilter,
action: SessionPickerAction,
) -> Result<SessionSelection> {
let alt = AltScreenGuard::enter(tui);
@@ -154,6 +186,7 @@ async fn run_session_picker(
let config = config.clone();
let loader_tx = bg_tx.clone();
let allowed_sources = source_filter.allowed_sources();
let page_loader: PageLoader = Arc::new(move |request: PageLoadRequest| {
let tx = loader_tx.clone();
let config = config.clone();
@@ -164,7 +197,7 @@ async fn run_session_picker(
PAGE_SIZE,
request.cursor.as_ref(),
request.sort_key,
INTERACTIVE_SESSION_SOURCES.as_slice(),
allowed_sources,
Some(provider_filter.as_slice()),
request.default_provider.as_str(),
/*search_term*/ None,