TUI + CLI project manager built on Zellij, written in Rust.
See also: PRD
zigzag unifies dev project management: terminal sessions, git worktrees, CI/CD monitoring, and automation via Claude — all in a beautiful TUI.
zigzag (no args) → interactive TUI
zigzag open <proj> → direct CLI
zigzag list → list projects/sessions
zigzag autopilot → automated workflows
| Tool | Role | Required |
|---|---|---|
| zellij | Terminal multiplexer, sessions, layouts | yes |
worktrunk (wt) |
Git worktree management | yes |
| gh | GitHub CLI — PR, CI status | yes |
zigzag checks presence and minimum version of each tool at launch. Fails with a clear message if missing.
zigzag/
├── Cargo.toml # workspace
├── crates/
│ ├── zigzag-core/ # business logic, 100% I/O-agnostic
│ ├── zigzag-tui/ # ratatui frontend
│ ├── zigzag-cli/ # non-interactive commands
│ ├── zigzag-autopilot/ # state machine, workflows, triggers, notifications
│ ├── zigzag-plugin/ # future WASM Zellij plugin (phase 4)
│ └── zigzag-web/ # future web server axum (phase 5)
zigzag-core is 100% I/O-agnostic. No direct calls to std::fs, std::process::Command, or any system I/O. Everything goes through traits:
trait ProjectStore {
fn list_projects(&self) -> Result<Vec<Project>>;
fn get_project(&self, name: &str) -> Result<Project>;
}
trait SessionManager {
fn list_sessions(&self, project: &str) -> Result<Vec<Session>>;
fn create_session(&self, project: &str, branch: &str, layout: Layout) -> Result<Session>;
fn attach_session(&self, session: &Session) -> Result<()>;
fn kill_session(&self, session: &Session) -> Result<()>;
}
trait WorktreeManager {
fn list_worktrees(&self, project: &str) -> Result<Vec<Worktree>>;
fn create_worktree(&self, project: &str, branch: &str) -> Result<Worktree>;
fn remove_worktree(&self, worktree: &Worktree) -> Result<()>;
}
trait ForgeClient {
fn get_pr(&self, project: &str, branch: &str) -> Result<Option<PullRequest>>;
fn get_ci_status(&self, project: &str, branch: &str) -> Result<CiStatus>;
}
trait Notifier {
fn notify(&self, message: &str, level: NotifyLevel) -> Result<()>;
}Reason: enables compiling zigzag-core to WASM for the Zellij plugin (phase 4) and web client (phase 5).
zigzag(no args) → launches ratatui TUIzigzag <command> [args]→ direct CLI execution- Behavior like
lazygit(TUI) vsgit(CLI)
// Global preferences
config {
// Default layout for new sessions
default-layout {
tab name="claude" {
pane command="claude" {
args "--dangerously-skip-permissions"
}
}
tab name="shell" {
pane
}
}
// TUI keybindings
keybindings {
navigation "arrows" // "arrows" (default) or "vim"
}
// Notifications
notifications {
macos-native true // default local
telegram false // configurable
tui true // always in TUI if open
}
// Dependencies — minimum versions
deps {
zellij ">=0.44.0"
wt ">=0.34.0"
gh ">=2.0.0"
}
}project "myapp" {
path "~/Code/myapp"
}
project "hermes" {
path "~/Library/Mobile Documents/iCloud~md~obsidian/Documents/HERMES"
layout "obsidian" // reference a named layout
}
project "prod-api" {
path "~/Code/prod-api"
host "https://vps.example.com:8082"
token "env:ZP_VPS_TOKEN"
}// Override layout for this project
layout {
tab name="claude" {
pane command="claude" {
args "--resume"
}
}
tab name="shell" {
pane
}
tab name="server" {
pane command="npm" {
args "run" "dev"
}
}
tab name="logs" {
pane command="tail" {
args "-f" "/var/log/app.log"
}
}
}
// Deployment
deploy {
command "./deploy.sh"
}
// Autopilot overrides
autopilot {
auto-push true // default
review false // default
}Format: {project}:{branch}
Examples:
myapp:mainmyapp:feat-loginprod-api:fix-bug-42
/ in branch names is replaced by - for Zellij URL compatibility.
Fully managed by worktrunk (wt). zigzag calls wt switch, wt remove, wt list. No custom worktree logic.
zigzag list # List projects + active sessions
zigzag open <project> [branch] # Open/attach a session
zigzag close <session> # Detach session (keep worktree)
zigzag delete <session> # Kill session + confirm worktree deletion
zigzag prune # Clean orphaned sessionszigzag open myapp
→ Local or remote project?
→ LOCAL:
→ Existing session for main? → attach
→ No session? → create session myapp:main with layout
→ Branch choice:
→ main (default)
→ existing branch (existing worktree)
→ new branch → wt switch -c <branch> → create session
zigzag open myapp feat/login
→ Worktree exists? → attach session myapp:feat-login
→ Otherwise → wt switch -c feat/login → create session → launch layout
zigzag open prod-api feat/x (remote project)
→ ssh vps "cd ~/Code/prod-api && wt switch -c feat/x"
→ zellij attach https://vps.example.com:8082/prod-api:feat-x --token $ZP_VPS_TOKEN
zigzag delete myapp:feat-login
→ Kill Zellij session myapp:feat-login
→ "Delete worktree feat/login? (y/N)"
→ y: wt remove feat/login
→ N: worktree kept
┌─ Zigzag ─────────────────────────────────────────────────────────┐
│ │
│ PROJECTS SESSIONS │
│ ───────── ──────── │
│ ▸ myapp ● main │
│ hermes ● feat/login 🔔 │
│ prod-api 🌐 feat/dashboard │
│ │
│ ┌─ PREVIEW ──────────────────────────────────────────────────┐ │
│ │ myapp:feat/login │ │
│ │ branch: feat/login (3 ahead, 1 behind) ● dirty │ │
│ │ PR: #42 (open) | CI: ✅ passing │ │
│ │ session: 3 tabs, 5 panes, up 2h34m │ │
│ │ │ │
│ │ recent commits: │ │
│ │ a1b2c3 fix: auth token refresh │ │
│ │ d4e5f6 feat: login form validation │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ [o]pen [n]ew [d]elete [p]rune [a]utopilot [/]search [q] │
│ myapp | local | worktrees: 3 | autopilots: 1 running │
└───────────────────────────────────────────────────────────────────┘
| Icon | Meaning |
|---|---|
● |
Active sessions for this project |
🌐 |
Remote project |
🔔 |
Notification (claude finished, CI changed, etc.) |
✅ / ❌ |
CI passing / failing |
● dirty / ● clean |
Working tree status |
- Fuzzy search:
/to filter projects and sessions - Progressive loading: renders structure immediately, fills preview async (git status, PR, CI)
- Keybindings: arrow keys by default, vim-style (
j/k/h/l) configurable - Theme: auto-detect from terminal
- Claude finished working in a pane
- CI status changed (pass → fail, fail → pass)
- Autopilot completed (success or failure)
- PR review received
File-based: events write to /tmp/zigzag/notifications/{session}. The TUI watches this directory.
Phase 4+: migrate to Zellij pipe / plugin events.
notifications {
macos-native true // macOS notification (default local)
telegram false // via Telegram bot
tui true // in Zigzag TUI if open
}Automated workflows defined in KDL. Execute action sequences in response to triggers, with Claude as a resolution agent.
- Background by default — user can close their laptop
- Optional pane —
zigzag autopilot watchto observe live - State persisted to disk — survives restarts
- Full auto by default — Claude commits + pushes directly
- Configurable:
auto_push: false+review: truefor human-in-the-loop
autopilot "pr-ci-fix" {
description "Monitor CI, fix failures with Claude, retry"
trigger "post-push"
step "monitor-ci" {
run "gh run watch --exit-status"
on-failure "fix-ci"
on-success "notify-done"
}
step "fix-ci" {
run "claude 'Fix the CI failure based on: $(gh run view --log-failed)'"
max-retries 3
on-complete "monitor-ci"
on-max-retries "notify-stuck"
}
step "notify-done" {
notify "PR CI passing ✅"
}
step "notify-stuck" {
notify "PR CI stuck after 3 attempts ❌"
}
}autopilot "pr-review-fix" {
description "Resolve PR review comments with Claude"
trigger "pr-review-received"
step "fix-comments" {
run "claude 'Resolve all PR review comments: $(gh pr view --json reviews)'"
on-complete "push-fixes"
}
step "push-fixes" {
run "git push"
on-complete "notify-done"
}
step "notify-done" {
notify "PR review comments resolved ✅"
}
}autopilot "pr-merge-when-ready" {
description "Auto-merge when PR approved + CI green"
trigger "pr-approved"
step "wait-ci" {
run "gh run watch --exit-status"
on-success "merge"
on-failure "notify-ci-fail"
}
step "merge" {
run "gh pr merge --squash --delete-branch"
on-complete "cleanup"
}
step "cleanup" {
run "zigzag delete {session}"
on-complete "notify-done"
}
step "notify-done" {
notify "PR merged and cleaned up ✅"
}
step "notify-ci-fail" {
notify "PR approved but CI failing ❌"
}
}autopilot "dependabot-auto" {
description "Auto-merge Dependabot PRs if tests pass"
trigger "pr-opened-by-dependabot"
step "run-tests" {
run "gh run watch --exit-status"
on-success "merge"
on-failure "notify-fail"
}
step "merge" {
run "gh pr merge --squash --delete-branch"
on-complete "notify-done"
}
step "notify-done" {
notify "Dependabot PR merged ✅"
}
step "notify-fail" {
notify "Dependabot PR failing ❌ — review needed"
}
}autopilot "deploy-watch" {
description "Monitor deploy after merge, rollback if error"
trigger "post-merge-main"
step "monitor-deploy" {
run "deploy_command --status"
timeout "10m"
on-success "notify-done"
on-failure "rollback"
}
step "rollback" {
run "deploy_command --rollback"
on-complete "notify-rollback"
}
step "notify-done" {
notify "Deploy successful ✅"
}
step "notify-rollback" {
notify "Deploy failed, rolled back ⚠️"
}
}autopilot "deploy-sync" {
description "Pull main changes, confirm, deploy"
trigger "new-commits-on-main"
poll-interval "5m"
step "pull" {
run "git pull origin main"
on-complete "diff-summary"
}
step "diff-summary" {
run "git log --oneline @{1}..HEAD"
on-complete "confirm-deploy"
}
step "confirm-deploy" {
confirm "Deploy these changes?"
on-accept "deploy"
on-reject "notify-skipped"
}
step "deploy" {
run "deploy_command"
on-success "notify-done"
on-failure "notify-fail"
}
step "notify-done" {
notify "Deploy successful ✅"
}
step "notify-skipped" {
notify "Deploy skipped by user"
}
step "notify-fail" {
notify "Deploy failed ❌"
}
}Users can define custom workflows in the project's .config/zigzag.kdl:
autopilot "my-custom-workflow" {
trigger "manual"
step "do-stuff" {
run "./scripts/my-script.sh"
on-complete "notify"
}
step "notify" {
notify "Done ✅"
}
}Escape hatch: run accepts any shell command.
Local machine Remote machine
───────────── ────────────────
zigzag open prod-api feat/x zellij (systemd service)
→ ssh vps "wt switch -c feat/x" → port 8082 HTTPS
→ zellij attach https://... → auth tokens
- Zellij installed + systemd service active (webserver port 8082)
- worktrunk (
wt) installed - Git repos cloned
- SSH access from local machine
Natively supported by Zellij. Multiple users can attach the same session with distinct colored cursors.
Exited (crashed/closed) Zellij sessions are ignored in zigzag list. No automatic resurrection.
| Phase | Scope | Crates |
|---|---|---|
| 1a | CLI: zigzag open, zigzag list, zigzag close, zigzag delete. KDL config. Dep checks. Dynamic layout generation. |
zigzag-core, zigzag-cli |
| 1b | TUI: ratatui, project/session navigation, fuzzy search, basic actions | zigzag-tui |
| 1c | Enriched TUI: preview pane (git + Zellij + PR/CI), Claude notifications | zigzag-tui, zigzag-core |
| 2 | Cleanup: zigzag prune, advanced worktrunk integration |
zigzag-core |
| 3 | Remote: SSH setup + Zellij HTTPS attach, host/token config | zigzag-core, zigzag-cli |
| 4 | Zellij WASM plugin — TUI embedded in Zellij | zigzag-plugin |
| 5 | Web UI — ratatui WASM + xterm.js, Leptos fallback + axum | zigzag-web |
| 6 | Autopilot: state machine, KDL DSL, built-in workflows, notifications | zigzag-autopilot |
| # | Decision | Rationale |
|---|---|---|
| 1 | Declarative project config ~/.config/zigzag/projects.kdl |
Explicit control |
| 2 | Session convention {project}:{branch} |
Readable, unique, URL-compatible |
| 3 | Worktrees via worktrunk (wt) |
Mature tool |
| 4 | zigzag delete = kill session + confirm worktree |
Protects unpushed work |
| 5 | Default layout: tab claude + tab shell, override .config/zigzag.kdl |
Covers 90% of cases |
| 6 | claude on every session |
Consistency |
| 7 | KDL config everywhere | Coherent with Zellij |
| 8 | Remote: SSH setup + zellij attach HTTPS | Worktree before session |
| 9 | Rust | WASM pipeline |
| 10 | Name zigzag |
Minimalist |
| 11 | Dep check at launch, fail if missing | Clear UX |
| 12 | Single binary: TUI without args, CLI with args | Simple |
| 13 | zigzag-core 100% I/O-agnostic via traits | WASM portability |
| 14 | TUI ratatui, auto-detect theme | Rust standard |
| 15 | Preview: git + Zellij + PR + CI, progressive loading | Full context |
| 16 | Fuzzy search | Fast navigation |
| 17 | Keybindings arrows default, vim configurable | Accessible |
| 18 | GitHub only via gh |
Simple |
| 19 | Configurable notifications: macOS native, Telegram, TUI | Flexible |
| 20 | Multiplayer supported | Native Zellij |
| 21 | Exited sessions ignored | Simplicity |
| 22 | Autopilot: KDL DSL + script escape hatch | 80/20 |
| 23 | Autopilot: background default + optional pane | Laptop-closeable |
| 24 | Autopilot: full auto default, configurable human-in-the-loop | Point of autopilot |
| 25 | Web: ratatui WASM + xterm.js, Leptos fallback | Reuses TUI |
| 26 | Deploy via deploy_command in project config |
Generic |