Agent skill
shell-integration
Interactive shell/TTY integration with tmux session management, shell command execution, and output capture in sidecar. Covers the tty package, key mapping, adaptive polling, cursor rendering, scrolling, paste handling, and inline editing. Use when working on shell integration, tmux features, command execution, or interactive mode.
Install this agent skill to your Project
npx add-skill https://github.com/marcus/sidecar/tree/main/.claude/skills/shell-integration
SKILL.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:
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
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:
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)
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:
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)
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:
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
tmux send-keys(~10ms)- 20ms debounce
capture-pane(~5ms) + cursor query (~5ms)- Hash check (~1ms), regex if changed (~5ms), buffer split (~1ms)
- Cursor overlay (<1ms)
Total: ~42ms worst case, ~36ms typical.
Cursor Positioning (cursor.go)
Query
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:
if paneHeight > displayHeight {
relativeRow = cursorRow - (paneHeight - displayHeight)
} else if paneHeight > 0 && paneHeight < displayHeight {
relativeRow = cursorRow + (displayHeight - paneHeight)
}
Scrolling
Scrolling operates on the captured buffer. No tmux copy-mode involved.
type Plugin struct {
previewOffset int // Lines from bottom (0 = at bottom/live)
autoScrollOutput bool // Auto-follow new output?
}
- Scroll UP: pause auto-scroll, increment
previewOffset - Scroll DOWN: decrement
previewOffset, re-enable auto-scroll at 0 - Bounded by capture window (default 600 lines)
- Instant response (pure state manipulation, no subprocess calls)
Copy/Paste
- Copy:
alt+c(configurable viainteractiveCopyKey) - Paste:
alt+v(configurable viainteractivePasteKey)
Paste wraps text with bracketed paste sequences (\x1b[200~...\x1b[201~) when the application has enabled bracketed paste mode.
Terminal Mode Detection (terminal_mode.go)
Detects bracketed paste and mouse reporting modes by scanning output for enable/disable escape sequences. Latest enable > latest disable = mode active.
Width Synchronization
Tmux panes are resized in background at all times (not just interactive mode):
func ResizeTmuxPane(paneID string, width, height int) {
// resize-window, fallback to resize-pane for older tmux
}
Resize triggers: window resize, sidebar toggle/drag, selection change, agent/shell creation, interactive mode entry.
Inline Edit Mode (Filebrowser)
Uses tty.Model for vim/nano/emacs editing in the file preview pane:
func (p *Plugin) enterInlineEditMode(path string) tea.Cmd {
editor := os.Getenv("EDITOR")
sessionName := fmt.Sprintf("sidecar-edit-%d", time.Now().UnixNano())
exec.Command("tmux", "new-session", "-d", "-s", sessionName, editor, path).Run()
return InlineEditStartedMsg{SessionName: sessionName}
}
Entry and Exit
Workspace Plugin:
- Enter:
iwhen preview pane focused with output tab - Exit:
Ctrl+\(instant) or double-Escape (150ms delay) - Attach:
Ctrl+](full tmux attach)
Filebrowser Plugin:
- Enter:
eorEnteron a file (if inline edit enabled) - Exit:
Ctrl+\or double-Escape - Attach:
Ctrl+]
Feature Flags
{
"features": {
"tmux_interactive_input": true,
"tmux_inline_edit": true
}
}
Configuration
{
"plugins": {
"workspace": {
"interactiveExitKey": "ctrl+\\",
"interactiveAttachKey": "ctrl+]",
"interactiveCopyKey": "alt+c",
"interactivePasteKey": "alt+v",
"tmuxCaptureMaxBytes": 600
}
}
}
Critical Rules
- Never clear OutputBuffer -- breaks hash-based change detection and rendering
- Always increment poll generation on entering interactive mode to avoid duplicate poll chains (causes 200% CPU)
- Never call subprocesses from View() -- cursor queries and tmux ops must run in poll handlers
- Don't mix shell/workspace polling -- shells use
scheduleShellPollByName()+shellPollGeneration, workspaces usescheduleAgentPoll()+pollGeneration - Hash before regex -- massive CPU savings when content unchanged
- Debouncing works -- 20ms delay reduces subprocess spam ~60%
- Atomic cursor capture -- query cursor with output to avoid race conditions
- Width sync matters -- resize panes in background at all times
References
- Tmux integration notes -- detailed tmux CLI techniques, cursor tracking, resize sync, bracketed paste, mouse forwarding, modified keys, adaptive polling, debugging
- Original spec:
docs/spec-tmux-interactive-input.md
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
create-prompt
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.
merge-strategy
Git merge strategies, conflict resolution approaches, merge vs rebase recommendations, and branch integration patterns in sidecar. Covers pull strategy menu, direct merge workflow, squash merge, commit message templates, configurable defaults, and protected branches. Use when working on git merge features or making decisions about merge strategies.
keyboard-shortcuts
Reference for keyboard shortcut implementation, keybinding registration, shortcut parity with vim and other TUI tools, and the complete shortcut assignment table across all sidecar plugins. Use when adding or modifying keyboard shortcuts, checking shortcut assignments, resolving key conflicts, or assessing alignment with vim conventions.
profile-memory
Profile memory usage in sidecar using Go pprof, system tools, and heap analysis. Covers identifying memory leaks, goroutine leaks, file descriptor accumulation, and CPU profiling. Use when investigating memory issues, profiling performance, debugging memory leaks, or diagnosing unresponsive plugins.
create-theme
Create custom color themes for Sidecar, including base theme selection, color overrides, gradient borders, tab styles, per-project themes, community themes, and programmatic theme registration. Use when creating or modifying themes, adjusting UI appearance, or debugging color/style issues. See references/palette-reference.md for the full color palette with all keys and per-theme values.
feature-flags
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.
Didn't find tool you were looking for?