mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 14:44:46 +00:00
2.7 KiB
2.7 KiB
Terminal unmount race cleanup
Prevent Ghostty Terminal/WebSocket leaks when unmounting mid-init
Summary
packages/app/src/components/terminal.tsx initializes Ghostty in onMount via async steps (import("ghostty-web"), Ghostty.load(), WebSocket creation, terminal creation, listeners). If the component unmounts while awaits are pending, onCleanup runs before ws/term exist. The async init can then continue and create resources that never get disposed.
This spec makes initialization abortable and ensures resources created after unmount are immediately cleaned up.
Scoped files (parallel-safe)
packages/app/src/components/terminal.tsx
Goals
- Never leave a WebSocket open after the terminal component unmounts
- Never leave window/container/textarea event listeners attached after unmount
- Avoid creating terminal resources if
disposedis already true
Non-goals
- Reworking terminal buffering/persistence format
- Changing PTY server protocol
Current state
disposedis checked in some WebSocket event handlers, but not during async init.onCleanupcloses/disposes only the resources already assigned at cleanup time.
Proposed approach
- Guard async init steps
- After each
await, checkdisposedand return early.
- Register cleanups as resources are created
- Maintain an array of cleanup callbacks (
cleanups: VoidFunction[]). - When creating
socket,term, adding event listeners, etc., push the corresponding cleanup. - In
onCleanup, run all registered cleanups exactly once.
- Avoid mutating shared vars until safe
- Prefer local variables inside
run()and assign to outerws/termonly after confirming not disposed.
Implementation steps
-
Add
const cleanups: VoidFunction[] = []andconst cleanup = () => { ... }in component scope -
In
onCleanup, setdisposed = trueand callcleanup() -
In
run():
await import(...)-> if disposed returnawait Ghostty.load()-> if disposed return- create WebSocket -> if disposed, close it and return
- create Terminal -> if disposed, dispose + close socket and return
- when adding listeners, register removers in
cleanups
- Ensure
cleanup()is idempotent
Acceptance criteria
- Rapidly mounting/unmounting terminal components does not leave open WebSockets
- No
resizelisteners remain after unmount - No errors are thrown if unmount occurs mid-initialization
Validation plan
- Manual:
- Open a session and rapidly switch sessions/tabs to force terminal unmount/mount
- Verify via devtools that no orphan WebSocket connections remain
- Verify that terminal continues to work normally when kept mounted