diff --git a/app/Cargo.toml b/app/Cargo.toml index d8ad94ae5b..57306f38b2 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -975,6 +975,7 @@ vertical_tabs = [] vertical_tabs_summary_mode = [] tab_configs = [] grouped_tabs = [] +cli_agent_file_links = [] agent_harness = [] oz_handoff = [] handoff_local_cloud = [] diff --git a/app/src/features.rs b/app/src/features.rs index c27cff3c67..4837065031 100644 --- a/app/src/features.rs +++ b/app/src/features.rs @@ -493,6 +493,8 @@ fn enabled_features() -> HashSet { FeatureFlag::RemoteCodeReview, #[cfg(feature = "custom_inference_endpoints")] FeatureFlag::CustomInferenceEndpoints, + #[cfg(feature = "cli_agent_file_links")] + FeatureFlag::CliAgentFileLinks, ]); flags diff --git a/app/src/settings_view/features_page.rs b/app/src/settings_view/features_page.rs index 9d770a37fe..abab330ba3 100644 --- a/app/src/settings_view/features_page.rs +++ b/app/src/settings_view/features_page.rs @@ -80,8 +80,8 @@ use crate::terminal::alt_screen_reporting::{ AltScreenReporting, FocusReportingEnabled, MouseReportingEnabled, ScrollReportingEnabled, }; use crate::terminal::general_settings::{ - AutoOpenCodeReviewPaneOnFirstAgentChange, GeneralSettings, LinkTooltip, LoginItem, - QuitOnLastWindowClosed, RestoreSession, ShowWarningBeforeQuitting, + AutoOpenCodeReviewPaneOnFirstAgentChange, CliAgentFileLinks, GeneralSettings, LinkTooltip, + LoginItem, QuitOnLastWindowClosed, RestoreSession, ShowWarningBeforeQuitting, }; use crate::terminal::input::OPEN_COMPLETIONS_KEYBINDING_NAME; use crate::terminal::keys_settings::{ @@ -731,6 +731,7 @@ pub enum FeaturesPageAction { ToggleSshWrapper, ToggleSnackbar, ToggleLinkTooltip, + ToggleCliAgentFileLinks, ToggleCompletionsOpenWhileTyping, ToggleCommandCorrections, ToggleErrorUnderlining, @@ -929,6 +930,10 @@ impl FeaturesPageAction { action: "ToggleLinkTooltip".to_string(), value: to_string(*GeneralSettings::as_ref(ctx).link_tooltip), }, + Self::ToggleCliAgentFileLinks => TelemetryEvent::FeaturesPageAction { + action: "ToggleCliAgentFileLinks".to_string(), + value: to_string(*GeneralSettings::as_ref(ctx).cli_agent_file_links), + }, Self::ToggleCompletionsOpenWhileTyping => TelemetryEvent::FeaturesPageAction { action: "ToggleCompletionsOpenWhileTyping".to_string(), value: to_string(*input_settings.completions_open_while_typing.value()), @@ -1844,6 +1849,11 @@ impl TypedActionView for FeaturesPageView { report_if_error!(settings.link_tooltip.toggle_and_save_value(ctx)); }); } + ToggleCliAgentFileLinks => { + GeneralSettings::handle(ctx).update(ctx, |settings, ctx| { + report_if_error!(settings.cli_agent_file_links.toggle_and_save_value(ctx)); + }); + } ToggleShowWarningBeforeQuitting => { GeneralSettings::handle(ctx).update(ctx, |warning_settings, ctx| { report_if_error!(warning_settings @@ -2648,6 +2658,9 @@ impl FeaturesPageView { general_widgets.push(Box::new(SnackbarHeaderWidget::default())); general_widgets.push(Box::new(LinkTooltipWidget::default())); + if FeatureFlag::CliAgentFileLinks.is_enabled() { + general_widgets.push(Box::new(CliAgentFileLinksWidget::default())); + } #[cfg(feature = "local_fs")] { @@ -4667,6 +4680,52 @@ impl SettingsWidget for LinkTooltipWidget { } } +#[derive(Default)] +struct CliAgentFileLinksWidget { + switch_state: SwitchStateHandle, +} + +impl SettingsWidget for CliAgentFileLinksWidget { + type View = FeaturesPageView; + + fn search_terms(&self) -> &str { + "agent cli file path link claude codex open" + } + + fn render( + &self, + view: &Self::View, + appearance: &Appearance, + app: &AppContext, + ) -> Box { + let ui_builder = appearance.ui_builder(); + render_body_item::( + "Detect file paths in CLI agent output".into(), + None, + LocalOnlyIconState::for_setting( + CliAgentFileLinks::storage_key(), + CliAgentFileLinks::sync_to_cloud(), + &mut view + .button_mouse_states + .local_only_icon_tooltip_states + .borrow_mut(), + app, + ), + ToggleState::Enabled, + appearance, + ui_builder + .switch(self.switch_state.clone()) + .check(*GeneralSettings::as_ref(app).cli_agent_file_links) + .build() + .on_click(move |ctx, _, _| { + ctx.dispatch_typed_action(FeaturesPageAction::ToggleCliAgentFileLinks); + }) + .finish(), + None, + ) + } +} + #[cfg(feature = "local_fs")] #[derive(Default)] struct ExternalEditorWidget {} diff --git a/app/src/terminal/general_settings.rs b/app/src/terminal/general_settings.rs index bd47567b2b..3ac9fd4d5a 100644 --- a/app/src/terminal/general_settings.rs +++ b/app/src/terminal/general_settings.rs @@ -70,6 +70,15 @@ define_settings_group!(GeneralSettings, settings: [ toml_path: "general.link_tooltip", description: "Whether to show a tooltip when hovering over links.", }, + cli_agent_file_links: CliAgentFileLinks { + type: bool, + default: true, + supported_platforms: SupportedPlatforms::ALL, + sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes), + private: false, + toml_path: "general.cli_agent_file_links", + description: "Whether to detect clickable file paths in CLI agent output (Claude Code, Codex, etc.).", + }, welcome_tips_features_used: WelcomeTipsFeaturesUsed { type: HashSet, default: HashSet::new(), diff --git a/app/src/terminal/view/link_detection.rs b/app/src/terminal/view/link_detection.rs index 9d3f1401d7..c6681db451 100644 --- a/app/src/terminal/view/link_detection.rs +++ b/app/src/terminal/view/link_detection.rs @@ -23,6 +23,9 @@ cfg_if::cfg_if! { use std::path::PathBuf; use warp_util::path::CleanPathResult; use warp_util::path::LineAndColumnArg; + use warpui::{AppContext, SingletonEntity}; + use crate::features::FeatureFlag; + use crate::terminal::general_settings::GeneralSettings; } } @@ -450,8 +453,13 @@ impl super::TerminalView { match pwd_to_scan_for { // Check if we are hovering on any file path. Don't scan for file path - // if user is hovering from an editor like vim or nano. - Some(path) if matches!(from_editor, TerminalEditor::No) => { + // if user is hovering from an editor like vim or nano -- unless a CLI + // agent TUI (Claude Code, Codex, etc.) is running, which we still want + // to linkify even though it enables SGR mouse reporting like an editor. + Some(path) + if matches!(from_editor, TerminalEditor::No) + || self.should_detect_cli_agent_file_links(ctx) => + { let possible_paths = self.model.lock().possible_file_paths_at_point(position); let max_columns = self.size_info.columns; let shell_launch_data = self @@ -491,6 +499,17 @@ impl super::TerminalView { }; } + /// True when file-path links should be detected for an on-screen CLI agent TUI + /// (Claude, Codex, etc.). Re-enables detection on the alt screen even though agent + /// TUIs enable SGR mouse reporting (which we otherwise treat as a vim/nano-style + /// editor). Gated by both the rollout feature flag and the user setting so it can + /// be staged and individually disabled. + pub(crate) fn should_detect_cli_agent_file_links(&self, ctx: &AppContext) -> bool { + FeatureFlag::CliAgentFileLinks.is_enabled() + && *GeneralSettings::as_ref(ctx).cli_agent_file_links + && self.has_active_cli_agent_session(ctx) + } + fn compute_valid_paths( working_directory: &str, possible_paths: impl Iterator>, diff --git a/app/src/terminal/view_tests.rs b/app/src/terminal/view_tests.rs index ae479ff8be..4455951b0f 100644 --- a/app/src/terminal/view_tests.rs +++ b/app/src/terminal/view_tests.rs @@ -13,7 +13,7 @@ use warp_cli::agent::Harness; use warp_terminal::model::escape_sequences::{BRACKETED_PASTE_END, BRACKETED_PASTE_START, C0}; use warpui::notification::UserNotification; use warpui::platform::WindowStyle; -use warpui::{App, Presenter, ReadModel, WindowInvalidation}; +use warpui::{App, EntityId, Presenter, ReadModel, WindowInvalidation}; use super::*; use crate::ai::agent::conversation::ConversationStatus; @@ -450,6 +450,94 @@ fn submit_cli_agent_rich_input_restores_unlocked_input_config() { }) } +/// Verifies the gating predicate that re-enables alt-screen file-path link detection +/// for CLI agent TUIs (Claude Code, Codex, etc.). Detection should turn on only when +/// the feature flag is enabled, the user setting is on, AND a CLI agent session is +/// tracked for the view -- so real editors (vim/nano) and opt-out users are unaffected. +#[cfg(feature = "local_fs")] +#[test] +fn alt_screen_file_links_gated_by_flag_setting_and_cli_agent_session() { + App::test((), |mut app| async move { + initialize_app_for_terminal_view(&mut app); + let terminal = add_window_with_terminal(&mut app, None); + + fn set_session(app: &mut App, view_id: EntityId, agent: CLIAgent) { + CLIAgentSessionsModel::handle(app).update(app, |sessions, ctx| { + sessions.set_session( + view_id, + CLIAgentSession { + agent, + status: CLIAgentSessionStatus::InProgress, + session_context: CLIAgentSessionContext::default(), + input_state: CLIAgentInputState::Closed, + should_auto_toggle_input: false, + listener: None, + remote_host: None, + plugin_version: None, + draft_text: None, + custom_command_prefix: None, + }, + ctx, + ); + }); + } + + // Flag enabled + setting on (default) + NO session -> not detected (vim/nano case). + terminal.update(&mut app, |view, ctx| { + let _flag = FeatureFlag::CliAgentFileLinks.override_enabled(true); + assert!( + !view.should_detect_cli_agent_file_links(ctx), + "should not detect without a tracked CLI agent session" + ); + }); + + // Flag enabled + setting on + Claude session -> detected. + let view_id = terminal.read(&app, |view, _| view.view_id); + set_session(&mut app, view_id, CLIAgent::Claude); + terminal.update(&mut app, |view, ctx| { + let _flag = FeatureFlag::CliAgentFileLinks.override_enabled(true); + assert!( + view.should_detect_cli_agent_file_links(ctx), + "should detect with flag on, setting on, and a Claude session" + ); + }); + + // Flag DISABLED + session present -> not detected (rollout gate). + terminal.update(&mut app, |view, ctx| { + let _flag = FeatureFlag::CliAgentFileLinks.override_enabled(false); + assert!( + !view.should_detect_cli_agent_file_links(ctx), + "should not detect when the feature flag is disabled" + ); + }); + + // Flag enabled + setting OFF + session present -> not detected (user opt-out). + crate::terminal::general_settings::GeneralSettings::handle(&app).update(&mut app, |settings, ctx| { + let _ = settings.cli_agent_file_links.set_value(false, ctx); + }); + terminal.update(&mut app, |view, ctx| { + let _flag = FeatureFlag::CliAgentFileLinks.override_enabled(true); + assert!( + !view.should_detect_cli_agent_file_links(ctx), + "should not detect when the user setting is turned off" + ); + }); + + // Setting back on + Codex session -> detected (multi-agent coverage). + crate::terminal::general_settings::GeneralSettings::handle(&app).update(&mut app, |settings, ctx| { + let _ = settings.cli_agent_file_links.set_value(true, ctx); + }); + set_session(&mut app, view_id, CLIAgent::Codex); + terminal.update(&mut app, |view, ctx| { + let _flag = FeatureFlag::CliAgentFileLinks.override_enabled(true); + assert!( + view.should_detect_cli_agent_file_links(ctx), + "should detect with a Codex session too" + ); + }); + }) +} + #[test] fn unregister_cli_agent_session_restores_unlocked_input_config() { App::test((), |mut app| async move { diff --git a/crates/warp_features/src/lib.rs b/crates/warp_features/src/lib.rs index 25aed3b5fc..f4401e381c 100644 --- a/crates/warp_features/src/lib.rs +++ b/crates/warp_features/src/lib.rs @@ -886,6 +886,11 @@ pub enum FeatureFlag { /// Gates the Grouped Tabs feature. GroupedTabs, + + /// Enables file-path link detection (hover tooltip + Cmd+Click to open in Warp) inside + /// CLI agent TUIs like Claude Code and Codex, which run on the alternate screen and would + /// otherwise be treated as vim/nano-style editors where file links are suppressed. + CliAgentFileLinks, } static FLAG_STATES: [AtomicBool; cardinality::()] = @@ -953,6 +958,7 @@ pub const DOGFOOD_FLAGS: &[FeatureFlag] = &[ FeatureFlag::GroupedTabs, FeatureFlag::AsyncFind, FeatureFlag::OrchestrationViewerStreamer, + FeatureFlag::CliAgentFileLinks, ]; /// Features enabled for feature preview build users (e.g.: Friends of Warp). diff --git a/skills-lock.json b/skills-lock.json index 02dd8bde28..800b703b99 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -61,6 +61,12 @@ "skillPath": ".agents/skills/resolve-merge-conflicts/SKILL.md", "computedHash": "5376b5692901c624e8f20a5a04aeea5f5a94f5168d29852a8a639aded6408f2e" }, + "respond-to-pr-comments-in-blocklist": { + "source": "warpdotdev/common-skills", + "sourceType": "github", + "skillPath": ".agents/skills/respond-to-pr-comments-in-blocklist/SKILL.md", + "computedHash": "f7408cf90c10397aa9048f14ab985a138641fc1e5f3245e290150437d62875f0" + }, "review-pr": { "source": "warpdotdev/common-skills", "sourceType": "github", @@ -71,7 +77,7 @@ "source": "warpdotdev/common-skills", "sourceType": "github", "skillPath": ".agents/skills/spec-driven-implementation/SKILL.md", - "computedHash": "e334d0f6f0e8fc39055314acad911f36d92d1919372b5e2973cc99d7f8c901b4" + "computedHash": "45793ca1e35b032ddfd2596f2e86fd6f6e938549373bfe4aeb74683486a179e4" }, "update-skill": { "source": "warpdotdev/common-skills", @@ -79,6 +85,12 @@ "skillPath": ".agents/skills/update-skill/SKILL.md", "computedHash": "1e23c5a5c37ed084eced7fa507031e3cdb8e23f09cd5d004e00efd6f66bf200f" }, + "validate-changes-match-specs": { + "source": "warpdotdev/common-skills", + "sourceType": "github", + "skillPath": ".agents/skills/validate-changes-match-specs/SKILL.md", + "computedHash": "9123fd70ced064bdd773fd8d2baa8d5d5291fef910eb2c028084074b3ac72c27" + }, "write-product-spec": { "source": "warpdotdev/common-skills", "sourceType": "github", @@ -89,7 +101,7 @@ "source": "warpdotdev/common-skills", "sourceType": "github", "skillPath": ".agents/skills/write-tech-spec/SKILL.md", - "computedHash": "3b5eb4ef021112d473984eca28412d372e87d9337ad5d9754f3ad3e01f94d39b" + "computedHash": "c7913bfd1ea2be7ce38d5beb7e923b96f5689f6145250af1d81b985e8be4a882" } } }