From 731b3fdbcfa919474d8ffa6ce035c781612460f6 Mon Sep 17 00:00:00 2001 From: cloudwaddie-agent Date: Sun, 22 Feb 2026 03:59:13 +0000 Subject: [PATCH] fix: cleanup SDK event listeners to prevent memory leaks This commit fixes memory leaks caused by event listeners not being properly cleaned up when TUI components unmount. Changes: - app.tsx: Added onCleanup for 6 SDK event listeners (CommandExecute, ToastShow, SessionSelect, SessionDeleted, SessionError, UpdateAvailable) - session/index.tsx: Added onCleanup for message.part.updated event listener - prompt/index.tsx: Added onCleanup for PromptAppend event listener These fixes ensure that event subscriptions are properly unsubscribed when components unmount, preventing memory growth over time. --- packages/opencode/src/cli/cmd/tui/app.tsx | 23 +++++++++++++------ .../cli/cmd/tui/component/prompt/index.tsx | 6 ++++- .../src/cli/cmd/tui/routes/session/index.tsx | 7 +++++- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ab3d0968925..3a4f278874e 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -3,7 +3,7 @@ import { Clipboard } from "@tui/util/clipboard" import { Selection } from "@tui/util/selection" import { MouseButton, TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" -import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" +import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on, onCleanup } from "solid-js" import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" import { Installation } from "@/installation" import { Flag } from "@/flag/flag" @@ -668,11 +668,11 @@ function App() { } }) - sdk.event.on(TuiEvent.CommandExecute.type, (evt) => { + const unsubCommandExecute = sdk.event.on(TuiEvent.CommandExecute.type, (evt) => { command.trigger(evt.properties.command) }) - sdk.event.on(TuiEvent.ToastShow.type, (evt) => { + const unsubToastShow = sdk.event.on(TuiEvent.ToastShow.type, (evt) => { toast.show({ title: evt.properties.title, message: evt.properties.message, @@ -681,14 +681,14 @@ function App() { }) }) - sdk.event.on(TuiEvent.SessionSelect.type, (evt) => { + const unsubSessionSelect = sdk.event.on(TuiEvent.SessionSelect.type, (evt) => { route.navigate({ type: "session", sessionID: evt.properties.sessionID, }) }) - sdk.event.on(SessionApi.Event.Deleted.type, (evt) => { + const unsubSessionDeleted = sdk.event.on(SessionApi.Event.Deleted.type, (evt) => { if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) { route.navigate({ type: "home" }) toast.show({ @@ -698,7 +698,7 @@ function App() { } }) - sdk.event.on(SessionApi.Event.Error.type, (evt) => { + const unsubSessionError = sdk.event.on(SessionApi.Event.Error.type, (evt) => { const error = evt.properties.error if (error && typeof error === "object" && error.name === "MessageAbortedError") return const message = (() => { @@ -720,7 +720,7 @@ function App() { }) }) - sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { + const unsubUpdateAvailable = sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { toast.show({ variant: "info", title: "Update Available", @@ -729,6 +729,15 @@ function App() { }) }) + onCleanup(() => { + unsubCommandExecute() + unsubToastShow() + unsubSessionSelect() + unsubSessionDeleted() + unsubSessionError() + unsubUpdateAvailable() + }) + return ( { + const unsubPromptAppend = sdk.event.on(TuiEvent.PromptAppend.type, (evt) => { if (!input || input.isDestroyed) return input.insertText(evt.properties.text) setTimeout(() => { @@ -108,6 +108,10 @@ export function Prompt(props: PromptProps) { }, 0) }) + onCleanup(() => { + unsubPromptAppend() + }) + createEffect(() => { if (props.disabled) input.cursorColor = theme.backgroundElement if (!props.disabled) input.cursorColor = theme.text diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f5a7f6f6ca4..995b2dcf62e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -7,6 +7,7 @@ import { For, Match, on, + onCleanup, Show, Switch, useContext, @@ -204,7 +205,7 @@ export function Session() { }) let lastSwitch: string | undefined = undefined - sdk.event.on("message.part.updated", (evt) => { + const unsubMessagePartUpdated = sdk.event.on("message.part.updated", (evt) => { const part = evt.properties.part if (part.type !== "tool") return if (part.sessionID !== route.sessionID) return @@ -220,6 +221,10 @@ export function Session() { } }) + onCleanup(() => { + unsubMessagePartUpdated() + }) + let scroll: ScrollBoxRenderable let prompt: PromptRef const keybind = useKeybind()