Create ModalSAFE
Use sidecar next to CLI agents for diffs, file trees, conversation history, and task management with td
Overview
Use sidecar next to CLI agents for diffs, file trees, conversation history, and task management with td
a69114b893d1OBSERVED · 2026-10-10Host compatibility
What the documentation claims. We have not run a compatibility test.
| Host | Status | Notes |
|---|---|---|
| cursor | mentioned |
What it tells the agent
The instruction file, verbatim from the audited commit — this is the text the model reads, and the surface the audit's instruction layer examines. Quoted here so you can judge it without cloning anything.
---
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
```go
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:
```go
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:**
```go
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
```go
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
```go
modal.Text("Static text with auto line wrapping")
modal.Spacer() // Single blank line
```
### Buttons
```go
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
```go
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
```go
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)
```go
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
```go
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)
```go
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.ShapTrust audit
SAFEgrade B · trust 89/100 Nothing in the source contradicts what it says it does. Grade A is reserved for packages that have also passed the behavioural sandbox.
| Layer | What it checks | Result |
|---|---|---|
| L0 | Provenance & inventory | WARN |
| L1 | Static analysis of the code | NA |
| L2 | Instruction surface (what it tells the agent) | PASS |
| L3 | Class-specific surface | PASS |
| L4 | Behavioural (sandbox) | SKIPPED |
What the source does
- Filesystem
- none-observed
- Network
- none-observed
- Shell
- none-observed
- Dependencies
- pinned
- Secrets in source
- none-found
Findings (2)
.codex/skills
GEMINI.md
Gates applied: no_behavioural_pass.
a69114b893d1full audit observations/trust-audit/skill/marcus__create-modal.json · Report an issue / request a re-scanAudit history
Every audit this skill has had.
| Date | Source | Verdict | Grade | Score | Change |
|---|---|---|---|---|---|
| 2026-10-10 | a69114b893d1 | SAFE | B | 89 | first audit |
Questions
What does the Create Modal skill do?
Use sidecar next to CLI agents for diffs, file trees, conversation history, and task management with td
Is Create Modal safe to install?
The audit found nothing in the source that contradicts what it says it does, and graded it B (89/100). Grade A is held back for packages that have also passed a sandboxed behavioural run, which is why a clean skill reads B.
What can Create Modal access on my machine?
The audit observed no filesystem, network or shell use at all in its source.
Which assistants does Create Modal work with?
Its documentation mentions cursor. That is what the text claims, not a compatibility test we ran.
How current is this page?
The grade is for one exact copy of the source (a69114b893d1), read on 2026-10-10. The repository is watched, and a new audit runs when it changes — this is the first audit.