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

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_number
  • app/src/workspace/view.rs:851plugin_install_modal field on Workspace
  • app/src/workspace/view.rs:14027-14064handle_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,197is_plugin_install_modal_open in WorkspaceState

Event chain (to be renamed):

  • app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs:1788,1959ShowPluginInstallModal / ShowPluginInstructionsModal actions and events
  • app/src/terminal/input.rs:1047InputEvent::ShowPluginInstructionsModal
  • app/src/terminal/view.rs:1892terminal::view::Event::ShowPluginInstructionsModal
  • app/src/pane_group/pane/terminal_pane.rs:664-668 — forwards to pane_group::Event
  • app/src/pane_group/mod.rs:695pane_group::Event::ShowPluginInstructionsModal
  • app/src/workspace/view.rs:11231 — workspace handler

Plugin instructions data:

  • app/src/terminal/cli_agent_sessions/plugin_manager/mod.rsPluginInstructions, 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.rsTerminalViewZeroStateBlock (container styling, positioned close/dismiss button via Stack::with_positioned_child)
  • app/src/terminal/view/rich_content.rsRichContent, RichContentMetadata, TerminalView::insert_rich_content
  • app/src/terminal/model/rich_content.rsRichContentType enum
  • app/src/ai/blocklist/code_block/mod.rsrender_code_block_plain, CodeBlockOptions, CodeSnippetButtonHandles (for code block rendering)
  • app/src/pane_group/mod.rs:3750-3766PaneGroup::add_terminal_pane returns TerminalPaneId
  • app/src/pane_group/mod.rs:6078-6085PaneGroup::terminal_view_from_pane_id returns ViewHandle<TerminalView>
  • app/src/pane_group/mod.rs:1057-1072PaneGroup::smart_split_direction
  • app/src/workspace/view.rs:569WORKFLOW_AND_ENV_VAR_SPLIT_RATIO

Current State

Event chain: info (ⓘ) button click → AgentInputFooterAction::ShowPluginInstallModalAgentInputFooterEvent::ShowPluginInstructionsModal(agent, kind)InputEventterminal::view::EventTerminalPanepane_group::EventWorkspace::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 (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 = PluginInstructionsBlockEventClose 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::ShowPluginInstallModalOpenPluginInstallInstructionsPane
  • AgentInputFooterAction::ShowPluginInstructionsModalOpenPluginUpdateInstructionsPane
  • AgentInputFooterEvent::ShowPluginInstructionsModalOpenPluginInstructionsPane (carries PluginModalKind)
  • InputEvent::ShowPluginInstructionsModalOpenPluginInstructionsPane (carries PluginModalKind)
  • terminal::view::Event::ShowPluginInstructionsModalOpenPluginInstructionsPane (carries PluginModalKind)
  • pane_group::Event::ShowPluginInstructionsModalOpenPluginInstructionsPane (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: AgentInputFooterInputTerminalViewTerminalPanePaneGroupWorkspace
  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.