/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,
Claude Code
$ npx -y skills add marcus/sidecar --skill drag-pane --agent claude-code
Codex
$ npx skills add marcus/sidecar --skill drag-pane -a codex -y
Cursor
$ npx skills add marcus/sidecar --skill drag-pane -a cursor -y
Gemini CLI
$ npx skills add marcus/sidecar --skill drag-pane -a gemini-cli -y
GitHub Copilot
$ npx skills add marcus/sidecar --skill drag-pane -a github-copilot -y
OpenCode
$ npx skills add marcus/sidecar --skill drag-pane -a opencode -y
Windsurf
$ npx skills add marcus/sidecar --skill drag-pane -a windsurf -y
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 (project) | `GetWorkspaceSidebarWidth()` / `SetWorkspaceSidebarWidth()` | `internal/plugins/workspace/view_list.go` | | Sessions (global workspace) | `GetWorkspaceSidebarWidth()` / `SetWorkspaceSidebarWidth()` | `internal/overview/workspaces.go` |
Windowing parity: project and global workspaces are one feature
The project workspace (`internal/plugins/workspace`) and the global Workspaces browser shown as **Sessions** in the navbar (`internal/overview`) are two projections of the same windowing model. They are not independent surfaces that happen to look similar.
**If a change affects panes, splits, drag handles, pane borders, focus chrome, or pane hit regions in one of them, it affects the other.** The rule is structural, not a habit to remember:
- `internal/panelayout` owns pane-tree structure and geometry.
- `internal/paneframe` owns presentation: chrome geometry (`Inset`, `Geometry`),
the leaf border states (`Chrome`, `WrapLeaf`), the drag handle (`RenderDividerHandle`, `DividerHitBox`, `HandleStateFor`), the compositor (`Compose`, `ComposeLeaf`, `RenderContent`), the chrome-aware floors (`ChromeFloors`), and the one order hit regions are registered in (`RegisterRegions`), and click-to-focus (`LeafAt`, `FocusLeafAt`).
- Each surface implements `paneframe.Host` and `paneframe.RegionSink` in exactly
one file: `internal/plugins/workspace/pane_host.go` and `internal/overview/pane_host.go`.
**Focus is one value, answered from geometry.** `Host.Focus()` draws the ring and `Host.SetFocus()` moves it; there is no third place a surface may record who is being typed into, so a surface whose live terminal holds the keyboard separately gives it up inside its own focus setter (`workspace.setFocusTarget`, `overview.focusPreviewLeaf`). A pointer moves focus through `paneframe.FocusLeafAt`, which resolves the leaf from its OUTER **box**, not from the hit region the press landed on — a terminal leaf owns no click-to-focus region, because its presses belong to the live pane and are forwarded to tmux. `FocusLeafAt` moves focus and nothing else, so the press still reaches whichever region claimed it, and it declines the divider's widened target so a press one cell off a handle resizes without also re-focusing. Hanging focus off the region handlers instead is td-43db92: one focus call per leaf kind, and the ring drawn on a neighbour for the kind nobody remembered.
`Host.Layout()` must answer the tree the surface last **drew**, not one it could place. A view that replaces the preview — the kanban board, a modal — draws no tree, and geometry that outlives the frame lets a click on whatever is drawn there move pane focus instead. The project plugin records the layout beside the hit regions it earned (`paneFrame`/`paneFrameDrawn`, cleared with the hit map at the top of `View`); the global browser's `previewPeerBox()` already refuses when the preview is not drawn.
**Do not add a second compositor, a second border rule, or a second divider renderer.** If a behaviour belongs to windowing, it goes in `paneframe`; if it belongs to one surface's content, it goes in that surface's host file. Both surfaces then get it at once.
Tests that hold this: `internal/paneframe/paneframe_test.go`, `internal/plugins/workspace/pane_peer_chrome_test.go`, and `internal/overview/pane_peer_chrome_test.go`.
The header's layout button is part of the same model
A pane can be **moved**, not only resized, and that entry point lives in the same chrome the drag handle does. `internal/panereposition` owns the shared interaction policy — the modal controller, the header adapter, the structural fingerprint, and the graft helpers — while `panelayout.PlanMove`/`ApplyMove` own the structure. There are three hosts, and all three get it: project Workspaces, global Sessions, and the app content decks.
- **Reserve.** `panereposition.ReserveMovableHeader(width, movable, hasClose)`
wraps `ui.ReserveHeaderControls` and returns the tab strip's width plus each control's column. Each host binds it once per frame in its own `reserveHeader`/`composeHeader` pair, and every header renderer, tab strip and region on that surface goes through those — a strip laid out for one reserve while the header composes another is how a tab click lands on the wrong tab. The **drop order as the row narrows is layout first, close `×` last**, all-or-nothing per control: a clipped button is a target whose meaning cannot be recovered.
- **`movable` comes from the tree** (`panereposition.Movable`): false with no
tree, and false for a tree of one leaf, because `PlanMove` refuses every destination on a single leaf. A header with no lea
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 (project) | `GetWorkspaceSidebarWidth()` / `SetWorkspaceSidebarWidth()` | `internal/plugins/workspace/view_list.go` | | Sessions (global workspace) | `GetWorkspaceSidebarWidth()` / `SetWorkspaceSidebarWidth()` | `internal/overview/workspaces.go` |
Windowing parity: project and global workspaces are one feature
The project workspace (`internal/plugins/workspace`) and the global Workspaces browser shown as **Sessions** in the navbar (`internal/overview`) are two projections of the same windowing model. They are not independent surfaces that happen to look similar.
**If a change affects panes, splits, drag handles, pane borders, focus chrome, or pane hit regions in one of them, it affects the other.** The rule is structural, not a habit to remember:
- `internal/panelayout` owns pane-tree structure and geometry.
- `internal/paneframe` owns presentation: chrome geometry (`Inset`, `Geometry`),
the leaf border states (`Chrome`, `WrapLeaf`), the drag handle (`RenderDividerHandle`, `DividerHitBox`, `HandleStateFor`), the compositor (`Compose`, `ComposeLeaf`, `RenderContent`), the chrome-aware floors (`ChromeFloors`), and the one order hit regions are registered in (`RegisterRegions`), and click-to-focus (`LeafAt`, `FocusLeafAt`).
- Each surface implements `paneframe.Host` and `paneframe.RegionSink` in exactly
one file: `internal/plugins/workspace/pane_host.go` and `internal/overview/pane_host.go`.
**Focus is one value, answered from geometry.** `Host.Focus()` draws the ring and `Host.SetFocus()` moves it; there is no third place a surface may record who is being typed into, so a surface whose live terminal holds the keyboard separately gives it up inside its own focus setter (`workspace.setFocusTarget`, `overview.focusPreviewLeaf`). A pointer moves focus through `paneframe.FocusLeafAt`, which resolves the leaf from its OUTER **box**, not from the hit region the press landed on — a terminal leaf owns no click-to-focus region, because its presses belong to the live pane and are forwarded to tmux. `FocusLeafAt` moves focus and nothing else, so the press still reaches whichever region claimed it, and it declines the divider's widened target so a press one cell off a handle resizes without also re-focusing. Hanging focus off the region handlers instead is td-43db92: one focus call per leaf kind, and the ring drawn on a neighbour for the kind nobody remembered.
`Host.Layout()` must answer the tree the surface last **drew**, not one it could place. A view that replaces the preview — the kanban board, a modal — draws no tree, and geometry that outlives the frame lets a click on whatever is drawn there move pane focus instead. The project plugin records the layout beside the hit regions it earned (`paneFrame`/`paneFrameDrawn`, cleared with the hit map at the top of `View`); the global browser's `previewPeerBox()` already refuses when the preview is not drawn.
**Do not add a second compositor, a second border rule, or a second divider renderer.** If a behaviour belongs to windowing, it goes in `paneframe`; if it belongs to one surface's content, it goes in that surface's host file. Both surfaces then get it at once.
Tests that hold this: `internal/paneframe/paneframe_test.go`, `internal/plugins/workspace/pane_peer_chrome_test.go`, and `internal/overview/pane_peer_chrome_test.go`.
The header's layout button is part of the same model
A pane can be **moved**, not only resized, and that entry point lives in the same chrome the drag handle does. `internal/panereposition` owns the shared interaction policy — the modal controller, the header adapter, the structural fingerprint, and the graft helpers — while `panelayout.PlanMove`/`ApplyMove` own the structure. There are three hosts, and all three get it: project Workspaces, global Sessions, and the app content decks.
- **Reserve.** `panereposition.ReserveMovableHeader(width, movable, hasClose)`
wraps `ui.ReserveHeaderControls` and returns the tab strip's width plus each control's column. Each host binds it once per frame in its own `reserveHeader`/`composeHeader` pair, and every header renderer, tab strip and region on that surface goes through those — a strip laid out for one reserve while the header composes another is how a tab click lands on the wrong tab. The **drop order as the row narrows is layout first, close `×` last**, all-or-nothing per control: a clipped button is a target whose meaning cannot be recovered.
- **`movable` comes from the tree** (`panereposition.Movable`): false with no
tree, and false for a tree of one leaf, because `PlanMove` refuses every destination on a single leaf. A header with no lea
Always check if you are running in Sidecar: run sidecar agents for capabilities. You might never open your editor again. Status: Ready for daily use. Please report any issues you encounter. Documentation · Getting Started · Comprehensive List of Features
Other skills on sidecar.
More skills from other plugins.
10x
Personalized audit that teaches the advanced Paradev (formerly Superset) features the user isn't using yet (automations, parallel…
ao-actions
Direct actions on one Orchestrator session or PR: stop or interrupt, resume, merge a PR, start a review, screenshot a session's…
capabilities
Your capability catalog — read this at boot. Lists the temporal date-range skills and the external integrations (reached via the…
Questions about drag-pane.
- Which coding agents can use drag-pane?
- drag-pane installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

