/drag-pane
Drag-and-drop pane resizing implementation for two-pane plugin layouts. Covers mouse event handling via the internal/mouse package, hit region registration, drag delta calculation, width clamping, state persistence, and pane layout management. Use when working on pane resizing,
$ npx -y skills add marcus/sidecar --skill drag-pane --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/drag-pane
Context preview
The summary Claude sees to decide when to auto-load this skill.
Drag-and-drop pane resizing implementation for two-pane plugin layouts. Covers mouse event handling via the internal/mouse package, hit region registration, drag delta calculation, width clamping, state persistence, and pane layout management. Use when working on pane resizing,
SKILL.md
drag-pane.SKILL.mdname: drag-pane
description: >
Drag-and-drop pane resizing implementation for two-pane plugin layouts.
Covers mouse event handling via the internal/mouse package, hit region
registration, drag delta calculation, width clamping, state persistence,
and pane layout management. Use when working on pane resizing, drag
interactions, layout management, or adding drag-to-resize to a new plugin.
Drag-to-Resize Pane Implementation
Overview
Add drag-to-resize support for two-pane plugin layouts (sidebar + main content). Users click and drag the divider between panes to resize them.
Prerequisites
- Plugin already has a two-pane layout (sidebar + main content)
- State persistence functions exist in `internal/state/state.go` (each plugin has its own getter/setter)
- Familiarity with `internal/mouse` package
Existing Implementations
| Plugin | State Functions | Mouse File | |--------|----------------|------------| | FileBrowser | `GetFileBrowserTreeWidth()` / `SetFileBrowserTreeWidth()` | `internal/plugins/filebrowser/mouse.go` | | GitStatus | `GetGitStatusSidebarWidth()` / `SetGitStatusSidebarWidth()` | `internal/plugins/gitstatus/mouse.go` | | Conversations | `GetConversationsSideWidth()` / `SetConversationsSideWidth()` | `internal/plugins/conversations/mouse.go` | | Workspace | `GetWorkspaceSidebarWidth()` / `SetWorkspaceSidebarWidth()` | `internal/plugins/workspace/view_list.go` |
Implementation Steps
Step 1: Add Mouse Handler to Plugin Struct
import "github.com/marcus/sidecar/internal/mouse"
type Plugin struct {
// ... other fields
mouseHandler *mouse.Handler
sidebarWidth int // Current sidebar width (persisted)
}
func New() *Plugin {
return &Plugin{
mouseHandler: mouse.NewHandler(),
}
}Step 2: Define Hit Region Constants
const (
regionSidebar = "sidebar"
regionMainPane = "main-pane"
regionPaneDivider = "pane-divider"
dividerWidth = 1 // Visual divider width
)Step 3: Initialize Width on First Render (NOT in Init)
**Important:** Do NOT load width in `Init()` - plugin dimensions (`p.width`) are not available yet. Initialize lazily on first render:
func (p *Plugin) renderTwoPane() string {
p.mouseHandler.HitMap.Clear() // CRITICAL: clear every render
if p.sidebarWidth == 0 {
p.sidebarWidth = state.GetYourPluginSidebarWidth()
if p.sidebarWidth == 0 {
available := p.width - dividerWidth
p.sidebarWidth = available * 30 / 100 // Default 30%
}
}
// ... rest of render
}Step 4: Handle MouseMsg in Update
func (p *Plugin) Update(msg tea.Msg) (plugin.Plugin, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
return p.handleMouse(msg)
}
}Step 5: Create mouse.go with Handlers
func (p *Plugin) handleMouse(msg tea.MouseMsg) (*Plugin, tea.Cmd) {
action := p.mouseHandler.HandleMouse(msg)
switch action.Type {
case mouse.ActionClick:
return p.handleMouseClick(action)
case mouse.ActionDrag:
return p.handleMouseDrag(action)
case mouse.ActionDragEnd:
return p.handleMouseDragEnd()
}
return p, nil
}
func (p *Plugin) handleMouseClick(action mouse.MouseAction) (*Plugin, tea.Cmd) {
if action.Region == nil {
return p, nil
}
switch action.Region.ID {
case regionSidebar:
p.activePane = PaneSidebar
case regionMainPane:
p.activePane = PaneMain
case regionPaneDivider:
p.mouseHandler.StartDrag(action.X, action.Y, regionPaneDivider, p.sidebarWidth)
}
return p, nil
}
func (p *Plugin) handleMouseDrag(action mouse.MouseAction) (*Plugin, tea.Cmd) {
if p.mouseHandler.DragRegion() != regionPaneDivider {
return p, nil
}
startValue := p.mouseHandler.DragStartValue()
newWidth := startValue + action.DragDX
// Clamp to bounds
// NOTE: Offset varies by plugin (border styling differences):
// GitStatus: -5, FileBrowser: -6, Conversations: -5, Workspace: just dividerWidth
available := p.width - 5 - dividerWidth
minWidth := 25
maxWidth := available - 40
if newWidth < minWidth {
newWidth = minWidth
} else if newWidth > maxWidth {
newWidth = maxWidth
}
p.sidebarWidth = newWidth
return p, nil
}
func (p *Plugin) handleMouseDragEnd() (*Plugin, tea.Cmd) {
_ = state.SetYourPluginSidebarWidth(p.sidebarWidth)
return p, nil
}Step 6: Register Hit Regions in Render
**This is where most bugs occur.** Follow this pattern exactly:
func (p *Plugin) renderTwoPane() string {
p.mouseHandler.HitMap.Clear() // CRITICAL: clear every render
available := p.width - 5 - dividerWidth
sidebarWidth := p.sidebarWidth
if sidebarWidth == 0 {
sidebarWidth = available * 30 / 100
}
if sidebarWidth < 25 {
sidebarWidth = 25
}
if sidebarWidth > available-40 {
sidebarWidth = available - 40
}
mainWidth := available - sidebarWidth
p.sidebarWidth = sidebarWidth
// ... render panes and divider ...
// CRITICAL: Register in priority order (last = highest priority)
p.mouseHandler.HitMap.AddRect(regionSidebar, 0, 0, sidebarWidth, p.height, nil)
mainX := sidebarWidth + dividerWidth
p.mouseHandler.HitMap.AddRect(regionMainPane, mainX, 0, mainWidth, p.height, nil)
// Divider LAST = highest priority
dividerX := sidebarWidth
dividerHitWidth := 3 // Wider than visual for easier clicking
p.mouseHandler.HitMap.AddRect(regionPaneDivider, dividerX, 0, dividerHitWidth, p.height, nil)
return content
}Step 7: Render Visible Divider
func (p *Plugin) renderDivider(height int) string {
dividerStyle := lipgloss.NewStyle().
Foreground(styles.BorderNormal).
MarginTop(1) // Aligns with pane content (below top border)
var sb strings.Builder
for i := 0; i < height; i++Read more
name: drag-pane description: > Drag-and-drop pane resizing implementation for two-pane plugin layouts. Covers mouse event handling via the internal/mouse package, hit region registration, drag delta calculation, width clamping, state persistence, and pane layout management. Use when working on pane resizing, drag interactions, layout management, or adding drag-to-resize to a new plugin.
Drag-to-Resize Pane Implementation
Overview
Add drag-to-resize support for two-pane plugin layouts (sidebar + main content). Users click and drag the divider between panes to resize them.
Prerequisites
- Plugin already has a two-pane layout (sidebar + main content)
- State persistence functions exist in `internal/state/state.go` (each plugin has its own getter/setter)
- Familiarity with `internal/mouse` package
Existing Implementations
| Plugin | State Functions | Mouse File | |--------|----------------|------------| | FileBrowser | `GetFileBrowserTreeWidth()` / `SetFileBrowserTreeWidth()` | `internal/plugins/filebrowser/mouse.go` | | GitStatus | `GetGitStatusSidebarWidth()` / `SetGitStatusSidebarWidth()` | `internal/plugins/gitstatus/mouse.go` | | Conversations | `GetConversationsSideWidth()` / `SetConversationsSideWidth()` | `internal/plugins/conversations/mouse.go` | | Workspace | `GetWorkspaceSidebarWidth()` / `SetWorkspaceSidebarWidth()` | `internal/plugins/workspace/view_list.go` |
Implementation Steps
Step 1: Add Mouse Handler to Plugin Struct
import "github.com/marcus/sidecar/internal/mouse"
type Plugin struct {
// ... other fields
mouseHandler *mouse.Handler
sidebarWidth int // Current sidebar width (persisted)
}
func New() *Plugin {
return &Plugin{
mouseHandler: mouse.NewHandler(),
}
}Step 2: Define Hit Region Constants
const (
regionSidebar = "sidebar"
regionMainPane = "main-pane"
regionPaneDivider = "pane-divider"
dividerWidth = 1 // Visual divider width
)Step 3: Initialize Width on First Render (NOT in Init)
**Important:** Do NOT load width in `Init()` - plugin dimensions (`p.width`) are not available yet. Initialize lazily on first render:
func (p *Plugin) renderTwoPane() string {
p.mouseHandler.HitMap.Clear() // CRITICAL: clear every render
if p.sidebarWidth == 0 {
p.sidebarWidth = state.GetYourPluginSidebarWidth()
if p.sidebarWidth == 0 {
available := p.width - dividerWidth
p.sidebarWidth = available * 30 / 100 // Default 30%
}
}
// ... rest of render
}Step 4: Handle MouseMsg in Update
func (p *Plugin) Update(msg tea.Msg) (plugin.Plugin, tea.Cmd) {
switch msg := msg.(type) {
case tea.MouseMsg:
return p.handleMouse(msg)
}
}Step 5: Create mouse.go with Handlers
func (p *Plugin) handleMouse(msg tea.MouseMsg) (*Plugin, tea.Cmd) {
action := p.mouseHandler.HandleMouse(msg)
switch action.Type {
case mouse.ActionClick:
return p.handleMouseClick(action)
case mouse.ActionDrag:
return p.handleMouseDrag(action)
case mouse.ActionDragEnd:
return p.handleMouseDragEnd()
}
return p, nil
}
func (p *Plugin) handleMouseClick(action mouse.MouseAction) (*Plugin, tea.Cmd) {
if action.Region == nil {
return p, nil
}
switch action.Region.ID {
case regionSidebar:
p.activePane = PaneSidebar
case regionMainPane:
p.activePane = PaneMain
case regionPaneDivider:
p.mouseHandler.StartDrag(action.X, action.Y, regionPaneDivider, p.sidebarWidth)
}
return p, nil
}
func (p *Plugin) handleMouseDrag(action mouse.MouseAction) (*Plugin, tea.Cmd) {
if p.mouseHandler.DragRegion() != regionPaneDivider {
return p, nil
}
startValue := p.mouseHandler.DragStartValue()
newWidth := startValue + action.DragDX
// Clamp to bounds
// NOTE: Offset varies by plugin (border styling differences):
// GitStatus: -5, FileBrowser: -6, Conversations: -5, Workspace: just dividerWidth
available := p.width - 5 - dividerWidth
minWidth := 25
maxWidth := available - 40
if newWidth < minWidth {
newWidth = minWidth
} else if newWidth > maxWidth {
newWidth = maxWidth
}
p.sidebarWidth = newWidth
return p, nil
}
func (p *Plugin) handleMouseDragEnd() (*Plugin, tea.Cmd) {
_ = state.SetYourPluginSidebarWidth(p.sidebarWidth)
return p, nil
}Step 6: Register Hit Regions in Render
**This is where most bugs occur.** Follow this pattern exactly:
func (p *Plugin) renderTwoPane() string {
p.mouseHandler.HitMap.Clear() // CRITICAL: clear every render
available := p.width - 5 - dividerWidth
sidebarWidth := p.sidebarWidth
if sidebarWidth == 0 {
sidebarWidth = available * 30 / 100
}
if sidebarWidth < 25 {
sidebarWidth = 25
}
if sidebarWidth > available-40 {
sidebarWidth = available - 40
}
mainWidth := available - sidebarWidth
p.sidebarWidth = sidebarWidth
// ... render panes and divider ...
// CRITICAL: Register in priority order (last = highest priority)
p.mouseHandler.HitMap.AddRect(regionSidebar, 0, 0, sidebarWidth, p.height, nil)
mainX := sidebarWidth + dividerWidth
p.mouseHandler.HitMap.AddRect(regionMainPane, mainX, 0, mainWidth, p.height, nil)
// Divider LAST = highest priority
dividerX := sidebarWidth
dividerHitWidth := 3 // Wider than visual for easier clicking
p.mouseHandler.HitMap.AddRect(regionPaneDivider, dividerX, 0, dividerHitWidth, p.height, nil)
return content
}Step 7: Render Visible Divider
func (p *Plugin) renderDivider(height int) string {
dividerStyle := lipgloss.NewStyle().
Foreground(styles.BorderNormal).
MarginTop(1) // Aligns with pane content (below top border)
var sb strings.Builder
for i := 0; i < height; i++You might never open your editor again. Status: Ready for daily use. Please report any issues you encounter.
Other skills on sidecar.
- /create-adapter
Create conversation adapters for importing AI chat history from different tools (Claude Code, Cursor, Warp, Codex, etc.). Covers the adapter.Adapter interface, caching strategies, incremental parsing, watch/FD management, and performance standards. Use when creating a new
Open skill - /create-modal
Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text, Buttons, Input, Textarea, Checkbox, List, When, Custom), rendering with OverlayModal, and keyboard/mouse handling. Use when adding modals or dialogs to the
Open skill - /create-plugin
Create new sidecar plugins implementing the plugin.Plugin interface, rendering views with Bubble Tea, handling keyboard input via keymap contexts, and integrating with the app shell (footer hints, event bus, adapters). Use when creating a new plugin, modifying plugin
Open skill - /create-prompt
Create prompts for sidecar workspaces. Covers prompt structure (name, ticketMode, body), template variables (ticket with fallbacks), config file locations (global vs project), and scope overrides. Use when creating or modifying prompts in sidecar config files.
Open skill - /create-theme
Create custom color themes for Sidecar, including base theme selection, color overrides, gradient borders, tab styles, per-project themes, community themes, and programmatic theme registration. Use when creating or modifying themes, adjusting UI appearance, or debugging
Open skill - /feature-flags
Creating and using feature flags in sidecar for gating experimental functionality. Covers flag registration, checking flags in code, config file and CLI overrides, and priority resolution. Use when adding feature flags, toggling features, or gating new functionality behind flags.
Open skill

