Skip to content
 
 

Latest commit

 

History

34 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wezterm-claude-sidebar

A left-side TUI for WezTerm that makes it easy to juggle a dozen Claude Code sessions at once.

Every tab is just ✳ Claude Code. You have no idea which tab is which project. This fixes that.

What it does

  • Sidebar pane: one keystroke opens a thin column on the left of the current wezterm window that lists every tab, labeled by project name, git branch, dirty flag, and the category you filed the project under (e.g. ~/project/work/foowork).
  • Live Claude state: detects whether each tab's Claude Code is currently running a turn, waiting for input, or asking for a permission / choice, by reading the pane's text buffer.
  • Notification badges: when a background Claude tab finishes a task (running → waiting), it gets a badge so you know without switching. Optionally fires notify-send for a desktop alert.
  • Smart tab titles: tabs running Claude Code get auto-renamed from ✳ Claude Code to 1 joyfit so you can tell them apart even without the sidebar open.
  • Fuzzy search: press / to filter tabs by project name.
  • New tab launcher: press n to pick a project directory and launch a new Claude Code session.
  • Category grouping: press g to toggle grouped-by-category view with dividers.
  • Emacs-style navigation: C-n / C-p, j / k, 1-9, or mouse click to jump to a tab. Enter activates the selection.

Install

1. Build the binary

Requires Go 1.24+.

go install github.com/Andyyyy64/wezterm-claude-sidebar@latest

...or from a clone:

git clone https://github.com/Andyyyy64/wezterm-claude-sidebar
cd wezterm-claude-sidebar
go build -o wcs .
sudo install wcs /usr/local/bin/wcs   # or anywhere on $PATH

2. Wire it into wezterm

Copy lua/sidebar.lua into ~/.config/wezterm/ (or somewhere on WezTerm's Lua package path) and add to your wezterm.lua:

local wezterm = require("wezterm")
local sidebar = require("sidebar")

local config = wezterm.config_builder()

config.leader = { key = "x", mods = "CTRL", timeout_milliseconds = 1000 }

sidebar.apply(config, {
  binary = "wcs",                              -- or absolute path
  toggle_key = { key = "c", mods = "LEADER|CTRL" },  -- C-x C-c, this tab only
  -- window-wide toggle: opens a mirror in every tab, so switching tabs
  -- reads as one fixed sidebar (all mirrors share the single daemon)
  window_toggle_key = { key = "P", mods = "CMD|SHIFT" },
  width_cells = 44,
  tab_title = true,                            -- also enhance tab titles
  refresh_ms = 3000,                           -- daemon poll interval, ms
})

return config

WSL note

If you run WezTerm on Windows but Claude Code inside WSL, install wcs inside the WSL distro, not on Windows. The sidebar shells out to wezterm.exe cli ... via interop, so it needs to talk to the Windows binary from inside WSL. Export WEZTERM_BIN=wezterm.exe if PATH resolution misses it.

Keybindings (inside the sidebar)

Navigation

The cursor runs through the spaces and the agents as one list: pressing up on the first agent moves onto the last space, and down off the last space comes back to the first agent.

Key Action
C-n / j / cursor down
C-p / k / cursor up
Home jump to the first space
G / End jump to the last agent
Enter / o / Space on an agent: activate its tab · on a space: filter the agents to it
1-9 jump directly to tab N
mouse click activate clicked tab / filter clicked space

Actions

Key Action
/ fuzzy search — filter tabs by project name
n new tab launcher — pick a project, start Claude
g toggle category grouping view
d close the selected tab (with y/n confirmation)
r / C-r refresh now
q / C-g / Esc close the sidebar

Flags

wcs [flags]
  -all-windows         show tabs from every wezterm window, not just this one
  -refresh int         refresh interval, ms (default 1500)
  -no-claude-detect    skip reading pane text for state detection
  -host-pane int       wezterm pane id of the pane that opened the sidebar
  -host-tab int        wezterm tab id of the tab the sidebar was opened from
  -daemon              run the shared sidebar daemon (autostarted; see below)
  -dump                print the collected tab list once and exit (no TUI)

How it works

One daemon, many mirrors

Every sidebar pane is a thin viewer: it draws frames pushed to it over a unix socket (~/.cache/wcs/wcs.sock or the platform equivalent) and forwards its keys, clicks, and resizes back. A single daemon process owns the actual sidebar — the polling, the item list, the cursor — and renders a frame per viewer size. That is what makes N open sidebars read as one UI: every tab shows the same list and the same arrow, and a click in any of them moves them all. It also means only one process ever polls wezterm, no matter how many sidebars are open.

The first viewer that finds no daemon listening starts one (from its own binary, passing its refresh/detect flags along); the daemon exits when the last viewer is gone for a while, and retires itself when a viewer from a newer install connects. Quitting (q) closes only the sidebar it was pressed in.

Collecting the list

The daemon shells out to wezterm cli list --format json to enumerate panes, groups them by (window_id, tab_id), and for each tab probes all panes to pick the best "face" (a Claude Code pane beats a plain shell, and among Claude panes the most urgent state wins). Each tab is enriched with:

  1. Project infogit rev-parse --show-toplevel to find the repo root, then rev-parse --abbrev-ref HEAD and status --porcelain. Results are cached for a few seconds to keep polling cheap.
  2. Category — the first path component under ~/project (configurable via WCS_PROJECT_ROOT).
  3. Claude statewezterm cli get-text of the pane, then a heuristic search for Claude Code's footer markers (bypass permissions, esc to interrupt, ? for shortcuts).

All per-tab enrichment runs in parallel goroutines to minimize wall-clock time (~110ms without Claude detection, ~210ms with).

Energy

Every poll costs subprocesses (one cli list, one cli list-clients, one cli get-text per real pane, plus cached git probes). Only the daemon polls — viewers sit idle on a socket read — and its rate adapts instead of burning -refresh around the clock:

  • when the focused tab has no sidebar in it (nobody is looking at a mirror), polls run at 2× the interval, stretching to 8× while nothing changes at all; visible but idle backs off to 4× — any state change, focus move, keypress, or click snaps the rate back;
  • while a turn is running the base rate always holds;
  • desktop notifications fire once per event, since exactly one process is watching;
  • the once-a-second elapsed-counter tick only runs while something is actually running.

Clicking or pressing Enter runs wezterm cli activate-tab --tab-id N.

License

MIT

About

A WezTerm sidebar to manage many Claude Code sessions at once — project-aware tabs, live state, Emacs-style navigation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages