Skip to content
Agent Workspaces
Skill

/create-modal

Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text, Buttons, Input, Textarea, Checkbox, Select, List, Combo, When, Custom), rendering with OverlayModal, and keyboard/mouse handling. Use when adding modals or

From plugin
sidecar
1.1k19 skills

Claude Code

$ npx -y skills add marcus/sidecar --skill create-modal --agent claude-code

SKILL.md

create-modal.SKILL.md
name: create-modal
description: Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text, Buttons, Input, Textarea, Checkbox, Select, List, Combo, When, Custom), rendering with OverlayModal, and keyboard/mouse handling. Use when adding modals or dialogs to the application.

Creating Declarative Modals

Use the `internal/modal` package. The library handles keyboard navigation, mouse hit regions, hover states, and scrolling automatically.

Quick Start

import "github.com/marcus/sidecar/internal/modal"

// 1. Create the modal
m := modal.New("Delete Worktree?",
    modal.WithWidth(58),
    modal.WithVariant(modal.VariantDanger),
    modal.WithPrimaryAction("delete"),
).
    AddSection(modal.Text("Name: " + wt.Name)).
    AddSection(modal.Spacer()).
    AddSection(modal.Buttons(
        modal.Btn(" Delete ", "delete", modal.BtnDanger()),
        modal.Btn(" Cancel ", "cancel"),
    ))

// 2. Render in View
func (p *Plugin) View(width, height int) string {
    background := p.renderListView(width, height)
    rendered := p.myModal.Render(width, height, p.mouseHandler)
    return ui.OverlayModal(background, rendered, width, height)
}

// 3. Handle input in Update
case tea.KeyMsg:
    action, cmd := p.myModal.HandleKey(msg)
    if action != "" {
        return p.handleAction(action) // "delete", "cancel", etc.
    }
    return p, cmd

case tea.MouseMsg:
    action := p.myModal.HandleMouse(msg, p.mouseHandler)
    if action != "" {
        return p.handleAction(action)
    }
    return p, nil

Critical: Modal Initialization Pattern

The modal must exist before input handling. Create an `ensure` function called in **both** View and Update:

func (p *Plugin) ensureMyModal() {
    if p.targetItem == nil {
        return // Required state missing
    }

    modalW := 50
    if modalW > p.width-4 {
        modalW = p.width - 4
    }
    if modalW < 20 {
        modalW = 20
    }

    // Only rebuild if needed
    if p.myModal != nil && p.myModalWidthCache == modalW {
        return
    }
    p.myModalWidthCache = modalW

    p.myModal = modal.New("Title", modal.WithWidth(modalW), ...).
        AddSection(...)
}

**Call `ensureModal()` before the nil check in key handlers:**

func (p *Plugin) handleMyModalKeys(msg tea.KeyMsg) tea.Cmd {
    p.ensureMyModal()  // CRITICAL: Before nil check
    if p.myModal == nil {
        return nil
    }
    action, cmd := p.myModal.HandleKey(msg)
    // ...
}

Without this, the first keypress after opening drops because View runs after Update in bubbletea.

Constructor and Options

m := modal.New(title string, opts ...Option)

| Option | Description | Default | |--------|-------------|---------| | `WithWidth(int)` | Modal width in characters | 50 | | `WithVariant(Variant)` | Visual style | `VariantDefault` | | `WithPrimaryAction(string)` | Action ID for Enter on inputs | "" | | `WithHints(bool)` | Show "Tab to switch..." hint | true | | `WithCloseOnBackdropClick(bool)` | Backdrop click returns "cancel" | true |

**Variants:** `VariantDefault`, `VariantDanger` (red), `VariantWarning` (yellow), `VariantInfo` (blue)

Built-in Sections

Text and Spacer

modal.Text("Static text with auto line wrapping")
modal.Spacer()  // Single blank line

Buttons

modal.Buttons(
    modal.Btn(" Save ", "save"),              // Standard button
    modal.Btn(" Delete ", "delete", modal.BtnDanger()),  // Red
    modal.Btn(" Submit ", "submit", modal.BtnPrimary()), // Primary
    modal.Btn(" Cancel ", "cancel"),
)
  • Include padding in labels: `" Save "` not `"Save"`
  • Button IDs are returned as actions
  • Tab/Shift+Tab cycles focus

Input

var nameInput textinput.Model
modal.Input("name-input", &nameInput)
modal.InputWithLabel("name-input", "Name:", &nameInput)
modal.Input("name-input", &nameInput,
    modal.WithSubmitOnEnter(true),       // Default: true
    modal.WithSubmitAction("submit"),    // Override primary action
)

Textarea

var msgArea textarea.Model
modal.Textarea("message", &msgArea, 5)          // height in lines
modal.TextareaWithLabel("message", "Label:", &msgArea, 5)
  • Enter inserts newlines (never submits)

Combo (floating dropdown)

items := []modal.DropdownItem{
    {ID: "main", Label: "main", Value: "main"},
    {ID: "dev", Label: "dev", Value: "dev"},
}
var selectedIdx int
modal.Combo("branch", &branchInput, items, &selectedIdx)
  • Single-line input; filtered results float over later sections (modal height does not change)
  • `selected` is an **items** index (same as List)
  • Typing filters and selects the top match; up/down move the highlight
  • Enter commits the highlight and, by default, returns the modal primary action
  • Tab commits and moves focus; Esc closes the overlay without cancelling the modal
  • Click an overlay row to commit without submitting

Checkbox

var includeFiles bool
modal.Checkbox("include-files", "Include untracked files", &includeFiles)
  • Space toggles
  • Enter does **not** toggle; it submits the modal primary action (if any)

Select (one choice out of a set)

items := []modal.SelectItem{
    {ID: "shell", Label: "Shell", Description: "new agent/shell session"},
    {ID: "worktree", Label: "Worktree", Description: "shell in a new worktree"},
}
var selectedIdx int
modal.Select("kind", items, &selectedIdx,
    modal.WithMaxVisible(6),
    modal.WithDisabled(func(i int) string { return reasons[i] }),
    modal.WithOnSelect(func(i int) { rebuildAround(i) }),
)
  • The default control for a single choice: sort, filter, kind. `modal.List` is the low-level column of rows for lists that are not a single choice.
  • Two shapes, chosen by count: a segmented `[ A | B | C ]` under five choices, a `❯`-cursor full-width list with an aligned description column at five or more (and the list whenever the segments would not fit the width). `WithShape(modal.Shap
Read more
@marcus
Ships withsidecar

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

Stars on GitHub

Momentum

stars in total
1.1k
Star history of marcus/sidecar on GitHub, a chart by star-history.com

Other skills on sidecar.

coordinate-agents

coordinate-agents

Drive another Sidecar-managed agent from a shell — discover targets, create the layout, start a provider, prompt and wait, read…

@marcus@marcusInstall Skill
create-adapter

create-adapter

Create conversation adapters for importing AI chat history from different tools (Claude Code, Cursor, Warp, Codex, etc.). Covers…

@marcus@marcusInstall Skill
create-plugin

create-plugin

Create new sidecar plugins implementing the plugin.Plugin interface, rendering views with Bubble Tea, handling keyboard input via…

@marcus@marcusInstall Skill
create-prompt

create-prompt

Create prompts for sidecar workspaces. Covers prompt structure (name, ticketMode, body), template variables (ticket with…

@marcus@marcusInstall Skill
create-theme

create-theme

Create custom color themes for Sidecar, including base theme selection, color overrides, gradient borders, tab styles,…

@marcus@marcusInstall Skill
drag-pane

drag-pane

Drag-and-drop pane resizing implementation for two-pane plugin layouts. Covers mouse event handling via the internal/mouse…

@marcus@marcusInstall Skill

More skills from other plugins.

paseo

paseo

Paseo reference for managing hosts, projects, workspaces, workspace scripts, agents, schedules, and heartbeats.

paseo11d23k
@getpaseo@getpaseoInstall Skill
10x
Auto-invoked

10x

Personalized audit that teaches the advanced Paradev (formerly Superset) features the user isn't using yet (automations, parallel…

paradev2d16k
@superset-sh@superset-shInstall Skill
capabilities

capabilities

Your capability catalog — read this at boot. Lists the temporal date-range skills and the external integrations (reached via the…

@chaitanyagiri@chaitanyagiriInstall Skill

Questions about create-modal.

Which coding agents can use create-modal?
create-modal 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.