mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
add fast mode toggle (#13212)
- add a local Fast mode setting in codex-core (similar to how model id is currently stored on disk locally) - send `service_tier=priority` on requests when Fast is enabled - add `/fast` in the TUI and persist it locally - feature flag
This commit is contained in:
+58
-1
@@ -1375,6 +1375,7 @@ impl App {
|
||||
// Start a fresh in-memory session while preserving resumability via persisted rollout
|
||||
// history.
|
||||
let model = self.chat_widget.current_model().to_string();
|
||||
let config = self.fresh_session_config();
|
||||
let summary = session_summary(
|
||||
self.chat_widget.token_usage(),
|
||||
self.chat_widget.thread_id(),
|
||||
@@ -1385,7 +1386,7 @@ impl App {
|
||||
tracing::warn!(error = %err, "failed to close all threads");
|
||||
}
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: self.config.clone(),
|
||||
config,
|
||||
frame_requester: tui.frame_requester(),
|
||||
app_event_tx: self.app_event_tx.clone(),
|
||||
// New sessions start without prefilled message content.
|
||||
@@ -1414,6 +1415,12 @@ impl App {
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
|
||||
fn fresh_session_config(&self) -> Config {
|
||||
let mut config = self.config.clone();
|
||||
config.service_tier = self.chat_widget.current_service_tier();
|
||||
config
|
||||
}
|
||||
|
||||
async fn drain_active_thread_events(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
let Some(mut rx) = self.active_thread_rx.take() else {
|
||||
return Ok(());
|
||||
@@ -2532,6 +2539,7 @@ impl App {
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
},
|
||||
@@ -2554,6 +2562,7 @@ impl App {
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
},
|
||||
@@ -2665,6 +2674,39 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::PersistServiceTierSelection { service_tier } => {
|
||||
self.refresh_status_line();
|
||||
let profile = self.active_profile.as_deref();
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_service_tier(service_tier)
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
let status = if service_tier.is_some() { "on" } else { "off" };
|
||||
let mut message = format!("Fast mode set to {status}");
|
||||
if let Some(profile) = profile {
|
||||
message.push_str(" for ");
|
||||
message.push_str(profile);
|
||||
message.push_str(" profile");
|
||||
}
|
||||
self.chat_widget.add_info_message(message, None);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(error = %err, "failed to persist fast mode selection");
|
||||
if let Some(profile) = profile {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to save Fast mode for profile `{profile}`: {err}"
|
||||
));
|
||||
} else {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to save default Fast mode: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::PersistRealtimeAudioDeviceSelection { kind, name } => {
|
||||
let builder = match kind {
|
||||
RealtimeAudioDeviceKind::Microphone => {
|
||||
@@ -2827,6 +2869,7 @@ impl App {
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
}));
|
||||
@@ -4916,6 +4959,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fresh_session_config_uses_current_service_tier() {
|
||||
let mut app = make_test_app().await;
|
||||
app.chat_widget
|
||||
.set_service_tier(Some(codex_protocol::config_types::ServiceTier::Fast));
|
||||
|
||||
let config = app.fresh_session_config();
|
||||
|
||||
assert_eq!(
|
||||
config.service_tier,
|
||||
Some(codex_protocol::config_types::ServiceTier::Fast)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::history_cell::HistoryCell;
|
||||
use codex_core::features::Feature;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
@@ -197,6 +198,11 @@ pub(crate) enum AppEvent {
|
||||
personality: Personality,
|
||||
},
|
||||
|
||||
/// Persist the selected service tier to the appropriate config.
|
||||
PersistServiceTierSelection {
|
||||
service_tier: Option<ServiceTier>,
|
||||
},
|
||||
|
||||
/// Open the device picker for a realtime microphone or speaker.
|
||||
OpenRealtimeAudioDeviceSelection {
|
||||
kind: RealtimeAudioDeviceKind,
|
||||
|
||||
@@ -178,6 +178,7 @@ use super::paste_burst::PasteBurst;
|
||||
use super::skill_popup::MentionItem;
|
||||
use super::skill_popup::SkillPopup;
|
||||
use super::slash_commands;
|
||||
use super::slash_commands::BuiltinCommandFlags;
|
||||
use crate::bottom_pane::paste_burst::FlushResult;
|
||||
use crate::bottom_pane::prompt_args::expand_custom_prompt;
|
||||
use crate::bottom_pane::prompt_args::expand_if_numeric_with_positional_args;
|
||||
@@ -398,6 +399,7 @@ pub(crate) struct ChatComposer {
|
||||
config: ChatComposerConfig,
|
||||
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
|
||||
connectors_enabled: bool,
|
||||
fast_command_enabled: bool,
|
||||
personality_command_enabled: bool,
|
||||
realtime_conversation_enabled: bool,
|
||||
audio_device_selection_enabled: bool,
|
||||
@@ -429,6 +431,18 @@ enum ActivePopup {
|
||||
const FOOTER_SPACING_HEIGHT: u16 = 0;
|
||||
|
||||
impl ChatComposer {
|
||||
fn builtin_command_flags(&self) -> BuiltinCommandFlags {
|
||||
BuiltinCommandFlags {
|
||||
collaboration_modes_enabled: self.collaboration_modes_enabled,
|
||||
connectors_enabled: self.connectors_enabled,
|
||||
fast_command_enabled: self.fast_command_enabled,
|
||||
personality_command_enabled: self.personality_command_enabled,
|
||||
realtime_conversation_enabled: self.realtime_conversation_enabled,
|
||||
audio_device_selection_enabled: self.audio_device_selection_enabled,
|
||||
allow_elevate_sandbox: self.windows_degraded_sandbox_active,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
has_input_focus: bool,
|
||||
app_event_tx: AppEventSender,
|
||||
@@ -504,6 +518,7 @@ impl ChatComposer {
|
||||
config,
|
||||
collaboration_mode_indicator: None,
|
||||
connectors_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: false,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
@@ -569,6 +584,10 @@ impl ChatComposer {
|
||||
self.connectors_enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_fast_command_enabled(&mut self, enabled: bool) {
|
||||
self.fast_command_enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_collaboration_mode_indicator(
|
||||
&mut self,
|
||||
indicator: Option<CollaborationModeIndicator>,
|
||||
@@ -2262,16 +2281,9 @@ impl ChatComposer {
|
||||
{
|
||||
let treat_as_plain_text = input_starts_with_space || name.contains('/');
|
||||
if !treat_as_plain_text {
|
||||
let is_builtin = slash_commands::find_builtin_command(
|
||||
name,
|
||||
self.collaboration_modes_enabled,
|
||||
self.connectors_enabled,
|
||||
self.personality_command_enabled,
|
||||
self.realtime_conversation_enabled,
|
||||
self.audio_device_selection_enabled,
|
||||
self.windows_degraded_sandbox_active,
|
||||
)
|
||||
.is_some();
|
||||
let is_builtin =
|
||||
slash_commands::find_builtin_command(name, self.builtin_command_flags())
|
||||
.is_some();
|
||||
let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:");
|
||||
let is_known_prompt = name
|
||||
.strip_prefix(&prompt_prefix)
|
||||
@@ -2479,15 +2491,8 @@ impl ChatComposer {
|
||||
let first_line = self.textarea.text().lines().next().unwrap_or("");
|
||||
if let Some((name, rest, _rest_offset)) = parse_slash_name(first_line)
|
||||
&& rest.is_empty()
|
||||
&& let Some(cmd) = slash_commands::find_builtin_command(
|
||||
name,
|
||||
self.collaboration_modes_enabled,
|
||||
self.connectors_enabled,
|
||||
self.personality_command_enabled,
|
||||
self.realtime_conversation_enabled,
|
||||
self.audio_device_selection_enabled,
|
||||
self.windows_degraded_sandbox_active,
|
||||
)
|
||||
&& 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);
|
||||
@@ -2515,15 +2520,7 @@ impl ChatComposer {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cmd = slash_commands::find_builtin_command(
|
||||
name,
|
||||
self.collaboration_modes_enabled,
|
||||
self.connectors_enabled,
|
||||
self.personality_command_enabled,
|
||||
self.realtime_conversation_enabled,
|
||||
self.audio_device_selection_enabled,
|
||||
self.windows_degraded_sandbox_active,
|
||||
)?;
|
||||
let cmd = slash_commands::find_builtin_command(name, self.builtin_command_flags())?;
|
||||
|
||||
if !cmd.supports_inline_args() {
|
||||
return None;
|
||||
@@ -3335,16 +3332,8 @@ impl ChatComposer {
|
||||
}
|
||||
|
||||
fn is_known_slash_name(&self, name: &str) -> bool {
|
||||
let is_builtin = slash_commands::find_builtin_command(
|
||||
name,
|
||||
self.collaboration_modes_enabled,
|
||||
self.connectors_enabled,
|
||||
self.personality_command_enabled,
|
||||
self.realtime_conversation_enabled,
|
||||
self.audio_device_selection_enabled,
|
||||
self.windows_degraded_sandbox_active,
|
||||
)
|
||||
.is_some();
|
||||
let is_builtin =
|
||||
slash_commands::find_builtin_command(name, self.builtin_command_flags()).is_some();
|
||||
if is_builtin {
|
||||
return true;
|
||||
}
|
||||
@@ -3398,15 +3387,7 @@ impl ChatComposer {
|
||||
return rest_after_name.is_empty();
|
||||
}
|
||||
|
||||
if slash_commands::has_builtin_prefix(
|
||||
name,
|
||||
self.collaboration_modes_enabled,
|
||||
self.connectors_enabled,
|
||||
self.personality_command_enabled,
|
||||
self.realtime_conversation_enabled,
|
||||
self.audio_device_selection_enabled,
|
||||
self.windows_degraded_sandbox_active,
|
||||
) {
|
||||
if slash_commands::has_builtin_prefix(name, self.builtin_command_flags()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3457,6 +3438,7 @@ impl ChatComposer {
|
||||
if is_editing_slash_command_name {
|
||||
let collaboration_modes_enabled = self.collaboration_modes_enabled;
|
||||
let connectors_enabled = self.connectors_enabled;
|
||||
let fast_command_enabled = self.fast_command_enabled;
|
||||
let personality_command_enabled = self.personality_command_enabled;
|
||||
let realtime_conversation_enabled = self.realtime_conversation_enabled;
|
||||
let audio_device_selection_enabled = self.audio_device_selection_enabled;
|
||||
@@ -3465,6 +3447,7 @@ impl ChatComposer {
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled,
|
||||
connectors_enabled,
|
||||
fast_command_enabled,
|
||||
personality_command_enabled,
|
||||
realtime_conversation_enabled,
|
||||
audio_device_selection_enabled,
|
||||
|
||||
@@ -38,26 +38,35 @@ pub(crate) struct CommandPopup {
|
||||
pub(crate) struct CommandPopupFlags {
|
||||
pub(crate) collaboration_modes_enabled: bool,
|
||||
pub(crate) connectors_enabled: bool,
|
||||
pub(crate) fast_command_enabled: bool,
|
||||
pub(crate) personality_command_enabled: bool,
|
||||
pub(crate) realtime_conversation_enabled: bool,
|
||||
pub(crate) audio_device_selection_enabled: bool,
|
||||
pub(crate) windows_degraded_sandbox_active: bool,
|
||||
}
|
||||
|
||||
impl From<CommandPopupFlags> for slash_commands::BuiltinCommandFlags {
|
||||
fn from(value: CommandPopupFlags) -> Self {
|
||||
Self {
|
||||
collaboration_modes_enabled: value.collaboration_modes_enabled,
|
||||
connectors_enabled: value.connectors_enabled,
|
||||
fast_command_enabled: value.fast_command_enabled,
|
||||
personality_command_enabled: value.personality_command_enabled,
|
||||
realtime_conversation_enabled: value.realtime_conversation_enabled,
|
||||
audio_device_selection_enabled: value.audio_device_selection_enabled,
|
||||
allow_elevate_sandbox: value.windows_degraded_sandbox_active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandPopup {
|
||||
pub(crate) fn new(mut prompts: Vec<CustomPrompt>, flags: CommandPopupFlags) -> Self {
|
||||
// Keep built-in availability in sync with the composer.
|
||||
let builtins: Vec<(&'static str, SlashCommand)> = slash_commands::builtins_for_input(
|
||||
flags.collaboration_modes_enabled,
|
||||
flags.connectors_enabled,
|
||||
flags.personality_command_enabled,
|
||||
flags.realtime_conversation_enabled,
|
||||
flags.audio_device_selection_enabled,
|
||||
flags.windows_degraded_sandbox_active,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|(name, _)| !name.starts_with("debug"))
|
||||
.collect();
|
||||
let builtins: Vec<(&'static str, SlashCommand)> =
|
||||
slash_commands::builtins_for_input(flags.into())
|
||||
.into_iter()
|
||||
.filter(|(name, _)| !name.starts_with("debug"))
|
||||
.collect();
|
||||
// Exclude prompts that collide with builtin command names and sort by name.
|
||||
let exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
|
||||
prompts.retain(|p| !exclude.contains(&p.name));
|
||||
@@ -498,6 +507,7 @@ mod tests {
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
@@ -519,6 +529,7 @@ mod tests {
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
@@ -540,6 +551,7 @@ mod tests {
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: false,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
@@ -569,6 +581,7 @@ mod tests {
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
@@ -590,6 +603,7 @@ mod tests {
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: false,
|
||||
connectors_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: true,
|
||||
audio_device_selection_enabled: false,
|
||||
|
||||
@@ -294,6 +294,11 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn set_fast_command_enabled(&mut self, enabled: bool) {
|
||||
self.composer.set_fast_command_enabled(enabled);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn set_realtime_conversation_enabled(&mut self, enabled: bool) {
|
||||
self.composer.set_realtime_conversation_enabled(enabled);
|
||||
self.request_redraw();
|
||||
|
||||
@@ -8,72 +8,47 @@ use codex_utils_fuzzy_match::fuzzy_match;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command::built_in_slash_commands;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct BuiltinCommandFlags {
|
||||
pub(crate) collaboration_modes_enabled: bool,
|
||||
pub(crate) connectors_enabled: bool,
|
||||
pub(crate) fast_command_enabled: bool,
|
||||
pub(crate) personality_command_enabled: bool,
|
||||
pub(crate) realtime_conversation_enabled: bool,
|
||||
pub(crate) audio_device_selection_enabled: bool,
|
||||
pub(crate) allow_elevate_sandbox: bool,
|
||||
}
|
||||
|
||||
/// Return the built-ins that should be visible/usable for the current input.
|
||||
pub(crate) fn builtins_for_input(
|
||||
collaboration_modes_enabled: bool,
|
||||
connectors_enabled: bool,
|
||||
personality_command_enabled: bool,
|
||||
realtime_conversation_enabled: bool,
|
||||
audio_device_selection_enabled: bool,
|
||||
allow_elevate_sandbox: bool,
|
||||
) -> Vec<(&'static str, SlashCommand)> {
|
||||
pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static str, SlashCommand)> {
|
||||
built_in_slash_commands()
|
||||
.into_iter()
|
||||
.filter(|(_, cmd)| allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox)
|
||||
.filter(|(_, cmd)| flags.allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox)
|
||||
.filter(|(_, cmd)| {
|
||||
collaboration_modes_enabled
|
||||
flags.collaboration_modes_enabled
|
||||
|| !matches!(*cmd, SlashCommand::Collab | SlashCommand::Plan)
|
||||
})
|
||||
.filter(|(_, cmd)| connectors_enabled || *cmd != SlashCommand::Apps)
|
||||
.filter(|(_, cmd)| personality_command_enabled || *cmd != SlashCommand::Personality)
|
||||
.filter(|(_, cmd)| realtime_conversation_enabled || *cmd != SlashCommand::Realtime)
|
||||
.filter(|(_, cmd)| audio_device_selection_enabled || *cmd != SlashCommand::Settings)
|
||||
.filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Apps)
|
||||
.filter(|(_, cmd)| flags.fast_command_enabled || *cmd != SlashCommand::Fast)
|
||||
.filter(|(_, cmd)| flags.personality_command_enabled || *cmd != SlashCommand::Personality)
|
||||
.filter(|(_, cmd)| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime)
|
||||
.filter(|(_, cmd)| flags.audio_device_selection_enabled || *cmd != SlashCommand::Settings)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find a single built-in command by exact name, after applying the gating rules.
|
||||
pub(crate) fn find_builtin_command(
|
||||
name: &str,
|
||||
collaboration_modes_enabled: bool,
|
||||
connectors_enabled: bool,
|
||||
personality_command_enabled: bool,
|
||||
realtime_conversation_enabled: bool,
|
||||
audio_device_selection_enabled: bool,
|
||||
allow_elevate_sandbox: bool,
|
||||
) -> Option<SlashCommand> {
|
||||
builtins_for_input(
|
||||
collaboration_modes_enabled,
|
||||
connectors_enabled,
|
||||
personality_command_enabled,
|
||||
realtime_conversation_enabled,
|
||||
audio_device_selection_enabled,
|
||||
allow_elevate_sandbox,
|
||||
)
|
||||
.into_iter()
|
||||
.find(|(command_name, _)| *command_name == name)
|
||||
.map(|(_, cmd)| cmd)
|
||||
pub(crate) fn find_builtin_command(name: &str, flags: BuiltinCommandFlags) -> Option<SlashCommand> {
|
||||
builtins_for_input(flags)
|
||||
.into_iter()
|
||||
.find(|(command_name, _)| *command_name == name)
|
||||
.map(|(_, cmd)| cmd)
|
||||
}
|
||||
|
||||
/// Whether any visible built-in fuzzily matches the provided prefix.
|
||||
pub(crate) fn has_builtin_prefix(
|
||||
name: &str,
|
||||
collaboration_modes_enabled: bool,
|
||||
connectors_enabled: bool,
|
||||
personality_command_enabled: bool,
|
||||
realtime_conversation_enabled: bool,
|
||||
audio_device_selection_enabled: bool,
|
||||
allow_elevate_sandbox: bool,
|
||||
) -> bool {
|
||||
builtins_for_input(
|
||||
collaboration_modes_enabled,
|
||||
connectors_enabled,
|
||||
personality_command_enabled,
|
||||
realtime_conversation_enabled,
|
||||
audio_device_selection_enabled,
|
||||
allow_elevate_sandbox,
|
||||
)
|
||||
.into_iter()
|
||||
.any(|(command_name, _)| fuzzy_match(command_name, name).is_some())
|
||||
pub(crate) fn has_builtin_prefix(name: &str, flags: BuiltinCommandFlags) -> bool {
|
||||
builtins_for_input(flags)
|
||||
.into_iter()
|
||||
.any(|(command_name, _)| fuzzy_match(command_name, name).is_some())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -81,41 +56,58 @@ mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn all_enabled_flags() -> BuiltinCommandFlags {
|
||||
BuiltinCommandFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: true,
|
||||
fast_command_enabled: true,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: true,
|
||||
audio_device_selection_enabled: true,
|
||||
allow_elevate_sandbox: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_command_still_resolves_for_dispatch() {
|
||||
let cmd = find_builtin_command("debug-config", true, true, true, false, false, false);
|
||||
let cmd = find_builtin_command("debug-config", all_enabled_flags());
|
||||
assert_eq!(cmd, Some(SlashCommand::DebugConfig));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_command_resolves_for_dispatch() {
|
||||
assert_eq!(
|
||||
find_builtin_command("clear", true, true, true, false, false, false),
|
||||
find_builtin_command("clear", all_enabled_flags()),
|
||||
Some(SlashCommand::Clear)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_command_is_hidden_when_disabled() {
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.fast_command_enabled = false;
|
||||
assert_eq!(find_builtin_command("fast", flags), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_command_is_hidden_when_realtime_is_disabled() {
|
||||
assert_eq!(
|
||||
find_builtin_command("realtime", true, true, true, false, true, false),
|
||||
None
|
||||
);
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.realtime_conversation_enabled = false;
|
||||
assert_eq!(find_builtin_command("realtime", flags), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_command_is_hidden_when_realtime_is_disabled() {
|
||||
assert_eq!(
|
||||
find_builtin_command("settings", true, true, true, false, false, false),
|
||||
None
|
||||
);
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.realtime_conversation_enabled = false;
|
||||
flags.audio_device_selection_enabled = false;
|
||||
assert_eq!(find_builtin_command("settings", flags), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_command_is_hidden_when_audio_device_selection_is_disabled() {
|
||||
assert_eq!(
|
||||
find_builtin_command("settings", true, true, true, true, false, false),
|
||||
None
|
||||
);
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.audio_device_selection_enabled = false;
|
||||
assert_eq!(find_builtin_command("settings", flags), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Settings;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -1156,6 +1157,7 @@ impl ChatWidget {
|
||||
mask.reasoning_effort = Some(event.reasoning_effort);
|
||||
}
|
||||
self.refresh_model_display();
|
||||
self.sync_fast_command_enabled();
|
||||
self.sync_personality_command_enabled();
|
||||
let startup_tooltip_override = self.startup_tooltip_override.take();
|
||||
let session_info_cell = history_cell::new_session_info(
|
||||
@@ -2960,6 +2962,7 @@ impl ChatWidget {
|
||||
.bottom_pane
|
||||
.set_status_line_enabled(!widget.configured_status_line_items().is_empty());
|
||||
widget.bottom_pane.set_collaboration_modes_enabled(true);
|
||||
widget.sync_fast_command_enabled();
|
||||
widget.sync_personality_command_enabled();
|
||||
widget
|
||||
.bottom_pane
|
||||
@@ -3139,6 +3142,7 @@ impl ChatWidget {
|
||||
.bottom_pane
|
||||
.set_status_line_enabled(!widget.configured_status_line_items().is_empty());
|
||||
widget.bottom_pane.set_collaboration_modes_enabled(true);
|
||||
widget.sync_fast_command_enabled();
|
||||
widget.sync_personality_command_enabled();
|
||||
widget
|
||||
.bottom_pane
|
||||
@@ -3307,6 +3311,7 @@ impl ChatWidget {
|
||||
.bottom_pane
|
||||
.set_status_line_enabled(!widget.configured_status_line_items().is_empty());
|
||||
widget.bottom_pane.set_collaboration_modes_enabled(true);
|
||||
widget.sync_fast_command_enabled();
|
||||
widget.sync_personality_command_enabled();
|
||||
widget
|
||||
.bottom_pane
|
||||
@@ -3605,6 +3610,14 @@ impl ChatWidget {
|
||||
SlashCommand::Model => {
|
||||
self.open_model_popup();
|
||||
}
|
||||
SlashCommand::Fast => {
|
||||
let next_tier = if self.config.service_tier.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(ServiceTier::Fast)
|
||||
};
|
||||
self.set_service_tier_selection(next_tier);
|
||||
}
|
||||
SlashCommand::Realtime => {
|
||||
if !self.realtime_conversation_enabled() {
|
||||
return;
|
||||
@@ -3884,6 +3897,27 @@ impl ChatWidget {
|
||||
|
||||
let trimmed = args.trim();
|
||||
match cmd {
|
||||
SlashCommand::Fast => {
|
||||
if trimmed.is_empty() {
|
||||
self.dispatch_command(cmd);
|
||||
return;
|
||||
}
|
||||
match trimmed.to_ascii_lowercase().as_str() {
|
||||
"on" => self.set_service_tier_selection(Some(ServiceTier::Fast)),
|
||||
"off" => self.set_service_tier_selection(None),
|
||||
"status" => {
|
||||
let status = if self.config.service_tier.is_some() {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
};
|
||||
self.add_info_message(format!("Fast mode is {status}."), None);
|
||||
}
|
||||
_ => {
|
||||
self.add_error_message("Usage: /fast [on|off|status]".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Rename if !trimmed.is_empty() => {
|
||||
self.otel_manager.counter("codex.thread.rename", 1, &[]);
|
||||
let Some((prepared_args, _prepared_elements)) =
|
||||
@@ -4222,6 +4256,7 @@ impl ChatWidget {
|
||||
.personality
|
||||
.filter(|_| self.config.features.enabled(Feature::Personality))
|
||||
.filter(|_| self.current_model_supports_personality());
|
||||
let service_tier = self.fast_mode_enabled().then_some(self.config.service_tier);
|
||||
let op = Op::UserTurn {
|
||||
items,
|
||||
cwd: self.config.cwd.clone(),
|
||||
@@ -4230,6 +4265,7 @@ impl ChatWidget {
|
||||
model: effective_mode.model().to_string(),
|
||||
effort: effective_mode.reasoning_effort(),
|
||||
summary: None,
|
||||
service_tier,
|
||||
final_output_json_schema: None,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
@@ -5210,6 +5246,7 @@ impl ChatWidget {
|
||||
model: Some(switch_model_for_events.clone()),
|
||||
effort: Some(Some(default_effort)),
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
}));
|
||||
@@ -5329,6 +5366,7 @@ impl ChatWidget {
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
windows_sandbox_level: None,
|
||||
personality: Some(personality),
|
||||
@@ -6242,6 +6280,7 @@ impl ChatWidget {
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
}));
|
||||
@@ -6777,6 +6816,9 @@ impl ChatWidget {
|
||||
self.reset_realtime_conversation_state();
|
||||
}
|
||||
}
|
||||
if feature == Feature::FastMode {
|
||||
self.sync_fast_command_enabled();
|
||||
}
|
||||
if feature == Feature::Personality {
|
||||
self.sync_personality_command_enabled();
|
||||
}
|
||||
@@ -6859,6 +6901,19 @@ impl ChatWidget {
|
||||
self.config.personality = Some(personality);
|
||||
}
|
||||
|
||||
/// Set Fast mode in the widget's config copy.
|
||||
pub(crate) fn set_service_tier(&mut self, service_tier: Option<ServiceTier>) {
|
||||
self.config.service_tier = service_tier;
|
||||
}
|
||||
|
||||
pub(crate) fn current_service_tier(&self) -> Option<ServiceTier> {
|
||||
self.config.service_tier
|
||||
}
|
||||
|
||||
fn fast_mode_enabled(&self) -> bool {
|
||||
self.config.features.enabled(Feature::FastMode)
|
||||
}
|
||||
|
||||
pub(crate) fn set_realtime_audio_device(
|
||||
&mut self,
|
||||
kind: RealtimeAudioDeviceKind,
|
||||
@@ -6888,6 +6943,25 @@ impl ChatWidget {
|
||||
self.refresh_model_display();
|
||||
}
|
||||
|
||||
fn set_service_tier_selection(&mut self, service_tier: Option<ServiceTier>) {
|
||||
self.set_service_tier(service_tier);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
windows_sandbox_level: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: Some(service_tier),
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
}));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistServiceTierSelection { service_tier });
|
||||
}
|
||||
|
||||
pub(crate) fn current_model(&self) -> &str {
|
||||
if !self.collaboration_modes_enabled() {
|
||||
return self.current_collaboration_mode.model();
|
||||
@@ -6914,6 +6988,11 @@ impl ChatWidget {
|
||||
.unwrap_or_else(|| "System default".to_string())
|
||||
}
|
||||
|
||||
fn sync_fast_command_enabled(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_fast_command_enabled(self.fast_mode_enabled());
|
||||
}
|
||||
|
||||
fn sync_personality_command_enabled(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_personality_command_enabled(self.config.features.enabled(Feature::Personality));
|
||||
|
||||
@@ -38,6 +38,7 @@ use codex_protocol::account::PlanType;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::AgentMessageItem;
|
||||
@@ -6519,6 +6520,61 @@ async fn disabled_slash_command_while_task_running_snapshot() {
|
||||
assert_snapshot!(blob);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fast_slash_command_updates_and_persists_local_service_tier() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
chat.set_feature_enabled(Feature::FastMode, true);
|
||||
|
||||
chat.dispatch_command(SlashCommand::Fast);
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
..
|
||||
})
|
||||
)),
|
||||
"expected fast-mode override app event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::PersistServiceTierSelection {
|
||||
service_tier: Some(ServiceTier::Fast),
|
||||
}
|
||||
)),
|
||||
"expected fast-mode persistence app event; events: {events:?}"
|
||||
);
|
||||
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_turn_carries_service_tier_after_fast_toggle() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.set_feature_enabled(Feature::FastMode, true);
|
||||
|
||||
chat.dispatch_command(SlashCommand::Fast);
|
||||
|
||||
let _events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("hello".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn {
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
..
|
||||
} => {}
|
||||
other => panic!("expected Op::UserTurn with fast service tier, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approvals_popup_shows_disabled_presets() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
@@ -13,6 +13,7 @@ pub enum SlashCommand {
|
||||
// DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so
|
||||
// more frequently used commands should be listed first.
|
||||
Model,
|
||||
Fast,
|
||||
Approvals,
|
||||
Permissions,
|
||||
#[strum(serialize = "setup-default-sandbox")]
|
||||
@@ -89,6 +90,7 @@ impl SlashCommand {
|
||||
SlashCommand::MemoryDrop => "DO NOT USE",
|
||||
SlashCommand::MemoryUpdate => "DO NOT USE",
|
||||
SlashCommand::Model => "choose what model and reasoning effort to use",
|
||||
SlashCommand::Fast => "toggle Fast mode for supported models",
|
||||
SlashCommand::Personality => "choose a communication style for Codex",
|
||||
SlashCommand::Realtime => "toggle realtime voice mode (experimental)",
|
||||
SlashCommand::Settings => "configure realtime microphone/speaker",
|
||||
@@ -123,6 +125,7 @@ impl SlashCommand {
|
||||
SlashCommand::Review
|
||||
| SlashCommand::Rename
|
||||
| SlashCommand::Plan
|
||||
| SlashCommand::Fast
|
||||
| SlashCommand::SandboxReadRoot
|
||||
)
|
||||
}
|
||||
@@ -137,6 +140,7 @@ impl SlashCommand {
|
||||
| SlashCommand::Compact
|
||||
// | SlashCommand::Undo
|
||||
| SlashCommand::Model
|
||||
| SlashCommand::Fast
|
||||
| SlashCommand::Personality
|
||||
| SlashCommand::Approvals
|
||||
| SlashCommand::Permissions
|
||||
|
||||
Reference in New Issue
Block a user