diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 97c910a47d..36ecb61eb5 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -3,7 +3,19 @@ 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" @@ -673,67 +685,69 @@ function App() { } }) - sdk.event.on(TuiEvent.CommandExecute.type, (evt) => { - command.trigger(evt.properties.command) - }) + const unsubs = [ + sdk.event.on(TuiEvent.CommandExecute.type, (evt) => { + command.trigger(evt.properties.command) + }), + sdk.event.on(TuiEvent.ToastShow.type, (evt) => { + toast.show({ + title: evt.properties.title, + message: evt.properties.message, + variant: evt.properties.variant, + duration: evt.properties.duration, + }) + }), + sdk.event.on(TuiEvent.SessionSelect.type, (evt) => { + route.navigate({ + type: "session", + sessionID: evt.properties.sessionID, + }) + }), + 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({ + variant: "info", + message: "The current session was deleted", + }) + } + }), + sdk.event.on(SessionApi.Event.Error.type, (evt) => { + const error = evt.properties.error + if (error && typeof error === "object" && error.name === "MessageAbortedError") return + const message = (() => { + if (!error) return "An error occurred" - sdk.event.on(TuiEvent.ToastShow.type, (evt) => { - toast.show({ - title: evt.properties.title, - message: evt.properties.message, - variant: evt.properties.variant, - duration: evt.properties.duration, - }) - }) + if (typeof error === "object") { + const data = error.data + if ("message" in data && typeof data.message === "string") { + return data.message + } + } + return String(error) + })() - sdk.event.on(TuiEvent.SessionSelect.type, (evt) => { - route.navigate({ - type: "session", - sessionID: evt.properties.sessionID, - }) - }) - - 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({ + variant: "error", + message, + duration: 5000, + }) + }), + sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { toast.show({ variant: "info", - message: "The current session was deleted", + title: "Update Available", + message: `OpenCode v${evt.properties.version} is available. Run 'opencode upgrade' to update manually.`, + duration: 10000, }) + }), + ] + onCleanup(() => { + for (const unsub of unsubs) { + unsub() } }) - sdk.event.on(SessionApi.Event.Error.type, (evt) => { - const error = evt.properties.error - if (error && typeof error === "object" && error.name === "MessageAbortedError") return - const message = (() => { - if (!error) return "An error occurred" - - if (typeof error === "object") { - const data = error.data - if ("message" in data && typeof data.message === "string") { - return data.message - } - } - return String(error) - })() - - toast.show({ - variant: "error", - message, - duration: 5000, - }) - }) - - sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { - toast.show({ - variant: "info", - title: "Update Available", - message: `OpenCode v${evt.properties.version} is available. Run 'opencode upgrade' to update manually.`, - duration: 10000, - }) - }) - return ( { + const unsub = sdk.event.on(TuiEvent.PromptAppend.type, (evt) => { if (!input || input.isDestroyed) return input.insertText(evt.properties.text) setTimeout(() => { @@ -107,6 +107,7 @@ export function Prompt(props: PromptProps) { renderer.requestRender() }, 0) }) + onCleanup(unsub) createEffect(() => { if (props.disabled) input.cursorColor = theme.backgroundElement