Skip to content
Agent Workspaces
Skill

/ui-features

Implementing UI/UX features in sidecar including modals (internal/modal library), keyboard shortcuts, mouse support, scrolling, pill/tab rendering, and pane resizing. Use when implementing UI features, handling user input, adding keyboard shortcuts, building modals, or working

From plugin
sidecar
1.1k19 skills

Claude Code

$ npx -y skills add marcus/sidecar --skill ui-features --agent claude-code

SKILL.md

ui-features.SKILL.md
name: ui-features
description: Implementing UI/UX features in sidecar including modals (internal/modal library), keyboard shortcuts, mouse support, scrolling, pill/tab rendering, and pane resizing. Use when implementing UI features, handling user input, adding keyboard shortcuts, building modals, or working on UX improvements.

UI Feature Implementation

Single entry point for sidecar UI work. All new modals must use `internal/modal`. For complete keyboard shortcut listings, see `references/keyboard-shortcuts-reference.md`.

Quick Checklist

  • Modals: use `internal/modal`, render with `ui.OverlayModal`, avoid manual hit region math
  • Pills/chips/tabs: use `styles.RenderPillWithStyle`; auto-fallback when `nerdFontsEnabled` is false
  • Keyboard: Commands + FocusContext + bindings must match; names short; priorities set
  • Mouse: rebuild hit regions on each render; add general regions first, specific last
  • Rendering: keep output within View width/height to avoid header/footer overlap. Use `contentHeight := height - headerLines - footerLines`
  • Testing: verify keyboard, mouse, hover, scrolling, and footer hints
  • Plugins must NOT render their own footer -- the app renders a unified footer from `Commands()`

Modals (internal/modal)

All new modals must use `internal/modal`. See `docs/guides/deprecated/declarative-modal-guide.md` for the full API.

Create a 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"),
    ))

Render in View

func (p *Plugin) renderDeleteView(width, height int) string {
    background := p.renderListView(width, height)
    rendered := p.deleteModal.Render(width, height, p.mouseHandler)
    return ui.OverlayModal(background, rendered, width, height)
}

Handle input in Update

case tea.KeyMsg:
    action, cmd := p.deleteModal.HandleKey(msg)
    if action != "" {
        return p.handleModalAction(action)
    }
    return p, cmd

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

Modal initialization and caching (critical)

Always call `ensureModal()` in BOTH View and Update handlers. Create an ensure function that: 1. Returns early if required state is missing 2. Caches based on width to avoid rebuilding every frame 3. Creates the modal only when needed

func (p *Plugin) ensureMyModal() {
    if p.targetItem == nil {
        return
    }
    modalW := 50
    if modalW > p.width-4 { modalW = p.width - 4 }
    if modalW < 20 { modalW = 20 }
    if p.myModal != nil && p.myModalWidthCache == modalW {
        return
    }
    p.myModalWidthCache = modalW
    p.myModal = modal.New("Title", modal.WithWidth(modalW), ...).
        AddSection(...)
}

**The key handler MUST call ensure before checking nil:**

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

Async content invalidation

When modal content depends on async data, invalidate the cache when data arrives:

case MyDataLoadedMsg:
    p.myData = msg.Data
    p.clearMyModal()  // Force rebuild with new content
    return p, nil

Modal keyboard shortcuts and footer hints

Modals need their own focus context and commands for footer hints:

1. Return a dedicated context from `FocusContext()` 2. Add commands for the modal context in `Commands()` 3. Add bindings in `internal/keymap/bindings.go` 4. Intercept custom keys before `modal.HandleKey` (Tab/Enter/Esc are handled internally)

func (p *Plugin) FocusContext() string {
    switch p.viewMode {
    case ViewModeError:  return "git-error"
    case ViewModePushMenu: return "git-push-menu"
    default: return "git-status"
    }
}

Modal notes

  • `HandleKey`/`HandleMouse` handle Tab, Shift+Tab, Enter, Esc internally
  • Backdrop clicks return "cancel"; use `WithCloseOnBackdropClick(false)` to disable
  • Use built-in sections (Text, Input, Textarea, Buttons, Checkbox, List, When) before custom layouts
  • For bespoke layouts, use `modal.Custom` and return explicit focusable offsets
  • `SetFocus(id)` auto-scrolls viewport to focused element
  • Prefer `ui.OverlayModal(background, modal, width, height)` for dimmed overlays; do not pre-center with `lipgloss.Place`

Background colors (critical)

Lipgloss `Background()` does not cascade into child content. ANSI resets clear the parent background. Solution: replace ANSI resets within viewport lines with reset + background re-apply, then pad short lines. See `fillBackground` in `internal/modal/layout.go`.

Pill-Shaped Elements (internal/styles)

Controlled by `nerdFontsEnabled` in `~/.config/sidecar/config.json` (`ui.nerdFontsEnabled`).

// With explicit colors
label := styles.RenderPill("Output", styles.TextPrimary, styles.Primary, "")

// With a lipgloss.Style (preferred for tabs/chips)
active := styles.RenderPillWithStyle("Output", styles.BarChipActive, "")
inactive := styles.RenderPillWithStyle("Diff", styles.BarChip, "")

Available styles: `styles.BarChip` (inactive), `styles.BarChipActive` (active), or custom `lipgloss.Style`.

Test with both `nerdFontsEnabled: true` and `false` to verify fallback.

Keyboard Shortcuts

For complete per-plugin shortcut listings, see `references/keyboard-shortcuts-reference.md`.

Three things must match

1. **Command ID** in `Commands()` (e.g., `"stage-file"`) 2. **Binding command** in `internal/keymap/bindings.go` (e.g., `"stage-file"`) 3. **Context string** in both places (e.g., `

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-modal

create-modal

Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text,…

@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

More skills from other plugins.

paseo

paseo

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

paseo9d23k
@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…

paradevtoday16k
@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 ui-features.

Which coding agents can use ui-features?
ui-features 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.