13 KiB
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:
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:
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:
SessionStartevent path:register_listener()now acceptsplugin_versionas a parameter, threaded from theSessionStartnotification payload. This is the primary path.- Mid-session install path:
register_cli_agent_listener()(called after install/update succeeds) setsplugin_versiontoMINIMUM_PLUGIN_VERSIONto 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:
claude plugin marketplace remove claude-code-warpclaude plugin marketplace add warpdotdev/claude-code-warpclaude plugin install warp@claude-code-warp- 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-pluginssuffices) - 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, dispatchesAgentInputFooterAction::UpdatePluginupdate_instructions_button: "Plugin update instructions",Icon::Info, dispatchesAgentInputFooterAction::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_versionisNoneor <MINIMUM_PLUGIN_VERSION→PluginChipKind::Update - (
Nonemeans 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— verifyupdate_instructions()returns non-empty stepsclaude_manager_returns_install_instructions— verifyinstall_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— setsession.plugin_versionto"1.1.0", verify update chip shownupdate_chip_shown_when_plugin_version_is_none— listener connected but noplugin_version, verify update chip shownno_chip_when_plugin_version_meets_minimum— setsession.plugin_versionto"2.0.0", verify no chipupdate_chip_hidden_when_dismissed_for_current_versionupdate_chip_shown_when_dismissed_for_older_versionupdate_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 fakeinstalled_plugins.jsonwith an old version, verify the "Update Warp plugin" chip renders in the footertest_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 stepstest_plugin_update_chip_dismiss_persists— click dismiss on the update chip, verify it stays hidden, then bumpMINIMUM_PLUGIN_VERSIONconcept (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,PluginModalKindenum,PluginChipKindenum (in footer) - Modified:
plugin_manager/claude.rs—update()implementation,MINIMUM_PLUGIN_VERSIONconstant (pub(crate)),compare_versions(pub(crate)), update instructionsLazyLock - Modified:
cli_agent_sessions/mod.rs—plugin_version: Option<String>onCLIAgentSession, populated fromSessionStartevent; renameplugin_install_failures→plugin_auto_failures - Modified:
workspace/view/plugin_install_modal.rs— generic instructions rendering viaset_instructions(), copy-to-clipboard with success toast - Modified:
agent_input_footer/mod.rs— new buttons, chip visibility viaplugin_chip_kind(),handle_plugin_operationhelper,plugin_operation_in_progress - Modified:
settings/ai.rs—plugin_chip_dismissed_for_versionsetting - Modified:
terminal/input.rs,terminal/view.rs,pane_group/mod.rs,pane_group/pane/terminal_pane.rs—ShowPluginInstructionsModalevent (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