use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingSelection {
Changed,
PersistedSame,
Refused,
}
impl SettingSelection {
#[must_use]
pub fn changed_live_state(self) -> bool {
matches!(self, Self::Changed)
}
#[must_use]
#[cfg(test)]
pub fn accepted(self) -> bool {
!matches!(self, Self::Refused)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode {
Agent,
#[allow(dead_code)]
Auto,
Yolo,
Plan,
Operate,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningEffort {
Off,
Low,
Medium,
High,
Auto,
#[default]
Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EffectiveReasoningEffort {
Tier(ReasoningEffort),
ThinkingEnabledGranularityUnavailable,
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CacheReplayTarget {
pub(crate) provider: ApiProvider,
pub(crate) provider_identity: String,
pub(crate) provider_id: Option<String>,
pub(crate) model: String,
pub(crate) base_url: Option<String>,
}
impl EffectiveReasoningEffort {
#[must_use]
pub(crate) const fn request_tier_for_replay(self) -> Option<ReasoningEffort> {
match self {
Self::Tier(tier) => Some(tier),
Self::ThinkingEnabledGranularityUnavailable => Some(ReasoningEffort::High),
Self::Unavailable => None,
}
}
}
impl From<EffectiveReasoningEffort> for crate::work_graph::ReasoningEffortTier {
fn from(value: EffectiveReasoningEffort) -> Self {
match value {
EffectiveReasoningEffort::Tier(tier) => tier.into(),
EffectiveReasoningEffort::ThinkingEnabledGranularityUnavailable => {
Self::ThinkingEnabledGranularityUnavailable
}
EffectiveReasoningEffort::Unavailable => Self::Unavailable,
}
}
}
impl From<crate::work_graph::ReasoningEffortTier> for EffectiveReasoningEffort {
fn from(value: crate::work_graph::ReasoningEffortTier) -> Self {
use crate::work_graph::ReasoningEffortTier as Tier;
match value {
Tier::Off => Self::Tier(ReasoningEffort::Off),
Tier::Low => Self::Tier(ReasoningEffort::Low),
Tier::Medium => Self::Tier(ReasoningEffort::Medium),
Tier::High => Self::Tier(ReasoningEffort::High),
Tier::Auto => Self::Tier(ReasoningEffort::Auto),
Tier::Max => Self::Tier(ReasoningEffort::Max),
Tier::ThinkingEnabledGranularityUnavailable => {
Self::ThinkingEnabledGranularityUnavailable
}
Tier::Unavailable => Self::Unavailable,
}
}
}
impl From<ReasoningEffort> for crate::work_graph::ReasoningEffortTier {
fn from(value: ReasoningEffort) -> Self {
match value {
ReasoningEffort::Off => Self::Off,
ReasoningEffort::Low => Self::Low,
ReasoningEffort::Medium => Self::Medium,
ReasoningEffort::High => Self::High,
ReasoningEffort::Auto => Self::Auto,
ReasoningEffort::Max => Self::Max,
}
}
}
impl ReasoningEffort {
pub fn parse_strict(value: &str) -> Result<Self, String> {
let trimmed = value.trim();
match trimmed.to_ascii_lowercase().as_str() {
"off" | "disabled" | "none" | "false" => Ok(Self::Off),
"low" | "minimum" | "minimal" | "light" => Ok(Self::Low),
"medium" | "mid" => Ok(Self::Medium),
"high" => Ok(Self::High),
"auto" | "automatic" => Ok(Self::Auto),
"max" | "maximum" | "xhigh" | "ultra" | "ultracode" => Ok(Self::Max),
_ => Err(format!(
"Unrecognized reasoning effort {trimmed:?}. Expected: auto, off, low, medium, high, or max."
)),
}
}
#[must_use]
pub fn from_setting(value: &str) -> Self {
Self::parse_strict(value).unwrap_or_default()
}
#[must_use]
pub fn from_setting_for_provider(value: &str, provider: ApiProvider) -> Self {
Self::from_setting(value).normalize_for_provider(provider)
}
#[must_use]
pub fn as_setting(self) -> &'static str {
match self {
Self::Off => "off",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Auto => "auto",
Self::Max => "max",
}
}
#[must_use]
pub fn short_label(self) -> &'static str {
match self {
Self::Off => "off",
Self::Low => "low",
Self::Medium => "med",
Self::High => "high",
Self::Auto => "auto",
Self::Max => "max",
}
}
#[must_use]
pub fn display_label_for_provider(self, provider: ApiProvider) -> &'static str {
match (provider, self.normalize_for_provider(provider)) {
(ApiProvider::OpenaiCodex, Self::Low) => "low",
(ApiProvider::OpenaiCodex, Self::Medium) => "medium",
(ApiProvider::OpenaiCodex, Self::High) => "high",
(ApiProvider::OpenaiCodex, Self::Max) => "xhigh",
(_, effort) => effort.short_label(),
}
}
#[must_use]
pub fn api_value(self) -> Option<&'static str> {
Some(self.as_setting())
}
#[must_use]
pub fn normalize_for_provider(self, provider: ApiProvider) -> Self {
if provider != ApiProvider::OpenaiCodex {
return self;
}
match self {
Self::Off => Self::Low,
Self::Auto => Self::Medium,
other => other,
}
}
#[must_use]
pub fn normalize_for_route(
self,
provider: ApiProvider,
base_url: &str,
wire_model: &str,
) -> Self {
let normalized = self.normalize_for_provider(provider);
if crate::config::is_exact_kimi_code_k3_route(provider, base_url, wire_model) {
return match normalized {
Self::Off => Self::Low,
other => other,
};
}
if crate::config::is_exact_direct_moonshot_k3_route(provider, base_url, wire_model) {
return match normalized {
Self::Off => Self::Low,
Self::Medium => Self::High,
other => other,
};
}
if provider == ApiProvider::OpenaiCodex {
return normalized;
}
match normalized {
Self::Low | Self::Medium => Self::High,
other => other,
}
}
#[must_use]
pub fn api_value_for_provider(self, provider: ApiProvider) -> Option<&'static str> {
if provider != ApiProvider::OpenaiCodex {
return self.api_value();
}
Some(match self.normalize_for_provider(provider) {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Max => "xhigh",
Self::Off => "low",
Self::Auto => "medium",
})
}
#[must_use]
pub fn api_value_for_route(
self,
provider: ApiProvider,
base_url: &str,
wire_model: &str,
) -> Option<&'static str> {
self.normalize_for_route(provider, base_url, wire_model)
.api_value_for_provider(provider)
}
#[must_use]
pub fn as_setting_for_provider(self, provider: ApiProvider) -> &'static str {
self.api_value_for_provider(provider)
.unwrap_or_else(|| self.as_setting())
}
#[must_use]
pub fn as_setting_for_route(
self,
provider: ApiProvider,
base_url: &str,
wire_model: &str,
) -> &'static str {
self.normalize_for_route(provider, base_url, wire_model)
.as_setting_for_provider(provider)
}
#[must_use]
pub fn cycle_next(self) -> Self {
match self {
Self::Off => Self::High,
Self::Auto => Self::Off,
Self::Low | Self::Medium | Self::High => Self::Max,
Self::Max => Self::Off,
}
}
#[must_use]
pub fn cycle_next_for_provider(self, provider: ApiProvider) -> Self {
if provider != ApiProvider::OpenaiCodex {
return self.cycle_next();
}
match self.normalize_for_provider(provider) {
Self::Low => Self::Medium,
Self::Medium => Self::High,
Self::High => Self::Max,
Self::Max => Self::Low,
Self::Off | Self::Auto => Self::Low,
}
}
#[must_use]
pub fn cycle_next_for_auto_model(self) -> Self {
match self {
Self::Auto => Self::Off,
Self::Off => Self::Low,
Self::Low => Self::Medium,
Self::Medium => Self::High,
Self::High => Self::Max,
Self::Max => Self::Auto,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComposerDensity {
Compact,
Comfortable,
Spacious,
}
impl ComposerDensity {
#[must_use]
pub fn from_setting(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"compact" | "tight" => Self::Compact,
"spacious" | "loose" => Self::Spacious,
_ => Self::Comfortable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscriptSpacing {
Compact,
Comfortable,
Spacious,
}
impl TranscriptSpacing {
#[must_use]
pub fn from_setting(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"compact" | "tight" => Self::Compact,
"spacious" | "loose" => Self::Spacious,
_ => Self::Comfortable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCollapseMode {
Compact,
Expanded,
Calm,
}
impl ToolCollapseMode {
#[must_use]
pub fn from_setting(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"expanded" | "off" | "none" => Self::Expanded,
"calm" | "calm-mode" | "calm_only" | "calm-only" => Self::Calm,
_ => Self::Compact,
}
}
#[must_use]
pub fn as_setting(self) -> &'static str {
match self {
Self::Compact => "compact",
Self::Expanded => "expanded",
Self::Calm => "calm",
}
}
#[must_use]
pub fn is_active(self, calm_mode: bool) -> bool {
match self {
Self::Compact => true,
Self::Expanded => false,
Self::Calm => calm_mode,
}
}
}
impl AppMode {
pub const CYCLE: [Self; 3] = [Self::Plan, Self::Agent, Self::Operate];
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"agent" | "act" | "auto" | "1" => Some(Self::Agent),
"plan" | "2" => Some(Self::Plan),
"operate" | "operation" | "ops" | "3" => Some(Self::Operate),
"yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions" => {
Some(Self::Yolo)
}
_ => None,
}
}
#[must_use]
pub fn from_setting(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"multitask" | "multi" | "5" => Self::Operate,
other => Self::parse(other).unwrap_or(Self::Agent),
}
}
#[must_use]
pub fn as_setting(self) -> &'static str {
match self {
Self::Agent => "agent",
Self::Auto => "agent",
Self::Yolo => "agent",
Self::Plan => "plan",
Self::Operate => "operate",
}
}
pub fn label(self) -> &'static str {
match self {
AppMode::Agent => "ACT",
AppMode::Auto => "ACT",
AppMode::Yolo => "ACT",
AppMode::Plan => "PLAN",
AppMode::Operate => "OPERATE",
}
}
#[must_use]
pub fn display_name(self) -> &'static str {
match self {
AppMode::Agent => "Act",
AppMode::Auto => "Act",
AppMode::Yolo => "Act",
AppMode::Plan => "Plan",
AppMode::Operate => "Operate",
}
}
#[must_use]
pub fn number(self) -> char {
match self {
AppMode::Agent | AppMode::Auto | AppMode::Yolo => '1',
AppMode::Plan => '2',
AppMode::Operate => '3',
}
}
#[must_use]
pub fn uses_agent_baseline(self) -> bool {
matches!(self, Self::Agent | Self::Auto | Self::Operate)
}
#[must_use]
pub fn mode_delegation_launch_floor(self) -> usize {
match self {
Self::Operate => 4,
_ => 1,
}
}
#[must_use]
pub fn display_name_localized(self, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match self {
AppMode::Agent | AppMode::Auto | AppMode::Yolo => MessageId::AppModeAgent,
AppMode::Plan => MessageId::AppModePlan,
AppMode::Operate => MessageId::AppModeOperate,
},
)
}
#[must_use]
pub fn picker_hint_localized(self, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match self {
AppMode::Agent | AppMode::Auto | AppMode::Yolo => MessageId::AppModeAgentHint,
AppMode::Plan => MessageId::AppModePlanHint,
AppMode::Operate => MessageId::AppModeOperateHint,
},
)
}
#[allow(dead_code)]
pub fn description(self) -> &'static str {
match self {
AppMode::Agent | AppMode::Auto => {
"Act mode - direct work in the current session with tools"
}
AppMode::Yolo => "Act mode with Full Access (legacy compatibility setting)",
AppMode::Plan => "Plan mode - research and design before implementing",
AppMode::Operate => "Operate mode - send tasks while Fleet workers run in parallel",
}
}
#[must_use]
pub fn next(self) -> Self {
let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
return Self::Agent;
};
Self::CYCLE[(index + 1) % Self::CYCLE.len()]
}
#[must_use]
pub fn previous(self) -> Self {
let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
return Self::Agent;
};
Self::CYCLE[(index + Self::CYCLE.len() - 1) % Self::CYCLE.len()]
}
}
#[derive(Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct TuiOptions {
pub model: String,
pub workspace: PathBuf,
pub config_path: Option<PathBuf>,
pub config_profile: Option<String>,
pub allow_shell: bool,
pub use_alt_screen: bool,
pub use_mouse_capture: bool,
pub use_bracketed_paste: bool,
pub max_subagents: usize,
#[allow(dead_code)]
pub skills_dir: PathBuf,
#[allow(dead_code)]
pub memory_path: PathBuf,
#[allow(dead_code)]
pub notes_path: PathBuf,
#[allow(dead_code)]
pub mcp_config_path: PathBuf,
#[allow(dead_code)]
pub use_memory: bool,
pub start_in_agent_mode: bool,
pub skip_onboarding: bool,
pub yolo: bool,
pub resume_session_id: Option<String>,
pub initial_input: Option<InitialInput>,
pub startup_notice: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InitialInput {
Prefill(String),
Submit(String),
RemoteControl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VimMode {
#[default]
Normal,
Insert,
Visual,
}
impl VimMode {
#[must_use]
pub fn label_localized(self, locale: Locale) -> Cow<'static, str> {
tr(
locale,
match self {
Self::Normal => MessageId::VimModeNormal,
Self::Insert => MessageId::VimModeInsert,
Self::Visual => MessageId::VimModeVisual,
},
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueuedMessage {
pub display: String,
pub skill_instruction: Option<String>,
pub skill_provenance: Option<crate::plugins::types::PluginAuthority>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubmitDisposition {
Immediate,
Queue,
Steer,
#[allow(dead_code)]
QueueFollowUp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComposerSubmitChord {
Enter,
CtrlEnter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComposerSubmitAction {
Submit(SubmitDisposition),
SendQueuedNow,
Noop,
}
#[derive(Debug, Clone)]
pub struct ToolDetailRecord {
pub tool_id: String,
pub tool_name: String,
pub input: Value,
pub output: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaskPanelEntry {
pub id: String,
pub status: String,
pub prompt_summary: String,
pub duration_ms: Option<u64>,
pub kind: TaskPanelEntryKind,
pub stale: bool,
pub elapsed_since_output_ms: Option<u64>,
pub owner_agent_id: Option<String>,
pub owner_agent_name: Option<String>,
pub current_tool: Option<String>,
pub role: Option<String>,
pub files_touched: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskPanelEntryKind {
Background,
}
impl QueuedMessage {
pub fn new(display: String, skill_instruction: Option<String>) -> Self {
Self {
display,
skill_instruction,
skill_provenance: None,
}
}
#[must_use]
pub fn with_skill_provenance(
mut self,
provenance: Option<crate::plugins::types::PluginAuthority>,
) -> Self {
self.skill_provenance = provenance;
self
}
#[allow(dead_code)] pub fn content(&self) -> String {
if let Some(skill_instruction) = self.skill_instruction.as_ref() {
format!(
"{skill_instruction}\n\n---\n\nUser request: {}",
self.display
)
} else {
self.display.clone()
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppAction {
Quit,
#[allow(dead_code)] SaveSession(PathBuf),
#[allow(dead_code)] LoadSession(PathBuf),
RemoteControl(crate::remote_control::RemoteControlAction),
SyncSession {
session_id: Option<String>,
messages: Vec<Message>,
system_prompt: Option<SystemPrompt>,
model: String,
workspace: PathBuf,
mode: AppMode,
},
OpenConfigEditor(ConfigUiMode),
OpenConfigView,
OpenWorktreeManager,
OpenModelPicker,
OpenProviderPicker,
OpenProviderSetup {
provider: Option<ApiProvider>,
},
StartXaiDeviceLogin,
OpenModePicker,
ModeChanged(AppMode),
ApprovalPolicyPersisted {
policy: Option<String>,
},
PermissionRulesChanged,
PluginRegistryChanged,
OpenStatusPicker,
OpenFeedbackPicker,
OpenThemePicker,
OpenSkillsManager,
OpenFleetRoster,
OpenFleetSetup,
OpenHotbarSetup,
OpenSetupWizard,
OpenSetupWizardAt {
step: codewhale_config::SetupStep,
},
UseBundledConstitution,
PreviewEffectiveBasePrompt,
DisableHotbar,
RestoreHotbarDefaults,
OpenExternalUrl {
url: String,
label: String,
},
SendMessage(String),
CancelSubAgent {
agent_id: String,
},
SetGoalStatus {
status: crate::tools::goal::GoalStatus,
clear: bool,
},
ListSubAgents,
PreviewOutboundRequest {
json: bool,
base_prompt_only: bool,
hypothetical_prompt: Option<String>,
},
OpenTextPager {
title: String,
content: String,
},
FetchModels,
RefreshModelsDevCatalog,
CacheWarmup,
SwitchProvider {
provider: ApiProvider,
model: Option<String>,
},
SwitchModelRoute {
provider: ApiProvider,
model: String,
},
UpdateCompaction(CompactionConfig),
UpdateStreamChunkTimeout(u64),
UpdateSubagentRuntimeConfig {
enabled: bool,
max_subagents: usize,
launch_concurrency: usize,
max_spawn_depth: u32,
api_timeout_secs: u64,
heartbeat_timeout_secs: u64,
},
OpenLiveTranscript,
OpenTurnInspector,
OpenContextInspector,
CompactContext {
focus: Option<String>,
},
PurgeContext,
TaskAdd {
prompt: String,
},
TaskList,
TaskShow {
id: String,
},
TaskCancel {
id: String,
},
ShellJob(ShellJobAction),
Mcp(McpUiAction),
SwitchProfile {
profile: String,
},
SwitchWorkspace {
workspace: PathBuf,
},
VoiceCapture,
ShareSession {
history_len: usize,
model: String,
mode: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShellJobAction {
List,
Show {
id: String,
},
Poll {
id: String,
wait: bool,
},
SendStdin {
id: String,
input: String,
close: bool,
},
Cancel {
id: String,
},
CancelAll,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpUiAction {
Show,
Init {
force: bool,
},
AddStdio {
name: String,
command: String,
args: Vec<String>,
},
AddHttp {
name: String,
url: String,
transport: Option<String>,
},
Enable {
name: String,
},
Disable {
name: String,
},
Remove {
name: String,
},
Login {
name: String,
scopes: Vec<String>,
},
Logout {
name: String,
},
ImportList,
ImportApprove {
name: String,
},
ImportDecline {
name: String,
},
Validate,
Reload,
}