Files
galaxy/specs/APP-3945/TECH.md
T

10 KiB

APP-3945: Channel-aware Warp home watching Technical Spec

Problem

Warp's hot-reload behavior for user-managed files depends on several consumers observing filesystem changes from the current channel's Warp home. This work centralizes those updates behind WarpDataDirectoryWatcher, but it must also preserve a platform-specific requirement: public settings live under config_local_dir() while most other Warp home content lives under data_dir().

The technical problem is to keep one Warp-specific watcher abstraction while:

  • filtering .warp*/worktrees only for the data-directory tree
  • watching config_local_dir() in addition to data_dir() when those roots differ
  • preserving the existing downstream event contracts used by WarpConfig, Warp home MCP watching, and Warp home skill watching
  • avoiding new generic filtering APIs in repo_metadata::DirectoryWatcher

Relevant code

  • app/src/warp_data_directory_watcher.rs — Warp-specific singleton watcher that owns a BulkFilesystemWatcher, registers Warp home roots, and emits WarpDataDirectoryWatcherEvent::FilesChanged.
  • app/src/lib.rs — startup wiring that prepares the Warp watch roots before registering WarpDataDirectoryWatcher.
  • app/src/user_config/native.rsWarpConfig subscription that maps watcher updates to Themes, Workflows, LaunchConfigs, TabConfigs, and Settings events.
  • app/src/settings/init.rs — settings-file hot-reload pipeline that reacts to WarpConfigUpdateEvent::Settings.
  • app/src/ai/mcp/file_mcp_watcher.rs — Warp home MCP watcher subscriber that depends on WarpDataDirectoryWatcher.
  • app/src/ai/mcp/mod.rs — helper that resolves the Warp MCP home config path through the shared Warp data directory helpers.
  • app/src/ai/skills/file_watchers/skill_watcher.rs — Warp home skill watcher subscriber that depends on WarpDataDirectoryWatcher.
  • app/src/ai/skills/file_watchers/utils.rs — skill path parsing helpers, including the Warp-home special case.
  • app/src/ai/skills/skill_utils.rs — helper for resolving a skill root from a changed file path.
  • crates/ai/src/skills/skill_provider.rs — provider/scope classification for channel-aware Warp home skill paths.
  • crates/integration/src/test/settings_file_hot_reload.rs — end-to-end settings hot-reload coverage.

Current state

WarpDataDirectoryWatcher is a Warp-specific singleton that owns its own BulkFilesystemWatcher, similar to HomeDirectoryWatcher. It does not depend on DirectoryWatcher, and it does not require any per-directory filter plumbing in repo_metadata.

At startup:

  • setup code prepares data_dir() before WarpDataDirectoryWatcher is registered
  • WarpDataDirectoryWatcher registers data_dir() recursively with a WatchFilter that excludes <data_dir>/worktrees
  • setup code prepares config_local_dir() when it differs from data_dir()
  • WarpDataDirectoryWatcher registers config_local_dir() recursively when it differs from data_dir()

The watcher receives BulkFilesystemWatcherEvent values, converts them into RepositoryUpdate, and emits WarpDataDirectoryWatcherEvent::FilesChanged directly. It does not apply a second worktree filter after registration-time filtering.

WarpConfig subscribes to that event stream and reloads themes, workflows, launch configs, and tab configs on background tasks when the update touches the relevant paths. Settings continue to flow through WarpConfigUpdateEvent::Settings.

FileMCPWatcher subscribes to the same singleton for Warp home MCP updates while continuing to use the existing home-directory and repository watching paths for non-Warp providers. The duplicated startup parse path and the duplicated single-config incremental update path are now shared with the non-Warp logic.

SkillWatcher subscribes to the same singleton for Warp home skill updates while continuing to use the existing home-directory and repository watching paths for non-Warp providers and project skills. The initial Warp home skill load and repository-scan load now share the same directory-read helper.

Because Warp home skills now live under the channel-aware data_dir()/skills, helper code also needs to recognize that path when determining provider, scope, and enclosing skill directory.

Chosen design

1. Dedicated Warp watcher ownership

WarpDataDirectoryWatcher owns a BulkFilesystemWatcher directly instead of layering on top of DirectoryWatcher.

This keeps the abstraction boundary simple:

  • project repositories still use DirectoryWatcher
  • Warp-owned home/config directories use WarpDataDirectoryWatcher

That separation avoids broadening the generic repo watcher API for a Warp-specific use case.

2. Watch root preparation and registration

Each Warp-owned root is prepared before watcher registration, but that preparation happens outside the watcher constructor:

  • call create_dir_all() during startup/setup
  • keep watcher registration separate from root creation
  • log failures before registration rather than implicitly recovering inside watcher construction

data_dir() is registered with a watcher-level WatchFilter that excludes <data_dir>/worktrees.

config_local_dir() is registered with WatchFilter::accept_all() when it is distinct from data_dir().

3. Update normalization

BulkFilesystemWatcherEvent is converted into RepositoryUpdate so existing subscribers can keep using the same helper logic and event vocabulary.

filter_repository_update() is retained as a downstream helper to:

  • keep only the paths relevant to a downstream consumer
  • convert cross-boundary moves into add/delete updates when a move crosses the watched prefix boundary

4. Downstream consumers

WarpConfig remains subscription-based and does not reintroduce a direct notify watcher.

It continues to:

  • reload themes, workflows, launch configs, and tab configs when updates touch the relevant directories
  • perform those file-backed reloads via ctx.spawn(...) so disk reads happen off the model thread
  • emit WarpConfigUpdateEvent::Settings when updates touch user_preferences_toml_file_path()

FileMCPWatcher subscribes to Warp watcher events for data_dir()/.mcp.json and keeps the existing logic for non-Warp providers. Warp remains on the Warp-specific watcher source, but shared helpers now cover:

  • the startup parse path for a single config file
  • the single-config incremental update path used by both Warp and non-Warp providers

SkillWatcher subscribes to Warp watcher events for data_dir()/skills and keeps the existing logic for non-Warp home providers and project repositories. The Warp home initialization path and repository scan path share the same helper for reading skill directories and emitting updates.

5. Channel-aware helper paths

The helper path logic is updated so Warp home skills and MCP config resolve through the active channel's data directory:

  • Warp MCP home config resolves to warp_data_mcp_config_file_path()
  • Warp home skills resolve to data_dir()/skills
  • provider and scope classification recognize those paths as Warp home paths rather than project paths

End-to-end flow

  1. Warp startup prepares data_dir() and, when distinct, config_local_dir() before constructing WarpDataDirectoryWatcher.
  2. WarpDataDirectoryWatcher registers data_dir() with a watcher-level filter that excludes <data_dir>/worktrees.
  3. If config_local_dir() differs from data_dir(), WarpDataDirectoryWatcher registers that root with no extra filter.
  4. The underlying BulkFilesystemWatcher emits BulkFilesystemWatcherEvent.
  5. WarpDataDirectoryWatcher converts that event into RepositoryUpdate and emits FilesChanged(update).
  6. WarpConfig, FileMCPWatcher, and SkillWatcher inspect the update and react only when their owned paths were touched.
  7. For themes, workflows, launch configs, and tab configs, WarpConfig reloads from disk on background tasks before applying the new state on the model thread.
  8. For settings.toml, WarpConfig emits WarpConfigUpdateEvent::Settings, and settings::init() reloads public settings from disk.

Risks and mitigations

  • Distinct-root platforms regress settings hot reload.
    • Mitigation: register config_local_dir() separately when it differs from data_dir().
  • Worktree events leak back into Warp home consumers.
    • Mitigation: apply a watcher-level filter to data_dir() and let downstream consumers do any additional prefix filtering they need.
  • Fresh installs fail to register watchers because the root directory is missing.
    • Mitigation: prepare each watch root during startup before registration.
  • Path comparisons miss updates because watcher event paths and logical config paths may differ by symlink or canonical form.
    • Mitigation: WarpConfig continues checking both raw and canonicalized paths in update_touches_dir() and update_touches_path().
  • Warp home skill or MCP paths are misclassified as project paths.
    • Mitigation: centralize Warp-home path handling in the MCP and skills helper utilities.

Testing and validation

  • cargo test -p warp --lib filter_repository_update_by_prefix_keeps_only_matching_paths
    • validates the downstream filtering helper that skills consumers rely on
  • cargo test -p integration --test integration settings_file_hot_reload -- --nocapture
    • verifies the end-to-end settings hot-reload behavior by rewriting settings.toml and asserting in-memory settings updates
  • Code review validation:
    • data_dir() is watched with the worktrees exclusion
    • config_local_dir() is watched only when distinct
    • WarpDataDirectoryWatcher owns its own filesystem watcher instead of extending DirectoryWatcher
    • watch-root preparation happens before watcher registration instead of inside the watcher constructor
    • Warp home MCP and skill helpers resolve through the channel-aware data directory

Follow-ups

  • Add a focused test for WarpDataDirectoryWatcher root registration when data_dir() and config_local_dir() differ.
  • Consider whether the remaining Warp-home helper changes in the MCP and skills layers can be reduced further without regressing path classification.