diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index e3b113d..4d8b8a0 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -182,9 +182,20 @@ function connectWebSocket(term, fitAddon, initPayload, onStateChange, onFirstMes ws.addEventListener('open', () => { ws.send(JSON.stringify(initPayload)) const dims = fitAddon.proposeDimensions() - if (dims) { - ws.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols })) - } + // Envoyer resize avec dimensions minimales garanties (24x80) + const rows = dims?.rows || 24 + const cols = dims?.cols || 80 + ws.send(JSON.stringify({ type: 'resize', rows, cols })) + // Forcer un fit après l'ouverture + setTimeout(() => { + try { + fitAddon.fit() + const newDims = fitAddon.proposeDimensions() + if (newDims && newDims.rows > 0 && newDims.cols > 0) { + ws.send(JSON.stringify({ type: 'resize', rows: newDims.rows, cols: newDims.cols })) + } + } catch (e) { console.warn('[Shell] fit failed:', e) } + }, 50) if (onStateChange) onStateChange(true) }) @@ -232,22 +243,33 @@ export default function Shell({ api }) { const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' }) const pendingCommandsRef = useRef({}) - const savedTabs = (() => { + const [tabs, setTabs] = useState(() => { try { const raw = localStorage.getItem(TABS_STORAGE_KEY) if (raw) { const parsed = JSON.parse(raw) - if (Array.isArray(parsed) && parsed.length > 0) { - return parsed.map(t => ({ ...t, connected: false })) + if (Array.isArray(parsed) && parsed.length > 0 && parsed.length <= MAX_TABS) { + return parsed.map((t, i) => ({ + id: t.id || i + 1, + name: t.name || `Tab ${i + 1}`, + type: t.type || 'local', + shell: t.shell || '', + host: t.host, + port: t.port, + user: t.user, + key_path: t.key_path, + connected: false + })) } } - } catch {} - return null - })() - - const [tabs, setTabs] = useState(savedTabs || [ - { id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false }, - ]) + } catch (e) { + console.warn('[Shell] Failed to parse saved tabs:', e) + localStorage.removeItem(TABS_STORAGE_KEY) + } + return [ + { id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false }, + ] + }) const [activeTab, setActiveTab] = useState(() => { if (savedTabs) { return savedTabs[0]?.id || 1 @@ -482,36 +504,60 @@ export default function Shell({ api }) { let cancelled = false const pending = [] + // Forcer le layout à se calculer + const forceLayout = () => { + const el = document.querySelector('.shell-terminal-col') + if (el) { + el.style.height = '' + el.style.minHeight = '' + // Forcer reflow + void el.offsetHeight + } + } + const tryInitTab = (tab, attempt) => { if (cancelled) return - const shellCol = document.querySelector('.shell-terminal-col') - if (!shellCol || shellCol.offsetParent === null) { - pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 200)) + if (attempt > 20) { + console.warn(`[Shell] max attempts reached for tab ${tab.id}`) return } + + forceLayout() + const shellCol = document.querySelector('.shell-terminal-col') + if (!shellCol) { + pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 150)) + return + } + const container = document.getElementById(`terminal-${tab.id}`) if (!container) { pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100)) return } - if (container.offsetHeight === 0) { + + const rect = container.getBoundingClientRect() + if (rect.height < 10 || rect.width < 10) { pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100)) return } + if (!tabsRef.current[tab.id]) { initTerminal(tab.id, tab) } - requestAnimationFrame(() => { - if (cancelled) return - const entry = tabsRef.current[tab.id] - if (entry) { - entry.fitAddon.fit() - setTimeout(() => { if (!cancelled) entry.fitAddon.fit() }, 100) - } + + // Multiple fit attempts avec délais croissants + const fitAttempts = [0, 50, 100, 200, 400] + fitAttempts.forEach(delay => { + setTimeout(() => { + if (cancelled) return + const entry = tabsRef.current[tab.id] + if (entry && entry.fitAddon) { + try { + entry.fitAddon.fit() + } catch (e) { console.warn(`[Shell] fit attempt ${delay}ms failed:`, e) } + } + }, delay) }) - if (!tabsRef.current[tab.id]) { - tryInitTab(tab, 0) - } } const wrapper = document.querySelector('.shell-layout')?.parentElement @@ -650,6 +696,19 @@ export default function Shell({ api }) { } return next }) + + // Redimensionner le nouveau tab actif + setTimeout(() => { + const newActiveTabId = next.length > 0 ? next[next.length - 1].id : null + if (newActiveTabId) { + const entry = tabsRef.current[newActiveTabId] + if (entry && entry.fitAddon) { + try { + entry.fitAddon.fit() + } catch (e) { console.warn('[Shell] fit after close failed:', e) } + } + } + }, 100) } const startRename = (tabId, e) => {