Cross-platform mission control for Claude Code. Node.js CLI/TUI with React/Ink.
- Package:
packages/cli/— monorepo, single package for now - Entry:
src/index.ts→ lazy-loads TUI or CLI based on args - TUI:
src/tui/App.tsx(full dashboard),src/tui/MiniApp.tsx(popup wizard) - Config:
~/.config/cldctrl/config.json(or%APPDATA%\cldctrl\) - Cache:
~/.config/cldctrl/cache.json(written by daemon every 5 min) - Log:
~/.config/cldctrl/debug.log(JSON, 5MB rotation) - Version: 0.2.0 (npm:
cldctrl) - Website:
docs/→ GitHub Pages → https://cld-ctrl.com - License: AGPL-3.0
# Full TUI dashboard
cc # or: cldctrl, cld
# Mini popup (Ctrl+Up hotkey)
cc --mini
# CLI commands
cc list [--json] # list projects with git status
cc launch <name> # launch project in Claude Code
cc stats [--json] # daily usage stats
cc issues [project] # GitHub issues
cc summarize # generate AI summaries for all sessions
cc setup # install Ctrl+Up hotkey
cc daemon # start background poller
# Demo mode (synthetic data for screenshots)
cc --demo [full|fresh|no-github|minimal]
# Safe mode (bypass diff renderer — more flicker, zero corruption)
cc --safe
# Debug diff renderer (logs frame stats to debug.log)
# Linux/macOS: DEBUG_DIFF=1 cc
# Windows cmd: set DEBUG_DIFF=1 && cc
# Windows PowerShell: $env:DEBUG_DIFF="1"; ccsrc/
├── index.ts Entry point — lazy-loads CLI or TUI
├── cli.ts Commander CLI (lazy-loaded, not on TUI path)
├── config.ts Zod-validated config with migration v1→v4
├── constants.ts Colors, chars, defaults, shared formatters
├── types.ts All type definitions (Config, Project, Session, etc.)
├── daemon.ts Background poller: git/issues/stats → cache.json
├── core/
│ ├── activity.ts Full session activity parsing (tools, models, tokens)
│ ├── analyzer.ts Session analysis to suggest skills and project memories
│ ├── background.ts Daemon cache I/O, seen-issues persistence
│ ├── claude-cli.ts Claude Code CLI interaction helpers
│ ├── claude-usage.ts Rate limit probing via API, tier detection, overage tracking
│ ├── command-usage.ts Slash command usage scanning
│ ├── demo-data.ts Synthetic data for --demo mode
│ ├── filetree.ts File tree: lazy dir reading, gitignore, icons, preview
│ ├── git.ts Git status + recent commits via child process
│ ├── github.ts GitHub issues via `gh` CLI
│ ├── launcher.ts Launch Claude Code with env cleanup
│ ├── logger.ts Structured JSON logging with rotation
│ ├── platform.ts Cross-platform helpers (paths, TTY, file explorer)
│ ├── pricing.ts Per-session cost estimation (blended rate model)
│ ├── processes.ts Active session detection (markers, PIDs, mtime)
│ ├── project-cache.ts Fast project list caching
│ ├── projects.ts Slug gen, discovery, list building (fast + full paths)
│ ├── scanner.ts Project scanner: BFS discovery with depth limit
│ ├── sessions.ts JSONL parsing, session stats, rolling usage
│ ├── setup.ts Cross-platform hotkey setup dispatch
│ ├── setup-windows.ts Windows hotkey: VBS + Scheduled Task + hotkey.ps1
│ ├── setup-macos.ts macOS hotkey: LaunchAgent + plist
│ ├── setup-linux.ts Linux hotkey: systemd user timer + shell script
│ ├── sixel.ts Sixel image protocol support
│ ├── skills.ts Claude Code commands/skills discovery
│ ├── summaries.ts AI summary generation via `claude --print`
│ ├── tailer.ts Live JSONL tailing with incremental byte-offset parsing
│ ├── tracker.ts PID-based session tracking
│ └── usage.ts Per-project daily usage aggregation
└── tui/
├── App.tsx Root TUI: split-pane layout, data orchestration
├── MiniApp.tsx Mini popup: 3-phase wizard
├── diffRenderer.ts Differential screen rendering (eliminates flicker)
├── helpItems.ts Help overlay key/description pairs
├── snapshot.tsx Screenshot capture for testing
├── components/
│ ├── ActiveBadge.tsx Live session status indicator
│ ├── ActivitySparkline.tsx Inline token usage sparkline
│ ├── ActivityTrace.tsx Session activity timeline
│ ├── CalendarHeatmap.tsx Weekly grid with ░▒▓█ shading
│ ├── ConversationDetail.tsx Expanded conversation view
│ ├── ConversationPane.tsx Live conversations list
│ ├── DetailPane.tsx Right pane: sessions, issues, commits, files
│ ├── FilterBar.tsx Type-to-filter overlay
│ ├── HelpOverlay.tsx Help screen
│ ├── MatrixGlitch.tsx Easter egg: Matrix-style green cascade
│ ├── MiniActionMenu.tsx Mini TUI action menu
│ ├── MiniProjectList.tsx Mini TUI project list
│ ├── ProgressBar.tsx Budget progress bar
│ ├── ProjectPane.tsx Left pane: project list + git badges + calendar
│ ├── PromptBar.tsx New session prompt input
│ ├── SettingsPane.tsx Settings editor (`,` key)
│ ├── StatusBar.tsx Bottom status bar
│ └── Welcome.tsx First-run welcome screen
├── hooks/
│ ├── useAnimations.ts Pulse, clock, spinner, animated counter
│ ├── useAppState.ts Reducer: config + projects + navigation
│ ├── useBackgroundData.ts Polling hooks: git, issues, usage, live tailing
│ ├── useFileTree.ts File tree state: lazy loading, expand/collapse
│ ├── useKeyboard.ts Keyboard input handler
│ ├── useMiniKeyboard.ts Mini TUI keyboard handler
│ └── useMiniState.ts Mini TUI state reducer
└── games/
└── GameScreen.tsx Hidden games (Ctrl+G)
docs/
├── index.html Landing page (cld-ctrl.com) — GitHub Pages
├── CNAME Custom domain config
└── *.png, *.svg, *.gif Screenshots and logo assets
index.tsloads onlyplatform.tsandtracker.tseagerly- CLI module (
cli.ts+ Commander/zod/git/github) lazy-loaded only for subcommands - TUI uses
buildProjectListFast()— cached names, no git spawns - Background hooks read daemon cache for instant git statuses + calendar data
- Full project names (with git remote extraction) refresh 500ms after mount
usePulse(800): boolean toggle for pulsing indicatorsuseSpinner(150): braille spinner at 6.7fps (not 12.5fps)useClock(): extracted toClockDisplaycomponent — 1s re-render scoped to header onlyuseAnimatedCounter(32ms): ease-out token count animationMatrixGlitch: 150ms interval during rare 2-3s burstsenrichedProcesses: value-based equality check prevents unnecessary Map allocationsProjectPaneandDetailPanewrapped inReact.memo
Daemon (background process)
↓ writes cache.json every 5 min
TUI hooks (useBackgroundData.ts)
↓ read cache on mount for instant first paint
↓ poll for fresh data in background
↓ shallowEqual prevents no-op re-renders
Components
↓ DetailSnapshot cache for instant scroll
↓ settledPath debounce (150ms) prevents expensive fetches during rapid scroll
Claude Code sets CLAUDECODE=1 when running. To launch Claude Code from within Claude Code, clear all CLAUDE* env vars except CLAUDE_CODE_GIT_BASH_PATH (needed for git-bash on Windows). See launcher.ts:getCleanEnv().
The VBS startup script and scheduled task both launch hotkey.ps1 via PowerShell.
Both MUST use -NoProfile to skip the user's PowerShell profile (which may activate
conda, load modules, etc.) — without it, the hotkey listener window stays visible
for minutes during profile loading instead of being hidden.
All hooks must be called unconditionally. Demo mode checks use const demo = isDemoMode() at the top, then guard side effects inside hooks with if (demo) return. Never add an early return before hook calls.
tsup build may fail with EBUSY: resource busy or locked due to Dropbox sync locking temp files. Just retry the build.
The renderer (diffRenderer.ts) intercepts both stdout.write() AND stderr.write()
during alternate screen mode. It uses a "last write replaces" strategy — each write()
call replaces the previous one (not accumulated). Ink writes each complete frame as a
single write() call, so the last write per event-loop tick IS the latest frame.
A setImmediate flush processes the latest write: strips non-SGR control sequences,
splits into lines, and redraws ALL lines with explicit cursor positioning. No line
diffing is used — full redraw every frame prevents stale-line bleed-through.
Critical: content-before-erase pattern. Each line is written as:
\x1b[row;1H\x1b[0m{content}\x1b[0m\x1b[K
Content overwrites old content in-place, then \x1b[K erases the tail. NEVER use
\x1b[2K (erase entire line) before content — it creates a visible blank-line flash
that terminals don't fully batch, even with DEC 2026 synchronized output.
stderr is suppressed during alt screen to prevent console.error, unhandled rejection warnings, or logger verbose output from corrupting the display.
Rules for future changes:
- Never use
overflow="hidden"on Ink Box components — it's broken in Ink 5.x (randomly drops lines instead of clipping). Enforce height budgets manually. - Every rendered row must be counted: borders = 2 rows,
marginTop={1}= 1 row. - Use
innerHeight = height - 2when a Box hasborderStyle. - Cap list items to fit within available rows — don't rely on Ink to clip.
- NEVER use
console.log/console.errorin TUI-path code. stdout writes replace the current frame with garbage. stderr is suppressed but still lost. Uselogger.tsfor debug output instead. - Test with real data, not just
--demo. Demo uses synchronous static data and won't expose timing-dependent rendering bugs. - NEVER use line diffing in the renderer. Full redraw every frame. The performance cost is negligible (~7KB/frame) and eliminates all sync issues.
- NEVER use buffer accumulation — "last write replaces" is simpler and correct.
- NEVER use
\x1b[2Kbefore content — always content-then-\x1b[K]. cc --safebypasses the diff renderer entirely as a fallback.
Session files can reach 50MB. Always read only what you need:
getProjectPathFromSlug(): reads first 32KB + regex fallback for truncated linestailer.ts: caps initial read to last 1MBsessions.ts: streams with readline, respectsmaxSessionFileSize
Three detection sources in processes.ts (priority order):
- Markers (
pids/*.json) — cc-launched sessions. Trusted while file exists. - Tracked PIDs (
tracked-sessions.json) — legacy, PIDs are unreliable. - JSONL mtime — externally-launched sessions. Creates one
ActiveSessionper recent JSONL file (supports multiple sessions per project).
Two thresholds:
ACTIVE_THRESHOLD_MS(5h) — how far back to look for sessions (conversations window)IDLE_THRESHOLD_MS(5min) — sessions older than this show as idle (yellow dot, dimmed)
Critical: The idle check must use !!session.idle, NOT session.tracked && session.idle.
The tracked flag is only set for marker/PID sessions. Mtime-detected sessions have
tracked: undefined, so tracked && idle is always false — they never appear idle.
Hidden projects with active sessions are auto-unhidden: useActiveProcesses receives
hiddenPaths so mtime detection can find them, and the auto-add effect dispatches
UNHIDE_PATHS to remove them from config.hidden_projects.
H key toggles showHidden state (display integration pending).
- File tree (
useFileTree.ts): Lazy-loads directories on expand. Caches children inMap<relativePath, FileNode[]>. Resets when project changes. Respects.gitignoreviaparseGitignore(). File type icons from extension/name maps. - Scanner (
scanner.ts): Synchronous BFS with configurable depth limit (default 5). UsesPROJECT_INDICATORS(CLAUDE.md, .git, package.json, etc.).SKIP_DIRSexcludes node_modules, .git, AppData, etc. SupportsAbortSignalfor cancellation. - Detail pane tabs:
sessions | commits | issues | files—fkey quick-jumps to files,Striggers scan.
Windows paths are case-insensitive. Use normalizePathForCompare() from platform.ts for path comparisons. The daemon cache uses raw paths as keys.
Defined in constants.ts. Both ANSI escape sequences (COLORS) and Ink hex values (INK_COLORS):
- Background:
#06080d(near-black) - Highlight:
#235F28(selected row green) - Accent:
#e87632(CLD orange) - Text:
#CCCCCC, Dim:#808080 - Green:
#2dd4bf(teal success) - Yellow:
#f59e0b(amber warning) - Blue:
#388cff(secondary blue)
{
"config_version": 4,
"projects": [{ "name": "My Project", "path": "/path/to/project", "alias": "mp" }],
"hidden_projects": [],
"launch": { "explorer": true, "vscode": true, "claude": true },
"icon_color": "#DA8F4E",
"global_hotkey": { "modifiers": "Ctrl", "key": "Up" },
"project_manager": { "enabled": true },
"daily_budget_tokens": 1000000,
"notifications": {
"github_issues": { "enabled": true, "poll_interval_minutes": 5 },
"usage_stats": { "enabled": true, "show_tooltip": true }
}
}| Package | Size | Purpose |
|---|---|---|
| commander | ~200KB | CLI arg parsing (lazy-loaded) |
| cross-spawn | ~30KB | Cross-platform child process spawning |
| ink | ~1MB | React for terminal UIs |
| react | ~300KB | React runtime |
| p-limit | ~24KB | Concurrency limiter for git/gh spawns |
| zod | ~4.8MB | Config schema validation |
| node-notifier | ~5.5MB | Desktop notifications (daemon only) |
# Type check
npx tsc --noEmit
# Build
npx tsup
# Manual smoke tests
cc --demo --snapshot # renders one frame, exits
cc --demo # full TUI with synthetic data
cc --version # quick startup test
cc list --json # CLI pipeline test