Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save rahulbansal16/dda503522feca5ec02b59100866fa77a to your computer and use it in GitHub Desktop.

Select an option

Save rahulbansal16/dda503522feca5ec02b59100866fa77a to your computer and use it in GitHub Desktop.
# 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