@@ -6776,35 +6776,208 @@ function showSshProfileManager(): void {
67766776// === SSH Folder Browsing ===
67776777
67786778async function openSshFolder(): Promise<void> {
6779- // First, refresh hosts so we have an up-to-date list
6780- await refreshSshHosts ( ) ;
6779+ // Show the SSH Connect & Browse modal
6780+ await showSshConnectModal();
6781+ }
6782+
6783+ async function showSshConnectModal(): Promise<void> {
6784+ const modal = document.getElementById('ssh-connect-modal')!;
6785+ const hostsList = document.getElementById('ssh-connect-hosts')!;
6786+ const statusEl = document.getElementById('ssh-connect-status')!;
6787+ const btnGo = document.getElementById('btn-ssh-connect-go') as HTMLButtonElement;
6788+ const btnCancel = document.getElementById('btn-ssh-connect-cancel') as HTMLButtonElement;
6789+ const hostInput = document.getElementById('ssh-connect-host') as HTMLInputElement;
6790+ const portInput = document.getElementById('ssh-connect-port') as HTMLInputElement;
6791+ const userInput = document.getElementById('ssh-connect-username') as HTMLInputElement;
6792+ const identInput = document.getElementById('ssh-connect-identity') as HTMLInputElement;
6793+ const pathInput = document.getElementById('ssh-connect-path') as HTMLInputElement;
6794+
6795+ statusEl.textContent = '';
6796+ statusEl.style.color = '';
6797+
6798+ // Load available hosts
6799+ const [configResult, profilesResult] = await Promise.all([
6800+ window.api.sshParseConfig(),
6801+ window.api.sshListProfiles(),
6802+ ]);
67816803
6782- // Find an active SSH connection for SFTP browsing
6783- let sshConn : LiveConnectionState | undefined ;
6804+ const hosts: Array<{ name: string; host: string; port: number; username: string; identityFile?: string; source: string }> = [];
6805+
6806+ // Add saved profiles
6807+ if (profilesResult.success && profilesResult.profiles) {
6808+ for (const p of profilesResult.profiles) {
6809+ hosts.push({ name: p.name, host: p.host, port: p.port || 22, username: p.username || '', identityFile: p.identityFile, source: 'profile' });
6810+ }
6811+ }
6812+ // Add SSH config hosts (avoid duplicates)
6813+ if (configResult.success && configResult.hosts) {
6814+ for (const h of configResult.hosts) {
6815+ if (!hosts.some(e => e.host === (h.hostName || h.host))) {
6816+ hosts.push({ name: h.host, host: h.hostName || h.host, port: h.port || 22, username: h.user || '', identityFile: h.identityFile, source: 'config' });
6817+ }
6818+ }
6819+ }
6820+
6821+ // Also check for active SSH connections
6822+ let activeConn: LiveConnectionState | undefined;
67846823 for (const conn of state.liveConnections.values()) {
6785- if ( conn . source === 'ssh' && conn . connected ) {
6786- sshConn = conn ;
6787- break ;
6824+ if (conn.source === 'ssh' && conn.connected) { activeConn = conn; break; }
6825+ }
6826+
6827+ // Render hosts list
6828+ if (hosts.length === 0 && !activeConn) {
6829+ hostsList.innerHTML = '<div class="ssh-connect-empty">No saved SSH profiles or config hosts found.<br>Enter connection details below.</div>';
6830+ } else {
6831+ let html = '';
6832+ if (activeConn) {
6833+ html += `<div class="ssh-connect-host-item active-conn" data-active="true">
6834+ <span class="ssh-host-icon">⚡</span>
6835+ <div><div class="ssh-host-name">${escapeHtml(activeConn.displayName)}</div><div class="ssh-host-detail">Active connection</div></div>
6836+ <span class="ssh-host-badge" style="background:#4caf50;color:#fff;">Connected</span>
6837+ </div>`;
6838+ }
6839+ for (const h of hosts) {
6840+ html += `<div class="ssh-connect-host-item" data-host="${escapeHtml(h.host)}" data-port="${h.port}" data-user="${escapeHtml(h.username)}" data-identity="${escapeHtml(h.identityFile || '')}">
6841+ <span class="ssh-host-icon">🖥</span>
6842+ <div><div class="ssh-host-name">${escapeHtml(h.name)}</div><div class="ssh-host-detail">${escapeHtml(h.username || 'user')}@${escapeHtml(h.host)}:${h.port}</div></div>
6843+ <span class="ssh-host-badge">${h.source}</span>
6844+ </div>`;
67886845 }
6846+ hostsList.innerHTML = html;
6847+
6848+ // Click to select and fill form
6849+ hostsList.querySelectorAll('.ssh-connect-host-item').forEach(item => {
6850+ item.addEventListener('click', () => {
6851+ hostsList.querySelectorAll('.ssh-connect-host-item').forEach(i => i.classList.remove('selected'));
6852+ item.classList.add('selected');
6853+ const el = item as HTMLElement;
6854+ if (el.dataset.active) {
6855+ // Active connection — just set path
6856+ hostInput.value = '';
6857+ userInput.value = '';
6858+ statusEl.textContent = 'Will use active SSH connection';
6859+ statusEl.style.color = '#4caf50';
6860+ } else {
6861+ hostInput.value = el.dataset.host || '';
6862+ portInput.value = el.dataset.port || '22';
6863+ userInput.value = el.dataset.user || '';
6864+ identInput.value = el.dataset.identity || '';
6865+ }
6866+ });
6867+ });
67896868 }
6790- if ( ! sshConn ) {
6791- // No active SSH connection — prompt user to connect
6792- // Show the SSH section in the Live panel and auto-focus the host selector
6793- openBottomTab ( 'live' ) ;
6794- // Focus on the SSH connect section
6795- const sshSection = document . querySelector ( '.live-ssh-section' ) as HTMLElement ;
6796- if ( sshSection ) sshSection . scrollIntoView ( { behavior : 'smooth' } ) ;
6797- // Show hint
6798- const statusEl = document . getElementById ( 'live-ssh-status' ) ;
6799- if ( statusEl ) {
6800- statusEl . textContent = 'Connect to an SSH host below, then click the SSH browse button in the Folders panel' ;
6869+
6870+ modal.classList.remove('hidden');
6871+ pathInput.focus();
6872+
6873+ // Handle Connect & Browse
6874+ return new Promise<void>((resolve) => {
6875+ const cleanup = () => {
6876+ modal.classList.add('hidden');
6877+ btnGo.removeEventListener('click', onConnect);
6878+ btnCancel.removeEventListener('click', onCancel);
6879+ resolve();
6880+ };
6881+
6882+ const onCancel = () => cleanup();
6883+
6884+ const onConnect = async () => {
6885+ const selectedActive = hostsList.querySelector('.ssh-connect-host-item.selected.active-conn');
6886+ const remotePath = pathInput.value.trim() || '/var/log';
6887+
6888+ btnGo.disabled = true;
6889+ statusEl.textContent = 'Connecting...';
68016890 statusEl.style.color = '#ffb840';
6802- setTimeout ( ( ) => { statusEl . textContent = '' ; statusEl . style . color = '' ; } , 8000 ) ;
6803- }
6804- return ;
6891+
6892+ try {
6893+ if (selectedActive || activeConn) {
6894+ // Use existing connection
6895+ const result = await window.api.sshListRemoteDir(remotePath);
6896+ if (!result.success) {
6897+ statusEl.textContent = `Failed: ${result.error}`;
6898+ statusEl.style.color = '#ff5050';
6899+ btnGo.disabled = false;
6900+ return;
6901+ }
6902+ const files = result.files || [];
6903+ const connLabel = activeConn?.displayName || 'SSH';
6904+ addSshFolderToPanel(connLabel, remotePath, files);
6905+ cleanup();
6906+ } else {
6907+ // Need to create a new SSH connection via Live panel
6908+ const host = hostInput.value.trim();
6909+ const port = parseInt(portInput.value) || 22;
6910+ const username = userInput.value.trim();
6911+ const identityFile = identInput.value.trim();
6912+
6913+ if (!host) { statusEl.textContent = 'Host is required'; statusEl.style.color = '#ff5050'; btnGo.disabled = false; return; }
6914+
6915+ // Connect via the live SSH mechanism
6916+ const displayName = `${username || 'user'}@${host}`;
6917+ const connResult = await window.api.liveConnect('ssh', {
6918+ host, port, username: username || undefined,
6919+ identityFile: identityFile || undefined,
6920+ }, displayName, `SFTP browse ${host}`);
6921+
6922+ if (!connResult.success) {
6923+ statusEl.textContent = `Connection failed: ${connResult.error}`;
6924+ statusEl.style.color = '#ff5050';
6925+ btnGo.disabled = false;
6926+ return;
6927+ }
6928+
6929+ statusEl.textContent = 'Connected! Browsing...';
6930+ // Small delay for connection to establish
6931+ await new Promise(r => setTimeout(r, 500));
6932+
6933+ const result = await window.api.sshListRemoteDir(remotePath);
6934+ if (!result.success) {
6935+ statusEl.textContent = `Browse failed: ${result.error}`;
6936+ statusEl.style.color = '#ff5050';
6937+ btnGo.disabled = false;
6938+ return;
6939+ }
6940+ const files = result.files || [];
6941+ addSshFolderToPanel(`${username || 'user'}@${host}`, remotePath, files);
6942+ cleanup();
6943+ }
6944+ } catch (err) {
6945+ statusEl.textContent = `Error: ${err}`;
6946+ statusEl.style.color = '#ff5050';
6947+ btnGo.disabled = false;
6948+ }
6949+ };
6950+
6951+ btnGo.addEventListener('click', onConnect);
6952+ btnCancel.addEventListener('click', onCancel);
6953+ modal.querySelector('.modal-close')?.addEventListener('click', onCancel);
6954+ });
6955+ }
6956+
6957+ function addSshFolderToPanel(hostLabel: string, remotePath: string, files: any[]): void {
6958+ const folderName = `${hostLabel}:${remotePath}`;
6959+
6960+ // Reuse existing openSshFolder logic for adding to state
6961+ state.folders.push({
6962+ name: folderName,
6963+ path: remotePath,
6964+ files: mapFolderEntries(files),
6965+ collapsed: false,
6966+ isRemote: true,
6967+ });
6968+ renderFolderTree();
6969+ // Open folders panel if not already open
6970+ if (activePanel !== 'folders') openPanel('folders');
6971+ }
6972+
6973+ async function _openSshFolderLegacy(): Promise<void> {
6974+ // Legacy path — used when there's already an active connection
6975+ let sshConn: LiveConnectionState | undefined;
6976+ for (const conn of state.liveConnections.values()) {
6977+ if (conn.source === 'ssh' && conn.connected) { sshConn = conn; break; }
68056978 }
6979+ if (!sshConn) return;
68066980
6807- // Prompt for remote path
68086981 const remotePath = prompt('Enter remote directory path:', '/var/log');
68096982 if (!remotePath) return;
68106983
0 commit comments