A macOS sidebar GUI for managing multiple concurrent Claude Code sessions. Tracks live state, organizes sessions by type, integrates with iTerm2, and lets any Claude session control the others via CLI.
You run many Claude Code sessions in parallel - one per task, research investigation, tool, or code review. jans-deck gives you a single sidebar that shows all of them at a glance: what each one is doing right now, which ones need your attention, which ones finished while you were looking elsewhere. One click brings any session to the front in iTerm2.
Generic session managers and terminal multiplexers give you panes and tabs. That is not the problem. The problem is that Claude Code sessions have structure that no generic tool understands:
- A session has a state — not just running/stopped, but processing, waiting for your reply, waiting for tool approval, or finished-but-you-haven't-looked-yet. That state lives in
~/.claude/sessions/and the JSONL transcript, not in the terminal. - A task session should create a git worktree and branch automatically, not just a directory.
- Sessions belong to features that span multiple repos. A ticket may have a dd-trace-java task, a system-tests task, and a research session — you want to see them grouped.
- Each session needs scaffolding files bootstrapped on creation:
task_plan.md,session.md,progress.md. These are what hooks inject into Claude on every tool call. - When a session closes, it should clean up the worktree, update the KB, and remove itself from the session list — a lifecycle, not just a tab.
jans-deck exists because these are Claude Code-specific concepts. A generic tool would need the same integration work added on top — at that point you are building jans-deck anyway.
Personal tool. Built for one person's specific workflow on macOS + iTerm2 + Claude Code. It assumes a fixed directory layout (
~/tasks,~/repos,~/research), a companion hooks and skills system, and opinionated conventions. It works well for that setup and will need adaptation for anything else.
Sessions are organized into five tabs:
| Tab | Directory | Created by |
|---|---|---|
| Features | ~/.claude/knowledge/_meta/features/ |
Groups sessions under a ticket ID |
| Tasks | ~/tasks/<repo>-<name>/ |
git worktree off a repo in ~/repos/ |
| Research | ~/research/<name>/ |
standalone dir |
| Tools | ~/tools/<name>/ |
standalone dir |
| Reviews | ~/reviews/<repo>-pr-<n>/ |
from a GitHub PR URL |
Each tab header shows the count of active sessions (processing + waiting + needs_input). If sessions in that tab finished while you were not looking, an unread badge appears: ◎2.
| Icon | Color | State | Meaning |
|---|---|---|---|
| ▶ | yellow | processing |
Claude is actively running |
| ● | green | waiting |
Claude finished, waiting for your next message |
| ⚡ | red | needs_input |
Tool use is pending your approval |
| ◎ | orange | unread |
Finished since you last focused it - click to clear |
| ◉ | blue | paused |
No active Claude process for this session |
| ✗ | grey | terminated |
Process exited |
State is detected every 3 seconds by reading ~/.claude/sessions/ (live process + session ID) and the JSONL transcript at ~/.claude/projects/<key>/<session-id>.jsonl. No hooks or modifications to Claude's config are needed.
How state detection works:
- Find the live Claude process for this session's
cwdin~/.claude/sessions/*.json - Locate the JSONL transcript using the live session ID (not the one stored in state.json, which may be stale)
- Classify:
- JSONL modified < 15s ago:
processing - Last message is
assistantwith a pendingtool_useand notool_result:needs_input - Last message is
assistant, quiet > 15s:waiting - No JSONL but live process found:
waiting(tab is open, no message sent yet) - No live process:
paused
- JSONL modified < 15s ago:
When a session transitions from an active state (processing, waiting, needs_input) to paused while its iTerm2 tab is not in focus, it is marked as unread. The session row shows ◎ in orange instead of its normal icon, and the tab header shows ◎N next to the count.
Clicking the session in jans clears the unread mark. Restarting jans clears all unread state (it is not persisted to disk).
The Features tab groups sessions by ticket ID. Each feature is a Markdown file in ~/.claude/knowledge/_meta/features/<TICKET>.md with YAML frontmatter:
---
ticket: APPSEC-63000
nickname: My Feature
description: What this feature does
sessions:
- dd-trace-java-my-feature-impl
- system-tests-my-feature
---Clicking a feature row expands it to show its linked sessions with live state indicators. Sessions that are not currently loaded in jans (e.g. already finished and removed) can be reloaded from the feature panel - jans will locate the session directory and add it back.
Creating a new feature from the GUI or via jans-ctl new-feature writes this file. Linking a session to a feature (at task creation time or later) appends its name to the sessions list.
Click + New in any session tab. A dialog opens:
- Task: pick a repo from
~/repos/(or paste a GitHub clone URL to clone it first), enter a name, optionally link to a feature ticket (pick from existing features or create a new one inline). Creates a git worktree at~/tasks/<repo>-<name>/and bootstrapssession.md,task_plan.md,progress.md. - Research: enter a name. Creates
~/research/<name>/with the same scaffolding files. - Review: paste a GitHub PR URL. Clones or locates the repo, creates
~/reviews/<repo>-pr-<n>/. - Tool: enter a name. Creates
~/tools/<name>/.
jans-ctl new-task <repo> <name> [ticket]
jans-ctl new-research <name>
jans-ctl new-tool <name>
jans-ctl new-review <github-pr-url>
jans-ctl new-feature <ticket> <nickname> [description]jans-ctl sends commands to the running jans instance via ~/.jans/pending_cmd.json. The GUI reads it on the next tick (≤3s) and writes the result to ~/.jans/cmd_result.json.
jans-ctl list # list all sessions and their states
jans-ctl new-research <name> # ~/research/<name>/
jans-ctl new-task <repo> <name> [ticket] # ~/tasks/<repo>-<name>/ (git worktree)
jans-ctl new-tool <name> # ~/tools/<name>/
jans-ctl new-review <github-pr-url> # ~/reviews/<repo>-pr-<n>/
jans-ctl new-feature <ticket> <nick> [desc] # feature manifest in KB
jans-ctl feature-status <ticket> # show sessions linked to a ticket
jans-ctl load <path> [name] # add an existing directory as a session
jans-ctl rename <current> <new> # rename a session
jans-ctl delete <name> # remove from jans (never deletes files or kills Claude)
jans-ctl color <name> <color> # set color tag (red orange yellow green blue purple pink teal)
jans-ctl switch <name> # bring session to front in iTerm2
jans-ctl home # focus the orchestrator session- Tab color: each session's iTerm2 tab is colored to match its state (yellow/green/red/blue). If the session has a user-defined color tag, that takes priority.
- Tab badge: the session name is set as the iTerm2 badge when the tab is first focused.
- Focus on click: clicking a session in jans brings its iTerm2 tab to the front. Matching is done by tty device (not tab title, which Claude Code overwrites dynamically).
- Paused detection: a session is considered paused when its Claude process's tty is not found in any open iTerm2 tab.
Each session can have a persistent color tag that overrides the state color on the iTerm2 tab:
red orange yellow green blue purple pink teal
Set via right-click context menu in the GUI or jans-ctl color <name> <color>.
- State is saved to
~/.jans/state.jsonevery 3 seconds. - External edits to
state.json(e.g. from a/finish-prscript removing a session) are detected by file mtime and merged into memory within one tick - jans never overwrites external changes. - Sessions deleted from
state.jsonexternally are removed from the GUI. Safety guard: active sessions (processing,waiting,needs_input) are never auto-removed even if they temporarily disappear from disk during a write race. - When a session's
cwdchanges externally, jans renames the corresponding~/.claude/projects/directory automatically to preserve conversation history.
jans runs a Claude Code session in ~/research/jans/ that acts as orchestrator. It has jans-ctl available and a CLAUDE.md that explains how to use it. You can dictate to it (e.g. via Wispr Flow) and it will create sessions, check status, and route work across all your other sessions on your behalf.
Example: say "open a task for dd-trace-java on the gRPC timeout fix linked to APPSEC-99001" and the orchestrator runs:
jans-ctl new-task dd-trace-java grpc-timeout-fix APPSEC-99001jans/
├── gui.py # tkinter main window (JansApp) - 3s tick loop
├── ctl.py # jans-ctl CLI entry point
├── models.py # Session dataclass, SessionState enum
└── core/
├── state_detector.py # reads ~/.claude/sessions/ + JSONL to classify state
├── persistence.py # save/load ~/.jans/state.json, mtime-based merge, project dir migration
├── commands.py # IPC: write/read ~/.jans/pending_cmd.json + cmd_result.json
├── features.py # read/write ~/.claude/knowledge/_meta/features/*.md
└── log.py # rotating file logger at ~/.jans/jans.log
Directory layout at runtime:
~/
├── research/jans/ # jans source + orchestrator session cwd
├── repos/ # bare or full git repos (sources for worktrees)
├── tasks/<repo>-<name>/ # task worktrees
├── research/<name>/ # research sessions
├── tools/<name>/ # tooling sessions
├── reviews/<repo>-pr-<n>/ # PR review sessions
└── .jans/
├── state.json # persisted session list
├── pending_cmd.json # IPC: jans-ctl -> GUI
├── cmd_result.json # IPC: GUI -> jans-ctl
└── jans.log # debug log
Requirements: macOS, Python 3.12+, iTerm2.
tkinter is not included in Homebrew Python. Use python.org or pyenv with framework build:
env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.12
git clone git@github.com:jandro996/jans-deck.git ~/research/jans
cd ~/research/jans
python3 -m venv .venv-menu
source .venv-menu/bin/activate
pip install -e .Start jans:
~/research/jans/.venv-menu/bin/python3 -m jans.gui &Restart after code changes:
pkill -f "python.*jans.gui"; sleep 1
~/research/jans/.venv-menu/bin/python3 -m jans.gui &