Skip to content

Commit 465be2e

Browse files
committed
add a shell as well
1 parent 393362d commit 465be2e

File tree

10 files changed

+502
-23
lines changed

10 files changed

+502
-23
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,15 @@
7979
"@tiptap/react": "^3.6.6",
8080
"@tiptap/starter-kit": "^3.6.6",
8181
"@tiptap/suggestion": "^3.6.6",
82+
"@xterm/addon-fit": "^0.10.0",
83+
"@xterm/addon-web-links": "^0.11.0",
84+
"@xterm/xterm": "^5.5.0",
8285
"ai": "^5.0.75",
8386
"axios": "^1.6.7",
8487
"clsx": "^2.1.0",
8588
"cmdk": "^1.1.1",
8689
"date-fns": "^3.3.1",
90+
"node-pty": "1.1.0-beta37",
8791
"radix-themes-tw": "0.2.3",
8892
"react": "^18.2.0",
8993
"react-dom": "^18.2.0",

pnpm-lock.yaml

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { registerFsIpc } from "./services/fs.js";
1414
import { registerOsIpc } from "./services/os.js";
1515
import { registerPosthogIpc } from "./services/posthog.js";
1616
import { registerRecordingIpc } from "./services/recording.js";
17+
import { registerShellIpc } from "./services/shell.js";
1718

1819
const __filename = fileURLToPath(import.meta.url);
1920
const __dirname = path.dirname(__filename);
@@ -182,3 +183,4 @@ registerOsIpc(() => mainWindow);
182183
registerAgentIpc(taskControllers, () => mainWindow);
183184
registerFsIpc();
184185
registerRecordingIpc();
186+
registerShellIpc();

src/main/preload.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,28 @@ contextBridge.exposeInMainWorld("electronAPI", {
162162
getDesktopSources: async (options: { types: ("screen" | "window")[] }) => {
163163
return await ipcRenderer.invoke("desktop-capturer:get-sources", options);
164164
},
165+
// Shell API
166+
shellCreate: (sessionId: string, cwd?: string): Promise<void> =>
167+
ipcRenderer.invoke("shell:create", sessionId, cwd),
168+
shellWrite: (sessionId: string, data: string): Promise<void> =>
169+
ipcRenderer.invoke("shell:write", sessionId, data),
170+
shellResize: (sessionId: string, cols: number, rows: number): Promise<void> =>
171+
ipcRenderer.invoke("shell:resize", sessionId, cols, rows),
172+
shellDestroy: (sessionId: string): Promise<void> =>
173+
ipcRenderer.invoke("shell:destroy", sessionId),
174+
onShellData: (
175+
sessionId: string,
176+
listener: (data: string) => void,
177+
): (() => void) => {
178+
const channel = `shell:data:${sessionId}`;
179+
const wrapped = (_event: IpcRendererEvent, data: string) => listener(data);
180+
ipcRenderer.on(channel, wrapped);
181+
return () => ipcRenderer.removeListener(channel, wrapped);
182+
},
183+
onShellExit: (sessionId: string, listener: () => void): (() => void) => {
184+
const channel = `shell:exit:${sessionId}`;
185+
const wrapped = () => listener();
186+
ipcRenderer.on(channel, wrapped);
187+
return () => ipcRenderer.removeListener(channel, wrapped);
188+
},
165189
});

src/main/services/shell.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import * as os from "node:os";
2+
import { type IpcMainInvokeEvent, ipcMain, type WebContents } from "electron";
3+
import * as pty from "node-pty";
4+
5+
interface ShellSession {
6+
pty: pty.IPty;
7+
webContents: WebContents;
8+
}
9+
10+
const sessions = new Map<string, ShellSession>();
11+
12+
function getDefaultShell(): string {
13+
const platform = os.platform();
14+
15+
if (platform === "win32") {
16+
return process.env.COMSPEC || "cmd.exe";
17+
}
18+
19+
return process.env.SHELL || "/bin/bash";
20+
}
21+
22+
export function registerShellIpc(): void {
23+
// Create new shell session
24+
ipcMain.handle(
25+
"shell:create",
26+
async (
27+
event: IpcMainInvokeEvent,
28+
sessionId: string,
29+
cwd?: string,
30+
): Promise<void> => {
31+
try {
32+
// Clean up existing session if any
33+
const existing = sessions.get(sessionId);
34+
if (existing) {
35+
existing.pty.kill();
36+
sessions.delete(sessionId);
37+
}
38+
39+
const shell = getDefaultShell();
40+
const homeDir = os.homedir();
41+
42+
const ptyProcess = pty.spawn(shell, [], {
43+
name: "xterm-256color",
44+
cols: 80,
45+
rows: 24,
46+
cwd: cwd || homeDir,
47+
env: process.env as Record<string, string>,
48+
});
49+
50+
// Send data to renderer
51+
ptyProcess.onData((data: string) => {
52+
event.sender.send(`shell:data:${sessionId}`, data);
53+
});
54+
55+
// Handle exit
56+
ptyProcess.onExit(() => {
57+
event.sender.send(`shell:exit:${sessionId}`);
58+
sessions.delete(sessionId);
59+
});
60+
61+
sessions.set(sessionId, {
62+
pty: ptyProcess,
63+
webContents: event.sender,
64+
});
65+
66+
console.log(`Created shell session ${sessionId} with shell: ${shell}`);
67+
} catch (error) {
68+
console.error(`Failed to create shell session ${sessionId}:`, error);
69+
throw error;
70+
}
71+
},
72+
);
73+
74+
// Write data to shell
75+
ipcMain.handle(
76+
"shell:write",
77+
async (
78+
_event: IpcMainInvokeEvent,
79+
sessionId: string,
80+
data: string,
81+
): Promise<void> => {
82+
const session = sessions.get(sessionId);
83+
if (!session) {
84+
throw new Error(`Shell session ${sessionId} not found`);
85+
}
86+
87+
session.pty.write(data);
88+
},
89+
);
90+
91+
// Resize shell
92+
ipcMain.handle(
93+
"shell:resize",
94+
async (
95+
_event: IpcMainInvokeEvent,
96+
sessionId: string,
97+
cols: number,
98+
rows: number,
99+
): Promise<void> => {
100+
const session = sessions.get(sessionId);
101+
if (!session) {
102+
throw new Error(`Shell session ${sessionId} not found`);
103+
}
104+
105+
session.pty.resize(cols, rows);
106+
},
107+
);
108+
109+
// Destroy shell session
110+
ipcMain.handle(
111+
"shell:destroy",
112+
async (_event: IpcMainInvokeEvent, sessionId: string): Promise<void> => {
113+
const session = sessions.get(sessionId);
114+
if (!session) {
115+
return; // Already destroyed
116+
}
117+
118+
session.pty.kill();
119+
sessions.delete(sessionId);
120+
console.log(`Destroyed shell session ${sessionId}`);
121+
},
122+
);
123+
}

0 commit comments

Comments
 (0)