244 lines
13 KiB
Markdown
244 lines
13 KiB
Markdown
# 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
|