diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0edc911344c..3d6f9a48c49 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -25,7 +25,7 @@ import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" import { useArgs } from "./args" -import { batch, onMount } from "solid-js" +import { batch, onCleanup, onMount } from "solid-js" import { Log } from "@/util/log" import type { Path } from "@opencode-ai/sdk" @@ -104,7 +104,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const sdk = useSDK() - sdk.event.listen((e) => { + const unsubscribe = sdk.event.listen((e) => { const event = e.details switch (event.type) { case "server.instance.disposed": @@ -307,6 +307,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } }) + // Clean up event listener on unmount to prevent memory leak + onCleanup(unsubscribe) + const exit = useExit() const args = useArgs() diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index d07e3dfb416..3b90c5c3b0a 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -19,7 +19,25 @@ const opencode = await createOpencode({ }) console.log("โœ… Opencode server ready") -const sessions = new Map() +const sessions = new Map() + +// Session cleanup: remove sessions older than 1 hour +const SESSION_TIMEOUT_MS = 60 * 60 * 1000 +const MAX_SESSIONS = 100 + +function cleanupOldSessions() { + const now = Date.now() + for (const [key, session] of sessions.entries()) { + if (now - session.lastUsed > SESSION_TIMEOUT_MS || sessions.size > MAX_SESSIONS) { + sessions.delete(key) + console.log("๐Ÿงน Cleaned up session:", key) + } + } +} + +// Run cleanup periodically +setInterval(cleanupOldSessions, 5 * 60 * 1000) // Every 5 minutes + ;(async () => { const events = await opencode.client.event.subscribe() for await (const event of events.stream) { @@ -29,6 +47,7 @@ const sessions = new Map { console.log("โœ… Created opencode session:", createResult.data.id) - session = { client, server, sessionId: createResult.data.id, channel, thread } + session = { client, server, sessionId: createResult.data.id, channel, thread, lastUsed: Date.now() } sessions.set(sessionKey, session) const shareResult = await client.session.share({ path: { id: createResult.data.id } }) @@ -102,6 +121,7 @@ app.message(async ({ message, say }) => { } console.log("๐Ÿ“ Sending to opencode:", message.text) + session.lastUsed = Date.now() const result = await session.client.session.prompt({ path: { id: session.sessionId }, body: { parts: [{ type: "text", text: message.text }] }, @@ -143,3 +163,16 @@ app.command("/test", async ({ command, ack, say }) => { await app.start() console.log("โšก๏ธ Slack bot is running!") + +// Graceful shutdown handler +process.on("SIGINT", () => { + console.log("\n๐Ÿ›‘ Shutting down...") + sessions.clear() + process.exit(0) +}) + +process.on("SIGTERM", () => { + console.log("\n๐Ÿ›‘ Shutting down...") + sessions.clear() + process.exit(0) +}) diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index c38ee5847db..2331de4a5a9 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -1,5 +1,5 @@ import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip" -import { children, createSignal, Match, onMount, splitProps, Switch, type JSX } from "solid-js" +import { children, createSignal, Match, onCleanup, onMount, splitProps, Switch, type JSX } from "solid-js" import type { ComponentProps } from "solid-js" export interface TooltipProps extends ComponentProps { @@ -36,17 +36,34 @@ export function Tooltip(props: TooltipProps) { onMount(() => { const childElements = c() + const cleanupFns: (() => void)[] = [] + + const addListeners = (el: HTMLElement) => { + const focusHandler = () => setOpen(true) + const blurHandler = () => setOpen(false) + el.addEventListener("focus", focusHandler) + el.addEventListener("blur", blurHandler) + cleanupFns.push(() => { + el.removeEventListener("focus", focusHandler) + el.removeEventListener("blur", blurHandler) + }) + } + if (childElements instanceof HTMLElement) { - childElements.addEventListener("focus", () => setOpen(true)) - childElements.addEventListener("blur", () => setOpen(false)) + addListeners(childElements) } else if (Array.isArray(childElements)) { for (const child of childElements) { if (child instanceof HTMLElement) { - child.addEventListener("focus", () => setOpen(true)) - child.addEventListener("blur", () => setOpen(false)) + addListeners(child) } } } + + onCleanup(() => { + for (const cleanup of cleanupFns) { + cleanup() + } + }) }) return (