fix(shell): improve tab reference stability and command queueing

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 <crush@charm.land>
This commit is contained in:
Augustin
2026-04-24 16:10:54 +02:00
parent a905f22f1a
commit e8924be182

View File

@@ -204,6 +204,10 @@ export default function Shell({ api }) {
const tabsRef = useRef({}) const tabsRef = useRef({})
const nextIdRef = useRef(1) const nextIdRef = useRef(1)
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 activeTabRef = useRef(activeTab)
const pendingCommandsRef = useRef({})
useEffect(() => { activeTabRef.current = activeTab }, [activeTab])
const savedTabs = (() => { const savedTabs = (() => {
try { try {
@@ -408,11 +412,21 @@ export default function Shell({ api }) {
const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000) 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 } tabsRef.current[tabId] = { term, fitAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed }
const origDispose = () => { disposed = true } tabsRef.current[tabId]._markDisposed = () => { disposed = true }
tabsRef.current[tabId]._markDisposed = origDispose
console.log(`[Shell] initTerminal tab=${tabId} done, tabsRef keys:`, Object.keys(tabsRef.current)) 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(() => { useEffect(() => {
@@ -600,24 +614,28 @@ export default function Shell({ api }) {
} }
const sendToTerminal = useCallback((code, tabId) => { const sendToTerminal = useCallback((code, tabId) => {
const targetId = tabId || activeTab const targetId = tabId || activeTabRef.current
const entry = tabsRef.current[targetId] const entry = tabsRef.current[targetId]
if (!entry) { 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 return
} }
if (!entry.ws || entry.ws.readyState !== WebSocket.OPEN) { 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 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' })) entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
}, [activeTab]) }, [])
const focusAiTerminal = useCallback(() => { const focusAiTerminal = useCallback(() => {
const entry = tabsRef.current[activeTab] const entry = tabsRef.current[activeTabRef.current]
if (entry) entry.term.focus() if (entry) entry.term.focus()
}, [activeTab]) }, [])
const _sendAiMessage = useCallback(async (text, fromEvent = false) => { const _sendAiMessage = useCallback(async (text, fromEvent = false) => {
if (!text || !text.trim() || aiLoadingRef.current || aiAtLimit) return if (!text || !text.trim() || aiLoadingRef.current || aiAtLimit) return
@@ -649,7 +667,8 @@ export default function Shell({ api }) {
return 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 }]) setAiMessages(prev => [...prev, { role: 'user', content: trimmed, _tabId: currentTab }])
setAiLoading(true) setAiLoading(true)