From 97513990a0336c6ae658e24f616bb589f26d7028 Mon Sep 17 00:00:00 2001 From: Moira Huang Date: Fri, 22 May 2026 13:10:03 -0700 Subject: [PATCH 1/2] Use remote-aware skill locations in UI consumers Co-Authored-By: Oz --- app/src/ai/blocklist/block.rs | 2 +- .../ai/blocklist/block/view_impl/output.rs | 37 ++++++---- .../blocklist/inline_action/code_diff_view.rs | 69 +++++++++++-------- app/src/ai/skills/mod.rs | 2 +- app/src/ai/skills/skill_utils.rs | 17 +++++ app/src/terminal/input/slash_command_model.rs | 9 +-- .../input/slash_commands/data_source/mod.rs | 8 +-- .../slash_commands/data_source/zero_state.rs | 4 +- 8 files changed, 89 insertions(+), 59 deletions(-) diff --git a/app/src/ai/blocklist/block.rs b/app/src/ai/blocklist/block.rs index 2ee653fd52..710cb9436a 100644 --- a/app/src/ai/blocklist/block.rs +++ b/app/src/ai/blocklist/block.rs @@ -3074,7 +3074,7 @@ impl AIBlock { ctx.emit(AIBlockEvent::OpenCodeInWarp { source: CodeSource::Skill { reference: reference.clone(), - location: LocalOrRemotePath::Local(path.clone()), + location: path.clone(), origin: SkillOpenOrigin::EditFiles, }, layout: *crate::util::file::external_editor::EditorSettings::as_ref( diff --git a/app/src/ai/blocklist/block/view_impl/output.rs b/app/src/ai/blocklist/block/view_impl/output.rs index 7ed38350fa..870410f7c6 100644 --- a/app/src/ai/blocklist/block/view_impl/output.rs +++ b/app/src/ai/blocklist/block/view_impl/output.rs @@ -22,6 +22,7 @@ use pathfinder_geometry::vector::vec2f; use ui_components::{button, Component as _, Options as _}; use warp_core::channel::ChannelState; use warp_core::ui::theme::color::internal_colors; +use warp_util::local_or_remote_path::LocalOrRemotePath; #[allow(unused_imports)] use warp_util::path::{common_path, CleanPathResult}; use warpui::elements::new_scrollable::SingleAxisConfig; @@ -487,12 +488,14 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box { .collect_vec(), }; - let file_paths: Vec<_> = files.iter().map(|f| &f.name).collect(); - let skill = common_path(&file_paths) - .and_then(|common| skill_path_from_file_path(&common)) - .and_then(|skill_path| { - SkillManager::as_ref(app).skill_by_path(&skill_path) - }); + let file_paths = files.iter().map(|file| { + PathBuf::from(shell_native_absolute_path( + &file.name, + props.shell_launch_data, + props.current_working_directory, + )) + }); + let skill = parsed_skill_for_common_local_paths(file_paths, app); output_items.add_child(render_read_files( props, id, @@ -1491,13 +1494,9 @@ fn render_search_codebase( .render(app) .finish() } else { - let file_paths: Vec<_> = - files.iter().map(|f| &f.file_name).collect(); - let skill = common_path(&file_paths) - .and_then(|common| skill_path_from_file_path(&common)) - .and_then(|skill_path| { - SkillManager::as_ref(app).skill_by_path(&skill_path) - }); + let file_paths = + files.iter().map(|file| PathBuf::from(&file.file_name)); + let skill = parsed_skill_for_common_local_paths(file_paths, app); let grouped = group_file_contexts_for_display(files, None, None); return Some(render_read_files( props, @@ -1866,6 +1865,18 @@ fn render_read_files( renderable_action.render(app).finish() } +fn parsed_skill_for_common_local_paths( + file_paths: impl IntoIterator, + app: &AppContext, +) -> Option<&ai::skills::ParsedSkill> { + let file_paths = file_paths.into_iter().collect_vec(); + common_path(&file_paths) + .and_then(|common| skill_path_from_file_path(&common)) + .and_then(|skill_path| { + SkillManager::as_ref(app).skill_by_path(&LocalOrRemotePath::Local(skill_path)) + }) +} + fn maybe_render_edit_document( props: Props, id: &AIAgentActionId, diff --git a/app/src/ai/blocklist/inline_action/code_diff_view.rs b/app/src/ai/blocklist/inline_action/code_diff_view.rs index 1511118505..ab209c7ffb 100644 --- a/app/src/ai/blocklist/inline_action/code_diff_view.rs +++ b/app/src/ai/blocklist/inline_action/code_diff_view.rs @@ -25,7 +25,8 @@ use warp_core::HostId; use warp_editor::content::buffer::InitialBufferState; use warp_editor::render::element::VerticalExpansionBehavior; use warp_util::file::FileSaveError; -use warp_util::path::common_path; +use warp_util::local_or_remote_path::LocalOrRemotePath; +use warp_util::remote_path::RemotePath; use warp_util::standardized_path::StandardizedPath; use warpui::elements::new_scrollable::{ScrollableAppearance, SingleAxisConfig}; use warpui::elements::{ @@ -65,10 +66,9 @@ use crate::ai::mcp::{mcp_provider_from_file_path, MCPProvider}; use crate::ai::paths::host_native_absolute_path; use crate::ai::predict::prompt_suggestions::ACCEPT_PROMPT_SUGGESTION_KEYBINDING; use crate::ai::skills::{ - icon_override_for_skill_name, render_skill_button, skill_path_from_file_path, SkillManager, + icon_override_for_skill_name, render_skill_button, skill_path_from_location, SkillManager, SkillOpenOrigin, SkillReference, SkillTelemetryEvent, }; -use crate::code::buffer_location::LocalOrRemotePath; use crate::code::diff_viewer::{DiffViewer, DisplayMode}; use crate::code::editor::view::{CodeEditorEvent, CodeEditorRenderOptions, CodeEditorView}; use crate::code::editor::{add_color, remove_color}; @@ -248,7 +248,7 @@ pub enum CodeDiffViewEvent { /// Emitted when the user opens a skill file from a code diff OpenSkill { reference: SkillReference, - path: PathBuf, + path: LocalOrRemotePath, }, /// Emitted when the user opens an MCP config file from a code diff OpenMCPConfig { @@ -423,7 +423,7 @@ pub enum CodeDiffViewAction { RevertChanges, OpenSkill { reference: SkillReference, - path: PathBuf, + path: LocalOrRemotePath, mouse_state: MouseStateHandle, }, OpenMCPConfig { @@ -1572,30 +1572,35 @@ impl CodeDiffView { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_main_axis_size(MainAxisSize::Min); - let file_paths: Vec = self + let file_locations: Vec = self .pending_diffs .iter() .filter_map(|diff| { - diff.diff_view - .as_ref(app) - .file_path() - .and_then(|p| p.to_local_path()) + self.location_for_standardized_path(diff.diff_view.as_ref(app).file_path()?) }) .collect(); + let file_paths: Vec = file_locations + .iter() + .filter_map(|path| path.to_local_path().map(Path::to_path_buf)) + .collect(); // Renders the 'open skill' button if all edited files live in the same skill directory - let skill = common_path(&file_paths) - .and_then(|common| skill_path_from_file_path(&common)) - .and_then(|skill_path| SkillManager::as_ref(app).skill_by_path(&skill_path)); - if let Some((skill, skill_path)) = skill.and_then(|skill| { - skill - .path - .to_local_path() - .map(|path| (skill, path.to_path_buf())) - }) { + let skill_paths = file_locations + .iter() + .filter_map(skill_path_from_location) + .collect::>(); + let skill = skill_paths.first().and_then(|first_path| { + skill_paths + .iter() + .all(|path| path == first_path) + .then(|| SkillManager::as_ref(app).skill_by_path(first_path)) + .flatten() + }); + if let Some(skill) = skill { + let skill_path = skill.path.clone(); let skill_reference = SkillManager::handle(app) .as_ref(app) - .reference_for_skill_path(&skill.path); + .reference_for_skill_path(&skill_path); let skill_button_handle = self.button_mouse_states.skill_button_handle.clone(); let skill_icon_override = icon_override_for_skill_name(&skill.name); @@ -2594,17 +2599,21 @@ impl CodeDiffView { /// Returns the primary file location as a `LocalOrRemotePath`, /// using `diff_session_type` to correctly identify remote files. pub fn primary_file_location(&self, app: &AppContext) -> Option { - let path_str = self.primary_file_path(app)?; + self.pending_diffs + .first()? + .diff_view + .as_ref(app) + .file_path() + .and_then(|path| self.location_for_standardized_path(path)) + } + + fn location_for_standardized_path(&self, path: &StandardizedPath) -> Option { match &self.diff_session_type { - DiffSessionType::Local => Some(LocalOrRemotePath::Local(PathBuf::from(path_str))), - DiffSessionType::Remote(host_id) => { - StandardizedPath::try_new(&path_str).ok().map(|path| { - LocalOrRemotePath::Remote(warp_util::remote_path::RemotePath { - host_id: host_id.clone(), - path, - }) - }) - } + DiffSessionType::Local => path.to_local_path().map(LocalOrRemotePath::Local), + DiffSessionType::Remote(host_id) => Some(LocalOrRemotePath::Remote(RemotePath { + host_id: host_id.clone(), + path: path.clone(), + })), } } } diff --git a/app/src/ai/skills/mod.rs b/app/src/ai/skills/mod.rs index ee6c4edfcb..80cf5c9887 100644 --- a/app/src/ai/skills/mod.rs +++ b/app/src/ai/skills/mod.rs @@ -24,7 +24,7 @@ pub use listed_skill::SkillDescriptor; mod skill_utils; pub use skill_utils::{ icon_override_for_skill_name, list_skills_if_changed, render_skill_button, - skill_path_from_file_path, + skill_path_from_file_path, skill_path_from_location, }; pub trait SkillPathQuery { fn to_skill_location(&self) -> LocalOrRemotePath; diff --git a/app/src/ai/skills/skill_utils.rs b/app/src/ai/skills/skill_utils.rs index 6ae75eb132..657947c25e 100644 --- a/app/src/ai/skills/skill_utils.rs +++ b/app/src/ai/skills/skill_utils.rs @@ -164,6 +164,23 @@ pub fn icon_override_for_skill_name(name: &str) -> Option { } } +pub fn skill_path_from_location(location: &LocalOrRemotePath) -> Option { + let mut current = Some(location.clone()); + while let Some(candidate_skill_dir) = current { + if let Some(provider_dir) = candidate_skill_dir.parent() { + if SKILL_PROVIDER_DEFINITIONS.iter().any(|definition| { + provider_dir + .path_component() + .ends_with(&definition.skills_path.to_string_lossy()) + }) { + return Some(candidate_skill_dir.join("SKILL.md")); + } + } + current = candidate_skill_dir.parent(); + } + None +} + pub fn skill_path_from_file_path(file_path: &Path) -> Option { for definition in SKILL_PROVIDER_DEFINITIONS.iter() { let home_skill_dirs = if definition.provider == SkillProvider::Warp { diff --git a/app/src/terminal/input/slash_command_model.rs b/app/src/terminal/input/slash_command_model.rs index 438e1894f1..71f03fa543 100644 --- a/app/src/terminal/input/slash_command_model.rs +++ b/app/src/terminal/input/slash_command_model.rs @@ -1,9 +1,7 @@ use ai::skills::SkillReference; use input_classifier::InputType; use settings::Setting as _; -use std::path::PathBuf; use warp_core::features::FeatureFlag; -use warp_util::local_or_remote_path::LocalOrRemotePath; use warpui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity}; use crate::ai::blocklist::{ @@ -250,14 +248,13 @@ impl SlashCommandModel { let skill_name = possible_command.strip_prefix('/')?; - let cwd = self + let cwd_path = self .active_session .as_ref(ctx) - .current_working_directory() - .map(|cwd| LocalOrRemotePath::Local(PathBuf::from(cwd))); + .current_working_directory_location(ctx); let skills = SkillManager::handle(ctx) .as_ref(ctx) - .get_skills_for_working_directory(cwd.as_ref(), ctx); + .get_skills_for_working_directory(cwd_path.as_ref(), ctx); let matched_skill = skills.into_iter().find(|skill| skill.name == skill_name)?; diff --git a/app/src/terminal/input/slash_commands/data_source/mod.rs b/app/src/terminal/input/slash_commands/data_source/mod.rs index 10bcdc801e..f744b2751c 100644 --- a/app/src/terminal/input/slash_commands/data_source/mod.rs +++ b/app/src/terminal/input/slash_commands/data_source/mod.rs @@ -13,7 +13,6 @@ use warp_cli::agent::Harness; use warp_core::features::FeatureFlag; use warp_core::ui::appearance::Appearance; use warp_core::ui::Icon as WarpIcon; -use warp_util::local_or_remote_path::LocalOrRemotePath; use warpui::fonts::FamilyId; use warpui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity}; pub use zero_state::*; @@ -505,14 +504,13 @@ impl SyncDataSource for SlashCommandDataSource { // Skills are invoked by the agent, so they're hidden entirely when AI is globally off. if FeatureFlag::ListSkills.is_enabled() && AISettings::as_ref(app).is_any_ai_enabled(app) { let cli_agent_providers = self.active_cli_agent_providers(app); - let cwd = self + let cwd_path = self .active_session .as_ref(app) - .current_working_directory() - .map(|cwd| LocalOrRemotePath::Local(cwd.into())); + .current_working_directory_location(app); let skills = SkillManager::handle(app) .as_ref(app) - .get_skills_for_working_directory(cwd.as_ref(), app); + .get_skills_for_working_directory(cwd_path.as_ref(), app); let skill_manager = SkillManager::as_ref(app); for mut skill in skills { diff --git a/app/src/terminal/input/slash_commands/data_source/zero_state.rs b/app/src/terminal/input/slash_commands/data_source/zero_state.rs index 112d6acc1d..94f8315985 100644 --- a/app/src/terminal/input/slash_commands/data_source/zero_state.rs +++ b/app/src/terminal/input/slash_commands/data_source/zero_state.rs @@ -1,6 +1,5 @@ use itertools::Itertools; use warp_core::features::FeatureFlag; -use warp_util::local_or_remote_path::LocalOrRemotePath; use warpui::{Entity, ModelHandle, SingletonEntity}; use crate::ai::skills::SkillManager; @@ -109,8 +108,7 @@ impl SyncDataSource for ZeroStateDataSource { let cwd = slash_command_data_source .active_session_for_v2_zero_state() .as_ref(app) - .current_working_directory() - .map(|cwd| LocalOrRemotePath::Local(cwd.into())); + .current_working_directory_location(app); let skill_manager_handle = SkillManager::handle(app); let skill_manager = skill_manager_handle.as_ref(app); let skills = skill_manager.get_skills_for_working_directory(cwd.as_ref(), app); From 9152f13397c141258a11ca7c6d210c30f32ee057 Mon Sep 17 00:00:00 2001 From: Moira Huang Date: Fri, 29 May 2026 15:37:29 -0700 Subject: [PATCH 2/2] use file_locations --- .../blocklist/inline_action/code_diff_view.rs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/ai/blocklist/inline_action/code_diff_view.rs b/app/src/ai/blocklist/inline_action/code_diff_view.rs index ab209c7ffb..735778e76b 100644 --- a/app/src/ai/blocklist/inline_action/code_diff_view.rs +++ b/app/src/ai/blocklist/inline_action/code_diff_view.rs @@ -1579,17 +1579,14 @@ impl CodeDiffView { self.location_for_standardized_path(diff.diff_view.as_ref(app).file_path()?) }) .collect(); - let file_paths: Vec = file_locations - .iter() - .filter_map(|path| path.to_local_path().map(Path::to_path_buf)) - .collect(); - // Renders the 'open skill' button if all edited files live in the same skill directory + // Renders the 'open skill' button only if every edited file lives in the same skill directory. let skill_paths = file_locations .iter() - .filter_map(skill_path_from_location) - .collect::>(); - let skill = skill_paths.first().and_then(|first_path| { + .map(skill_path_from_location) + .collect::>>(); + let skill = skill_paths.and_then(|skill_paths| { + let first_path = skill_paths.first()?; skill_paths .iter() .all(|path| path == first_path) @@ -1628,7 +1625,12 @@ impl CodeDiffView { // Renders the 'open config' button only when every MCP config file in this diff // belongs to the same provider. Mixed-provider diffs (e.g. editing both a Claude // config and a Warp config at once) show no badge to avoid misleading attribution. - let mcp_configs: Vec<_> = file_paths + // MCP config actions currently operate on local paths only. + let local_file_paths: Vec = file_locations + .iter() + .filter_map(|path| path.to_local_path().map(Path::to_path_buf)) + .collect(); + let mcp_configs: Vec<_> = local_file_paths .iter() .filter_map(|path| { mcp_provider_from_file_path(path).map(|provider| (provider, path.to_path_buf()))