diff --git a/src/frontend/app.js b/src/frontend/app.js index 9e6ce60..911561e 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -1615,15 +1615,16 @@ function renderSettings(container) { // Terminal html += '
'; html += ''; - html += ''; + html += ''; if (Array.isArray(availableTerminals)) { availableTerminals.forEach(function(t) { if (!t.available) return; - var sel = t.id === savedTerminal ? ' selected' : ''; - html += ''; + html += ''; }); } - html += ''; + html += ''; html += '
'; // AI Titles diff --git a/src/terminals.js b/src/terminals.js index 75286b7..4818948 100644 --- a/src/terminals.js +++ b/src/terminals.js @@ -52,20 +52,25 @@ function detectTerminals() { } } catch {} } else if (platform === 'linux') { - const linuxTerms = [ - { id: 'gnome-terminal', name: 'GNOME Terminal', cmd: 'gnome-terminal' }, - { id: 'konsole', name: 'Konsole', cmd: 'konsole' }, - { id: 'kitty', name: 'Kitty', cmd: 'kitty' }, - { id: 'alacritty', name: 'Alacritty', cmd: 'alacritty' }, - { id: 'xterm', name: 'xterm', cmd: 'xterm' }, + // Detect system default terminal (x-terminal-emulator symlink) + try { + const defaultBin = execSync('readlink -f $(which x-terminal-emulator) 2>/dev/null', { encoding: 'utf8' }).trim(); + if (defaultBin) { + terminals.push({ id: defaultBin, name: 'System Default (' + path.basename(defaultBin) + ')', available: true }); + } + } catch {} + // Scan for known terminals as suggestions + const knownBins = [ + 'gnome-terminal', 'konsole', 'kitty', 'alacritty', 'wezterm', + 'terminator', 'tilix', 'xfce4-terminal', 'mate-terminal', 'foot', 'xterm', ]; - for (const t of linuxTerms) { + for (const cmd of knownBins) { try { - execSync(`which ${t.cmd}`, { stdio: 'pipe' }); - terminals.push({ ...t, available: true }); - } catch { - terminals.push({ ...t, available: false }); - } + const fullPath = execSync(`which ${cmd} 2>/dev/null`, { encoding: 'utf8' }).trim(); + if (fullPath && !terminals.find(t => t.id === fullPath)) { + terminals.push({ id: fullPath, name: cmd, available: true }); + } + } catch {} } } else { terminals.push({ id: 'cmd', name: 'Command Prompt', available: true }); @@ -182,23 +187,22 @@ function openInTerminal(sessionId, tool, flags, projectDir, terminalId) { } } } else if (platform === 'linux') { - switch (terminalId) { - case 'kitty': - exec(`kitty bash -c '${fullCmd}; exec bash'`); - break; - case 'alacritty': - exec(`alacritty -e bash -c '${fullCmd}; exec bash'`); - break; - case 'konsole': - exec(`konsole -e bash -c '${fullCmd}; exec bash'`); - break; - case 'xterm': - exec(`xterm -e bash -c '${fullCmd}; exec bash'`); - break; - case 'gnome-terminal': - default: - exec(`gnome-terminal -- bash -c "${fullCmd}; exec bash"`); - break; + // terminalId can be a known name or an arbitrary binary path + const bin = terminalId || 'x-terminal-emulator'; + const baseName = path.basename(bin); + termLog('TERM', `linux: bin=${bin} baseName=${baseName}`); + + // gnome-terminal uses -- instead of -e + if (baseName === 'gnome-terminal' || baseName === 'gnome-terminal-server') { + exec(`${bin} -- bash -c "${fullCmd}; exec bash"`); + } + // wezterm uses "start --" + else if (baseName === 'wezterm' || baseName === 'wezterm-gui') { + exec(`${bin} start -- bash -c '${fullCmd}; exec bash'`); + } + // Generic: most terminals support -e or direct argument + else { + exec(`${bin} -e bash -c '${fullCmd}; exec bash'`); } } else { switch (terminalId) { @@ -357,7 +361,117 @@ function focusTerminalByPid(pid) { } catch {} } - // Linux/other: not much we can do without window manager integration + // Linux: use WINDOWID from /proc for precise window matching, + // terminal-specific APIs for tab-level focus, wmctrl/xdotool as fallback + if (platform === 'linux') { + try { + const pidChain = []; + let walkPid = String(pid); + let windowId = null; + let detectedTerminal = ''; + const knownTerminals = ['gnome-terminal', 'konsole', 'kitty', 'alacritty', 'xterm', + 'terminator', 'tilix', 'xfce4-terminal', 'mate-terminal', 'wezterm', 'foot', 'st', 'urxvt']; + + // Walk parent chain: collect PIDs, find WINDOWID, detect terminal + for (let depth = 0; depth < 20; depth++) { + pidChain.push(walkPid); + + // Read WINDOWID from /proc//environ (set by terminal emulators) + if (!windowId) { + try { + const envData = fs.readFileSync(`/proc/${walkPid}/environ`); + const vars = envData.toString('utf8').split('\0'); + for (const v of vars) { + if (v.startsWith('WINDOWID=')) { + windowId = v.slice(9); + break; + } + } + } catch {} + } + + // Detect terminal name from process command + if (!detectedTerminal) { + try { + const cmd = execSync(`ps -p ${walkPid} -o comm= 2>/dev/null`, { encoding: 'utf8' }).trim(); + for (const name of knownTerminals) { + if (cmd.includes(name)) { detectedTerminal = name; break; } + } + } catch {} + } + + try { + const ppid = execSync(`ps -p ${walkPid} -o ppid= 2>/dev/null`, { encoding: 'utf8' }).trim(); + if (!ppid || ppid === '0' || ppid === '1') break; + walkPid = ppid; + } catch { break; } + } + + termLog('FOCUS', `linux: windowId=${windowId || 'none'} terminal=${detectedTerminal || 'none'} chain=${pidChain.length} pids`); + + // ── Terminal-specific tab focus (works at tab level, not just window) ── + + if (detectedTerminal === 'kitty') { + try { + execSync(`kitty @ focus-window --match pid:${pid}`, { stdio: 'pipe', timeout: 2000 }); + termLog('FOCUS', 'kitty remote control: focused by pid'); + return { ok: true, terminal: 'kitty' }; + } catch { + termLog('FOCUS', 'kitty remote control failed (enable allow_remote_control), falling back'); + } + } + + // ── Focus by WINDOWID (precise — each tab gets its own WINDOWID) ── + + if (windowId && windowId !== '0') { + termLog('FOCUS', `trying WINDOWID=${windowId}`); + + // xdotool accepts decimal WINDOWID directly + try { + execSync(`xdotool windowactivate ${windowId}`, { stdio: 'pipe', timeout: 2000 }); + termLog('FOCUS', `xdotool focused WINDOWID=${windowId}`); + return { ok: true, terminal: detectedTerminal || 'linux' }; + } catch {} + + // wmctrl needs hex format + try { + const hexId = '0x' + parseInt(windowId, 10).toString(16).padStart(8, '0'); + execSync(`wmctrl -ia ${hexId}`, { stdio: 'pipe', timeout: 2000 }); + termLog('FOCUS', `wmctrl focused WINDOWID=${hexId}`); + return { ok: true, terminal: detectedTerminal || 'linux' }; + } catch {} + } + + // ── Fallback: match PID chain against wmctrl window list ── + + try { + const windows = execSync('wmctrl -l -p 2>/dev/null', { encoding: 'utf8' }); + for (const line of windows.split('\n')) { + const parts = line.trim().split(/\s+/); + if (parts.length >= 3 && pidChain.includes(parts[2])) { + execSync(`wmctrl -ia ${parts[0]}`, { stdio: 'pipe' }); + termLog('FOCUS', `wmctrl pid-match focused ${parts[0]}`); + return { ok: true, terminal: detectedTerminal || 'linux' }; + } + } + } catch {} + + // ── Last resort: xdotool search by PID ── + + try { + for (const p of pidChain) { + const wids = execSync(`xdotool search --pid ${p} 2>/dev/null`, { encoding: 'utf8' }).trim(); + if (wids) { + const wid = wids.split('\n')[0]; + execSync(`xdotool windowactivate ${wid}`, { stdio: 'pipe' }); + termLog('FOCUS', `xdotool pid-search focused ${wid}`); + return { ok: true, terminal: detectedTerminal || 'linux' }; + } + } + } catch {} + } catch {} + } + return { ok: false }; }