Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# Separate Preview Config Directory on macOS
|
||||
|
||||
## Summary
|
||||
|
||||
On macOS, give the Preview channel its own config directory (`~/.warp-preview`) instead of sharing `~/.warp` with Stable. On first launch, migrate existing config by symlinking each top-level entry from `~/.warp` into `~/.warp-preview`, so existing Preview users keep all their customizations.
|
||||
|
||||
## Problem
|
||||
|
||||
Stable and Preview currently share the `~/.warp` directory on macOS. This means:
|
||||
|
||||
- Settings changes made in one channel affect the other.
|
||||
- Preview experiments (themes, keybindings, tab configs, etc.) can break the Stable experience.
|
||||
- There is no safe way to test config-level changes on Preview without risking Stable user data.
|
||||
|
||||
Other channels (Dev, Integration, Local) already use separate directories (`.warp-dev`, `.warp-integration`, `.warp-local`). Preview is the only non-Stable channel that still shares with Stable.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Preview uses `~/.warp-preview` as its config directory on macOS.
|
||||
2. Existing Preview users retain all their customizations after the change.
|
||||
3. Stable users see no change whatsoever.
|
||||
4. The migration is automatic and requires no user action.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing behavior on Linux or Windows (these platforms already use separate directories via `ProjectDirs`).
|
||||
- Migrating the SQLite database (already stored separately in `~/Library/Application Support/` or the App Group container).
|
||||
- Changing Dev, Integration, or Local channel directories.
|
||||
- Achieving full bidirectional sync between Stable and Preview configs going forward.
|
||||
|
||||
## Figma
|
||||
|
||||
Figma: none needed (no UI changes).
|
||||
|
||||
## User Experience
|
||||
|
||||
### First launch after upgrade (Preview channel)
|
||||
|
||||
When a Preview user launches Warp for the first time after this change:
|
||||
|
||||
1. Warp detects that `~/.warp-preview` does not exist.
|
||||
2. Warp creates `~/.warp-preview`.
|
||||
3. For each top-level entry in `~/.warp` (files and directories), Warp creates a symbolic link in `~/.warp-preview` pointing to the corresponding entry in `~/.warp`.
|
||||
4. Preview proceeds using `~/.warp-preview` as its config directory.
|
||||
|
||||
After migration, the directory looks like:
|
||||
|
||||
```
|
||||
~/.warp-preview/
|
||||
keybindings.yaml -> ~/.warp/keybindings.yaml
|
||||
themes/ -> ~/.warp/themes/
|
||||
workflows/ -> ~/.warp/workflows/
|
||||
tab_configs/ -> ~/.warp/tab_configs/
|
||||
...etc
|
||||
```
|
||||
|
||||
### Subsequent launches (Preview)
|
||||
|
||||
No migration occurs. Preview reads and writes from `~/.warp-preview`.
|
||||
|
||||
### Stable users
|
||||
|
||||
Zero change. Stable continues to use `~/.warp`.
|
||||
|
||||
### settings.toml is not symlinked
|
||||
|
||||
The `settings.toml` file (used by the Settings File feature) is intentionally kept separate between Stable and Preview. The migration uses an explicit exclude list (`MIGRATION_EXCLUDED_FILES`) to skip `settings.toml` during symlinking. This handles the case where a user runs Stable first (which creates `settings.toml` in `~/.warp`) and later updates Preview — without the filter, the migration would symlink the file, causing both channels to share settings.
|
||||
|
||||
### Behavior after migration
|
||||
|
||||
- **Reading config**: Preview reads through symlinks, so existing Stable config is visible.
|
||||
- **Modifying a shared file**: If Preview modifies a symlinked file (e.g., `keybindings.yaml`), the change affects Stable too, since the symlink points to the same file. This is the intended tradeoff — users who customize on Preview are likely the same user customizing on Stable.
|
||||
- **Creating new entries in a symlinked directory**: If Preview creates a new file inside a symlinked directory (e.g., a new tab config in `tab_configs/`), the file is created in `~/.warp/tab_configs/` because the directory symlink resolves there. This is an acceptable limitation of the symlink approach.
|
||||
- **Breaking the symlink**: If the user wants full independence for a specific config, they can delete the symlink in `~/.warp-preview/` and replace it with a real file or directory. Preview will use the local copy from that point on.
|
||||
|
||||
### Edge cases
|
||||
|
||||
- **`~/.warp` does not exist**: No migration needed. Preview creates `~/.warp-preview` fresh, and Warp's existing directory-creation logic populates it.
|
||||
- **`~/.warp-preview` already exists**: Migration is skipped entirely. This handles the case where the user has manually created the directory or is launching for a second time.
|
||||
- **Entries in `~/.warp` are themselves symlinks** (e.g., `keybindings.yaml -> ~/dotfiles/...`): The migration creates a symlink to the symlink. The chain resolves correctly. No special handling needed.
|
||||
- **`.DS_Store` and hidden OS files**: These should be skipped during migration to avoid unnecessary clutter.
|
||||
- **Simultaneous Stable and Preview launch**: The directory creation (`create_dir`) fails atomically if the directory already exists, so concurrent launches are safe. The migration will run in one process and be skipped in the other.
|
||||
- **Permissions errors**: If symlink creation fails for a specific entry, log a warning and continue with the remaining entries. Partial migration is better than no migration.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Full copy instead of symlinks
|
||||
Copy all files and directories from `~/.warp` to `~/.warp-preview`.
|
||||
|
||||
- Pro: Full independence from day one.
|
||||
- Con: Doubles disk usage for themes, workflows, etc. Changes in Stable no longer propagate to Preview, which could confuse users who expect their config to be shared.
|
||||
|
||||
### 2. Symlink the entire directory
|
||||
Create `~/.warp-preview` as a symlink to `~/.warp`.
|
||||
|
||||
- Pro: Simplest implementation.
|
||||
- Con: Not actually a separate directory. Any new entries created by Preview (e.g., new directories Warp adds in future versions) would appear in `~/.warp`. Defeats the purpose of separation.
|
||||
|
||||
### 3. No migration (clean start)
|
||||
Simply start using `~/.warp-preview` with no migration.
|
||||
|
||||
- Pro: Simplest. No migration code.
|
||||
- Con: Users lose all custom keybindings, themes, workflows, launch configs, tab configs, and settings. This is a bad experience.
|
||||
|
||||
### 4. Deep symlink (per-file in each subdirectory)
|
||||
For each directory like `themes/`, create the directory in `.warp-preview` and symlink each individual file.
|
||||
|
||||
- Pro: New files created inside directories stay in `.warp-preview` rather than leaking into `.warp`.
|
||||
- Con: Significantly more complex. The improvement is marginal — most users don't create new config files frequently, and those who do are likely sophisticated enough to understand the symlink behavior.
|
||||
|
||||
**Decision**: Symlink top-level contents (option in the main proposal) is the best balance of simplicity, backwards compatibility, and user experience.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. After upgrading, a Preview user's keybindings, themes, workflows, launch configs, tab configs, MCP config, and settings are all accessible in Preview without manual action.
|
||||
2. A Stable user's `~/.warp` directory is unchanged after the upgrade.
|
||||
3. `~/.warp-preview` is created on first launch of Preview and contains symlinks to each top-level entry in `~/.warp`.
|
||||
4. If `~/.warp` does not exist, `~/.warp-preview` is created empty (normal startup logic applies).
|
||||
5. If `~/.warp-preview` already exists, no migration runs.
|
||||
6. The migration only runs on macOS.
|
||||
7. Partial migration failures (e.g., one symlink fails) do not prevent Warp from launching.
|
||||
|
||||
## Validation
|
||||
|
||||
- **Manual testing on macOS Preview build**:
|
||||
- Fresh install with no `~/.warp`: Preview creates `~/.warp-preview`, normal setup.
|
||||
- Existing `~/.warp` with custom keybindings/themes: After upgrade, verify symlinks exist and config loads correctly.
|
||||
- Launch Stable after Preview migration: Stable still uses `~/.warp` and is unaffected.
|
||||
- **Integration test**: Verify the migration function creates expected symlinks given a mock `~/.warp` directory.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Tech Spec: Separate Preview Config Directory on macOS
|
||||
|
||||
## Problem
|
||||
|
||||
On macOS, `macos_config_dir_name()` returns `.warp` for both Stable and Preview channels, causing them to share a single config directory. This needs to change so Preview uses `.warp-preview`, with a one-time symlink migration for existing users.
|
||||
|
||||
## Relevant Code
|
||||
|
||||
- `crates/warp_core/src/paths.rs:42-51` — `macos_config_dir_name()` maps channels to directory names. The `Channel::Preview` arm currently returns `WARP_CONFIG_DIR` (`.warp`).
|
||||
- `crates/warp_core/src/paths.rs:57-67` — `data_dir()` uses `macos_config_dir_name()` on macOS.
|
||||
- `crates/warp_core/src/paths.rs:71-83` — `config_local_dir()` also uses `macos_config_dir_name()` on macOS. On macOS, `data_dir()` and `config_local_dir()` return the same path.
|
||||
- `app/src/warp_data_directory_watcher.rs:28-46` — `ensure_warp_watch_roots_exist()` creates the data and config directories at startup.
|
||||
- `app/src/lib.rs:966` — calls `ensure_warp_watch_roots_exist()` during `initialize_app()`.
|
||||
- `app/src/persistence/sqlite.rs:350-389` — existing migration precedent: migrates the SQLite database from `state_dir()` to `secure_state_dir()` on first launch.
|
||||
- `crates/warp_core/src/channel/mod.rs:8-15` — `Channel` enum definition.
|
||||
|
||||
## Current State
|
||||
|
||||
The `macos_config_dir_name()` function in `paths.rs` determines the macOS config directory:
|
||||
|
||||
```rust
|
||||
fn macos_config_dir_name() -> String {
|
||||
match ChannelState::channel() {
|
||||
Channel::Stable | Channel::Preview => WARP_CONFIG_DIR.to_owned(),
|
||||
Channel::Dev => format!("{WARP_CONFIG_DIR}-dev"),
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both `data_dir()` and `config_local_dir()` use this on macOS, so all config paths (`settings.toml`, `keybindings.yaml`, `themes/`, `workflows/`, `tab_configs/`, etc.) resolve under `~/.warp` for both Stable and Preview.
|
||||
|
||||
The SQLite database is **not** affected — it is stored under `secure_state_dir()` (App Group container) or `state_dir()` (`~/Library/Application Support/dev.warp.Warp-Preview/`), both of which already use the bundle ID and are channel-specific.
|
||||
|
||||
The `WARP_CONFIG_DIR` constant (`.warp`) is also used for per-repository project configs (e.g., `./.warp/workflows`). This is unrelated to the home-directory config and must not change.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. Update `macos_config_dir_name()` — `crates/warp_core/src/paths.rs`
|
||||
|
||||
Change the Preview arm to return `.warp-preview`:
|
||||
|
||||
```rust
|
||||
fn macos_config_dir_name() -> String {
|
||||
match ChannelState::channel() {
|
||||
Channel::Stable => WARP_CONFIG_DIR.to_owned(),
|
||||
Channel::Preview => format!("{WARP_CONFIG_DIR}-preview"),
|
||||
Channel::Dev => format!("{WARP_CONFIG_DIR}-dev"),
|
||||
Channel::Integration => format!("{WARP_CONFIG_DIR}-integration"),
|
||||
Channel::Local => format!("{WARP_CONFIG_DIR}-local"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the only change needed to route Preview to a new directory. All downstream consumers (`data_dir()`, `config_local_dir()`, `themes_dir()`, `workflows_dir()`, etc.) automatically pick up the new path.
|
||||
|
||||
### 2. Add migration module — `app/src/preview_config_migration.rs`
|
||||
|
||||
Add a new module in the `warp` (app) crate that owns the migration. The migration is app startup logic, not a general path utility, so it lives in the app crate alongside other one-time migrations (e.g., `app/src/persistence/sqlite.rs`). The `warp_core::paths::data_dir()` helper is already channel-aware, so the module does not need access to the private `macos_config_dir_name()` in `warp_core`.
|
||||
|
||||
The module exposes two functions:
|
||||
|
||||
- `migrate_preview_config_dir_if_needed()` — `pub(crate)` entry point that does the channel check and computes paths from `warp_core::paths::data_dir()`, then delegates to the inner helper.
|
||||
- `migrate_config_dir_via_symlinks(old_dir, new_dir)` — `pub(crate)` core logic that creates `new_dir`, then symlinks each top-level entry from `old_dir` into `new_dir`. Skips `.DS_Store`, `._*` metadata files, and any file in `MIGRATION_EXCLUDED_FILES` (currently `settings.toml`).
|
||||
|
||||
The directory's existence is the migration marker — no separate marker file is needed. Subsequent launches see `~/.warp-preview` already exists and no-op.
|
||||
|
||||
### 3. Call migration before directory creation — `app/src/lib.rs`
|
||||
|
||||
Call the migration directly in `initialize_app()`, before `ensure_warp_watch_roots_exist()`. This placement ensures the migration runs:
|
||||
- Before `ensure_warp_watch_roots_exist()` would `create_dir_all` on `~/.warp-preview` (which would make the `new_dir.exists()` check true and skip migration).
|
||||
- After `ChannelState` is initialized (it is — `initialize_app` runs after `ChannelState::set()`).
|
||||
- Exactly once per launch.
|
||||
|
||||
### 4. Integration testing hook — `app/src/integration_testing/preview_config_migration.rs`
|
||||
|
||||
The integration test framework runs under `Channel::Integration`, so the public entry point no-ops. To let integration tests exercise the migration logic, expose the inner helper via `warp::integration_testing::preview_config_migration::run_config_dir_symlink_migration(old_dir, new_dir)`. This follows the existing pattern of keeping app internals private while exposing test hooks through `integration_testing`.
|
||||
|
||||
## End-to-End Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Main as main() / run()
|
||||
participant CS as ChannelState::set()
|
||||
participant Init as initialize_app()
|
||||
participant Migrate as migrate_preview_config_dir_if_needed()
|
||||
participant Ensure as ensure_warp_watch_roots_exist()
|
||||
participant FS as Filesystem
|
||||
|
||||
Main->>CS: Set channel to Preview
|
||||
Main->>Init: initialize_app()
|
||||
Init->>Migrate: migrate_preview_config_dir_if_needed()
|
||||
Migrate->>FS: Check ~/.warp-preview exists?
|
||||
alt Already exists
|
||||
Migrate-->>Init: No-op
|
||||
else Does not exist
|
||||
Migrate->>FS: Check ~/.warp exists?
|
||||
alt Does not exist
|
||||
Migrate-->>Init: No-op (fresh install)
|
||||
else Exists
|
||||
Migrate->>FS: create_dir(~/.warp-preview)
|
||||
Migrate->>FS: read_dir(~/.warp)
|
||||
loop Each top-level entry
|
||||
Migrate->>FS: symlink(~/.warp-preview/X -> ~/.warp/X)
|
||||
end
|
||||
end
|
||||
end
|
||||
Init->>Ensure: ensure_warp_watch_roots_exist()
|
||||
Ensure->>FS: create_dir_all(~/.warp-preview) [no-op if exists]
|
||||
Init->>Init: Continue startup (WarpConfig, settings, etc.)
|
||||
```
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
### Symlink breakage if `~/.warp` is deleted
|
||||
If a user deletes `~/.warp` after migration, all symlinks in `~/.warp-preview` become dangling. Warp's existing directory-creation logic (`ensure_warp_watch_roots_exist`) will recreate `~/.warp-preview` subdirectories as needed, and missing files will be treated as defaults. This is the same behavior as a fresh install.
|
||||
|
||||
**Mitigation**: Acceptable degradation. No action needed.
|
||||
|
||||
### Filesystem watcher and symlinks
|
||||
The `notify` crate (used by `BulkFilesystemWatcher`) watches `~/.warp-preview` recursively. For symlinked directories, `notify` with `RecursiveMode::Recursive` follows symlinks by default on macOS (using `kqueue`/`FSEvents`). Changes to the underlying files in `~/.warp` will trigger events on the watched path in `~/.warp-preview`.
|
||||
|
||||
**Mitigation**: Verify in manual testing. If events are missed, we may need to also watch `~/.warp` from Preview, but this is unlikely to be needed.
|
||||
|
||||
### Concurrent launch race
|
||||
Two Preview processes launching simultaneously could both attempt the migration. The `create_dir` call will succeed for one and return `AlreadyExists` for the other. The losing process may encounter partially-created symlinks, but since `symlink` on an existing path returns an error (which we log and skip), this is safe.
|
||||
|
||||
**Mitigation**: Built into the implementation — `create_dir` + per-entry error handling.
|
||||
|
||||
### Entries created between `read_dir` and symlink creation
|
||||
If Stable creates a new entry in `~/.warp` after Preview's `read_dir` call but before symlinking completes, that entry won't have a symlink. This is a negligible window and the entry would only be visible in Stable.
|
||||
|
||||
**Mitigation**: None needed — this is an edge case with no practical impact.
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Unit tests
|
||||
Add tests in `app/src/preview_config_migration_tests.rs` that call `migrate_config_dir_via_symlinks` with temp directories:
|
||||
1. **Migration creates symlinks**: Set up a temp dir with mock `.warp` contents, run migration, assert symlinks exist and point to correct targets.
|
||||
2. **Migration skips when target exists**: Pre-create the target directory, run migration, assert no symlinks are created.
|
||||
3. **Migration skips when source missing**: Run migration without a `.warp` directory, assert no errors.
|
||||
4. **Migration skips `.DS_Store`**: Include `.DS_Store` and `._*` files in the source, assert they're not symlinked.
|
||||
5. **Migration skips `settings.toml`**: Include `settings.toml` in the source, assert it's not symlinked.
|
||||
6. **Idempotent**: Run twice; second call is a no-op.
|
||||
|
||||
### Integration test
|
||||
Add an integration test in `crates/integration/src/test/preview_config_migration.rs` that verifies the end-to-end symlink creation:
|
||||
|
||||
1. In `with_setup`, populate the hermetic home's `.warp/` directory with representative entries (e.g., `keybindings.yaml`, `themes/`, `workflows/`, `settings.toml`, a `.DS_Store`).
|
||||
2. In `with_setup`, call `warp::integration_testing::preview_config_migration::run_config_dir_symlink_migration` (since the integration channel is `Integration`, not `Preview`, the public entry point would no-op — so we use the integration-testing hook).
|
||||
3. In a `TestStep`, assert:
|
||||
- `~/.warp-preview` exists and is a directory (not a symlink).
|
||||
- Each expected entry in `~/.warp-preview` is a symlink pointing to the corresponding entry in `~/.warp`.
|
||||
- `.DS_Store`, `._*`, and `settings.toml` were not symlinked.
|
||||
4. Wire the test into the nextest suite via `crates/integration/tests/integration/ui_tests.rs`.
|
||||
|
||||
### Manual validation
|
||||
- Build a Preview binary, launch with existing `~/.warp`, verify:
|
||||
- `~/.warp-preview` is created with symlinks.
|
||||
- Settings, keybindings, themes all load correctly.
|
||||
- Creating a new tab config in Preview appears in the expected location.
|
||||
- Launch Stable after, verify `~/.warp` is unmodified.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- After this change has been stable for several release cycles, consider whether to add a user-facing setting or migration to break symlinks and fully copy config for users who want complete isolation.
|
||||
Reference in New Issue
Block a user