# PTY Manager Architecture in Munder Difflin: How It Spawns and Manages Agent Processes

> Discover the PTY manager architecture in Munder Difflin. Learn how it spawns and manages agent processes for terminal sessions and routes output efficiently.

- Repository: [Chaitanya Giri/munder-difflin](https://github.com/chaitanyagiri/munder-difflin)
- Tags: architecture
- Published: 2026-08-20

---

**The PTY manager in `munder-difflin` is a centralized process controller that tracks live terminal sessions in a `Map`, spawns cross-platform agent processes through command resolution and Windows shim decoding, and routes all output and exit events to renderer windows via Electron's `WebContents`.**

The `munder-difflin` repository implements a robust **PTY manager architecture** inside [`src/main/pty.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/pty.ts) to orchestrate the terminal processes that power each agent. This component acts as the single source of truth for creating, monitoring, and tearing down pseudo-terminals across macOS, Linux, and Windows. Understanding its design reveals how the application isolates multi-window agent output while guaranteeing correct command resolution and safe process lifecycle handling.

## Core Architecture of the PTY Manager

The PTY manager is built around three structural pillars: session tracking, lifecycle management, and event routing. Each pillar is implemented as concrete methods and data structures inside [`src/main/pty.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/pty.ts).

### Session Tracking with `Map<string, PtySession>`

Every live agent process is stored in a **`Map<string, PtySession>`** that maps a unique session identifier to a session object. According to the source code at lines 31–36, each `PtySession` records:

- The spawned `proc` instance.
- The current working directory and resolved command string.
- The owner `BrowserWindow` reference.
- Timestamps for the last output activity.
- A boolean flag indicating whether any output has been produced yet.

This map enables O(1) lookups for any subsequent operation—write, resize, kill, or redraw—because every public method first resolves the target session by its ID before touching the underlying `node-pty` instance.

### Lifecycle Management Methods

The manager exposes explicit control methods for every stage of a PTY's life: **`spawn`**, **`write`**, **`resize`**, **`redraw`**, **`kill`**, **`killByOwner`**, and **`killAll`**. As implemented in `chaitanyagiri/munder-difflin`, `spawn` handles the heavy lifting of command resolution and process creation, while all other operations simply look up the session in the map and delegate to the corresponding `node-pty` API (lines 124–132).

### Event Routing to the Renderer

Once a PTY is running, its `onData` and `onExit` callbacks forward events to the appropriate Electron renderer via `WebContents.send`. To prevent crashes during application shutdown, the helper **`safeSend`** (lines 55–61) validates that the target window still exists before transmitting any payload. This guarantees that stale callbacks from dying sessions cannot corrupt new sessions or crash the main process.

## How the PTY Manager Spawns Agent Processes

Spawning is the most complex operation in the PTY manager architecture. The `spawn` method (lines 425–437) executes a deterministic pipeline to ensure cross-platform correctness.

### Expanding Tilde and Resolving Commands

First, the manager expands `~` in the requested working directory using **`expandTilde`**. Next, it resolves the command through **`resolveCommand`**, which runs a login-shell `which` on Unix or `where` on Windows and caches successful lookups (lines 90–100). Callers can also preemptively check tool availability through **`isCommandAvailable`** and **`commandPath`**, which expose the same resolution cache.

### Windows Shim Decoding

On Windows, the manager attempts to decode npm-style command shims via **`resolveWindowsShimSpawn`** and **`parseNpmCmdShim`** (lines 484–523). If decoding succeeds, the shim’s interpreter and script are spawned as a raw **argv array**, preserving multiline arguments and avoiding the truncation bugs that occur when routing through `cmd.exe`.

### Fallback Command Line Construction

If shim decoding fails—or on non-Windows platforms—the pipeline falls back to **`buildCmdCommandLine`** (lines 143–150). This helper constructs a properly quoted command line for `cmd.exe`, ensuring that spaces and special characters are handled safely before `node-pty` receives the final executable and arguments.

### Process Registration and Callback Binding

After resolution, the manager calls **`pty.spawn`** with the chosen executable and arguments. It immediately registers `onData` and `onExit` callbacks that:

1. Update the session’s last-output timestamp and ready flag.
2. Forward the payload to the owner window through `safeSend`.
3. Clean up the session entry when the process exits.

## Utility and Helper Methods

Beyond spawning and event routing, the PTY manager provides several utilities that support the broader application:

- **`attachWebContents`** – Sets a default `WebContents` sink for sessions that have no explicit owner window.
- **`countByOwner`** – Returns how many PTYs belong to a specific window, enabling per-floor close confirmations in the UI.
- **`isCommandAvailable` / `commandPath`** – Reuse the internal resolution cache so the renderer or main process can verify CLI tools before attempting a spawn.

These helpers are consumed downstream by [`src/preload/index.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/preload/index.ts), which bridges the PTY API to the renderer, and by [`src/renderer/src/hooks/useHive.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/renderer/src/hooks/useHive.ts) and [`src/renderer/src/components/terminalPool.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/renderer/src/components/terminalPool.ts), which queue writes and render terminal output. Process termination guarantees are reinforced by [`src/main/procKill.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/procKill.ts), which provides **`ensureKilled`** for forcibly cleaning up orphaned child processes.

## Practical Example: Spawning and Controlling a PTY

The following TypeScript example demonstrates the full lifecycle of an agent process under the PTY manager:

```typescript
// 1️⃣ Create a manager and attach the main window
import { PtyManager } from './main/pty';
import { BrowserWindow } from 'electron';

const ptyMgr = new PtyManager();
const mainWin = BrowserWindow.getAllWindows()[0];
ptyMgr.attachWebContents(mainWin.webContents);

// 2️⃣ Spawn an agent process (e.g., the Claude CLI)
const spawnResult = ptyMgr.spawn(
  {
    id: 'pty-1',
    cwd: '~/projects/my-agent',
    command: 'claude',
    args: ['--seed', '"Hello world"'],
    cols: 120,
    rows: 30,
  },
  mainWin.webContents,
);
if (!spawnResult.ok) {
  console.error('Failed to spawn PTY:', spawnResult.error);
}

// 3️⃣ Write data to the PTY (simulate user typing)
ptyMgr.write('pty-1', 'ls -la\n');

// 4️⃣ Resize the terminal when the UI changes
ptyMgr.resize('pty-1', 140, 40);

// 5️⃣ Gracefully kill the PTY (e.g., when the floor window closes)
ptyMgr.kill('pty-1');

```

This pattern is repeated for every agent floor in the application, with each PTY isolated to its owning window.

## Summary

- The **PTY manager architecture** centers on a `Map<string, PtySession>` that tracks every live agent process in [`src/main/pty.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/pty.ts).
- **Spawning** resolves commands through cached shell lookups, decodes Windows npm shims to preserve arguments, and falls back to quoted `cmd.exe` command lines when necessary.
- **Lifecycle methods** such as `write`, `resize`, `kill`, and `killAll` perform O(1) map lookups before delegating to `node-pty`.
- **Event routing** uses `safeSend` to forward `onData` and `onExit` events only to valid renderer windows, preventing shutdown crashes.
- **Utility methods** like `countByOwner`, `isCommandAvailable`, and `attachWebContents` support multi-window isolation and preemptive command validation.

## Frequently Asked Questions

### What data structure does the PTY manager use to track active sessions?

The PTY manager uses a **`Map<string, PtySession>`** defined at lines 31–36 of [`src/main/pty.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/pty.ts). Each entry stores the `node-pty` process instance, working directory, resolved command, owner window, output timestamps, and a readiness flag.

### How does the PTY manager handle Windows npm-style shims?

It invokes **`resolveWindowsShimSpawn`** and **`parseNpmCmdShim`** (lines 484–523) to decode the shim file. When successful, the manager spawns the interpreter and script as a raw argv array, bypassing `cmd.exe` and preserving complex arguments such as multiline strings.

### How are PTY output and exit events forwarded to the UI?

Each session registers `onData` and `onExit` callbacks that call **`safeSend`** (lines 55–61). This helper verifies the target `WebContents` still exists before calling `webContents.send`, ensuring that output is routed to the correct renderer window without crashing during shutdown.

### What utilities does the PTY manager expose for command resolution?

The manager exposes **`isCommandAvailable`** and **`commandPath`**, both backed by the same cache used inside **`resolveCommand`**. These let callers confirm that a CLI tool exists before spawning a PTY, avoiding unnecessary failed sessions.