shell-integration
Shell Integration provides tmux-backed interactive shell functionality for Sidecar, enabling users to send keystrokes directly to tmux sessions through a TUI without emulating a terminal. Use this when you need to relay keyboard input to tmux panes, handle cursor positioning, manage clipboard operations, and render terminal output with change detection across workspace and file browser plugins.
git clone --depth 1 https://github.com/marcus/sidecar /tmp/shell-integration && cp -r /tmp/shell-integration/.claude/skills/shell-integration ~/.claude/skills/shell-integrationSKILL.md
# Shell Integration
Sidecar's interactive shell allows users to type directly into tmux sessions from within the TUI. It is NOT a terminal emulator -- tmux is the PTY backend, sidecar acts as an input/output relay.
## Package Structure
```
internal/tty/ # Shared tmux terminal abstraction
tty.go # Core Model and State types
keymap.go # Bubble Tea -> tmux key translation
messages.go # Message types (CaptureResultMsg, PollTickMsg, etc.)
session.go # tmux operations (send-keys, capture-pane, resize)
polling.go # Polling interval constants and calculation
cursor.go # Cursor rendering and position query
paste.go # Paste handling (clipboard, bracketed paste)
terminal_mode.go # Terminal mode detection (mouse, bracketed paste)
output_buffer.go # Thread-safe buffer with hash-based change detection
internal/plugins/workspace/
interactive.go # Workspace-specific interactive mode logic
interactive_selection.go # Text selection in interactive mode
view_preview.go # Rendering with cursor overlay and scroll offset
mouse.go # Scroll handling
types.go # InteractiveState type
internal/plugins/filebrowser/
inline_edit.go # Inline editor mode using tty.Model
handlers.go # Message handling for inline edit
```
## Data Flow
```
User Keypress -> handleInteractiveKeys()
-> tty.MapKeyToTmux()
-> tmux send-keys
-> schedulePoll(20ms debounce)
-> capture-pane + cursor query
-> CaptureResultMsg
-> OutputBuffer.Update()
-> pollInteractivePane() (adaptive 50-250ms)
-> renderWithCursor()
```
## Core Abstractions
### tty.Model
Embeddable component for interactive tmux functionality:
```go
type Model struct {
Config Config // Exit key, copy/paste keys, scrollback lines
State *State // Current interactive state
Width int
Height int
OnExit func() tea.Cmd
OnAttach func() tea.Cmd
}
// Usage:
p.inlineEditor = tty.New(&tty.Config{
ExitKey: "ctrl+\\",
ScrollbackLines: 600,
})
cmd := p.inlineEditor.Enter(sessionName, paneID)
```
### tty.State
```go
type State struct {
Active bool
TargetPane string // tmux pane ID (e.g., "%12")
TargetSession string
LastKeyTime time.Time // For polling decay
CursorRow, CursorCol int
CursorVisible bool
PaneHeight, PaneWidth int
BracketedPasteEnabled bool
MouseReportingEnabled bool
OutputBuf *OutputBuffer
PollGeneration int // For invalidating stale polls
}
```
### tty.OutputBuffer
Thread-safe bounded buffer with hash-based change detection:
```go
func (b *OutputBuffer) Update(content string) bool {
rawHash := maphash.String(seed, content)
if rawHash == b.lastRawHash { return false } // Skip ALL processing
content = mouseEscapeRegex.ReplaceAllString(content, "")
b.lines = strings.Split(content, "\n")
return true
}
func (b *OutputBuffer) LinesRange(start, end int) []string
```
## Key Mapping (`keymap.go`)
```go
func MapKeyToTmux(msg tea.KeyMsg) (key string, useLiteral bool) {
switch msg.Type {
case tea.KeyEnter: return "Enter", false
case tea.KeyBackspace: return "BSpace", false
case tea.KeyTab: return "Tab", false
case tea.KeyUp: return "Up", false
case tea.KeyCtrlC: return "C-c", false
case tea.KeyRunes: return string(msg.Runes), true // Literal mode
}
}
```
Modified keys use CSI sequences:
```go
case "shift+up": return "\x1b[1;2A", true
case "ctrl+up": return "\x1b[1;5A", true
case "alt+up": return "\x1b[1;3A", true
case "shift+tab": return "\x1b[Z", true
```
For printable characters, `tmux send-keys -l` prevents interpretation.
## Adaptive Polling (`polling.go`)
```go
const (
PollingDecayFast = 50ms // During active typing
PollingDecayMedium = 200ms // After 2s inactivity
PollingDecaySlow = 250ms // After 10s inactivity
KeystrokeDebounce = 20ms // Delay after keystroke
)
```
### Three-State Visibility Polling (Workspace)
| State | Active | Idle |
|-------|--------|------|
| Visible + focused | 200ms | 2s |
| Visible + unfocused | 500ms | 500ms |
| Not visible | 10-20s | 10-20s |
### Poll Generation
Stale polls invalidated using generation counter:
```go
func (m *Model) schedulePoll(delay time.Duration) tea.Cmd {
m.State.PollGeneration++
gen := m.State.PollGeneration
return tea.Tick(delay, func(t time.Time) tea.Msg {
return PollTickMsg{Generation: gen}
})
}
```
### Performance Per Keystroke
1. `tmux send-keys` (~10ms)
2. 20ms debounce
3. `capture-pane` (~5ms) + cursor query (~5ms)
4. Hash check (~1ms), regex if changed (~5ms), buffer split (~1ms)
5. Cursor overlay (<1ms)
Total: ~42ms worst case, ~36ms typical.
## Cursor Positioning (`cursor.go`)
### Query
```go
func QueryCursorPositionSync(target string) (row, col, paneHeight, paneWidth int, visible, ok bool) {
cmd := exec.Command("tmux", "display-message", "-t", target,
"-p", "#{cursor_x},#{cursor_y},#{cursor_flag},#{pane_height},#{pane_width}")
}
```
### Rendering
Cursor is rendered as a block character overlaid on captured output. Handles cursor past end of line (pad with spaces) and cursor within line (ANSI-aware slicing with `ansi.Cut`).
### Height Mismatch Adjustment
When display height differs from tmux pane height:
```go
if paneHeight > displayHeight {
relativeRow = cursorRow - (paneHeight - displayHeight)
} else if paneHeight > 0 && paneHeight < displayHeight {
relativeRow = cursorRow + (displayHeight - paneHeight)
}
```
## Scrolling
Scrolling operates on>
Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text, Buttons, Input, Textarea, Checkbox, List, When, Custom), rendering with OverlayModal, and keyboard/mouse handling. Use when adding modals or dialogs to the application.
>
Create prompts for sidecar workspaces. Covers prompt structure (name, ticketMode, body), template variables (ticket with fallbacks), config file locations (global vs project), and scope overrides. Use when creating or modifying prompts in sidecar config files.
>
>
Creating and using feature flags in sidecar for gating experimental functionality. Covers flag registration, checking flags in code, config file and CLI overrides, and priority resolution. Use when adding feature flags, toggling features, or gating new functionality behind flags.
>