diff --git a/bun.lock b/bun.lock index 40099dddd3..2d3f815f5d 100644 --- a/bun.lock +++ b/bun.lock @@ -36,9 +36,10 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/i18n": "2.2.1", "@solid-primitives/media": "2.3.3", - "@solid-primitives/resize-observer": "2.1.3", + "@solid-primitives/resize-observer": "2.1.5", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", "@solid-primitives/timer": "1.4.4", @@ -514,6 +515,7 @@ "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", @@ -572,6 +574,7 @@ "@astrojs/starlight": "0.34.3", "@fontsource/ibm-plex-mono": "5.2.5", "@shikijs/transformers": "3.20.0", + "@solid-primitives/resize-observer": "2.1.5", "@types/luxon": "catalog:", "ai": "catalog:", "astro": "5.7.13", @@ -1934,7 +1937,7 @@ "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="], - "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.5", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.5", "@solid-primitives/rootless": "^1.5.3", "@solid-primitives/static-store": "^0.1.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw=="], "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], @@ -5212,6 +5215,8 @@ "@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], + "@kobalte/core/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -5304,6 +5309,8 @@ "@opencode-ai/desktop-electron/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], @@ -5366,6 +5373,8 @@ "@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@solid-primitives/bounds/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.1", "", {}, "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw=="], "@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], diff --git a/nix/hashes.json b/nix/hashes.json index b9e7bb9db2..23e4a310c9 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-bjfe8/aD0hvUQQEfaNdmKV/Y3dzpf8oz1OUJdgf61WI=", - "aarch64-linux": "sha256-iU9v+ekSCB/qTUG+pOOpSMhPh+0hWnWU5jzDNllEkxU=", - "aarch64-darwin": "sha256-SgNydQLeAjbX0J49f2VKcgKg2Y30pK826R2qQJBMWE4=", - "x86_64-darwin": "sha256-/rzwNuI9x55qi0UcU7QvPUTupErmkt62T09g1omXkQk=" + "x86_64-linux": "sha256-SQVfq41OQdGCgWuWqyqIN6aggL0r3Hzn2hJ9BwPJN+I=", + "aarch64-linux": "sha256-4w/1HhxsTzPFTHNf4JlnKle6Boz1gVTEedWG64T8E/M=", + "aarch64-darwin": "sha256-uMd+pU1u1yqP4OP/9461Tyy3zwwv/llr+rlllLjM98A=", + "x86_64-darwin": "sha256-BhIW3FPqKkM2vGfCrxXUvj5tarey33Q7dxCuaj5A+yU=" } } diff --git a/packages/app/package.json b/packages/app/package.json index 670bec60e1..d179e4a524 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -46,9 +46,10 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/i18n": "2.2.1", "@solid-primitives/media": "2.3.3", - "@solid-primitives/resize-observer": "2.1.3", + "@solid-primitives/resize-observer": "2.1.5", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", "@solid-primitives/timer": "1.4.4", diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index f4b7a1bc0e..11f9f59e4e 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -1,6 +1,7 @@ import { useIsRouting, useLocation } from "@solidjs/router" import { batch, createEffect, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { Tooltip } from "@opencode-ai/ui/tooltip" import { useLanguage } from "@/context/language" @@ -349,13 +350,12 @@ export function DebugBar() { syncHeap() start() - document.addEventListener("visibilitychange", vis) + makeEventListener(document, "visibilitychange", vis) onCleanup(() => { if (one !== 0) cancelAnimationFrame(one) if (two !== 0) cancelAnimationFrame(two) stop() - document.removeEventListener("visibilitychange", vis) for (const ob of obs) ob.disconnect() }) }) diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index fa9930f683..f12a4210c0 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -1,4 +1,5 @@ -import { onCleanup, onMount } from "solid-js" +import { onMount } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import { showToast } from "@opencode-ai/ui/toast" import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" import { useLanguage } from "@/context/language" @@ -181,15 +182,9 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { } onMount(() => { - document.addEventListener("dragover", handleGlobalDragOver) - document.addEventListener("dragleave", handleGlobalDragLeave) - document.addEventListener("drop", handleGlobalDrop) - }) - - onCleanup(() => { - document.removeEventListener("dragover", handleGlobalDragOver) - document.removeEventListener("dragleave", handleGlobalDragLeave) - document.removeEventListener("drop", handleGlobalDrop) + makeEventListener(document, "dragover", handleGlobalDragOver) + makeEventListener(document, "dragleave", handleGlobalDragLeave) + makeEventListener(document, "drop", handleGlobalDrop) }) return { diff --git a/packages/app/src/components/server/server-row.tsx b/packages/app/src/components/server/server-row.tsx index 63a40bac24..d4f68d6306 100644 --- a/packages/app/src/components/server/server-row.tsx +++ b/packages/app/src/components/server/server-row.tsx @@ -1,11 +1,11 @@ import { Tooltip } from "@opencode-ai/ui/tooltip" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { children, createEffect, createMemo, createSignal, type JSXElement, - onCleanup, onMount, type ParentProps, Show, @@ -46,12 +46,9 @@ export function ServerRow(props: ServerRowProps) { }) onMount(() => { - check() if (typeof ResizeObserver !== "function") return - const observer = new ResizeObserver(check) - if (nameRef) observer.observe(nameRef) - if (versionRef) observer.observe(versionRef) - onCleanup(() => observer.disconnect()) + createResizeObserver([nameRef, versionRef], check) + check() }) const tooltipValue = () => ( diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 7e2a48110c..7d2dfaa636 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -1,5 +1,6 @@ import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -250,8 +251,7 @@ function useKeyCapture(input: { input.stop() } - document.addEventListener("keydown", handle, true) - onCleanup(() => document.removeEventListener("keydown", handle, true)) + makeEventListener(document, "keydown", handle, { capture: true }) }) } diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 65805f40c8..d2238828c6 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -2,6 +2,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { dict as en } from "@/i18n/en" @@ -378,11 +379,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex } onMount(() => { - document.addEventListener("keydown", handleKeyDown) - }) - - onCleanup(() => { - document.removeEventListener("keydown", handleKeyDown) + makeEventListener(document, "keydown", handleKeyDown) }) function register(cb: () => CommandOption[]): void diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index d240f9eeff..1205a8fa82 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -1,7 +1,8 @@ import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { batch, onCleanup } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import { batch, onCleanup, onMount } from "solid-js" import z from "zod" import { createSdkForServer } from "@/utils/server" import { useLanguage } from "./language" @@ -206,21 +207,16 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo clearHeartbeat() } - const onVisibility = () => { - if (typeof document === "undefined") return - if (document.visibilityState !== "visible") return - if (!started) return - if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return - attempt?.abort() - } - if (typeof document !== "undefined") { - document.addEventListener("visibilitychange", onVisibility) - } + onMount(() => { + makeEventListener(document, "visibilitychange", () => { + if (document.visibilityState !== "visible") return + if (!started) return + if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return + attempt?.abort() + }) + }) onCleanup(() => { - if (typeof document !== "undefined") { - document.removeEventListener("visibilitychange", onVisibility) - } stop() abort.abort() flush() diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index aafa4fb66c..bab3d39f38 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -1,6 +1,7 @@ import { createStore, produce } from "solid-js/store" import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" +import { makeEventListener } from "@solid-primitives/event-listener" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" import { useServer } from "./server" @@ -366,12 +367,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( flush() } - window.addEventListener("pagehide", flush) - document.addEventListener("visibilitychange", handleVisibility) + makeEventListener(window, "pagehide", flush) + makeEventListener(document, "visibilitychange", handleVisibility) onCleanup(() => { - window.removeEventListener("pagehide", flush) - document.removeEventListener("visibilitychange", handleVisibility) scroll.dispose() }) }) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b5a96110f6..79b9abd332 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -12,6 +12,7 @@ import { untrack, type Accessor, } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import { useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" @@ -215,18 +216,11 @@ export default function Layout(props: ParentProps) { if (document.visibilityState !== "hidden") return reset() } - window.addEventListener("pointerup", stop) - window.addEventListener("pointercancel", stop) - window.addEventListener("blur", stop) - window.addEventListener("blur", blur) - document.addEventListener("visibilitychange", hide) - onCleanup(() => { - window.removeEventListener("pointerup", stop) - window.removeEventListener("pointercancel", stop) - window.removeEventListener("blur", stop) - window.removeEventListener("blur", blur) - document.removeEventListener("visibilitychange", hide) - }) + makeEventListener(window, "pointerup", stop) + makeEventListener(window, "pointercancel", stop) + makeEventListener(window, "blur", stop) + makeEventListener(window, "blur", blur) + makeEventListener(document, "visibilitychange", hide) }) const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) @@ -1394,8 +1388,7 @@ export default function Layout(props: ParentProps) { } handleDeepLinks(drainPendingDeepLinks(window)) - window.addEventListener(deepLinkEvent, handler as EventListener) - onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener)) + makeEventListener(window, deepLinkEvent, handler as EventListener) }) async function renameProject(project: LocalProject, next: string) { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 18bae6e2d0..98d06fda74 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -14,6 +14,7 @@ import { onMount, untrack, } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { useLocal } from "@/context/local" @@ -329,10 +330,9 @@ export default function Page() { const { params, sessionKey, tabs, view } = useSessionLayout() createEffect(() => { - if (!untrack(() => prompt.ready())) return - prompt.ready() + if (!prompt.ready()) return untrack(() => { - if (params.id || !prompt.ready()) return + if (params.id) return const text = searchParams.prompt if (!text) return prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) @@ -1688,11 +1688,10 @@ export default function Page() { ) onMount(() => { - document.addEventListener("keydown", handleKeyDown) + makeEventListener(document, "keydown", handleKeyDown) }) onCleanup(() => { - document.removeEventListener("keydown", handleKeyDown) if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index a5263cd743..372adef96a 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -13,6 +13,7 @@ import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import type { SessionComposerState } from "@/pages/session/composer/session-composer-state" import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" import type { FollowupDraft } from "@/components/prompt-input/submit" +import { createResizeObserver } from "@solid-primitives/resize-observer" export function SessionComposerRegion(props: { state: SessionComposerState @@ -115,13 +116,9 @@ export function SessionComposerRegion(props: { createEffect(() => { const el = store.body if (!el) return - const update = () => { - setStore("height", el.getBoundingClientRect().height) - } + const update = () => setStore("height", el.getBoundingClientRect().height) + createResizeObserver(store.body, update) update() - const observer = new ResizeObserver(update) - observer.observe(el) - onCleanup(() => observer.disconnect()) }) return ( diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index 0884f4cc60..eab2108687 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -1,5 +1,6 @@ import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" import { showToast } from "@opencode-ai/ui/toast" @@ -86,8 +87,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() => pull() } - window.addEventListener(composerEvent, onEvent) - onCleanup(() => window.removeEventListener(composerEvent, onEvent)) + makeEventListener(window, composerEvent, onEvent) }) const todos = createMemo((): Todo[] => { diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 38974b2465..35690030c9 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -8,6 +8,8 @@ import { showToast } from "@opencode-ai/ui/toast" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" +import { makeEventListener } from "@solid-primitives/event-listener" +import { createResizeObserver } from "@solid-primitives/resize-observer" const cache = new Map() @@ -172,17 +174,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } update() - window.addEventListener("resize", update) + + makeEventListener(window, "resize", update) const dock = root?.closest('[data-component="session-prompt-dock"]') const scroller = document.querySelector(".scroll-view__viewport") - const observer = new ResizeObserver(update) - if (dock instanceof HTMLElement) observer.observe(dock) - if (scroller instanceof HTMLElement) observer.observe(scroller) + createResizeObserver([dock, scroller], update) onCleanup(() => { - window.removeEventListener("resize", update) - observer.disconnect() if (raf !== undefined) cancelAnimationFrame(raf) }) diff --git a/packages/app/src/pages/session/composer/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx index c16ac83993..7928bcc9ca 100644 --- a/packages/app/src/pages/session/composer/session-todo-dock.tsx +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -6,6 +6,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { useSpring } from "@opencode-ai/ui/motion-spring" import { TextReveal } from "@opencode-ai/ui/text-reveal" import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { Index, createEffect, createMemo, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { composerEnabled, composerProbe } from "@/testing/session-composer" @@ -91,9 +92,7 @@ export function SessionTodoDock(props: { setStore("height", el.getBoundingClientRect().height) } update() - const observer = new ResizeObserver(update) - observer.observe(el) - onCleanup(() => observer.disconnect()) + createResizeObserver(el, update) }) createEffect(() => { diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 9430b70253..cb76175236 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -1,6 +1,7 @@ -import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js" +import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" +import { makeEventListener } from "@solid-primitives/event-listener" import type { FileSearchHandle } from "@opencode-ai/ui/file" import { useFileComponent } from "@opencode-ai/ui/context/file" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" @@ -59,7 +60,7 @@ function createScrollSync(input: { tab: () => string; view: ReturnType([]) const getCode = () => { const el = scroll @@ -106,17 +107,9 @@ function createScrollSync(input: { tab: () => string; view: ReturnType { const next = getCode() - if (next.length === code.length && next.every((el, i) => el === code[i])) return - - for (const item of code) { - item.removeEventListener("scroll", onCodeScroll) - } - - code = next - - for (const item of code) { - item.addEventListener("scroll", onCodeScroll) - } + const current = code() + if (next.length === current.length && next.every((el, i) => el === current[i])) return + setCode(next) } const restore = () => { @@ -128,14 +121,14 @@ function createScrollSync(input: { tab: () => string; view: ReturnType 0) { - for (const item of code) { + if (code().length > 0) { + for (const item of code()) { if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x } } if (el.scrollTop !== pos.y) el.scrollTop = pos.y - if (code.length > 0) return + if (code().length > 0) return if (el.scrollLeft !== pos.x) el.scrollLeft = pos.x } @@ -149,24 +142,24 @@ function createScrollSync(input: { tab: () => string; view: ReturnType { - if (code.length === 0) sync() + if (code().length === 0) sync() save({ - x: code[0]?.scrollLeft ?? event.currentTarget.scrollLeft, + x: code()[0]?.scrollLeft ?? event.currentTarget.scrollLeft, y: event.currentTarget.scrollTop, }) } + createEffect(() => { + for (const item of code()) makeEventListener(item, "scroll", onCodeScroll) + }) + const setViewport = (el: HTMLDivElement) => { scroll = el restore() } onCleanup(() => { - for (const item of code) { - item.removeEventListener("scroll", onCodeScroll) - } - if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame) if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) }) @@ -358,8 +351,7 @@ export function FileTabContent(props: { tab: string }) { find?.focus() } - window.addEventListener("keydown", onKeyDown, { capture: true }) - onCleanup(() => window.removeEventListener("keydown", onKeyDown, { capture: true })) + makeEventListener(window, "keydown", onKeyDown, { capture: true }) }) createEffect( diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index 7e2c1ccf7b..f3215f6850 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -1,5 +1,6 @@ import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { same } from "@/utils/same" const emptyTabs: string[] = [] @@ -171,14 +172,9 @@ export const createSizing = () => { } onMount(() => { - window.addEventListener("pointerup", stop) - window.addEventListener("pointercancel", stop) - window.addEventListener("blur", stop) - onCleanup(() => { - window.removeEventListener("pointerup", stop) - window.removeEventListener("pointercancel", stop) - window.removeEventListener("blur", stop) - }) + makeEventListener(window, "pointerup", stop) + makeEventListener(window, "pointercancel", stop) + makeEventListener(window, "blur", stop) }) onCleanup(() => { diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 76b65a2210..b681286450 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,4 +1,5 @@ -import { createEffect, onCleanup, type JSX } from "solid-js" +import { createEffect, createSignal, onCleanup, type JSX } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" import type { FileDiff } from "@opencode-ai/sdk/v2" import { SessionReview } from "@opencode-ai/ui/session-review" import type { @@ -123,13 +124,6 @@ export function SessionReviewTab(props: SessionReviewTabProps) { onCleanup(() => { if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) - if (scroll) { - scroll.removeEventListener("wheel", handleInteraction, { capture: true }) - scroll.removeEventListener("mousewheel", handleInteraction, { capture: true }) - scroll.removeEventListener("pointerdown", handleInteraction, { capture: true }) - scroll.removeEventListener("touchstart", handleInteraction, { capture: true }) - scroll.removeEventListener("keydown", handleInteraction, { capture: true }) - } }) return ( @@ -138,11 +132,11 @@ export function SessionReviewTab(props: SessionReviewTabProps) { empty={props.empty} scrollRef={(el) => { scroll = el - el.addEventListener("wheel", handleInteraction, { passive: true, capture: true }) - el.addEventListener("mousewheel", handleInteraction, { passive: true, capture: true }) - el.addEventListener("pointerdown", handleInteraction, { passive: true, capture: true }) - el.addEventListener("touchstart", handleInteraction, { passive: true, capture: true }) - el.addEventListener("keydown", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "wheel", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "mousewheel", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "pointerdown", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "touchstart", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "keydown", handleInteraction, { capture: true }) props.onScrollRef?.(el) queueRestore() }} diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index c663d7d671..1161d565a7 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -1,5 +1,6 @@ import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { Tabs } from "@opencode-ai/ui/tabs" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -50,12 +51,8 @@ export function TerminalPanel() { const port = window.visualViewport sync() - window.addEventListener("resize", sync) - port?.addEventListener("resize", sync) - onCleanup(() => { - window.removeEventListener("resize", sync) - port?.removeEventListener("resize", sync) - }) + makeEventListener(window, "resize", sync) + if (port) makeEventListener(port, "resize", sync) }) createEffect(() => { diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 393396c36a..8896df1c8b 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -363,6 +363,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}", "zen.api.error.modelDisabled": "النموذج معطل", + "zen.api.error.trialEnded": + "انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | الوصول إلى أفضل نماذج البرمجة في العالم", "black.meta.description": "احصل على وصول إلى Claude، GPT، Gemini والمزيد مع خطط اشتراك OpenCode Black.", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 764434264f..0259b5f884 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -371,6 +371,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}", "zen.api.error.modelDisabled": "O modelo está desabilitado", + "zen.api.error.trialEnded": + "A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Acesse os melhores modelos de codificação do mundo", "black.meta.description": "Tenha acesso ao Claude, GPT, Gemini e mais com os planos de assinatura OpenCode Black.", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 3f9d9deace..5a376cb5df 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -368,6 +368,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktiveret", + "zen.api.error.trialEnded": + "Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Få adgang til verdens bedste kodningsmodeller", "black.meta.description": "Få adgang til Claude, GPT, Gemini og mere med OpenCode Black-abonnementer.", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 535bafe514..d6e8533921 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -371,6 +371,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}", "zen.api.error.modelDisabled": "Modell ist deaktiviert", + "zen.api.error.trialEnded": + "Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}", "black.meta.title": "OpenCode Black | Zugriff auf die weltweit besten Coding-Modelle", "black.meta.description": "Erhalte Zugriff auf Claude, GPT, Gemini und mehr mit OpenCode Black Abos.", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index c68711dd25..49339a3f52 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -364,6 +364,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}", "zen.api.error.modelDisabled": "Model is disabled", + "zen.api.error.trialEnded": + "Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Access all the world's best coding models", "black.meta.description": "Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans.", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index d56099d7a6..632a29a1d2 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -371,6 +371,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}", "zen.api.error.modelDisabled": "El modelo está deshabilitado", + "zen.api.error.trialEnded": + "La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Accede a los mejores modelos de codificación del mundo", "black.meta.description": "Obtén acceso a Claude, GPT, Gemini y más con los planes de suscripción de OpenCode Black.", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index ad1d377cc0..f657c6164f 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -372,6 +372,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}", "zen.api.error.modelDisabled": "Le modèle est désactivé", + "zen.api.error.trialEnded": + "La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Accédez aux meilleurs modèles de code au monde", "black.meta.description": "Accédez à Claude, GPT, Gemini et plus avec les forfaits d'abonnement OpenCode Black.", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 0b6b6b6cfa..c3c73b2835 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -367,6 +367,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}", "zen.api.error.modelDisabled": "Il modello è disabilitato", + "zen.api.error.trialEnded": + "La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Accedi ai migliori modelli di coding al mondo", "black.meta.description": diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 3ef298ee31..f645637f07 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -369,6 +369,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}", "zen.api.error.modelDisabled": "モデルが無効です", + "zen.api.error.trialEnded": + "{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}", "black.meta.title": "OpenCode Black | 世界最高峰のコーディングモデルすべてにアクセス", "black.meta.description": "OpenCode Black サブスクリプションプランで、Claude、GPT、Gemini などにアクセス。", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 526791652c..f2a6147130 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -363,6 +363,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}", "zen.api.error.modelDisabled": "모델이 비활성화되었습니다", + "zen.api.error.trialEnded": + "{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}", "black.meta.title": "OpenCode Black | 세계 최고의 코딩 모델에 액세스하세요", "black.meta.description": "OpenCode Black 구독 플랜으로 Claude, GPT, Gemini 등에 액세스하세요.", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 3e405506b3..95c55c1e99 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -368,6 +368,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktivert", + "zen.api.error.trialEnded": + "Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Få tilgang til verdens beste kodemodeller", "black.meta.description": "Få tilgang til Claude, GPT, Gemini og mer med OpenCode Black-abonnementer.", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 513d8f9cc9..c119fad5b3 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -369,6 +369,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}", "zen.api.error.modelDisabled": "Model jest wyłączony", + "zen.api.error.trialEnded": + "Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Dostęp do najlepszych na świecie modeli kodujących", "black.meta.description": "Uzyskaj dostęp do Claude, GPT, Gemini i innych dzięki planom subskrypcji OpenCode Black.", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index ed90e61584..4b9401af9e 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -373,6 +373,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}", "zen.api.error.modelDisabled": "Модель отключена", + "zen.api.error.trialEnded": + "Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | Доступ к лучшим моделям для кодинга в мире", "black.meta.description": "Получите доступ к Claude, GPT, Gemini и другим моделям с подпиской OpenCode Black.", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 3eb7e63123..8ba75548a6 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -365,6 +365,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}", "zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน", + "zen.api.error.trialEnded": + "โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}", "black.meta.title": "OpenCode Black | เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก", "black.meta.description": "เข้าถึง Claude, GPT, Gemini และอื่นๆ ด้วยแผนสมาชิก OpenCode Black", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index a663e65119..ce3ec7e1d6 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -372,6 +372,8 @@ export const dict = { "zen.api.error.userMonthlyLimitReached": "Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}", "zen.api.error.modelDisabled": "Model devre dışı", + "zen.api.error.trialEnded": + "{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}", "black.meta.title": "OpenCode Black | Dünyanın en iyi kodlama modellerine erişin", "black.meta.description": "OpenCode Black abonelik planlarıyla Claude, GPT, Gemini ve daha fazlasına erişin.", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index cbf4932ef2..0803ffd132 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -349,6 +349,7 @@ export const dict = { "您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已禁用", + "zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}", "black.meta.title": "OpenCode Black | 访问全球顶尖编程模型", "black.meta.description": "通过 OpenCode Black 订阅计划使用 Claude, GPT, Gemini 等模型。", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 0670c11936..66e242eb76 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -349,6 +349,7 @@ export const dict = { "你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已停用", + "zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}", "black.meta.title": "OpenCode Black | 存取全球最佳編碼模型", "black.meta.description": "透過 OpenCode Black 訂閱方案存取 Claude、GPT、Gemini 等模型。", diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 53bd0e6012..db5977bc16 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -404,6 +404,14 @@ export async function handler( }), ) + if (modelData.trialEnded) + throw new ModelError( + `${t("zen.api.error.trialEnded", { + model: modelData.name, + link: "https://opencode.ai/go", + })}`, + ) + logger.metric({ model: modelId }) return { id: modelId, ...modelData } diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 191fdf1b7e..3b24394316 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -27,6 +27,7 @@ export namespace ZenData { byokProvider: z.enum(["openai", "anthropic", "google"]).optional(), stickyProvider: z.enum(["strict", "prefer"]).optional(), trialProviders: z.array(z.string()).optional(), + trialEnded: z.boolean().optional(), fallbackProvider: z.string().optional(), rateLimit: z.number().optional(), providers: z.array( diff --git a/packages/opencode/script/build-node.ts b/packages/opencode/script/build-node.ts index 17bc86307a..fc515a67a6 100644 --- a/packages/opencode/script/build-node.ts +++ b/packages/opencode/script/build-node.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun +import { Script } from "@opencode-ai/script" import fs from "fs" import path from "path" import { fileURLToPath } from "url" @@ -48,6 +49,7 @@ await Bun.build({ external: ["jsonc-parser"], define: { OPENCODE_MIGRATIONS: JSON.stringify(migrations), + OPENCODE_CHANNEL: `'${Script.channel}'`, }, }) diff --git a/packages/ui/package.json b/packages/ui/package.json index f84454695f..8c925753e8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -48,6 +48,7 @@ "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/event-listener": "2.4.5", "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index 15915dd52d..fb488729e2 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -16,6 +16,7 @@ import { } from "@pierre/diffs" import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { createMediaQuery } from "@solid-primitives/media" +import { makeEventListener } from "@solid-primitives/event-listener" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" import { createDefaultOptions, styleVariables } from "../pierre" import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines" @@ -286,17 +287,10 @@ function useFileViewer(config: ViewerConfig) { createEffect(() => { if (!config.enableLineSelection()) return - container.addEventListener("mousedown", handleMouseDown) - container.addEventListener("mousemove", handleMouseMove) - window.addEventListener("mouseup", handleMouseUp) - document.addEventListener("selectionchange", handleSelectionChange) - - onCleanup(() => { - container.removeEventListener("mousedown", handleMouseDown) - container.removeEventListener("mousemove", handleMouseMove) - window.removeEventListener("mouseup", handleMouseUp) - document.removeEventListener("selectionchange", handleSelectionChange) - }) + makeEventListener(container, "mousedown", handleMouseDown) + makeEventListener(container, "mousemove", handleMouseMove) + makeEventListener(window, "mouseup", handleMouseUp) + makeEventListener(document, "selectionchange", handleSelectionChange) }) onCleanup(() => { diff --git a/packages/ui/src/components/line-comment-annotations.tsx b/packages/ui/src/components/line-comment-annotations.tsx index 80018d3dda..f0286a36a9 100644 --- a/packages/ui/src/components/line-comment-annotations.tsx +++ b/packages/ui/src/components/line-comment-annotations.tsx @@ -294,11 +294,6 @@ export function createLineCommentState(props: LineCommentStateProps) { cancelDraft() } - createEffect(() => { - props.commenting() - setDraft("") - }) - return { draft, setDraft, diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index f0e29a485d..26e763bb3e 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/packages/ui/src/components/line-comment.tsx @@ -1,6 +1,6 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { createEffect, createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js" +import { createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js" import { Button } from "./button" import { FileIcon } from "./file-icon" import { Icon } from "./icon" @@ -210,7 +210,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { const refs = { textarea: undefined as HTMLTextAreaElement | undefined, } - const [text, setText] = createSignal(split.value) const [open, setOpen] = createSignal(false) function selectMention(item: { path: string } | undefined) { @@ -220,10 +219,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { const query = currentMention() if (!textarea || !query) return - const value = `${text().slice(0, query.start)}@${item.path} ${text().slice(query.end)}` + const value = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}` const cursor = query.start + item.path.length + 2 - setText(value) split.onInput(value) closeMention() @@ -257,10 +255,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { fn() } - createEffect(() => { - setText(split.value) - }) - const closeMention = () => { setOpen(false) mention.clear() @@ -302,7 +296,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { } const submit = () => { - const value = text().trim() + const value = split.value.trim() if (!value) return split.onSubmit(value) } @@ -322,10 +316,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { data-slot="line-comment-textarea" rows={split.rows ?? 3} placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")} - value={text()} + value={split.value} on:input={(e) => { const value = (e.currentTarget as HTMLTextAreaElement).value - setText(value) split.onInput(value) syncMention() }} @@ -422,7 +415,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { type="button" data-slot="line-comment-action" data-variant="primary" - disabled={text().trim().length === 0} + disabled={split.value.trim().length === 0} on:mousedown={hold as any} on:click={click(submit) as any} > @@ -434,7 +427,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => { - diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 8ce45bc5c6..b5879624e0 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -1,6 +1,7 @@ import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" -import { createEffect, For, onCleanup, type JSX, on, Show } from "solid-js" +import { createEffect, For, type JSX, on, Show } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { useI18n } from "../context/i18n" import { Icon, type IconProps } from "./icon" import { IconButton } from "./icon-button" @@ -228,9 +229,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0) } - scroll.addEventListener("scroll", handler, { passive: true }) + makeEventListener(scroll, "scroll", handler, { passive: true }) handler() - onCleanup(() => scroll.removeEventListener("scroll", handler)) }) return ( diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 1555a09a07..03477e5a7f 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -230,6 +230,19 @@ function createPacedValue(getValue: () => string, live?: () => boolean) { return value } +function PacedMarkdown(props: { text: string; cacheKey: string; streaming: boolean }) { + const value = createPacedValue( + () => props.text, + () => props.streaming, + ) + + return ( + + + + ) +} + function relativizeProjectPath(path: string, directory?: string) { if (!path) return "" if (!directory) return path @@ -1373,8 +1386,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { const streaming = createMemo( () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", ) - const displayText = () => (part().text ?? "").trim() - const throttledText = createPacedValue(displayText, streaming) + const text = () => (part().text ?? "").trim() const isLastTextPart = createMemo(() => { const last = (data.store.part?.[props.message.id] ?? []) .filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim()) @@ -1390,7 +1402,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { const [copied, setCopied] = createSignal(false) const handleCopy = async () => { - const content = displayText() + const content = text() if (!content) return await navigator.clipboard.writeText(content) setCopied(true) @@ -1398,10 +1410,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { } return ( - +
- + }> + +
@@ -1437,12 +1451,13 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) { () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", ) const text = () => part().text.trim() - const throttledText = createPacedValue(text, streaming) return ( - +
- + }> + +
) diff --git a/packages/ui/src/components/popover.tsx b/packages/ui/src/components/popover.tsx index 9d3da41090..8263640a50 100644 --- a/packages/ui/src/components/popover.tsx +++ b/packages/ui/src/components/popover.tsx @@ -1,15 +1,7 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { - ComponentProps, - JSXElement, - ParentProps, - Show, - createEffect, - onCleanup, - splitProps, - ValidComponent, -} from "solid-js" +import { ComponentProps, JSXElement, ParentProps, Show, createEffect, splitProps, ValidComponent } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { useI18n } from "../context/i18n" import { IconButton } from "./icon-button" @@ -104,15 +96,9 @@ export function Popover(props: PopoverProps close("outside") } - window.addEventListener("keydown", onKeyDown, true) - window.addEventListener("pointerdown", onPointerDown, true) - window.addEventListener("focusin", onFocusIn, true) - - onCleanup(() => { - window.removeEventListener("keydown", onKeyDown, true) - window.removeEventListener("pointerdown", onPointerDown, true) - window.removeEventListener("focusin", onFocusIn, true) - }) + makeEventListener(window, "keydown", onKeyDown, { capture: true }) + makeEventListener(window, "pointerdown", onPointerDown, { capture: true }) + makeEventListener(window, "focusin", onFocusIn, { capture: true }) }) const content = () => ( diff --git a/packages/ui/src/components/scroll-view.tsx b/packages/ui/src/components/scroll-view.tsx index 2b58300b9f..3ff00f117d 100644 --- a/packages/ui/src/components/scroll-view.tsx +++ b/packages/ui/src/components/scroll-view.tsx @@ -1,4 +1,5 @@ -import { onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js" +import { onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" import { useI18n } from "../context/i18n" @@ -97,19 +98,7 @@ export function ScrollView(props: ScrollViewProps) { local.viewportRef(viewportRef) } - const observer = new ResizeObserver(() => { - updateThumb() - }) - - observer.observe(viewportRef) - // Also observe the first child if possible to catch content changes - if (viewportRef.firstElementChild) { - observer.observe(viewportRef.firstElementChild) - } - - onCleanup(() => { - observer.disconnect() - }) + createResizeObserver([viewportRef, viewportRef.firstElementChild], updateThumb) updateThumb() }) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index ed4c0e9149..fe029485a1 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -343,14 +343,12 @@ export function SessionTurn( }) const assistantDerived = createMemo(() => { let visible = 0 - let tail: "text" | "other" | undefined let reason: string | undefined const show = showReasoningSummaries() for (const message of assistantMessages()) { for (const part of list(data.store.part?.[message.id], emptyParts)) { if (partState(part, show) === "visible") { visible++ - tail = part.type === "text" ? "text" : "other" } if (part.type === "reasoning" && part.text) { const h = heading(part.text) @@ -358,10 +356,9 @@ export function SessionTurn( } } } - return { visible, tail, reason } + return { visible, reason } }) const assistantVisible = createMemo(() => assistantDerived().visible) - const assistantTailVisible = createMemo(() => assistantDerived().tail) const reasoningHeading = createMemo(() => assistantDerived().reason) const showThinking = createMemo(() => { if (!working() || !!error()) return false diff --git a/packages/ui/src/components/text-strikethrough.stories.tsx b/packages/ui/src/components/text-strikethrough.stories.tsx index 5e86413f93..5ef06dbcf4 100644 --- a/packages/ui/src/components/text-strikethrough.stories.tsx +++ b/packages/ui/src/components/text-strikethrough.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { createSignal, onMount } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" import { TextStrikethrough } from "./text-strikethrough" @@ -144,13 +145,7 @@ function VariantF(props: { active: boolean; text: string }) { } onMount(measure) - createEffect(() => { - const el = containerRef - if (!el) return - const observer = new ResizeObserver(measure) - observer.observe(el) - onCleanup(() => observer.disconnect()) - }) + createResizeObserver(() => containerRef, measure) const clipRight = () => { const cw = containerWidth() diff --git a/packages/ui/src/components/text-strikethrough.tsx b/packages/ui/src/components/text-strikethrough.tsx index aee5e0cbd0..958befff68 100644 --- a/packages/ui/src/components/text-strikethrough.tsx +++ b/packages/ui/src/components/text-strikethrough.tsx @@ -1,5 +1,6 @@ import type { JSX } from "solid-js" -import { createEffect, onCleanup, onMount } from "solid-js" +import { onMount } from "solid-js" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" import { useSpring } from "./motion-spring" @@ -33,14 +34,7 @@ export function TextStrikethrough(props: { } onMount(measure) - - createEffect(() => { - const el = containerRef - if (!el) return - const observer = new ResizeObserver(measure) - observer.observe(el) - onCleanup(() => observer.disconnect()) - }) + createResizeObserver(() => containerRef, measure) // Revealed pixels from left = progress * textWidth const revealedPx = () => { diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx index afba5f648c..c1c56212b5 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -12,6 +12,7 @@ import { type JSX, } from "solid-js" import { Dialog as Kobalte } from "@kobalte/core/dialog" +import { makeEventListener } from "@solid-primitives/event-listener" type DialogElement = () => JSX.Element @@ -68,8 +69,7 @@ function init() { event.stopPropagation() } - window.addEventListener("keydown", onKeyDown, true) - onCleanup(() => window.removeEventListener("keydown", onKeyDown, true)) + makeEventListener(window, "keydown", onKeyDown, { capture: true }) }) const show = (element: DialogElement, owner: Owner, onClose?: () => void) => { diff --git a/packages/ui/src/hooks/create-auto-scroll.tsx b/packages/ui/src/hooks/create-auto-scroll.tsx index 3dc520c621..9733b094ec 100644 --- a/packages/ui/src/hooks/create-auto-scroll.tsx +++ b/packages/ui/src/hooks/create-auto-scroll.tsx @@ -1,5 +1,6 @@ -import { createEffect, on, onCleanup } from "solid-js" +import { createEffect, createSignal, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" export interface AutoScrollOptions { @@ -14,7 +15,6 @@ export function createAutoScroll(options: AutoScrollOptions) { let settling = false let settleTimer: ReturnType | undefined let autoTimer: ReturnType | undefined - let cleanup: (() => void) | undefined let auto: { top: number; time: number } | undefined const threshold = () => options.bottomThreshold ?? 10 @@ -216,26 +216,14 @@ export function createAutoScroll(options: AutoScrollOptions) { onCleanup(() => { if (settleTimer) clearTimeout(settleTimer) if (autoTimer) clearTimeout(autoTimer) - if (cleanup) cleanup() }) return { scrollRef: (el: HTMLElement | undefined) => { - if (cleanup) { - cleanup() - cleanup = undefined - } - - scroll = el - if (!el) return updateOverflowAnchor(el) - el.addEventListener("wheel", handleWheel, { passive: true }) - - cleanup = () => { - el.removeEventListener("wheel", handleWheel) - } + makeEventListener(el, "wheel", handleWheel, { passive: true }) }, contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el), handleScroll, diff --git a/packages/ui/src/pierre/file-find.ts b/packages/ui/src/pierre/file-find.ts index 692ab31670..d1cf6dd305 100644 --- a/packages/ui/src/pierre/file-find.ts +++ b/packages/ui/src/pierre/file-find.ts @@ -1,4 +1,6 @@ -import { createEffect, onCleanup, onMount } from "solid-js" +import { createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { createStore } from "solid-js/store" export type FindHost = { @@ -104,9 +106,9 @@ type CreateFileFindOptions = { export function createFileFind(opts: CreateFileFindOptions) { let input: HTMLInputElement | undefined let overlayFrame: number | undefined - let overlayScroll: HTMLElement[] = [] let mode: "highlights" | "overlay" = "overlay" let hits: Range[] = [] + const [overlayScroll, setOverlayScroll] = createSignal([]) const [state, setState] = createStore({ open: false, @@ -122,8 +124,7 @@ export function createFileFind(opts: CreateFileFindOptions) { const pos = () => state.pos const clearOverlayScroll = () => { - for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay) - overlayScroll = [] + setOverlayScroll([]) } const clearOverlay = () => { @@ -196,11 +197,11 @@ export function createFileFind(opts: CreateFileFindOptions) { (node): node is HTMLElement => node instanceof HTMLElement, ) : [] - if (next.length === overlayScroll.length && next.every((el, i) => el === overlayScroll[i])) return + const current = overlayScroll() + if (next.length === current.length && next.every((el, i) => el === current[i])) return clearOverlayScroll() - overlayScroll = next - for (const el of overlayScroll) el.addEventListener("scroll", scheduleOverlay, { passive: true }) + setOverlayScroll(next) } const clearFind = () => { @@ -403,6 +404,10 @@ export function createFileFind(opts: CreateFileFindOptions) { close, } + createEffect(() => { + for (const el of overlayScroll()) makeEventListener(el, "scroll", scheduleOverlay, { passive: true }) + }) + onMount(() => { mode = supportsHighlights() ? "highlights" : "overlay" installShortcuts() @@ -424,18 +429,12 @@ export function createFileFind(opts: CreateFileFindOptions) { const update = () => positionBar() requestAnimationFrame(update) - window.addEventListener("resize", update, { passive: true }) + makeEventListener(window, "resize", update, { passive: true }) const wrapper = opts.wrapper() if (!wrapper) return const root = scrollParent(wrapper) ?? wrapper - const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => update()) - observer?.observe(root) - - onCleanup(() => { - window.removeEventListener("resize", update) - observer?.disconnect() - }) + createResizeObserver(root, update) }) onCleanup(() => { diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx index 7d25ac3972..5664eeebd5 100644 --- a/packages/ui/src/theme/context.tsx +++ b/packages/ui/src/theme/context.tsx @@ -1,5 +1,6 @@ -import { createEffect, onCleanup, onMount } from "solid-js" +import { createEffect, onMount } from "solid-js" import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" import { createSimpleContext } from "../context/helper" import oc2ThemeJson from "./themes/oc-2.json" import { resolveThemeVariant, themeToCss } from "./resolve" @@ -237,19 +238,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ } } - if (typeof window === "object") { - window.addEventListener("storage", onStorage) - onCleanup(() => window.removeEventListener("storage", onStorage)) - } - onMount(() => { + makeEventListener(window, "storage", onStorage) + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const onMedia = () => { if (store.colorScheme !== "system") return setStore("mode", getSystemMode()) } - mediaQuery.addEventListener("change", onMedia) - onCleanup(() => mediaQuery.removeEventListener("change", onMedia)) + makeEventListener(mediaQuery, "change", onMedia) const rawTheme = read(STORAGE_KEYS.THEME_ID) const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2" diff --git a/packages/web/package.json b/packages/web/package.json index ef0d8aa6c3..60082740bb 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,6 +18,7 @@ "@astrojs/starlight": "0.34.3", "@fontsource/ibm-plex-mono": "5.2.5", "@shikijs/transformers": "3.20.0", + "@solid-primitives/resize-observer": "2.1.5", "@types/luxon": "catalog:", "ai": "catalog:", "astro": "5.7.13", diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index de12baede0..3ee86c270d 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -366,21 +366,13 @@ export default function Share(props: { {(part, partIndex) => { - const last = createMemo( - () => - data().messages.length === msgIndex() + 1 && - filteredParts().length === partIndex() + 1, - ) + const last = () => + data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1 onMount(() => { const hash = window.location.hash.slice(1) // Wait till all parts are loaded - if ( - hash !== "" && - !hasScrolledToAnchor && - filteredParts().length === partIndex() + 1 && - data().messages.length === msgIndex() + 1 - ) { + if (hash !== "" && !hasScrolledToAnchor && last()) { hasScrolledToAnchor = true scrollToAnchor(hash) } diff --git a/packages/web/src/components/share/common.tsx b/packages/web/src/components/share/common.tsx index 7ca4daa6ac..ad50e425fa 100644 --- a/packages/web/src/components/share/common.tsx +++ b/packages/web/src/components/share/common.tsx @@ -1,5 +1,6 @@ -import { createContext, createSignal, onCleanup, splitProps, useContext } from "solid-js" +import { createContext, createSignal, splitProps, useContext } from "solid-js" import type { JSX } from "solid-js/jsx-runtime" +import { makeResizeObserver } from "@solid-primitives/resize-observer" import { IconCheckCircle, IconHashtag } from "../icons" export type ShareMessages = { locale: string } & Record @@ -83,17 +84,14 @@ export function createOverflow() { return overflow() }, ref(el: HTMLElement) { - const ro = new ResizeObserver(() => { - if (el.scrollHeight > el.clientHeight + 1) { - setOverflow(true) - } - return - }) - ro.observe(el) + const sync = () => { + setOverflow(el.scrollHeight > el.clientHeight + 1) + } - onCleanup(() => { - ro.disconnect() - }) + const obs = makeResizeObserver(sync) + obs.observe(el) + + sync() }, } } diff --git a/packages/web/src/components/share/content-diff.tsx b/packages/web/src/components/share/content-diff.tsx index 9ccd554d04..c8dd35b38f 100644 --- a/packages/web/src/components/share/content-diff.tsx +++ b/packages/web/src/components/share/content-diff.tsx @@ -1,5 +1,5 @@ import { parsePatch } from "diff" -import { createMemo } from "solid-js" +import { createMemo, For } from "solid-js" import { ContentCode } from "./content-code" import styles from "./content-diff.module.css" @@ -160,28 +160,37 @@ export function ContentDiff(props: Props) { return (
- {rows().map((r) => ( -
-
- + + {(row) => ( +
+
+ +
+
+ +
-
- -
-
- ))} + )} +
- {mobileRows().map((block) => ( -
- {block.lines.map((line) => ( -
- -
- ))} -
- ))} + + {(block) => ( +
+ + {(line) => ( +
+ +
+ )} +
+
+ )} +
)