feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# Claude Code Statusline
|
||||
|
||||
A color-coded statusline for Claude Code showing model, user, working directory, git branch, context window usage, token counts, and session cost.
|
||||
|
||||
## Preview
|
||||
|
||||
Example statusline with all segments visible in a git repo and 24% context window usage:
|
||||
| Segment | Color | Example |
|
||||
| ----------- | -------------------- | -------------------------------------------------- |
|
||||
| Model | Cyan | `Sonnet 4.6` |
|
||||
| User | Green | `nguyens6` |
|
||||
| CWD | Yellow | `~/agent-dashboard/client` |
|
||||
| Git branch | Magenta | `main` (hidden outside git repos) |
|
||||
| Context bar | Green → Yellow → Red | `████████░░ 79%` |
|
||||
| Tokens | Green / Cyan / Dim | `3↑ 2↓ 156586c` (green `↑` in, cyan `↓` out, dim `c` cache reads) |
|
||||
| Cost (USD) | Green → Yellow → Red | `$0.4231` (session total — shown on API and subscription plans) |
|
||||
|
||||
Context bar color thresholds:
|
||||
|
||||
- **Green** — under 50% used
|
||||
- **Yellow** — 50–79% used
|
||||
- **Red** — 80%+ used
|
||||
|
||||
Cost color thresholds:
|
||||
|
||||
- **Green** — under $5
|
||||
- **Yellow** — $5–$20
|
||||
- **Red** — $20+
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6+
|
||||
- Git (for branch detection)
|
||||
- Claude Code 2.x+
|
||||
|
||||
## Installation
|
||||
|
||||
**1. Copy both files into your Claude config directory:**
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
cp statusline.py ~/.claude/statusline.py
|
||||
cp statusline-command.sh ~/.claude/statusline-command.sh
|
||||
chmod +x ~/.claude/statusline-command.sh
|
||||
|
||||
# Windows (Git Bash)
|
||||
cp statusline.py "$HOME/.claude/statusline.py"
|
||||
cp statusline-command.sh "$HOME/.claude/statusline-command.sh"
|
||||
```
|
||||
|
||||
**2. Update the path in `statusline-command.sh`:**
|
||||
|
||||
Open `~/.claude/statusline-command.sh` and replace the path with your own home directory:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
PYTHONUTF8=1 python3 "/your/home/.claude/statusline.py"
|
||||
```
|
||||
|
||||
On Windows this looks like:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
PYTHONUTF8=1 python3 "C:/Users/YOUR_USERNAME/.claude/statusline.py"
|
||||
```
|
||||
|
||||
On macOS/Linux:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
python3 "$HOME/.claude/statusline.py"
|
||||
```
|
||||
|
||||
**3. Add to `~/.claude/settings.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"/path/to/home/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Windows example:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"C:/Users/YOUR_USERNAME/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
macOS/Linux example:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"/home/YOUR_USERNAME/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Restart Claude Code** — fully exit and relaunch. Claude Code does not hot-reload `settings.json`, so the new statusline will not show up until the process is restarted.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**The statusline still shows the default (e.g. `[Model] 📁 cwd`) after restarting.**
|
||||
|
||||
Claude Code resolves `statusLine` from multiple settings files, in this precedence (later wins):
|
||||
|
||||
1. `~/.claude/settings.json` (user, global)
|
||||
2. `~/.claude/settings.local.json` (user, local — **often overrides the global**)
|
||||
3. `<project>/.claude/settings.json` (project, shared)
|
||||
4. `<project>/.claude/settings.local.json` (project, local)
|
||||
|
||||
If any of those later files defines its own `statusLine` block, it **replaces** the global one — yours will never run. Grep for it:
|
||||
|
||||
```bash
|
||||
grep -l statusLine ~/.claude/settings*.json $(find . -maxdepth 3 -name 'settings*.json' -path '*.claude*' 2>/dev/null)
|
||||
```
|
||||
|
||||
For each file that defines a competing `statusLine`, either delete that block (to fall back to the global one) or point it at the same script:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "bash \"/path/to/home/.claude/statusline-command.sh\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**The statusline is blank / missing segments.**
|
||||
|
||||
Run the script manually with a sample payload to confirm it works:
|
||||
|
||||
```bash
|
||||
echo '{"model":{"display_name":"Sonnet 4.6"},"workspace":{"current_dir":"'"$HOME"'"},"context_window":{"used_percentage":25,"current_usage":{"input_tokens":1000,"output_tokens":500,"cache_read_input_tokens":200}},"cost":{"total_cost_usd":0.4231}}' \
|
||||
| sh ~/.claude/statusline-command.sh
|
||||
```
|
||||
|
||||
If nothing prints, check that `python3` is on your `PATH` and `~/.claude/statusline.py` is readable. The script always exits 0 by design so Claude Code never blocks — errors are silent, so test from the shell first.
|
||||
|
||||
## How It Works
|
||||
|
||||
Claude Code pipes a JSON object to the statusline command's stdin on each update. The script reads that JSON, extracts the relevant fields, and prints a color-coded string using ANSI escape codes.
|
||||
|
||||
Key fields used from the JSON payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": { "display_name": "Sonnet 4.6" },
|
||||
"workspace": { "current_dir": "C:\\Users\\..." },
|
||||
"context_window": {
|
||||
"used_percentage": 79,
|
||||
"current_usage": {
|
||||
"input_tokens": 3,
|
||||
"output_tokens": 2,
|
||||
"cache_read_input_tokens": 156586
|
||||
}
|
||||
},
|
||||
"cost": { "total_cost_usd": 0.4231 }
|
||||
}
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
Edit `statusline.py` directly to change colors, reorder segments, or remove ones you don't want. Each segment is clearly labeled with a comment.
|
||||
|
||||
Color constants at the top of the file:
|
||||
|
||||
```python
|
||||
CYAN = '\033[0;36m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[0;33m'
|
||||
MAGENTA = '\033[0;35m'
|
||||
RED = '\033[0;31m'
|
||||
DIM = '\033[2m'
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# @file statusline-command.sh — wrapper that Claude Code invokes for its custom statusline;
|
||||
# pipes the JSON event payload into statusline.py (update the path to match your install).
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
#
|
||||
PYTHONUTF8=1 python3 "C:/Users/nguyens6/.claude/statusline.py"
|
||||
# IMPORTANT:
|
||||
# Put the statusline.py file in the .claude folder in your user directory (e.g. C:/Users/nguyens6/.claude/statusline.py) and make sure to update the path in the above command if you place it somewhere else.
|
||||
# Change the above path to the location of your statusline.py file. You can also change the command to run a different script if you want to display something else in your status line.
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
statusline.py — Claude Code custom statusline renderer.
|
||||
|
||||
Reads a JSON event payload from stdin (emitted by Claude Code on every turn)
|
||||
and prints a single ANSI-colored status line to stdout. Renders the model
|
||||
display name, current user, working directory (with home collapsed to ``~``),
|
||||
active git branch, a color-coded context window usage bar (green / yellow /
|
||||
red thresholds), and token counts. Always exits 0 so it can never block
|
||||
Claude Code — missing fields are silently skipped.
|
||||
|
||||
Intended to be referenced from Claude Code's ``statusLine`` setting (see
|
||||
``statusline-command.sh`` for the wrapper) or installed to
|
||||
``~/.claude/statusline.py``.
|
||||
|
||||
:file: statusline/statusline.py
|
||||
:module: Claude-Code-Agent-Monitor.statusline
|
||||
:author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
:maintainer: Nguyễn Ngọc Trí Vĩ (a.k.a. David Nguyen, smartgift)
|
||||
:copyright: (c) 2026 Nguyễn Ngọc Trí Vĩ
|
||||
:license: MIT
|
||||
:repository: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor
|
||||
:requires: Python 3.6+
|
||||
:encoding: utf-8
|
||||
|
||||
@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
"""
|
||||
|
||||
__file_name__ = "statusline.py"
|
||||
__module__ = "Claude-Code-Agent-Monitor.statusline"
|
||||
__author__ = "Nguyễn Ngọc Trí Vĩ"
|
||||
__email__ = "vinnt@smartgift.vn"
|
||||
__maintainer__ = "Nguyễn Ngọc Trí Vĩ"
|
||||
__copyright__ = "Copyright (c) 2026 Nguyễn Ngọc Trí Vĩ"
|
||||
__license__ = "MIT"
|
||||
__version__ = "1.0.0"
|
||||
__status__ = "Production"
|
||||
__repository__ = "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor"
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
CYAN = '\033[0;36m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[0;33m'
|
||||
MAGENTA = '\033[0;35m'
|
||||
RED = '\033[0;31m'
|
||||
DIM = '\033[2m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
raw = sys.stdin.read().strip()
|
||||
if not raw:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
parts = []
|
||||
|
||||
# Model
|
||||
model = (data.get('model') or {}).get('display_name', '')
|
||||
if model:
|
||||
parts.append(f"{CYAN}{model}{RESET}")
|
||||
|
||||
# User
|
||||
user = os.environ.get('USERNAME') or os.environ.get('USER', '')
|
||||
if user:
|
||||
parts.append(f"{GREEN}{user}{RESET}")
|
||||
|
||||
# CWD — strip home prefix
|
||||
cwd = (data.get('workspace') or {}).get('current_dir') or data.get('cwd', '')
|
||||
if cwd:
|
||||
home = os.path.expanduser('~') # C:\Users\nguyens6
|
||||
if cwd.startswith(home):
|
||||
cwd = '~' + cwd[len(home):].replace('\\', '/')
|
||||
else:
|
||||
cwd = cwd.replace('\\', '/')
|
||||
parts.append(f"{YELLOW}{cwd}{RESET}")
|
||||
|
||||
# Git branch
|
||||
git_dir = (data.get('workspace') or {}).get('current_dir') or data.get('cwd', '')
|
||||
if git_dir:
|
||||
try:
|
||||
branch = subprocess.check_output(
|
||||
['git', '-C', git_dir, '--no-optional-locks', 'symbolic-ref', '--short', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL
|
||||
).decode().strip()
|
||||
except Exception:
|
||||
try:
|
||||
branch = subprocess.check_output(
|
||||
['git', '-C', git_dir, '--no-optional-locks', 'rev-parse', '--short', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL
|
||||
).decode().strip()
|
||||
except Exception:
|
||||
branch = ''
|
||||
if branch:
|
||||
parts.append(f"{MAGENTA}{branch}{RESET}")
|
||||
|
||||
# Context bar
|
||||
ctx = data.get('context_window') or {}
|
||||
used_pct = ctx.get('used_percentage')
|
||||
if used_pct is not None:
|
||||
bar_len = 10
|
||||
filled = round(bar_len * used_pct / 100)
|
||||
bar = '█' * filled + '░' * (bar_len - filled)
|
||||
color = RED if used_pct >= 80 else YELLOW if used_pct >= 50 else GREEN
|
||||
parts.append(f"{color}{bar} {used_pct}%{RESET}")
|
||||
|
||||
# Tokens
|
||||
usage = ctx.get('current_usage') or {}
|
||||
in_tok = usage.get('input_tokens')
|
||||
out_tok = usage.get('output_tokens')
|
||||
cache = usage.get('cache_read_input_tokens')
|
||||
if in_tok is not None and out_tok is not None:
|
||||
tok_parts = [f"{GREEN}{in_tok}↑{RESET}", f"{CYAN}{out_tok}↓{RESET}"]
|
||||
if cache:
|
||||
tok_parts.append(f"{DIM}{cache}c{RESET}")
|
||||
parts.append(' '.join(tok_parts))
|
||||
|
||||
# Session cost (USD) — shown on both API and subscription plans
|
||||
cost = (data.get('cost') or {}).get('total_cost_usd')
|
||||
if cost is not None:
|
||||
try:
|
||||
cost_f = float(cost)
|
||||
cost_color = RED if cost_f >= 20 else YELLOW if cost_f >= 5 else GREEN
|
||||
parts.append(f"{cost_color}${cost_f:.4f}{RESET}")
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
sep = f"{DIM} | {RESET}"
|
||||
print(sep.join(parts))
|
||||
Reference in New Issue
Block a user