mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Use backend service-tier metadata in app-server and TUI
This commit is contained in:
+23
-7
@@ -11208,10 +11208,10 @@
|
||||
},
|
||||
"Model": {
|
||||
"properties": {
|
||||
"additionalSpeedTiers": {
|
||||
"serviceTiers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/v2/ModelServiceTier"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -11306,6 +11306,25 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModelServiceTier": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModelListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -14143,10 +14162,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"description": "String-backed service tier identifier. Known values include `priority`, `flex`, and `ultrafast`, but other backend-provided ids are allowed.",
|
||||
"type": "string"
|
||||
},
|
||||
"SessionMigration": {
|
||||
@@ -18230,4 +18246,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocol",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
+23
-7
@@ -7862,10 +7862,10 @@
|
||||
},
|
||||
"Model": {
|
||||
"properties": {
|
||||
"additionalSpeedTiers": {
|
||||
"serviceTiers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/ModelServiceTier"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -7960,6 +7960,25 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModelServiceTier": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModelListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -12029,10 +12048,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"description": "String-backed service tier identifier. Known values include `priority`, `flex`, and `ultrafast`, but other backend-provided ids are allowed.",
|
||||
"type": "string"
|
||||
},
|
||||
"SessionMigration": {
|
||||
@@ -16115,4 +16131,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocolV2",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
},
|
||||
"Model": {
|
||||
"properties": {
|
||||
"additionalSpeedTiers": {
|
||||
"serviceTiers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/ModelServiceTier"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -120,6 +120,25 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModelServiceTier": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModelUpgradeInfo": {
|
||||
"properties": {
|
||||
"migrationMarkdown": {
|
||||
@@ -175,6 +194,10 @@
|
||||
"reasoningEffort"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"description": "String-backed service tier identifier. Known values include `priority`, `flex`, and `ultrafast`, but other backend-provided ids are allowed.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -197,4 +220,4 @@
|
||||
],
|
||||
"title": "ModelListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ServiceTier = "fast" | "flex";
|
||||
export type ServiceTier = string;
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@
|
||||
import type { InputModality } from "../InputModality";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
|
||||
import type { ModelServiceTier } from "./ModelServiceTier";
|
||||
import type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
|
||||
import type { ReasoningEffortOption } from "./ReasoningEffortOption";
|
||||
|
||||
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, additionalSpeedTiers: Array<string>, isDefault: boolean, };
|
||||
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, serviceTiers: Array<ModelServiceTier>, isDefault: boolean, };
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
|
||||
export type ModelServiceTier = { id: ServiceTier, name: string, description: string, };
|
||||
@@ -241,6 +241,7 @@ export type { Model } from "./Model";
|
||||
export type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
|
||||
export type { ModelListParams } from "./ModelListParams";
|
||||
export type { ModelListResponse } from "./ModelListResponse";
|
||||
export type { ModelServiceTier } from "./ModelServiceTier";
|
||||
export type { ModelProviderCapabilitiesReadParams } from "./ModelProviderCapabilitiesReadParams";
|
||||
export type { ModelProviderCapabilitiesReadResponse } from "./ModelProviderCapabilitiesReadResponse";
|
||||
export type { ModelRerouteReason } from "./ModelRerouteReason";
|
||||
|
||||
@@ -49,6 +49,7 @@ use codex_protocol::models::PermissionProfile as CorePermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailabilityNux;
|
||||
use codex_protocol::openai_models::ModelServiceTier as CoreModelServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand;
|
||||
@@ -2508,6 +2509,35 @@ impl From<CoreModelAvailabilityNux> for ModelAvailabilityNux {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ModelServiceTier {
|
||||
pub id: ServiceTier,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl From<CoreModelServiceTier> for ModelServiceTier {
|
||||
fn from(value: CoreModelServiceTier) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ModelServiceTier> for CoreModelServiceTier {
|
||||
fn from(value: ModelServiceTier) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -2527,7 +2557,7 @@ pub struct Model {
|
||||
#[serde(default)]
|
||||
pub supports_personality: bool,
|
||||
#[serde(default)]
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
pub service_tiers: Vec<ModelServiceTier>,
|
||||
// Only one model should be marked as default.
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ Example with notification opt-out:
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
|
||||
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `serviceTiers` (`id`, `name`, `description`), optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `remote_control`, `tool_search`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
|
||||
@@ -10352,7 +10352,7 @@ mod tests {
|
||||
path: None,
|
||||
model: None,
|
||||
model_provider: None,
|
||||
service_tier: Some(Some(codex_protocol::config_types::ServiceTier::Fast)),
|
||||
service_tier: Some(Some(codex_protocol::config_types::ServiceTier::priority())),
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
approvals_reviewer: None,
|
||||
@@ -10368,7 +10368,7 @@ mod tests {
|
||||
let config_snapshot = ThreadConfigSnapshot {
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider_id: "openai".to_string(),
|
||||
service_tier: Some(codex_protocol::config_types::ServiceTier::Flex),
|
||||
service_tier: Some(codex_protocol::config_types::ServiceTier::flex()),
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
|
||||
|
||||
@@ -42,7 +42,7 @@ fn model_from_preset(preset: ModelPreset) -> Model {
|
||||
default_reasoning_effort: preset.default_reasoning_effort,
|
||||
input_modalities: preset.input_modalities,
|
||||
supports_personality: preset.supports_personality,
|
||||
additional_speed_tiers: preset.additional_speed_tiers,
|
||||
service_tiers: preset.service_tiers.into_iter().map(Into::into).collect(),
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
},
|
||||
supported_in_api: preset.supported_in_api,
|
||||
priority,
|
||||
additional_speed_tiers: preset.additional_speed_tiers.clone(),
|
||||
service_tiers: preset.service_tiers.clone(),
|
||||
upgrade: preset.upgrade.as_ref().map(Into::into),
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -50,7 +50,12 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
|
||||
// cache report `supports_personality = false`.
|
||||
// todo(sayan): fix, maybe make roundtrip use ModelInfo only
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: preset.additional_speed_tiers.clone(),
|
||||
service_tiers: preset
|
||||
.service_tiers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> {
|
||||
|
||||
let req_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
service_tier: Some(Some(ServiceTier::Flex)),
|
||||
service_tier: Some(Some(ServiceTier::flex())),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -352,7 +352,7 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> {
|
||||
.await??;
|
||||
let ThreadStartResponse { service_tier, .. } = to_response::<ThreadStartResponse>(resp)?;
|
||||
|
||||
assert_eq!(service_tier, Some(ServiceTier::Flex));
|
||||
assert_eq!(service_tier, Some(ServiceTier::flex()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ async fn models_client_hits_models_endpoint() {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -2107,10 +2107,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast",
|
||||
"flex"
|
||||
],
|
||||
"description": "String-backed service tier identifier. Known values include `priority`, `flex`, and `ultrafast`, but other backend-provided ids are allowed.",
|
||||
"type": "string"
|
||||
},
|
||||
"ShellEnvironmentPolicyInherit": {
|
||||
@@ -4036,4 +4033,4 @@
|
||||
},
|
||||
"title": "ConfigToml",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,11 +889,7 @@ impl ModelClientSession {
|
||||
store: provider.is_azure_responses_endpoint(),
|
||||
stream: true,
|
||||
include,
|
||||
service_tier: match service_tier {
|
||||
Some(ServiceTier::Fast) => Some("priority".to_string()),
|
||||
Some(service_tier) => Some(service_tier.to_string()),
|
||||
None => None,
|
||||
},
|
||||
service_tier: service_tier.map(|service_tier| service_tier.to_string()),
|
||||
prompt_cache_key,
|
||||
text,
|
||||
client_metadata: Some(HashMap::from([(
|
||||
|
||||
@@ -155,7 +155,7 @@ fn serializes_flex_service_tier_when_set() {
|
||||
stream: true,
|
||||
include: vec![],
|
||||
prompt_cache_key: None,
|
||||
service_tier: Some(ServiceTier::Flex.to_string()),
|
||||
service_tier: Some(ServiceTier::flex().to_string()),
|
||||
text: None,
|
||||
client_metadata: None,
|
||||
};
|
||||
|
||||
@@ -530,7 +530,7 @@ async fn drain_to_completed(
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
// Rollout tracing currently models remote compaction only; local compaction streams
|
||||
// are left untraced until the reducer has a first-class local compaction lifecycle.
|
||||
|
||||
@@ -2578,12 +2578,10 @@ impl Config {
|
||||
None => config_profile.service_tier.or(cfg.service_tier),
|
||||
};
|
||||
let service_tier = match service_tier {
|
||||
Some(ServiceTier::Fast) if features.enabled(Feature::FastMode) => {
|
||||
Some(ServiceTier::Fast)
|
||||
Some(service_tier) if service_tier.is_priority() && !features.enabled(Feature::FastMode) => {
|
||||
None
|
||||
}
|
||||
Some(ServiceTier::Fast) => None,
|
||||
Some(ServiceTier::Flex) => Some(ServiceTier::Flex),
|
||||
None => None,
|
||||
other => other,
|
||||
};
|
||||
|
||||
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
|
||||
|
||||
@@ -586,7 +586,7 @@ impl Codex {
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.account_plan_type());
|
||||
let service_tier = get_service_tier(
|
||||
config.service_tier,
|
||||
config.service_tier.clone(),
|
||||
config.notices.fast_default_opt_out.unwrap_or(false),
|
||||
account_plan_type,
|
||||
config.features.enabled(Feature::FastMode),
|
||||
@@ -789,7 +789,7 @@ fn get_service_tier(
|
||||
|
||||
account_plan_type
|
||||
.is_some_and(is_enterprise_default_service_tier_plan)
|
||||
.then_some(ServiceTier::Fast)
|
||||
.then(ServiceTier::priority)
|
||||
}
|
||||
|
||||
fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool {
|
||||
|
||||
@@ -130,7 +130,7 @@ impl SessionConfiguration {
|
||||
ThreadConfigSnapshot {
|
||||
model: self.collaboration_mode.model().to_string(),
|
||||
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
|
||||
service_tier: self.service_tier,
|
||||
service_tier: self.service_tier.clone(),
|
||||
approval_policy: self.approval_policy.value(),
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
permission_profile: self.permission_profile(),
|
||||
@@ -175,7 +175,7 @@ impl SessionConfiguration {
|
||||
if let Some(summary) = updates.reasoning_summary {
|
||||
next_configuration.model_reasoning_summary = Some(summary);
|
||||
}
|
||||
if let Some(service_tier) = updates.service_tier {
|
||||
if let Some(service_tier) = updates.service_tier.clone() {
|
||||
next_configuration.service_tier = service_tier;
|
||||
}
|
||||
if let Some(personality) = updates.personality {
|
||||
|
||||
@@ -2790,7 +2790,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() {
|
||||
Some(AccountPlanType::Enterprise),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast)
|
||||
Some(ServiceTier::priority())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
@@ -2799,7 +2799,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() {
|
||||
Some(AccountPlanType::EnterpriseCbpUsageBased),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast)
|
||||
Some(ServiceTier::priority())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
@@ -2808,7 +2808,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() {
|
||||
Some(AccountPlanType::Business),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast)
|
||||
Some(ServiceTier::priority())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
@@ -2817,7 +2817,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() {
|
||||
Some(AccountPlanType::Team),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast)
|
||||
Some(ServiceTier::priority())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
@@ -2826,7 +2826,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() {
|
||||
Some(AccountPlanType::SelfServeBusinessUsageBased),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast)
|
||||
Some(ServiceTier::priority())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -689,7 +689,7 @@ async fn track_turn_resolved_config_analytics(
|
||||
permission_profile_cwd: turn_context.cwd.to_path_buf(),
|
||||
reasoning_effort: turn_context.reasoning_effort,
|
||||
reasoning_summary: Some(turn_context.reasoning_summary),
|
||||
service_tier: turn_context.config.service_tier,
|
||||
service_tier: turn_context.config.service_tier.clone(),
|
||||
approval_policy: turn_context.approval_policy.value(),
|
||||
approvals_reviewer: turn_context.config.approvals_reviewer,
|
||||
sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(),
|
||||
@@ -1837,7 +1837,7 @@ async fn try_run_sampling_request(
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
&inference_trace,
|
||||
)
|
||||
|
||||
@@ -394,7 +394,7 @@ impl Session {
|
||||
per_turn_config.model_reasoning_effort =
|
||||
session_configuration.collaboration_mode.reasoning_effort();
|
||||
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
|
||||
per_turn_config.service_tier = session_configuration.service_tier;
|
||||
per_turn_config.service_tier = session_configuration.service_tier.clone();
|
||||
per_turn_config.personality = session_configuration.personality;
|
||||
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
|
||||
per_turn_config.permissions.permission_profile =
|
||||
|
||||
@@ -269,7 +269,7 @@ async fn websocket_v2_first_turn_uses_updated_fast_tier_after_startup_prewarm()
|
||||
assert_eq!(warmup["generate"].as_bool(), Some(false));
|
||||
assert_eq!(warmup.get("service_tier"), None);
|
||||
|
||||
test.submit_turn_with_service_tier("hello", Some(ServiceTier::Fast))
|
||||
test.submit_turn_with_service_tier("hello", Some(ServiceTier::priority()))
|
||||
.await?;
|
||||
|
||||
assert_eq!(server.handshakes().len(), 1);
|
||||
@@ -313,7 +313,7 @@ async fn websocket_v2_first_turn_drops_fast_tier_after_startup_prewarm() -> Resu
|
||||
.features
|
||||
.enable(Feature::ResponsesWebsocketsV2)
|
||||
.expect("test config should allow feature update");
|
||||
config.service_tier = Some(ServiceTier::Fast);
|
||||
config.service_tier = Some(ServiceTier::priority());
|
||||
});
|
||||
let test = builder.build_with_websocket_server(&server).await?;
|
||||
|
||||
@@ -385,7 +385,7 @@ async fn websocket_v2_next_turn_uses_updated_service_tier() -> Result<()> {
|
||||
assert_eq!(warmup["generate"].as_bool(), Some(false));
|
||||
assert_eq!(warmup.get("service_tier"), None);
|
||||
|
||||
test.submit_turn_with_service_tier("first", Some(ServiceTier::Fast))
|
||||
test.submit_turn_with_service_tier("first", Some(ServiceTier::priority()))
|
||||
.await?;
|
||||
test.submit_turn_with_service_tier("second", /*service_tier*/ None)
|
||||
.await?;
|
||||
|
||||
@@ -105,7 +105,7 @@ fn test_model_info(
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
@@ -297,7 +297,7 @@ async fn service_tier_change_is_applied_on_next_http_turn() -> Result<()> {
|
||||
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast))
|
||||
test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::priority()))
|
||||
.await?;
|
||||
test.submit_turn_with_service_tier("standard turn", /*service_tier*/ None)
|
||||
.await?;
|
||||
@@ -323,7 +323,7 @@ async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> {
|
||||
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::Flex))
|
||||
test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::flex()))
|
||||
.await?;
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
@@ -817,7 +817,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -340,7 +340,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -578,7 +578,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
@@ -686,7 +686,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
|
||||
@@ -532,7 +532,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
@@ -788,7 +788,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: remote_base.to_string(),
|
||||
model_messages: None,
|
||||
@@ -1278,7 +1278,7 @@ fn test_remote_model_with_policy(
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -1282,7 +1282,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -66,7 +66,7 @@ fn test_model_info(
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -1059,7 +1059,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -253,14 +253,14 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
service_tier: Some(Some(ServiceTier::priority())),
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let config_snapshot = wait_for_service_tier(&test, Some(ServiceTier::Fast)).await?;
|
||||
assert_eq!(config_snapshot.service_tier, Some(ServiceTier::Fast));
|
||||
let config_snapshot = wait_for_service_tier(&test, Some(ServiceTier::priority())).await?;
|
||||
assert_eq!(config_snapshot.service_tier, Some(ServiceTier::priority()));
|
||||
|
||||
let context = crate::runtime::MemoryStartupContext::new(
|
||||
Arc::clone(&test.thread_manager),
|
||||
@@ -277,7 +277,7 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu
|
||||
ReasoningEffort::Low,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(request_context.service_tier, Some(ServiceTier::Fast));
|
||||
assert_eq!(request_context.service_tier, Some(ServiceTier::priority()));
|
||||
|
||||
shutdown_test_codex(&test).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use codex_models_manager::model_info::BASE_INSTRUCTIONS;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -46,7 +48,11 @@ fn gpt_5_4_cmb_bedrock_model(priority: i32) -> ModelInfo {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority,
|
||||
additional_speed_tiers: vec!["fast".to_string()],
|
||||
service_tiers: vec![ModelServiceTier {
|
||||
id: ServiceTier::priority(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Fast tier".to_string(),
|
||||
}],
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
@@ -86,7 +92,7 @@ fn bedrock_oss_model(slug: &str, display_name: &str, priority: i32) -> ModelInfo
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
|
||||
@@ -82,8 +82,12 @@
|
||||
"team"
|
||||
],
|
||||
"supports_search_tool": true,
|
||||
"additional_speed_tiers": [
|
||||
"fast"
|
||||
"service_tiers": [
|
||||
{
|
||||
"id": "priority",
|
||||
"name": "Fast",
|
||||
"description": "Fast tier"
|
||||
}
|
||||
],
|
||||
"supports_reasoning_summaries": true
|
||||
},
|
||||
@@ -164,8 +168,12 @@
|
||||
"team"
|
||||
],
|
||||
"supports_search_tool": true,
|
||||
"additional_speed_tiers": [
|
||||
"fast"
|
||||
"service_tiers": [
|
||||
{
|
||||
"id": "priority",
|
||||
"name": "Fast",
|
||||
"description": "Fast tier"
|
||||
}
|
||||
],
|
||||
"supports_reasoning_summaries": true
|
||||
},
|
||||
@@ -249,7 +257,7 @@
|
||||
"team"
|
||||
],
|
||||
"supports_search_tool": true,
|
||||
"additional_speed_tiers": [],
|
||||
"service_tiers": [],
|
||||
"supports_reasoning_summaries": true
|
||||
},
|
||||
{
|
||||
@@ -332,7 +340,7 @@
|
||||
"team"
|
||||
],
|
||||
"supports_search_tool": true,
|
||||
"additional_speed_tiers": [],
|
||||
"service_tiers": [],
|
||||
"supports_reasoning_summaries": true
|
||||
},
|
||||
{
|
||||
@@ -411,7 +419,7 @@
|
||||
"team"
|
||||
],
|
||||
"supports_search_tool": true,
|
||||
"additional_speed_tiers": [],
|
||||
"service_tiers": [],
|
||||
"supports_reasoning_summaries": true
|
||||
},
|
||||
{
|
||||
@@ -491,7 +499,7 @@
|
||||
"team"
|
||||
],
|
||||
"supports_search_tool": true,
|
||||
"additional_speed_tiers": [],
|
||||
"service_tiers": [],
|
||||
"supports_reasoning_summaries": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -75,7 +75,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
|
||||
visibility: ModelVisibility::None,
|
||||
supported_in_api: true,
|
||||
priority: 99,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
|
||||
@@ -347,12 +347,93 @@ impl From<WebSearchToolConfig> for WebSearchConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum ServiceTier {
|
||||
Fast,
|
||||
Flex,
|
||||
pub const SERVICE_TIER_PRIORITY: &str = "priority";
|
||||
pub const SERVICE_TIER_FLEX: &str = "flex";
|
||||
pub const SERVICE_TIER_ULTRAFAST: &str = "ultrafast";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, TS)]
|
||||
#[serde(transparent)]
|
||||
#[ts(type = "string")]
|
||||
pub struct ServiceTier(String);
|
||||
|
||||
impl ServiceTier {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn priority() -> Self {
|
||||
Self::new(SERVICE_TIER_PRIORITY)
|
||||
}
|
||||
|
||||
pub fn flex() -> Self {
|
||||
Self::new(SERVICE_TIER_FLEX)
|
||||
}
|
||||
|
||||
pub fn ultrafast() -> Self {
|
||||
Self::new(SERVICE_TIER_ULTRAFAST)
|
||||
}
|
||||
|
||||
pub fn is_priority(&self) -> bool {
|
||||
self.as_str() == SERVICE_TIER_PRIORITY
|
||||
}
|
||||
|
||||
pub fn is_flex(&self) -> bool {
|
||||
self.as_str() == SERVICE_TIER_FLEX
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ServiceTier {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ServiceTier {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ServiceTier {
|
||||
fn from(value: String) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServiceTier> for String {
|
||||
fn from(value: ServiceTier) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServiceTier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for ServiceTier {
|
||||
fn schema_name() -> String {
|
||||
"ServiceTier".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"String-backed service tier identifier. Known values include `priority`, `flex`, and `ultrafast`, but other backend-provided ids are allowed."
|
||||
.to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
|
||||
@@ -17,10 +17,10 @@ use ts_rs::TS;
|
||||
|
||||
use crate::config_types::Personality;
|
||||
use crate::config_types::ReasoningSummary;
|
||||
use crate::config_types::ServiceTier;
|
||||
use crate::config_types::Verbosity;
|
||||
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
pub const SPEED_TIER_FAST: &str = "fast";
|
||||
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
#[derive(
|
||||
@@ -115,6 +115,13 @@ pub struct ModelAvailabilityNux {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ModelServiceTier {
|
||||
pub id: ServiceTier,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Metadata describing a Codex-supported model.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
pub struct ModelPreset {
|
||||
@@ -133,9 +140,9 @@ pub struct ModelPreset {
|
||||
/// Whether this model supports personality-specific instructions.
|
||||
#[serde(default)]
|
||||
pub supports_personality: bool,
|
||||
/// Additional speed tiers this model can run with beyond the standard path.
|
||||
/// User-selectable service tiers supported by this model.
|
||||
#[serde(default)]
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
pub service_tiers: Vec<ModelServiceTier>,
|
||||
/// Whether this is the default model for new users.
|
||||
pub is_default: bool,
|
||||
/// recommended upgrade model
|
||||
@@ -257,7 +264,7 @@ pub struct ModelInfo {
|
||||
pub supported_in_api: bool,
|
||||
pub priority: i32,
|
||||
#[serde(default)]
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
pub service_tiers: Vec<ModelServiceTier>,
|
||||
pub availability_nux: Option<ModelAvailabilityNux>,
|
||||
pub upgrade: Option<ModelInfoUpgrade>,
|
||||
pub base_instructions: String,
|
||||
@@ -441,7 +448,7 @@ impl From<ModelInfo> for ModelPreset {
|
||||
.unwrap_or(ReasoningEffort::None),
|
||||
supported_reasoning_efforts: info.supported_reasoning_levels.clone(),
|
||||
supports_personality,
|
||||
additional_speed_tiers: info.additional_speed_tiers,
|
||||
service_tiers: info.service_tiers,
|
||||
is_default: false, // default is the highest priority available model
|
||||
upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {
|
||||
id: upgrade.model.clone(),
|
||||
@@ -463,10 +470,16 @@ impl From<ModelInfo> for ModelPreset {
|
||||
}
|
||||
|
||||
impl ModelPreset {
|
||||
pub fn supports_fast_mode(&self) -> bool {
|
||||
self.additional_speed_tiers
|
||||
pub fn supports_service_tier(&self, service_tier: &ServiceTier) -> bool {
|
||||
self.service_tiers
|
||||
.iter()
|
||||
.any(|tier| tier == SPEED_TIER_FAST)
|
||||
.any(|tier| tier.id == *service_tier)
|
||||
}
|
||||
|
||||
pub fn service_tier(&self, service_tier: &ServiceTier) -> Option<&ModelServiceTier> {
|
||||
self.service_tiers
|
||||
.iter()
|
||||
.find(|tier| tier.id == *service_tier)
|
||||
}
|
||||
|
||||
/// Filter models based on authentication mode.
|
||||
@@ -547,7 +560,7 @@ mod tests {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base".to_string(),
|
||||
@@ -817,7 +830,11 @@ mod tests {
|
||||
availability_nux: Some(ModelAvailabilityNux {
|
||||
message: "Try Spark.".to_string(),
|
||||
}),
|
||||
additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()],
|
||||
service_tiers: vec![ModelServiceTier {
|
||||
id: ServiceTier::priority(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Fast tier".to_string(),
|
||||
}],
|
||||
..test_model(/*spec*/ None)
|
||||
});
|
||||
|
||||
@@ -827,6 +844,6 @@ mod tests {
|
||||
message: "Try Spark.".to_string(),
|
||||
})
|
||||
);
|
||||
assert!(preset.supports_fast_mode());
|
||||
assert!(preset.supports_service_tier(&ServiceTier::priority()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {
|
||||
description: "Balanced".to_string(),
|
||||
}],
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker,
|
||||
|
||||
@@ -1163,25 +1163,26 @@ impl App {
|
||||
AppEvent::PersistServiceTierSelection { service_tier } => {
|
||||
self.refresh_status_line();
|
||||
let profile = self.active_profile.as_deref();
|
||||
self.config.service_tier = service_tier;
|
||||
self.config.service_tier = service_tier.clone();
|
||||
let mut edits = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_service_tier(service_tier);
|
||||
.set_service_tier(service_tier.clone());
|
||||
if service_tier.is_none() {
|
||||
self.config.notices.fast_default_opt_out = Some(true);
|
||||
edits = edits.set_fast_default_opt_out(/*opted_out*/ true);
|
||||
}
|
||||
match edits.apply().await {
|
||||
Ok(()) => {
|
||||
let status = if matches!(
|
||||
service_tier,
|
||||
Some(codex_protocol::config_types::ServiceTier::Fast)
|
||||
) {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
let mut message = match service_tier.as_ref() {
|
||||
Some(service_tier) => format!(
|
||||
"Service tier set to {}",
|
||||
self.chat_widget.service_tier_display_name(
|
||||
self.chat_widget.current_model(),
|
||||
service_tier,
|
||||
)
|
||||
),
|
||||
None => "Service tier cleared".to_string(),
|
||||
};
|
||||
let mut message = format!("Fast mode set to {status}");
|
||||
if let Some(profile) = profile {
|
||||
message.push_str(" for ");
|
||||
message.push_str(profile);
|
||||
@@ -1190,14 +1191,14 @@ impl App {
|
||||
self.chat_widget.add_info_message(message, /*hint*/ None);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(error = %err, "failed to persist fast mode selection");
|
||||
tracing::error!(error = %err, "failed to persist service tier selection");
|
||||
if let Some(profile) = profile {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to save Fast mode for profile `{profile}`: {err}"
|
||||
"Failed to save service tier for profile `{profile}`: {err}"
|
||||
));
|
||||
} else {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to save default Fast mode: {err}"
|
||||
"Failed to save default service tier: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3662,7 +3662,7 @@ async fn clear_ui_header_shows_fast_status_for_fast_capable_models() {
|
||||
app.chat_widget
|
||||
.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
|
||||
app.chat_widget
|
||||
.set_service_tier(Some(codex_protocol::config_types::ServiceTier::Fast));
|
||||
.set_service_tier(Some(codex_protocol::config_types::ServiceTier::priority()));
|
||||
set_chatgpt_auth(&mut app.chat_widget);
|
||||
set_fast_mode_test_catalog(&mut app.chat_widget);
|
||||
|
||||
@@ -4280,13 +4280,13 @@ fn active_turn_steer_race_extracts_actual_turn_id_from_mismatch() {
|
||||
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));
|
||||
.set_service_tier(Some(codex_protocol::config_types::ServiceTier::priority()));
|
||||
|
||||
let config = app.fresh_session_config();
|
||||
|
||||
assert_eq!(
|
||||
config.service_tier,
|
||||
Some(codex_protocol::config_types::ServiceTier::Fast)
|
||||
Some(codex_protocol::config_types::ServiceTier::priority())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1073,7 +1073,7 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
|
||||
})
|
||||
.collect(),
|
||||
supports_personality: model.supports_personality,
|
||||
additional_speed_tiers: model.additional_speed_tiers,
|
||||
service_tiers: model.service_tiers.into_iter().map(Into::into).collect(),
|
||||
is_default: model.is_default,
|
||||
upgrade,
|
||||
show_in_picker: !model.hidden,
|
||||
|
||||
@@ -184,6 +184,8 @@ use super::skill_popup::MentionItem;
|
||||
use super::skill_popup::SkillPopup;
|
||||
use super::slash_commands;
|
||||
use super::slash_commands::BuiltinCommandFlags;
|
||||
use super::slash_commands::ServiceTierCommand;
|
||||
use super::slash_commands::SlashCommandAction;
|
||||
use crate::bottom_pane::paste_burst::FlushResult;
|
||||
use crate::bottom_pane::prompt_args::parse_slash_name;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
@@ -262,13 +264,13 @@ pub enum InputResult {
|
||||
///
|
||||
/// Callers that dispatch this variant are also responsible for resolving any pending local
|
||||
/// command-history entry that the composer staged before clearing the visible input.
|
||||
Command(SlashCommand),
|
||||
Command(SlashCommandAction),
|
||||
/// An inline slash command and its trimmed argument text.
|
||||
///
|
||||
/// The `TextElement` ranges are rebased into the argument string, while any pending local
|
||||
/// command-history entry still represents the original command invocation that should be
|
||||
/// committed only if dispatch accepts it.
|
||||
CommandWithArgs(SlashCommand, String, Vec<TextElement>),
|
||||
CommandWithArgs(SlashCommandAction, String, Vec<TextElement>),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -386,6 +388,7 @@ pub(crate) struct ChatComposer {
|
||||
goal_status_indicator: Option<GoalStatusIndicator>,
|
||||
connectors_enabled: bool,
|
||||
plugins_command_enabled: bool,
|
||||
service_tier_commands: Vec<ServiceTierCommand>,
|
||||
fast_command_enabled: bool,
|
||||
goal_command_enabled: bool,
|
||||
personality_command_enabled: bool,
|
||||
@@ -573,6 +576,7 @@ impl ChatComposer {
|
||||
goal_status_indicator: None,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
service_tier_commands: Vec::new(),
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: false,
|
||||
@@ -646,6 +650,11 @@ impl ChatComposer {
|
||||
self.plugins_command_enabled = enabled;
|
||||
}
|
||||
|
||||
pub fn set_service_tier_commands(&mut self, commands: Vec<ServiceTierCommand>) {
|
||||
self.service_tier_commands = commands;
|
||||
self.sync_popups();
|
||||
}
|
||||
|
||||
/// Toggle composer-side image paste handling.
|
||||
///
|
||||
/// This only affects whether image-like paste content is converted into attachments; the
|
||||
@@ -1634,13 +1643,10 @@ impl ChatComposer {
|
||||
// before applying completion.
|
||||
let first_line = self.textarea.text().lines().next().unwrap_or("");
|
||||
popup.on_composer_text_change(first_line.to_string());
|
||||
let selected_cmd = popup.selected_item().map(|sel| {
|
||||
let CommandItem::Builtin(cmd) = sel;
|
||||
cmd
|
||||
});
|
||||
let selected_cmd = popup.selected_item();
|
||||
if let Some(cmd) = selected_cmd {
|
||||
if cmd == SlashCommand::Skills {
|
||||
self.stage_selected_slash_command_history(cmd);
|
||||
if cmd.as_builtin() == Some(SlashCommand::Skills) {
|
||||
self.stage_selected_slash_command_history(&cmd);
|
||||
self.textarea.set_text_clearing_elements("");
|
||||
self.is_bash_mode = false;
|
||||
return (InputResult::Command(cmd), true);
|
||||
@@ -1672,11 +1678,7 @@ impl ChatComposer {
|
||||
// while the slash-command popup is active.
|
||||
let first_line = self.textarea.text().lines().next().unwrap_or("");
|
||||
popup.on_composer_text_change(first_line.to_string());
|
||||
let selected_cmd = popup.selected_item().map(|sel| {
|
||||
let CommandItem::Builtin(cmd) = sel;
|
||||
cmd
|
||||
});
|
||||
if let Some(cmd) = selected_cmd {
|
||||
if let Some(cmd) = popup.selected_item() {
|
||||
let starts_with_cmd = first_line
|
||||
.trim_start()
|
||||
.starts_with(&format!("/{}", cmd.command()));
|
||||
@@ -1696,9 +1698,8 @@ impl ChatComposer {
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} => {
|
||||
if let Some(sel) = popup.selected_item() {
|
||||
let CommandItem::Builtin(cmd) = sel;
|
||||
self.stage_selected_slash_command_history(cmd);
|
||||
if let Some(cmd) = popup.selected_item() {
|
||||
self.stage_selected_slash_command_history(&cmd);
|
||||
self.textarea.set_text_clearing_elements("");
|
||||
self.is_bash_mode = false;
|
||||
return (InputResult::Command(cmd), true);
|
||||
@@ -2496,10 +2497,13 @@ 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.builtin_command_flags())
|
||||
.is_some();
|
||||
if !is_builtin {
|
||||
if slash_commands::find_command(
|
||||
name,
|
||||
self.builtin_command_flags(),
|
||||
&self.service_tier_commands,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
let message = format!(
|
||||
r#"Unrecognized command '/{name}'. Type "/" for a list of supported commands."#
|
||||
);
|
||||
@@ -2700,10 +2704,13 @@ 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.builtin_command_flags())
|
||||
&& let Some(cmd) = slash_commands::find_command(
|
||||
name,
|
||||
self.builtin_command_flags(),
|
||||
&self.service_tier_commands,
|
||||
)
|
||||
{
|
||||
if self.reject_slash_command_if_unavailable(cmd) {
|
||||
if self.reject_slash_command_if_unavailable(&cmd) {
|
||||
self.stage_slash_command_history();
|
||||
self.record_pending_slash_command_history();
|
||||
return Some(InputResult::None);
|
||||
@@ -2733,12 +2740,16 @@ impl ChatComposer {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cmd = slash_commands::find_builtin_command(name, self.builtin_command_flags())?;
|
||||
let cmd = slash_commands::find_command(
|
||||
name,
|
||||
self.builtin_command_flags(),
|
||||
&self.service_tier_commands,
|
||||
)?;
|
||||
|
||||
if !cmd.supports_inline_args() {
|
||||
return None;
|
||||
}
|
||||
if self.reject_slash_command_if_unavailable(cmd) {
|
||||
if self.reject_slash_command_if_unavailable(&cmd) {
|
||||
self.stage_slash_command_history();
|
||||
self.record_pending_slash_command_history();
|
||||
return Some(InputResult::None);
|
||||
@@ -2783,7 +2794,7 @@ impl ChatComposer {
|
||||
Some((trimmed_rest.to_string(), args_elements))
|
||||
}
|
||||
|
||||
fn reject_slash_command_if_unavailable(&self, cmd: SlashCommand) -> bool {
|
||||
fn reject_slash_command_if_unavailable(&self, cmd: &SlashCommandAction) -> bool {
|
||||
if !self.is_task_running || cmd.available_during_task() {
|
||||
return false;
|
||||
}
|
||||
@@ -2828,7 +2839,7 @@ impl ChatComposer {
|
||||
///
|
||||
/// Popup filtering text can be partial, so recording the selected command avoids recalling
|
||||
/// `/di` after the user actually accepted `/diff`.
|
||||
fn stage_selected_slash_command_history(&mut self, cmd: SlashCommand) {
|
||||
fn stage_selected_slash_command_history(&mut self, cmd: &SlashCommandAction) {
|
||||
self.stage_slash_command_history_text(format!("/{}", cmd.command()));
|
||||
}
|
||||
|
||||
@@ -3545,7 +3556,12 @@ impl ChatComposer {
|
||||
}
|
||||
|
||||
fn is_known_slash_name(&self, name: &str) -> bool {
|
||||
slash_commands::find_builtin_command(name, self.builtin_command_flags()).is_some()
|
||||
slash_commands::find_command(
|
||||
name,
|
||||
self.builtin_command_flags(),
|
||||
&self.service_tier_commands,
|
||||
)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// If the cursor is currently within a slash command on the first line,
|
||||
@@ -3577,7 +3593,7 @@ impl ChatComposer {
|
||||
}
|
||||
|
||||
/// Heuristic for whether the typed slash command looks like a valid
|
||||
/// prefix for any known built-in command.
|
||||
/// prefix for any known command.
|
||||
/// Empty names only count when there is no extra content after the '/'.
|
||||
fn looks_like_slash_prefix(&self, name: &str, rest_after_name: &str) -> bool {
|
||||
if !self.slash_commands_enabled() {
|
||||
@@ -3587,7 +3603,11 @@ impl ChatComposer {
|
||||
return rest_after_name.is_empty();
|
||||
}
|
||||
|
||||
slash_commands::has_builtin_prefix(name, self.builtin_command_flags())
|
||||
slash_commands::has_command_prefix(
|
||||
name,
|
||||
self.builtin_command_flags(),
|
||||
&self.service_tier_commands,
|
||||
)
|
||||
}
|
||||
|
||||
/// Synchronize `self.command_popup` with the current text in the
|
||||
@@ -3638,18 +3658,21 @@ impl ChatComposer {
|
||||
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;
|
||||
let mut command_popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled,
|
||||
connectors_enabled,
|
||||
plugins_command_enabled,
|
||||
fast_command_enabled,
|
||||
goal_command_enabled,
|
||||
personality_command_enabled,
|
||||
realtime_conversation_enabled,
|
||||
audio_device_selection_enabled,
|
||||
windows_degraded_sandbox_active: self.windows_degraded_sandbox_active,
|
||||
side_conversation_active: self.side_conversation_active,
|
||||
});
|
||||
let mut command_popup = CommandPopup::new(
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled,
|
||||
connectors_enabled,
|
||||
plugins_command_enabled,
|
||||
fast_command_enabled,
|
||||
goal_command_enabled,
|
||||
personality_command_enabled,
|
||||
realtime_conversation_enabled,
|
||||
audio_device_selection_enabled,
|
||||
windows_degraded_sandbox_active: self.windows_degraded_sandbox_active,
|
||||
side_conversation_active: self.side_conversation_active,
|
||||
},
|
||||
&self.service_tier_commands,
|
||||
);
|
||||
command_popup.on_composer_text_change(first_line.to_string());
|
||||
self.active_popup = ActivePopup::Command(command_popup);
|
||||
}
|
||||
@@ -6416,7 +6439,10 @@ mod tests {
|
||||
|
||||
let (result, _) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert!(matches!(result, InputResult::Command(SlashCommand::Diff)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
InputResult::Command(SlashCommandAction::Builtin(SlashCommand::Diff))
|
||||
));
|
||||
}
|
||||
|
||||
/// Behavior: if a burst is buffering text and the user presses a non-char key, flush the
|
||||
@@ -6883,7 +6909,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn slash_popup_model_first_for_mo_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
@@ -6897,9 +6922,7 @@ mod tests {
|
||||
|
||||
match &composer.active_popup {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "model")
|
||||
}
|
||||
Some(command) => assert_eq!(command.command(), "model"),
|
||||
None => panic!("no selected command for '/mo'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/mo'"),
|
||||
@@ -6936,7 +6959,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn slash_popup_resume_for_res_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
@@ -6950,9 +6972,7 @@ mod tests {
|
||||
|
||||
match &composer.active_popup {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "resume")
|
||||
}
|
||||
Some(command) => assert_eq!(command.command(), "resume"),
|
||||
None => panic!("no selected command for '/res'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/res'"),
|
||||
@@ -9083,7 +9103,10 @@ mod tests {
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(result, InputResult::Command(SlashCommand::Diff));
|
||||
assert_eq!(
|
||||
result,
|
||||
InputResult::Command(SlashCommandAction::Builtin(SlashCommand::Diff))
|
||||
);
|
||||
composer.record_pending_slash_command_history();
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
@@ -9108,7 +9131,10 @@ mod tests {
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(result, InputResult::Command(SlashCommand::Diff));
|
||||
assert_eq!(
|
||||
result,
|
||||
InputResult::Command(SlashCommandAction::Builtin(SlashCommand::Diff))
|
||||
);
|
||||
composer.record_pending_slash_command_history();
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
@@ -9137,7 +9163,7 @@ mod tests {
|
||||
|
||||
match result {
|
||||
InputResult::CommandWithArgs(cmd, args, text_elements) => {
|
||||
assert_eq!(cmd, SlashCommand::Plan);
|
||||
assert_eq!(cmd.command(), "plan");
|
||||
assert_eq!(args, "investigate this");
|
||||
assert!(text_elements.is_empty());
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_popup_common::measure_rows_height_with_col_width_mode;
|
||||
use super::selection_popup_common::render_rows_with_col_width_mode;
|
||||
use super::slash_commands;
|
||||
use super::slash_commands::ServiceTierCommand;
|
||||
use super::slash_commands::SlashCommandAction;
|
||||
use crate::render::Insets;
|
||||
use crate::render::RectExt;
|
||||
use crate::slash_command::SlashCommand;
|
||||
@@ -24,14 +26,11 @@ const COMMAND_COLUMN_WIDTH: ColumnWidthConfig = ColumnWidthConfig::new(
|
||||
);
|
||||
|
||||
/// A selectable item in the popup.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum CommandItem {
|
||||
Builtin(SlashCommand),
|
||||
}
|
||||
pub(crate) type CommandItem = SlashCommandAction;
|
||||
|
||||
pub(crate) struct CommandPopup {
|
||||
command_filter: String,
|
||||
builtins: Vec<(&'static str, SlashCommand)>,
|
||||
commands: Vec<CommandItem>,
|
||||
state: ScrollState,
|
||||
}
|
||||
|
||||
@@ -67,17 +66,19 @@ impl From<CommandPopupFlags> for slash_commands::BuiltinCommandFlags {
|
||||
}
|
||||
|
||||
impl CommandPopup {
|
||||
pub(crate) fn new(flags: CommandPopupFlags) -> Self {
|
||||
// Keep built-in availability in sync with the composer.
|
||||
let builtins: Vec<(&'static str, SlashCommand)> =
|
||||
slash_commands::builtins_for_input(flags.into())
|
||||
.into_iter()
|
||||
.filter(|(name, _)| !name.starts_with("debug"))
|
||||
.filter(|(_, cmd)| *cmd != SlashCommand::Apps)
|
||||
.collect();
|
||||
pub(crate) fn new(
|
||||
flags: CommandPopupFlags,
|
||||
service_tier_commands: &[ServiceTierCommand],
|
||||
) -> Self {
|
||||
// Keep command availability in sync with the composer.
|
||||
let commands = slash_commands::commands_for_input(flags.into(), service_tier_commands)
|
||||
.into_iter()
|
||||
.filter(|command| !command.command().starts_with("debug"))
|
||||
.filter(|command| command.as_builtin() != Some(SlashCommand::Apps))
|
||||
.collect();
|
||||
Self {
|
||||
command_filter: String::new(),
|
||||
builtins,
|
||||
commands,
|
||||
state: ScrollState::new(),
|
||||
}
|
||||
}
|
||||
@@ -127,18 +128,20 @@ impl CommandPopup {
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute exact/prefix matches over built-in commands and user prompts,
|
||||
/// paired with optional highlight indices. Preserves the original
|
||||
/// presentation order for built-ins and prompts.
|
||||
/// Compute exact/prefix matches over visible commands, paired with
|
||||
/// optional highlight indices. Preserves the original presentation order.
|
||||
fn filtered(&self) -> Vec<(CommandItem, Option<Vec<usize>>)> {
|
||||
let filter = self.command_filter.trim();
|
||||
let mut out: Vec<(CommandItem, Option<Vec<usize>>)> = Vec::new();
|
||||
if filter.is_empty() {
|
||||
for (_, cmd) in self.builtins.iter() {
|
||||
if ALIAS_COMMANDS.contains(cmd) {
|
||||
for command in self.commands.iter() {
|
||||
if command
|
||||
.as_builtin()
|
||||
.is_some_and(|builtin| ALIAS_COMMANDS.contains(&builtin))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
out.push((CommandItem::Builtin(*cmd), None));
|
||||
out.push((command.clone(), None));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -170,8 +173,8 @@ impl CommandPopup {
|
||||
}
|
||||
};
|
||||
|
||||
for (_, cmd) in self.builtins.iter() {
|
||||
push_match(CommandItem::Builtin(*cmd), cmd.command(), None, 0);
|
||||
for command in self.commands.iter() {
|
||||
push_match(command.clone(), command.command(), None, 0);
|
||||
}
|
||||
|
||||
out.extend(exact);
|
||||
@@ -190,9 +193,8 @@ impl CommandPopup {
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|(item, indices)| {
|
||||
let CommandItem::Builtin(cmd) = item;
|
||||
let name = format!("/{}", cmd.command());
|
||||
let description = cmd.description().to_string();
|
||||
let name = format!("/{}", item.command());
|
||||
let description = item.description().to_string();
|
||||
GenericDisplayRow {
|
||||
name,
|
||||
name_prefix_spans: Vec::new(),
|
||||
@@ -228,7 +230,7 @@ impl CommandPopup {
|
||||
let matches = self.filtered_items();
|
||||
self.state
|
||||
.selected_idx
|
||||
.and_then(|idx| matches.get(idx).copied())
|
||||
.and_then(|idx| matches.get(idx).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +258,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn filter_includes_init_when_typing_prefix() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
// Simulate the composer line starting with '/in' so the popup filters
|
||||
// matching commands by prefix.
|
||||
popup.on_composer_text_change("/in".to_string());
|
||||
@@ -264,9 +266,7 @@ mod tests {
|
||||
// Access the filtered list via the selected command and ensure that
|
||||
// one of the matches is the new "init" command.
|
||||
let matches = popup.filtered_items();
|
||||
let has_init = matches.iter().any(|item| match item {
|
||||
CommandItem::Builtin(cmd) => cmd.command() == "init",
|
||||
});
|
||||
let has_init = matches.iter().any(|item| item.command() == "init");
|
||||
assert!(
|
||||
has_init,
|
||||
"expected '/init' to appear among filtered commands"
|
||||
@@ -275,55 +275,51 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn selecting_init_by_exact_match() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
popup.on_composer_text_change("/init".to_string());
|
||||
|
||||
// When an exact match exists, the selected command should be that
|
||||
// command by default.
|
||||
let selected = popup.selected_item();
|
||||
match selected {
|
||||
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "init"),
|
||||
Some(command) => assert_eq!(command.command(), "init"),
|
||||
None => panic!("expected a selected command for exact match"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_is_first_suggestion_for_mo() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
popup.on_composer_text_change("/mo".to_string());
|
||||
let matches = popup.filtered_items();
|
||||
match matches.first() {
|
||||
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "model"),
|
||||
Some(command) => assert_eq!(command.command(), "model"),
|
||||
None => panic!("expected at least one match for '/mo'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_commands_keep_presentation_order_for_prefix() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
popup.on_composer_text_change("/m".to_string());
|
||||
|
||||
let cmds: Vec<&str> = popup
|
||||
.filtered_items()
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
CommandItem::Builtin(cmd) => cmd.command(),
|
||||
})
|
||||
.map(|item| item.command())
|
||||
.collect();
|
||||
assert_eq!(cmds, vec!["model", "memories", "mention", "mcp"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_filter_limits_matches_for_ac() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
popup.on_composer_text_change("/ac".to_string());
|
||||
|
||||
let cmds: Vec<&str> = popup
|
||||
.filtered_items()
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
CommandItem::Builtin(cmd) => cmd.command(),
|
||||
})
|
||||
.map(|item| item.command())
|
||||
.collect();
|
||||
assert!(
|
||||
!cmds.contains(&"compact"),
|
||||
@@ -333,7 +329,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn quit_hidden_in_empty_filter_but_shown_for_prefix() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
popup.on_composer_text_change("/".to_string());
|
||||
let items = popup.filtered_items();
|
||||
assert!(!items.contains(&CommandItem::Builtin(SlashCommand::Quit)));
|
||||
@@ -345,15 +341,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn collab_command_hidden_when_collaboration_modes_disabled() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
popup.on_composer_text_change("/".to_string());
|
||||
|
||||
let cmds: Vec<&str> = popup
|
||||
.filtered_items()
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
CommandItem::Builtin(cmd) => cmd.command(),
|
||||
})
|
||||
.map(|item| item.command())
|
||||
.collect();
|
||||
assert!(
|
||||
!cmds.contains(&"collab"),
|
||||
@@ -367,70 +361,77 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn collab_command_visible_when_collaboration_modes_enabled() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
});
|
||||
let mut popup = CommandPopup::new(
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
},
|
||||
&[],
|
||||
);
|
||||
popup.on_composer_text_change("/collab".to_string());
|
||||
|
||||
match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "collab"),
|
||||
Some(command) => assert_eq!(command.command(), "collab"),
|
||||
other => panic!("expected collab to be selected for exact match, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_command_visible_when_collaboration_modes_enabled() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
});
|
||||
let mut popup = CommandPopup::new(
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
},
|
||||
&[],
|
||||
);
|
||||
popup.on_composer_text_change("/plan".to_string());
|
||||
|
||||
match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "plan"),
|
||||
Some(command) => assert_eq!(command.command(), "plan"),
|
||||
other => panic!("expected plan to be selected for exact match, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn personality_command_hidden_when_disabled() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: false,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
});
|
||||
let mut popup = CommandPopup::new(
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: false,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
},
|
||||
&[],
|
||||
);
|
||||
popup.on_composer_text_change("/pers".to_string());
|
||||
|
||||
let cmds: Vec<&str> = popup
|
||||
.filtered_items()
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
CommandItem::Builtin(cmd) => cmd.command(),
|
||||
})
|
||||
.map(|item| item.command())
|
||||
.collect();
|
||||
assert!(
|
||||
!cmds.contains(&"personality"),
|
||||
@@ -440,48 +441,52 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn personality_command_visible_when_enabled() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
});
|
||||
let mut popup = CommandPopup::new(
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: true,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: false,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
},
|
||||
&[],
|
||||
);
|
||||
popup.on_composer_text_change("/personality".to_string());
|
||||
|
||||
match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "personality"),
|
||||
Some(command) => assert_eq!(command.command(), "personality"),
|
||||
other => panic!("expected personality to be selected for exact match, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_command_hidden_when_audio_device_selection_is_disabled() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags {
|
||||
collaboration_modes_enabled: false,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: true,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
});
|
||||
let mut popup = CommandPopup::new(
|
||||
CommandPopupFlags {
|
||||
collaboration_modes_enabled: false,
|
||||
connectors_enabled: false,
|
||||
plugins_command_enabled: false,
|
||||
fast_command_enabled: false,
|
||||
goal_command_enabled: false,
|
||||
personality_command_enabled: true,
|
||||
realtime_conversation_enabled: true,
|
||||
audio_device_selection_enabled: false,
|
||||
windows_degraded_sandbox_active: false,
|
||||
side_conversation_active: false,
|
||||
},
|
||||
&[],
|
||||
);
|
||||
popup.on_composer_text_change("/aud".to_string());
|
||||
|
||||
let cmds: Vec<&str> = popup
|
||||
.filtered_items()
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
CommandItem::Builtin(cmd) => cmd.command(),
|
||||
})
|
||||
.map(|item| item.command())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
@@ -492,13 +497,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn debug_commands_are_hidden_from_popup() {
|
||||
let popup = CommandPopup::new(CommandPopupFlags::default());
|
||||
let popup = CommandPopup::new(CommandPopupFlags::default(), &[]);
|
||||
let cmds: Vec<&str> = popup
|
||||
.filtered_items()
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
CommandItem::Builtin(cmd) => cmd.command(),
|
||||
})
|
||||
.map(|item| item.command())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -137,6 +137,7 @@ mod textarea;
|
||||
mod unified_exec_footer;
|
||||
pub(crate) use feedback_view::FeedbackNoteView;
|
||||
pub(crate) use selection_tabs::SelectionTab;
|
||||
use slash_commands::ServiceTierCommand;
|
||||
|
||||
/// How long the "press again to quit" hint stays visible.
|
||||
///
|
||||
@@ -386,6 +387,11 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn set_service_tier_commands(&mut self, commands: Vec<ServiceTierCommand>) {
|
||||
self.composer.set_service_tier_commands(commands);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub fn set_goal_command_enabled(&mut self, enabled: bool) {
|
||||
self.composer.set_goal_command_enabled(enabled);
|
||||
self.request_redraw();
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
//! Shared helpers for filtering and matching built-in slash commands.
|
||||
//! Shared helpers for filtering and matching slash commands.
|
||||
//!
|
||||
//! The same sandbox- and feature-gating rules are used by both the composer
|
||||
//! and the command popup. Centralizing them here keeps those call sites small
|
||||
//! and ensures they stay in sync.
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_utils_fuzzy_match::fuzzy_match;
|
||||
|
||||
use crate::slash_command::SlashCommand;
|
||||
@@ -24,6 +28,75 @@ pub(crate) struct BuiltinCommandFlags {
|
||||
pub(crate) side_conversation_active: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ServiceTierCommand {
|
||||
pub(crate) service_tier: ServiceTier,
|
||||
pub(crate) command: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: String,
|
||||
}
|
||||
|
||||
impl ServiceTierCommand {
|
||||
fn from_model_service_tier(service_tier: &ModelServiceTier, command: String) -> Self {
|
||||
Self {
|
||||
service_tier: service_tier.id.clone(),
|
||||
command,
|
||||
name: service_tier.name.clone(),
|
||||
description: service_tier.description.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SlashCommandAction {
|
||||
Builtin(SlashCommand),
|
||||
ServiceTier(ServiceTierCommand),
|
||||
}
|
||||
|
||||
impl SlashCommandAction {
|
||||
pub(crate) fn command(&self) -> &str {
|
||||
match self {
|
||||
Self::Builtin(command) => command.command(),
|
||||
Self::ServiceTier(command) => command.command.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn description(&self) -> &str {
|
||||
match self {
|
||||
Self::Builtin(command) => command.description(),
|
||||
Self::ServiceTier(command) => command.description.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn supports_inline_args(&self) -> bool {
|
||||
match self {
|
||||
Self::Builtin(command) => command.supports_inline_args(),
|
||||
Self::ServiceTier(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn available_in_side_conversation(&self) -> bool {
|
||||
match self {
|
||||
Self::Builtin(command) => command.available_in_side_conversation(),
|
||||
Self::ServiceTier(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn available_during_task(&self) -> bool {
|
||||
match self {
|
||||
Self::Builtin(command) => command.available_during_task(),
|
||||
Self::ServiceTier(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_builtin(&self) -> Option<SlashCommand> {
|
||||
match self {
|
||||
Self::Builtin(command) => Some(*command),
|
||||
Self::ServiceTier(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the built-ins that should be visible/usable for the current input.
|
||||
pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static str, SlashCommand)> {
|
||||
built_in_slash_commands()
|
||||
@@ -35,7 +108,6 @@ pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static st
|
||||
})
|
||||
.filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Apps)
|
||||
.filter(|(_, cmd)| flags.plugins_command_enabled || *cmd != SlashCommand::Plugins)
|
||||
.filter(|(_, cmd)| flags.fast_command_enabled || *cmd != SlashCommand::Fast)
|
||||
.filter(|(_, cmd)| flags.goal_command_enabled || *cmd != SlashCommand::Goal)
|
||||
.filter(|(_, cmd)| flags.personality_command_enabled || *cmd != SlashCommand::Personality)
|
||||
.filter(|(_, cmd)| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime)
|
||||
@@ -44,31 +116,131 @@ pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static st
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find a single built-in command by exact name, after applying feature gating.
|
||||
fn visible_service_tier_commands(
|
||||
flags: BuiltinCommandFlags,
|
||||
service_tier_commands: &[ServiceTierCommand],
|
||||
) -> Vec<ServiceTierCommand> {
|
||||
if !flags.fast_command_enabled || flags.side_conversation_active {
|
||||
return Vec::new();
|
||||
}
|
||||
service_tier_commands.to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn commands_for_input(
|
||||
flags: BuiltinCommandFlags,
|
||||
service_tier_commands: &[ServiceTierCommand],
|
||||
) -> Vec<SlashCommandAction> {
|
||||
let visible_service_tier_commands = visible_service_tier_commands(flags, service_tier_commands);
|
||||
let mut commands = Vec::new();
|
||||
for (_, command) in builtins_for_input(flags) {
|
||||
commands.push(SlashCommandAction::Builtin(command));
|
||||
if command == SlashCommand::Model {
|
||||
commands.extend(
|
||||
visible_service_tier_commands
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(SlashCommandAction::ServiceTier),
|
||||
);
|
||||
}
|
||||
}
|
||||
commands
|
||||
}
|
||||
|
||||
/// Find a single slash command by exact name, after applying feature gating.
|
||||
///
|
||||
/// Side-conversation gating is intentionally enforced by dispatch rather than exact lookup so a
|
||||
/// typed command can produce a side-specific unavailable message while the popup still hides it.
|
||||
pub(crate) fn find_builtin_command(name: &str, flags: BuiltinCommandFlags) -> Option<SlashCommand> {
|
||||
let cmd = SlashCommand::from_str(name).ok()?;
|
||||
builtins_for_input(BuiltinCommandFlags {
|
||||
pub(crate) fn find_command(
|
||||
name: &str,
|
||||
flags: BuiltinCommandFlags,
|
||||
service_tier_commands: &[ServiceTierCommand],
|
||||
) -> Option<SlashCommandAction> {
|
||||
let lookup_flags = BuiltinCommandFlags {
|
||||
side_conversation_active: false,
|
||||
..flags
|
||||
})
|
||||
.into_iter()
|
||||
.any(|(_, visible_cmd)| visible_cmd == cmd)
|
||||
.then_some(cmd)
|
||||
};
|
||||
if let Some(command) = SlashCommand::from_str(name).ok().filter(|command| {
|
||||
builtins_for_input(lookup_flags)
|
||||
.into_iter()
|
||||
.any(|(_, visible_command)| visible_command == *command)
|
||||
}) {
|
||||
return Some(SlashCommandAction::Builtin(command));
|
||||
}
|
||||
visible_service_tier_commands(lookup_flags, service_tier_commands)
|
||||
.into_iter()
|
||||
.find(|command| command.command == name)
|
||||
.map(SlashCommandAction::ServiceTier)
|
||||
}
|
||||
|
||||
/// Whether any visible built-in fuzzily matches the provided prefix.
|
||||
pub(crate) fn has_builtin_prefix(name: &str, flags: BuiltinCommandFlags) -> bool {
|
||||
builtins_for_input(flags)
|
||||
/// Whether any visible slash command fuzzily matches the provided prefix.
|
||||
pub(crate) fn has_command_prefix(
|
||||
name: &str,
|
||||
flags: BuiltinCommandFlags,
|
||||
service_tier_commands: &[ServiceTierCommand],
|
||||
) -> bool {
|
||||
commands_for_input(flags, service_tier_commands)
|
||||
.into_iter()
|
||||
.any(|(command_name, _)| fuzzy_match(command_name, name).is_some())
|
||||
.any(|command| fuzzy_match(command.command(), name).is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn service_tier_commands_for_model(model: &ModelPreset) -> Vec<ServiceTierCommand> {
|
||||
let mut reserved_names = HashSet::new();
|
||||
let mut commands = Vec::new();
|
||||
for service_tier in &model.service_tiers {
|
||||
let Some(command) = command_token_for_service_tier(service_tier, &reserved_names) else {
|
||||
continue;
|
||||
};
|
||||
reserved_names.insert(command.clone());
|
||||
commands.push(ServiceTierCommand::from_model_service_tier(
|
||||
service_tier,
|
||||
command,
|
||||
));
|
||||
}
|
||||
commands
|
||||
}
|
||||
|
||||
fn command_token_for_service_tier(
|
||||
service_tier: &ModelServiceTier,
|
||||
reserved_names: &HashSet<String>,
|
||||
) -> Option<String> {
|
||||
[
|
||||
normalize_service_tier_command_token(service_tier.name.as_str()),
|
||||
Some(service_tier.id.to_string()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|candidate| !command_token_conflicts(candidate, reserved_names))
|
||||
}
|
||||
|
||||
fn command_token_conflicts(candidate: &str, reserved_names: &HashSet<String>) -> bool {
|
||||
candidate.is_empty()
|
||||
|| reserved_names.contains(candidate)
|
||||
|| SlashCommand::from_str(candidate).is_ok()
|
||||
}
|
||||
|
||||
fn normalize_service_tier_command_token(name: &str) -> Option<String> {
|
||||
let mut normalized = String::new();
|
||||
let mut pending_dash = false;
|
||||
|
||||
for ch in name.chars() {
|
||||
if ch.is_ascii_alphanumeric() {
|
||||
if pending_dash && !normalized.is_empty() {
|
||||
normalized.push('-');
|
||||
}
|
||||
normalized.push(ch.to_ascii_lowercase());
|
||||
pending_dash = false;
|
||||
} else if !normalized.is_empty() {
|
||||
pending_dash = true;
|
||||
}
|
||||
}
|
||||
|
||||
(!normalized.is_empty()).then_some(normalized)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn all_enabled_flags() -> BuiltinCommandFlags {
|
||||
@@ -88,53 +260,62 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn debug_command_still_resolves_for_dispatch() {
|
||||
let cmd = find_builtin_command("debug-config", all_enabled_flags());
|
||||
assert_eq!(cmd, Some(SlashCommand::DebugConfig));
|
||||
let cmd = find_command("debug-config", all_enabled_flags(), &[]);
|
||||
assert_eq!(
|
||||
cmd,
|
||||
Some(SlashCommandAction::Builtin(SlashCommand::DebugConfig))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_command_resolves_for_dispatch() {
|
||||
assert_eq!(
|
||||
find_builtin_command("clear", all_enabled_flags()),
|
||||
Some(SlashCommand::Clear)
|
||||
find_command("clear", all_enabled_flags(), &[]),
|
||||
Some(SlashCommandAction::Builtin(SlashCommand::Clear))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_command_resolves_for_dispatch() {
|
||||
assert_eq!(
|
||||
find_builtin_command("stop", all_enabled_flags()),
|
||||
Some(SlashCommand::Stop)
|
||||
find_command("stop", all_enabled_flags(), &[]),
|
||||
Some(SlashCommandAction::Builtin(SlashCommand::Stop))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_command_alias_resolves_for_dispatch() {
|
||||
assert_eq!(
|
||||
find_builtin_command("clean", all_enabled_flags()),
|
||||
Some(SlashCommand::Stop)
|
||||
find_command("clean", all_enabled_flags(), &[]),
|
||||
Some(SlashCommandAction::Builtin(SlashCommand::Stop))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_command_is_hidden_when_disabled() {
|
||||
fn service_tier_command_is_hidden_when_disabled() {
|
||||
let service_tier_commands = vec![ServiceTierCommand {
|
||||
service_tier: ServiceTier::priority(),
|
||||
command: "fast".to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Fast tier".to_string(),
|
||||
}];
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.fast_command_enabled = false;
|
||||
assert_eq!(find_builtin_command("fast", flags), None);
|
||||
assert_eq!(find_command("fast", flags, &service_tier_commands), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goal_command_is_hidden_when_disabled() {
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.goal_command_enabled = false;
|
||||
assert_eq!(find_builtin_command("goal", flags), None);
|
||||
assert_eq!(find_command("goal", flags, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_command_is_hidden_when_realtime_is_disabled() {
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.realtime_conversation_enabled = false;
|
||||
assert_eq!(find_builtin_command("realtime", flags), None);
|
||||
assert_eq!(find_command("realtime", flags, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -142,33 +323,36 @@ mod tests {
|
||||
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);
|
||||
assert_eq!(find_command("settings", flags, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_command_is_hidden_when_audio_device_selection_is_disabled() {
|
||||
let mut flags = all_enabled_flags();
|
||||
flags.audio_device_selection_enabled = false;
|
||||
assert_eq!(find_builtin_command("settings", flags), None);
|
||||
assert_eq!(find_command("settings", flags, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_conversation_hides_commands_without_side_flag() {
|
||||
let commands = builtins_for_input(BuiltinCommandFlags {
|
||||
side_conversation_active: true,
|
||||
..all_enabled_flags()
|
||||
})
|
||||
let commands = commands_for_input(
|
||||
BuiltinCommandFlags {
|
||||
side_conversation_active: true,
|
||||
..all_enabled_flags()
|
||||
},
|
||||
&[],
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(_, command)| command)
|
||||
.map(|command| command.command().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
commands,
|
||||
vec![
|
||||
SlashCommand::Copy,
|
||||
SlashCommand::Diff,
|
||||
SlashCommand::Mention,
|
||||
SlashCommand::Status,
|
||||
"copy".to_string(),
|
||||
"diff".to_string(),
|
||||
"mention".to_string(),
|
||||
"status".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -176,14 +360,66 @@ mod tests {
|
||||
#[test]
|
||||
fn side_conversation_exact_lookup_still_resolves_hidden_commands_for_dispatch_error() {
|
||||
assert_eq!(
|
||||
find_builtin_command(
|
||||
find_command(
|
||||
"review",
|
||||
BuiltinCommandFlags {
|
||||
side_conversation_active: true,
|
||||
..all_enabled_flags()
|
||||
},
|
||||
&[],
|
||||
),
|
||||
Some(SlashCommand::Review)
|
||||
Some(SlashCommandAction::Builtin(SlashCommand::Review))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_tier_command_uses_name_then_id_on_collision() {
|
||||
let model = ModelPreset {
|
||||
id: "test".to_string(),
|
||||
model: "test".to_string(),
|
||||
display_name: "Test".to_string(),
|
||||
description: String::new(),
|
||||
default_reasoning_effort: codex_protocol::openai_models::ReasoningEffort::Medium,
|
||||
supported_reasoning_efforts: Vec::new(),
|
||||
supports_personality: false,
|
||||
service_tiers: vec![
|
||||
ModelServiceTier {
|
||||
id: ServiceTier::priority(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Fast tier".to_string(),
|
||||
},
|
||||
ModelServiceTier {
|
||||
id: ServiceTier::ultrafast(),
|
||||
name: "Model".to_string(),
|
||||
description: "Ultrafast tier".to_string(),
|
||||
},
|
||||
],
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker: true,
|
||||
availability_nux: None,
|
||||
supported_in_api: true,
|
||||
input_modalities: Vec::new(),
|
||||
};
|
||||
|
||||
let commands = service_tier_commands_for_model(&model);
|
||||
|
||||
assert_eq!(
|
||||
commands,
|
||||
vec![
|
||||
ServiceTierCommand {
|
||||
service_tier: ServiceTier::priority(),
|
||||
command: "fast".to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Fast tier".to_string(),
|
||||
},
|
||||
ServiceTierCommand {
|
||||
service_tier: ServiceTier::ultrafast(),
|
||||
command: "ultrafast".to_string(),
|
||||
name: "Model".to_string(),
|
||||
description: "Ultrafast tier".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ pub(crate) enum StatusLineItem {
|
||||
/// Full session UUID.
|
||||
SessionId,
|
||||
|
||||
/// Whether Fast mode is currently active.
|
||||
/// Current service-tier selection.
|
||||
FastMode,
|
||||
|
||||
/// Current thread title (if set by user).
|
||||
@@ -148,7 +148,7 @@ impl StatusLineItem {
|
||||
StatusLineItem::SessionId => {
|
||||
"Current session identifier (omitted until session starts)"
|
||||
}
|
||||
StatusLineItem::FastMode => "Whether Fast mode is currently active",
|
||||
StatusLineItem::FastMode => "Current service tier (or default when unset)",
|
||||
StatusLineItem::ThreadTitle => "Current thread title (omitted when unavailable)",
|
||||
StatusLineItem::TaskProgress => {
|
||||
"Latest task progress from update_plan (omitted until available)"
|
||||
|
||||
@@ -47,7 +47,7 @@ impl StatusSurfacePreviewItem {
|
||||
StatusSurfacePreviewItem::TotalInputTokens => "0 in",
|
||||
StatusSurfacePreviewItem::TotalOutputTokens => "0 out",
|
||||
StatusSurfacePreviewItem::SessionId => "550e8400-e29b-41d4",
|
||||
StatusSurfacePreviewItem::FastMode => "Fast on",
|
||||
StatusSurfacePreviewItem::FastMode => "Tier Fast",
|
||||
StatusSurfacePreviewItem::Model => "gpt-5.2-codex",
|
||||
StatusSurfacePreviewItem::ModelWithReasoning => "gpt-5.2-codex medium",
|
||||
StatusSurfacePreviewItem::TaskProgress => "Tasks 0/0",
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(crate) enum TerminalTitleItem {
|
||||
TotalOutputTokens,
|
||||
/// Full session UUID.
|
||||
SessionId,
|
||||
/// Whether Fast mode is currently active.
|
||||
/// Current service-tier selection.
|
||||
FastMode,
|
||||
/// Current model name.
|
||||
#[strum(to_string = "model", serialize = "model-name")]
|
||||
@@ -117,7 +117,7 @@ impl TerminalTitleItem {
|
||||
TerminalTitleItem::SessionId => {
|
||||
"Current session identifier (omitted until session starts)"
|
||||
}
|
||||
TerminalTitleItem::FastMode => "Whether Fast mode is currently active",
|
||||
TerminalTitleItem::FastMode => "Current service tier (or default when unset)",
|
||||
TerminalTitleItem::Model => "Current model name",
|
||||
TerminalTitleItem::ModelWithReasoning => "Current model name with reasoning level",
|
||||
TerminalTitleItem::TaskProgress => {
|
||||
|
||||
@@ -57,6 +57,7 @@ use crate::bottom_pane::StatusSurfacePreviewData;
|
||||
use crate::bottom_pane::StatusSurfacePreviewItem;
|
||||
use crate::bottom_pane::TerminalTitleItem;
|
||||
use crate::bottom_pane::TerminalTitleSetupView;
|
||||
use crate::bottom_pane::slash_commands::ServiceTierCommand;
|
||||
use crate::legacy_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::Constrained;
|
||||
@@ -2364,7 +2365,7 @@ impl ChatWidget {
|
||||
self.current_rollout_path = event.rollout_path.clone();
|
||||
self.current_cwd = Some(event.cwd.to_path_buf());
|
||||
self.config.cwd = event.cwd.clone();
|
||||
self.effective_service_tier = event.service_tier;
|
||||
self.effective_service_tier = event.service_tier.clone();
|
||||
if let Err(err) = self
|
||||
.config
|
||||
.permissions
|
||||
@@ -2406,6 +2407,7 @@ impl ChatWidget {
|
||||
self.refresh_model_display();
|
||||
self.refresh_status_surfaces();
|
||||
self.sync_fast_command_enabled();
|
||||
self.sync_service_tier_commands();
|
||||
self.sync_personality_command_enabled();
|
||||
self.sync_plugins_command_enabled();
|
||||
self.sync_goal_command_enabled();
|
||||
@@ -2413,7 +2415,7 @@ impl ChatWidget {
|
||||
if display == SessionConfiguredDisplay::Normal {
|
||||
let startup_tooltip_override = self.startup_tooltip_override.take();
|
||||
let show_fast_status =
|
||||
self.should_show_fast_status(&model_for_header, event.service_tier);
|
||||
self.should_show_fast_status(&model_for_header, event.service_tier.clone());
|
||||
#[cfg(test)]
|
||||
let initial_messages = event.initial_messages.clone();
|
||||
let session_info_cell = history_cell::new_session_info(
|
||||
@@ -5270,7 +5272,7 @@ impl ChatWidget {
|
||||
let active_cell = Some(Self::placeholder_session_header_cell(&config));
|
||||
|
||||
let current_cwd = Some(config.cwd.to_path_buf());
|
||||
let effective_service_tier = config.service_tier;
|
||||
let effective_service_tier = config.service_tier.clone();
|
||||
let current_terminal_info = terminal_info();
|
||||
let runtime_keymap = RuntimeKeymap::from_config(&config.tui_keymap).ok();
|
||||
let default_keymap = RuntimeKeymap::defaults();
|
||||
@@ -5451,6 +5453,7 @@ impl ChatWidget {
|
||||
.bottom_pane
|
||||
.set_collaboration_modes_enabled(/*enabled*/ true);
|
||||
widget.sync_fast_command_enabled();
|
||||
widget.sync_service_tier_commands();
|
||||
widget.sync_personality_command_enabled();
|
||||
widget.sync_plugins_command_enabled();
|
||||
widget.sync_goal_command_enabled();
|
||||
@@ -6236,7 +6239,7 @@ impl ChatWidget {
|
||||
.personality
|
||||
.filter(|_| self.config.features.enabled(Feature::Personality))
|
||||
.filter(|_| self.current_model_supports_personality());
|
||||
let service_tier = match self.config.service_tier {
|
||||
let service_tier = match self.config.service_tier.clone() {
|
||||
Some(service_tier) => Some(Some(service_tier)),
|
||||
None if self.config.notices.fast_default_opt_out == Some(true) => Some(None),
|
||||
None => None,
|
||||
@@ -10358,18 +10361,18 @@ impl ChatWidget {
|
||||
self.config.personality = Some(personality);
|
||||
}
|
||||
|
||||
/// Set Fast mode in the widget's config copy.
|
||||
/// Set the selected service tier in the widget's config copy.
|
||||
pub(crate) fn set_service_tier(&mut self, service_tier: Option<ServiceTier>) {
|
||||
self.config.service_tier = service_tier;
|
||||
self.config.service_tier = service_tier.clone();
|
||||
self.effective_service_tier = service_tier;
|
||||
}
|
||||
|
||||
pub(crate) fn current_service_tier(&self) -> Option<ServiceTier> {
|
||||
self.effective_service_tier
|
||||
self.effective_service_tier.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn configured_service_tier(&self) -> Option<ServiceTier> {
|
||||
self.config.service_tier
|
||||
self.config.service_tier.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn fast_default_opt_out(&self) -> Option<bool> {
|
||||
@@ -10412,7 +10415,7 @@ impl ChatWidget {
|
||||
service_tier: Option<ServiceTier>,
|
||||
) -> bool {
|
||||
self.model_supports_fast_mode(model)
|
||||
&& matches!(service_tier, Some(ServiceTier::Fast))
|
||||
&& service_tier.as_ref().is_some_and(ServiceTier::is_priority)
|
||||
&& self.has_chatgpt_account
|
||||
}
|
||||
|
||||
@@ -10505,6 +10508,11 @@ impl ChatWidget {
|
||||
.set_fast_command_enabled(self.fast_mode_enabled());
|
||||
}
|
||||
|
||||
fn sync_service_tier_commands(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_service_tier_commands(self.available_service_tier_commands());
|
||||
}
|
||||
|
||||
fn sync_personality_command_enabled(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_personality_command_enabled(self.config.features.enabled(Feature::Personality));
|
||||
@@ -10520,30 +10528,55 @@ impl ChatWidget {
|
||||
.set_goal_command_enabled(self.config.features.enabled(Feature::Goals));
|
||||
}
|
||||
|
||||
fn current_model_supports_personality(&self) -> bool {
|
||||
let model = self.current_model();
|
||||
fn model_preset(&self, model: &str) -> Option<ModelPreset> {
|
||||
self.model_catalog
|
||||
.try_list_models()
|
||||
.ok()
|
||||
.and_then(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.find(|preset| preset.model == model)
|
||||
.map(|preset| preset.supports_personality)
|
||||
.and_then(|models| models.into_iter().find(|preset| preset.model == model))
|
||||
}
|
||||
|
||||
fn available_service_tier_commands(&self) -> Vec<ServiceTierCommand> {
|
||||
self.model_preset(self.current_model())
|
||||
.map(|model| {
|
||||
crate::bottom_pane::slash_commands::service_tier_commands_for_model(&model)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn service_tier_display_name(
|
||||
&self,
|
||||
model: &str,
|
||||
service_tier: &ServiceTier,
|
||||
) -> String {
|
||||
self.model_preset(model)
|
||||
.and_then(|preset| {
|
||||
preset
|
||||
.service_tier(service_tier)
|
||||
.map(|tier| tier.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| service_tier.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn current_service_tier_name(&self) -> Option<String> {
|
||||
self.current_service_tier()
|
||||
.map(|tier| self.service_tier_display_name(self.current_model(), &tier))
|
||||
}
|
||||
|
||||
pub(crate) fn current_service_tier_status_label(&self) -> String {
|
||||
self.current_service_tier_name()
|
||||
.map(|name| format!("Tier {name}"))
|
||||
.unwrap_or_else(|| "Tier default".to_string())
|
||||
}
|
||||
|
||||
fn current_model_supports_personality(&self) -> bool {
|
||||
self.model_preset(self.current_model())
|
||||
.map(|preset| preset.supports_personality)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn model_supports_fast_mode(&self, model: &str) -> bool {
|
||||
self.model_catalog
|
||||
.try_list_models()
|
||||
.ok()
|
||||
.and_then(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.find(|preset| preset.model == model)
|
||||
.map(|preset| preset.supports_fast_mode())
|
||||
})
|
||||
self.model_preset(model)
|
||||
.map(|preset| preset.supports_service_tier(&ServiceTier::priority()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -10552,16 +10585,8 @@ impl ChatWidget {
|
||||
/// We intentionally default to `true` when model metadata cannot be read so transient catalog
|
||||
/// failures do not hard-block user input in the UI.
|
||||
fn current_model_supports_images(&self) -> bool {
|
||||
let model = self.current_model();
|
||||
self.model_catalog
|
||||
.try_list_models()
|
||||
.ok()
|
||||
.and_then(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.find(|preset| preset.model == model)
|
||||
.map(|preset| preset.input_modalities.contains(&InputModality::Image))
|
||||
})
|
||||
self.model_preset(self.current_model())
|
||||
.map(|preset| preset.input_modalities.contains(&InputModality::Image))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
@@ -10699,6 +10724,7 @@ impl ChatWidget {
|
||||
/// (`refresh_status_line`).
|
||||
fn refresh_model_dependent_surfaces(&mut self) {
|
||||
self.refresh_model_display();
|
||||
self.sync_service_tier_commands();
|
||||
self.refresh_status_line();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use super::*;
|
||||
use crate::app_event::ThreadGoalSetMode;
|
||||
use crate::bottom_pane::prompt_args::parse_slash_name;
|
||||
use crate::bottom_pane::slash_commands;
|
||||
use crate::bottom_pane::slash_commands::SlashCommandAction;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SlashCommandDispatchSource {
|
||||
@@ -38,9 +39,10 @@ impl ChatWidget {
|
||||
/// The composer stages history before returning `InputResult::Command`; this wrapper commits
|
||||
/// that staged entry after dispatch so slash-command recall follows the same "submitted input"
|
||||
/// rule as normal text.
|
||||
pub(super) fn handle_slash_command_dispatch(&mut self, cmd: SlashCommand) {
|
||||
pub(super) fn handle_slash_command_dispatch(&mut self, cmd: SlashCommandAction) {
|
||||
let is_goal = cmd.as_builtin() == Some(SlashCommand::Goal);
|
||||
self.dispatch_command(cmd);
|
||||
if cmd == SlashCommand::Goal {
|
||||
if is_goal {
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
self.bottom_pane.record_pending_slash_command_history();
|
||||
@@ -53,7 +55,7 @@ impl ChatWidget {
|
||||
/// the only input-result entry point avoids double-recording commands with inline args.
|
||||
pub(super) fn handle_slash_command_with_args_dispatch(
|
||||
&mut self,
|
||||
cmd: SlashCommand,
|
||||
cmd: SlashCommandAction,
|
||||
args: String,
|
||||
text_elements: Vec<TextElement>,
|
||||
) {
|
||||
@@ -103,11 +105,11 @@ impl ChatWidget {
|
||||
self.request_side_conversation(parent_thread_id, /*user_message*/ None);
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_command(&mut self, cmd: SlashCommand) {
|
||||
if !self.ensure_slash_command_allowed_in_side_conversation(cmd) {
|
||||
pub(super) fn dispatch_command(&mut self, cmd: SlashCommandAction) {
|
||||
if !self.ensure_slash_command_allowed_in_side_conversation(&cmd) {
|
||||
return;
|
||||
}
|
||||
if !self.ensure_side_command_allowed_outside_review(cmd) {
|
||||
if !self.ensure_side_command_allowed_outside_review(&cmd) {
|
||||
return;
|
||||
}
|
||||
if !cmd.available_during_task() && self.bottom_pane.is_task_running() {
|
||||
@@ -122,32 +124,40 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
match cmd {
|
||||
SlashCommand::Feedback => {
|
||||
SlashCommandAction::ServiceTier(command) => {
|
||||
let next_tier =
|
||||
if self.current_service_tier().as_ref() == Some(&command.service_tier) {
|
||||
None
|
||||
} else {
|
||||
Some(command.service_tier)
|
||||
};
|
||||
self.set_service_tier_selection(next_tier);
|
||||
}
|
||||
SlashCommandAction::Builtin(SlashCommand::Feedback) => {
|
||||
if !self.config.feedback_enabled {
|
||||
let params = crate::bottom_pane::feedback_disabled_params();
|
||||
self.bottom_pane.show_selection_view(params);
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
// Step 1: pick a category (UI built in feedback_view)
|
||||
let params =
|
||||
crate::bottom_pane::feedback_selection_params(self.app_event_tx.clone());
|
||||
self.bottom_pane.show_selection_view(params);
|
||||
self.request_redraw();
|
||||
}
|
||||
SlashCommand::New => {
|
||||
SlashCommandAction::Builtin(SlashCommand::New) => {
|
||||
self.app_event_tx.send(AppEvent::NewSession);
|
||||
}
|
||||
SlashCommand::Clear => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Clear) => {
|
||||
self.app_event_tx.send(AppEvent::ClearUi);
|
||||
}
|
||||
SlashCommand::Resume => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Resume) => {
|
||||
self.app_event_tx.send(AppEvent::OpenResumePicker);
|
||||
}
|
||||
SlashCommand::Fork => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Fork) => {
|
||||
self.app_event_tx.send(AppEvent::ForkCurrentSession);
|
||||
}
|
||||
SlashCommand::Init => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Init) => {
|
||||
let init_target = self.config.cwd.join(DEFAULT_AGENTS_MD_FILENAME);
|
||||
if init_target.exists() {
|
||||
let message = format!(
|
||||
@@ -159,33 +169,25 @@ impl ChatWidget {
|
||||
const INIT_PROMPT: &str = include_str!("../../prompt_for_init_command.md");
|
||||
self.submit_user_message(INIT_PROMPT.to_string().into());
|
||||
}
|
||||
SlashCommand::Compact => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Compact) => {
|
||||
self.clear_token_usage();
|
||||
if !self.bottom_pane.is_task_running() {
|
||||
self.bottom_pane.set_task_running(/*running*/ true);
|
||||
}
|
||||
self.app_event_tx.compact();
|
||||
}
|
||||
SlashCommand::Review => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Review) => {
|
||||
self.open_review_popup();
|
||||
}
|
||||
SlashCommand::Rename => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Rename) => {
|
||||
self.session_telemetry
|
||||
.counter("codex.thread.rename", /*inc*/ 1, &[]);
|
||||
self.show_rename_prompt();
|
||||
}
|
||||
SlashCommand::Model => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Model) => {
|
||||
self.open_model_popup();
|
||||
}
|
||||
SlashCommand::Fast => {
|
||||
let next_tier = if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) {
|
||||
None
|
||||
} else {
|
||||
Some(ServiceTier::Fast)
|
||||
};
|
||||
self.set_service_tier_selection(next_tier);
|
||||
}
|
||||
SlashCommand::Realtime => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Realtime) => {
|
||||
if !self.realtime_conversation_enabled() {
|
||||
return;
|
||||
}
|
||||
@@ -195,19 +197,19 @@ impl ChatWidget {
|
||||
self.start_realtime_conversation();
|
||||
}
|
||||
}
|
||||
SlashCommand::Settings => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Settings) => {
|
||||
if !self.realtime_audio_device_selection_enabled() {
|
||||
return;
|
||||
}
|
||||
self.open_realtime_audio_popup();
|
||||
}
|
||||
SlashCommand::Personality => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Personality) => {
|
||||
self.open_personality_popup();
|
||||
}
|
||||
SlashCommand::Plan => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Plan) => {
|
||||
self.apply_plan_slash_command();
|
||||
}
|
||||
SlashCommand::Goal => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Goal) => {
|
||||
if !self.config.features.enabled(Feature::Goals) {
|
||||
return;
|
||||
}
|
||||
@@ -221,7 +223,7 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
SlashCommand::Collab => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Collab) => {
|
||||
if !self.collaboration_modes_enabled() {
|
||||
self.add_info_message(
|
||||
"Collaboration modes are disabled.".to_string(),
|
||||
@@ -231,22 +233,21 @@ impl ChatWidget {
|
||||
}
|
||||
self.open_collaboration_modes_popup();
|
||||
}
|
||||
SlashCommand::Side => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Side) => {
|
||||
self.request_empty_side_conversation();
|
||||
}
|
||||
SlashCommand::Agent | SlashCommand::MultiAgents => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Agent)
|
||||
| SlashCommandAction::Builtin(SlashCommand::MultiAgents) => {
|
||||
self.app_event_tx.send(AppEvent::OpenAgentPicker);
|
||||
}
|
||||
SlashCommand::Approvals => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Approvals)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Permissions) => {
|
||||
self.open_permissions_popup();
|
||||
}
|
||||
SlashCommand::Permissions => {
|
||||
self.open_permissions_popup();
|
||||
}
|
||||
SlashCommand::Keymap => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Keymap) => {
|
||||
self.open_keymap_picker();
|
||||
}
|
||||
SlashCommand::ElevateSandbox => {
|
||||
SlashCommandAction::Builtin(SlashCommand::ElevateSandbox) => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
|
||||
@@ -255,8 +256,6 @@ impl ChatWidget {
|
||||
if !windows_degraded_sandbox_enabled
|
||||
|| !crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
{
|
||||
// This command should not be visible/recognized outside degraded mode,
|
||||
// but guard anyway in case something dispatches it directly.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -264,8 +263,6 @@ impl ChatWidget {
|
||||
.into_iter()
|
||||
.find(|preset| preset.id == "auto")
|
||||
else {
|
||||
// Avoid panicking in interactive UI; treat this as a recoverable
|
||||
// internal error.
|
||||
self.add_error_message(
|
||||
"Internal error: missing the 'auto' approval preset.".to_string(),
|
||||
);
|
||||
@@ -293,36 +290,33 @@ impl ChatWidget {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = &self.session_telemetry;
|
||||
// Not supported; on non-Windows this command should never be reachable.
|
||||
}
|
||||
}
|
||||
SlashCommand::SandboxReadRoot => {
|
||||
SlashCommandAction::Builtin(SlashCommand::SandboxReadRoot) => {
|
||||
self.add_error_message(
|
||||
"Usage: /sandbox-add-read-dir <absolute-directory-path>".to_string(),
|
||||
);
|
||||
}
|
||||
SlashCommand::Experimental => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Experimental) => {
|
||||
self.open_experimental_popup();
|
||||
}
|
||||
SlashCommand::AutoReview => {
|
||||
SlashCommandAction::Builtin(SlashCommand::AutoReview) => {
|
||||
self.open_auto_review_denials_popup();
|
||||
}
|
||||
SlashCommand::Memories => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Memories) => {
|
||||
self.open_memories_popup();
|
||||
}
|
||||
SlashCommand::Quit | SlashCommand::Exit => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Quit)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Exit) => {
|
||||
self.request_quit_without_confirmation();
|
||||
}
|
||||
SlashCommand::Logout => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Logout) => {
|
||||
self.app_event_tx.send(AppEvent::Logout);
|
||||
}
|
||||
// SlashCommand::Undo => {
|
||||
// self.app_event_tx.send(AppEvent::CodexOp(Op::Undo));
|
||||
// }
|
||||
SlashCommand::Copy => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Copy) => {
|
||||
self.copy_last_agent_markdown();
|
||||
}
|
||||
SlashCommand::Diff => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Diff) => {
|
||||
self.add_diff_in_progress();
|
||||
let tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -339,13 +333,13 @@ impl ChatWidget {
|
||||
tx.send(AppEvent::DiffResult(text));
|
||||
});
|
||||
}
|
||||
SlashCommand::Mention => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Mention) => {
|
||||
self.insert_str("@");
|
||||
}
|
||||
SlashCommand::Skills => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Skills) => {
|
||||
self.open_skills_menu();
|
||||
}
|
||||
SlashCommand::Status => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Status) => {
|
||||
if self.should_prefetch_rate_limits() {
|
||||
let request_id = self.next_status_refresh_request_id;
|
||||
self.next_status_refresh_request_id =
|
||||
@@ -360,40 +354,38 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
SlashCommand::DebugConfig => {
|
||||
SlashCommandAction::Builtin(SlashCommand::DebugConfig) => {
|
||||
self.add_debug_config_output();
|
||||
}
|
||||
SlashCommand::Title => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Title) => {
|
||||
self.open_terminal_title_setup();
|
||||
}
|
||||
SlashCommand::Statusline => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Statusline) => {
|
||||
self.open_status_line_setup();
|
||||
}
|
||||
SlashCommand::Theme => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Theme) => {
|
||||
self.open_theme_picker();
|
||||
}
|
||||
SlashCommand::Ps => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Ps) => {
|
||||
self.add_ps_output();
|
||||
}
|
||||
SlashCommand::Stop => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Stop) => {
|
||||
self.clean_background_terminals();
|
||||
}
|
||||
SlashCommand::MemoryDrop => {
|
||||
SlashCommandAction::Builtin(SlashCommand::MemoryDrop)
|
||||
| SlashCommandAction::Builtin(SlashCommand::MemoryUpdate) => {
|
||||
self.add_app_server_stub_message("Memory maintenance");
|
||||
}
|
||||
SlashCommand::MemoryUpdate => {
|
||||
self.add_app_server_stub_message("Memory maintenance");
|
||||
}
|
||||
SlashCommand::Mcp => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Mcp) => {
|
||||
self.add_mcp_output(McpServerStatusDetail::ToolsAndAuthOnly);
|
||||
}
|
||||
SlashCommand::Apps => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Apps) => {
|
||||
self.add_connectors_output();
|
||||
}
|
||||
SlashCommand::Plugins => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Plugins) => {
|
||||
self.add_plugins_output();
|
||||
}
|
||||
SlashCommand::Rollout => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Rollout) => {
|
||||
if let Some(path) = self.rollout_path() {
|
||||
self.add_info_message(
|
||||
format!("Current rollout path: {}", path.display()),
|
||||
@@ -406,7 +398,7 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
SlashCommand::TestApproval => {
|
||||
SlashCommandAction::Builtin(SlashCommand::TestApproval) => {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
@@ -447,14 +439,14 @@ impl ChatWidget {
|
||||
/// path as well would make a single command appear twice during Up-arrow navigation.
|
||||
pub(super) fn dispatch_command_with_args(
|
||||
&mut self,
|
||||
cmd: SlashCommand,
|
||||
cmd: SlashCommandAction,
|
||||
args: String,
|
||||
text_elements: Vec<TextElement>,
|
||||
) {
|
||||
if !self.ensure_slash_command_allowed_in_side_conversation(cmd) {
|
||||
if !self.ensure_slash_command_allowed_in_side_conversation(&cmd) {
|
||||
return;
|
||||
}
|
||||
if !self.ensure_side_command_allowed_outside_review(cmd) {
|
||||
if !self.ensure_side_command_allowed_outside_review(&cmd) {
|
||||
return;
|
||||
}
|
||||
if !cmd.supports_inline_args() {
|
||||
@@ -535,7 +527,7 @@ impl ChatWidget {
|
||||
|
||||
fn dispatch_prepared_command_with_args(
|
||||
&mut self,
|
||||
cmd: SlashCommand,
|
||||
cmd: SlashCommandAction,
|
||||
prepared: PreparedSlashCommandArgs,
|
||||
) {
|
||||
let PreparedSlashCommandArgs {
|
||||
@@ -547,33 +539,40 @@ impl ChatWidget {
|
||||
source,
|
||||
} = prepared;
|
||||
let trimmed = args.trim();
|
||||
let is_goal_command = cmd.as_builtin() == Some(SlashCommand::Goal);
|
||||
match cmd {
|
||||
SlashCommand::Fast => {
|
||||
SlashCommandAction::ServiceTier(command) => {
|
||||
match trimmed.to_ascii_lowercase().as_str() {
|
||||
"on" => self.set_service_tier_selection(Some(ServiceTier::Fast)),
|
||||
"on" => self.set_service_tier_selection(Some(command.service_tier.clone())),
|
||||
"off" => self.set_service_tier_selection(/*service_tier*/ None),
|
||||
"status" => {
|
||||
let status =
|
||||
if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
};
|
||||
let status = if self.current_service_tier().as_ref()
|
||||
== Some(&command.service_tier)
|
||||
{
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
};
|
||||
self.add_info_message(
|
||||
format!("Fast mode is {status}."),
|
||||
format!("{} service tier is {status}.", command.name),
|
||||
/*hint*/ None,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
self.add_error_message("Usage: /fast [on|off|status]".to_string());
|
||||
self.add_error_message(format!(
|
||||
"Usage: /{} [on|off|status]",
|
||||
command.command
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
SlashCommand::Mcp => match trimmed.to_ascii_lowercase().as_str() {
|
||||
"verbose" => self.add_mcp_output(McpServerStatusDetail::Full),
|
||||
_ => self.add_error_message("Usage: /mcp [verbose]".to_string()),
|
||||
},
|
||||
SlashCommand::Rename if !trimmed.is_empty() => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Mcp) => {
|
||||
match trimmed.to_ascii_lowercase().as_str() {
|
||||
"verbose" => self.add_mcp_output(McpServerStatusDetail::Full),
|
||||
_ => self.add_error_message("Usage: /mcp [verbose]".to_string()),
|
||||
}
|
||||
}
|
||||
SlashCommandAction::Builtin(SlashCommand::Rename) if !trimmed.is_empty() => {
|
||||
if !self.ensure_thread_rename_allowed() {
|
||||
return;
|
||||
}
|
||||
@@ -585,7 +584,7 @@ impl ChatWidget {
|
||||
};
|
||||
self.app_event_tx.set_thread_name(name);
|
||||
}
|
||||
SlashCommand::Plan if !trimmed.is_empty() => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Plan) if !trimmed.is_empty() => {
|
||||
if !self.apply_plan_slash_command() {
|
||||
return;
|
||||
}
|
||||
@@ -606,7 +605,7 @@ impl ChatWidget {
|
||||
self.queue_user_message(user_message);
|
||||
}
|
||||
}
|
||||
SlashCommand::Goal if !trimmed.is_empty() => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Goal) if !trimmed.is_empty() => {
|
||||
if !self.config.features.enabled(Feature::Goals) {
|
||||
return;
|
||||
}
|
||||
@@ -687,7 +686,7 @@ impl ChatWidget {
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
}
|
||||
SlashCommand::Side if !trimmed.is_empty() => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Side) if !trimmed.is_empty() => {
|
||||
let Some(parent_thread_id) = self.thread_id else {
|
||||
self.add_error_message(
|
||||
"'/side' is unavailable before the session starts.".to_string(),
|
||||
@@ -704,23 +703,23 @@ impl ChatWidget {
|
||||
);
|
||||
self.request_side_conversation(parent_thread_id, Some(user_message));
|
||||
}
|
||||
SlashCommand::Review if !trimmed.is_empty() => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Review) if !trimmed.is_empty() => {
|
||||
self.submit_op(AppCommand::review(ReviewRequest {
|
||||
target: ReviewTarget::Custom { instructions: args },
|
||||
user_facing_hint: None,
|
||||
}));
|
||||
}
|
||||
SlashCommand::Resume if !trimmed.is_empty() => {
|
||||
SlashCommandAction::Builtin(SlashCommand::Resume) if !trimmed.is_empty() => {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::ResumeSessionByIdOrName(args));
|
||||
}
|
||||
SlashCommand::SandboxReadRoot if !trimmed.is_empty() => {
|
||||
SlashCommandAction::Builtin(SlashCommand::SandboxReadRoot) if !trimmed.is_empty() => {
|
||||
self.app_event_tx
|
||||
.send(AppEvent::BeginWindowsSandboxGrantReadRoot { path: args });
|
||||
}
|
||||
_ => self.dispatch_command(cmd),
|
||||
}
|
||||
if source == SlashCommandDispatchSource::Live && cmd != SlashCommand::Goal {
|
||||
if source == SlashCommandDispatchSource::Live && !is_goal_command {
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
}
|
||||
@@ -755,8 +754,11 @@ impl ChatWidget {
|
||||
return QueueDrain::Stop;
|
||||
}
|
||||
|
||||
let Some(cmd) = slash_commands::find_builtin_command(name, self.builtin_command_flags())
|
||||
else {
|
||||
let Some(cmd) = slash_commands::find_command(
|
||||
name,
|
||||
self.builtin_command_flags(),
|
||||
&self.available_service_tier_commands(),
|
||||
) else {
|
||||
self.add_info_message(
|
||||
format!(
|
||||
r#"Unrecognized command '/{name}'. Type "/" for a list of supported commands."#
|
||||
@@ -791,7 +793,7 @@ impl ChatWidget {
|
||||
&text_elements,
|
||||
);
|
||||
self.dispatch_prepared_command_with_args(
|
||||
cmd,
|
||||
cmd.clone(),
|
||||
PreparedSlashCommandArgs {
|
||||
args: trimmed_rest.to_string(),
|
||||
text_elements: args_elements,
|
||||
@@ -827,60 +829,60 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn queued_command_drain_result(&self, cmd: SlashCommand) -> QueueDrain {
|
||||
fn queued_command_drain_result(&self, cmd: SlashCommandAction) -> QueueDrain {
|
||||
if self.is_user_turn_pending_or_running() || !self.bottom_pane.no_modal_or_popup_active() {
|
||||
return QueueDrain::Stop;
|
||||
}
|
||||
match cmd {
|
||||
SlashCommand::Fast
|
||||
| SlashCommand::Status
|
||||
| SlashCommand::DebugConfig
|
||||
| SlashCommand::Ps
|
||||
| SlashCommand::Stop
|
||||
| SlashCommand::MemoryDrop
|
||||
| SlashCommand::MemoryUpdate
|
||||
| SlashCommand::Mcp
|
||||
| SlashCommand::Apps
|
||||
| SlashCommand::Plugins
|
||||
| SlashCommand::Rollout
|
||||
| SlashCommand::Copy
|
||||
| SlashCommand::Diff
|
||||
| SlashCommand::Rename
|
||||
| SlashCommand::TestApproval => QueueDrain::Continue,
|
||||
SlashCommand::Feedback
|
||||
| SlashCommand::New
|
||||
| SlashCommand::Clear
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Fork
|
||||
| SlashCommand::Init
|
||||
| SlashCommand::Compact
|
||||
| SlashCommand::Review
|
||||
| SlashCommand::Model
|
||||
| SlashCommand::Realtime
|
||||
| SlashCommand::Settings
|
||||
| SlashCommand::Personality
|
||||
| SlashCommand::Plan
|
||||
| SlashCommand::Goal
|
||||
| SlashCommand::Collab
|
||||
| SlashCommand::Side
|
||||
| SlashCommand::Keymap
|
||||
| SlashCommand::Agent
|
||||
| SlashCommand::MultiAgents
|
||||
| SlashCommand::Approvals
|
||||
| SlashCommand::Permissions
|
||||
| SlashCommand::ElevateSandbox
|
||||
| SlashCommand::SandboxReadRoot
|
||||
| SlashCommand::Experimental
|
||||
| SlashCommand::AutoReview
|
||||
| SlashCommand::Memories
|
||||
| SlashCommand::Quit
|
||||
| SlashCommand::Exit
|
||||
| SlashCommand::Logout
|
||||
| SlashCommand::Mention
|
||||
| SlashCommand::Skills
|
||||
| SlashCommand::Title
|
||||
| SlashCommand::Statusline
|
||||
| SlashCommand::Theme => QueueDrain::Stop,
|
||||
SlashCommandAction::ServiceTier(_)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Status)
|
||||
| SlashCommandAction::Builtin(SlashCommand::DebugConfig)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Ps)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Stop)
|
||||
| SlashCommandAction::Builtin(SlashCommand::MemoryDrop)
|
||||
| SlashCommandAction::Builtin(SlashCommand::MemoryUpdate)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Mcp)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Apps)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Plugins)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Rollout)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Copy)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Diff)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Rename)
|
||||
| SlashCommandAction::Builtin(SlashCommand::TestApproval) => QueueDrain::Continue,
|
||||
SlashCommandAction::Builtin(SlashCommand::Feedback)
|
||||
| SlashCommandAction::Builtin(SlashCommand::New)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Clear)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Resume)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Fork)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Init)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Compact)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Review)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Model)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Realtime)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Settings)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Personality)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Plan)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Goal)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Collab)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Side)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Keymap)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Agent)
|
||||
| SlashCommandAction::Builtin(SlashCommand::MultiAgents)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Approvals)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Permissions)
|
||||
| SlashCommandAction::Builtin(SlashCommand::ElevateSandbox)
|
||||
| SlashCommandAction::Builtin(SlashCommand::SandboxReadRoot)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Experimental)
|
||||
| SlashCommandAction::Builtin(SlashCommand::AutoReview)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Memories)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Quit)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Exit)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Logout)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Mention)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Skills)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Title)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Statusline)
|
||||
| SlashCommandAction::Builtin(SlashCommand::Theme) => QueueDrain::Stop,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,7 +911,10 @@ impl ChatWidget {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ensure_slash_command_allowed_in_side_conversation(&mut self, cmd: SlashCommand) -> bool {
|
||||
fn ensure_slash_command_allowed_in_side_conversation(
|
||||
&mut self,
|
||||
cmd: &SlashCommandAction,
|
||||
) -> bool {
|
||||
if !self.active_side_conversation || cmd.available_in_side_conversation() {
|
||||
return true;
|
||||
}
|
||||
@@ -921,8 +926,8 @@ impl ChatWidget {
|
||||
false
|
||||
}
|
||||
|
||||
fn ensure_side_command_allowed_outside_review(&mut self, cmd: SlashCommand) -> bool {
|
||||
if cmd != SlashCommand::Side || !self.is_review_mode {
|
||||
fn ensure_side_command_allowed_outside_review(&mut self, cmd: &SlashCommandAction) -> bool {
|
||||
if cmd.as_builtin() != Some(SlashCommand::Side) || !self.is_review_mode {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" Fast on "
|
||||
" Tier Fast "
|
||||
|
||||
@@ -559,13 +559,7 @@ impl ChatWidget {
|
||||
format_tokens_compact(self.status_line_total_usage().output_tokens)
|
||||
)),
|
||||
StatusLineItem::SessionId => self.thread_id.map(|id| id.to_string()),
|
||||
StatusLineItem::FastMode => Some(
|
||||
if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) {
|
||||
"Fast on".to_string()
|
||||
} else {
|
||||
"Fast off".to_string()
|
||||
},
|
||||
),
|
||||
StatusLineItem::FastMode => Some(self.current_service_tier_status_label()),
|
||||
StatusLineItem::ThreadTitle => self.thread_name.as_ref().and_then(|name| {
|
||||
let trimmed = name.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
@@ -679,13 +673,11 @@ impl ChatWidget {
|
||||
|
||||
fn model_with_reasoning_display_name(&self) -> String {
|
||||
let label = Self::status_line_reasoning_effort_label(self.effective_reasoning_effort());
|
||||
let fast_label =
|
||||
if self.should_show_fast_status(self.current_model(), self.current_service_tier()) {
|
||||
" fast"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{} {label}{fast_label}", self.model_display_name())
|
||||
let service_tier_label = self
|
||||
.current_service_tier_name()
|
||||
.map(|name| format!(" {}", name.to_ascii_lowercase()))
|
||||
.unwrap_or_default();
|
||||
format!("{} {label}{service_tier_label}", self.model_display_name())
|
||||
}
|
||||
|
||||
/// Computes the compact runtime status label used by word-based status items.
|
||||
|
||||
@@ -372,8 +372,12 @@ pub(crate) fn set_chatgpt_auth(chat: &mut ChatWidget) {
|
||||
}
|
||||
|
||||
fn test_model_info(slug: &str, priority: i32, supports_fast_mode: bool) -> ModelInfo {
|
||||
let additional_speed_tiers = if supports_fast_mode {
|
||||
vec![codex_protocol::openai_models::SPEED_TIER_FAST]
|
||||
let service_tiers = if supports_fast_mode {
|
||||
vec![json!({
|
||||
"id": "priority",
|
||||
"name": "Fast",
|
||||
"description": "Fast tier",
|
||||
})]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -387,7 +391,7 @@ fn test_model_info(slug: &str, priority: i32, supports_fast_mode: bool) -> Model
|
||||
"visibility": "list",
|
||||
"supported_in_api": true,
|
||||
"priority": priority,
|
||||
"additional_speed_tiers": additional_speed_tiers,
|
||||
"service_tiers": service_tiers,
|
||||
"availability_nux": null,
|
||||
"upgrade": null,
|
||||
"base_instructions": "base instructions",
|
||||
|
||||
@@ -2170,7 +2170,7 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() {
|
||||
description: "medium".to_string(),
|
||||
}],
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker,
|
||||
@@ -2392,7 +2392,7 @@ async fn single_reasoning_option_skips_selection() {
|
||||
default_reasoning_effort: ReasoningEffortConfig::High,
|
||||
supported_reasoning_efforts: single_effort,
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker: true,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use crate::bottom_pane::slash_commands::ServiceTierCommand;
|
||||
use crate::bottom_pane::slash_commands::SlashCommandAction;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn turn_complete_event(turn_id: &str, last_agent_message: Option<&str>) -> TurnCompleteEvent {
|
||||
@@ -23,6 +25,15 @@ fn queue_composer_text_with_tab(chat: &mut ChatWidget, text: &str) {
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
}
|
||||
|
||||
fn fast_service_tier_command() -> SlashCommandAction {
|
||||
SlashCommandAction::ServiceTier(ServiceTierCommand {
|
||||
service_tier: ServiceTier::priority(),
|
||||
command: "fast".to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Fast tier".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn recall_latest_after_clearing(chat: &mut ChatWidget) -> String {
|
||||
chat.bottom_pane
|
||||
.set_composer_text(String::new(), Vec::new(), Vec::new());
|
||||
@@ -2050,14 +2061,14 @@ 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, /*enabled*/ true);
|
||||
|
||||
chat.dispatch_command(SlashCommand::Fast);
|
||||
chat.dispatch_command(fast_service_tier_command());
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::CodexOp(AppCommand::OverrideTurnContext {
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
service_tier: Some(Some(ServiceTier::priority())),
|
||||
..
|
||||
})
|
||||
)),
|
||||
@@ -2067,7 +2078,7 @@ async fn fast_slash_command_updates_and_persists_local_service_tier() {
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::PersistServiceTierSelection {
|
||||
service_tier: Some(ServiceTier::Fast),
|
||||
service_tier: Some(ServiceTier::priority()),
|
||||
}
|
||||
)),
|
||||
"expected fast-mode persistence app event; events: {events:?}"
|
||||
@@ -2083,7 +2094,7 @@ async fn user_turn_carries_service_tier_after_fast_toggle() {
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true);
|
||||
|
||||
chat.dispatch_command(SlashCommand::Fast);
|
||||
chat.dispatch_command(fast_service_tier_command());
|
||||
|
||||
let _events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
|
||||
@@ -2093,7 +2104,7 @@ async fn user_turn_carries_service_tier_after_fast_toggle() {
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn {
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
service_tier: Some(Some(ServiceTier::priority())),
|
||||
..
|
||||
} => {}
|
||||
other => panic!("expected Op::UserTurn with fast service tier, got {other:?}"),
|
||||
@@ -2129,7 +2140,7 @@ async fn queued_fast_slash_applies_before_next_queued_message() {
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::CodexOp(AppCommand::OverrideTurnContext {
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
service_tier: Some(Some(ServiceTier::priority())),
|
||||
..
|
||||
})
|
||||
)),
|
||||
@@ -2139,7 +2150,7 @@ async fn queued_fast_slash_applies_before_next_queued_message() {
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn {
|
||||
items,
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
service_tier: Some(Some(ServiceTier::priority())),
|
||||
..
|
||||
} => assert_eq!(
|
||||
items,
|
||||
@@ -2159,10 +2170,10 @@ async fn user_turn_sends_standard_override_after_fast_is_turned_off() {
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true);
|
||||
|
||||
chat.dispatch_command(SlashCommand::Fast);
|
||||
chat.dispatch_command(fast_service_tier_command());
|
||||
let _events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
|
||||
chat.dispatch_command_with_args(SlashCommand::Fast, "off".to_string(), Vec::new());
|
||||
chat.dispatch_command_with_args(fast_service_tier_command(), "off".to_string(), Vec::new());
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
|
||||
@@ -1037,14 +1037,14 @@ async fn commentary_completion_restores_status_indicator_before_exec_begin() {
|
||||
async fn fast_status_indicator_requires_chatgpt_auth() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
chat.set_service_tier(Some(ServiceTier::Fast));
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
chat.set_service_tier(Some(ServiceTier::priority()));
|
||||
|
||||
assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),));
|
||||
|
||||
set_chatgpt_auth(&mut chat);
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
|
||||
assert!(chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),));
|
||||
}
|
||||
@@ -1053,11 +1053,17 @@ async fn fast_status_indicator_requires_chatgpt_auth() {
|
||||
async fn fast_status_indicator_is_hidden_for_models_without_fast_support() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(!get_available_model(&chat, "gpt-5.3-codex").supports_fast_mode());
|
||||
chat.set_service_tier(Some(ServiceTier::Fast));
|
||||
assert!(
|
||||
!get_available_model(&chat, "gpt-5.3-codex")
|
||||
.supports_service_tier(&ServiceTier::priority())
|
||||
);
|
||||
chat.set_service_tier(Some(ServiceTier::priority()));
|
||||
set_chatgpt_auth(&mut chat);
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(!get_available_model(&chat, "gpt-5.3-codex").supports_fast_mode());
|
||||
assert!(
|
||||
!get_available_model(&chat, "gpt-5.3-codex")
|
||||
.supports_service_tier(&ServiceTier::priority())
|
||||
);
|
||||
|
||||
assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),));
|
||||
}
|
||||
@@ -1066,10 +1072,10 @@ async fn fast_status_indicator_is_hidden_for_models_without_fast_support() {
|
||||
async fn fast_status_indicator_is_hidden_when_fast_mode_is_off() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
set_chatgpt_auth(&mut chat);
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
|
||||
assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),));
|
||||
}
|
||||
@@ -1438,11 +1444,11 @@ async fn status_line_fast_mode_renders_on_and_off() {
|
||||
chat.config.tui_status_line = Some(vec!["fast-mode".to_string()]);
|
||||
|
||||
chat.refresh_status_line();
|
||||
assert_eq!(status_line_text(&chat), Some("Fast off".to_string()));
|
||||
assert_eq!(status_line_text(&chat), Some("Tier default".to_string()));
|
||||
|
||||
chat.set_service_tier(Some(ServiceTier::Fast));
|
||||
chat.set_service_tier(Some(ServiceTier::priority()));
|
||||
chat.refresh_status_line();
|
||||
assert_eq!(status_line_text(&chat), Some("Fast on".to_string()));
|
||||
assert_eq!(status_line_text(&chat), Some("Tier Fast".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1453,7 +1459,7 @@ async fn status_line_fast_mode_footer_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.show_welcome_banner = false;
|
||||
chat.config.tui_status_line = Some(vec!["fast-mode".to_string()]);
|
||||
chat.set_service_tier(Some(ServiceTier::Fast));
|
||||
chat.set_service_tier(Some(ServiceTier::priority()));
|
||||
chat.refresh_status_line();
|
||||
|
||||
let width = 80;
|
||||
@@ -1472,7 +1478,7 @@ async fn status_line_fast_mode_footer_snapshot() {
|
||||
async fn status_line_model_with_reasoning_includes_fast_for_fast_capable_models() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
chat.config.cwd = test_project_path().abs();
|
||||
chat.config.tui_status_line = Some(vec![
|
||||
"model-with-reasoning".to_string(),
|
||||
@@ -1480,10 +1486,10 @@ async fn status_line_model_with_reasoning_includes_fast_for_fast_capable_models(
|
||||
"current-dir".to_string(),
|
||||
]);
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
|
||||
chat.set_service_tier(Some(ServiceTier::Fast));
|
||||
chat.set_service_tier(Some(ServiceTier::priority()));
|
||||
set_chatgpt_auth(&mut chat);
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
chat.refresh_status_line();
|
||||
let test_cwd = test_path_display("/tmp/project");
|
||||
|
||||
@@ -1617,7 +1623,7 @@ async fn status_line_model_with_reasoning_fast_footer_snapshot() {
|
||||
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
chat.show_welcome_banner = false;
|
||||
chat.config.cwd = test_project_path().abs();
|
||||
chat.config.tui_status_line = Some(vec![
|
||||
@@ -1626,10 +1632,10 @@ async fn status_line_model_with_reasoning_fast_footer_snapshot() {
|
||||
"current-dir".to_string(),
|
||||
]);
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
|
||||
chat.set_service_tier(Some(ServiceTier::Fast));
|
||||
chat.set_service_tier(Some(ServiceTier::priority()));
|
||||
set_chatgpt_auth(&mut chat);
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
chat.refresh_status_line();
|
||||
|
||||
let width = 80;
|
||||
@@ -1651,7 +1657,7 @@ async fn status_line_model_with_reasoning_context_remaining_footer_snapshot() {
|
||||
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
chat.show_welcome_banner = false;
|
||||
chat.config.cwd = test_project_path().abs();
|
||||
chat.config.tui_status_line = Some(vec![
|
||||
@@ -1660,10 +1666,10 @@ async fn status_line_model_with_reasoning_context_remaining_footer_snapshot() {
|
||||
"current-dir".to_string(),
|
||||
]);
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
|
||||
chat.set_service_tier(Some(ServiceTier::Fast));
|
||||
chat.set_service_tier(Some(ServiceTier::priority()));
|
||||
set_chatgpt_auth(&mut chat);
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_fast_mode());
|
||||
assert!(get_available_model(&chat, "gpt-5.4").supports_service_tier(&ServiceTier::priority()));
|
||||
chat.refresh_status_line();
|
||||
|
||||
let width = 80;
|
||||
|
||||
@@ -13,7 +13,6 @@ 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,
|
||||
Keymap,
|
||||
@@ -101,9 +100,6 @@ 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 to enable fastest inference with increased plan usage"
|
||||
}
|
||||
SlashCommand::Personality => "choose a communication style for Codex",
|
||||
SlashCommand::Realtime => "toggle realtime voice mode (experimental)",
|
||||
SlashCommand::Settings => "configure realtime microphone/speaker",
|
||||
@@ -145,7 +141,6 @@ impl SlashCommand {
|
||||
| SlashCommand::Rename
|
||||
| SlashCommand::Plan
|
||||
| SlashCommand::Goal
|
||||
| SlashCommand::Fast
|
||||
| SlashCommand::Mcp
|
||||
| SlashCommand::Side
|
||||
| SlashCommand::Resume
|
||||
@@ -171,7 +166,6 @@ impl SlashCommand {
|
||||
| SlashCommand::Compact
|
||||
// | SlashCommand::Undo
|
||||
| SlashCommand::Model
|
||||
| SlashCommand::Fast
|
||||
| SlashCommand::Personality
|
||||
| SlashCommand::Approvals
|
||||
| SlashCommand::Permissions
|
||||
|
||||
@@ -3343,9 +3343,16 @@ class ServerRequestResolvedNotification(BaseModel):
|
||||
thread_id: Annotated[str, Field(alias="threadId")]
|
||||
|
||||
|
||||
class ServiceTier(Enum):
|
||||
fast = "fast"
|
||||
flex = "flex"
|
||||
class ServiceTier(RootModel[str]):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
root: Annotated[
|
||||
str,
|
||||
Field(
|
||||
description="String-backed service tier identifier. Known values include `priority`, `flex`, and `ultrafast`, but other backend-provided ids are allowed."
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class SessionSourceValue(Enum):
|
||||
@@ -5854,13 +5861,19 @@ class MigrationDetails(BaseModel):
|
||||
plugins: list[PluginsMigration]
|
||||
|
||||
|
||||
class ModelServiceTier(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
description: str
|
||||
id: ServiceTier
|
||||
name: str
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
additional_speed_tiers: Annotated[
|
||||
list[str] | None, Field(alias="additionalSpeedTiers")
|
||||
] = []
|
||||
availability_nux: Annotated[
|
||||
ModelAvailabilityNux | None, Field(alias="availabilityNux")
|
||||
] = None
|
||||
@@ -5876,6 +5889,9 @@ class Model(BaseModel):
|
||||
] = ["text", "image"]
|
||||
is_default: Annotated[bool, Field(alias="isDefault")]
|
||||
model: str
|
||||
service_tiers: Annotated[
|
||||
list[ModelServiceTier] | None, Field(alias="serviceTiers")
|
||||
] = []
|
||||
supported_reasoning_efforts: Annotated[
|
||||
list[ReasoningEffortOption], Field(alias="supportedReasoningEfforts")
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user