Skip to content
Development
Skill

/worktree-switching

Git worktree support in sidecar: worktree detection, switching between worktrees, worktree state management, and plugin reinitialization. Covers the full lifecycle of worktree context switching including registry reinit, per-worktree state persistence, deleted worktree detection

From plugin
sidecar
1.1k19 skills
Install
$ npx -y skills add marcus/sidecar --skill worktree-switching --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/worktree-switching

Context preview

The summary Claude sees to decide when to auto-load this skill.

Git worktree support in sidecar: worktree detection, switching between worktrees, worktree state management, and plugin reinitialization. Covers the full lifecycle of worktree context switching including registry reinit, per-worktree state persistence, deleted worktree detection

SKILL.md

worktree-switching.SKILL.md
name: worktree-switching
description: >
  Git worktree support in sidecar: worktree detection, switching between worktrees,
  worktree state management, and plugin reinitialization. Covers the full lifecycle
  of worktree context switching including registry reinit, per-worktree state
  persistence, deleted worktree detection and fallback. Use when working on git
  worktree features or worktree-related functionality.

Worktree Switching

Sidecar supports seamless switching between git worktrees. When switching: 1. All plugins are stopped, reinitialized with the new WorkDir, and restarted 2. Per-worktree state (active plugin, sidebar selections) is saved/restored 3. Project-specific themes are applied 4. If a worktree is deleted externally, sidecar gracefully falls back to main

Core Mechanism

Project Switching

Worktree switching uses `Model.switchProject()` in `internal/app/model.go`:

m.switchProject(worktreePath)

This triggers in order: 1. Save active plugin for old WorkDir 2. Update `m.ui.WorkDir` to new path 3. Apply resolved theme for new path 4. Call `registry.Reinit(newWorkDir)` -- stops all plugins, updates context, reinits all 5. Send `WindowSizeMsg` to all plugins for layout recalculation 6. Restore saved active plugin for new WorkDir 7. Show toast notification

Registry Reinitialization

`Registry.Reinit()` in `internal/plugin/registry.go`:

func (r *Registry) Reinit(newWorkDir string) []tea.Cmd {
    // Stop all plugins (reverse order)
    for i := len(r.plugins) - 1; i >= 0; i-- {
        r.safeStop(r.plugins[i])
    }
    // Update context
    r.ctx.WorkDir = newWorkDir
    // Reinit all plugins
    for _, p := range r.plugins {
        r.safeInit(p)
    }
    // Collect and return start commands
    return startCmds
}

Plugin Responsibilities on Worktree Switch

Handle Reinitialization Cleanly

Your plugin will be stopped and reinitialized on worktree switch. Ensure:

1. **`Stop()`** releases all resources (watchers, goroutines, channels) 2. **`Init(ctx)`** resets state and reads from new `ctx.WorkDir` 3. **`Start()`** kicks off fresh async work for the new context

func (p *Plugin) Stop() {
    p.stopOnce.Do(func() {
        if p.watcher != nil {
            p.watcher.Close()
        }
        close(p.done)
    })
}

func (p *Plugin) Init(ctx *plugin.Context) error {
    p.ctx = ctx
    p.items = nil            // Reset state
    p.stopOnce = sync.Once{} // Reset stop guard
    p.done = make(chan struct{})
    return nil
}

Handle WindowSizeMsg After Switch

After reinitialization, the app sends `tea.WindowSizeMsg`. Handle it in `Update`:

case tea.WindowSizeMsg:
    p.width = msg.Width
    p.height = msg.Height
    return p, nil

Persist Per-Worktree State

Use `internal/state` to save/restore preferences keyed by WorkDir:

// Restore state in Init or Start
saved := state.GetMyPluginState(p.ctx.WorkDir)
if saved.Selection != "" {
    p.selection = saved.Selection
}

// Save state on user action
state.SetMyPluginState(p.ctx.WorkDir, MyPluginState{
    Selection: p.selection,
})

Add state struct and accessors following `internal/state/state.go`:

type MyPluginState struct {
    Selection string `json:"selection,omitempty"`
}

func GetMyPluginState(workdir string) MyPluginState {
    mu.RLock()
    defer mu.RUnlock()
    if current == nil || current.MyPlugin == nil {
        return MyPluginState{}
    }
    return current.MyPlugin[workdir]
}

State is saved to `~/.config/sidecar/state.json` keyed by absolute WorkDir path. State is automatically per-worktree when you pass `p.ctx.WorkDir`.

Deleted Worktree Detection

When a worktree is deleted externally, plugins should detect this and request fallback to main.

App-Level Commands

Defined in `internal/app/commands.go`:

  • `SwitchWorktreeMsg{WorktreePath}` -- requests switching to a specific worktree
  • `SwitchWorktree(path) tea.Cmd` -- helper to create the above
  • `SwitchToMainWorktreeMsg{MainWorktreePath}` -- requests fallback to main worktree
  • `SwitchToMainWorktree(mainPath) tea.Cmd` -- helper to create the above

Detection Pattern (from workspace plugin)

**1. Define plugin-local message** (`internal/plugins/workspace/worktree.go`):

type WorkDirDeletedMsg struct {
    MainWorktreePath string
}

**2. Detect deletion in refresh command:**

func (p *Plugin) refreshWorktrees() tea.Cmd {
    workDir := p.ctx.WorkDir
    return func() tea.Msg {
        if _, err := os.Stat(workDir); os.IsNotExist(err) {
            mainPath := findMainWorktreeFromDeleted(workDir)
            if mainPath != "" {
                return WorkDirDeletedMsg{MainWorktreePath: mainPath}
            }
        }
        return RefreshDoneMsg{Worktrees: worktrees, Err: err}
    }
}

**3. Handle message, return app command:**

case WorkDirDeletedMsg:
    p.refreshing = false
    if msg.MainWorktreePath != "" {
        return p, app.SwitchToMainWorktree(msg.MainWorktreePath)
    }
    return p, nil

Git Helpers

`internal/app/git.go` provides:

| Function | Purpose | |----------|---------| | `GetWorktrees(workDir)` | List all worktrees for the repo | | `GetMainWorktreePath(workDir)` | Get path to main worktree | | `WorktreeNameForPath(workDir, path)` | Derive display name for a worktree | | `GetAllRelatedPaths(workDir)` | Get all paths sharing the same repo |

Per-WorkDir State Keys

| Key | Purpose | |-----|---------| | `ActivePlugin` | Which plugin tab was focused | | `FileBrowser` | File browser selections and view state | | `Workspace` | Workspace/shell selections |

Best Practices

1. **Reset all state in `Init()`** -- do not carry over stale data from previous worktree 2. **Use `sync.Once` for `Stop()`** -- prevents double-close panics during rapid switching 3. **Validate WorkDir exists** before expensive operations 4. **Store WorkDir at command creation time** -- closures may execute after switch 5. **Ke

Read more
Ships withsidecar

Always check if you are running in Sidecar: run sidecar agents for capabilities. You might never open your editor again. Status: Ready for daily use. Please report any issues you encounter. Documentation · Getting Started · Comprehensive List of Features

Get the whole plugin
Stats
1,072
Stars
81
Forks
Active
Maintenance
Go
Language
MIT
License
10h ago
Last commit
8mo ago
Created

Repo: marcus/sidecar

Other skills on sidecar.