diff --git a/Makefile b/Makefile index 44771171d..339f8190b 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ include fmt.mk .PHONY: docs docs-build docs-watch .PHONY: storybook storybook-build test-storybook chromatic .PHONY: benchmark-terminal -.PHONY: ensure-deps +.PHONY: ensure-deps rebuild-native .PHONY: check-eager-imports check-bundle-size check-startup # Build tools @@ -95,6 +95,12 @@ node_modules/.installed: package.json bun.lock # Legacy target for backwards compatibility ensure-deps: node_modules/.installed +# Rebuild native modules for Electron +rebuild-native: node_modules/.installed ## Rebuild native modules (node-pty) for Electron + @echo "Rebuilding native modules for Electron..." + @npx @electron/rebuild -f -m node_modules/node-pty + @echo "Native modules rebuilt successfully" + ## Help help: ## Show this help message @echo 'Usage: make [target]' diff --git a/package.json b/package.json index 9b3b97b2d..0294c8c32 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "access": "public" }, "scripts": { + "postinstall": "npx @electron/rebuild -f -m node_modules/node-pty", "dev": "make dev", "prebuild:main": "./scripts/generate-version.sh", "build": "make build", diff --git a/src/browser/api.ts b/src/browser/api.ts index ee039b360..8db0acc67 100644 --- a/src/browser/api.ts +++ b/src/browser/api.ts @@ -184,22 +184,6 @@ class WebSocketManager { const wsManager = new WebSocketManager(); -// Cache workspace metadata to avoid async lookup during user gestures (popup blocker issue) -let workspaceMetadataCache: Array<{ - id: string; - runtimeConfig?: { type: "local" | "ssh" }; -}> = []; - -// Update cache when workspace metadata changes -function updateWorkspaceCache() { - void invokeIPC(IPC_CHANNELS.WORKSPACE_LIST).then((workspaces) => { - workspaceMetadataCache = workspaces as typeof workspaceMetadataCache; - }); -} - -// Initialize cache -updateWorkspaceCache(); - // Create the Web API implementation const webApi: IPCApi = { tokenizer: { @@ -256,9 +240,7 @@ const webApi: IPCApi = { }, onMetadata: (callback) => { - // Update cache whenever workspace metadata changes const unsubscribe = wsManager.on(IPC_CHANNELS.WORKSPACE_METADATA, (data: unknown) => { - updateWorkspaceCache(); callback(data as Parameters[0]); }); return unsubscribe; @@ -289,18 +271,12 @@ const webApi: IPCApi = { return wsManager.on(channel, callback as (data: unknown) => void); }, openWindow: (workspaceId) => { - // Check workspace runtime type using cached metadata (synchronous) + // In browser mode, always open terminal in a new browser window (for both local and SSH workspaces) // This must be synchronous to avoid popup blocker during user gesture - const workspace = workspaceMetadataCache.find((ws) => ws.id === workspaceId); - const isSSH = workspace?.runtimeConfig?.type === "ssh"; - - if (isSSH) { - // SSH workspace - open browser tab with terminal UI (must be synchronous) - const url = `/terminal.html?workspaceId=${encodeURIComponent(workspaceId)}`; - window.open(url, `terminal-${workspaceId}-${Date.now()}`, "width=1000,height=600"); - } - // For local workspaces, the IPC handler will open a native terminal + const url = `/terminal.html?workspaceId=${encodeURIComponent(workspaceId)}`; + window.open(url, `terminal-${workspaceId}-${Date.now()}`, "width=1000,height=600,popup=yes"); + // Also invoke IPC to let backend know (desktop mode will handle native/ghostty-web routing) return invokeIPC(IPC_CHANNELS.TERMINAL_WINDOW_OPEN, workspaceId); }, closeWindow: (workspaceId) => invokeIPC(IPC_CHANNELS.TERMINAL_WINDOW_CLOSE, workspaceId), diff --git a/src/services/ipcMain.ts b/src/services/ipcMain.ts index f43d36cb9..f72dfdb79 100644 --- a/src/services/ipcMain.ts +++ b/src/services/ipcMain.ts @@ -1575,21 +1575,27 @@ export class IpcMain { } const runtimeConfig = workspace.runtimeConfig; - - // For local workspaces, use native terminal (both desktop and browser mode) - // For SSH workspaces, use ghostty-web (desktop) or browser terminal (browser mode) - if (!isSSHRuntime(runtimeConfig)) { - // Local workspace - use native terminal + const isSSH = isSSHRuntime(runtimeConfig); + const isDesktop = !!this.terminalWindowManager; + + // Terminal routing logic: + // - Desktop + Local: Native terminal + // - Desktop + SSH: Web terminal (ghostty-web Electron window) + // - Browser + Local: Web terminal (browser tab) + // - Browser + SSH: Web terminal (browser tab) + if (isDesktop && !isSSH) { + // Desktop + Local: Native terminal log.info(`Opening native terminal for local workspace: ${workspaceId}`); await this.openTerminal({ type: "local", workspacePath: workspace.namedWorkspacePath }); - } else if (this.terminalWindowManager) { - // SSH workspace in desktop mode - use ghostty-web Electron window + } else if (isDesktop && isSSH) { + // Desktop + SSH: Web terminal (ghostty-web Electron window) log.info(`Opening ghostty-web terminal for SSH workspace: ${workspaceId}`); - await this.terminalWindowManager.openTerminalWindow(workspaceId); + await this.terminalWindowManager!.openTerminalWindow(workspaceId); } else { - // SSH workspace in browser mode - let browser handle it + // Browser mode (local or SSH): Web terminal (browser window) + // Browser will handle opening the terminal window via window.open() log.info( - `Browser mode: terminal UI handled by browser for SSH workspace: ${workspaceId}` + `Browser mode: terminal UI handled by browser for ${isSSH ? "SSH" : "local"} workspace: ${workspaceId}` ); }