Created
April 16, 2026 21:21
-
-
Save rahulbansal16/dda503522feca5ec02b59100866fa77a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Build: Conductor-lite for rovodev | |
| Build a minimal Mac desktop app — a stripped-down clone of conductor.build — | |
| that lets me run rovodev coding agents in isolated git worktrees, one per chat. | |
| ## Stack | |
| - **Electron** (latest stable) with **electron-vite** for the build setup. | |
| - **Frontend**: React + TypeScript + Tailwind. | |
| - **Terminal emulator**: xterm.js on the renderer, PTY via **node-pty** in the | |
| main process, bridged over IPC. | |
| - **State**: Zustand (or plain React context — whichever is lighter). | |
| - **Persistence**: a single JSON file at | |
| `~/Library/Application Support/conductor-lite/state.json` for the repo list | |
| and chat list. No database. | |
| - **Security**: `contextIsolation: true`, `nodeIntegration: false`, all | |
| privileged ops go through a typed `preload.ts` that exposes a narrow API. | |
| No auth. No login. No telemetry. No cloud. Everything local. | |
| ## Layout (three panes, left to right) | |
| 1. **Left sidebar (~240px)**: list of added repos. Each repo is collapsible and | |
| shows its chats underneath. Buttons: "+ Add repo" at the top, "+ New chat" | |
| on hover of each repo row. | |
| 2. **Middle pane (flex-grow)**: terminal session for the currently selected | |
| chat. Full xterm.js instance, resizable, with the worktree path shown in | |
| the header. | |
| 3. **Right pane**: skip for MVP. Leave layout room for it. | |
| Top bar shows the selected chat's name, branch, and worktree path. | |
| ## Core behaviors | |
| ### Adding a repo | |
| - "Add repo" opens a native folder picker via `dialog.showOpenDialog`. | |
| - Validate it's a git repo (`.git` exists). If not, show an inline error. | |
| - Store `{ id, name, path }` in state.json. Name = basename of the path. | |
| ### Creating a new chat | |
| - Click "+ New chat" on a repo. Prompt for a chat name | |
| (default: `chat-<timestamp>`). | |
| - Create a git worktree at | |
| `~/Library/Application Support/conductor-lite/worktrees/<repo-id>/<chat-id>/` | |
| on a new branch named `conductor/<chat-id>`. | |
| - Shell out from main process: | |
| `git -C <repo-path> worktree add -b conductor/<chat-id> <worktree-path>`. | |
| Use `child_process.execFile` — never `exec` with a string — to avoid shell | |
| injection on the chat name. | |
| - Persist `{ id, repoId, name, branch, worktreePath, createdAt }`. | |
| ### Terminal session | |
| - When a chat is selected, spawn a PTY in the worktree path via node-pty | |
| running the user's default shell (`process.env.SHELL`, fallback `/bin/zsh`). | |
| - Stream data from PTY → main → renderer over a per-chat IPC channel | |
| (`pty:data:<chatId>`). Forward keystrokes renderer → main → PTY. | |
| - Sessions **stay alive in the main process** while the app is open. | |
| Switching chats keeps every PTY running and just swaps which one xterm | |
| is attached to. This persistence is the whole point of the app. | |
| - Handle resize: xterm's `onResize` → IPC → `pty.resize(cols, rows)`. | |
| - On chat delete or app close, kill PTYs cleanly (`pty.kill()`). | |
| ### Rovodev | |
| - No special integration. The user types `rovodev` (or whatever the CLI is) | |
| in the terminal themselves. The app just provides the isolated worktree | |
| plus a persistent terminal. That's the MVP. | |
| ### Deleting a chat | |
| - Confirm dialog. Then: | |
| 1. Kill the PTY. | |
| 2. `git worktree remove --force <worktree-path>` (execFile). | |
| 3. Optionally delete the branch (ask in the confirm dialog). | |
| 4. Remove from state.json. | |
| ## Preload API shape (contract) | |
| Expose exactly this on `window.api` via `contextBridge`: | |
| ```ts | |
| type Api = { | |
| repos: { | |
| list(): Promise<Repo[]> | |
| add(): Promise<Repo | null> // opens native picker | |
| remove(id: string): Promise<void> | |
| } | |
| chats: { | |
| list(repoId: string): Promise<Chat[]> | |
| create(repoId: string, name: string): Promise<Chat> | |
| delete(id: string, deleteBranch: boolean): Promise<void> | |
| } | |
| pty: { | |
| ensure(chatId: string): Promise<void> // spawn if not running | |
| write(chatId: string, data: string): void | |
| resize(chatId: string, cols: number, rows: number): void | |
| onData(chatId: string, cb: (data: string) => void): () => void // returns unsubscribe | |
| onExit(chatId: string, cb: (code: number) => void): () => void | |
| } | |
| } | |
| ``` | |
| No other `ipcRenderer` access from the renderer. Everything typed. | |
| ## What to skip for the MVP | |
| - No diff viewer. No merge UI. No PR creation. No agent output parsing. | |
| - No multi-repo-per-chat. | |
| - No settings screen. Hardcode sensible defaults. | |
| - No auto-updates, no code signing, no notarization. Unsigned dev build is fine. | |
| ## Deliverables | |
| 1. A working `npm run dev` that launches the app. | |
| 2. `npm run build` produces a `.app` bundle (unsigned is fine). | |
| 3. README with prereqs (Node 20+, Xcode CLI tools for node-pty), run | |
| instructions, and a known-issues section. | |
| 4. One commit per logical chunk so I can review incrementally. | |
| ## Build order (do these one at a time, stop and show me after each) | |
| 1. Scaffold Electron + electron-vite + React + Tailwind. Blank three-pane | |
| layout. Working preload with a hello-world IPC call. | |
| 2. Add-repo flow + sidebar list + state.json persistence. | |
| 3. New-chat flow + git worktree creation. | |
| 4. xterm.js + node-pty bridge for the selected chat. | |
| 5. Background PTYs — switching chats preserves sessions. | |
| 6. Delete chat + worktree cleanup. | |
| ## Gotchas to watch for | |
| - **node-pty native build**: will need `xcode-select --install` and a matching | |
| Electron ABI. Rebuild with `electron-rebuild` after install. | |
| - **Shell injection**: chat names become branch names and paths. Sanitize to | |
| `[a-zA-Z0-9_-]+` before using anywhere in a shell command, even with execFile. | |
| - **Worktree cleanup on crash**: if the app dies mid-create, a half-made | |
| worktree can block future ops. Add a "repair" command that runs | |
| `git worktree prune` when a repo is re-opened. | |
| - **PTY leaks**: make sure `app.on('before-quit')` kills every live PTY. | |
| Ask me before adding any dependency beyond: electron, electron-vite, | |
| electron-builder, react, react-dom, zustand, xterm, xterm-addon-fit, | |
| xterm-addon-web-links, node-pty, tailwindcss. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment