Skip to content

Commit 1a8dca5

Browse files
Fix WebKit terminal creation issues
- Don't close WebSockets still in CONNECTING state to prevent "closed before established" errors in WebKit/Safari - Add xtermOpened state set synchronously after term.open() to avoid WebKit's delayed requestAnimationFrame during navigation - Track terminal creation locally with weCreatedTerminalRef instead of relying on per-hook-instance newTerminalIds which gets out of sync when multiple useTerminalWS() hooks exist
1 parent d8c5038 commit 1a8dca5

3 files changed

Lines changed: 37 additions & 12 deletions

File tree

src/components/terminal/task-terminal.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ export function TaskTerminal({ taskId, taskName, cwd, className, aiMode, descrip
2525
const termRef = useRef<XTerm | null>(null)
2626
const fitAddonRef = useRef<FitAddon | null>(null)
2727
const createdTerminalRef = useRef(false)
28+
const weCreatedTerminalRef = useRef(false)
2829
const attachedRef = useRef(false)
2930
const [terminalId, setTerminalId] = useState<string | null>(null)
3031
const [xtermReady, setXtermReady] = useState(false)
32+
const [xtermOpened, setXtermOpened] = useState(false)
3133

3234
const { setTerminalFocused } = useKeyboardContext()
3335

@@ -100,7 +102,12 @@ export function TaskTerminal({ taskId, taskName, cwd, className, aiMode, descrip
100102
termRef.current = term
101103
fitAddonRef.current = fitAddon
102104

103-
// Initial fit after container is sized
105+
// Mark xterm as opened synchronously - this gates terminal creation
106+
// We can get cols/rows immediately after open(), no need to wait for rAF
107+
setXtermOpened(true)
108+
109+
// Initial fit after container is sized - this is for visual display only
110+
// WebKit can delay rAF during navigation, so we don't gate creation on this
104111
requestAnimationFrame(() => {
105112
fitAddon.fit()
106113
setXtermReady(true)
@@ -132,6 +139,7 @@ export function TaskTerminal({ taskId, taskName, cwd, className, aiMode, descrip
132139
term.dispose()
133140
termRef.current = null
134141
fitAddonRef.current = null
142+
setXtermOpened(false)
135143
setXtermReady(false)
136144
}
137145
}, [setTerminalFocused])
@@ -200,8 +208,9 @@ export function TaskTerminal({ taskId, taskName, cwd, className, aiMode, descrip
200208

201209
// Find existing terminal or create new one
202210
// Wait for terminalsLoaded to ensure we have accurate knowledge of existing terminals
211+
// Use xtermOpened (not xtermReady) to avoid WebKit rAF timing issues during navigation
203212
useEffect(() => {
204-
if (!connected || !cwd || !xtermReady || !terminalsLoaded) return
213+
if (!connected || !cwd || !xtermOpened || !terminalsLoaded) return
205214

206215
// Look for an existing terminal with matching cwd
207216
const existingTerminal = terminals.find((t) => t.cwd === cwd)
@@ -213,6 +222,7 @@ export function TaskTerminal({ taskId, taskName, cwd, className, aiMode, descrip
213222
// Create terminal only once
214223
if (!createdTerminalRef.current && termRef.current) {
215224
createdTerminalRef.current = true
225+
weCreatedTerminalRef.current = true // Track that THIS component created the terminal
216226
const { cols, rows } = termRef.current
217227
createTerminal({
218228
name: taskName,
@@ -221,7 +231,7 @@ export function TaskTerminal({ taskId, taskName, cwd, className, aiMode, descrip
221231
cwd,
222232
})
223233
}
224-
}, [connected, cwd, xtermReady, terminalsLoaded, terminals, taskName, createTerminal])
234+
}, [connected, cwd, xtermOpened, terminalsLoaded, terminals, taskName, createTerminal])
225235

226236
// Update terminalId when terminal appears in list
227237

@@ -238,11 +248,14 @@ export function TaskTerminal({ taskId, taskName, cwd, className, aiMode, descrip
238248
useEffect(() => {
239249
if (!terminalId || !termRef.current || !containerRef.current || attachedRef.current) return
240250

241-
const isNewTerminal = newTerminalIds.has(terminalId)
242-
// Remove from set immediately so startup commands don't run again on re-mount
243-
if (isNewTerminal) {
244-
newTerminalIds.delete(terminalId)
245-
}
251+
// Use our local ref to determine if we created this terminal
252+
// This is more reliable than newTerminalIds which is per-hook-instance
253+
// and can get out of sync when multiple useTerminalWS() hooks exist
254+
const isNewTerminal = weCreatedTerminalRef.current
255+
// Reset immediately so startup commands don't run again on re-mount
256+
weCreatedTerminalRef.current = false
257+
// Also clean up the hook's newTerminalIds for consistency
258+
newTerminalIds.delete(terminalId)
246259

247260
// Capture current values for use in callbacks
248261
const currentTerminalId = terminalId

src/hooks/use-task-sync.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,14 @@ export function useTaskSync() {
109109
if (reconnectTimeoutRef.current) {
110110
clearTimeout(reconnectTimeoutRef.current)
111111
}
112-
if (wsRef.current) {
113-
wsRef.current.close()
112+
const ws = wsRef.current
113+
if (ws) {
114+
// Don't close WebSocket if it's still connecting - this causes
115+
// "WebSocket is closed before the connection is established" errors in WebKit.
116+
// Let it naturally complete or fail, then it will close on its own.
117+
if (ws.readyState === WebSocket.OPEN) {
118+
ws.close()
119+
}
114120
wsRef.current = null
115121
}
116122
}

src/hooks/use-terminal-ws.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,14 @@ export function useTerminalWS(options: UseTerminalWSOptions = {}): UseTerminalWS
296296
if (reconnectTimeoutRef.current) {
297297
clearTimeout(reconnectTimeoutRef.current)
298298
}
299-
if (wsRef.current) {
300-
wsRef.current.close()
299+
const ws = wsRef.current
300+
if (ws) {
301+
// Don't close WebSocket if it's still connecting - this causes
302+
// "WebSocket is closed before the connection is established" errors in WebKit.
303+
// Let it naturally complete or fail, then it will close on its own.
304+
if (ws.readyState === WebSocket.OPEN) {
305+
ws.close()
306+
}
301307
wsRef.current = null
302308
}
303309
}

0 commit comments

Comments
 (0)