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
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 4 additions & 28 deletions src/browser/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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<typeof callback>[0]);
});
return unsubscribe;
Expand Down Expand Up @@ -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),
Expand Down
26 changes: 16 additions & 10 deletions src/services/ipcMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
);
}

Expand Down