Side project · macOS desktop app
Agents City
I run a lot of Claude Code agents in parallel. Agents City turns them into a living isometric city, so I can tell who's working, who's done, and who needs me without opening a single terminal.
Role
Solo: design, engineering, product
Platform
macOS (Electron) + browser mode
Year
2026
Status
Daily driver
All screens are recreations with invented data. The real app's UI is in Spanish.
Ten agents, ten terminals, zero overview.
My day job and side projects run on Claude Code: interactive sessions, background agents (claude --bg) and the subagents they spawn. On a busy day that's ten or more agents across five repos.
The bottleneck stopped being the agents and became me. An agent would sit blocked for twenty minutes waiting for a yes/no, while I was staring at another terminal. Finished work went unnoticed. I had no picture of what was happening where.
A map of everything my agents are doing.
01
Your folders become a city
Every folder in ~/dev becomes a city, sub-folders are districts and each repo is a building. Buildings grow with the project's history, light their windows when agents are inside and put up scaffolding while one is building something.
- Agents are people: hard hat when working, “z” when idle, red bubble when they need you
- Subagents are little robots that patrol the block, colored by type (Explore, Plan…)
- A speech bubble shows the live tool call: 💻 Bash pnpm test, ✏️ Edit hero.tsx
- City tabs, a ⌘K jump palette and a roster grouped by “needs you / working / waiting / done”
02
Talk to any agent from the map
Click an agent to open its conversation, rendered as markdown with every tool call and running subagent. From the same panel I send follow-up orders, pick model, permission mode and effort, attach screenshots or queue work for when the turn ends.
- Interrupt-and-send, queue, hand off to another agent, stop or remove
- Rename an agent mid-turn: applied safely once the turn ends
- Prompt history with ↑ / ↓, just like a shell
- Dark mode with stars, because agents work late too
03
A terminal and dev servers, per building
Click a building to launch a new agent there, or prefix the order with ! to run a shell command in that repo without going through any agent. Output can be shared with an agent in one click, which is perfect for “here's the error, fix it”.
- Runs through the login shell, so aliases and .zshrc just work
- Exit code and timing per command, last five kept
- Running dev servers detected per repo and worktree, with a stop button
- Start a dev server from the panel, on any branch or worktree
04
It taps you on the shoulder
The menu bar shows “❗ blocked · working” at a glance. Native notifications fire when an agent needs me, when a background agent finishes, or when a long turn ends while I'm in another app. Clicking one flies the camera to that agent.
- Dock badge with the number of blocked agents
- Per-city mute and per-type notification toggles
- No noise: the first snapshot is a baseline and short turns are ignored
- Opens at login and lives in the menu bar when the window is closed
Four states you can read from across the room.
Working
Hard hat, walking, live tool bubble. The building's antenna blinks.
Needs you
Pulsing red “❗ Te necesita” bubble. Notification and Dock badge.
Waiting
Standing still with floating z's. Ready for the next order.
Done
Green “✅ Listo” bubble. Fades out after the session ends.
No database, no cloud. Just files, a watcher and a canvas.
Claude Code already writes everything to disk. Agents City watches those files, builds a model of the world in a Node process inside Electron, and streams changes to a Canvas 2D renderer over Server-Sent Events.
Sources on disk
~/.claude/sessions/*.json
~/.claude/projects/**/*.jsonl
~/.claude/jobs/*/state.json
~/dev folder tree
lsof (dev servers)
Node core (Electron main)
watcher
fs.watch + 150 ms debounce + 2 s poll
collector
incremental JSONL by byte offset
world model
cities → districts → buildings
actions
claude CLI via login shell
What you see
Canvas 2D city
SSE /api/events
Menu bar + Dock
tray rebuilt ≤ 1/s
Notifications
diffed against a baseline
Problems worth solving
01
fs.watch is unreliable on macOS
Events get dropped or duplicated. I combined a 150 ms debounced watcher with a 2 s polling fallback and only emit when the serialized snapshot actually changes.
02
Transcripts grow forever
Session logs are append-only JSONL that can reach many MB. The collector reads incrementally from the last byte offset and resets if the file shrinks.
03
A localhost API anyone could call
The server binds to 127.0.0.1 on a random port, injects a per-launch token into the page and checks Host and Origin, so other browser tabs can't drive your agents.
04
Notifications that don't cry wolf
The first snapshot only sets a baseline, turn-end alerts need 30 s+ and an unfocused window, and every city can be muted.
05
Apps launched from Finder have no shell
Agent commands run through $SHELL -lic so PATH, aliases and version managers behave exactly like in the terminal.
06
Two agents, one worktree
Resuming a background agent with new options can spawn a twin on the same worktree. The app detects twins and removes worktrees safely, keeping or committing dirty changes first.
0
runtime dependencies
~8.6k
lines of TypeScript and JS
19
agent actions exposed by the local API
150 ms
from a file change to the city
Built with
Want something like this for your team?
I build internal tools, AI agents and the dashboards that keep them in check.