Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/marcus-worktree-switching/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -4% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 21% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 6% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 6% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 17% | 0% |
Sidecar supports seamless switching between git worktrees. When switching:
Worktree switching uses Model.switchProject() in internal/app/model.go:
gom.switchProject(worktreePath)
This triggers in order:
m.ui.WorkDir to new pathregistry.Reinit(newWorkDir) -- stops all plugins, updates context, reinits allWindowSizeMsg to all plugins for layout recalculationRegistry.Reinit() in internal/plugin/registry.go:
gofunc (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 }
Your plugin will be stopped and reinitialized on worktree switch. Ensure:
Stop() releases all resources (watchers, goroutines, channels)Init(ctx) resets state and reads from new ctx.WorkDirStart() kicks off fresh async work for the new contextgofunc (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 }
After reinitialization, the app sends tea.WindowSizeMsg. Handle it in Update:
gocase tea.WindowSizeMsg: p.width = msg.Width p.height = msg.Height return p, nil
Use internal/state to save/restore preferences keyed by WorkDir:
go// 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:
gotype 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.
When a worktree is deleted externally, plugins should detect this and request fallback to main.
Defined in internal/app/commands.go:
SwitchWorktreeMsg{WorktreePath} -- requests switching to a specific worktreeSwitchWorktree(path) tea.Cmd -- helper to create the aboveSwitchToMainWorktreeMsg{MainWorktreePath} -- requests fallback to main worktreeSwitchToMainWorktree(mainPath) tea.Cmd -- helper to create the above1. Define plugin-local message (internal/plugins/workspace/worktree.go):
gotype WorkDirDeletedMsg struct { MainWorktreePath string }
2. Detect deletion in refresh command:
gofunc (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:
gocase WorkDirDeletedMsg: p.refreshing = false if msg.MainWorktreePath != "" { return p, app.SwitchToMainWorktree(msg.MainWorktreePath) } return p, nil
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 |
| Key | Purpose | |-----|---------| | ActivePlugin | Which plugin tab was focused | | FileBrowser | File browser selections and view state | | Workspace | Workspace/shell selections |
Init() -- do not carry over stale data from previous worktreesync.Once for Stop() -- prevents double-close panics during rapid switchingStart() non-blocking -- return commands that do async workgit worktree add ../my-feature feature-branchOther measured skills in the registry, with their headline benchmark lift.