Ui FeaturesSAFE
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: 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
```go
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
```go
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
```go
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
```go
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:**
```go
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:
```go
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)
```go
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`).
```go
// 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., `Trust 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__ui-features.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 Ui Features skill do?
Use sidecar next to CLI agents for diffs, file trees, conversation history, and task management with td
Is Ui Features 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 Ui Features access on my machine?
The audit observed no filesystem, network or shell use at all in its source.
Which assistants does Ui Features 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.