Compare commits

...

18 Commits

Author SHA1 Message Date
Ahmed Ibrahim
b120eb0757 Update config_types.rs 2025-08-18 13:13:52 -07:00
Ahmed Ibrahim
05f3ea1c28 Merge branch 'main' into change-reasoning-effort-tui 2025-08-18 13:13:28 -07:00
Ahmed Ibrahim
748d9788aa introduce variable reasoning effort 2025-08-18 13:09:40 -07:00
Ahmed Ibrahim
1c5a7cd1ca introduce variable reasoning effort 2025-08-18 13:09:13 -07:00
Ahmed Ibrahim
d4385420f4 progress 2025-08-18 12:57:59 -07:00
Ahmed Ibrahim
af6ddb7f5c tests 2025-08-18 12:52:24 -07:00
Ahmed Ibrahim
4305db2d76 auth 2025-08-18 12:45:54 -07:00
Ahmed Ibrahim
4b76e81caa Merge branch 'change-reasoning-effort' of https://github.com/openai/codex into change-reasoning-effort 2025-08-18 12:19:38 -07:00
Ahmed Ibrahim
ae6c8f036f update the turn context 2025-08-18 12:19:08 -07:00
Ahmed Ibrahim
ca60bb7ea5 update the turn context 2025-08-18 12:18:32 -07:00
Ahmed Ibrahim
a3c3432fa1 Merge branch 'main' into change-reasoning-effort 2025-08-18 12:10:12 -07:00
Ahmed Ibrahim
8e560cbe6c remove test 2025-08-18 12:06:46 -07:00
Ahmed Ibrahim
b409771a6d add override operation 2025-08-18 12:06:23 -07:00
Ahmed Ibrahim
c90e87def2 clippy 2025-08-18 11:23:50 -07:00
Ahmed Ibrahim
e6c2294700 Update config.rs 2025-08-18 11:20:52 -07:00
Ahmed Ibrahim
813387b707 Update config.rs 2025-08-18 11:19:13 -07:00
Ahmed Ibrahim
d7c999c130 Merge branch 'main' into remove-reasoning-effort 2025-08-18 11:15:53 -07:00
Ahmed Ibrahim
b374dab9bf consalidate reasoning types 2025-08-18 11:08:27 -07:00
10 changed files with 287 additions and 23 deletions

View File

@@ -1,10 +1,11 @@
use serde::Deserialize;
use serde::Serialize;
use strum_macros::Display;
use strum_macros::EnumIter;
use ts_rs::TS;
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS)]
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS, EnumIter)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ReasoningEffort {
@@ -13,8 +14,6 @@ pub enum ReasoningEffort {
#[default]
Medium,
High,
/// Option to disable reasoning.
None,
}
/// A summary of the reasoning performed by the model. This can be useful for

View File

@@ -416,6 +416,13 @@ impl App<'_> {
widget.add_status_output();
}
}
SlashCommand::ReasoningEffort => {
if self.config.model_family.supports_reasoning_summaries {
if let AppState::Chat { widget } = &mut self.app_state {
widget.open_reasoning_effort_popup();
}
}
}
#[cfg(debug_assertions)]
SlashCommand::TestApproval => {
use codex_core::protocol::EventMsg;

View File

@@ -99,6 +99,7 @@ mod tests {
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
assert_eq!(CancellationEvent::Handled, view.on_ctrl_c(&mut pane));
assert!(view.queue.is_empty());

View File

@@ -21,6 +21,8 @@ use ratatui::widgets::StatefulWidgetRef;
use ratatui::widgets::WidgetRef;
use super::chat_composer_history::ChatComposerHistory;
use super::choice_popup::ChoicePayload;
use super::choice_popup::ChoicePopup;
use super::command_popup::CommandPopup;
use super::file_search_popup::FileSearchPopup;
@@ -28,6 +30,8 @@ use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::textarea::TextArea;
use crate::bottom_pane::textarea::TextAreaState;
use codex_core::protocol::Op;
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
use codex_file_search::FileMatch;
use std::cell::RefCell;
@@ -61,6 +65,7 @@ pub(crate) struct ChatComposer {
token_usage_info: Option<TokenUsageInfo>,
has_focus: bool,
placeholder_text: String,
show_reasoning_commands: bool,
}
/// Popup state at most one can be visible at any time.
@@ -68,6 +73,7 @@ enum ActivePopup {
None,
Command(CommandPopup),
File(FileSearchPopup),
Choice(ChoicePopup),
}
impl ChatComposer {
@@ -76,6 +82,7 @@ impl ChatComposer {
app_event_tx: AppEventSender,
enhanced_keys_supported: bool,
placeholder_text: String,
show_reasoning_commands: bool,
) -> Self {
let use_shift_enter_hint = enhanced_keys_supported;
@@ -93,6 +100,7 @@ impl ChatComposer {
token_usage_info: None,
has_focus: has_input_focus,
placeholder_text,
show_reasoning_commands,
}
}
@@ -102,6 +110,7 @@ impl ChatComposer {
ActivePopup::None => 1u16,
ActivePopup::Command(c) => c.calculate_required_height(),
ActivePopup::File(c) => c.calculate_required_height(),
ActivePopup::Choice(c) => c.calculate_required_height(),
}
}
@@ -109,6 +118,7 @@ impl ChatComposer {
let popup_height = match &self.active_popup {
ActivePopup::Command(popup) => popup.calculate_required_height(),
ActivePopup::File(popup) => popup.calculate_required_height(),
ActivePopup::Choice(popup) => popup.calculate_required_height(),
ActivePopup::None => 1,
};
let [textarea_rect, _] =
@@ -211,15 +221,22 @@ impl ChatComposer {
let result = match &mut self.active_popup {
ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event),
ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event),
ActivePopup::Choice(_) => self.handle_key_event_with_choice_popup(key_event),
ActivePopup::None => self.handle_key_event_without_popup(key_event),
};
// Update (or hide/show) popup after processing the key.
self.sync_command_popup();
if matches!(self.active_popup, ActivePopup::Command(_)) {
self.dismissed_file_popup_token = None;
} else {
self.sync_file_search_popup();
match self.active_popup {
ActivePopup::Command(_) => {
self.dismissed_file_popup_token = None;
}
ActivePopup::File(_) | ActivePopup::None => {
self.sync_file_search_popup();
}
ActivePopup::Choice(_) => {
// Do not clobber a generic choice popup with file-search sync.
}
}
result
@@ -335,6 +352,60 @@ impl ChatComposer {
}
}
/// Handle key events when a generic choice popup is visible.
fn handle_key_event_with_choice_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
let ActivePopup::Choice(popup) = &mut self.active_popup else {
unreachable!();
};
match key_event {
KeyEvent {
code: KeyCode::Up, ..
} => {
popup.move_up();
(InputResult::None, true)
}
KeyEvent {
code: KeyCode::Down,
..
} => {
popup.move_down();
(InputResult::None, true)
}
KeyEvent {
code: KeyCode::Esc, ..
} => {
self.active_popup = ActivePopup::None;
(InputResult::None, true)
}
KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE,
..
} => {
if let Some(sel) = popup.selected_payload() {
match sel {
ChoicePayload::ReasoningEffort(effort) => {
self.app_event_tx
.send(AppEvent::CodexOp(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
sandbox_policy: None,
model: None,
effort: Some(*effort),
summary: None,
}));
}
}
self.active_popup = ActivePopup::None;
return (InputResult::None, true);
}
(InputResult::None, false)
}
input => self.handle_input_basic(input),
}
}
/// Extract the `@token` that the cursor is currently positioned on, if any.
///
/// The returned string **does not** include the leading `@`.
@@ -562,6 +633,7 @@ impl ChatComposer {
_ => {
if input_starts_with_slash {
let mut command_popup = CommandPopup::new();
command_popup.filter_for_capabilities(self.show_reasoning_commands);
command_popup.on_composer_text_change(first_line.to_string());
self.active_popup = ActivePopup::Command(command_popup);
}
@@ -618,6 +690,12 @@ impl ChatComposer {
fn set_has_focus(&mut self, has_focus: bool) {
self.has_focus = has_focus;
}
/// Open a choice popup to select a reasoning effort value.
pub(crate) fn open_reasoning_effort_popup(&mut self, current: ReasoningEffortConfig) {
let popup = ChoicePopup::new_reasoning_effort(current);
self.active_popup = ActivePopup::Choice(popup);
}
}
impl WidgetRef for &ChatComposer {
@@ -625,6 +703,7 @@ impl WidgetRef for &ChatComposer {
let popup_height = match &self.active_popup {
ActivePopup::Command(popup) => popup.calculate_required_height(),
ActivePopup::File(popup) => popup.calculate_required_height(),
ActivePopup::Choice(popup) => popup.calculate_required_height(),
ActivePopup::None => 1,
};
let [textarea_rect, popup_rect] =
@@ -636,6 +715,9 @@ impl WidgetRef for &ChatComposer {
ActivePopup::File(popup) => {
popup.render_ref(popup_rect, buf);
}
ActivePopup::Choice(popup) => {
popup.render_ref(popup_rect, buf);
}
ActivePopup::None => {
let bottom_line_rect = popup_rect;
let key_hint_style = Style::default().fg(Color::Cyan);
@@ -887,8 +969,13 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
let needs_redraw = composer.handle_paste("hello".to_string());
assert!(needs_redraw);
@@ -911,8 +998,13 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
let large = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 10);
let needs_redraw = composer.handle_paste(large.clone());
@@ -941,8 +1033,13 @@ mod tests {
let large = "y".repeat(LARGE_PASTE_CHAR_THRESHOLD + 1);
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
composer.handle_paste(large);
assert_eq!(composer.pending_pastes.len(), 1);
@@ -983,6 +1080,7 @@ mod tests {
sender.clone(),
false,
"Ask Codex to do anything".to_string(),
false,
);
if let Some(text) = input {
@@ -1021,8 +1119,13 @@ mod tests {
let (tx, rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
// Type the slash command.
for ch in [
@@ -1065,8 +1168,13 @@ mod tests {
let (tx, rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
for ch in ['/', 'm', 'e', 'n', 't', 'i', 'o', 'n'] {
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
@@ -1105,8 +1213,13 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
// Define test cases: (paste content, is_large)
let test_cases = [
@@ -1179,8 +1292,13 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
// Define test cases: (content, is_large)
let test_cases = [
@@ -1246,8 +1364,13 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
// Define test cases: (cursor_position_from_end, expected_pending_count)
let test_cases = [

View File

@@ -0,0 +1,93 @@
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::WidgetRef;
use super::popup_consts::MAX_POPUP_ROWS;
use super::scroll_state::ScrollState;
use super::selection_popup_common::GenericDisplayRow;
use super::selection_popup_common::render_rows;
use strum::IntoEnumIterator;
/// Payload associated with a selected item in a generic choice popup.
pub(crate) enum ChoicePayload {
ReasoningEffort(ReasoningEffortConfig),
}
pub(crate) struct ChoiceItem {
pub name: String,
pub is_current: bool,
pub description: Option<String>,
pub payload: ChoicePayload,
}
/// A simple reusable choice popup that displays a fixed list of items and
/// allows the user to select one using Up/Down/Enter.
pub(crate) struct ChoicePopup {
items: Vec<ChoiceItem>,
state: ScrollState,
}
impl ChoicePopup {
pub(crate) fn new_reasoning_effort(current: ReasoningEffortConfig) -> Self {
let items: Vec<ChoiceItem> = ReasoningEffortConfig::iter()
.map(|v| ChoiceItem {
name: v.to_string(),
is_current: v == current,
description: None,
payload: ChoicePayload::ReasoningEffort(v),
})
.collect();
let mut state = ScrollState::new();
// Default selection to the current value when present
if let Some((idx, _)) = items.iter().enumerate().find(|(_, it)| it.is_current) {
state.selected_idx = Some(idx);
}
Self { items, state }
}
pub(crate) fn move_up(&mut self) {
let len = self.items.len();
self.state.move_up_wrap(len);
self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
}
pub(crate) fn move_down(&mut self) {
let len = self.items.len();
self.state.move_down_wrap(len);
self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS));
}
pub(crate) fn selected_payload(&self) -> Option<&ChoicePayload> {
self.state
.selected_idx
.and_then(|idx| self.items.get(idx))
.map(|it| &it.payload)
}
pub(crate) fn calculate_required_height(&self) -> u16 {
self.items.len().clamp(1, MAX_POPUP_ROWS) as u16
}
}
impl WidgetRef for &ChoicePopup {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let rows_all: Vec<GenericDisplayRow> = if self.items.is_empty() {
Vec::new()
} else {
self.items
.iter()
.map(|item| GenericDisplayRow {
name: item.name.clone(),
match_indices: None,
is_current: item.is_current,
description: item.description.clone(),
})
.collect()
};
render_rows(area, buf, &rows_all, &self.state, MAX_POPUP_ROWS);
}
}

View File

@@ -25,6 +25,15 @@ impl CommandPopup {
}
}
/// Filter commands based on runtime capabilities (e.g., hide reasoning-related
/// commands when the current model does not support them).
pub(crate) fn filter_for_capabilities(&mut self, show_reasoning_commands: bool) {
if !show_reasoning_commands {
self.all_commands
.retain(|(_, c)| !matches!(c, SlashCommand::ReasoningEffort));
}
}
/// Update the filter string based on the current composer text. The text
/// passed in is expected to start with a leading '/'. Everything after the
/// *first* '/" on the *first* line becomes the active filter that is used

View File

@@ -16,6 +16,7 @@ mod bottom_pane_view;
mod chat_composer;
mod chat_composer_history;
mod command_popup;
mod choice_popup;
mod file_search_popup;
mod popup_consts;
mod scroll_state;
@@ -59,6 +60,8 @@ pub(crate) struct BottomPaneParams {
pub(crate) has_input_focus: bool,
pub(crate) enhanced_keys_supported: bool,
pub(crate) placeholder_text: String,
/// Whether to show reasoning-related slash commands.
pub(crate) show_reasoning_commands: bool,
}
impl BottomPane<'_> {
@@ -71,6 +74,7 @@ impl BottomPane<'_> {
params.app_event_tx.clone(),
enhanced_keys_supported,
params.placeholder_text,
params.show_reasoning_commands,
),
active_view: None,
app_event_tx: params.app_event_tx,
@@ -297,6 +301,16 @@ impl BottomPane<'_> {
self.composer.on_file_search_result(query, matches);
self.request_redraw();
}
/// Open a popup to choose the reasoning effort and apply the chosen
/// override on selection.
pub(crate) fn open_reasoning_effort_popup(
&mut self,
current: codex_core::protocol_config_types::ReasoningEffort,
) {
self.composer.open_reasoning_effort_popup(current);
self.request_redraw();
}
}
impl WidgetRef for &BottomPane<'_> {
@@ -355,6 +369,7 @@ mod tests {
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
pane.push_approval_request(exec_request());
assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c());
@@ -373,6 +388,7 @@ mod tests {
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
// Create an approval modal (active view).
@@ -402,6 +418,7 @@ mod tests {
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
// Start a running task so the status indicator replaces the composer.
@@ -452,6 +469,7 @@ mod tests {
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
// Begin a task: show initial status.
@@ -484,6 +502,7 @@ mod tests {
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
// Activate spinner (status view replaces composer) with no live ring.
@@ -536,6 +555,7 @@ mod tests {
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
pane.set_task_running(true);

View File

@@ -507,6 +507,7 @@ impl ChatWidget<'_> {
has_input_focus: true,
enhanced_keys_supported,
placeholder_text: placeholder,
show_reasoning_commands: config.model_family.supports_reasoning_summaries,
}),
active_exec_cell: None,
config: config.clone(),
@@ -685,6 +686,12 @@ impl ChatWidget<'_> {
));
}
/// Open a popup to choose the model reasoning effort.
pub(crate) fn open_reasoning_effort_popup(&mut self) {
let current = self.config.model_reasoning_effort;
self.bottom_pane.open_reasoning_effort_popup(current);
}
/// Forward file-search results to the bottom pane.
pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
self.bottom_pane.on_file_search_result(query, matches);

View File

@@ -125,6 +125,7 @@ fn make_chatwidget_manual() -> (
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
show_reasoning_commands: false,
});
let widget = ChatWidget {
app_event_tx,

View File

@@ -17,6 +17,7 @@ pub enum SlashCommand {
Compact,
Diff,
Mention,
ReasoningEffort,
Status,
Logout,
Quit,
@@ -32,6 +33,9 @@ impl SlashCommand {
SlashCommand::Init => "create an AGENTS.md file with instructions for Codex",
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
SlashCommand::Quit => "exit Codex",
SlashCommand::ReasoningEffort => {
"choose model reasoning effort (low/medium/high/minimal)"
}
SlashCommand::Diff => "show git diff (including untracked files)",
SlashCommand::Mention => "mention a file",
SlashCommand::Status => "show current session configuration and token usage",