diff options
| author | adamelmore <[email protected]> | 2026-01-27 14:51:34 -0600 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-27 15:25:07 -0600 |
| commit | 842f17d6d97c52d1efac66a8dca298f6ca692a56 (patch) | |
| tree | fd45e06f014dc5a7e72e509bb46f4c3ec1fb444c /packages/app/src/components | |
| parent | 1ebf63c70c552c95794325f40bbd278ba3e0c725 (diff) | |
| download | opencode-842f17d6d97c52d1efac66a8dca298f6ca692a56.tar.gz opencode-842f17d6d97c52d1efac66a8dca298f6ca692a56.zip | |
perf(app): better memory management
Diffstat (limited to 'packages/app/src/components')
| -rw-r--r-- | packages/app/src/components/prompt-input.tsx | 8 | ||||
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 96 |
2 files changed, 74 insertions, 30 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 1bd7aa4eb..9f038b6e8 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -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) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 022369afe..d38844802 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -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 ( |
