Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ config({ path: resolve(__dirname, "../../../../.env"), override: true });
import path from "node:path";
import { app } from "electron";
import { makeAppSetup } from "lib/electron-app/factories/app/setup";
import { registerDeepLinkIpcs } from "main/lib/deep-link-ipcs";
import { deepLinkManager } from "main/lib/deep-link-manager";
import { registerPortIpcs } from "main/lib/port-ipcs";
import { getPort } from "main/lib/port-manager";
import { MainWindow } from "./windows/main";
import windowManager from "main/lib/window-manager";
import { registerWorkspaceIPCs } from "main/lib/workspace-ipcs";

// Protocol scheme for deep linking
const PROTOCOL_SCHEME = "superset";
Expand All @@ -19,11 +22,9 @@ const PROTOCOL_SCHEME = "superset";
// In development, we need to provide the execPath and args
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(
PROTOCOL_SCHEME,
process.execPath,
[path.resolve(process.argv[1])],
);
app.setAsDefaultProtocolClient(PROTOCOL_SCHEME, process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient(PROTOCOL_SCHEME);
Expand All @@ -46,5 +47,11 @@ app.on("open-url", (event, url) => {
console.log(`Using dev server port: ${port}`);

await app.whenReady();
await makeAppSetup(MainWindow);

// Register IPC handlers once at startup (not per-window)
registerWorkspaceIPCs();
registerPortIpcs();
registerDeepLinkIpcs();

await makeAppSetup(() => windowManager.createWindow());
Comment on lines +51 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Wire up the new window-create IPC channel
You added registerWindowIPCs() but never invoke it here, so any renderer calling ipcRenderer.invoke("window-create") will reject because no ipcMain.handle was registered. Please import the new helper and call it alongside the other startup registrations so the channel is live before windows request it. (electronjs.org)

-import { registerWorkspaceIPCs } from "main/lib/workspace-ipcs";
-import { registerPortIpcs } from "main/lib/port-ipcs";
-import { registerDeepLinkIpcs } from "main/lib/deep-link-ipcs";
+import { registerWorkspaceIPCs } from "main/lib/workspace-ipcs";
+import { registerPortIpcs } from "main/lib/port-ipcs";
+import { registerDeepLinkIpcs } from "main/lib/deep-link-ipcs";
+import { registerWindowIPCs } from "main/lib/window-ipcs";
…
 	// Register IPC handlers once at startup (not per-window)
 	registerWorkspaceIPCs();
 	registerPortIpcs();
 	registerDeepLinkIpcs();
+	registerWindowIPCs();

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/main/index.ts around lines 53 to 58, the new
registerWindowIPCs() helper was added but not invoked at startup, so ipcMain has
no handler for the "window-create" channel and
ipcRenderer.invoke("window-create") will reject; import registerWindowIPCs at
the top of the file (if not already) and call registerWindowIPCs() alongside
registerWorkspaceIPCs(), registerPortIpcs(), and registerDeepLinkIpcs() so the
IPC handler is registered before makeAppSetup creates windows.

})();
18 changes: 18 additions & 0 deletions apps/desktop/src/main/lib/menu.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { app, type BrowserWindow, dialog, Menu } from "electron";
import windowManager from "./window-manager";
import workspaceManager from "./workspace-manager";

export function createApplicationMenu(mainWindow: BrowserWindow) {
const template: Electron.MenuItemConstructorOptions[] = [
{
label: "File",
submenu: [
{
label: "New Window",
accelerator: "CmdOrCtrl+Shift+N",
click: async () => {
try {
await windowManager.createWindow();
} catch (error) {
console.error("[Menu] Failed to create new window:", error);
dialog.showErrorBox(
"Error",
"Failed to create new window: " +
(error instanceof Error ? error.message : "Unknown error"),
);
}
},
},
{ type: "separator" },
{
label: "Open Repository...",
accelerator: "CmdOrCtrl+O",
Expand Down
190 changes: 107 additions & 83 deletions apps/desktop/src/main/lib/terminal-ipcs.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,115 @@
import { type BrowserWindow, ipcMain, shell } from "electron";
import {
BrowserWindow,
type BrowserWindow as BrowserWindowType,
ipcMain,
shell,
} from "electron";
import tmuxManager from "./tmux-manager";

export function registerTerminalIPCs(mainWindow: BrowserWindow) {
// Set main window reference
tmuxManager.setMainWindow(mainWindow);

// Initialize tmux manager (restore sessions)
tmuxManager.initialize().catch((error) => {
console.error("[Terminal IPC] Failed to initialize tmux manager:", error);
});

// Create terminal (or reattach to existing tmux session)
ipcMain.handle(
"terminal-create",
async (
_event,
options: {
id?: string;
cols?: number;
rows?: number;
cwd?: string;
command?: string;
let ipcHandlersRegistered = false;

export function registerTerminalIPCs(window: BrowserWindowType) {
// Initialize tmux manager (restore sessions) only once
if (!ipcHandlersRegistered) {
tmuxManager.initialize().catch((error) => {
console.error("[Terminal IPC] Failed to initialize tmux manager:", error);
});
}

// Register IPC handlers only once globally
if (!ipcHandlersRegistered) {
// Create terminal (or reattach to existing tmux session)
ipcMain.handle(
"terminal-create",
async (
event,
options: {
id?: string;
cols?: number;
rows?: number;
cwd?: string;
command?: string;
},
) => {
// Get the window that sent this request
const senderWindow = BrowserWindow.fromWebContents(event.sender);
const terminalId = await tmuxManager.create(options);

// Register this window to receive output from this terminal
if (senderWindow) {
tmuxManager.registerTerminalWindow(terminalId, senderWindow);
}

return terminalId;
},
);

// Send input to terminal
ipcMain.on(
"terminal-input",
(_event, message: { id: string; data: string }) => {
tmuxManager.write(message.id, message.data);
},
);

// Resize terminal with sequence tracking
ipcMain.on(
"terminal-resize",
(
_event,
message: { id: string; cols: number; rows: number; seq: number },
) => {
tmuxManager.resize(message.id, message.cols, message.rows, message.seq);
},
);

// Send signal to terminal foreground process
ipcMain.on(
"terminal-signal",
(_event, message: { id: string; signal: string }) => {
tmuxManager.signal(message.id, message.signal);
},
) => {
return await tmuxManager.create(options);
},
);

// Send input to terminal
ipcMain.on(
"terminal-input",
(_event, message: { id: string; data: string }) => {
tmuxManager.write(message.id, message.data);
},
);

// Resize terminal with sequence tracking
ipcMain.on(
"terminal-resize",
(
_event,
message: { id: string; cols: number; rows: number; seq: number },
) => {
tmuxManager.resize(message.id, message.cols, message.rows, message.seq);
},
);

// Send signal to terminal foreground process
ipcMain.on(
"terminal-signal",
(_event, message: { id: string; signal: string }) => {
tmuxManager.signal(message.id, message.signal);
},
);

// Detach from terminal (keep tmux session alive)
ipcMain.on("terminal-detach", (_event, id: string) => {
tmuxManager.detach(id);
});

// Execute command in terminal
ipcMain.on(
"terminal-execute-command",
(_event, message: { id: string; command: string }) => {
tmuxManager.executeCommand(message.id, message.command);
},
);

// Kill terminal (destroy tmux session completely)
ipcMain.on("terminal-kill", (_event, id: string) => {
tmuxManager.kill(id);
});

// Get terminal history
ipcMain.handle("terminal-get-history", (_event, id: string) => {
return tmuxManager.getHistory(id);
});

// Open external URLs
ipcMain.handle("open-external", async (_event, url: string) => {
await shell.openExternal(url);
});

// Clean up on app quit
);

// Detach from terminal (keep tmux session alive)
ipcMain.on("terminal-detach", (_event, id: string) => {
tmuxManager.detach(id);
});

// Execute command in terminal
ipcMain.on(
"terminal-execute-command",
(_event, message: { id: string; command: string }) => {
tmuxManager.executeCommand(message.id, message.command);
},
);

// Kill terminal (destroy tmux session completely)
ipcMain.on("terminal-kill", (_event, id: string) => {
tmuxManager.kill(id);
});

// Get terminal history
ipcMain.handle("terminal-get-history", (_event, id: string) => {
return tmuxManager.getHistory(id);
});

// Open external URLs
ipcMain.handle("open-external", async (_event, url: string) => {
await shell.openExternal(url);
});

ipcHandlersRegistered = true;
}

// Clean up when this window closes
const cleanup = () => {
// tmuxManager.killAll();
console.log("[Terminal IPC] Cleaning up window terminal registrations");
tmuxManager.unregisterWindow(window);
};

// Register cleanup on window close
window.on("closed", cleanup);

return cleanup;
}
65 changes: 57 additions & 8 deletions apps/desktop/src/main/lib/tmux-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface ActiveSession {
class TmuxManager {
private static instance: TmuxManager;
private sessions: Map<string, ActiveSession>;
private mainWindow: BrowserWindow | null = null;
private terminalWindows: Map<string, Set<BrowserWindow>> = new Map();
private sessionRegistryPath: string;
// tmux socket name - unique per user
private readonly TMUX_SOCKET = "onlook";
Expand All @@ -52,8 +52,50 @@ class TmuxManager {
return TmuxManager.instance;
}

setMainWindow(window: BrowserWindow | null): void {
this.mainWindow = window;
/**
* Register a window to receive output from a specific terminal
*/
registerTerminalWindow(terminalId: string, window: BrowserWindow): void {
if (!this.terminalWindows.has(terminalId)) {
this.terminalWindows.set(terminalId, new Set());
}
this.terminalWindows.get(terminalId)?.add(window);
console.log(
`[TmuxManager] Registered window for terminal ${terminalId}, now ${this.terminalWindows.get(terminalId)?.size} window(s)`,
);
}

/**
* Unregister a window from receiving output from a specific terminal
*/
unregisterTerminalWindow(terminalId: string, window: BrowserWindow): void {
const windows = this.terminalWindows.get(terminalId);
if (windows) {
windows.delete(window);
console.log(
`[TmuxManager] Unregistered window from terminal ${terminalId}, now ${windows.size} window(s)`,
);
if (windows.size === 0) {
this.terminalWindows.delete(terminalId);
}
}
}

/**
* Unregister a window from all terminals (when window closes)
*/
unregisterWindow(window: BrowserWindow): void {
let count = 0;
for (const [terminalId, windows] of this.terminalWindows.entries()) {
if (windows.has(window)) {
windows.delete(window);
count++;
if (windows.size === 0) {
this.terminalWindows.delete(terminalId);
}
}
}
console.log(`[TmuxManager] Unregistered window from ${count} terminal(s)`);
}

/**
Expand Down Expand Up @@ -596,13 +638,20 @@ class TmuxManager {
}

/**
* Emit terminal data to renderer
* Emit terminal data to all windows viewing this terminal
*/
private emitMessage(sid: string, data: string): void {
this.mainWindow?.webContents.send("terminal-on-data", {
id: sid,
data,
});
const windows = this.terminalWindows.get(sid);
if (windows && windows.size > 0) {
for (const window of windows) {
if (!window.isDestroyed()) {
window.webContents.send("terminal-on-data", {
id: sid,
data,
});
}
}
}
}

/**
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/main/lib/window-ipcs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ipcMain } from "electron";
import windowManager from "./window-manager";

export function registerWindowIPCs() {
ipcMain.handle("window-create", async () => {
try {
await windowManager.createWindow();
return { success: true };
} catch (error) {
console.error("[Window IPC] Failed to create window:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
});
}
Loading
Loading