Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
9478b34
queue slash commands in tui
charley-oai Mar 10, 2026
072d5d9
remove dead tui helper methods
charley-oai Mar 10, 2026
a8f1f43
preserve queued slash actions on interrupt
charley-oai Mar 10, 2026
e5f1b84
queue interactive slash command selections
charley-oai Mar 11, 2026
9ab49d2
narrow esc interrupts in tui
charley-oai Mar 11, 2026
35e8aa9
Simplify queued slash command replay
charley-oai Mar 11, 2026
fa50564
tui: restore interactive slash queue behavior
charley-oai Mar 11, 2026
122e147
tui: simplify interrupt status plumbing
charley-oai Mar 11, 2026
ed546bc
tui: preserve queued slash draft intent
charley-oai Mar 11, 2026
393740d
tui: clarify live slash queueing branch
charley-oai Mar 11, 2026
8267155
tui: document slash dispatch replay contract
charley-oai Mar 12, 2026
173d0ef
tui: resolve interactive slash commands before queueing
charley-oai Mar 12, 2026
bb3e618
tui: clarify queue replay stop comment
charley-oai Mar 12, 2026
217da0c
tui: make slash command helpers exhaustive
charley-oai Mar 12, 2026
8c45c1a
tui: canonicalize interactive slash drafts
charley-oai Mar 12, 2026
bcb3555
tui: reject repeated model modifiers
charley-oai Mar 12, 2026
7ea03de
tui: resume queued replay after idle slash actions
charley-oai Mar 12, 2026
ad586ba
tui: share slash command shlex codec
charley-oai Mar 12, 2026
8fda4e0
tui: add slash command help page
charley-oai Mar 12, 2026
6a278e9
tui: make slash help dismissible
charley-oai Mar 12, 2026
528e3df
tui: let slash help grow with terminal
charley-oai Mar 12, 2026
8fbc9d3
tui: let q dismiss slash help
charley-oai Mar 12, 2026
77e1e80
tui: add slash help search and status
charley-oai Mar 12, 2026
6e20c5a
tui: refine slash help search controls
charley-oai Mar 12, 2026
0727144
tui: space slash help footer
charley-oai Mar 12, 2026
56c7646
tui: shorten slash help close hint
charley-oai Mar 12, 2026
541bc6e
tui: let shift-n go backward in slash help search
charley-oai Mar 13, 2026
e477780
tui: clear slash help search before closing
charley-oai Mar 13, 2026
2268f98
tui: clean up slash help footer hints
charley-oai Mar 13, 2026
93ead12
tui: block queued replay behind open popups
charley-oai Mar 13, 2026
efa6715
tui: cfg-gate realtime settings draft helper
charley-oai Mar 13, 2026
d8c0839
tui: reject unavailable queued slash drafts
charley-oai Mar 13, 2026
7250e82
tui: preserve world-writable warning opt-out
charley-oai Mar 13, 2026
96fe896
codex: fix CI failure on PR #14170
charley-oai Mar 13, 2026
614936d
tui: fix duplicate windows sandbox import
charley-oai Mar 13, 2026
3a6e3a9
tui: remove dead windows sandbox event
charley-oai Mar 13, 2026
6487398
codex: fix windows slash help snapshots (#14170)
charley-oai Mar 13, 2026
ef2b09c
tui: preserve selected resume target path
charley-oai Mar 13, 2026
7bcff58
tui: reject unavailable queued slash drafts on replay
charley-oai Mar 13, 2026
295f7fe
tui: serialize exact resume picker targets
charley-oai Mar 13, 2026
3b4b483
tui: route elevated approvals through setup flow
charley-oai Mar 13, 2026
a80a654
tui: honor resume cwd prompt exits
charley-oai Mar 13, 2026
02f6733
tui: clear empty-arg slash drafts on dispatch
charley-oai Mar 13, 2026
027524d
tui: classify slash commands by execution kind
charley-oai Mar 13, 2026
7e91f70
tui: run legacy sandbox preflight before enable
charley-oai Mar 13, 2026
5ebd348
tui: refresh windows slash help snapshots
charley-oai Mar 13, 2026
51d1e06
tui: encode queued resume paths losslessly
charley-oai Mar 13, 2026
39f436f
tui: complete legacy sandbox preflight asynchronously
charley-oai Mar 13, 2026
eb31929
tui: align slash help subagents naming
charley-oai Mar 13, 2026
90e964f
tui: pause replay after personality updates
charley-oai Mar 13, 2026
d65960d
tui: restore multi-agents slash alias
charley-oai Mar 13, 2026
1a54e52
tui: dedupe alias-expanded popup commands
charley-oai Mar 13, 2026
a6ffd1c
tui: gate slash help by runtime availability
charley-oai Mar 13, 2026
e0627c8
tui: reserve builtin aliases in command popup
charley-oai Mar 14, 2026
6d0be7f
tui: preserve queued drafts across session switches
charley-oai Mar 14, 2026
f34490a
tui: gate inline feedback command
charley-oai Mar 14, 2026
6cc1476
tui: fix windows selection action inference
charley-oai Mar 14, 2026
8d3ed74
tui: defer windows sandbox popup selection
charley-oai Mar 14, 2026
11d49d9
tui: queue smart approvals updates
charley-oai Mar 14, 2026
3b8dbed
codex: address PR review feedback (#14170)
charley-oai Mar 15, 2026
89f7c08
codex: address PR review feedback (#14170)
charley-oai Mar 15, 2026
5d81980
codex: fix windows help snapshots (#14170)
charley-oai Mar 15, 2026
9ec1452
tui: simplify slash command policy wiring
charley-oai Mar 15, 2026
d932510
tui: resume queued drafts after inline app events
charley-oai Mar 16, 2026
f701d9e
tui: match slash popup aliases
charley-oai Mar 16, 2026
ef28639
tui: accept plain p in help search
charley-oai Mar 16, 2026
6c9bac8
tui: wait for async slash replay updates
charley-oai Mar 16, 2026
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
676 changes: 555 additions & 121 deletions codex-rs/tui/src/app.rs

Large diffs are not rendered by default.

44 changes: 41 additions & 3 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use codex_utils_approval_presets::ApprovalPreset;

use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::StatusLineItem;
use crate::chatwidget::UserMessage;
use crate::history_cell::HistoryCell;

use codex_core::config::types::ApprovalsReviewer;
Expand Down Expand Up @@ -97,6 +98,12 @@ pub(crate) enum AppEvent {
/// Open the resume picker inside the running TUI session.
OpenResumePicker,

/// Resume a saved session by thread id.
ResumeSession(ThreadId),

/// Resume a saved session using the exact picker-selected rollout target.
ResumeSessionTarget(crate::resume_picker::SessionTarget),

/// Fork the current session into a new thread.
ForkCurrentSession,

Expand Down Expand Up @@ -182,6 +189,14 @@ pub(crate) enum AppEvent {
/// Update the current model slug in the running app and widget.
UpdateModel(String),

/// Evaluate a serialized built-in slash-command draft. If a task is currently running, the
/// draft is queued and replayed later through the same path as queued composer input.
HandleSlashCommandDraft(UserMessage),

/// Notify the app that an interactive bottom-pane view finished, so queued replay can resume
/// once the UI is idle again.
BottomPaneViewCompleted,

/// Update the active collaboration mask in the running app and widget.
UpdateCollaborationMode(CollaborationModeMask),

Expand Down Expand Up @@ -253,6 +268,7 @@ pub(crate) enum AppEvent {
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
OpenWorldWritableWarningConfirmation {
preset: Option<ApprovalPreset>,
approvals_reviewer: Option<ApprovalsReviewer>,
/// Up to 3 sample world-writable directories to display in the warning.
sample_paths: Vec<String>,
/// If there are more than `sample_paths`, this carries the remaining count.
Expand All @@ -265,24 +281,44 @@ pub(crate) enum AppEvent {
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
OpenWindowsSandboxEnablePrompt {
preset: ApprovalPreset,
approvals_reviewer: ApprovalsReviewer,
},

/// Open the Windows sandbox fallback prompt after declining or failing elevation.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
OpenWindowsSandboxFallbackPrompt {
preset: ApprovalPreset,
approvals_reviewer: ApprovalsReviewer,
},

/// Begin the elevated Windows sandbox setup flow.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
BeginWindowsSandboxElevatedSetup {
preset: ApprovalPreset,
approvals_reviewer: ApprovalsReviewer,
},

/// Result of the elevated Windows sandbox setup flow.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
WindowsSandboxElevatedSetupCompleted {
preset: ApprovalPreset,
approvals_reviewer: ApprovalsReviewer,
setup_succeeded: bool,
},

/// Begin the non-elevated Windows sandbox setup flow.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
BeginWindowsSandboxLegacySetup {
preset: ApprovalPreset,
approvals_reviewer: ApprovalsReviewer,
},

/// Result of the non-elevated Windows sandbox setup flow.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
WindowsSandboxLegacySetupCompleted {
preset: ApprovalPreset,
approvals_reviewer: ApprovalsReviewer,
error: Option<String>,
},

/// Begin a non-elevated grant of read access for an additional directory.
Expand All @@ -298,11 +334,16 @@ pub(crate) enum AppEvent {
error: Option<String>,
},

/// Result of the asynchronous Windows world-writable scan.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
WorldWritableScanCompleted,

/// Enable the Windows sandbox feature and switch to Agent mode.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
EnableWindowsSandboxForAgentMode {
preset: ApprovalPreset,
mode: WindowsSandboxEnableMode,
approvals_reviewer: ApprovalsReviewer,
},

/// Update the Windows sandbox feature mode without changing approval presets.
Expand Down Expand Up @@ -361,9 +402,6 @@ pub(crate) enum AppEvent {
/// Re-open the approval presets popup.
OpenApprovalsPopup,

/// Open the skills list popup.
OpenSkillsList,

/// Open the skills enable/disable picker.
OpenManageSkillsPopup,

Expand Down
146 changes: 80 additions & 66 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
//!
//! The numeric auto-submit path used by the slash popup performs the same pending-paste expansion
//! and attachment pruning, and clears pending paste state on success.
//! Slash commands with arguments (like `/plan` and `/review`) reuse the same preparation path so
//! pasted content and text elements are preserved when extracting args.
//! Slash commands with arguments (like `/model`, `/plan`, and `/review`) reuse the same
//! preparation path so pasted content and text elements are preserved when extracting args.
//!
//! # Remote Image Rows (Up/Down/Delete)
//!
Expand Down Expand Up @@ -572,23 +572,6 @@ impl ChatComposer {
self.sync_popups();
}

pub(crate) fn take_mention_bindings(&mut self) -> Vec<MentionBinding> {
let elements = self.current_mention_elements();
let mut ordered = Vec::new();
for (id, mention) in elements {
if let Some(binding) = self.mention_bindings.remove(&id)
&& binding.mention == mention
{
ordered.push(MentionBinding {
mention: binding.mention,
path: binding.path,
});
}
}
self.mention_bindings.clear();
ordered
}

pub fn set_collaboration_modes_enabled(&mut self, enabled: bool) {
self.collaboration_modes_enabled = enabled;
}
Expand Down Expand Up @@ -2525,9 +2508,6 @@ impl ChatComposer {
&& let Some(cmd) =
slash_commands::find_builtin_command(name, self.builtin_command_flags())
{
if self.reject_slash_command_if_unavailable(cmd) {
return Some(InputResult::None);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was "'/{}' is disabled while a task is in progress.", which we no longer emit

self.textarea.set_text_clearing_elements("");
Some(InputResult::Command(cmd))
} else {
Expand All @@ -2553,13 +2533,6 @@ impl ChatComposer {

let cmd = slash_commands::find_builtin_command(name, self.builtin_command_flags())?;

if !cmd.supports_inline_args() {
return None;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every command supports inline args now

if self.reject_slash_command_if_unavailable(cmd) {
return Some(InputResult::None);
}

let mut args_elements =
Self::slash_command_args_elements(rest, rest_offset, &self.textarea.text_elements());
let trimmed_rest = rest.trim();
Expand All @@ -2573,10 +2546,10 @@ impl ChatComposer {

/// Expand pending placeholders and extract normalized inline-command args.
///
/// Inline-arg commands are initially dispatched using the raw draft so command rejection does
/// not consume user input. Once a command is accepted, this helper performs the usual
/// submission preparation (paste expansion, element trimming) and rebases element ranges from
/// full-text offsets to command-arg offsets.
/// Inline-arg commands are initially dispatched using the raw draft so command-specific
/// handling can decide whether to consume the input. Once a command is accepted, this helper
/// performs the usual submission preparation (paste expansion, element trimming) and rebases
/// element ranges from full-text offsets to command-arg offsets.
pub(crate) fn prepare_inline_args_submission(
&mut self,
record_history: bool,
Expand All @@ -2593,20 +2566,6 @@ impl ChatComposer {
Some((trimmed_rest.to_string(), args_elements))
}

fn reject_slash_command_if_unavailable(&self, cmd: SlashCommand) -> bool {
if !self.is_task_running || cmd.available_during_task() {
return false;
}
let message = format!(
"'/{}' is disabled while a task is in progress.",
cmd.command()
);
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::new_error_event(message),
)));
true
}

/// Translate full-text element ranges into command-argument ranges.
///
/// `rest_offset` is the byte offset where `rest` begins in the full text.
Expand Down Expand Up @@ -6422,6 +6381,69 @@ mod tests {
});
}

#[test]
fn slash_popup_help_first_for_root_ui() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;

let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);

let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);

type_chars_humanlike(&mut composer, &['/']);

let mut terminal = match Terminal::new(TestBackend::new(60, 8)) {
Ok(t) => t,
Err(e) => panic!("Failed to create terminal: {e}"),
};
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.unwrap_or_else(|e| panic!("Failed to draw composer: {e}"));

if cfg!(target_os = "windows") {
insta::with_settings!({ snapshot_suffix => "windows" }, {
insta::assert_snapshot!("slash_popup_root", terminal.backend());
});
} else {
insta::assert_snapshot!("slash_popup_root", terminal.backend());
}
}

#[test]
fn slash_popup_help_first_for_root_logic() {
use super::super::command_popup::CommandItem;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
type_chars_humanlike(&mut composer, &['/']);

match &composer.active_popup {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => {
assert_eq!(cmd.command(), "help")
}
Some(CommandItem::UserPrompt(_)) => {
panic!("unexpected prompt selected for '/'")
}
None => panic!("no selected command for '/'"),
},
_ => panic!("slash popup not active after typing '/'"),
}
}

#[test]
fn slash_popup_model_first_for_mo_ui() {
use ratatui::Terminal;
Expand Down Expand Up @@ -6678,7 +6700,7 @@ mod tests {
}

#[test]
fn slash_command_disabled_while_task_running_keeps_text() {
fn slash_command_while_task_running_still_dispatches() {
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
Expand All @@ -6700,24 +6722,16 @@ mod tests {
let (result, _needs_redraw) =
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

assert_eq!(InputResult::None, result);
assert_eq!(
InputResult::CommandWithArgs(
SlashCommand::Review,
"these changes".to_string(),
Vec::new(),
),
result
);
assert_eq!("/review these changes", composer.textarea.text());

let mut found_error = false;
while let Ok(event) = rx.try_recv() {
if let AppEvent::InsertHistoryCell(cell) = event {
let message = cell
.display_lines(80)
.into_iter()
.map(|line| line.to_string())
.collect::<Vec<_>>()
.join("\n");
assert!(message.contains("disabled while a task is in progress"));
found_error = true;
break;
}
}
assert!(found_error, "expected error history cell to be sent");
assert!(rx.try_recv().is_err(), "no error should be emitted");
}

#[test]
Expand Down Expand Up @@ -7626,7 +7640,7 @@ mod tests {
composer.take_recent_submission_mention_bindings(),
mention_bindings
);
assert!(composer.take_mention_bindings().is_empty());
assert!(composer.mention_bindings().is_empty());
}

#[test]
Expand Down
Loading
Loading