fix(terminal): improve dimension calculation and tab init reliability

- Guarantee minimum 24x80 dimensions on WebSocket open
- Force reflow before init attempts
- Multiple fit attempts with increasing delays (0/50/100/200/400ms)
- Validate saved tabs structure from localStorage
- Resize active tab after closing another tab

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-24 21:30:07 +02:00
parent c8506d4dfc
commit 1885616068

View File

@@ -182,9 +182,20 @@ function connectWebSocket(term, fitAddon, initPayload, onStateChange, onFirstMes
ws.addEventListener('open', () => { ws.addEventListener('open', () => {
ws.send(JSON.stringify(initPayload)) ws.send(JSON.stringify(initPayload))
const dims = fitAddon.proposeDimensions() const dims = fitAddon.proposeDimensions()
if (dims) { // Envoyer resize avec dimensions minimales garanties (24x80)
ws.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols })) 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) 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 settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' })
const pendingCommandsRef = useRef({}) const pendingCommandsRef = useRef({})
const savedTabs = (() => { const [tabs, setTabs] = useState(() => {
try { try {
const raw = localStorage.getItem(TABS_STORAGE_KEY) const raw = localStorage.getItem(TABS_STORAGE_KEY)
if (raw) { if (raw) {
const parsed = JSON.parse(raw) const parsed = JSON.parse(raw)
if (Array.isArray(parsed) && parsed.length > 0) { if (Array.isArray(parsed) && parsed.length > 0 && parsed.length <= MAX_TABS) {
return parsed.map(t => ({ ...t, connected: false })) 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 {} } catch (e) {
return null console.warn('[Shell] Failed to parse saved tabs:', e)
})() localStorage.removeItem(TABS_STORAGE_KEY)
}
const [tabs, setTabs] = useState(savedTabs || [ return [
{ id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false }, { id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
]) ]
})
const [activeTab, setActiveTab] = useState(() => { const [activeTab, setActiveTab] = useState(() => {
if (savedTabs) { if (savedTabs) {
return savedTabs[0]?.id || 1 return savedTabs[0]?.id || 1
@@ -482,36 +504,60 @@ export default function Shell({ api }) {
let cancelled = false let cancelled = false
const pending = [] 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) => { const tryInitTab = (tab, attempt) => {
if (cancelled) return if (cancelled) return
const shellCol = document.querySelector('.shell-terminal-col') if (attempt > 20) {
if (!shellCol || shellCol.offsetParent === null) { console.warn(`[Shell] max attempts reached for tab ${tab.id}`)
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 200))
return 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}`) const container = document.getElementById(`terminal-${tab.id}`)
if (!container) { if (!container) {
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100)) pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
return return
} }
if (container.offsetHeight === 0) {
const rect = container.getBoundingClientRect()
if (rect.height < 10 || rect.width < 10) {
pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100)) pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
return return
} }
if (!tabsRef.current[tab.id]) { if (!tabsRef.current[tab.id]) {
initTerminal(tab.id, tab) initTerminal(tab.id, tab)
} }
requestAnimationFrame(() => {
if (cancelled) return // Multiple fit attempts avec délais croissants
const entry = tabsRef.current[tab.id] const fitAttempts = [0, 50, 100, 200, 400]
if (entry) { fitAttempts.forEach(delay => {
entry.fitAddon.fit() setTimeout(() => {
setTimeout(() => { if (!cancelled) entry.fitAddon.fit() }, 100) 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 const wrapper = document.querySelector('.shell-layout')?.parentElement
@@ -650,6 +696,19 @@ export default function Shell({ api }) {
} }
return next 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) => { const startRename = (tabId, e) => {