Skip to main content
ClaudeWave
Skill1k repo starsupdated 4d ago

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.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/marcus/sidecar /tmp/shell-integration && cp -r /tmp/shell-integration/.claude/skills/shell-integration ~/.claude/skills/shell-integration
Then start a new Claude Code session; the skill loads automatically.

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:

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