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
+76
View File
@@ -0,0 +1,76 @@
# Product Spec: Plugin Update Flow
## Problem
We're releasing a new version of the Warp notification plugin for Claude Code (v2.0.0). Existing users on v1.1.0 won't automatically receive the update because Claude Code's plugin update system is unreliable. We need Warp to detect outdated plugin versions and prompt users to update.
## Current Chip Behavior
Today, a green chip appears in the CLI agent footer when the plugin isn't installed:
- Local session → chip auto-installs on click
- SSH session or prior install failure → chip opens a modal with manual steps
- Plugin is active (connected) or installed → chip is hidden
- User dismissed the chip → chip is hidden
There is no concept of an "outdated" plugin. A user on v1.1.0 will never see a prompt to update.
## New Behavior: Update Chip
When the plugin is installed but on an old version, show an update chip:
- Label: "Update Warp plugin"
- Tooltip: "A new version of the Warp plugin is available"
- Same green styling and dismiss (X) button as the install chip
- On local sessions: clicking runs the update automatically
- On SSH or after a failed auto-update: clicking opens a modal with manual update steps
The update chip replaces the install chip in the same position — they never appear simultaneously.
## How Version Detection Works
The plugin reports its own version via a `plugin_version` field in the `SessionStart` event when it connects. Warp compares this against a minimum required version. This works identically for local and remote sessions — no filesystem check needed for update detection.
A missing `plugin_version` (from a plugin that predates version reporting) is treated as outdated.
## When Each Chip Appears
1. Plugin connected, version >= minimum → **no chip**
2. Plugin connected, version < minimum or not reported → **update chip** (new)
3. Plugin not connected, not installed locally → **install chip** (existing behavior)
4. Plugin not connected, installed locally, on-disk version outdated → **update chip** (filesystem fallback for plugins too old to send structured events)
5. Plugin not connected, installed locally, on-disk version current → **no chip** (waiting for connection)
6. Remote session, no listener → **install chip** (can't check filesystem remotely)
7. Just completed install/update → **no chip** (assume current version until next `SessionStart`)
8. Chip dismissed for this version → **no chip**
## Auto-Update (Local Sessions)
On click, Warp runs `marketplace add` (to refresh the local clone) + `plugin update` via the CLI. Same UX pattern as auto-install: persistent toast while running, success/failure toast on completion. A post-update sanity check verifies the on-disk version actually changed.
On success: "Warp plugin updated. Please run /reload-plugins to activate."
On failure: transition to manual mode (modal) for the rest of the session.
## Manual Update (SSH / Failed Auto-Update)
Opens a modal with step-by-step update instructions. Unlike the install modal (which uses in-session `/plugin` slash commands), the update modal uses CLI commands (`claude plugin ...`) because there is no working in-session slash command for updating plugins. Users are instructed to run the commands in a separate terminal, or inside Claude Code by typing `!` before each command.
## Dismiss Behavior
The install chip and update chip have **independent** dismiss state:
- Dismissing the install chip hides the install chip (existing boolean behavior, unchanged)
- Dismissing the update chip hides the update chip for the current minimum version
- If we later release a newer version (e.g., v3.0.0), the update chip reappears
This means tracking *which version* was dismissed for the update chip, not just whether it was dismissed.
## Edge Cases
- **User updates manually in Claude Code:** The listener reconnects with a new `plugin_version`, chip disappears automatically.
- **Plugin doesn't report version:** Treated as outdated — these are pre-versioning builds that definitely need an update.
- **Plugin too old to send structured events:** Falls back to on-disk version check. If the on-disk version is below minimum, the update chip appears even without a listener.
- **Just completed install/update (mid-session):** The session's `plugin_version` is set to `MINIMUM_PLUGIN_VERSION` to suppress the update chip until the user runs `/reload-plugins` and the plugin sends a real `SessionStart`.
- **Multiple tabs:** All tabs see the same session state. Update failure tracking is shared across tabs.
- **Plugin connected over SSH:** Version detection works the same way — the plugin reports its own version regardless of where it's running.
- **Old plugin over SSH (pre-structured-events):** The old public plugin (v1.1.0) doesn't send structured events, so no listener connects and we can't check the remote filesystem. The install chip shows instead of the update chip. This is functionally correct — the install instructions work to upgrade — but the label says "install" rather than "update".
+243
View File
@@ -0,0 +1,243 @@
# Tech Spec: Plugin Update Flow
See `specs/APP-3661/PRODUCT.md` for the product spec.
## 1. Trait Changes
`plugin_manager/mod.rs`
Keep `is_installed() -> bool` on the trait (filesystem check: is the plugin key present in `installed_plugins.json`?). Add `update()`, `update_instructions()`, and `needs_update()` methods:
```rust
trait CliAgentPluginManager: Send + Sync {
fn is_installed(&self) -> bool;
fn needs_update(&self) -> bool;
async fn install(&self) -> Result<(), PluginInstallError>;
async fn update(&self) -> Result<(), PluginInstallError>;
fn install_instructions(&self) -> &'static PluginInstructions;
fn update_instructions(&self) -> &'static PluginInstructions;
}
```
The **update** chip is primarily driven by `plugin_version` reported in the `SessionStart` event (see section 3b). As a fallback for plugins too old to send structured events, `needs_update()` checks the on-disk version. Also add `PluginModalKind { Install, Update }` enum for event plumbing.
## 2. Renamed Instructions Struct
`plugin_manager/mod.rs`
Rename `PluginInstallInstructions``PluginInstructions` and `PluginInstallStep``PluginInstructionStep`:
```rust
pub(crate) struct PluginInstructionStep {
pub description: &'static str,
pub command: &'static str,
}
pub(crate) struct PluginInstructions {
pub title: &'static str,
pub subtitle: &'static str,
pub steps: &'static [PluginInstructionStep],
pub success_toast: &'static str,
}
```
## 3. Claude Implementation
`plugin_manager/claude.rs`
### is_installed()
Unchanged — reads `installed_plugins.json`, returns true if the `PLUGIN_KEY` entry exists and is non-empty.
### MINIMUM_PLUGIN_VERSION
New `pub(crate)` `&str` constant (initially `"2.0.0"`). Exported so the footer can compare against it.
Must be kept in sync with the plugin version in `warpdotdev/claude-code-warp`. Add a comment on the constant pointing to the plugin repo, and a reciprocal comment in the plugin repo's README.
### compare_versions()
New `pub(crate)` helper. Compares two `X.Y.Z` version strings using simple integer comparison. Unparseable components treated as 0.
### needs_update()
Checks the on-disk version in `installed_plugins.json`. Returns true if the installed version is below `MINIMUM_PLUGIN_VERSION`, or if the entry exists but has no version field (very old plugin). Used as a fallback when no listener has connected (e.g., the plugin is too old to send structured events).
### update()
Runs `marketplace remove` + `marketplace add` (to ensure the local clone is fresh) + `plugin install` (to reinstall the plugin from the freshly added marketplace). We use `plugin install` instead of `plugin update` because `marketplace remove` unlinks the plugin, so `plugin update` would fail with `Plugin "warp" is not installed`.
As an internal sanity check, re-reads `installed_plugins.json` and checks the version. If still below minimum → returns `Err` with a message like "Plugin update did not take effect". This triggers the fallback to manual mode in the footer.
## 3b. Session Version Tracking
`cli_agent_sessions/mod.rs`
Add `plugin_version: Option<String>` to `CLIAgentSession`. Populated via two paths:
1. **`SessionStart` event path:** `register_listener()` now accepts `plugin_version` as a parameter, threaded from the `SessionStart` notification payload. This is the primary path.
2. **Mid-session install path:** `register_cli_agent_listener()` (called after install/update succeeds) sets `plugin_version` to `MINIMUM_PLUGIN_VERSION` to suppress the update chip until the user runs `/reload-plugins`.
Also set in `apply_event()` when the event type is `SessionStart` (for subsequent `SessionStart` events after the initial one).
This is the **authoritative signal** for whether the plugin is outdated. It works for both local and remote sessions because the plugin reports its own version. A `None` value means the plugin predates version reporting and is definitely outdated.
### update_instructions()
Returns a `&'static PluginInstructions` (via `LazyLock`) with:
- Title: "Update Warp Plugin for Claude Code"
- Subtitle: "Run the following commands in Claude Code by typing ! before each command, or in a separate terminal."
- Steps:
1. `claude plugin marketplace remove claude-code-warp`
2. `claude plugin marketplace add warpdotdev/claude-code-warp`
3. `claude plugin install warp@claude-code-warp`
4. Restart Claude Code to activate → `/exit`
- Success toast (auto-update): "Warp plugin updated. Please run /reload-plugins to activate." (the one-click flow registers the listener programmatically, so `/reload-plugins` suffices)
- Success toast (manual modal): tells user to restart Claude Code (manual installs require a full restart for hooks to fire)
Note: the update modal uses CLI commands (not in-session slash commands) because there is no working `/plugin update` slash command in Claude Code. The install modal continues to use slash commands since `/plugin install` works in-session.
## 4. Modal Changes
`workspace/view/plugin_install_modal.rs`
The modal becomes a generic instructions renderer. Replace `agent: Option<CLIAgent>` with `instructions: Option<&'static PluginInstructions>`. The `set_agent(agent)` method becomes `set_instructions(instructions: &'static PluginInstructions)` which stores the reference and resizes `step_code_handles` for the steps.
Copy button on each step copies the command to clipboard and shows a green success toast.
## 5. Footer Changes
`agent_input_footer/mod.rs`
### New buttons
Add two new `ActionButton` views (same `InstallPluginButtonTheme` and construction pattern as the existing install buttons):
- `update_plugin_button`: "Update Warp plugin", `Icon::Download`, dispatches `AgentInputFooterAction::UpdatePlugin`
- `update_instructions_button`: "Plugin update instructions", `Icon::Info`, dispatches `AgentInputFooterAction::ShowPluginInstructionsModal`
### Chip visibility and mode
The footer uses a `PluginChipKind` enum (`Install` / `Update`) returned by `plugin_chip_kind()`. The logic uses three layers of version detection:
**Install chip** (pre-connection, local only):
- No listener, local session, `is_installed()` returns false, install chip not dismissed → `PluginChipKind::Install`
- Same conditions as today (unchanged)
**Update chip** (post-connection, local and remote):
- Listener connected, `session.plugin_version` is `None` or < `MINIMUM_PLUGIN_VERSION``PluginChipKind::Update`
- (`None` means the plugin predates version reporting and definitely needs an update)
- Update chip dismissed for current minimum version → hidden
- Listener connected, `session.plugin_version` >= minimum → no chip
**Update chip** (pre-connection filesystem fallback, local only):
- No listener, `is_installed()` true, `needs_update()` true → `PluginChipKind::Update`
- Handles plugins too old to send structured events (no listener ever connects)
- Only works locally — for remote sessions with old plugins that don't send structured events, we fall through to the install chip since we can't check the remote filesystem
**No chip:**
- No listener, plugin installed on disk, on-disk version current → waiting for connection
- Notifications disabled
- Operation in progress
### Chip mode selection
The render method selects which button based on install-vs-update and auto-vs-manual:
- Install + auto → `install_plugin_button`
- Install + manual → `plugin_instructions_button` (install instructions modal)
- Update + auto → `update_plugin_button`
- Update + manual → `update_instructions_button` (update instructions modal)
Manual mode is triggered by: remote session, or prior auto-operation failure for this agent/host.
### Failure tracking
Reuse the existing `plugin_install_failures: HashSet<(CLIAgent, Option<String>)>` on `CLIAgentSessionsModel` — rename to `plugin_auto_failures`. This single set covers both install and update failures. This works because `PluginStatus` already determines which operation the chip shows; there's no scenario where install failures and update failures need to be distinguished (and the set resets each session anyway).
### handle_plugin_operation()
Extract a shared helper from `handle_install_plugin` that both install and update use. The shared logic: set `plugin_operation_in_progress`, show persistent toast, spawn the async operation, on success emit `PluginInstalled` event, on failure record in `plugin_auto_failures` and show error toast. Replace the two separate `plugin_install_in_progress`/`plugin_update_in_progress` bools with a single `plugin_operation_in_progress: bool` (install and update are mutually exclusive based on `PluginStatus`).
The only differences between install and update are: (a) which async fn to call (`manager.install()` vs `manager.update()`), (b) progress/success/error toast messages. All three are passed as `&str` parameters.
## 6. Settings
`settings/ai.rs`
Add a new setting for update chip dismissal:
```
plugin_chip_dismissed_for_version: PluginChipDismissedForVersion {
type: String,
default: "",
supported_platforms: SupportedPlatforms::DESKTOP,
sync_to_cloud: SyncToCloud::Never,
hierarchy: "private",
}
```
When the user dismisses the update chip, store the current `MINIMUM_PLUGIN_VERSION`. In `should_show_plugin_chip`, compare the dismissed version against the current minimum — if dismissed version >= current minimum, hide; otherwise show.
## 7. Event Plumbing
Replace the existing `ShowPluginInstallModal(CLIAgent)` with a single `ShowPluginInstructionsModal(CLIAgent, PluginModalKind)` where `PluginModalKind { Install, Update }`. This avoids duplicating an event variant through every layer of the chain.
`AgentInputFooterEvent``Input::Event``TerminalView::Event``pane_group::Event` → Workspace handler
The workspace handler matches on the kind, calls the appropriate `install_instructions()` or `update_instructions()`, and passes the result to `modal.set_instructions(...)` before opening.
## 8. Testing
### Unit tests (`plugin_manager/claude_tests.rs`)
Existing `check_installed` tests remain valid (now testing `is_installed()`).
Add:
- `compare_versions` — covers equal, less-than, greater-than, different major/minor/patch, unparseable components
### Unit tests (`cli_agent_sessions/mod_tests.rs`)
Rename existing `plugin_install_failure` tests to `plugin_auto_failure` and verify the renamed set works identically. No new test logic needed — just a rename.
### Unit tests (`plugin_manager/mod_tests.rs`)
Add tests for the new trait methods:
- `claude_manager_returns_update_instructions` — verify `update_instructions()` returns non-empty steps
- `claude_manager_returns_install_instructions` — verify `install_instructions()` returns non-empty steps
### View tests (`terminal/view_test.rs`)
Using the existing `App::test` + `CLIAgentSessionsModel` pattern:
- `update_chip_shown_when_plugin_version_below_minimum` — set `session.plugin_version` to `"1.1.0"`, verify update chip shown
- `update_chip_shown_when_plugin_version_is_none` — listener connected but no `plugin_version`, verify update chip shown
- `no_chip_when_plugin_version_meets_minimum` — set `session.plugin_version` to `"2.0.0"`, verify no chip
- `update_chip_hidden_when_dismissed_for_current_version`
- `update_chip_shown_when_dismissed_for_older_version`
- `update_chip_and_install_chip_dismiss_are_independent`
### Integration tests
Add to `integration/tests/integration/ui_tests.rs`:
- `test_plugin_update_chip_appears_for_outdated_plugin` — start a Claude Code session (via OSC event injection), set up a fake `installed_plugins.json` with an old version, verify the "Update Warp plugin" chip renders in the footer
- `test_plugin_update_modal_opens` — same setup as above but in manual mode (inject a failure first), click the instructions chip, verify the modal opens with update steps
- `test_plugin_update_chip_dismiss_persists` — click dismiss on the update chip, verify it stays hidden, then bump `MINIMUM_PLUGIN_VERSION` concept (or re-render), verify it reappears for a new minimum
## 9. Files Changed
- **Modified:** `plugin_manager/mod.rs` — renamed structs, `update()` + `update_instructions()` + `needs_update()` on trait, `PluginModalKind` enum, `PluginChipKind` enum (in footer)
- **Modified:** `plugin_manager/claude.rs``update()` implementation, `MINIMUM_PLUGIN_VERSION` constant (pub(crate)), `compare_versions` (pub(crate)), update instructions `LazyLock`
- **Modified:** `cli_agent_sessions/mod.rs``plugin_version: Option<String>` on `CLIAgentSession`, populated from `SessionStart` event; rename `plugin_install_failures``plugin_auto_failures`
- **Modified:** `workspace/view/plugin_install_modal.rs` — generic instructions rendering via `set_instructions()`, copy-to-clipboard with success toast
- **Modified:** `agent_input_footer/mod.rs` — new buttons, chip visibility via `plugin_chip_kind()`, `handle_plugin_operation` helper, `plugin_operation_in_progress`
- **Modified:** `settings/ai.rs``plugin_chip_dismissed_for_version` setting
- **Modified:** `terminal/input.rs`, `terminal/view.rs`, `pane_group/mod.rs`, `pane_group/pane/terminal_pane.rs``ShowPluginInstructionsModal` event (replaces old install-only variant)
- **Modified:** `workspace/view.rs`, `workspace/mod.rs` — modal rename, instruction-kind dispatch
- **Modified:** `workspace/util.rs` — rename modal state field