Merge branch 'dev' into effect-sync-event

pull/20597/head
Kit Langton 2026-04-02 11:48:33 -04:00 committed by GitHub
commit 44e96fd358
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 407 additions and 331 deletions

View File

@ -36,9 +36,10 @@
"@solid-primitives/active-element": "2.1.3", "@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2", "@solid-primitives/audio": "1.4.2",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/i18n": "2.2.1", "@solid-primitives/i18n": "2.2.1",
"@solid-primitives/media": "2.3.3", "@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/scroll": "2.1.3",
"@solid-primitives/storage": "catalog:", "@solid-primitives/storage": "catalog:",
"@solid-primitives/timer": "1.4.4", "@solid-primitives/timer": "1.4.4",
@ -382,6 +383,7 @@
"tree-sitter-powershell": "0.25.10", "tree-sitter-powershell": "0.25.10",
"turndown": "7.2.0", "turndown": "7.2.0",
"ulid": "catalog:", "ulid": "catalog:",
"venice-ai-sdk-provider": "2.0.1",
"vscode-jsonrpc": "8.2.1", "vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10", "web-tree-sitter": "0.25.10",
"which": "6.0.1", "which": "6.0.1",
@ -513,6 +515,7 @@
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3", "@solid-primitives/bounds": "0.1.3",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
@ -571,6 +574,7 @@
"@astrojs/starlight": "0.34.3", "@astrojs/starlight": "0.34.3",
"@fontsource/ibm-plex-mono": "5.2.5", "@fontsource/ibm-plex-mono": "5.2.5",
"@shikijs/transformers": "3.20.0", "@shikijs/transformers": "3.20.0",
"@solid-primitives/resize-observer": "2.1.5",
"@types/luxon": "catalog:", "@types/luxon": "catalog:",
"ai": "catalog:", "ai": "catalog:",
"astro": "5.7.13", "astro": "5.7.13",
@ -1933,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/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=="], "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="],
@ -4755,6 +4759,8 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="],
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@ -5209,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=="], "@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=="], "@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=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
@ -5301,6 +5309,8 @@
"@opencode-ai/desktop-electron/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], "@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=="], "@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=="], "@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=="],
@ -5363,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=="], "@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/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=="], "@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=="],

View File

@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-bjfe8/aD0hvUQQEfaNdmKV/Y3dzpf8oz1OUJdgf61WI=", "x86_64-linux": "sha256-SQVfq41OQdGCgWuWqyqIN6aggL0r3Hzn2hJ9BwPJN+I=",
"aarch64-linux": "sha256-iU9v+ekSCB/qTUG+pOOpSMhPh+0hWnWU5jzDNllEkxU=", "aarch64-linux": "sha256-4w/1HhxsTzPFTHNf4JlnKle6Boz1gVTEedWG64T8E/M=",
"aarch64-darwin": "sha256-SgNydQLeAjbX0J49f2VKcgKg2Y30pK826R2qQJBMWE4=", "aarch64-darwin": "sha256-uMd+pU1u1yqP4OP/9461Tyy3zwwv/llr+rlllLjM98A=",
"x86_64-darwin": "sha256-/rzwNuI9x55qi0UcU7QvPUTupErmkt62T09g1omXkQk=" "x86_64-darwin": "sha256-BhIW3FPqKkM2vGfCrxXUvj5tarey33Q7dxCuaj5A+yU="
} }
} }

View File

@ -46,9 +46,10 @@
"@solid-primitives/active-element": "2.1.3", "@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2", "@solid-primitives/audio": "1.4.2",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/i18n": "2.2.1", "@solid-primitives/i18n": "2.2.1",
"@solid-primitives/media": "2.3.3", "@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/scroll": "2.1.3",
"@solid-primitives/storage": "catalog:", "@solid-primitives/storage": "catalog:",
"@solid-primitives/timer": "1.4.4", "@solid-primitives/timer": "1.4.4",

View File

@ -1,6 +1,7 @@
import { useIsRouting, useLocation } from "@solidjs/router" import { useIsRouting, useLocation } from "@solidjs/router"
import { batch, createEffect, onCleanup, onMount } from "solid-js" import { batch, createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@ -349,13 +350,12 @@ export function DebugBar() {
syncHeap() syncHeap()
start() start()
document.addEventListener("visibilitychange", vis) makeEventListener(document, "visibilitychange", vis)
onCleanup(() => { onCleanup(() => {
if (one !== 0) cancelAnimationFrame(one) if (one !== 0) cancelAnimationFrame(one)
if (two !== 0) cancelAnimationFrame(two) if (two !== 0) cancelAnimationFrame(two)
stop() stop()
document.removeEventListener("visibilitychange", vis)
for (const ob of obs) ob.disconnect() for (const ob of obs) ob.disconnect()
}) })
}) })

View File

@ -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 { showToast } from "@opencode-ai/ui/toast"
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@ -181,15 +182,9 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
} }
onMount(() => { onMount(() => {
document.addEventListener("dragover", handleGlobalDragOver) makeEventListener(document, "dragover", handleGlobalDragOver)
document.addEventListener("dragleave", handleGlobalDragLeave) makeEventListener(document, "dragleave", handleGlobalDragLeave)
document.addEventListener("drop", handleGlobalDrop) makeEventListener(document, "drop", handleGlobalDrop)
})
onCleanup(() => {
document.removeEventListener("dragover", handleGlobalDragOver)
document.removeEventListener("dragleave", handleGlobalDragLeave)
document.removeEventListener("drop", handleGlobalDrop)
}) })
return { return {

View File

@ -1,11 +1,11 @@
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { import {
children, children,
createEffect, createEffect,
createMemo, createMemo,
createSignal, createSignal,
type JSXElement, type JSXElement,
onCleanup,
onMount, onMount,
type ParentProps, type ParentProps,
Show, Show,
@ -46,12 +46,9 @@ export function ServerRow(props: ServerRowProps) {
}) })
onMount(() => { onMount(() => {
check()
if (typeof ResizeObserver !== "function") return if (typeof ResizeObserver !== "function") return
const observer = new ResizeObserver(check) createResizeObserver([nameRef, versionRef], check)
if (nameRef) observer.observe(nameRef) check()
if (versionRef) observer.observe(versionRef)
onCleanup(() => observer.disconnect())
}) })
const tooltipValue = () => ( const tooltipValue = () => (

View File

@ -1,5 +1,6 @@
import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js" import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@ -250,8 +251,7 @@ function useKeyCapture(input: {
input.stop() input.stop()
} }
document.addEventListener("keydown", handle, true) makeEventListener(document, "keydown", handle, { capture: true })
onCleanup(() => document.removeEventListener("keydown", handle, true))
}) })
} }

View File

@ -2,6 +2,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js" import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
import { dict as en } from "@/i18n/en" import { dict as en } from "@/i18n/en"
@ -378,11 +379,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
} }
onMount(() => { onMount(() => {
document.addEventListener("keydown", handleKeyDown) makeEventListener(document, "keydown", handleKeyDown)
})
onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
}) })
function register(cb: () => CommandOption[]): void function register(cb: () => CommandOption[]): void

View File

@ -1,7 +1,8 @@
import type { Event } from "@opencode-ai/sdk/v2/client" import type { Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus" 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 z from "zod"
import { createSdkForServer } from "@/utils/server" import { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language" import { useLanguage } from "./language"
@ -206,21 +207,16 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
clearHeartbeat() clearHeartbeat()
} }
const onVisibility = () => { onMount(() => {
if (typeof document === "undefined") return makeEventListener(document, "visibilitychange", () => {
if (document.visibilityState !== "visible") return if (document.visibilityState !== "visible") return
if (!started) return if (!started) return
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
attempt?.abort() attempt?.abort()
} })
if (typeof document !== "undefined") { })
document.addEventListener("visibilitychange", onVisibility)
}
onCleanup(() => { onCleanup(() => {
if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibility)
}
stop() stop()
abort.abort() abort.abort()
flush() flush()

View File

@ -1,6 +1,7 @@
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useGlobalSync } from "./global-sync" import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server" import { useServer } from "./server"
@ -366,12 +367,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
flush() flush()
} }
window.addEventListener("pagehide", flush) makeEventListener(window, "pagehide", flush)
document.addEventListener("visibilitychange", handleVisibility) makeEventListener(document, "visibilitychange", handleVisibility)
onCleanup(() => { onCleanup(() => {
window.removeEventListener("pagehide", flush)
document.removeEventListener("visibilitychange", handleVisibility)
scroll.dispose() scroll.dispose()
}) })
}) })

View File

@ -12,6 +12,7 @@ import {
untrack, untrack,
type Accessor, type Accessor,
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout" import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
@ -215,18 +216,11 @@ export default function Layout(props: ParentProps) {
if (document.visibilityState !== "hidden") return if (document.visibilityState !== "hidden") return
reset() reset()
} }
window.addEventListener("pointerup", stop) makeEventListener(window, "pointerup", stop)
window.addEventListener("pointercancel", stop) makeEventListener(window, "pointercancel", stop)
window.addEventListener("blur", stop) makeEventListener(window, "blur", stop)
window.addEventListener("blur", blur) makeEventListener(window, "blur", blur)
document.addEventListener("visibilitychange", hide) makeEventListener(document, "visibilitychange", hide)
onCleanup(() => {
window.removeEventListener("pointerup", stop)
window.removeEventListener("pointercancel", stop)
window.removeEventListener("blur", stop)
window.removeEventListener("blur", blur)
document.removeEventListener("visibilitychange", hide)
})
}) })
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
@ -1394,8 +1388,7 @@ export default function Layout(props: ParentProps) {
} }
handleDeepLinks(drainPendingDeepLinks(window)) handleDeepLinks(drainPendingDeepLinks(window))
window.addEventListener(deepLinkEvent, handler as EventListener) makeEventListener(window, deepLinkEvent, handler as EventListener)
onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener))
}) })
async function renameProject(project: LocalProject, next: string) { async function renameProject(project: LocalProject, next: string) {

View File

@ -14,6 +14,7 @@ import {
onMount, onMount,
untrack, untrack,
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createMediaQuery } from "@solid-primitives/media" import { createMediaQuery } from "@solid-primitives/media"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
@ -329,10 +330,9 @@ export default function Page() {
const { params, sessionKey, tabs, view } = useSessionLayout() const { params, sessionKey, tabs, view } = useSessionLayout()
createEffect(() => { createEffect(() => {
if (!untrack(() => prompt.ready())) return if (!prompt.ready()) return
prompt.ready()
untrack(() => { untrack(() => {
if (params.id || !prompt.ready()) return if (params.id) return
const text = searchParams.prompt const text = searchParams.prompt
if (!text) return if (!text) return
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
@ -1688,11 +1688,10 @@ export default function Page() {
) )
onMount(() => { onMount(() => {
document.addEventListener("keydown", handleKeyDown) makeEventListener(document, "keydown", handleKeyDown)
}) })
onCleanup(() => { onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)

View File

@ -13,6 +13,7 @@ import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state" import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
import type { FollowupDraft } from "@/components/prompt-input/submit" import type { FollowupDraft } from "@/components/prompt-input/submit"
import { createResizeObserver } from "@solid-primitives/resize-observer"
export function SessionComposerRegion(props: { export function SessionComposerRegion(props: {
state: SessionComposerState state: SessionComposerState
@ -115,13 +116,9 @@ export function SessionComposerRegion(props: {
createEffect(() => { createEffect(() => {
const el = store.body const el = store.body
if (!el) return if (!el) return
const update = () => { const update = () => setStore("height", el.getBoundingClientRect().height)
setStore("height", el.getBoundingClientRect().height) createResizeObserver(store.body, update)
}
update() update()
const observer = new ResizeObserver(update)
observer.observe(el)
onCleanup(() => observer.disconnect())
}) })
return ( return (

View File

@ -1,5 +1,6 @@
import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
@ -86,8 +87,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
pull() pull()
} }
window.addEventListener(composerEvent, onEvent) makeEventListener(window, composerEvent, onEvent)
onCleanup(() => window.removeEventListener(composerEvent, onEvent))
}) })
const todos = createMemo((): Todo[] => { const todos = createMemo((): Todo[] => {

View File

@ -8,6 +8,8 @@ import { showToast } from "@opencode-ai/ui/toast"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer"
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>() const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
@ -172,17 +174,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
} }
update() update()
window.addEventListener("resize", update)
makeEventListener(window, "resize", update)
const dock = root?.closest('[data-component="session-prompt-dock"]') const dock = root?.closest('[data-component="session-prompt-dock"]')
const scroller = document.querySelector(".scroll-view__viewport") const scroller = document.querySelector(".scroll-view__viewport")
const observer = new ResizeObserver(update) createResizeObserver([dock, scroller], update)
if (dock instanceof HTMLElement) observer.observe(dock)
if (scroller instanceof HTMLElement) observer.observe(scroller)
onCleanup(() => { onCleanup(() => {
window.removeEventListener("resize", update)
observer.disconnect()
if (raf !== undefined) cancelAnimationFrame(raf) if (raf !== undefined) cancelAnimationFrame(raf)
}) })

View File

@ -6,6 +6,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { useSpring } from "@opencode-ai/ui/motion-spring" import { useSpring } from "@opencode-ai/ui/motion-spring"
import { TextReveal } from "@opencode-ai/ui/text-reveal" import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough" 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 { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { composerEnabled, composerProbe } from "@/testing/session-composer" import { composerEnabled, composerProbe } from "@/testing/session-composer"
@ -91,9 +92,7 @@ export function SessionTodoDock(props: {
setStore("height", el.getBoundingClientRect().height) setStore("height", el.getBoundingClientRect().height)
} }
update() update()
const observer = new ResizeObserver(update) createResizeObserver(el, update)
observer.observe(el)
onCleanup(() => observer.disconnect())
}) })
createEffect(() => { createEffect(() => {

View File

@ -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 { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { FileSearchHandle } from "@opencode-ai/ui/file" import type { FileSearchHandle } from "@opencode-ai/ui/file"
import { useFileComponent } from "@opencode-ai/ui/context/file" import { useFileComponent } from "@opencode-ai/ui/context/file"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
@ -59,7 +60,7 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
let scrollFrame: number | undefined let scrollFrame: number | undefined
let restoreFrame: number | undefined let restoreFrame: number | undefined
let pending: ScrollPos | undefined let pending: ScrollPos | undefined
let code: HTMLElement[] = [] const [code, setCode] = createSignal<HTMLElement[]>([])
const getCode = () => { const getCode = () => {
const el = scroll const el = scroll
@ -106,17 +107,9 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
const sync = () => { const sync = () => {
const next = getCode() const next = getCode()
if (next.length === code.length && next.every((el, i) => el === code[i])) return const current = code()
if (next.length === current.length && next.every((el, i) => el === current[i])) return
for (const item of code) { setCode(next)
item.removeEventListener("scroll", onCodeScroll)
}
code = next
for (const item of code) {
item.addEventListener("scroll", onCodeScroll)
}
} }
const restore = () => { const restore = () => {
@ -128,14 +121,14 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
sync() sync()
if (code.length > 0) { if (code().length > 0) {
for (const item of code) { for (const item of code()) {
if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x
} }
} }
if (el.scrollTop !== pos.y) el.scrollTop = pos.y 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 if (el.scrollLeft !== pos.x) el.scrollLeft = pos.x
} }
@ -149,24 +142,24 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
} }
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
if (code.length === 0) sync() if (code().length === 0) sync()
save({ save({
x: code[0]?.scrollLeft ?? event.currentTarget.scrollLeft, x: code()[0]?.scrollLeft ?? event.currentTarget.scrollLeft,
y: event.currentTarget.scrollTop, y: event.currentTarget.scrollTop,
}) })
} }
createEffect(() => {
for (const item of code()) makeEventListener(item, "scroll", onCodeScroll)
})
const setViewport = (el: HTMLDivElement) => { const setViewport = (el: HTMLDivElement) => {
scroll = el scroll = el
restore() restore()
} }
onCleanup(() => { onCleanup(() => {
for (const item of code) {
item.removeEventListener("scroll", onCodeScroll)
}
if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame) if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
}) })
@ -358,8 +351,7 @@ export function FileTabContent(props: { tab: string }) {
find?.focus() find?.focus()
} }
window.addEventListener("keydown", onKeyDown, { capture: true }) makeEventListener(window, "keydown", onKeyDown, { capture: true })
onCleanup(() => window.removeEventListener("keydown", onKeyDown, { capture: true }))
}) })
createEffect( createEffect(

View File

@ -1,5 +1,6 @@
import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { same } from "@/utils/same" import { same } from "@/utils/same"
const emptyTabs: string[] = [] const emptyTabs: string[] = []
@ -171,14 +172,9 @@ export const createSizing = () => {
} }
onMount(() => { onMount(() => {
window.addEventListener("pointerup", stop) makeEventListener(window, "pointerup", stop)
window.addEventListener("pointercancel", stop) makeEventListener(window, "pointercancel", stop)
window.addEventListener("blur", stop) makeEventListener(window, "blur", stop)
onCleanup(() => {
window.removeEventListener("pointerup", stop)
window.removeEventListener("pointercancel", stop)
window.removeEventListener("blur", stop)
})
}) })
onCleanup(() => { onCleanup(() => {

View File

@ -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 type { FileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
import type { import type {
@ -123,13 +124,6 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
onCleanup(() => { onCleanup(() => {
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) 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 ( return (
@ -138,11 +132,11 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
empty={props.empty} empty={props.empty}
scrollRef={(el) => { scrollRef={(el) => {
scroll = el scroll = el
el.addEventListener("wheel", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "wheel", handleInteraction, { passive: true, capture: true })
el.addEventListener("mousewheel", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "mousewheel", handleInteraction, { passive: true, capture: true })
el.addEventListener("pointerdown", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "pointerdown", handleInteraction, { passive: true, capture: true })
el.addEventListener("touchstart", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "touchstart", handleInteraction, { passive: true, capture: true })
el.addEventListener("keydown", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "keydown", handleInteraction, { capture: true })
props.onScrollRef?.(el) props.onScrollRef?.(el)
queueRestore() queueRestore()
}} }}

View File

@ -1,5 +1,6 @@
import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@ -50,12 +51,8 @@ export function TerminalPanel() {
const port = window.visualViewport const port = window.visualViewport
sync() sync()
window.addEventListener("resize", sync) makeEventListener(window, "resize", sync)
port?.addEventListener("resize", sync) if (port) makeEventListener(port, "resize", sync)
onCleanup(() => {
window.removeEventListener("resize", sync)
port?.removeEventListener("resize", sync)
})
}) })
createEffect(() => { createEffect(() => {

View File

@ -363,6 +363,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}", "لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}",
"zen.api.error.modelDisabled": "النموذج معطل", "zen.api.error.modelDisabled": "النموذج معطل",
"zen.api.error.trialEnded":
"انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}",
"black.meta.title": "OpenCode Black | الوصول إلى أفضل نماذج البرمجة في العالم", "black.meta.title": "OpenCode Black | الوصول إلى أفضل نماذج البرمجة في العالم",
"black.meta.description": "احصل على وصول إلى Claude، GPT، Gemini والمزيد مع خطط اشتراك OpenCode Black.", "black.meta.description": "احصل على وصول إلى Claude، GPT، Gemini والمزيد مع خطط اشتراك OpenCode Black.",

View File

@ -371,6 +371,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}", "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.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.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.", "black.meta.description": "Tenha acesso ao Claude, GPT, Gemini e mais com os planos de assinatura OpenCode Black.",

View File

@ -368,6 +368,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}", "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.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.title": "OpenCode Black | Få adgang til verdens bedste kodningsmodeller",
"black.meta.description": "Få adgang til Claude, GPT, Gemini og mere med OpenCode Black-abonnementer.", "black.meta.description": "Få adgang til Claude, GPT, Gemini og mere med OpenCode Black-abonnementer.",

View File

@ -371,6 +371,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}", "Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}",
"zen.api.error.modelDisabled": "Modell ist deaktiviert", "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.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.", "black.meta.description": "Erhalte Zugriff auf Claude, GPT, Gemini und mehr mit OpenCode Black Abos.",

View File

@ -364,6 +364,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}", "You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model is disabled", "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.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.", "black.meta.description": "Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans.",

View File

@ -371,6 +371,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}", "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.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.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.", "black.meta.description": "Obtén acceso a Claude, GPT, Gemini y más con los planes de suscripción de OpenCode Black.",

View File

@ -372,6 +372,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}", "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.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.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.", "black.meta.description": "Accédez à Claude, GPT, Gemini et plus avec les forfaits d'abonnement OpenCode Black.",

View File

@ -367,6 +367,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}", "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.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.title": "OpenCode Black | Accedi ai migliori modelli di coding al mondo",
"black.meta.description": "black.meta.description":

View File

@ -369,6 +369,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}", "月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}",
"zen.api.error.modelDisabled": "モデルが無効です", "zen.api.error.modelDisabled": "モデルが無効です",
"zen.api.error.trialEnded":
"{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}",
"black.meta.title": "OpenCode Black | 世界最高峰のコーディングモデルすべてにアクセス", "black.meta.title": "OpenCode Black | 世界最高峰のコーディングモデルすべてにアクセス",
"black.meta.description": "OpenCode Black サブスクリプションプランで、Claude、GPT、Gemini などにアクセス。", "black.meta.description": "OpenCode Black サブスクリプションプランで、Claude、GPT、Gemini などにアクセス。",

View File

@ -363,6 +363,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}", "월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}",
"zen.api.error.modelDisabled": "모델이 비활성화되었습니다", "zen.api.error.modelDisabled": "모델이 비활성화되었습니다",
"zen.api.error.trialEnded":
"{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}",
"black.meta.title": "OpenCode Black | 세계 최고의 코딩 모델에 액세스하세요", "black.meta.title": "OpenCode Black | 세계 최고의 코딩 모델에 액세스하세요",
"black.meta.description": "OpenCode Black 구독 플랜으로 Claude, GPT, Gemini 등에 액세스하세요.", "black.meta.description": "OpenCode Black 구독 플랜으로 Claude, GPT, Gemini 등에 액세스하세요.",

View File

@ -368,6 +368,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}", "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.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.title": "OpenCode Black | Få tilgang til verdens beste kodemodeller",
"black.meta.description": "Få tilgang til Claude, GPT, Gemini og mer med OpenCode Black-abonnementer.", "black.meta.description": "Få tilgang til Claude, GPT, Gemini og mer med OpenCode Black-abonnementer.",

View File

@ -369,6 +369,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}", "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.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.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.", "black.meta.description": "Uzyskaj dostęp do Claude, GPT, Gemini i innych dzięki planom subskrypcji OpenCode Black.",

View File

@ -373,6 +373,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}", "Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}",
"zen.api.error.modelDisabled": "Модель отключена", "zen.api.error.modelDisabled": "Модель отключена",
"zen.api.error.trialEnded":
"Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}",
"black.meta.title": "OpenCode Black | Доступ к лучшим моделям для кодинга в мире", "black.meta.title": "OpenCode Black | Доступ к лучшим моделям для кодинга в мире",
"black.meta.description": "Получите доступ к Claude, GPT, Gemini и другим моделям с подпиской OpenCode Black.", "black.meta.description": "Получите доступ к Claude, GPT, Gemini и другим моделям с подпиской OpenCode Black.",

View File

@ -365,6 +365,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}", "คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}",
"zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน", "zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน",
"zen.api.error.trialEnded":
"โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}",
"black.meta.title": "OpenCode Black | เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก", "black.meta.title": "OpenCode Black | เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
"black.meta.description": "เข้าถึง Claude, GPT, Gemini และอื่นๆ ด้วยแผนสมาชิก OpenCode Black", "black.meta.description": "เข้าถึง Claude, GPT, Gemini และอื่นๆ ด้วยแผนสมาชิก OpenCode Black",

View File

@ -372,6 +372,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached": "zen.api.error.userMonthlyLimitReached":
"Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}", "Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model devre dışı", "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.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.", "black.meta.description": "OpenCode Black abonelik planlarıyla Claude, GPT, Gemini ve daha fazlasına erişin.",

View File

@ -349,6 +349,7 @@ export const dict = {
"您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}", "您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}",
"zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}", "zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}",
"zen.api.error.modelDisabled": "模型已禁用", "zen.api.error.modelDisabled": "模型已禁用",
"zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}",
"black.meta.title": "OpenCode Black | 访问全球顶尖编程模型", "black.meta.title": "OpenCode Black | 访问全球顶尖编程模型",
"black.meta.description": "通过 OpenCode Black 订阅计划使用 Claude, GPT, Gemini 等模型。", "black.meta.description": "通过 OpenCode Black 订阅计划使用 Claude, GPT, Gemini 等模型。",

View File

@ -349,6 +349,7 @@ export const dict = {
"你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}", "你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}",
"zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}", "zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}",
"zen.api.error.modelDisabled": "模型已停用", "zen.api.error.modelDisabled": "模型已停用",
"zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}",
"black.meta.title": "OpenCode Black | 存取全球最佳編碼模型", "black.meta.title": "OpenCode Black | 存取全球最佳編碼模型",
"black.meta.description": "透過 OpenCode Black 訂閱方案存取 Claude、GPT、Gemini 等模型。", "black.meta.description": "透過 OpenCode Black 訂閱方案存取 Claude、GPT、Gemini 等模型。",

View File

@ -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 }) logger.metric({ model: modelId })
return { id: modelId, ...modelData } return { id: modelId, ...modelData }

View File

@ -27,6 +27,7 @@ export namespace ZenData {
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(), byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
stickyProvider: z.enum(["strict", "prefer"]).optional(), stickyProvider: z.enum(["strict", "prefer"]).optional(),
trialProviders: z.array(z.string()).optional(), trialProviders: z.array(z.string()).optional(),
trialEnded: z.boolean().optional(),
fallbackProvider: z.string().optional(), fallbackProvider: z.string().optional(),
rateLimit: z.number().optional(), rateLimit: z.number().optional(),
providers: z.array( providers: z.array(

View File

@ -147,6 +147,7 @@
"tree-sitter-powershell": "0.25.10", "tree-sitter-powershell": "0.25.10",
"turndown": "7.2.0", "turndown": "7.2.0",
"ulid": "catalog:", "ulid": "catalog:",
"venice-ai-sdk-provider": "2.0.1",
"vscode-jsonrpc": "8.2.1", "vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10", "web-tree-sitter": "0.25.10",
"which": "6.0.1", "which": "6.0.1",

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
@ -48,6 +49,7 @@ await Bun.build({
external: ["jsonc-parser"], external: ["jsonc-parser"],
define: { define: {
OPENCODE_MIGRATIONS: JSON.stringify(migrations), OPENCODE_MIGRATIONS: JSON.stringify(migrations),
OPENCODE_CHANNEL: `'${Script.channel}'`,
}, },
}) })

View File

@ -120,6 +120,10 @@ class TokenRefreshRequest extends Schema.Class<TokenRefreshRequest>("TokenRefres
const clientId = "opencode-cli" const clientId = "opencode-cli"
const eagerRefreshThreshold = Duration.minutes(5) const eagerRefreshThreshold = Duration.minutes(5)
const eagerRefreshThresholdMs = Duration.toMillis(eagerRefreshThreshold)
const isTokenFresh = (tokenExpiry: number | null, now: number) =>
tokenExpiry != null && tokenExpiry > now + eagerRefreshThresholdMs
const mapAccountServiceError = const mapAccountServiceError =
(message = "Account service operation failed") => (message = "Account service operation failed") =>
@ -219,7 +223,7 @@ export namespace Account {
const account = maybeAccount.value const account = maybeAccount.value
const now = yield* Clock.currentTimeMillis const now = yield* Clock.currentTimeMillis
if (account.token_expiry && account.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) { if (isTokenFresh(account.token_expiry, now)) {
return account.access_token return account.access_token
} }
@ -229,7 +233,7 @@ export namespace Account {
const resolveToken = Effect.fnUntraced(function* (row: AccountRow) { const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
const now = yield* Clock.currentTimeMillis const now = yield* Clock.currentTimeMillis
if (row.token_expiry && row.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) { if (isTokenFresh(row.token_expiry, now)) {
return row.access_token return row.access_token
} }

View File

@ -12,3 +12,4 @@ Focus on information that would be helpful for continuing the conversation, incl
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood. Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
Do not respond to any questions in the conversation, only output the summary. Do not respond to any questions in the conversation, only output the summary.
Respond in the same language the user used in the conversation.

View File

@ -1,6 +1,7 @@
import z from "zod" import z from "zod"
import { EOL } from "os" import { EOL } from "os"
import { NamedError } from "@opencode-ai/util/error" import { NamedError } from "@opencode-ai/util/error"
import { logo as glyphs } from "./logo"
export namespace UI { export namespace UI {
const wordmark = [ const wordmark = [
@ -47,12 +48,60 @@ export namespace UI {
} }
export function logo(pad?: string) { export function logo(pad?: string) {
const result = [] if (!process.stdout.isTTY && !process.stderr.isTTY) {
for (const row of wordmark) { const result = []
if (pad) result.push(pad) for (const row of wordmark) {
result.push(row) if (pad) result.push(pad)
result.push(EOL) result.push(row)
result.push(EOL)
}
return result.join("").trimEnd()
} }
const result: string[] = []
const reset = "\x1b[0m"
const left = {
fg: "\x1b[90m",
shadow: "\x1b[38;5;235m",
bg: "\x1b[48;5;235m",
}
const right = {
fg: reset,
shadow: "\x1b[38;5;238m",
bg: "\x1b[48;5;238m",
}
const gap = " "
const draw = (line: string, fg: string, shadow: string, bg: string) => {
const parts: string[] = []
for (const char of line) {
if (char === "_") {
parts.push(bg, " ", reset)
continue
}
if (char === "^") {
parts.push(fg, bg, "▀", reset)
continue
}
if (char === "~") {
parts.push(shadow, "▀", reset)
continue
}
if (char === " ") {
parts.push(" ")
continue
}
parts.push(fg, char, reset)
}
return parts.join("")
}
glyphs.left.forEach((row, index) => {
if (pad) result.push(pad)
result.push(draw(row, left.fg, left.shadow, left.bg))
result.push(gap)
const other = glyphs.right[index] ?? ""
result.push(draw(other, right.fg, right.shadow, right.bg))
result.push(EOL)
})
return result.join("").trimEnd() return result.join("").trimEnd()
} }

View File

@ -48,7 +48,19 @@ process.on("uncaughtException", (e) => {
}) })
}) })
const cli = yargs(hideBin(process.argv)) const args = hideBin(process.argv)
function show(out: string) {
const text = out.trimStart()
if (!text.startsWith("opencode ")) {
process.stderr.write(UI.logo() + EOL + EOL)
process.stderr.write(text)
return
}
process.stderr.write(out)
}
const cli = yargs(args)
.parserConfiguration({ "populate--": true }) .parserConfiguration({ "populate--": true })
.scriptName("opencode") .scriptName("opencode")
.wrap(100) .wrap(100)
@ -130,7 +142,7 @@ const cli = yargs(hideBin(process.argv))
process.stderr.write("Database migration complete." + EOL) process.stderr.write("Database migration complete." + EOL)
} }
}) })
.usage("\n" + UI.logo()) .usage("")
.completion("completion", "generate shell completion script") .completion("completion", "generate shell completion script")
.command(AcpCommand) .command(AcpCommand)
.command(McpCommand) .command(McpCommand)
@ -162,7 +174,7 @@ const cli = yargs(hideBin(process.argv))
msg?.startsWith("Invalid values:") msg?.startsWith("Invalid values:")
) { ) {
if (err) throw err if (err) throw err
cli.showHelp("log") cli.showHelp(show)
} }
if (err) throw err if (err) throw err
process.exit(1) process.exit(1)
@ -170,7 +182,15 @@ const cli = yargs(hideBin(process.argv))
.strict() .strict()
try { try {
await cli.parse() if (args.includes("-h") || args.includes("--help")) {
await cli.parse(args, (err: Error | undefined, _argv: unknown, out: string) => {
if (err) throw err
if (!out) return
show(out)
})
} else {
await cli.parse()
}
} catch (e) { } catch (e) {
let data: Record<string, any> = {} let data: Record<string, any> = {}
if (e instanceof NamedError) { if (e instanceof NamedError) {

View File

@ -44,6 +44,7 @@ import { createGateway } from "@ai-sdk/gateway"
import { createTogetherAI } from "@ai-sdk/togetherai" import { createTogetherAI } from "@ai-sdk/togetherai"
import { createPerplexity } from "@ai-sdk/perplexity" import { createPerplexity } from "@ai-sdk/perplexity"
import { createVercel } from "@ai-sdk/vercel" import { createVercel } from "@ai-sdk/vercel"
import { createVenice } from "venice-ai-sdk-provider"
import { import {
createGitLab, createGitLab,
VERSION as GITLAB_PROVIDER_VERSION, VERSION as GITLAB_PROVIDER_VERSION,
@ -139,6 +140,7 @@ export namespace Provider {
"@ai-sdk/vercel": createVercel, "@ai-sdk/vercel": createVercel,
"gitlab-ai-provider": createGitLab, "gitlab-ai-provider": createGitLab,
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
"venice-ai-sdk-provider": createVenice,
} }
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any> type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>

View File

@ -190,6 +190,7 @@ export namespace SessionCompaction {
Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.
The summary that you construct will be used so that another agent can read it and continue the work. The summary that you construct will be used so that another agent can read it and continue the work.
Do not call any tools. Respond only with the summary text. Do not call any tools. Respond only with the summary text.
Respond in the same language as the user's messages in the conversation.
When constructing the summary, try to stick to this template: When constructing the summary, try to stick to this template:
--- ---

View File

@ -756,7 +756,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
} }
const model = input.model ?? agent.model ?? (yield* lastModel(input.sessionID)) const model = input.model ?? agent.model ?? (yield* lastModel(input.sessionID))
const userMsg: MessageV2.User = { const userMsg: MessageV2.User = {
id: MessageID.ascending(), id: input.messageID ?? MessageID.ascending(),
sessionID: input.sessionID, sessionID: input.sessionID,
time: { created: Date.now() }, time: { created: Date.now() },
role: "user", role: "user",
@ -1362,9 +1362,18 @@ NOTE: At any point in time through this workflow you should feel free to ask the
} }
if (!lastUser) throw new Error("No user message found in stream. This should never happen.") if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
const lastAssistantMsg = msgs.findLast(
(msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
)
// Some providers return "stop" even when the assistant message contains tool calls.
// Keep the loop running so tool results can be sent back to the model.
const hasToolCalls = lastAssistantMsg?.parts.some((part) => part.type === "tool") ?? false
if ( if (
lastAssistant?.finish && lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) && !["tool-calls"].includes(lastAssistant.finish) &&
!hasToolCalls &&
lastUser.id < lastAssistant.id lastUser.id < lastAssistant.id
) { ) {
log.info("exiting loop", { sessionID }) log.info("exiting loop", { sessionID })
@ -1818,6 +1827,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
export const ShellInput = z.object({ export const ShellInput = z.object({
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod.optional(),
agent: z.string(), agent: z.string(),
model: z model: z
.object({ .object({

View File

@ -54,7 +54,7 @@ export namespace SessionRetry {
if (MessageV2.APIError.isInstance(error)) { if (MessageV2.APIError.isInstance(error)) {
if (!error.data.isRetryable) return undefined if (!error.data.isRetryable) return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError")) if (error.data.responseBody?.includes("FreeUsageLimitError"))
return `Free usage exceeded, add credits https://opencode.ai/zen` return `Free usage exceeded, subscribe to Go https://opencode.ai/go`
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
} }

View File

@ -18,6 +18,9 @@ const truncate = Layer.effectDiscard(
const it = testEffect(Layer.merge(AccountRepo.layer, truncate)) const it = testEffect(Layer.merge(AccountRepo.layer, truncate))
const insideEagerRefreshWindow = Duration.toMillis(Duration.minutes(1))
const outsideEagerRefreshWindow = Duration.toMillis(Duration.minutes(10))
const live = (client: HttpClient.HttpClient) => const live = (client: HttpClient.HttpClient) =>
Account.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client))) Account.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client)))
@ -63,7 +66,7 @@ it.live("orgsByAccount groups orgs per account", () =>
url: "https://one.example.com", url: "https://one.example.com",
accessToken: AccessToken.make("at_1"), accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"), refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 10 * 60_000, expiry: Date.now() + outsideEagerRefreshWindow,
orgID: Option.none(), orgID: Option.none(),
}), }),
) )
@ -75,7 +78,7 @@ it.live("orgsByAccount groups orgs per account", () =>
url: "https://two.example.com", url: "https://two.example.com",
accessToken: AccessToken.make("at_2"), accessToken: AccessToken.make("at_2"),
refreshToken: RefreshToken.make("rt_2"), refreshToken: RefreshToken.make("rt_2"),
expiry: Date.now() + 10 * 60_000, expiry: Date.now() + outsideEagerRefreshWindow,
orgID: Option.none(), orgID: Option.none(),
}), }),
) )
@ -159,7 +162,7 @@ it.live("token refreshes before expiry when inside the eager refresh window", ()
url: "https://one.example.com", url: "https://one.example.com",
accessToken: AccessToken.make("at_old"), accessToken: AccessToken.make("at_old"),
refreshToken: RefreshToken.make("rt_old"), refreshToken: RefreshToken.make("rt_old"),
expiry: Date.now() + 60_000, expiry: Date.now() + insideEagerRefreshWindow,
orgID: Option.none(), orgID: Option.none(),
}), }),
) )
@ -267,7 +270,7 @@ it.live("config sends the selected org header", () =>
url: "https://one.example.com", url: "https://one.example.com",
accessToken: AccessToken.make("at_1"), accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"), refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 10 * 60_000, expiry: Date.now() + outsideEagerRefreshWindow,
orgID: Option.none(), orgID: Option.none(),
}), }),
) )

View File

@ -3,7 +3,6 @@ import { expect, spyOn } from "bun:test"
import { Cause, Effect, Exit, Fiber, Layer } from "effect" import { Cause, Effect, Exit, Fiber, Layer } from "effect"
import path from "path" import path from "path"
import z from "zod" import z from "zod"
import type { Agent } from "../../src/agent/agent"
import { Agent as AgentSvc } from "../../src/agent/agent" import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Command } from "../../src/command" import { Command } from "../../src/command"
@ -35,7 +34,7 @@ import { Log } from "../../src/util/log"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { TestLLMServer } from "../lib/llm-server" import { reply, TestLLMServer } from "../lib/llm-server"
Log.init({ print: false }) Log.init({ print: false })
@ -453,6 +452,36 @@ it.live("loop continues when finish is tool-calls", () =>
), ),
) )
it.live("loop continues when finish is stop but assistant has tool parts", () =>
provideTmpdirServer(
Effect.fnUntraced(function* ({ llm }) {
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const session = yield* sessions.create({
title: "Pinned",
permission: [{ permission: "*", pattern: "*", action: "allow" }],
})
yield* prompt.prompt({
sessionID: session.id,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "hello" }],
})
yield* llm.push(reply().tool("first", { value: "first" }).stop())
yield* llm.text("second")
const result = yield* prompt.loop({ sessionID: session.id })
expect(yield* llm.calls).toBe(2)
expect(result.info.role).toBe("assistant")
if (result.info.role === "assistant") {
expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
expect(result.info.finish).toBe("stop")
}
}),
{ git: true, config: providerCfg },
),
)
it.live("failed subtask preserves metadata on error tool state", () => it.live("failed subtask preserves metadata on error tool state", () =>
provideTmpdirServer( provideTmpdirServer(
Effect.fnUntraced(function* ({ llm }) { Effect.fnUntraced(function* ({ llm }) {

View File

@ -2231,6 +2231,7 @@ export class Session2 extends HeyApiClient {
sessionID: string sessionID: string
directory?: string directory?: string
workspace?: string workspace?: string
messageID?: string
agent?: string agent?: string
model?: { model?: {
providerID: string providerID: string
@ -2248,6 +2249,7 @@ export class Session2 extends HeyApiClient {
{ in: "path", key: "sessionID" }, { in: "path", key: "sessionID" },
{ in: "query", key: "directory" }, { in: "query", key: "directory" },
{ in: "query", key: "workspace" }, { in: "query", key: "workspace" },
{ in: "body", key: "messageID" },
{ in: "body", key: "agent" }, { in: "body", key: "agent" },
{ in: "body", key: "model" }, { in: "body", key: "model" },
{ in: "body", key: "command" }, { in: "body", key: "command" },

View File

@ -3815,6 +3815,7 @@ export type SessionCommandResponse = SessionCommandResponses[keyof SessionComman
export type SessionShellData = { export type SessionShellData = {
body?: { body?: {
messageID?: string
agent: string agent: string
model?: { model?: {
providerID: string providerID: string

View File

@ -3942,6 +3942,10 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"messageID": {
"type": "string",
"pattern": "^msg.*"
},
"agent": { "agent": {
"type": "string" "type": "string"
}, },

View File

@ -48,6 +48,7 @@
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3", "@solid-primitives/bounds": "0.1.3",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",

View File

@ -16,6 +16,7 @@ import {
} from "@pierre/diffs" } from "@pierre/diffs"
import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { createMediaQuery } from "@solid-primitives/media" 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 { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js"
import { createDefaultOptions, styleVariables } from "../pierre" import { createDefaultOptions, styleVariables } from "../pierre"
import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines" import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines"
@ -286,17 +287,10 @@ function useFileViewer(config: ViewerConfig) {
createEffect(() => { createEffect(() => {
if (!config.enableLineSelection()) return if (!config.enableLineSelection()) return
container.addEventListener("mousedown", handleMouseDown) makeEventListener(container, "mousedown", handleMouseDown)
container.addEventListener("mousemove", handleMouseMove) makeEventListener(container, "mousemove", handleMouseMove)
window.addEventListener("mouseup", handleMouseUp) makeEventListener(window, "mouseup", handleMouseUp)
document.addEventListener("selectionchange", handleSelectionChange) makeEventListener(document, "selectionchange", handleSelectionChange)
onCleanup(() => {
container.removeEventListener("mousedown", handleMouseDown)
container.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("mouseup", handleMouseUp)
document.removeEventListener("selectionchange", handleSelectionChange)
})
}) })
onCleanup(() => { onCleanup(() => {

View File

@ -294,11 +294,6 @@ export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
cancelDraft() cancelDraft()
} }
createEffect(() => {
props.commenting()
setDraft("")
})
return { return {
draft, draft,
setDraft, setDraft,

View File

@ -1,6 +1,6 @@
import { useFilteredList } from "@opencode-ai/ui/hooks" import { useFilteredList } from "@opencode-ai/ui/hooks"
import { getDirectory, getFilename } from "@opencode-ai/util/path" 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 { Button } from "./button"
import { FileIcon } from "./file-icon" import { FileIcon } from "./file-icon"
import { Icon } from "./icon" import { Icon } from "./icon"
@ -210,7 +210,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
const refs = { const refs = {
textarea: undefined as HTMLTextAreaElement | undefined, textarea: undefined as HTMLTextAreaElement | undefined,
} }
const [text, setText] = createSignal(split.value)
const [open, setOpen] = createSignal(false) const [open, setOpen] = createSignal(false)
function selectMention(item: { path: string } | undefined) { function selectMention(item: { path: string } | undefined) {
@ -220,10 +219,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
const query = currentMention() const query = currentMention()
if (!textarea || !query) return 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 const cursor = query.start + item.path.length + 2
setText(value)
split.onInput(value) split.onInput(value)
closeMention() closeMention()
@ -257,10 +255,6 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
fn() fn()
} }
createEffect(() => {
setText(split.value)
})
const closeMention = () => { const closeMention = () => {
setOpen(false) setOpen(false)
mention.clear() mention.clear()
@ -302,7 +296,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
} }
const submit = () => { const submit = () => {
const value = text().trim() const value = split.value.trim()
if (!value) return if (!value) return
split.onSubmit(value) split.onSubmit(value)
} }
@ -322,10 +316,9 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
data-slot="line-comment-textarea" data-slot="line-comment-textarea"
rows={split.rows ?? 3} rows={split.rows ?? 3}
placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")} placeholder={split.placeholder ?? i18n.t("ui.lineComment.placeholder")}
value={text()} value={split.value}
on:input={(e) => { on:input={(e) => {
const value = (e.currentTarget as HTMLTextAreaElement).value const value = (e.currentTarget as HTMLTextAreaElement).value
setText(value)
split.onInput(value) split.onInput(value)
syncMention() syncMention()
}} }}
@ -422,7 +415,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
type="button" type="button"
data-slot="line-comment-action" data-slot="line-comment-action"
data-variant="primary" data-variant="primary"
disabled={text().trim().length === 0} disabled={split.value.trim().length === 0}
on:mousedown={hold as any} on:mousedown={hold as any}
on:click={click(submit) as any} on:click={click(submit) as any}
> >
@ -434,7 +427,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
<Button size="small" variant="ghost" onClick={split.onCancel}> <Button size="small" variant="ghost" onClick={split.onCancel}>
{split.cancelLabel ?? i18n.t("ui.common.cancel")} {split.cancelLabel ?? i18n.t("ui.common.cancel")}
</Button> </Button>
<Button size="small" variant="primary" disabled={text().trim().length === 0} onClick={submit}> <Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}>
{split.submitLabel ?? i18n.t("ui.lineComment.submit")} {split.submitLabel ?? i18n.t("ui.lineComment.submit")}
</Button> </Button>
</Show> </Show>

View File

@ -1,6 +1,7 @@
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" 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 { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { Icon, type IconProps } from "./icon" import { Icon, type IconProps } from "./icon"
import { IconButton } from "./icon-button" import { IconButton } from "./icon-button"
@ -228,9 +229,8 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0) setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
} }
scroll.addEventListener("scroll", handler, { passive: true }) makeEventListener(scroll, "scroll", handler, { passive: true })
handler() handler()
onCleanup(() => scroll.removeEventListener("scroll", handler))
}) })
return ( return (

View File

@ -230,6 +230,19 @@ function createPacedValue(getValue: () => string, live?: () => boolean) {
return value return value
} }
function PacedMarkdown(props: { text: string; cacheKey: string; streaming: boolean }) {
const value = createPacedValue(
() => props.text,
() => props.streaming,
)
return (
<Show when={value()}>
<Markdown text={value()} cacheKey={props.cacheKey} streaming={props.streaming} />
</Show>
)
}
function relativizeProjectPath(path: string, directory?: string) { function relativizeProjectPath(path: string, directory?: string) {
if (!path) return "" if (!path) return ""
if (!directory) return path if (!directory) return path
@ -1373,8 +1386,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
const streaming = createMemo( const streaming = createMemo(
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
) )
const displayText = () => (part().text ?? "").trim() const text = () => (part().text ?? "").trim()
const throttledText = createPacedValue(displayText, streaming)
const isLastTextPart = createMemo(() => { const isLastTextPart = createMemo(() => {
const last = (data.store.part?.[props.message.id] ?? []) const last = (data.store.part?.[props.message.id] ?? [])
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim()) .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 [copied, setCopied] = createSignal(false)
const handleCopy = async () => { const handleCopy = async () => {
const content = displayText() const content = text()
if (!content) return if (!content) return
await navigator.clipboard.writeText(content) await navigator.clipboard.writeText(content)
setCopied(true) setCopied(true)
@ -1398,10 +1410,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
} }
return ( return (
<Show when={throttledText()}> <Show when={text()}>
<div data-component="text-part"> <div data-component="text-part">
<div data-slot="text-part-body"> <div data-slot="text-part-body">
<Markdown text={throttledText()} cacheKey={part().id} streaming={streaming()} /> <Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
</Show>
</div> </div>
<Show when={showCopy()}> <Show when={showCopy()}>
<div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}> <div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}>
@ -1437,12 +1451,13 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
) )
const text = () => part().text.trim() const text = () => part().text.trim()
const throttledText = createPacedValue(text, streaming)
return ( return (
<Show when={throttledText()}> <Show when={text()}>
<div data-component="reasoning-part"> <div data-component="reasoning-part">
<Markdown text={throttledText()} cacheKey={part().id} streaming={streaming()} /> <Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
</Show>
</div> </div>
</Show> </Show>
) )

View File

@ -1,15 +1,7 @@
import { Popover as Kobalte } from "@kobalte/core/popover" import { Popover as Kobalte } from "@kobalte/core/popover"
import { import { ComponentProps, JSXElement, ParentProps, Show, createEffect, splitProps, ValidComponent } from "solid-js"
ComponentProps,
JSXElement,
ParentProps,
Show,
createEffect,
onCleanup,
splitProps,
ValidComponent,
} from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { IconButton } from "./icon-button" import { IconButton } from "./icon-button"
@ -104,15 +96,9 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
close("outside") close("outside")
} }
window.addEventListener("keydown", onKeyDown, true) makeEventListener(window, "keydown", onKeyDown, { capture: true })
window.addEventListener("pointerdown", onPointerDown, true) makeEventListener(window, "pointerdown", onPointerDown, { capture: true })
window.addEventListener("focusin", onFocusIn, true) makeEventListener(window, "focusin", onFocusIn, { capture: true })
onCleanup(() => {
window.removeEventListener("keydown", onKeyDown, true)
window.removeEventListener("pointerdown", onPointerDown, true)
window.removeEventListener("focusin", onFocusIn, true)
})
}) })
const content = () => ( const content = () => (

View File

@ -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 { createStore } from "solid-js/store"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
@ -97,19 +98,7 @@ export function ScrollView(props: ScrollViewProps) {
local.viewportRef(viewportRef) local.viewportRef(viewportRef)
} }
const observer = new ResizeObserver(() => { createResizeObserver([viewportRef, viewportRef.firstElementChild], updateThumb)
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()
})
updateThumb() updateThumb()
}) })

View File

@ -343,14 +343,12 @@ export function SessionTurn(
}) })
const assistantDerived = createMemo(() => { const assistantDerived = createMemo(() => {
let visible = 0 let visible = 0
let tail: "text" | "other" | undefined
let reason: string | undefined let reason: string | undefined
const show = showReasoningSummaries() const show = showReasoningSummaries()
for (const message of assistantMessages()) { for (const message of assistantMessages()) {
for (const part of list(data.store.part?.[message.id], emptyParts)) { for (const part of list(data.store.part?.[message.id], emptyParts)) {
if (partState(part, show) === "visible") { if (partState(part, show) === "visible") {
visible++ visible++
tail = part.type === "text" ? "text" : "other"
} }
if (part.type === "reasoning" && part.text) { if (part.type === "reasoning" && part.text) {
const h = heading(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 assistantVisible = createMemo(() => assistantDerived().visible)
const assistantTailVisible = createMemo(() => assistantDerived().tail)
const reasoningHeading = createMemo(() => assistantDerived().reason) const reasoningHeading = createMemo(() => assistantDerived().reason)
const showThinking = createMemo(() => { const showThinking = createMemo(() => {
if (!working() || !!error()) return false if (!working() || !!error()) return false

View File

@ -1,5 +1,6 @@
// @ts-nocheck // @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 { createStore } from "solid-js/store"
import { useSpring } from "./motion-spring" import { useSpring } from "./motion-spring"
import { TextStrikethrough } from "./text-strikethrough" import { TextStrikethrough } from "./text-strikethrough"
@ -144,13 +145,7 @@ function VariantF(props: { active: boolean; text: string }) {
} }
onMount(measure) onMount(measure)
createEffect(() => { createResizeObserver(() => containerRef, measure)
const el = containerRef
if (!el) return
const observer = new ResizeObserver(measure)
observer.observe(el)
onCleanup(() => observer.disconnect())
})
const clipRight = () => { const clipRight = () => {
const cw = containerWidth() const cw = containerWidth()

View File

@ -1,5 +1,6 @@
import type { JSX } from "solid-js" 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 { createStore } from "solid-js/store"
import { useSpring } from "./motion-spring" import { useSpring } from "./motion-spring"
@ -33,14 +34,7 @@ export function TextStrikethrough(props: {
} }
onMount(measure) onMount(measure)
createResizeObserver(() => containerRef, measure)
createEffect(() => {
const el = containerRef
if (!el) return
const observer = new ResizeObserver(measure)
observer.observe(el)
onCleanup(() => observer.disconnect())
})
// Revealed pixels from left = progress * textWidth // Revealed pixels from left = progress * textWidth
const revealedPx = () => { const revealedPx = () => {

View File

@ -12,6 +12,7 @@ import {
type JSX, type JSX,
} from "solid-js" } from "solid-js"
import { Dialog as Kobalte } from "@kobalte/core/dialog" import { Dialog as Kobalte } from "@kobalte/core/dialog"
import { makeEventListener } from "@solid-primitives/event-listener"
type DialogElement = () => JSX.Element type DialogElement = () => JSX.Element
@ -68,8 +69,7 @@ function init() {
event.stopPropagation() event.stopPropagation()
} }
window.addEventListener("keydown", onKeyDown, true) makeEventListener(window, "keydown", onKeyDown, { capture: true })
onCleanup(() => window.removeEventListener("keydown", onKeyDown, true))
}) })
const show = (element: DialogElement, owner: Owner, onClose?: () => void) => { const show = (element: DialogElement, owner: Owner, onClose?: () => void) => {

View File

@ -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 { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
export interface AutoScrollOptions { export interface AutoScrollOptions {
@ -14,7 +15,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
let settling = false let settling = false
let settleTimer: ReturnType<typeof setTimeout> | undefined let settleTimer: ReturnType<typeof setTimeout> | undefined
let autoTimer: ReturnType<typeof setTimeout> | undefined let autoTimer: ReturnType<typeof setTimeout> | undefined
let cleanup: (() => void) | undefined
let auto: { top: number; time: number } | undefined let auto: { top: number; time: number } | undefined
const threshold = () => options.bottomThreshold ?? 10 const threshold = () => options.bottomThreshold ?? 10
@ -216,26 +216,14 @@ export function createAutoScroll(options: AutoScrollOptions) {
onCleanup(() => { onCleanup(() => {
if (settleTimer) clearTimeout(settleTimer) if (settleTimer) clearTimeout(settleTimer)
if (autoTimer) clearTimeout(autoTimer) if (autoTimer) clearTimeout(autoTimer)
if (cleanup) cleanup()
}) })
return { return {
scrollRef: (el: HTMLElement | undefined) => { scrollRef: (el: HTMLElement | undefined) => {
if (cleanup) {
cleanup()
cleanup = undefined
}
scroll = el
if (!el) return if (!el) return
updateOverflowAnchor(el) updateOverflowAnchor(el)
el.addEventListener("wheel", handleWheel, { passive: true }) makeEventListener(el, "wheel", handleWheel, { passive: true })
cleanup = () => {
el.removeEventListener("wheel", handleWheel)
}
}, },
contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el), contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el),
handleScroll, handleScroll,

View File

@ -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" import { createStore } from "solid-js/store"
export type FindHost = { export type FindHost = {
@ -104,9 +106,9 @@ type CreateFileFindOptions = {
export function createFileFind(opts: CreateFileFindOptions) { export function createFileFind(opts: CreateFileFindOptions) {
let input: HTMLInputElement | undefined let input: HTMLInputElement | undefined
let overlayFrame: number | undefined let overlayFrame: number | undefined
let overlayScroll: HTMLElement[] = []
let mode: "highlights" | "overlay" = "overlay" let mode: "highlights" | "overlay" = "overlay"
let hits: Range[] = [] let hits: Range[] = []
const [overlayScroll, setOverlayScroll] = createSignal<HTMLElement[]>([])
const [state, setState] = createStore({ const [state, setState] = createStore({
open: false, open: false,
@ -122,8 +124,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
const pos = () => state.pos const pos = () => state.pos
const clearOverlayScroll = () => { const clearOverlayScroll = () => {
for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay) setOverlayScroll([])
overlayScroll = []
} }
const clearOverlay = () => { const clearOverlay = () => {
@ -196,11 +197,11 @@ export function createFileFind(opts: CreateFileFindOptions) {
(node): node is HTMLElement => node instanceof HTMLElement, (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() clearOverlayScroll()
overlayScroll = next setOverlayScroll(next)
for (const el of overlayScroll) el.addEventListener("scroll", scheduleOverlay, { passive: true })
} }
const clearFind = () => { const clearFind = () => {
@ -403,6 +404,10 @@ export function createFileFind(opts: CreateFileFindOptions) {
close, close,
} }
createEffect(() => {
for (const el of overlayScroll()) makeEventListener(el, "scroll", scheduleOverlay, { passive: true })
})
onMount(() => { onMount(() => {
mode = supportsHighlights() ? "highlights" : "overlay" mode = supportsHighlights() ? "highlights" : "overlay"
installShortcuts() installShortcuts()
@ -424,18 +429,12 @@ export function createFileFind(opts: CreateFileFindOptions) {
const update = () => positionBar() const update = () => positionBar()
requestAnimationFrame(update) requestAnimationFrame(update)
window.addEventListener("resize", update, { passive: true }) makeEventListener(window, "resize", update, { passive: true })
const wrapper = opts.wrapper() const wrapper = opts.wrapper()
if (!wrapper) return if (!wrapper) return
const root = scrollParent(wrapper) ?? wrapper const root = scrollParent(wrapper) ?? wrapper
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => update()) createResizeObserver(root, update)
observer?.observe(root)
onCleanup(() => {
window.removeEventListener("resize", update)
observer?.disconnect()
})
}) })
onCleanup(() => { onCleanup(() => {

View File

@ -1,5 +1,6 @@
import { createEffect, onCleanup, onMount } from "solid-js" import { createEffect, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createSimpleContext } from "../context/helper" import { createSimpleContext } from "../context/helper"
import oc2ThemeJson from "./themes/oc-2.json" import oc2ThemeJson from "./themes/oc-2.json"
import { resolveThemeVariant, themeToCss } from "./resolve" 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(() => { onMount(() => {
makeEventListener(window, "storage", onStorage)
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const onMedia = () => { const onMedia = () => {
if (store.colorScheme !== "system") return if (store.colorScheme !== "system") return
setStore("mode", getSystemMode()) setStore("mode", getSystemMode())
} }
mediaQuery.addEventListener("change", onMedia) makeEventListener(mediaQuery, "change", onMedia)
onCleanup(() => mediaQuery.removeEventListener("change", onMedia))
const rawTheme = read(STORAGE_KEYS.THEME_ID) const rawTheme = read(STORAGE_KEYS.THEME_ID)
const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2" const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2"

View File

@ -18,6 +18,7 @@
"@astrojs/starlight": "0.34.3", "@astrojs/starlight": "0.34.3",
"@fontsource/ibm-plex-mono": "5.2.5", "@fontsource/ibm-plex-mono": "5.2.5",
"@shikijs/transformers": "3.20.0", "@shikijs/transformers": "3.20.0",
"@solid-primitives/resize-observer": "2.1.5",
"@types/luxon": "catalog:", "@types/luxon": "catalog:",
"ai": "catalog:", "ai": "catalog:",
"astro": "5.7.13", "astro": "5.7.13",

View File

@ -366,21 +366,13 @@ export default function Share(props: {
<Suspense> <Suspense>
<For each={filteredParts()}> <For each={filteredParts()}>
{(part, partIndex) => { {(part, partIndex) => {
const last = createMemo( const last = () =>
() => data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1
data().messages.length === msgIndex() + 1 &&
filteredParts().length === partIndex() + 1,
)
onMount(() => { onMount(() => {
const hash = window.location.hash.slice(1) const hash = window.location.hash.slice(1)
// Wait till all parts are loaded // Wait till all parts are loaded
if ( if (hash !== "" && !hasScrolledToAnchor && last()) {
hash !== "" &&
!hasScrolledToAnchor &&
filteredParts().length === partIndex() + 1 &&
data().messages.length === msgIndex() + 1
) {
hasScrolledToAnchor = true hasScrolledToAnchor = true
scrollToAnchor(hash) scrollToAnchor(hash)
} }

View File

@ -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 type { JSX } from "solid-js/jsx-runtime"
import { makeResizeObserver } from "@solid-primitives/resize-observer"
import { IconCheckCircle, IconHashtag } from "../icons" import { IconCheckCircle, IconHashtag } from "../icons"
export type ShareMessages = { locale: string } & Record<string, string> export type ShareMessages = { locale: string } & Record<string, string>
@ -83,17 +84,14 @@ export function createOverflow() {
return overflow() return overflow()
}, },
ref(el: HTMLElement) { ref(el: HTMLElement) {
const ro = new ResizeObserver(() => { const sync = () => {
if (el.scrollHeight > el.clientHeight + 1) { setOverflow(el.scrollHeight > el.clientHeight + 1)
setOverflow(true) }
}
return
})
ro.observe(el)
onCleanup(() => { const obs = makeResizeObserver(sync)
ro.disconnect() obs.observe(el)
})
sync()
}, },
} }
} }

View File

@ -1,5 +1,5 @@
import { parsePatch } from "diff" import { parsePatch } from "diff"
import { createMemo } from "solid-js" import { createMemo, For } from "solid-js"
import { ContentCode } from "./content-code" import { ContentCode } from "./content-code"
import styles from "./content-diff.module.css" import styles from "./content-diff.module.css"
@ -160,28 +160,37 @@ export function ContentDiff(props: Props) {
return ( return (
<div class={styles.root}> <div class={styles.root}>
<div data-component="desktop"> <div data-component="desktop">
{rows().map((r) => ( <For each={rows()}>
<div data-component="diff-row" data-type={r.type}> {(row) => (
<div data-slot="before" data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}> <div data-component="diff-row" data-type={row.type}>
<ContentCode code={r.left} flush lang={props.lang} /> <div
data-slot="before"
data-diff-type={row.type === "removed" || row.type === "modified" ? "removed" : ""}
>
<ContentCode code={row.left} flush lang={props.lang} />
</div>
<div data-slot="after" data-diff-type={row.type === "added" || row.type === "modified" ? "added" : ""}>
<ContentCode code={row.right} lang={props.lang} flush />
</div>
</div> </div>
<div data-slot="after" data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}> )}
<ContentCode code={r.right} lang={props.lang} flush /> </For>
</div>
</div>
))}
</div> </div>
<div data-component="mobile"> <div data-component="mobile">
{mobileRows().map((block) => ( <For each={mobileRows()}>
<div data-component="diff-block" data-type={block.type}> {(block) => (
{block.lines.map((line) => ( <div data-component="diff-block" data-type={block.type}>
<div data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}> <For each={block.lines}>
<ContentCode code={line} lang={props.lang} flush /> {(line) => (
</div> <div data-diff-type={block.type === "removed" ? "removed" : block.type === "added" ? "added" : ""}>
))} <ContentCode code={line} lang={props.lang} flush />
</div> </div>
))} )}
</For>
</div>
)}
</For>
</div> </div>
</div> </div>
) )