Skip to content
Agent Workspaces
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

Claude Code

$ npx -y skills add marcus/sidecar --skill worktree-switching --agent claude-code

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
@marcus
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

Stars on GitHub

Momentum

stars in total
1.1k
Star history of marcus/sidecar on GitHub, a chart by star-history.com

Other skills on sidecar.

coordinate-agents

coordinate-agents

Drive another Sidecar-managed agent from a shell — discover targets, create the layout, start a provider, prompt and wait, read…

@marcus@marcusInstall Skill
create-adapter

create-adapter

Create conversation adapters for importing AI chat history from different tools (Claude Code, Cursor, Warp, Codex, etc.). Covers…

@marcus@marcusInstall Skill
create-modal

create-modal

Create declarative modals using the modal library API. Covers modal types (confirm, input, select, form), sections (Text,…

@marcus@marcusInstall Skill
create-plugin

create-plugin

Create new sidecar plugins implementing the plugin.Plugin interface, rendering views with Bubble Tea, handling keyboard input via…

@marcus@marcusInstall Skill
create-prompt

create-prompt

Create prompts for sidecar workspaces. Covers prompt structure (name, ticketMode, body), template variables (ticket with…

@marcus@marcusInstall Skill
create-theme

create-theme

Create custom color themes for Sidecar, including base theme selection, color overrides, gradient borders, tab styles,…

@marcus@marcusInstall Skill

More skills from other plugins.

paseo

paseo

Paseo reference for managing hosts, projects, workspaces, workspace scripts, agents, schedules, and heartbeats.

paseo11d23k
@getpaseo@getpaseoInstall Skill
10x
Auto-invoked

10x

Personalized audit that teaches the advanced Paradev (formerly Superset) features the user isn't using yet (automations, parallel…

paradev2d16k
@superset-sh@superset-shInstall Skill
capabilities

capabilities

Your capability catalog — read this at boot. Lists the temporal date-range skills and the external integrations (reached via the…

@chaitanyagiri@chaitanyagiriInstall Skill

Questions about worktree-switching.

Which coding agents can use worktree-switching?
worktree-switching installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.