Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+129
View File
@@ -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.
+166
View File
@@ -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.