perf(app): better memory management
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user