Skip to content

Commit 2566f54

Browse files
authored
Enable async find on dogfood, add toggle for Preview/Stable (warpdotdev#11555)
## Description Surfaces an "Asynchronous find" toggle on the Features settings page so non-dogfood users can opt into this experimental feature, while keeping it force-enabled (no toggle) on dogfood channels. Behavior by channel: - **Dogfood (Local/Dev)**: `FeatureFlag::AsyncFind` is in `DOGFOOD_FLAGS`, so the feature is force-enabled and no toggle is shown. - **Preview/Stable**: the flag is off, so the toggle appears on the Features page with `default: false`. Users opt in via the toggle. The feature flag itself is the discriminator between these two modes — there's no separate channel-aware default baked into the setting. Promoting the feature through the normal flag-promotion ladder (add to `PREVIEW_FLAGS`, then `RELEASE_FLAGS`) automatically hides the toggle and force-enables the feature; no code changes here required. On channels where the toggle is visible, a small "BETA" chip is rendered next to the title to signal the experimental nature of the feature, styled to match the `TitleChip` pattern used elsewhere (e.g. MCP server cards). Concrete changes: - `crates/warp_features/src/lib.rs`: add `FeatureFlag::AsyncFind` to `DOGFOOD_FLAGS` so the feature is force-on for Local/Dev. - `app/src/terminal/settings.rs`: add the `async_find_enabled` setting (`terminal.async_find_enabled`) with `default: false`, and a `TerminalSettings::is_async_find_enabled()` helper that returns `FeatureFlag::AsyncFind.is_enabled() || *self.async_find_enabled`. - `app/src/terminal/find/model.rs`: route the async-find init through `TerminalSettings::is_async_find_enabled()` so callers don't need to track both the flag and the setting. - `app/src/settings_view/features_page.rs`: add the `AsyncFindWidget` to the Features page, gated by `!FeatureFlag::AsyncFind.is_enabled()` so it only renders on channels where the user can meaningfully toggle. Adds a free `render_beta_chip` helper for the inline "BETA" pill. ## Testing Confirmed via manual testing on Preview/Stable-like builds (flag off, toggle visible) and dogfood-like builds (flag on, toggle hidden, feature force-enabled). - [x] I have manually tested my changes locally with `./script/run` — toggle appears with the BETA chip on the Features page when the flag is off, is hidden when the flag is on, and async find engages/disengages with the setting. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-IMPROVEMENT: A new, faster implementation of find is now available as an opt-in setting under Features → Asynchronous find. This will help keep the UI responsive on large outputs.
1 parent 8f8ff4a commit 2566f54

7 files changed

Lines changed: 157 additions & 20 deletions

File tree

app/src/settings_view/features_page.rs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ use super::keybindings::KeyBindingModifyingState;
3636
#[cfg(feature = "local_tty")]
3737
use super::settings_page::render_sub_sub_header;
3838
use super::settings_page::{
39-
add_setting, build_reset_button, render_body_item, render_body_item_label,
40-
render_dropdown_item, render_dropdown_item_label, render_local_only_icon, AdditionalInfo,
41-
Category, LocalOnlyIconState, MatchData, PageType, SettingsPageMeta, SettingsPageViewHandle,
42-
SettingsWidget, ToggleState, CONTENT_FONT_SIZE, HEADER_PADDING, TOGGLE_BUTTON_RIGHT_PADDING,
39+
add_setting, build_reset_button, build_toggle_element, render_body_item,
40+
render_body_item_label, render_dropdown_item, render_dropdown_item_label,
41+
render_local_only_icon, AdditionalInfo, Category, LocalOnlyIconState, MatchData, PageType,
42+
SettingsPageMeta, SettingsPageViewHandle, SettingsWidget, ToggleState, CONTENT_FONT_SIZE,
43+
HEADER_PADDING, TOGGLE_BUTTON_RIGHT_PADDING,
4344
};
4445
use super::{
45-
features, flags, DisplayCount, SettingsAction, SettingsSection, ToggleSettingActionPair,
46+
features, flags, render_beta_chip, DisplayCount, SettingsAction, SettingsSection,
47+
ToggleSettingActionPair,
4648
};
4749
use crate::appearance::Appearance;
4850
use crate::default_terminal::DefaultTerminal;
@@ -94,8 +96,8 @@ use crate::terminal::session_settings::{
9496
SessionSettingsChangedEvent, ShouldConfirmCloseSession,
9597
};
9698
use crate::terminal::settings::{
97-
MaximumGridSize, ShowTerminalZeroStateBlock, TerminalSettings, TerminalSettingsChangedEvent,
98-
UseAudibleBell,
99+
AsyncFindEnabled, MaximumGridSize, ShowTerminalZeroStateBlock, TerminalSettings,
100+
TerminalSettingsChangedEvent, UseAudibleBell,
99101
};
100102
use crate::terminal::{BlockListSettings, SnackbarEnabled};
101103
use crate::undo_close::UndoCloseSettings;
@@ -559,6 +561,7 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
559561
#[derive(Clone, Debug)]
560562
pub enum FeaturesPageAction {
561563
ToggleCopyOnSelect,
564+
ToggleAsyncFind,
562565
ToggleNotifications,
563566
ToggleRestoreSession,
564567
ToggleAutocompleteSymbols,
@@ -1150,6 +1153,10 @@ impl FeaturesPageAction {
11501153
action: "ToggleAgentInAppNotifications".to_string(),
11511154
value: to_string(*AISettings::as_ref(ctx).show_agent_notifications),
11521155
},
1156+
Self::ToggleAsyncFind => TelemetryEvent::FeaturesPageAction {
1157+
action: "ToggleAsyncFind".to_string(),
1158+
value: to_string(*TerminalSettings::as_ref(ctx).async_find_enabled),
1159+
},
11531160
}
11541161
}
11551162
}
@@ -1905,6 +1912,13 @@ impl TypedActionView for FeaturesPageView {
19051912
default_terminal.make_warp_default(ctx);
19061913
});
19071914
}
1915+
ToggleAsyncFind => {
1916+
TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| {
1917+
report_if_error!(terminal_settings
1918+
.async_find_enabled
1919+
.toggle_and_save_value(ctx));
1920+
});
1921+
}
19081922
}
19091923

19101924
send_telemetry_from_ctx!(action.telemetry_event(ctx), ctx);
@@ -2509,6 +2523,11 @@ impl FeaturesPageView {
25092523
general_widgets.push(Box::new(DefaultTerminalWidget::default()));
25102524
}
25112525

2526+
// The widget is the opt-in surface for channels where `FeatureFlag::AsyncFind`
2527+
// is off. Channels with the flag on force the feature on and hide the toggle
2528+
// entirely; see `TerminalSettings::is_async_find_enabled`.
2529+
general_widgets.push(Box::new(AsyncFindWidget::default()));
2530+
25122531
let app_editor_settings = AppEditorSettings::as_ref(ctx);
25132532

25142533
let notifications_widgets: Vec<Box<dyn SettingsWidget<View = Self>>> =
@@ -7256,3 +7275,73 @@ impl SettingsWidget for GraphicsBackendWidget {
72567275
col.finish()
72577276
}
72587277
}
7278+
7279+
#[derive(Default)]
7280+
struct AsyncFindWidget {
7281+
switch_state: SwitchStateHandle,
7282+
}
7283+
7284+
impl SettingsWidget for AsyncFindWidget {
7285+
type View = FeaturesPageView;
7286+
7287+
fn search_terms(&self) -> &str {
7288+
"async asynchronous fast find search"
7289+
}
7290+
7291+
fn should_render(&self, _app: &AppContext) -> bool {
7292+
// Here, the feature flag being enabled means the feature is force-enabled,
7293+
// so we don't need to render the toggle.
7294+
!FeatureFlag::AsyncFind.is_enabled()
7295+
}
7296+
7297+
fn render(
7298+
&self,
7299+
view: &Self::View,
7300+
appearance: &Appearance,
7301+
app: &AppContext,
7302+
) -> Box<dyn Element> {
7303+
let ui_builder = appearance.ui_builder();
7304+
7305+
let label = render_body_item_label::<FeaturesPageAction>(
7306+
"Asynchronous find".into(),
7307+
None,
7308+
None,
7309+
LocalOnlyIconState::for_setting(
7310+
AsyncFindEnabled::storage_key(),
7311+
AsyncFindEnabled::sync_to_cloud(),
7312+
&mut view
7313+
.button_mouse_states
7314+
.local_only_icon_tooltip_states
7315+
.borrow_mut(),
7316+
app,
7317+
),
7318+
ToggleState::Enabled,
7319+
appearance,
7320+
);
7321+
7322+
let label_with_chip = Flex::row()
7323+
.with_cross_axis_alignment(CrossAxisAlignment::Center)
7324+
.with_child(label)
7325+
.with_child(render_beta_chip(appearance))
7326+
.finish();
7327+
7328+
let switch = ui_builder
7329+
.switch(self.switch_state.clone())
7330+
.check(*TerminalSettings::as_ref(app).async_find_enabled)
7331+
.build()
7332+
.on_click(move |ctx, _, _| {
7333+
ctx.dispatch_typed_action(FeaturesPageAction::ToggleAsyncFind);
7334+
})
7335+
.finish();
7336+
7337+
build_toggle_element(
7338+
label_with_chip,
7339+
switch,
7340+
appearance,
7341+
Some(
7342+
"Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs."
7343+
.into(),
7344+
),
7345+
)
7346+
}
7347+
}

app/src/settings_view/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,24 @@ pub(super) fn editor_text_colors(appearance: &Appearance) -> TextColors {
162162
}
163163
}
164164

165+
/// Small inline pill rendered next to a settings label to mark a feature as beta.
166+
/// Used for experimental features (i.e. AsyncFind) that are enabled for Friends of Warp (i.e. Dogfood/Preview) and toggleable by others.
167+
pub(super) fn render_beta_chip(appearance: &Appearance) -> Box<dyn Element> {
168+
let theme = appearance.theme();
169+
let chip_color = theme.sub_text_color(theme.surface_3()).into_solid();
170+
Container::new(
171+
Text::new_inline("BETA", appearance.ui_font_family(), 10.)
172+
.with_color(chip_color)
173+
.finish(),
174+
)
175+
.with_background(theme.surface_3())
176+
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(3.)))
177+
.with_horizontal_padding(4.)
178+
.with_vertical_padding(1.)
179+
.with_margin_left(8.)
180+
.finish()
181+
}
182+
165183
/// Renders a horizontal row of pill-shaped chips for model labels.
166184
/// Used by custom inference endpoint cards and the remove confirmation dialog.
167185
pub(super) fn render_model_chips(

app/src/terminal/find/model.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use parking_lot::FairMutex;
1818
use rich_content::FindableRichContentHandle;
1919
pub use rich_content::{FindableRichContentView, RichContentMatchId};
2020
use settings::Setting as _;
21-
use warp_core::features::FeatureFlag;
2221
use warpui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity, ViewHandle};
2322

2423
use crate::settings::InputModeSettings;
@@ -28,6 +27,7 @@ use crate::terminal::model::grid::grid_handler::GridHandler;
2827
use crate::terminal::model::index::Point;
2928
use crate::terminal::model::terminal_model::BlockIndex;
3029
use crate::terminal::model::TerminalModel;
30+
use crate::terminal::settings::TerminalSettings;
3131
use crate::view_components::find::{FindDirection, FindEvent, FindModel};
3232

3333
/// Pre-computed find data for rendering a single block.
@@ -188,7 +188,7 @@ pub struct TerminalFindModel {
188188
/// `true` if the find bar is open.
189189
is_find_bar_open: bool,
190190

191-
/// Controller for async find operations (used when AsyncFind feature flag is enabled).
191+
/// Controller for async find operations.
192192
pub(crate) async_find_controller: Option<AsyncFindController>,
193193
}
194194

@@ -237,8 +237,8 @@ impl FindModel for TerminalFindModel {
237237
}
238238

239239
impl TerminalFindModel {
240-
pub fn new(terminal_model: Arc<FairMutex<TerminalModel>>) -> Self {
241-
let async_find_controller = if FeatureFlag::AsyncFind.is_enabled() {
240+
pub fn new(terminal_model: Arc<FairMutex<TerminalModel>>, ctx: &AppContext) -> Self {
241+
let async_find_controller = if TerminalSettings::as_ref(ctx).is_async_find_enabled() {
242242
Some(AsyncFindController::new(terminal_model.clone()))
243243
} else {
244244
None

app/src/terminal/find/model/async_find_tests.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::terminal::model::grid::grid_handler::AbsolutePoint;
1919
use crate::terminal::model::index::Point;
2020
use crate::terminal::model::terminal_model::{BlockIndex, BlockSortDirection};
2121
use crate::terminal::model::TerminalModel;
22+
use crate::test_util::settings::initialize_settings_for_tests;
2223
use crate::view_components::find::FindDirection;
2324

2425
/// Helper to create an AbsoluteMatch at a given row with default column span.
@@ -43,6 +44,8 @@ fn make_match_at(row: u64, start_col: usize, end_col: usize) -> AbsoluteMatch {
4344
#[test]
4445
fn test_async_find_produces_same_results_as_sync_find() {
4546
App::test((), |mut app| async move {
47+
initialize_settings_for_tests(&mut app);
48+
4649
let mut mock_terminal_model = TerminalModel::mock(None, None);
4750
mock_terminal_model.simulate_block("foobar", "foo\r\nbar\r\n");
4851
mock_terminal_model.simulate_block("barbaz", "bar baz\r\n");
@@ -66,8 +69,8 @@ fn test_async_find_produces_same_results_as_sync_find() {
6669
});
6770

6871
// Run async find using TerminalFindModel.
69-
let test_model = app.add_model(|_| {
70-
let mut model = TerminalFindModel::new(terminal_model.clone());
72+
let test_model = app.add_model(|ctx| {
73+
let mut model = TerminalFindModel::new(terminal_model.clone(), ctx);
7174
if model.async_find_controller.is_none() {
7275
model.async_find_controller =
7376
Some(AsyncFindController::new(terminal_model.clone()));
@@ -170,14 +173,16 @@ fn test_async_find_produces_same_results_as_sync_find() {
170173
#[test]
171174
fn test_async_find_cancellation() {
172175
App::test((), |mut app| async move {
176+
initialize_settings_for_tests(&mut app);
177+
173178
let mut mock_terminal_model = TerminalModel::mock(None, None);
174179
// Create some blocks with content.
175180
mock_terminal_model.simulate_block("cmd1", "line1\r\nline2\r\n");
176181
mock_terminal_model.simulate_block("cmd2", "line3\r\nline4\r\n");
177182

178183
let terminal_model = Arc::new(FairMutex::new(mock_terminal_model));
179-
let test_model = app.add_model(|_| {
180-
let mut model = TerminalFindModel::new(terminal_model.clone());
184+
let test_model = app.add_model(|ctx| {
185+
let mut model = TerminalFindModel::new(terminal_model.clone(), ctx);
181186
if model.async_find_controller.is_none() {
182187
model.async_find_controller =
183188
Some(AsyncFindController::new(terminal_model.clone()));
@@ -250,11 +255,13 @@ fn test_async_find_cancellation() {
250255
#[test]
251256
fn test_message_processing_updates_state() {
252257
App::test((), |mut app| async move {
258+
initialize_settings_for_tests(&mut app);
259+
253260
let mock_terminal_model = TerminalModel::mock(None, None);
254261
let terminal_model = Arc::new(FairMutex::new(mock_terminal_model));
255262

256-
let test_model = app.add_model(|_| {
257-
let mut model = TerminalFindModel::new(terminal_model.clone());
263+
let test_model = app.add_model(|ctx| {
264+
let mut model = TerminalFindModel::new(terminal_model.clone(), ctx);
258265
let mut controller = AsyncFindController::new(terminal_model);
259266
// Manually set up state as if a find is in progress.
260267
controller.set_test_status(AsyncFindStatus::Scanning);
@@ -651,6 +658,8 @@ fn test_update_dirty_matches_clear_range() {
651658

652659
fn assert_async_focused_order_matches_sync(block_sort_direction: BlockSortDirection) {
653660
App::test((), |mut app| async move {
661+
initialize_settings_for_tests(&mut app);
662+
654663
let mut mock_terminal_model = TerminalModel::mock(None, None);
655664
mock_terminal_model.simulate_block(
656665
"ordtok command old ordtok",
@@ -690,8 +699,8 @@ fn assert_async_focused_order_matches_sync(block_sort_direction: BlockSortDirect
690699
.collect::<Vec<_>>()
691700
});
692701

693-
let test_model = app.add_model(|_| {
694-
let mut model = TerminalFindModel::new(terminal_model.clone());
702+
let test_model = app.add_model(|ctx| {
703+
let mut model = TerminalFindModel::new(terminal_model.clone(), ctx);
695704
if model.async_find_controller.is_none() {
696705
model.async_find_controller =
697706
Some(AsyncFindController::new(terminal_model.clone()));

app/src/terminal/settings.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use serde::{Deserialize, Serialize};
22
use settings::macros::define_settings_group;
33
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
4+
use warp_core::features::FeatureFlag;
45
use warpui::units::Pixels;
56
use warpui::{AppContext, SingletonEntity};
67

@@ -133,6 +134,18 @@ define_settings_group!(TerminalSettings, settings: [
133134
toml_path: "terminal.show_terminal_zero_state_block",
134135
description: "Whether to show the AI zero-state block in new terminal sessions.",
135136
},
137+
// Opt-in toggle for running terminal find on a background thread. Only consulted on
138+
// channels where `FeatureFlag::AsyncFind` is off; channels with the flag on force the
139+
// feature on and hide this toggle. See `is_async_find_enabled` for the composite check.
140+
async_find_enabled: AsyncFindEnabled {
141+
type: bool,
142+
default: false,
143+
supported_platforms: SupportedPlatforms::ALL,
144+
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
145+
private: false,
146+
toml_path: "experimental.async_find_enabled",
147+
description: "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs.",
148+
},
136149
]);
137150

138151
impl TerminalSettings {
@@ -150,6 +163,13 @@ impl TerminalSettings {
150163
*self.show_terminal_zero_state_block && AISettings::as_ref(ctx).is_any_ai_enabled(ctx)
151164
}
152165

166+
/// Whether asynchronous terminal find should be used. On channels where
167+
/// `FeatureFlag::AsyncFind` is on, the feature is force-enabled (no toggle shown).
168+
/// On other channels, users opt in via the `async_find_enabled` setting.
169+
pub fn is_async_find_enabled(&self) -> bool {
170+
FeatureFlag::AsyncFind.is_enabled() || *self.async_find_enabled
171+
}
172+
153173
/// Spacing for the input box.
154174
pub fn terminal_input_spacing(
155175
&self,

app/src/terminal/view.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3487,7 +3487,7 @@ impl TerminalView {
34873487
legacy: legacy_passive_suggestions_model,
34883488
};
34893489

3490-
let find_model = ctx.add_model(|_| TerminalFindModel::new(model.clone()));
3490+
let find_model = ctx.add_model(|ctx| TerminalFindModel::new(model.clone(), ctx));
34913491

34923492
ctx.subscribe_to_model(
34933493
&TerminalSettings::handle(ctx),

crates/warp_features/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,7 @@ pub const DOGFOOD_FLAGS: &[FeatureFlag] = &[
944944
FeatureFlag::DragTabsToWindows,
945945
FeatureFlag::RemoteCodebaseIndexing,
946946
FeatureFlag::GroupedTabs,
947+
FeatureFlag::AsyncFind,
947948
];
948949

949950
/// Features enabled for feature preview build users (e.g.: Friends of Warp).

0 commit comments

Comments
 (0)