Complete agent monitoring and Galaxy Control integration

- expose command-monitor conversations and preserve visible agent transcripts
- add bounded polling and a dedicated shell interrupt tool
- improve direct-provider images, skills, tool history, and usage handling
- package and brand Galaxy Control across releases, installers, persistence, and docs
This commit is contained in:
2026-07-29 15:04:58 -05:00
parent 100f1eff1c
commit dbfa8bcd48
172 changed files with 6357 additions and 3825 deletions
+13 -28
View File
@@ -22,7 +22,7 @@ use crate::settings::user_preferences_toml_file_path;
pub enum BundledSkillActivation {
/// Always active.
Always,
/// Active only when a specific Warp feature is enabled.
/// Active only when a specific Galaxy feature is enabled.
RequiresFeature(FeatureFlag),
/// Active only when a specific MCP server is running.
RequiresMcp(McpIntegration),
@@ -170,14 +170,14 @@ struct BundledSkillDefinition {
icon: Icon,
}
/// Skills bundled with Warp for a single host.
/// Skills bundled with Galaxy for a single host.
#[derive(Debug, Default)]
pub struct BundledSkill {
definitions: HashMap<String, BundledSkillDefinition>,
}
impl BundledSkill {
/// Detect all skill definitions bundled with Warp for the local host.
/// Detect all skill definitions bundled with Galaxy for the local host.
pub async fn detect() -> Self {
let Some(resources_dir) = galaxy_core::paths::bundled_resources_dir() else {
return Self::default();
@@ -336,7 +336,7 @@ impl BundledSkill {
}
}
/// Load skill definitions bundled with Warp.
/// Load skill definitions bundled with Galaxy.
async fn load_bundled_skill_definitions(
resources_dir: &Path,
) -> HashMap<String, BundledSkillDefinition> {
@@ -439,11 +439,8 @@ pub(crate) async fn read_bundled_skills(
/// Builds the context map for bundled skill variable substitution.
///
/// Supported variables:
/// - `{{warp_server_url}}` - The server root URL (e.g., `https://api.warp.dev`)
/// - `{{warp_cli_binary_name}}` - The CLI binary name (e.g., `warp` or `warp-cli`)
/// - `{{warpctrl_binary_name}}` - The channel-specific Warp Control command name
/// - `{{warpctrl_wrapper_path}}` - Path to the bundled Warp Control wrapper
/// - `{{warp_url_scheme}}` - The URL scheme (e.g., `warp`, `warpdev`, `warppreview`)
/// - `{{galaxyctrl_binary_name}}` - The channel-specific Galaxy Control command name
/// - `{{galaxyctrl_wrapper_path}}` - Path to the bundled Galaxy Control wrapper
/// - `{{settings_schema_path}}` - Path to the bundled JSON settings schema
/// - `{{skill_dir}}` - Path to the bundled skill's directory
/// - `{{settings_file_path}}` - Path to the user's settings TOML file
@@ -454,29 +451,17 @@ pub(crate) fn build_bundled_skill_context(
) -> HashMap<String, String> {
[
(
"warp_server_url".to_owned(),
ChannelState::server_root_url().into_owned(),
"galaxyctrl_binary_name".to_owned(),
ChannelState::channel().galaxyctrl_command_name().to_owned(),
),
(
"warp_cli_binary_name".to_owned(),
ChannelState::channel().cli_command_name().to_owned(),
),
(
"warpctrl_binary_name".to_owned(),
ChannelState::channel().warpctrl_command_name().to_owned(),
),
(
"warpctrl_wrapper_path".to_owned(),
"galaxyctrl_wrapper_path".to_owned(),
resources_dir
.join("bin")
.join(ChannelState::channel().warpctrl_command_name())
.join(ChannelState::channel().galaxyctrl_command_name())
.display()
.to_string(),
),
(
"warp_url_scheme".to_owned(),
ChannelState::url_scheme().to_owned(),
),
(
"settings_file_path".to_owned(),
user_preferences_toml_file_path().display().to_string(),
@@ -500,11 +485,11 @@ pub(crate) fn build_bundled_skill_context(
/// Returns the icon for a bundled skill, given its directory-based ID.
/// Skills with a known brand (e.g. `pr-comments` → GitHub) get a
/// branded icon; everything else falls back to the Warp logo.
/// branded icon; everything else falls back to the Galaxy logo.
pub(crate) fn icon_for_bundled_skill(skill_id: &str) -> Icon {
match skill_id {
"pr-comments" => Icon::Github,
_ => Icon::WarpLogoLight,
_ => Icon::GalaxyLogo,
}
}
@@ -520,7 +505,7 @@ pub(crate) fn activation_for_bundled_skill(
"modify-settings" => {
BundledSkillActivation::RequiresFile(resources_dir.join("settings_schema.json"))
}
"warpctrl" => BundledSkillActivation::RequiresFeature(FeatureFlag::WarpControlCli),
"galaxyctrl" => BundledSkillActivation::RequiresFeature(FeatureFlag::GalaxyControlCli),
_ => BundledSkillActivation::Always,
}
}
+6
View File
@@ -29,6 +29,12 @@ fn remote_content<'a>(bundled_skills: &'a BundledSkills, host_id: &HostId) -> Op
.map(|skill| skill.content.as_str())
}
#[test]
fn bundled_skill_icons_use_galaxy_brand_by_default() {
assert_eq!(icon_for_bundled_skill("galaxyctrl"), Icon::GalaxyLogo);
assert_eq!(icon_for_bundled_skill("pr-comments"), Icon::Github);
}
#[test]
fn local_and_remote_catalogs_are_isolated() {
let first_host_id = HostId::new("first-host".to_string());
+1 -1
View File
@@ -60,7 +60,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,
icon_override_for_skill_name, list_skills_for_request, render_skill_button,
skill_path_from_location,
};
pub trait SkillPathQuery {
+48 -61
View File
@@ -533,7 +533,7 @@ fn test_read_bundled_skills_with_variable_substitution() {
let resources_dir = temp_dir.path();
let skills_dir = resources_dir.join("bundled/skills");
// Create a test skill with variables
// Create a test skill with local variables.
let skill_dir = skills_dir.join("test-skill");
fs::create_dir_all(&skill_dir).unwrap();
let skill_file = skill_dir.join("SKILL.md");
@@ -544,8 +544,8 @@ name: test-skill
description: Test skill with variables
---
Run `{{galaxy_cli_binary_name}}` to connect to {{warp_server_url}}.
Use `{{warpctrl_binary_name}}` from {{warpctrl_wrapper_path}}.
Use `{{galaxyctrl_binary_name}}` from {{galaxyctrl_wrapper_path}}.
Read {{settings_schema_path}} when validating settings.
"#,
)
.unwrap();
@@ -555,17 +555,16 @@ Use `{{warpctrl_binary_name}}` from {{warpctrl_wrapper_path}}.
assert_eq!(skills.len(), 1);
let skill = skills.get("test-skill").unwrap();
let expected_cli = ChannelState::channel().cli_command_name();
let expected_url = ChannelState::server_root_url();
let expected_galaxyctrl = ChannelState::channel().galaxyctrl_command_name();
let expected_wrapper = resources_dir.join("bin").join(expected_galaxyctrl);
assert!(skill.content.contains(&format!(
"Run `{expected_cli}` to connect to {expected_url}."
)));
let expected_warpctrl = ChannelState::channel().warpctrl_command_name();
let expected_wrapper = resources_dir.join("bin").join(expected_warpctrl);
assert!(skill.content.contains(&format!(
"Use `{expected_warpctrl}` from {}.",
"Use `{expected_galaxyctrl}` from {}.",
expected_wrapper.display()
)));
assert!(skill.content.contains(&format!(
"Read {} when validating settings.",
resources_dir.join("settings_schema.json").display()
)));
}
#[test]
@@ -611,7 +610,7 @@ fn test_read_bundled_skills_preserves_other_content() {
let resources_dir = temp_dir.path();
let skills_dir = resources_dir.join("bundled/skills");
// Create a test skill with both warp and non-warp variables
// Create a test skill with both known and unknown variables.
let skill_dir = skills_dir.join("test-skill");
fs::create_dir_all(&skill_dir).unwrap();
let skill_file = skill_dir.join("SKILL.md");
@@ -622,7 +621,7 @@ name: test-skill
description: Test skill with mixed variables
---
Use {{other_var}}, {{galaxy_cli_binary_name}}, and {{skill_dir}} together.
Use {{other_var}}, {{galaxyctrl_binary_name}}, and {{skill_dir}} together.
"#,
)
.unwrap();
@@ -632,9 +631,9 @@ Use {{other_var}}, {{galaxy_cli_binary_name}}, and {{skill_dir}} together.
assert_eq!(skills.len(), 1);
let skill = skills.get("test-skill").unwrap();
let expected_cli = ChannelState::channel().cli_command_name();
let expected_control = ChannelState::channel().galaxyctrl_command_name();
assert!(skill.content.contains(&format!(
"Use {{{{other_var}}}}, {expected_cli}, and {} together.",
"Use {{{{other_var}}}}, {expected_control}, and {} together.",
skill_dir.display()
)));
}
@@ -675,14 +674,14 @@ fn test_build_bundled_skill_context() {
let skill_dir = resources_dir.join("bundled/skills/test-skill");
let context = build_bundled_skill_context(resources_dir, &skill_dir);
assert_eq!(context.len(), 9);
assert!(context.contains_key("warp_server_url"));
assert!(context.contains_key("galaxy_cli_binary_name"));
assert!(context.contains_key("warpctrl_binary_name"));
assert!(context.contains_key("warpctrl_wrapper_path"));
assert!(context.contains_key("warp_url_scheme"));
assert_eq!(context.len(), 6);
assert!(context.contains_key("galaxyctrl_binary_name"));
assert!(context.contains_key("galaxyctrl_wrapper_path"));
assert!(context.contains_key("settings_file_path"));
assert!(context.contains_key("keybindings_file_path"));
assert!(!context.contains_key("warp_server_url"));
assert!(!context.contains_key("warp_cli_binary_name"));
assert!(!context.contains_key("warp_url_scheme"));
assert_eq!(
context.get("settings_schema_path").unwrap(),
&resources_dir
@@ -696,29 +695,17 @@ fn test_build_bundled_skill_context() {
);
assert_eq!(
context.get("warp_server_url").unwrap(),
&ChannelState::server_root_url().to_string()
context.get("galaxyctrl_binary_name").unwrap(),
ChannelState::channel().galaxyctrl_command_name()
);
assert_eq!(
context.get("galaxy_cli_binary_name").unwrap(),
ChannelState::channel().cli_command_name()
);
assert_eq!(
context.get("warpctrl_binary_name").unwrap(),
ChannelState::channel().warpctrl_command_name()
);
assert_eq!(
context.get("warpctrl_wrapper_path").unwrap(),
context.get("galaxyctrl_wrapper_path").unwrap(),
&resources_dir
.join("bin")
.join(ChannelState::channel().warpctrl_command_name())
.join(ChannelState::channel().galaxyctrl_command_name())
.display()
.to_string()
);
assert_eq!(
context.get("warp_url_scheme").unwrap(),
ChannelState::url_scheme()
);
assert_eq!(
context.get("settings_file_path").unwrap(),
&crate::settings::user_preferences_toml_file_path()
@@ -1024,13 +1011,13 @@ fn feature_gated_bundled_skill_is_listed_only_when_enabled() {
app.add_singleton_model(WarpManagedPathsWatcher::new_for_testing);
let handle = app.add_singleton_model(SkillManager::new);
let bundled_skills_guard = FeatureFlag::BundledSkills.override_enabled(true);
let warp_control_cli = FeatureFlag::WarpControlCli.override_enabled(false);
let galaxy_control_cli = FeatureFlag::GalaxyControlCli.override_enabled(false);
handle.update(&mut app, |manager, _| {
manager.add_bundled_skill_for_testing(
"warpctrl",
bundled_test_skill("warpctrl", "Control Warp"),
BundledSkillActivation::RequiresFeature(FeatureFlag::WarpControlCli),
"galaxyctrl",
bundled_test_skill("galaxyctrl", "Control Galaxy"),
BundledSkillActivation::RequiresFeature(FeatureFlag::GalaxyControlCli),
);
manager.add_bundled_skill_for_testing(
"always",
@@ -1046,11 +1033,11 @@ fn feature_gated_bundled_skill_is_listed_only_when_enabled() {
.map(|skill| skill.name)
.collect::<HashSet<_>>()
});
assert!(!disabled_names.contains("warpctrl"));
assert!(!disabled_names.contains("galaxyctrl"));
assert!(disabled_names.contains("always"));
drop(warp_control_cli);
let warp_control_cli_enabled = FeatureFlag::WarpControlCli.override_enabled(true);
drop(galaxy_control_cli);
let galaxy_control_cli_enabled = FeatureFlag::GalaxyControlCli.override_enabled(true);
let enabled_names = handle.read(&app, |manager, ctx| {
manager
.get_skills_for_working_directory(None, ctx)
@@ -1058,36 +1045,36 @@ fn feature_gated_bundled_skill_is_listed_only_when_enabled() {
.map(|skill| skill.name)
.collect::<HashSet<_>>()
});
assert!(enabled_names.contains("warpctrl"));
assert!(enabled_names.contains("galaxyctrl"));
assert!(enabled_names.contains("always"));
drop(warp_control_cli_enabled);
drop(galaxy_control_cli_enabled);
drop(bundled_skills_guard);
});
}
#[test]
fn warp_control_bundled_skill_activations_track_warp_control_feature() {
fn galaxy_control_bundled_skill_activations_track_galaxy_control_feature() {
App::test((), |app| async move {
let settings = app.add_singleton_model(AISettings::new_with_defaults);
let warp_control_cli = FeatureFlag::WarpControlCli.override_enabled(false);
let activations = ["warpctrl"]
let galaxy_control_cli = FeatureFlag::GalaxyControlCli.override_enabled(false);
let activations = ["galaxyctrl"]
.map(|skill_id| activation_for_bundled_skill(skill_id, Path::new("/resources")));
for activation in &activations {
assert!(!settings.read(&app, |_, ctx| activation.is_enabled(ctx)));
}
drop(warp_control_cli);
let warp_control_cli_enabled = FeatureFlag::WarpControlCli.override_enabled(true);
drop(galaxy_control_cli);
let galaxy_control_cli_enabled = FeatureFlag::GalaxyControlCli.override_enabled(true);
for activation in &activations {
assert!(settings.read(&app, |_, ctx| activation.is_enabled(ctx)));
}
drop(warp_control_cli_enabled);
drop(galaxy_control_cli_enabled);
});
}
#[test]
fn warp_control_direct_read_respects_warp_control_feature() {
let reference = SkillReference::BundledSkillId("warpctrl".to_owned());
fn galaxy_control_direct_read_respects_galaxy_control_feature() {
let reference = SkillReference::BundledSkillId("galaxyctrl".to_owned());
App::test((), |mut app| async move {
app.add_singleton_model(DirectoryWatcher::new);
@@ -1097,13 +1084,13 @@ fn warp_control_direct_read_respects_warp_control_feature() {
app.add_singleton_model(HomeDirectoryWatcher::new_for_test);
app.add_singleton_model(WarpManagedPathsWatcher::new_for_testing);
let handle = app.add_singleton_model(SkillManager::new);
let warp_control_cli = FeatureFlag::WarpControlCli.override_enabled(false);
let galaxy_control_cli = FeatureFlag::GalaxyControlCli.override_enabled(false);
handle.update(&mut app, |manager, _| {
manager.add_bundled_skill_for_testing(
"warpctrl",
bundled_test_skill("warpctrl", "Control Warp"),
BundledSkillActivation::RequiresFeature(FeatureFlag::WarpControlCli),
"galaxyctrl",
bundled_test_skill("galaxyctrl", "Control Galaxy"),
BundledSkillActivation::RequiresFeature(FeatureFlag::GalaxyControlCli),
);
});
@@ -1114,12 +1101,12 @@ fn warp_control_direct_read_respects_warp_control_feature() {
.active_skill_by_reference(&reference, ctx)
.is_none()));
drop(warp_control_cli);
let warp_control_cli_enabled = FeatureFlag::WarpControlCli.override_enabled(true);
drop(galaxy_control_cli);
let galaxy_control_cli_enabled = FeatureFlag::GalaxyControlCli.override_enabled(true);
assert!(handle.read(&app, |manager, ctx| manager
.active_skill_by_reference(&reference, ctx)
.is_some()));
drop(warp_control_cli_enabled);
drop(galaxy_control_cli_enabled);
});
}
#[test]
+10 -36
View File
@@ -1,7 +1,7 @@
//! Utility functions for working with skills.
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use ai::skills::{
@@ -18,9 +18,7 @@ use warpui::prelude::MouseStateHandle;
use warpui::{AppContext, Element, EventContext, SingletonEntity};
use super::{SkillDescriptor, SkillManager};
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::blocklist::view_util::render_provider_icon_button;
use crate::ai::blocklist::BlocklistAIHistoryModel;
lazy_static! {
static ref CONTENT_HASHER: SipHasher = SipHasher::new_with_keys(0, 0);
@@ -110,45 +108,21 @@ pub(crate) fn unique_skills(
deduplicator.into_descriptors()
}
/// Returns the list of skills if they have changed since the last time we sent them to the server.
/// Skills are always included except when the current list matches the last list sent.
pub fn list_skills_if_changed(
/// Returns the current skill catalog for a model request.
///
/// Direct Bedrock and LiteLLM requests rebuild their system prompt on every
/// turn, so the complete catalog must be present on every request rather than
/// relying on hosted-server delta state.
pub fn list_skills_for_request(
working_directory: Option<&LocalOrRemotePath>,
path_origin: &SkillPathOrigin,
conversation_id: Option<AIConversationId>,
app: &AppContext,
) -> Option<Vec<SkillDescriptor>> {
let current_skills = SkillManager::as_ref(app).get_skills_for_working_directory_with_origin(
) -> Vec<SkillDescriptor> {
SkillManager::as_ref(app).get_skills_for_working_directory_with_origin(
working_directory,
path_origin,
app,
);
let previous_skills: Option<Vec<SkillDescriptor>> =
conversation_id.and_then(|conversation_id| {
let history_model = BlocklistAIHistoryModel::as_ref(app);
history_model
.conversation(&conversation_id)
.and_then(|conversation| conversation.latest_skills())
});
// If there are no previous skills, we consider the skills changed and push the current skills to the context
let skills_changed = previous_skills
.map(|previous_skills| {
let previous_skills_set: HashSet<SkillDescriptor> =
HashSet::from_iter(previous_skills.iter().cloned());
let current_skills_set: HashSet<SkillDescriptor> =
HashSet::from_iter(current_skills.iter().cloned());
previous_skills_set != current_skills_set
})
.unwrap_or(true);
if skills_changed {
Some(current_skills)
} else {
None
}
)
}
/// Renders an 'open skill' button for blocklist AI actions and the code diff view.