{"slug": "how-we-run-five-coding-agents-side-by-side-in-one-window", "title": "How we run five coding agents side by side in one window", "summary": "NestMux, a desktop app developed by an engineer, runs five coding agents side by side in a grid of real terminals, each with its own account and git worktree. The app avoids AI-specific code by treating agents as plain terminal processes, using isolated HOME directories and git worktrees for parallel operation. The developer highlights that broadcasting keystrokes to all panes is simple but has trade-offs, and measuring per-pane resource usage is the hardest part due to process trees.", "body_md": "I build NestMux, a desktop app that runs Claude Code, Codex, Gemini CLI, Copilot and OpenCode in a grid of real terminals, each with its own account and its own git worktree. This is how it works inside, including the parts that are heuristics wearing a confident UI and the parts that do not survive a restart.\n\nIf you are building something similar, most of this transfers. If you are deciding whether to use it, this is the honest version of what it does.\n\nThirty-five seconds of it running, so the rest of this has something to attach to:\n\nEvery cell in the grid is described by a plain object. Simplified, and this really is most of it:\n\n```\ninterface PaneNode {\n  id: string\n  aiType: 'claude' | 'codex' | 'gemini' | 'copilot' | 'opencode' | 'terminal' | 'custom'\n  accountName: string\n  accountDir: string        // becomes HOME for this pane's processes\n  cmd: string               // '' means a plain shell\n  repoPath?: string         // cwd override: the worktree this pane works in\n  shellId?: string          // which shell to spawn on Windows\n  borderColor: string\n}\n```\n\nThere is no AI-specific code behind any of it. A pane spawns a shell with `node-pty`\n\n, redirects `HOME`\n\nto `accountDir`\n\n, sets the cwd to `repoPath`\n\n, and types `cmd`\n\ninto it. Claude Code is a pane whose `cmd`\n\nis `claude`\n\n. A plain terminal is a pane whose `cmd`\n\nis empty. Adding support for a new agent CLI is a row in a list, which is why custom CLIs are a user-facing feature rather than a release.\n\nThe reason it is worth saying out loud: everything below is about the environment around the process, not about the agent. We do not parse the agent's output, we do not wrap its API, and we do not know what it is doing. It is a program in a terminal.\n\nThe two things that make it more than five terminals are what `accountDir`\n\nand `repoPath`\n\nare pointed at.\n\n`accountDir`\n\nis an isolated HOME, so two panes can be signed into two different Claude accounts at once, and `~/.claude`\n\nmeans something different in each. `repoPath`\n\nis a git worktree, so four agents editing the same repository do not overwrite each other. I wrote up [the Windows specifics of both](https://dev.to/eliseomdq/isolation-file-ownership-and-cleanup-the-boring-half-of-running-coding-agents-in-parallel-on-1n0f) separately, because that part is genuinely fiddly and not interesting here.\n\nSending one prompt to every pane sounds like it needs a protocol. It does not. The terminal emulator already hands you every keystroke:\n\n``` js\nterm.onData((data) => {\n  const targets = broadcastMode ? panes.map(p => p.id) : [pane.id]\n  targets.forEach(id => window.pty.write(id, data))\n})\n```\n\nThat is the whole feature. Because it operates on raw input rather than on a message, it works with anything that reads a terminal: agents with their own TUI, a REPL, `git rebase -i`\n\n. Nothing had to be taught about it.\n\nThe cost of that simplicity is real and worth stating. It broadcasts everything, including `Ctrl+C`\n\nand arrow keys. It does not check whether a pane is ready, so if one agent is still booting it gets your prompt as input to whatever prompt it happens to be showing. There is no \"wait for all panes to be idle\". That is a design position, not an oversight: the moment you add readiness detection you are parsing agent output, and then every CLI update can break you.\n\nShowing CPU, memory and open ports per pane sounds like bookkeeping. It is the hardest thing in the app.\n\nThe problem is that the pane's own process is a shell, and nobody cares about the shell. The agent is its child. The dev server the agent started is a grandchild, or worse, has been reparented and is nobody's child at all. If you measure the PID you spawned, every pane reads about 77 MB and looks idle.\n\nSo you resolve the whole tree per pane, per polling cycle. On Windows that means one `Get-CimInstance Win32_Process`\n\nsnapshot for the entire machine, cached for a second and a half and shared by everything that needs it in that cycle, then walked in memory. ([Why not pidtree](https://dev.to/eliseomdq/wmic-is-gone-and-your-node-process-tree-is-silently-wrong-on-windows-11-3e8c) is its own story.)\n\nAnd when the parent link is gone, which on Windows it frequently is, the fallback is the filesystem:\n\n``` js\nconst prefix = rootPath.toLowerCase().replace(/\\\\/g, '/').replace(/\\/+$/, '') + '/'\nfor (const [pid, path] of snap.pathByPid) {\n  if (path.toLowerCase().replace(/\\\\/g, '/').startsWith(prefix)) out.add(pid)\n}\n```\n\nIf a process's executable path or command line points inside a worktree, that process belongs to that worktree's pane. For processes that do not carry the path either, there is a third pass that reads each process's current directory, which on Windows means going through `ntdll`\n\nbecause there is no API for it.\n\nBe clear about what this is: a heuristic. It attributes correctly for the common cases and it misses elevated processes and processes from other users, because `OpenProcess`\n\nrefuses them. The resource bar is a good signal and it is not accounting.\n\nFour agents produce four diffs, and the first version of this app made you read them with `git diff`\n\nin a fifth pane. The diff viewer is deliberately thin:\n\n```\nexecFile('git', ['-C', worktreePath, 'diff', '--no-color', '--unified=3', base])\n```\n\nThen parse the unified diff into files and hunks in about a hundred lines. No library, no server, no LSP.\n\nTwo decisions in there that matter more than the parsing. Files over 10,000 lines get marked oversized and are not rendered, because one regenerated lockfile will otherwise freeze the renderer while it builds a hundred thousand DOM rows. And `base`\n\ndefaults to `HEAD`\n\n, which means what you see is uncommitted work. That is the right default for reviewing an agent that just finished, and it is the wrong one if the agent committed as it went, which several of them now do by default. You can pass another base. Most people do not know that, which is a UI failure rather than an engine one.\n\n`session.json`\n\nholds the pane descriptors. Types, account names, colors, repo paths, layout, tab structure. It is written atomically, serialize first, write to a temp sibling, rename over the real file, because a crash mid-write used to leave truncated JSON that failed to parse and took the whole workspace with it.\n\nWhat it does not hold is any terminal state. Scrollback lives in memory in the main process and dies with the PTY. Reopening the app respawns the panes and re-runs the CLIs. Your grid comes back, your agents restart, and the conversation on screen is gone.\n\nIn practice that hurts less than it should, because the agents keep their own histories and most of them can resume a session. But it is a real limit and the app does not pretend otherwise: the panes are containers for processes, not a saved document.\n\nThe thing the app is worst at is the thing this whole design makes hardest. Every pane is an opaque process, which is why adding a new agent is trivial. It is also why there is no unified log: no single timeline with timestamps and exit codes across panes, telling you which agent touched which file and in what order. There is a per-pane transcript you can export and that is all.\n\nThat gap is the price of not parsing agent output, and I have not found a way to close it that does not involve becoming a wrapper around each CLI. Filesystem watching per worktree is the obvious idea and it tells you what changed without telling you who did it. If you have a better one, that is the comment I want.\n\nNestMux runs on Windows 10 and up, macOS 13 and up, and Linux, same build on all three, local-first with no telemetry. It is at [nestmux.com](https://nestmux.com), free during launch.", "url": "https://wpnews.pro/news/how-we-run-five-coding-agents-side-by-side-in-one-window", "canonical_source": "https://dev.to/eliseomdq/how-we-run-five-coding-agents-side-by-side-in-one-window-32gf", "published_at": "2026-08-23 23:12:04+00:00", "updated_at": "2026-08-23 23:43:40.903818+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["NestMux", "Claude Code", "Codex", "Gemini CLI", "Copilot", "OpenCode"], "alternates": {"html": "https://wpnews.pro/news/how-we-run-five-coding-agents-side-by-side-in-one-window", "markdown": "https://wpnews.pro/news/how-we-run-five-coding-agents-side-by-side-in-one-window.md", "text": "https://wpnews.pro/news/how-we-run-five-coding-agents-side-by-side-in-one-window.txt", "jsonld": "https://wpnews.pro/news/how-we-run-five-coding-agents-side-by-side-in-one-window.jsonld"}}