Files
galaxy/specs/APP-3781/TECH.md
T

149 lines
10 KiB
Markdown

# APP-3781: Tech Spec — Plugin Instructions in a Split Pane
Linear: [APP-3781](https://linear.app/warpdotdev/issue/APP-3781/move-plugin-installation-settings-into-a-dedicated-pane)
## Problem
The plugin install/update instructions are rendered as a modal (`PluginInstallModal`) that overlays the entire workspace. This change replaces the modal with a split terminal pane containing a rich-content instructions block with a manual close button.
## Relevant Code
**Current modal (to be deleted):**
- `app/src/workspace/view/plugin_install_modal.rs` — the modal view, step rendering, copy-to-clipboard, `render_step_number`
- `app/src/workspace/view.rs:851``plugin_install_modal` field on `Workspace`
- `app/src/workspace/view.rs:14027-14064``handle_plugin_install_modal_event` and `open_plugin_install_modal`
- `app/src/workspace/view.rs:19587-19590` — modal render in workspace overlay stack
- `app/src/workspace/util.rs:119,156,197``is_plugin_install_modal_open` in `WorkspaceState`
**Event chain (to be renamed):**
- `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs:1788,1959``ShowPluginInstallModal` / `ShowPluginInstructionsModal` actions and events
- `app/src/terminal/input.rs:1047``InputEvent::ShowPluginInstructionsModal`
- `app/src/terminal/view.rs:1892``terminal::view::Event::ShowPluginInstructionsModal`
- `app/src/pane_group/pane/terminal_pane.rs:664-668` — forwards to `pane_group::Event`
- `app/src/pane_group/mod.rs:695``pane_group::Event::ShowPluginInstructionsModal`
- `app/src/workspace/view.rs:11231` — workspace handler
**Plugin instructions data:**
- `app/src/terminal/cli_agent_sessions/plugin_manager/mod.rs``PluginInstructions`, `PluginInstructionStep`, `PluginModalKind`
- `app/src/terminal/cli_agent_sessions/plugin_manager/claude.rs:126-170` — static `INSTALL_INSTRUCTIONS` and `UPDATE_INSTRUCTIONS`
**Patterns to follow:**
- `app/src/terminal/view/zero_state_block.rs``TerminalViewZeroStateBlock` (container styling, positioned close/dismiss button via `Stack::with_positioned_child`)
- `app/src/terminal/view/rich_content.rs``RichContent`, `RichContentMetadata`, `TerminalView::insert_rich_content`
- `app/src/terminal/model/rich_content.rs``RichContentType` enum
- `app/src/ai/blocklist/code_block/mod.rs``render_code_block_plain`, `CodeBlockOptions`, `CodeSnippetButtonHandles` (for code block rendering)
- `app/src/pane_group/mod.rs:3750-3766``PaneGroup::add_terminal_pane` returns `TerminalPaneId`
- `app/src/pane_group/mod.rs:6078-6085``PaneGroup::terminal_view_from_pane_id` returns `ViewHandle<TerminalView>`
- `app/src/pane_group/mod.rs:1057-1072``PaneGroup::smart_split_direction`
- `app/src/workspace/view.rs:569``WORKFLOW_AND_ENV_VAR_SPLIT_RATIO`
## Current State
Event chain: info (ⓘ) button click → `AgentInputFooterAction::ShowPluginInstallModal``AgentInputFooterEvent::ShowPluginInstructionsModal(agent, kind)``InputEvent``terminal::view::Event``TerminalPane``pane_group::Event``Workspace::open_plugin_install_modal`.
The workspace resolves `PluginInstructions` from `plugin_manager_for(agent)`, sets them on the modal view, sets `is_plugin_install_modal_open = true`, and focuses the modal. The modal renders as a centered overlay with dimmed background.
## Proposed Changes
### 1. New file: `app/src/terminal/view/plugin_instructions_block.rs`
A `PluginInstructionsBlock` view that renders plugin instructions as terminal rich content with a close button.
**Struct:**
```rust
struct PluginInstructionsBlock {
instructions: &'static PluginInstructions,
close_button_mouse_state: MouseStateHandle,
step_code_handles: Vec<CodeSnippetButtonHandles>,
should_hide: bool,
}
```
**Rendering:** Uses a `Stack` with:
- Main child: a bordered `Container` (matching `TerminalViewZeroStateBlock` style — horizontal terminal padding, vertical padding, top/bottom border) containing a `Flex::column` with title, subtitle, and numbered step rows
- Positioned child: a close button (using `appearance.ui_builder().close_button()`) in the top-right corner via `OffsetPositioning::offset_from_parent`
Each step row reuses the `render_step_number` badge (moved from the modal into this file as a private `fn`, since no other caller exists) and `render_code_block_plain` for the command code block with copy button.
**Actions and events:**
- `PluginInstructionsBlockAction::Close` — sets `should_hide = true`, emits a close event to the owning `TerminalView` so the rich-content item is removed from the blocklist sumtree, and calls `ctx.notify()`
- `PluginInstructionsBlockAction::CopyCommand(usize)` — copies to clipboard via `ctx.clipboard().write()`, shows toast via `ToastStack::handle(ctx)` singleton (same pattern as the modal, `plugin_install_modal.rs:248-257`)
- `Entity::Event = PluginInstructionsBlockEvent``Close` bubbles to `TerminalView` for rich-content cleanup; toast and clipboard are still handled directly in the block
When `should_hide` is true, `render` returns `Empty::new().finish()`.
### 2. New `RichContentType` and `RichContentMetadata` variants
In `app/src/terminal/model/rich_content.rs`, add `PluginInstructionsBlock` to the `RichContentType` enum.
In `app/src/terminal/view/rich_content.rs`, add `PluginInstructionsBlock` to the `RichContentMetadata` enum.
### 3. Rename event variants
Rename across the entire chain to reflect pane-based behavior:
- `AgentInputFooterAction::ShowPluginInstallModal``OpenPluginInstallInstructionsPane`
- `AgentInputFooterAction::ShowPluginInstructionsModal``OpenPluginUpdateInstructionsPane`
- `AgentInputFooterEvent::ShowPluginInstructionsModal``OpenPluginInstructionsPane` (carries `PluginModalKind`)
- `InputEvent::ShowPluginInstructionsModal``OpenPluginInstructionsPane` (carries `PluginModalKind`)
- `terminal::view::Event::ShowPluginInstructionsModal``OpenPluginInstructionsPane` (carries `PluginModalKind`)
- `pane_group::Event::ShowPluginInstructionsModal``OpenPluginInstructionsPane` (carries `PluginModalKind`)
### 4. Workspace: replace modal with split pane creation
Replace `Workspace::open_plugin_install_modal` with `open_plugin_instructions_pane`. The method:
1. Resolves `PluginInstructions` from `plugin_manager_for(agent)` (same as before)
2. Creates a new terminal pane via `PaneGroup::add_terminal_pane_ignoring_default_session_mode(direction, None, ctx)` so the pane stays in terminal mode even if the user's default mode for new sessions is Agent Mode. Split panes do not show the homepage zero-state, so no `hide_homepage` option is needed.
3. Gets the `ViewHandle<TerminalView>` via `PaneGroup::terminal_view_from_pane_id(pane_id, ctx)`
4. Inside `terminal_view.update()`, creates a `PluginInstructionsBlock` view and calls `view.insert_rich_content(...)` to add it
This keeps `TerminalView` fully decoupled from plugin concepts — the workspace owns the orchestration, and the block is just another rich content view.
### 5. Delete modal code
Remove:
- `app/src/workspace/view/plugin_install_modal.rs` (entire file)
- `mod plugin_install_modal` declaration (`view.rs:13`)
- `use crate::workspace::view::plugin_install_modal::{PluginInstallModal, PluginInstallModalEvent}` import (`view.rs:129`)
- `plugin_install_modal` field from `Workspace` struct (`view.rs:851`)
- `plugin_install_modal` field initialization in `Workspace::new` (`view.rs:2393-2394`)
- `is_plugin_install_modal_open` from `WorkspaceState` and all references in `is_any_non_palette_modal_open`, `close_all_modals` (`util.rs:119,156,197`)
- `handle_plugin_install_modal_event` (`view.rs:14027-14040`)
- Modal construction and subscription in `Workspace::new` (`view.rs:1896-1901`)
- Modal render in overlay stack (`view.rs:19587-19590`)
- `view::plugin_install_modal::init(app)` call (`workspace/mod.rs:87`)
## End-to-End Flow
1. User clicks info (ⓘ) button on install/update chip
2. `AgentInputFooterAction::OpenPluginInstallInstructionsPane` (or update variant) dispatched
3. Event bubbles: `AgentInputFooter``Input``TerminalView``TerminalPane``PaneGroup``Workspace`
4. `Workspace::open_plugin_instructions_pane(agent, kind, ctx)` called
5. Workspace resolves `PluginInstructions` from `CliAgentPluginManager`
6. Inside `active_tab_pane_group().update()`:
- Creates terminal pane with `add_terminal_pane_ignoring_default_session_mode(Direction::Right, None, ctx)``TerminalPaneId`
- Gets `ViewHandle<TerminalView>` via `terminal_view_from_pane_id`
- Inside `terminal_view.update()`: creates `PluginInstructionsBlock`, calls `insert_rich_content`
7. New pane renders with instructions block at top; user types/runs commands below it
8. User clicks close (X) button → block hides, the corresponding rich-content item is removed from the blocklist sumtree, and the terminal pane remains functional
## Risks and Mitigations
**Block insertion timing.** `insert_rich_content` appends to the block list model and works before session bootstrap. The block will be visible while the session bootstraps (sub-second). No special handling needed.
**Toast access.** The block accesses `ToastStack::handle(ctx)` directly (it's a singleton), same pattern as the modal. No event bubbling required for toasts.
**`PluginInstructions` visibility.** `PluginInstructions` and `PluginInstructionStep` are currently `pub(crate)` in `plugin_manager/mod.rs`. The new block file is within the same crate, so no visibility changes needed.
## Testing and Validation
- `cargo check` — no remaining references to deleted modal types
- `cargo fmt` and `cargo clippy` per presubmit
- Manual test: click install info button → split pane with instructions. Copy command → clipboard + toast. Run commands → instructions block persists. Click close (X) → block hides. Close pane → clean.
- Both flows: verify install and update info buttons show correct instructions.
## Follow-ups
- The two `AgentInputFooterAction` variants (`OpenPluginInstallInstructionsPane` for install, `OpenPluginUpdateInstructionsPane` for update) could be collapsed into a single variant carrying `PluginModalKind`. Left as-is for minimal diff, can unify later.