Skip to content

Commit 9c53404

Browse files
Replace exec terminal with a real PTY (node-pty + xterm.js over WebSocket)
The old terminal spawned a fresh `bash -lc` per command, which re-sourced the login shell every time and felt slow and "custom". Now the daemon spawns a genuine persistent PTY (node-pty) and relays it over a `/api/pty` WebSocket, rendered with xterm.js — a real, responsive shell window with full line editing, colors, and interactive programs. - daemon: lazy-loaded node-pty + ws PTY relay (self-heals spawn-helper perms), marked external in tsup so the native addon resolves at runtime - app: xterm.js terminal themed from the app's CSS variables, with the Codex-style tab bar "+" menu (New terminal / Clear) - electron: spawn the daemon under standalone Node (not Electron's Node) so the node-pty prebuilt binary loads Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d3ed57c commit 9c53404

9 files changed

Lines changed: 468 additions & 145 deletions

File tree

packages/app/electron/main.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,41 @@ async function ping(port: number): Promise<DaemonInfo | null> {
4444
}
4545
}
4646

47-
/** Resolve the `coderouter` CLI entry to spawn the daemon. */
48-
function resolveCli(): { cmd: string; args: string[] } {
49-
// Prefer an explicit override, else the locally built CLI, else PATH.
50-
if (process.env.CODEROUTER_CLI) return { cmd: process.execPath, args: [process.env.CODEROUTER_CLI] };
51-
const localCli = join(app.getAppPath(), '..', 'cli', 'dist', 'cli.js');
52-
if (existsSync(localCli)) return { cmd: process.execPath, args: [localCli] };
53-
return { cmd: 'coderouter', args: [] };
47+
/**
48+
* Locate a standalone Node binary. The daemon must run under regular Node
49+
* (not Electron's bundled Node) so native addons like `node-pty` — which
50+
* powers the Studio terminal and ships a prebuilt binary for the standard
51+
* Node ABI — load correctly. Returns null if none can be found.
52+
*/
53+
function findNode(): string | null {
54+
const candidates = [
55+
process.env.CODEROUTER_NODE,
56+
process.env.npm_node_execpath,
57+
'/opt/homebrew/bin/node',
58+
'/usr/local/bin/node',
59+
'/usr/bin/node',
60+
];
61+
for (const c of candidates) {
62+
if (c && existsSync(c) && !/electron/i.test(c)) return c;
63+
}
64+
return null;
65+
}
66+
67+
/** Resolve the command used to spawn the daemon. */
68+
function resolveCli(): { cmd: string; args: string[]; electronNode: boolean } {
69+
const cliPath = process.env.CODEROUTER_CLI || (() => {
70+
const local = join(app.getAppPath(), '..', 'cli', 'dist', 'cli.js');
71+
return existsSync(local) ? local : null;
72+
})();
73+
74+
if (cliPath) {
75+
const node = findNode();
76+
// Prefer standalone Node so node-pty loads; fall back to Electron-as-Node
77+
// (the daemon still works, only the terminal backend would be unavailable).
78+
if (node) return { cmd: node, args: [cliPath], electronNode: false };
79+
return { cmd: process.execPath, args: [cliPath], electronNode: true };
80+
}
81+
return { cmd: 'coderouter', args: [], electronNode: false };
5482
}
5583

5684
async function ensureDaemon(): Promise<DaemonInfo> {
@@ -67,14 +95,13 @@ async function ensureDaemon(): Promise<DaemonInfo> {
6795
if (alive) return alive;
6896
}
6997

70-
const { cmd, args } = resolveCli();
71-
// When spawning via Electron's own binary (process.execPath), it must be
72-
// told to behave as plain Node, otherwise it boots a second app instance.
73-
const runAsNode = cmd === process.execPath;
98+
const { cmd, args, electronNode } = resolveCli();
99+
// When falling back to Electron's own binary, it must be told to behave as
100+
// plain Node, otherwise it boots a second app instance.
74101
const child = spawn(cmd, [...args, 'daemon'], {
75102
detached: true,
76103
stdio: 'ignore',
77-
env: runAsNode ? { ...process.env, ELECTRON_RUN_AS_NODE: '1' } : process.env,
104+
env: electronNode ? { ...process.env, ELECTRON_RUN_AS_NODE: '1' } : process.env,
78105
});
79106
child.unref();
80107

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
},
1818
"dependencies": {
1919
"@coderouter/core": "workspace:*",
20+
"@xterm/addon-fit": "^0.11.0",
21+
"@xterm/xterm": "^6.0.0",
2022
"lucide-react": "^1.21.0",
2123
"react-markdown": "^10.1.0",
2224
"remark-gfm": "^4.0.1"

0 commit comments

Comments
 (0)