perf(app): better memory management

This commit is contained in:
adamelmore
2026-01-27 14:51:34 -06:00
parent 1ebf63c70c
commit 842f17d6d9
18 changed files with 1185 additions and 82 deletions

View File

@@ -1557,13 +1557,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const timeoutMs = 5 * 60 * 1000
const timer = { id: undefined as number | undefined }
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
setTimeout(() => {
timer.id = window.setTimeout(() => {
resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
}, timeoutMs)
})
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout])
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout]).finally(() => {
if (timer.id === undefined) return
clearTimeout(timer.id)
})
pending.delete(session.id)
if (controller.signal.aborted) return false
if (result.status === "failed") throw new Error(result.message)

View File

@@ -67,6 +67,19 @@ export const Terminal = (props: TerminalProps) => {
let handleTextareaFocus: () => void
let handleTextareaBlur: () => void
let disposed = false
const cleanups: VoidFunction[] = []
const cleanup = () => {
if (!cleanups.length) return
const fns = cleanups.splice(0).reverse()
for (const fn of fns) {
try {
fn()
} catch {
// ignore
}
}
}
const getTerminalColors = (): TerminalColors => {
const mode = theme.mode()
@@ -128,7 +141,7 @@ export const Terminal = (props: TerminalProps) => {
if (disposed) return
const mod = loaded.mod
ghostty = loaded.ghostty
const g = loaded.ghostty
const once = { value: false }
@@ -138,6 +151,13 @@ export const Terminal = (props: TerminalProps) => {
url.password = window.__OPENCODE__?.serverPassword
}
const socket = new WebSocket(url)
cleanups.push(() => {
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
})
if (disposed) {
cleanup()
return
}
ws = socket
const t = new mod.Terminal({
@@ -148,8 +168,14 @@ export const Terminal = (props: TerminalProps) => {
allowTransparency: true,
theme: terminalColors(),
scrollback: 10_000,
ghostty,
ghostty: g,
})
cleanups.push(() => t.dispose())
if (disposed) {
cleanup()
return
}
ghostty = g
term = t
const copy = () => {
@@ -201,13 +227,17 @@ export const Terminal = (props: TerminalProps) => {
return false
})
fitAddon = new mod.FitAddon()
serializeAddon = new SerializeAddon()
t.loadAddon(serializeAddon)
t.loadAddon(fitAddon)
const fit = new mod.FitAddon()
const serializer = new SerializeAddon()
cleanups.push(() => (fit as unknown as { dispose?: VoidFunction }).dispose?.())
t.loadAddon(serializer)
t.loadAddon(fit)
fitAddon = fit
serializeAddon = serializer
t.open(container)
container.addEventListener("pointerdown", handlePointerDown)
cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown))
handleTextareaFocus = () => {
t.options.cursorBlink = true
@@ -218,6 +248,8 @@ export const Terminal = (props: TerminalProps) => {
t.textarea?.addEventListener("focus", handleTextareaFocus)
t.textarea?.addEventListener("blur", handleTextareaBlur)
cleanups.push(() => t.textarea?.removeEventListener("focus", handleTextareaFocus))
cleanups.push(() => t.textarea?.removeEventListener("blur", handleTextareaBlur))
focusTerminal()
@@ -233,10 +265,11 @@ export const Terminal = (props: TerminalProps) => {
})
}
fitAddon.observeResize()
handleResize = () => fitAddon.fit()
fit.observeResize()
handleResize = () => fit.fit()
window.addEventListener("resize", handleResize)
t.onResize(async (size) => {
cleanups.push(() => window.removeEventListener("resize", handleResize))
const onResize = t.onResize(async (size) => {
if (socket.readyState === WebSocket.OPEN) {
await sdk.client.pty
.update({
@@ -249,20 +282,24 @@ export const Terminal = (props: TerminalProps) => {
.catch(() => {})
}
})
t.onData((data) => {
cleanups.push(() => (onResize as unknown as { dispose?: VoidFunction }).dispose?.())
const onData = t.onData((data) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(data)
}
})
t.onKey((key) => {
cleanups.push(() => (onData as unknown as { dispose?: VoidFunction }).dispose?.())
const onKey = t.onKey((key) => {
if (key.key == "Enter") {
props.onSubmit?.()
}
})
cleanups.push(() => (onKey as unknown as { dispose?: VoidFunction }).dispose?.())
// t.onScroll((ydisp) => {
// console.log("Scroll position:", ydisp)
// })
socket.addEventListener("open", () => {
const handleOpen = () => {
local.onConnect?.()
sdk.client.pty
.update({
@@ -273,18 +310,27 @@ export const Terminal = (props: TerminalProps) => {
},
})
.catch(() => {})
})
socket.addEventListener("message", (event) => {
}
socket.addEventListener("open", handleOpen)
cleanups.push(() => socket.removeEventListener("open", handleOpen))
const handleMessage = (event: MessageEvent) => {
t.write(event.data)
})
socket.addEventListener("error", (error) => {
}
socket.addEventListener("message", handleMessage)
cleanups.push(() => socket.removeEventListener("message", handleMessage))
const handleError = (error: Event) => {
if (disposed) return
if (once.value) return
once.value = true
console.error("WebSocket error:", error)
local.onConnectError?.(error)
})
socket.addEventListener("close", (event) => {
}
socket.addEventListener("error", handleError)
cleanups.push(() => socket.removeEventListener("error", handleError))
const handleClose = (event: CloseEvent) => {
if (disposed) return
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
// For other codes (network issues, server restart), trigger error handler
@@ -293,7 +339,9 @@ export const Terminal = (props: TerminalProps) => {
once.value = true
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
}
})
}
socket.addEventListener("close", handleClose)
cleanups.push(() => socket.removeEventListener("close", handleClose))
}
void run().catch((err) => {
@@ -309,13 +357,6 @@ export const Terminal = (props: TerminalProps) => {
onCleanup(() => {
disposed = true
if (handleResize) {
window.removeEventListener("resize", handleResize)
}
container.removeEventListener("pointerdown", handlePointerDown)
term?.textarea?.removeEventListener("focus", handleTextareaFocus)
term?.textarea?.removeEventListener("blur", handleTextareaBlur)
const t = term
if (serializeAddon && props.onCleanup && t) {
const buffer = (() => {
@@ -334,8 +375,7 @@ export const Terminal = (props: TerminalProps) => {
})
}
ws?.close()
t?.dispose()
cleanup()
})
return (