diff --git a/app/src/cortex_settings/action.rs b/app/src/cortex_settings/action.rs index 9f29b78c1a..1fd7118300 100644 --- a/app/src/cortex_settings/action.rs +++ b/app/src/cortex_settings/action.rs @@ -13,6 +13,7 @@ use crate::settings::{ pub enum CortexSettingsSection { WorkingPanes, Tabs, + Ai, } impl CortexSettingsSection { @@ -20,11 +21,12 @@ impl CortexSettingsSection { match self { Self::WorkingPanes => "Working Panes", Self::Tabs => "Tabs", + Self::Ai => "AI", } } pub fn all() -> &'static [Self] { - &[Self::WorkingPanes, Self::Tabs] + &[Self::WorkingPanes, Self::Tabs, Self::Ai] } } @@ -72,4 +74,11 @@ pub enum CortexSettingsAction { SetTabTitleFontWeight(Weight), /// Flip the italic toggle for tab titles. ToggleTabTitleItalic, + /// Flip the "Allow Claude Code / Codex as orchestrate child agents" toggle + /// on the AI page. Mirrors the persisted [`crate::settings::CortexSettings`] + /// bool *and* pushes the same value into + /// `FeatureFlag::LocalClaudeCodexChildHarnesses` via `set_user_preference`, + /// so the orchestration controls (`local_child_harnesses.rs`, + /// `orchestration_controls.rs`) react without a restart. + ToggleAllowLocalClaudeCodexChildHarnesses, } diff --git a/app/src/cortex_settings/ai_page.rs b/app/src/cortex_settings/ai_page.rs new file mode 100644 index 0000000000..48a24c461f --- /dev/null +++ b/app/src/cortex_settings/ai_page.rs @@ -0,0 +1,109 @@ +//! Content rendered on the right side of the Cortex Settings pane when the +//! "AI" section is selected. +//! +//! Currently houses a single toggle: `Allow Claude Code / Codex as +//! orchestrate child agents`. The toggle is the user-facing surface for +//! [`warp_core::features::FeatureFlag::LocalClaudeCodexChildHarnesses`]; the +//! flag stays the single source of truth read by +//! `app/src/ai/local_child_harnesses.rs` and +//! `app/src/ai/blocklist/inline_action/orchestration_controls.rs`, and the +//! setting just hydrates that flag at startup and on each user flip. +use warpui::{ + elements::{ + Align, Container, CrossAxisAlignment, Element, Flex, Padding, ParentElement, Shrinkable, + }, + ui_components::{components::UiComponent, switch::SwitchStateHandle}, + AppContext, SingletonEntity, +}; + +use crate::appearance::Appearance; +use crate::cortex_settings::action::CortexSettingsAction; +use crate::settings::CortexSettings; + +const ROW_VERTICAL_PADDING: f32 = 6.0; +const CONTROL_RIGHT_PADDING: f32 = 5.0; + +/// Per-toggle UI state that has to outlive a single render frame (switch +/// animation state). Owned by `CortexSettingsView`. +#[derive(Default)] +pub struct AiPageState { + allow_local_claude_codex_child_harnesses_switch: SwitchStateHandle, +} + +pub fn ai_page_search_terms() -> &'static [&'static str] { + &[ + "ai", + "agent", + "orchestrate", + "orchestration", + "claude", + "claude code", + "codex", + "child", + "harness", + "subagent", + "sub-agent", + ] +} + +pub fn render_ai_page( + state: &AiPageState, + appearance: &Appearance, + app: &AppContext, +) -> Box { + Flex::column() + .with_cross_axis_alignment(CrossAxisAlignment::Stretch) + .with_child(render_allow_local_claude_codex_child_harnesses_row( + state, appearance, app, + )) + .finish() +} + +fn render_allow_local_claude_codex_child_harnesses_row( + state: &AiPageState, + appearance: &Appearance, + app: &AppContext, +) -> Box { + let ui_builder = appearance.ui_builder(); + let current_value = *CortexSettings::as_ref(app).allow_local_claude_codex_child_harnesses; + + let label = ui_builder + .span("Allow Claude Code / Codex as orchestrate child agents".to_string()) + .build() + .finish(); + + let switch = ui_builder + .switch( + state + .allow_local_claude_codex_child_harnesses_switch + .clone(), + ) + .check(current_value) + .build() + .on_click(move |ctx, _, _| { + ctx.dispatch_typed_action( + CortexSettingsAction::ToggleAllowLocalClaudeCodexChildHarnesses, + ); + }) + .finish(); + + let header = Shrinkable::new( + 1.0, + Container::new(Align::new(label).left().finish()).finish(), + ) + .finish(); + + let control = Container::new(switch) + .with_padding_right(CONTROL_RIGHT_PADDING) + .finish(); + + let row = Flex::row() + .with_cross_axis_alignment(CrossAxisAlignment::Center) + .with_child(header) + .with_child(control) + .finish(); + + Container::new(row) + .with_padding(Padding::uniform(ROW_VERTICAL_PADDING)) + .finish() +} diff --git a/app/src/cortex_settings/mod.rs b/app/src/cortex_settings/mod.rs index 111a91299c..b20b34c2e8 100644 --- a/app/src/cortex_settings/mod.rs +++ b/app/src/cortex_settings/mod.rs @@ -19,6 +19,7 @@ //! `app/src/pane_group/pane/cortex_settings_pane.rs` because it needs access //! to `pub(super)` items in the pane-group module hierarchy. pub mod action; +pub mod ai_page; pub mod brand; pub mod pane_manager; pub mod tabs_page; diff --git a/app/src/cortex_settings/view.rs b/app/src/cortex_settings/view.rs index 8f87a87f6e..610c40bb86 100644 --- a/app/src/cortex_settings/view.rs +++ b/app/src/cortex_settings/view.rs @@ -24,6 +24,7 @@ use warpui::{ use crate::appearance::Appearance; use crate::cortex_settings::action::{CortexSettingsAction, CortexSettingsSection}; +use crate::cortex_settings::ai_page::{ai_page_search_terms, render_ai_page, AiPageState}; use crate::cortex_settings::brand::{ BRAND_HEADER_ICON_TO_TITLE_FONT_RATIO, BRAND_HEADER_TITLE_TO_FONT_RATIO, BRAND_MENU_ICON_LABEL_GAP_RATIO, @@ -81,6 +82,7 @@ pub struct CortexSettingsView { sidebar_states: Vec<(CortexSettingsSection, MouseStateHandle)>, working_panes_state: WorkingPanesPageState, tabs_state: TabsPageState, + ai_state: AiPageState, search_editor: ViewHandle, } @@ -119,6 +121,7 @@ impl CortexSettingsView { sidebar_states, working_panes_state: WorkingPanesPageState::default(), tabs_state: TabsPageState::new(ctx), + ai_state: AiPageState::default(), search_editor, } } @@ -321,6 +324,32 @@ impl CortexSettingsView { ctx.notify(); } + fn toggle_allow_local_claude_codex_child_harnesses(&mut self, ctx: &mut ViewContext) { + use crate::settings::CortexSettings; + use settings::ToggleableSetting; + use warp_core::features::FeatureFlag; + use warpui::SingletonEntity; + + // Read pre-toggle value so we can compute the post-toggle value and push + // it into the feature flag without re-reading after the closure (which + // would race with rendering threads that have already snapshotted). + let previous_value = *CortexSettings::as_ref(ctx).allow_local_claude_codex_child_harnesses; + + CortexSettings::handle(ctx).update(ctx, |settings, ctx| { + let _ = settings + .allow_local_claude_codex_child_harnesses + .toggle_and_save_value(ctx); + }); + + // Mirror the setting into the runtime feature flag so the existing + // gate sites (`local_child_harnesses.rs`, `orchestration_controls.rs`) + // pick up the change on the next `is_enabled()` call without needing + // to be ported to read the setting directly. + FeatureFlag::LocalClaudeCodexChildHarnesses.set_user_preference(!previous_value); + + ctx.notify(); + } + fn handle_search_editor_event( &mut self, _editor: ViewHandle, @@ -442,6 +471,7 @@ impl CortexSettingsView { render_working_panes_page(&self.working_panes_state, appearance, app) } CortexSettingsSection::Tabs => render_tabs_page(&self.tabs_state, appearance, app), + CortexSettingsSection::Ai => render_ai_page(&self.ai_state, appearance, app), } } } @@ -523,6 +553,9 @@ impl warpui::TypedActionView for CortexSettingsView { self.set_tab_title_font_weight(*value, ctx) } CortexSettingsAction::ToggleTabTitleItalic => self.toggle_tab_title_italic(ctx), + CortexSettingsAction::ToggleAllowLocalClaudeCodexChildHarnesses => { + self.toggle_allow_local_claude_codex_child_harnesses(ctx) + } } } } @@ -628,8 +661,9 @@ impl BackingView for CortexSettingsView { #[allow(dead_code)] pub fn cortex_settings_search_terms() -> String { format!( - "cortex settings {} {}", + "cortex settings {} {} {}", working_panes_page_search_terms().join(" "), - tabs_page_search_terms().join(" ") + tabs_page_search_terms().join(" "), + ai_page_search_terms().join(" ") ) } diff --git a/app/src/settings/cortex.rs b/app/src/settings/cortex.rs index 0a89f091a0..d4aa2aa7f9 100644 --- a/app/src/settings/cortex.rs +++ b/app/src/settings/cortex.rs @@ -262,5 +262,14 @@ define_settings_group!(CortexSettings, settings: [ private: false, toml_path: "cortex.terminal.cli_agent_clear_scrolls_to_top", description: "When a running CLI agent (Claude Code, Codex, Cursor, Gemini) runs /clear, scroll the viewport so the agent's freshly-cleared UI sits at the top of the visible pane and the prior conversation remains in scrollback. Claude is wired via Cortex's OSC-777 → SessionEnd hook; other agents are detected when they emit ESC[2J. Off restores upstream Warp behavior.", + }, + allow_local_claude_codex_child_harnesses: AllowLocalClaudeCodexChildHarnesses { + type: bool, + default: true, + supported_platforms: SupportedPlatforms::ALL, + sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), + private: false, + toml_path: "cortex.ai.allow_local_claude_codex_child_harnesses", + description: "Whether /orchestrate's Local execution mode may spawn child agents using the Claude Code or Codex CLI harnesses instead of being limited to Oz. Upstream Warp keeps this gated behind FeatureFlag::LocalClaudeCodexChildHarnesses; Cortex hydrates that flag from this setting at startup and on each toggle, so checks at the existing call sites (local_child_harnesses.rs, orchestration_controls.rs) react without a restart. Default on — the whole point of the Cortex fork on this branch is to route /orchestrate children through your local Claude Code login.", } ]); diff --git a/app/src/settings/init.rs b/app/src/settings/init.rs index ba5f7e069d..8802c8e7cd 100644 --- a/app/src/settings/init.rs +++ b/app/src/settings/init.rs @@ -67,6 +67,20 @@ pub fn register_all_settings(ctx: &mut AppContext) { TerminalSettings::register(ctx); PaneSettings::register(ctx); CortexSettings::register(ctx); + // Cortex divergence — hydrate `FeatureFlag::LocalClaudeCodexChildHarnesses` + // from the persisted `CortexSettings.allow_local_claude_codex_child_harnesses` + // toggle so the gate sites in `local_child_harnesses.rs` and + // `orchestration_controls.rs` reflect the user's last choice from frame + // zero (without needing a runtime-feature-flags debug menu, and without + // forcing every gate site to be ported to read the setting directly). + // The Cortex Settings → AI page mirrors any subsequent toggle back into + // the flag, so this read is only needed for the cold-start path. + { + let allow_local_claude_codex_child_harnesses = + *CortexSettings::as_ref(ctx).allow_local_claude_codex_child_harnesses; + FeatureFlag::LocalClaudeCodexChildHarnesses + .set_user_preference(allow_local_claude_codex_child_harnesses); + } // Cortex divergence — surface a dev-build warning when the CLI-agent // `/clear` viewport pin is disabled in dev's settings.toml. This is // typically a debug-iteration leftover that silently disables the pin