From dc1bee95b2b51a1bd4386257edeee527f8c0a200 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 04:33:09 +0000 Subject: [PATCH] feat(cortex-settings): AI section with /orchestrate Claude/Codex toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new "AI" section to Cortex Settings exposing the single toggle "Allow Claude Code / Codex as orchestrate child agents", which is the user-facing surface for FeatureFlag::LocalClaudeCodexChildHarnesses. Upstream gates that flag for local /orchestrate children; on this fork the whole point is to route them through the user's local Claude Code login, so the new setting defaults to on and hydrates the runtime flag at startup and on every toggle. The two existing gate sites (local_child_harnesses.rs, orchestration_controls.rs) react live via their existing is_enabled() checks — no restart needed. --- app/src/cortex_settings/action.rs | 11 ++- app/src/cortex_settings/ai_page.rs | 109 +++++++++++++++++++++++++++++ app/src/cortex_settings/mod.rs | 1 + app/src/cortex_settings/view.rs | 38 +++++++++- app/src/settings/cortex.rs | 9 +++ app/src/settings/init.rs | 14 ++++ 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 app/src/cortex_settings/ai_page.rs 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