From e8924be182a9a23182a222aaf5b1450abe639d4c Mon Sep 17 00:00:00 2001 From: Augustin Date: Fri, 24 Apr 2026 16:10:54 +0200 Subject: [PATCH] fix(shell): improve tab reference stability and command queueing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add refs to track activeTab and pending commands outside render cycle. Flush queued commands after terminal initialization completes. Fix sendToTerminal to use stable refs instead of stale state. Enhance debug logging for tab operations. 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush --- web/src/components/Shell.jsx | 41 ++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index 50493b9..5667fba 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -204,6 +204,10 @@ export default function Shell({ api }) { const tabsRef = useRef({}) const nextIdRef = useRef(1) const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'default' }) + const activeTabRef = useRef(activeTab) + const pendingCommandsRef = useRef({}) + + useEffect(() => { activeTabRef.current = activeTab }, [activeTab]) const savedTabs = (() => { try { @@ -408,11 +412,21 @@ export default function Shell({ api }) { const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000) - console.log(`[Shell] initTerminal tab=${tabId} type=${tab.type} container=${!!container}`) + console.log(`[Shell] initTerminal tab=${tabId} type=${tab.type} name="${tab.name}" shell="${tab.shell || '(default)'}"`) tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed } - const origDispose = () => { disposed = true } - tabsRef.current[tabId]._markDisposed = origDispose + tabsRef.current[tabId]._markDisposed = () => { disposed = true } console.log(`[Shell] initTerminal tab=${tabId} done, tabsRef keys:`, Object.keys(tabsRef.current)) + + const pending = pendingCommandsRef.current[tabId] + if (pending && pending.length > 0) { + console.log(`[Shell] Flushing ${pending.length} pending commands for tab ${tabId}`) + for (const cmd of pending) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'input', data: cmd + '\r' })) + } + } + delete pendingCommandsRef.current[tabId] + } }, []) useEffect(() => { @@ -600,24 +614,28 @@ export default function Shell({ api }) { } const sendToTerminal = useCallback((code, tabId) => { - const targetId = tabId || activeTab + const targetId = tabId || activeTabRef.current const entry = tabsRef.current[targetId] if (!entry) { - console.warn(`[Shell] sendToTerminal: tab ${targetId} not in tabsRef. Available:`, Object.keys(tabsRef.current), 'activeTab:', activeTab, 'requested tabId:', tabId) + console.warn(`[Shell] sendToTerminal: tab ${targetId} not ready. Queueing. tabsRef:`, Object.keys(tabsRef.current), 'activeTab:', activeTabRef.current, 'requested:', tabId) + if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = [] + pendingCommandsRef.current[targetId].push(code) return } if (!entry.ws || entry.ws.readyState !== WebSocket.OPEN) { - console.warn(`[Shell] sendToTerminal: WebSocket not ready for tab ${targetId}, state:`, entry.ws?.readyState) + console.warn(`[Shell] sendToTerminal: WS not open for tab ${targetId} (state=${entry.ws?.readyState}). Queueing.`) + if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = [] + pendingCommandsRef.current[targetId].push(code) return } - console.log(`[Shell] sendToTerminal: sending code to tab ${targetId} (${code.length} chars)`) + console.log(`[Shell] sendToTerminal: tab ${targetId} ← ${code.length} chars`) entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' })) - }, [activeTab]) + }, []) const focusAiTerminal = useCallback(() => { - const entry = tabsRef.current[activeTab] + const entry = tabsRef.current[activeTabRef.current] if (entry) entry.term.focus() - }, [activeTab]) + }, []) const _sendAiMessage = useCallback(async (text, fromEvent = false) => { if (!text || !text.trim() || aiLoadingRef.current || aiAtLimit) return @@ -649,7 +667,8 @@ export default function Shell({ api }) { return } - const currentTab = activeTab + const currentTab = activeTabRef.current + console.log(`[Shell] _sendAiMessage: activeTab=${currentTab}, fromEvent=${fromEvent}, text="${trimmed.slice(0, 50)}"`) setAiMessages(prev => [...prev, { role: 'user', content: trimmed, _tabId: currentTab }]) setAiLoading(true)