Compare commits

...

2 Commits

Author SHA1 Message Date
Augustin
1edd4f053a fix(shell): improve tab reference stability and command queueing
All checks were successful
Beta Release / beta (push) Successful in 47s
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>
2026-04-24 16:10:54 +02:00
Augustin
92f943c3e6 fix(shell): add debug logging for tab tracking and WebSocket state
All checks were successful
Beta Release / beta (push) Successful in 46s
Track which tab messages belong to via _tabId field to ensure AI
responses are sent to the correct terminal tab. Add console.log in
initTerminal, sendToTerminal for troubleshooting tab lifecycle issues.

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-24 15:53:13 +02:00

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,9 +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} 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))
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(() => {
@@ -598,23 +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('sendToTerminal: no terminal initialized for tab', targetId) 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('sendToTerminal: WebSocket not ready for tab', targetId) 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: 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
@@ -646,7 +667,9 @@ export default function Shell({ api }) {
return return
} }
setAiMessages(prev => [...prev, { role: 'user', content: trimmed }]) 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) setAiLoading(true)
try { try {
@@ -655,13 +678,13 @@ export default function Shell({ api }) {
accumulated = partial accumulated = partial
setAiMessages(prev => { setAiMessages(prev => {
const filtered = prev.filter(m => !m._streaming) const filtered = prev.filter(m => !m._streaming)
return [...filtered, { role: 'assistant', content: partial, _streaming: true }] return [...filtered, { role: 'assistant', content: partial, _streaming: true, _tabId: currentTab }]
}) })
}) })
setAiMessages(prev => { setAiMessages(prev => {
const filtered = prev.filter(m => !m._streaming) const filtered = prev.filter(m => !m._streaming)
return [...filtered, { role: 'assistant', content: accumulated }] return [...filtered, { role: 'assistant', content: accumulated, _tabId: currentTab }]
}) })
api.getShellChatHistory().then(d => { api.getShellChatHistory().then(d => {
setAiTokens(d.tokens || 0) setAiTokens(d.tokens || 0)
@@ -858,7 +881,7 @@ export default function Shell({ api }) {
</div> </div>
<div className="ai-panel-messages" ref={aiMessagesRef}> <div className="ai-panel-messages" ref={aiMessagesRef}>
{aiMessages.map((msg, i) => ( {aiMessages.map((msg, i) => (
<ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} terminalTabId={activeTab} /> <ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} terminalTabId={msg._tabId || activeTab} />
))} ))}
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>} {aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
</div> </div>