@@ -905,76 +905,30 @@ export class IpcMain {
905905 }
906906 ) ;
907907
908- ipcMain . handle ( IPC_CHANNELS . WORKSPACE_OPEN_TERMINAL , async ( _event , workspacePath : string ) => {
908+ ipcMain . handle ( IPC_CHANNELS . WORKSPACE_OPEN_TERMINAL , async ( _event , workspaceId : string ) => {
909909 try {
910- if ( process . platform === "darwin" ) {
911- // macOS - try Ghostty first, fallback to Terminal.app
912- const terminal = await this . findAvailableCommand ( [ "ghostty" , "terminal" ] ) ;
913- if ( terminal === "ghostty" ) {
914- // Match main: pass workspacePath to 'open -a Ghostty' to avoid regressions
915- const cmd = "open" ;
916- const args = [ "-a" , "Ghostty" , workspacePath ] ;
917- log . info ( `Opening terminal: ${ cmd } ${ args . join ( " " ) } ` ) ;
918- const child = spawn ( cmd , args , {
919- detached : true ,
920- stdio : "ignore" ,
921- } ) ;
922- child . unref ( ) ;
923- } else {
924- // Terminal.app opens in the directory when passed as argument
925- const cmd = "open" ;
926- const args = [ "-a" , "Terminal" , workspacePath ] ;
927- log . info ( `Opening terminal: ${ cmd } ${ args . join ( " " ) } ` ) ;
928- const child = spawn ( cmd , args , {
929- detached : true ,
930- stdio : "ignore" ,
931- } ) ;
932- child . unref ( ) ;
933- }
934- } else if ( process . platform === "win32" ) {
935- // Windows
936- const cmd = "cmd" ;
937- const args = [ "/c" , "start" , "cmd" , "/K" , "cd" , "/D" , workspacePath ] ;
938- log . info ( `Opening terminal: ${ cmd } ${ args . join ( " " ) } ` ) ;
939- const child = spawn ( cmd , args , {
940- detached : true ,
941- shell : true ,
942- stdio : "ignore" ,
910+ // Look up workspace metadata to get runtime config
911+ const allMetadata = this . config . getAllWorkspaceMetadata ( ) ;
912+ const workspace = allMetadata . find ( ( w ) => w . id === workspaceId ) ;
913+
914+ if ( ! workspace ) {
915+ log . error ( `Workspace not found: ${ workspaceId } ` ) ;
916+ return ;
917+ }
918+
919+ const runtimeConfig = workspace . runtimeConfig ;
920+ const isSSH = runtimeConfig ?. type === "ssh" ;
921+
922+ if ( isSSH && runtimeConfig . type === "ssh" ) {
923+ // SSH workspace - spawn local terminal that SSHs into remote host
924+ await this . openTerminal ( {
925+ type : "ssh" ,
926+ sshConfig : runtimeConfig ,
927+ remotePath : workspace . namedWorkspacePath ,
943928 } ) ;
944- child . unref ( ) ;
945929 } else {
946- // Linux - try terminal emulators in order of preference
947- // x-terminal-emulator is checked first as it respects user's system-wide preference
948- const terminals = [
949- { cmd : "x-terminal-emulator" , args : [ ] , cwd : workspacePath } ,
950- { cmd : "ghostty" , args : [ "--working-directory=" + workspacePath ] } ,
951- { cmd : "alacritty" , args : [ "--working-directory" , workspacePath ] } ,
952- { cmd : "kitty" , args : [ "--directory" , workspacePath ] } ,
953- { cmd : "wezterm" , args : [ "start" , "--cwd" , workspacePath ] } ,
954- { cmd : "gnome-terminal" , args : [ "--working-directory" , workspacePath ] } ,
955- { cmd : "konsole" , args : [ "--workdir" , workspacePath ] } ,
956- { cmd : "xfce4-terminal" , args : [ "--working-directory" , workspacePath ] } ,
957- { cmd : "xterm" , args : [ ] , cwd : workspacePath } ,
958- ] ;
959-
960- const availableTerminal = await this . findAvailableTerminal ( terminals ) ;
961-
962- if ( availableTerminal ) {
963- const cwdInfo = availableTerminal . cwd ? ` (cwd: ${ availableTerminal . cwd } )` : "" ;
964- log . info (
965- `Opening terminal: ${ availableTerminal . cmd } ${ availableTerminal . args . join ( " " ) } ${ cwdInfo } `
966- ) ;
967- const child = spawn ( availableTerminal . cmd , availableTerminal . args , {
968- cwd : availableTerminal . cwd ?? workspacePath ,
969- detached : true ,
970- stdio : "ignore" ,
971- } ) ;
972- child . unref ( ) ;
973- } else {
974- log . error (
975- "No terminal emulator found. Tried: " + terminals . map ( ( t ) => t . cmd ) . join ( ", " )
976- ) ;
977- }
930+ // Local workspace - spawn terminal with cwd set
931+ await this . openTerminal ( { type : "local" , workspacePath : workspace . namedWorkspacePath } ) ;
978932 }
979933 } catch ( error ) {
980934 const message = error instanceof Error ? error . message : String ( error ) ;
@@ -1326,6 +1280,154 @@ export class IpcMain {
13261280 }
13271281 }
13281282
1283+ /**
1284+ * Open a terminal (local or SSH) with platform-specific handling
1285+ */
1286+ private async openTerminal (
1287+ config :
1288+ | { type : "local" ; workspacePath : string }
1289+ | {
1290+ type : "ssh" ;
1291+ sshConfig : Extract < RuntimeConfig , { type : "ssh" } > ;
1292+ remotePath : string ;
1293+ }
1294+ ) : Promise < void > {
1295+ // Build SSH args if needed
1296+ let sshArgs : string [ ] | null = null ;
1297+ if ( config . type === "ssh" ) {
1298+ sshArgs = [ ] ;
1299+ // Add port if specified
1300+ if ( config . sshConfig . port ) {
1301+ sshArgs . push ( "-p" , String ( config . sshConfig . port ) ) ;
1302+ }
1303+ // Add identity file if specified
1304+ if ( config . sshConfig . identityFile ) {
1305+ sshArgs . push ( "-i" , config . sshConfig . identityFile ) ;
1306+ }
1307+ // Force pseudo-terminal allocation
1308+ sshArgs . push ( "-t" ) ;
1309+ // Add host
1310+ sshArgs . push ( config . sshConfig . host ) ;
1311+ // Add remote command to cd into directory and start shell
1312+ // Use single quotes to prevent local shell expansion
1313+ // exec $SHELL replaces the SSH process with the shell, avoiding nested processes
1314+ sshArgs . push ( `cd '${ config . remotePath . replace ( / ' / g, "'\\''" ) } ' && exec $SHELL` ) ;
1315+ }
1316+
1317+ const isSSH = config . type === "ssh" ;
1318+ const logPrefix = isSSH ? "SSH terminal" : "terminal" ;
1319+
1320+ if ( process . platform === "darwin" ) {
1321+ // macOS - try Ghostty first, fallback to Terminal.app
1322+ const terminal = await this . findAvailableCommand ( [ "ghostty" , "terminal" ] ) ;
1323+ if ( terminal === "ghostty" ) {
1324+ const cmd = "open" ;
1325+ let args : string [ ] ;
1326+ if ( isSSH && sshArgs ) {
1327+ // Ghostty: Run ssh command directly
1328+ args = [ "-a" , "Ghostty" , "--args" , "ssh" , ...sshArgs ] ;
1329+ } else {
1330+ // Ghostty: Pass workspacePath to 'open -a Ghostty' to avoid regressions
1331+ if ( config . type !== "local" ) throw new Error ( "Expected local config" ) ;
1332+ args = [ "-a" , "Ghostty" , config . workspacePath ] ;
1333+ }
1334+ log . info ( `Opening ${ logPrefix } : ${ cmd } ${ args . join ( " " ) } ` ) ;
1335+ const child = spawn ( cmd , args , {
1336+ detached : true ,
1337+ stdio : "ignore" ,
1338+ } ) ;
1339+ child . unref ( ) ;
1340+ } else {
1341+ // Terminal.app
1342+ const cmd = isSSH ? "osascript" : "open" ;
1343+ let args : string [ ] ;
1344+ if ( isSSH && sshArgs ) {
1345+ // Terminal.app: Use osascript to run ssh command
1346+ const sshCommand = `ssh ${ sshArgs . map ( ( arg ) => ( arg . includes ( " " ) ? `'${ arg } '` : arg ) ) . join ( " " ) } ` ;
1347+ const script = `tell application "Terminal" to do script "${ sshCommand . replace ( / " / g, '\\"' ) } "` ;
1348+ args = [ "-e" , script ] ;
1349+ } else {
1350+ // Terminal.app opens in the directory when passed as argument
1351+ if ( config . type !== "local" ) throw new Error ( "Expected local config" ) ;
1352+ args = [ "-a" , "Terminal" , config . workspacePath ] ;
1353+ }
1354+ log . info ( `Opening ${ logPrefix } : ${ cmd } ${ args . join ( " " ) } ` ) ;
1355+ const child = spawn ( cmd , args , {
1356+ detached : true ,
1357+ stdio : "ignore" ,
1358+ } ) ;
1359+ child . unref ( ) ;
1360+ }
1361+ } else if ( process . platform === "win32" ) {
1362+ // Windows
1363+ const cmd = "cmd" ;
1364+ let args : string [ ] ;
1365+ if ( isSSH && sshArgs ) {
1366+ // Windows - use cmd to start ssh
1367+ args = [ "/c" , "start" , "cmd" , "/K" , "ssh" , ...sshArgs ] ;
1368+ } else {
1369+ if ( config . type !== "local" ) throw new Error ( "Expected local config" ) ;
1370+ args = [ "/c" , "start" , "cmd" , "/K" , "cd" , "/D" , config . workspacePath ] ;
1371+ }
1372+ log . info ( `Opening ${ logPrefix } : ${ cmd } ${ args . join ( " " ) } ` ) ;
1373+ const child = spawn ( cmd , args , {
1374+ detached : true ,
1375+ shell : true ,
1376+ stdio : "ignore" ,
1377+ } ) ;
1378+ child . unref ( ) ;
1379+ } else {
1380+ // Linux - try terminal emulators in order of preference
1381+ let terminals : Array < { cmd : string ; args : string [ ] ; cwd ?: string } > ;
1382+
1383+ if ( isSSH && sshArgs ) {
1384+ // x-terminal-emulator is checked first as it respects user's system-wide preference
1385+ terminals = [
1386+ { cmd : "x-terminal-emulator" , args : [ "-e" , "ssh" , ...sshArgs ] } ,
1387+ { cmd : "ghostty" , args : [ "ssh" , ...sshArgs ] } ,
1388+ { cmd : "alacritty" , args : [ "-e" , "ssh" , ...sshArgs ] } ,
1389+ { cmd : "kitty" , args : [ "ssh" , ...sshArgs ] } ,
1390+ { cmd : "wezterm" , args : [ "start" , "--" , "ssh" , ...sshArgs ] } ,
1391+ { cmd : "gnome-terminal" , args : [ "--" , "ssh" , ...sshArgs ] } ,
1392+ { cmd : "konsole" , args : [ "-e" , "ssh" , ...sshArgs ] } ,
1393+ { cmd : "xfce4-terminal" , args : [ "-e" , `ssh ${ sshArgs . join ( " " ) } ` ] } ,
1394+ { cmd : "xterm" , args : [ "-e" , "ssh" , ...sshArgs ] } ,
1395+ ] ;
1396+ } else {
1397+ if ( config . type !== "local" ) throw new Error ( "Expected local config" ) ;
1398+ const workspacePath = config . workspacePath ;
1399+ terminals = [
1400+ { cmd : "x-terminal-emulator" , args : [ ] , cwd : workspacePath } ,
1401+ { cmd : "ghostty" , args : [ "--working-directory=" + workspacePath ] } ,
1402+ { cmd : "alacritty" , args : [ "--working-directory" , workspacePath ] } ,
1403+ { cmd : "kitty" , args : [ "--directory" , workspacePath ] } ,
1404+ { cmd : "wezterm" , args : [ "start" , "--cwd" , workspacePath ] } ,
1405+ { cmd : "gnome-terminal" , args : [ "--working-directory" , workspacePath ] } ,
1406+ { cmd : "konsole" , args : [ "--workdir" , workspacePath ] } ,
1407+ { cmd : "xfce4-terminal" , args : [ "--working-directory" , workspacePath ] } ,
1408+ { cmd : "xterm" , args : [ ] , cwd : workspacePath } ,
1409+ ] ;
1410+ }
1411+
1412+ const availableTerminal = await this . findAvailableTerminal ( terminals ) ;
1413+
1414+ if ( availableTerminal ) {
1415+ const cwdInfo = availableTerminal . cwd ? ` (cwd: ${ availableTerminal . cwd } )` : "" ;
1416+ log . info (
1417+ `Opening ${ logPrefix } : ${ availableTerminal . cmd } ${ availableTerminal . args . join ( " " ) } ${ cwdInfo } `
1418+ ) ;
1419+ const child = spawn ( availableTerminal . cmd , availableTerminal . args , {
1420+ cwd : availableTerminal . cwd ,
1421+ detached : true ,
1422+ stdio : "ignore" ,
1423+ } ) ;
1424+ child . unref ( ) ;
1425+ } else {
1426+ log . error ( "No terminal emulator found. Tried: " + terminals . map ( ( t ) => t . cmd ) . join ( ", " ) ) ;
1427+ }
1428+ }
1429+ }
1430+
13291431 /**
13301432 * Find the first available command from a list of commands
13311433 */
0 commit comments