10 KiB
APP-3781: Tech Spec — Plugin Instructions in a Split Pane
Linear: APP-3781
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_numberapp/src/workspace/view.rs:851—plugin_install_modalfield onWorkspaceapp/src/workspace/view.rs:14027-14064—handle_plugin_install_modal_eventandopen_plugin_install_modalapp/src/workspace/view.rs:19587-19590— modal render in workspace overlay stackapp/src/workspace/util.rs:119,156,197—is_plugin_install_modal_openinWorkspaceState
Event chain (to be renamed):
app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs:1788,1959—ShowPluginInstallModal/ShowPluginInstructionsModalactions and eventsapp/src/terminal/input.rs:1047—InputEvent::ShowPluginInstructionsModalapp/src/terminal/view.rs:1892—terminal::view::Event::ShowPluginInstructionsModalapp/src/pane_group/pane/terminal_pane.rs:664-668— forwards topane_group::Eventapp/src/pane_group/mod.rs:695—pane_group::Event::ShowPluginInstructionsModalapp/src/workspace/view.rs:11231— workspace handler
Plugin instructions data:
app/src/terminal/cli_agent_sessions/plugin_manager/mod.rs—PluginInstructions,PluginInstructionStep,PluginModalKindapp/src/terminal/cli_agent_sessions/plugin_manager/claude.rs:126-170— staticINSTALL_INSTRUCTIONSandUPDATE_INSTRUCTIONS
Patterns to follow:
app/src/terminal/view/zero_state_block.rs—TerminalViewZeroStateBlock(container styling, positioned close/dismiss button viaStack::with_positioned_child)app/src/terminal/view/rich_content.rs—RichContent,RichContentMetadata,TerminalView::insert_rich_contentapp/src/terminal/model/rich_content.rs—RichContentTypeenumapp/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_panereturnsTerminalPaneIdapp/src/pane_group/mod.rs:6078-6085—PaneGroup::terminal_view_from_pane_idreturnsViewHandle<TerminalView>app/src/pane_group/mod.rs:1057-1072—PaneGroup::smart_split_directionapp/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:
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(matchingTerminalViewZeroStateBlockstyle — horizontal terminal padding, vertical padding, top/bottom border) containing aFlex::columnwith title, subtitle, and numbered step rows - Positioned child: a close button (using
appearance.ui_builder().close_button()) in the top-right corner viaOffsetPositioning::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— setsshould_hide = true, emits a close event to the owningTerminalViewso the rich-content item is removed from the blocklist sumtree, and callsctx.notify()PluginInstructionsBlockAction::CopyCommand(usize)— copies to clipboard viactx.clipboard().write(), shows toast viaToastStack::handle(ctx)singleton (same pattern as the modal,plugin_install_modal.rs:248-257)Entity::Event = PluginInstructionsBlockEvent—Closebubbles toTerminalViewfor 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→OpenPluginInstallInstructionsPaneAgentInputFooterAction::ShowPluginInstructionsModal→OpenPluginUpdateInstructionsPaneAgentInputFooterEvent::ShowPluginInstructionsModal→OpenPluginInstructionsPane(carriesPluginModalKind)InputEvent::ShowPluginInstructionsModal→OpenPluginInstructionsPane(carriesPluginModalKind)terminal::view::Event::ShowPluginInstructionsModal→OpenPluginInstructionsPane(carriesPluginModalKind)pane_group::Event::ShowPluginInstructionsModal→OpenPluginInstructionsPane(carriesPluginModalKind)
4. Workspace: replace modal with split pane creation
Replace Workspace::open_plugin_install_modal with open_plugin_instructions_pane. The method:
- Resolves
PluginInstructionsfromplugin_manager_for(agent)(same as before) - 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 nohide_homepageoption is needed. - Gets the
ViewHandle<TerminalView>viaPaneGroup::terminal_view_from_pane_id(pane_id, ctx) - Inside
terminal_view.update(), creates aPluginInstructionsBlockview and callsview.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_modaldeclaration (view.rs:13)use crate::workspace::view::plugin_install_modal::{PluginInstallModal, PluginInstallModalEvent}import (view.rs:129)plugin_install_modalfield fromWorkspacestruct (view.rs:851)plugin_install_modalfield initialization inWorkspace::new(view.rs:2393-2394)is_plugin_install_modal_openfromWorkspaceStateand all references inis_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
- User clicks info (ⓘ) button on install/update chip
AgentInputFooterAction::OpenPluginInstallInstructionsPane(or update variant) dispatched- Event bubbles:
AgentInputFooter→Input→TerminalView→TerminalPane→PaneGroup→Workspace Workspace::open_plugin_instructions_pane(agent, kind, ctx)called- Workspace resolves
PluginInstructionsfromCliAgentPluginManager - 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>viaterminal_view_from_pane_id - Inside
terminal_view.update(): createsPluginInstructionsBlock, callsinsert_rich_content
- Creates terminal pane with
- New pane renders with instructions block at top; user types/runs commands below it
- 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 typescargo fmtandcargo clippyper 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
AgentInputFooterActionvariants (OpenPluginInstallInstructionsPanefor install,OpenPluginUpdateInstructionsPanefor update) could be collapsed into a single variant carryingPluginModalKind. Left as-is for minimal diff, can unify later.