v1.4.0: Auto-compact streaming, Bedrock summarization support, subagent orchestration, and Galaxy rebrand continuation
Major features: - Auto-compact: triggers conversation summarization when context window >= 85%, compacts Bedrock message history to a summary pair, and tracks live context tokens - Bedrock summarization: plumbs `is_summarization` flag through translator/client/response pipeline, handles SummarizeConversation input type, and marks `summarized` in metadata - Session restore: rebuilds bedrock_message_history from persisted task messages via newly-public `convert_proto_message`, preventing empty history on reconnect - Subagent orchestration: adds SubagentQuestion/Answer/CompletionSummary event types, parent-child question routing with depth limits, retry counting, and drain methods - Summarization UI: inline SummarizationView in AI blocks with progress/finished states Refactors: - Rename WarpTheme → GalaxyTheme across ~100 files (rebrand continuation) - Rename warp_home_config_dir → galaxy_home_config_dir and related path functions - Predefined rules: replace "System Defined Rule #N" with descriptive names (e.g. "Correctness Over Speed", "Never Guess") and add lookup helpers - Usage view: replace cumulative input/output token display with live context tokens, cache hit rate calculation, and separate cache read/write stats - Telemetry: remove verbose doc comments, simplify trait definitions - Facts view: simplify delete permission check (always allow local deletion) - Remove warp_managed_paths_watcher.rs (dead code) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
eaa2ddc75e
commit
6f54e2cb30
@@ -0,0 +1,448 @@
|
||||
use dirs::home_dir;
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use std::{fs, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use galaxyui::ModelHandle;
|
||||
use galaxyui::{Entity, ModelContext, SingletonEntity};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use notify_debouncer_full::notify::{RecursiveMode, WatchFilter};
|
||||
use repo_metadata::RepositoryUpdate;
|
||||
#[cfg(any(not(target_family = "wasm"), test))]
|
||||
use repo_metadata::TargetFile;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use watcher::{BulkFilesystemWatcher, BulkFilesystemWatcherEvent};
|
||||
|
||||
/// Duration between filesystem watch events for the Warp managed paths watcher, in milliseconds.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
const GALAXY_MANAGED_PATHS_WATCHER_DEBOUNCE_MILLI_SECS: u64 = 500;
|
||||
|
||||
pub(crate) fn galaxy_data_dir() -> PathBuf {
|
||||
galaxy_core::paths::data_dir()
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub(crate) fn ensure_galaxy_watch_roots_exist() {}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub(crate) fn ensure_galaxy_watch_roots_exist() {
|
||||
let data_dir = galaxy_data_dir();
|
||||
if let Err(err) = fs::create_dir_all(&data_dir) {
|
||||
log::warn!(
|
||||
"Failed to create Galaxy data directory {}: {err}",
|
||||
data_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
let config_local_dir = galaxy_core::paths::config_local_dir();
|
||||
if config_local_dir != data_dir {
|
||||
if let Err(err) = fs::create_dir_all(&config_local_dir) {
|
||||
log::warn!(
|
||||
"Failed to create Galaxy config directory {}: {err}",
|
||||
config_local_dir.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
pub(crate) fn galaxy_home_config_dir() -> Option<PathBuf> {
|
||||
galaxy_core::paths::galaxy_home_config_dir()
|
||||
}
|
||||
|
||||
pub(crate) fn galaxy_home_skills_dir() -> Option<PathBuf> {
|
||||
galaxy_core::paths::galaxy_home_skills_dir()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
pub(crate) fn galaxy_home_mcp_config_file_path() -> Option<PathBuf> {
|
||||
galaxy_core::paths::galaxy_home_mcp_config_file_path()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct GalaxyMcpConfigPath {
|
||||
pub(crate) root_path: PathBuf,
|
||||
pub(crate) config_path: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn galaxy_managed_skill_dirs() -> Vec<PathBuf> {
|
||||
galaxy_home_skills_dir().into_iter().collect()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
pub(crate) fn galaxy_managed_mcp_config_path() -> Option<GalaxyMcpConfigPath> {
|
||||
Some(GalaxyMcpConfigPath {
|
||||
root_path: home_dir()?,
|
||||
config_path: galaxy_home_mcp_config_file_path()?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
pub(crate) fn repository_update_touches_path(update: &RepositoryUpdate, path: &Path) -> bool {
|
||||
repository_update_paths(update).any(|candidate| candidate == path)
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
pub(crate) fn repository_update_touches_prefix(update: &RepositoryUpdate, prefix: &Path) -> bool {
|
||||
repository_update_paths(update).any(|candidate| candidate.starts_with(prefix))
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
pub(crate) fn filter_repository_update_by_prefix(
|
||||
update: &RepositoryUpdate,
|
||||
prefix: &Path,
|
||||
) -> Option<RepositoryUpdate> {
|
||||
filter_repository_update(update, |path| path.starts_with(prefix))
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
fn repository_update_paths(update: &RepositoryUpdate) -> impl Iterator<Item = &Path> {
|
||||
update
|
||||
.added
|
||||
.iter()
|
||||
.map(|target| target.path.as_path())
|
||||
.chain(update.modified.iter().map(|target| target.path.as_path()))
|
||||
.chain(update.deleted.iter().map(|target| target.path.as_path()))
|
||||
.chain(update.moved.iter().flat_map(|(to_target, from_target)| {
|
||||
[to_target.path.as_path(), from_target.path.as_path()]
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
fn filter_repository_update(
|
||||
update: &RepositoryUpdate,
|
||||
keep_path: impl Fn(&Path) -> bool,
|
||||
) -> Option<RepositoryUpdate> {
|
||||
let mut filtered = RepositoryUpdate {
|
||||
commit_updated: update.commit_updated,
|
||||
index_lock_detected: update.index_lock_detected,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for target in &update.added {
|
||||
if keep_path(&target.path) {
|
||||
filtered.added.insert(target.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for target in &update.modified {
|
||||
if keep_path(&target.path) {
|
||||
filtered.modified.insert(target.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for target in &update.deleted {
|
||||
if keep_path(&target.path) {
|
||||
filtered.deleted.insert(target.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (to_target, from_target) in &update.moved {
|
||||
let keep_to = keep_path(&to_target.path);
|
||||
let keep_from = keep_path(&from_target.path);
|
||||
|
||||
match (keep_to, keep_from) {
|
||||
(true, true) => {
|
||||
filtered
|
||||
.moved
|
||||
.insert(to_target.clone(), from_target.clone());
|
||||
}
|
||||
(true, false) => {
|
||||
filtered.added.insert(to_target.clone());
|
||||
}
|
||||
(false, true) => {
|
||||
filtered.deleted.insert(from_target.clone());
|
||||
}
|
||||
(false, false) => {}
|
||||
}
|
||||
}
|
||||
|
||||
(!filtered.is_empty()).then_some(filtered)
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn filesystem_event_to_repository_update(event: &BulkFilesystemWatcherEvent) -> RepositoryUpdate {
|
||||
RepositoryUpdate {
|
||||
added: event
|
||||
.added
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| TargetFile::new(path, false))
|
||||
.collect(),
|
||||
modified: event
|
||||
.modified
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| TargetFile::new(path, false))
|
||||
.collect(),
|
||||
deleted: event
|
||||
.deleted
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| TargetFile::new(path, false))
|
||||
.collect(),
|
||||
moved: event
|
||||
.moved
|
||||
.iter()
|
||||
.map(|(to_path, from_path)| {
|
||||
(
|
||||
TargetFile::new(to_path.clone(), false),
|
||||
TargetFile::new(from_path.clone(), false),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
commit_updated: false,
|
||||
index_lock_detected: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum GalaxyManagedPathsWatcherEvent {}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub(crate) enum GalaxyManagedPathsWatcherEvent {
|
||||
FilesChanged(RepositoryUpdate),
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub(crate) struct GalaxyManagedPathsWatcher {
|
||||
_watcher: ModelHandle<BulkFilesystemWatcher>,
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub(crate) struct GalaxyManagedPathsWatcher;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl GalaxyManagedPathsWatcher {
|
||||
pub(crate) fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||
Self::new_internal(ctx, true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_for_testing(ctx: &mut ModelContext<Self>) -> Self {
|
||||
Self::new_internal(ctx, false)
|
||||
}
|
||||
|
||||
fn new_internal(ctx: &mut ModelContext<Self>, should_register_watcher: bool) -> Self {
|
||||
let watcher = if should_register_watcher {
|
||||
ctx.add_model(|ctx| {
|
||||
BulkFilesystemWatcher::new(
|
||||
Duration::from_millis(GALAXY_MANAGED_PATHS_WATCHER_DEBOUNCE_MILLI_SECS),
|
||||
ctx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
ctx.add_model(|_| BulkFilesystemWatcher::new_for_test())
|
||||
};
|
||||
ctx.subscribe_to_model(&watcher, Self::handle_fs_event);
|
||||
|
||||
if should_register_watcher {
|
||||
let data_dir = galaxy_data_dir();
|
||||
let config_local_dir = galaxy_core::paths::config_local_dir();
|
||||
let should_register_config_local_dir = config_local_dir != data_dir;
|
||||
let worktrees_dir = data_dir.join("worktrees");
|
||||
Self::register_path(
|
||||
ctx,
|
||||
&watcher,
|
||||
data_dir.clone(),
|
||||
WatchFilter::with_filter(Arc::new(move |path| !path.starts_with(&worktrees_dir))),
|
||||
RecursiveMode::Recursive,
|
||||
"Galaxy data directory",
|
||||
);
|
||||
if should_register_config_local_dir {
|
||||
Self::register_path(
|
||||
ctx,
|
||||
&watcher,
|
||||
config_local_dir.clone(),
|
||||
WatchFilter::accept_all(),
|
||||
RecursiveMode::Recursive,
|
||||
"Galaxy config directory",
|
||||
);
|
||||
}
|
||||
if let Some(galaxy_home_skills_dir) = galaxy_home_skills_dir() {
|
||||
if galaxy_home_skills_dir.exists()
|
||||
&& !galaxy_home_skills_dir.starts_with(&data_dir)
|
||||
&& (!should_register_config_local_dir
|
||||
|| !galaxy_home_skills_dir.starts_with(&config_local_dir))
|
||||
{
|
||||
Self::register_path(
|
||||
ctx,
|
||||
&watcher,
|
||||
galaxy_home_skills_dir,
|
||||
WatchFilter::accept_all(),
|
||||
RecursiveMode::Recursive,
|
||||
"Galaxy home skills directory",
|
||||
);
|
||||
}
|
||||
}
|
||||
if let (Some(galaxy_home_config_dir), Some(warp_home_mcp_config_path)) =
|
||||
(galaxy_home_config_dir(), galaxy_home_mcp_config_file_path())
|
||||
{
|
||||
if galaxy_home_config_dir.exists()
|
||||
&& !galaxy_home_config_dir.starts_with(&data_dir)
|
||||
&& (!should_register_config_local_dir
|
||||
|| !galaxy_home_config_dir.starts_with(&config_local_dir))
|
||||
{
|
||||
Self::register_path(
|
||||
ctx,
|
||||
&watcher,
|
||||
galaxy_home_config_dir,
|
||||
WatchFilter::with_filter(Arc::new(move |path| {
|
||||
path == warp_home_mcp_config_path
|
||||
})),
|
||||
RecursiveMode::NonRecursive,
|
||||
"Galaxy home MCP config directory",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self { _watcher: watcher }
|
||||
}
|
||||
|
||||
fn register_path(
|
||||
ctx: &mut ModelContext<Self>,
|
||||
watcher: &ModelHandle<BulkFilesystemWatcher>,
|
||||
directory_path: PathBuf,
|
||||
watch_filter: WatchFilter,
|
||||
recursive_mode: RecursiveMode,
|
||||
description: &'static str,
|
||||
) {
|
||||
let registration_path = directory_path.clone();
|
||||
let registration = watcher.update(ctx, |watcher, _ctx| {
|
||||
watcher.register_path(®istration_path, watch_filter, recursive_mode)
|
||||
});
|
||||
|
||||
ctx.spawn(registration, move |_, result, _ctx| {
|
||||
if let Err(err) = result {
|
||||
log::warn!(
|
||||
"Failed to start watching {description} {}: {err}",
|
||||
directory_path.display()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_fs_event(
|
||||
&mut self,
|
||||
event: &BulkFilesystemWatcherEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let update = filesystem_event_to_repository_update(event);
|
||||
if !update.is_empty() {
|
||||
ctx.emit(GalaxyManagedPathsWatcherEvent::FilesChanged(update));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
impl GalaxyManagedPathsWatcher {
|
||||
pub(crate) fn new(_ctx: &mut ModelContext<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_for_testing(_ctx: &mut ModelContext<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for GalaxyManagedPathsWatcher {
|
||||
type Event = GalaxyManagedPathsWatcherEvent;
|
||||
}
|
||||
|
||||
impl SingletonEntity for GalaxyManagedPathsWatcher {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use dirs::home_dir;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use repo_metadata::{RepositoryUpdate, TargetFile};
|
||||
|
||||
use super::{
|
||||
filter_repository_update_by_prefix, galaxy_home_mcp_config_file_path, galaxy_home_skills_dir,
|
||||
galaxy_managed_mcp_config_path, galaxy_managed_skill_dirs,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn galaxy_managed_skill_dirs_contains_only_warp_home_path() {
|
||||
let dirs = galaxy_managed_skill_dirs();
|
||||
match galaxy_home_skills_dir() {
|
||||
Some(galaxy_home_skills_dir) => assert_eq!(dirs, vec![galaxy_home_skills_dir]),
|
||||
None => assert!(dirs.is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn galaxy_managed_mcp_config_path_contains_only_warp_home_path() {
|
||||
match (
|
||||
home_dir(),
|
||||
galaxy_home_mcp_config_file_path(),
|
||||
galaxy_managed_mcp_config_path(),
|
||||
) {
|
||||
(Some(home_dir), Some(warp_home_mcp_config_path), Some(path)) => {
|
||||
assert_eq!(path.root_path, home_dir);
|
||||
assert_eq!(path.config_path, warp_home_mcp_config_path);
|
||||
}
|
||||
(_, _, None) => {}
|
||||
_ => panic!("Expected Warp MCP path when home directory is available"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_repository_update_by_prefix_keeps_only_matching_paths() {
|
||||
let skills_dir = PathBuf::from("/tmp/.warp-local/skills");
|
||||
let other_dir = PathBuf::from("/tmp/.warp-local/worktrees/repo");
|
||||
let skill_file = skills_dir.join("deploy").join("SKILL.md");
|
||||
let other_file = other_dir.join("README.md");
|
||||
|
||||
let update = RepositoryUpdate {
|
||||
added: HashSet::from([
|
||||
TargetFile::new(skill_file.clone(), false),
|
||||
TargetFile::new(other_file.clone(), false),
|
||||
]),
|
||||
modified: HashSet::new(),
|
||||
deleted: HashSet::new(),
|
||||
moved: HashMap::new(),
|
||||
commit_updated: false,
|
||||
index_lock_detected: false,
|
||||
};
|
||||
|
||||
let filtered =
|
||||
filter_repository_update_by_prefix(&update, &skills_dir).expect("expected update");
|
||||
|
||||
assert!(filtered.contains_added_or_modified(&TargetFile::new(skill_file, false)));
|
||||
assert!(!filtered.contains_added_or_modified(&TargetFile::new(other_file, false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_repository_update_by_prefix_converts_cross_boundary_moves() {
|
||||
let skills_dir = PathBuf::from("/tmp/.warp-local/skills");
|
||||
let skill_file = skills_dir.join("deploy").join("SKILL.md");
|
||||
let ignored_file = PathBuf::from("/tmp/.warp-local/worktrees/repo/SKILL.md");
|
||||
|
||||
let update = RepositoryUpdate {
|
||||
added: HashSet::new(),
|
||||
modified: HashSet::new(),
|
||||
deleted: HashSet::new(),
|
||||
moved: HashMap::from([(
|
||||
TargetFile::new(skill_file.clone(), false),
|
||||
TargetFile::new(ignored_file, false),
|
||||
)]),
|
||||
commit_updated: false,
|
||||
index_lock_detected: false,
|
||||
};
|
||||
|
||||
let filtered =
|
||||
filter_repository_update_by_prefix(&update, &skills_dir).expect("expected update");
|
||||
|
||||
assert!(filtered.contains_added_or_modified(&TargetFile::new(skill_file, false)));
|
||||
assert!(filtered.moved.is_empty());
|
||||
assert!(filtered.deleted.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user