summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-27 14:51:34 -0600
committeradamelmore <[email protected]>2026-01-27 15:25:07 -0600
commit842f17d6d97c52d1efac66a8dca298f6ca692a56 (patch)
treefd45e06f014dc5a7e72e509bb46f4c3ec1fb444c /packages/app/src/components
parent1ebf63c70c552c95794325f40bbd278ba3e0c725 (diff)
downloadopencode-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.tsx8
-rw-r--r--packages/app/src/components/terminal.tsx96
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 (