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)