Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
2 changes: 2 additions & 0 deletions app/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ fn enabled_features() -> HashSet<FeatureFlag> {
FeatureFlag::RemoteCodeReview,
#[cfg(feature = "custom_inference_endpoints")]
FeatureFlag::CustomInferenceEndpoints,
#[cfg(feature = "cli_agent_file_links")]
FeatureFlag::CliAgentFileLinks,
]);

flags
Expand Down
63 changes: 61 additions & 2 deletions app/src/settings_view/features_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -731,6 +731,7 @@ pub enum FeaturesPageAction {
ToggleSshWrapper,
ToggleSnackbar,
ToggleLinkTooltip,
ToggleCliAgentFileLinks,
ToggleCompletionsOpenWhileTyping,
ToggleCommandCorrections,
ToggleErrorUnderlining,
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")]
{
Expand Down Expand Up @@ -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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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 {}
Expand Down
9 changes: 9 additions & 0 deletions app/src/terminal/general_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tip>,
default: HashSet::new(),
Expand Down
23 changes: 21 additions & 2 deletions app/src/terminal/view/link_detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Item = WithinModel<grid_handler::PossiblePath>>,
Expand Down
90 changes: 89 additions & 1 deletion app/src/terminal/view_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions crates/warp_features/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<FeatureFlag>()] =
Expand Down Expand Up @@ -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).
Expand Down
16 changes: 14 additions & 2 deletions skills-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -71,14 +77,20 @@
"source": "warpdotdev/common-skills",
"sourceType": "github",
"skillPath": ".agents/skills/spec-driven-implementation/SKILL.md",
"computedHash": "e334d0f6f0e8fc39055314acad911f36d92d1919372b5e2973cc99d7f8c901b4"
"computedHash": "45793ca1e35b032ddfd2596f2e86fd6f6e938549373bfe4aeb74683486a179e4"
},
"update-skill": {
"source": "warpdotdev/common-skills",
"sourceType": "github",
"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",
Expand All @@ -89,7 +101,7 @@
"source": "warpdotdev/common-skills",
"sourceType": "github",
"skillPath": ".agents/skills/write-tech-spec/SKILL.md",
"computedHash": "3b5eb4ef021112d473984eca28412d372e87d9337ad5d9754f3ad3e01f94d39b"
"computedHash": "c7913bfd1ea2be7ce38d5beb7e923b96f5689f6145250af1d81b985e8be4a882"
}
}
}
Loading