diff options
35 files changed, 339 insertions, 144 deletions
diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 68097c8d9..ce4a6658b 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -1,9 +1,9 @@ { "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-openai-codex-auth"], - "enterprise": { - "url": "https://enterprise.dev.opencode.ai", - }, + // "enterprise": { + // "url": "https://enterprise.dev.opencode.ai", + // }, "provider": { "opencode": { "options": { @@ -157,3 +157,4 @@ | 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) | | 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) | | 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) | +| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) | @@ -20,7 +20,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -48,7 +48,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -75,7 +75,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -99,7 +99,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -123,7 +123,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -164,7 +164,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -192,7 +192,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -208,7 +208,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.126", + "version": "1.0.127", "bin": { "opencode": "./bin/opencode", }, @@ -297,7 +297,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -317,7 +317,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.126", + "version": "1.0.127", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -328,7 +328,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -341,7 +341,7 @@ }, "packages/tauri": { "name": "@opencode-ai/tauri", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", @@ -354,7 +354,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -386,7 +386,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "zod": "catalog:", }, @@ -397,7 +397,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -443,7 +443,7 @@ "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.5.7", + "@pierre/precision-diffs": "0.6.0-beta.3", "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", @@ -1218,7 +1218,7 @@ "@petamoriken/float16": ["@petamoriken/[email protected]", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], - "@pierre/precision-diffs": ["@pierre/[email protected]", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-Y+e4kJ9pT2I4NS5fE39KdoiXtwMkVPRvrwLM6O2IqO7PDCRWLBS7CYxcSgSyngEndccUll2krx66I2QnfO0Ovg=="], + "@pierre/precision-diffs": ["@pierre/[email protected]", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-1FBm9jhLWZvs7BqN3yG2Wh9SpGuO1us2QsKZlQqSwyCctMr9DRGzYQJ9lF6yR03LHzXs3fuIzO++d9sCObYzrQ=="], "@pkgjs/parseargs": ["@pkgjs/[email protected]", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], diff --git a/flake.lock b/flake.lock index 107fb6a00..2ef89f661 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764587062, - "narHash": "sha256-hdFa0TAVQAQLDF31cEW3enWmBP+b592OvHs6WVe3D8k=", + "lastModified": 1764611609, + "narHash": "sha256-yU9BNcP0oadUKupw0UKmO9BKDOVIg9NStdJosEbXf8U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c1cb7d097cb250f6e1904aacd5f2ba5ffd8a49ce", + "rev": "8c29968b3a942f2903f90797f9623737c215737c", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index 734e47928..e34d21200 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-9BfJ3dFq/UYyhsnK3Sfx6rb6CT8bCvFOFOqD2+W1WQE=" + "nodeModules": "sha256-HyH219sZn4gOPyVg/bij7K3mfZ0MnBSM/7NmsOyrD4o=" } diff --git a/package.json b/package.json index e85f08e4e..a5e7c1462 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.5.7", + "@pierre/precision-diffs": "0.6.0-beta.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "ai": "5.0.97", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 500129094..ade902b71 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.126", + "version": "1.0.127", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 25cf96616..8e1e690b2 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.0.126", + "version": "1.0.127", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index dc474827b..7a7f5fa70 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.126", + "version": "1.0.127", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 423f23da3..f58563edf 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.126", + "version": "1.0.127", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index e47e27af7..0e84c10a7 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "1.0.126", + "version": "1.0.127", "description": "", "type": "module", "scripts": { diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 281b6765a..ddc2a60c5 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -30,6 +30,7 @@ import { useSync } from "@/context/sync" import { useSession } from "@/context/session" import { useLayout } from "@/context/layout" import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { Diff } from "@opencode-ai/ui/diff" export default function Page() { const layout = useLayout() @@ -357,6 +358,7 @@ export default function Page() { content: "pb-20", container: "w-full " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"), }} + diffComponent={Diff} /> </div> </Match> @@ -405,6 +407,7 @@ export default function Page() { container: "px-6", }} diffs={session.diffs()} + diffComponent={Diff} actions={ <Tooltip value="Open in tab"> <IconButton @@ -436,6 +439,7 @@ export default function Page() { container: "px-6", }} diffs={session.diffs()} + diffComponent={Diff} split /> </div> diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 982d95499..d615d3dab 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.126", + "version": "1.0.127", "private": true, "type": "module", "scripts": { diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index b95004749..f579bfa03 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -18,6 +18,10 @@ import z from "zod" import NotFound from "../[...404]" import { Tabs } from "@opencode-ai/ui/tabs" import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr" +import { Diff } from "@opencode-ai/ui/diff-ssr" +import { clientOnly } from "@solidjs/start" + +const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff }))) const SessionDataMissingError = NamedError.create( "SessionDataMissingError", @@ -230,6 +234,7 @@ export default function () { "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", container: "px-4", }} + diffComponent={ClientOnlyDiff} /> )} </For> @@ -299,6 +304,7 @@ export default function () { content: "flex flex-col justify-between items-start", container: "w-full pb-20 " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"), }} + diffComponent={ClientOnlyDiff} > <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}> <Logo class="w-58.5 opacity-12" /> @@ -311,6 +317,7 @@ export default function () { <SessionReview class="@4xl:hidden" diffs={diffs()} + diffComponent={Diff} classes={{ root: "pb-20", header: "px-6", @@ -318,9 +325,10 @@ export default function () { }} /> <SessionReview - class="hidden @4xl:flex" split + class="hidden @4xl:flex" diffs={splitDiffs()} + diffComponent={Diff} classes={{ root: "pb-20", header: "px-6", @@ -352,6 +360,7 @@ export default function () { <div class="relative h-full pt-8 overflow-y-auto no-scrollbar"> <SessionReview diffs={diffs()} + diffComponent={Diff} classes={{ root: "pb-20", header: "px-4", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 577943242..2b15573df 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The AI coding agent built for the terminal" -version = "1.0.126" +version = "1.0.127" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.127/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.127/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-linux-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.127/opencode-linux-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-linux-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.127/opencode-linux-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.126/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.127/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 46053e5bf..5aa956ac1 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.126", + "version": "1.0.127", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8652b0d64..d2580661f 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.126", + "version": "1.0.127", "name": "opencode", "type": "module", "private": true, diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 88b9616b0..ed77e04b4 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -169,6 +169,9 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { throw new Error(`Color reference "${c}" not found in defs or theme`) } } + if (typeof c === "number") { + return ansiToRgba(c) + } return resolveColor(c[mode]) } @@ -203,6 +206,51 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { } as Theme } +function ansiToRgba(code: number): RGBA { + // Standard ANSI colors (0-15) + if (code < 16) { + const ansiColors = [ + "#000000", // Black + "#800000", // Red + "#008000", // Green + "#808000", // Yellow + "#000080", // Blue + "#800080", // Magenta + "#008080", // Cyan + "#c0c0c0", // White + "#808080", // Bright Black + "#ff0000", // Bright Red + "#00ff00", // Bright Green + "#ffff00", // Bright Yellow + "#0000ff", // Bright Blue + "#ff00ff", // Bright Magenta + "#00ffff", // Bright Cyan + "#ffffff", // Bright White + ] + return RGBA.fromHex(ansiColors[code] ?? "#000000") + } + + // 6x6x6 Color Cube (16-231) + if (code < 232) { + const index = code - 16 + const b = index % 6 + const g = Math.floor(index / 6) % 6 + const r = Math.floor(index / 36) + + const val = (x: number) => (x === 0 ? 0 : x * 40 + 55) + return RGBA.fromInts(val(r), val(g), val(b)) + } + + // Grayscale Ramp (232-255) + if (code < 256) { + const gray = (code - 232) * 10 + 8 + return RGBA.fromInts(gray, gray, gray) + } + + // Fallback for invalid codes + return RGBA.fromInts(0, 0, 0) +} + export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ name: "Theme", init: (props: { mode: "dark" | "light" }) => { diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 84691617c..58232a539 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.0.126", + "version": "1.0.127", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 2ea5d4cc8..168159934 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.0.126", + "version": "1.0.127", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/slack/package.json b/packages/slack/package.json index fb995a0f6..8ae877164 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.126", + "version": "1.0.127", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/tauri/package.json b/packages/tauri/package.json index 2233f6fca..dccecada5 100644 --- a/packages/tauri/package.json +++ b/packages/tauri/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/tauri", "private": true, - "version": "1.0.126", + "version": "1.0.127", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/ui/package.json b/packages/ui/package.json index 5a6da5268..8d54d752b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,10 +1,10 @@ { "name": "@opencode-ai/ui", - "version": "1.0.126", + "version": "1.0.127", "type": "module", "exports": { "./*": "./src/components/*.tsx", - "./pierre": "./src/components/pierre.ts", + "./pierre": "./src/pierre/index.ts", "./hooks": "./src/hooks/index.ts", "./context": "./src/context/index.ts", "./context/*": "./src/context/*.tsx", diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index 788baf549..b4b772816 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -1,6 +1,22 @@ import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/precision-diffs" import { ComponentProps, createEffect, splitProps } from "solid-js" -import { createDefaultOptions, styleVariables } from "./pierre" +import { createDefaultOptions, styleVariables } from "../pierre" +import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker" +import { workerFactory } from "../pierre/worker" + +const workerPool = getOrCreateWorkerPoolSingleton({ + poolOptions: { + workerFactory, + // poolSize defaults to 8. More workers = more parallelism but + // also more memory. Too many can actually slow things down. + // poolSize: 8, + }, + highlighterOptions: { + theme: "OpenCode", + // Optionally preload languages to avoid lazy-loading delays + // langs: ["typescript", "javascript", "css", "html"], + }, +}) export type CodeProps<T = {}> = FileOptions<T> & { file: FileContents @@ -14,10 +30,13 @@ export function Code<T>(props: CodeProps<T>) { const [local, others] = splitProps(props, ["file", "class", "classList", "annotations"]) createEffect(() => { - const instance = new File<T>({ - ...createDefaultOptions<T>("unified"), - ...others, - }) + const instance = new File<T>( + { + ...createDefaultOptions<T>("unified"), + ...others, + }, + workerPool, + ) container.innerHTML = "" instance.render({ diff --git a/packages/ui/src/components/diff-ssr.tsx b/packages/ui/src/components/diff-ssr.tsx new file mode 100644 index 000000000..800aa3730 --- /dev/null +++ b/packages/ui/src/components/diff-ssr.tsx @@ -0,0 +1,75 @@ +import { FileDiff } from "@pierre/precision-diffs" +import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr" +import { onCleanup, onMount, Show, splitProps } from "solid-js" +import { isServer } from "solid-js/web" +import { createDefaultOptions, styleVariables, type DiffProps } from "../pierre" + +export type SSRDiffProps<T = {}> = DiffProps<T> & { + preloadedDiff: PreloadMultiFileDiffResult<T> +} + +export function Diff<T>(props: SSRDiffProps<T>) { + let container!: HTMLDivElement + let fileDiffRef!: HTMLElement + const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"]) + + let fileDiffInstance: FileDiff<T> | undefined + const cleanupFunctions: Array<() => void> = [] + + onMount(() => { + if (isServer || !props.preloadedDiff) return + fileDiffInstance = new FileDiff<T>({ + ...createDefaultOptions(props.diffStyle), + ...others, + ...props.preloadedDiff, + }) + // @ts-expect-error - fileContainer is private but needed for SSR hydration + fileDiffInstance.fileContainer = fileDiffRef + fileDiffInstance.hydrate({ + oldFile: local.before, + newFile: local.after, + lineAnnotations: local.annotations, + fileContainer: fileDiffRef, + containerWrapper: container, + }) + + // Hydrate annotation slots with interactive SolidJS components + // if (props.annotations.length > 0 && props.renderAnnotation != null) { + // for (const annotation of props.annotations) { + // const slotName = `annotation-${annotation.side}-${annotation.lineNumber}`; + // const slotElement = fileDiffRef.querySelector( + // `[slot="${slotName}"]` + // ) as HTMLElement; + // + // if (slotElement != null) { + // // Clear the static server-rendered content from the slot + // slotElement.innerHTML = ''; + // + // // Mount a fresh SolidJS component into this slot using render(). + // // This enables full SolidJS reactivity (signals, effects, etc.) + // const dispose = render( + // () => props.renderAnnotation!(annotation), + // slotElement + // ); + // cleanupFunctions.push(dispose); + // } + // } + // } + }) + + onCleanup(() => { + // Clean up FileDiff event handlers and dispose SolidJS components + fileDiffInstance?.cleanUp() + cleanupFunctions.forEach((dispose) => dispose()) + }) + + return ( + <div data-component="diff" style={styleVariables} ref={container}> + <file-diff ref={fileDiffRef} id="ssr-diff"> + <Show when={isServer}> + <template shadowrootmode="open" innerHTML={props.preloadedDiff.prerenderedHTML} /> + </Show> + </file-diff> + </div> + ) +} diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx index bd2134515..8e19c3172 100644 --- a/packages/ui/src/components/diff.tsx +++ b/packages/ui/src/components/diff.tsx @@ -1,17 +1,22 @@ -import { type FileContents, FileDiff, type DiffLineAnnotation, FileDiffOptions } from "@pierre/precision-diffs" -import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr" -import { ComponentProps, createEffect, onCleanup, onMount, Show, splitProps } from "solid-js" -import { isServer } from "solid-js/web" -import { createDefaultOptions, styleVariables } from "./pierre" - -export type DiffProps<T = {}> = FileDiffOptions<T> & { - preloadedDiff?: PreloadMultiFileDiffResult<T> - before: FileContents - after: FileContents - annotations?: DiffLineAnnotation<T>[] - class?: string - classList?: ComponentProps<"div">["classList"] -} +import { FileDiff } from "@pierre/precision-diffs" +import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker" +import { createEffect, onCleanup, splitProps } from "solid-js" +import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre" +import { workerFactory } from "../pierre/worker" + +const workerPool = getOrCreateWorkerPoolSingleton({ + poolOptions: { + workerFactory, + // poolSize defaults to 8. More workers = more parallelism but + // also more memory. Too many can actually slow things down. + // poolSize: 8, + }, + highlighterOptions: { + theme: "OpenCode", + // Optionally preload languages to avoid lazy-loading delays + // langs: ["typescript", "javascript", "css", "html"], + }, +}) // interface ThreadMetadata { // threadId: string @@ -21,21 +26,21 @@ export type DiffProps<T = {}> = FileDiffOptions<T> & { export function Diff<T>(props: DiffProps<T>) { let container!: HTMLDivElement - let fileDiffRef!: HTMLElement const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"]) let fileDiffInstance: FileDiff<T> | undefined const cleanupFunctions: Array<() => void> = [] createEffect(() => { - if (props.preloadedDiff) return container.innerHTML = "" if (!fileDiffInstance) { - fileDiffInstance = new FileDiff<T>({ - ...createDefaultOptions(props.diffStyle), - ...others, - ...(props.preloadedDiff ?? {}), - }) + fileDiffInstance = new FileDiff<T>( + { + ...createDefaultOptions(props.diffStyle), + ...others, + }, + workerPool, + ) } fileDiffInstance.render({ oldFile: local.before, @@ -45,60 +50,11 @@ export function Diff<T>(props: DiffProps<T>) { }) }) - onMount(() => { - if (isServer || !props.preloadedDiff) return - fileDiffInstance = new FileDiff<T>({ - ...createDefaultOptions(props.diffStyle), - ...others, - ...(props.preloadedDiff ?? {}), - }) - // @ts-expect-error - fileContainer is private but needed for SSR hydration - fileDiffInstance.fileContainer = fileDiffRef - fileDiffInstance.hydrate({ - oldFile: local.before, - newFile: local.after, - lineAnnotations: local.annotations, - fileContainer: fileDiffRef, - containerWrapper: container, - }) - - // Hydrate annotation slots with interactive SolidJS components - // if (props.annotations.length > 0 && props.renderAnnotation != null) { - // for (const annotation of props.annotations) { - // const slotName = `annotation-${annotation.side}-${annotation.lineNumber}`; - // const slotElement = fileDiffRef.querySelector( - // `[slot="${slotName}"]` - // ) as HTMLElement; - // - // if (slotElement != null) { - // // Clear the static server-rendered content from the slot - // slotElement.innerHTML = ''; - // - // // Mount a fresh SolidJS component into this slot using render(). - // // This enables full SolidJS reactivity (signals, effects, etc.) - // const dispose = render( - // () => props.renderAnnotation!(annotation), - // slotElement - // ); - // cleanupFunctions.push(dispose); - // } - // } - // } - }) - onCleanup(() => { // Clean up FileDiff event handlers and dispose SolidJS components fileDiffInstance?.cleanUp() cleanupFunctions.forEach((dispose) => dispose()) }) - return ( - <div data-component="diff" style={styleVariables} ref={container}> - <file-diff ref={fileDiffRef} id="ssr-diff"> - <Show when={isServer && props.preloadedDiff}> - {(preloadedDiff) => <template shadowrootmode="open" innerHTML={preloadedDiff().prerenderedHTML} />} - </Show> - </file-diff> - </div> - ) + return <div data-component="diff" style={styleVariables} ref={container} /> } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 807e56db0..a0e6e91b6 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1,4 +1,4 @@ -import { Component, createMemo, For, Match, Show, Switch } from "solid-js" +import { Component, createMemo, For, Match, Show, Switch, ValidComponent } from "solid-js" import { Dynamic } from "solid-js/web" import { AssistantMessage, @@ -13,7 +13,6 @@ import { GenericTool } from "./basic-tool" import { Card } from "./card" import { Icon } from "./icon" import { Checkbox } from "./checkbox" -import { Diff } from "./diff" import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" import { getDirectory, getFilename } from "@opencode-ai/util/path" @@ -23,12 +22,14 @@ import { unwrap } from "solid-js/store" export interface MessageProps { message: MessageType parts: PartType[] + diffComponent: ValidComponent sanitize?: RegExp } export interface MessagePartProps { part: PartType message: MessageType + diffComponent: ValidComponent hideDetails?: boolean sanitize?: RegExp } @@ -53,6 +54,7 @@ export function Message(props: MessageProps) { message={assistantMessage() as AssistantMessage} parts={props.parts} sanitize={props.sanitize} + diffComponent={props.diffComponent} /> )} </Match> @@ -60,7 +62,12 @@ export function Message(props: MessageProps) { ) } -export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[]; sanitize?: RegExp }) { +export function AssistantMessageDisplay(props: { + message: AssistantMessage + parts: PartType[] + sanitize?: RegExp + diffComponent: ValidComponent +}) { const filteredParts = createMemo(() => { return props.parts?.filter((x) => { if (x.type === "reasoning") return false @@ -68,7 +75,11 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part }) }) return ( - <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} sanitize={props.sanitize} />}</For> + <For each={filteredParts()}> + {(part) => ( + <Part part={part} message={props.message} sanitize={props.sanitize} diffComponent={props.diffComponent} /> + )} + </For> ) } @@ -87,7 +98,13 @@ export function Part(props: MessagePartProps) { const part = createMemo(() => sanitizePart(unwrap(props.part), props.sanitize)) return ( <Show when={component()}> - <Dynamic component={component()} part={part()} message={props.message} hideDetails={props.hideDetails} /> + <Dynamic + component={component()} + part={part()} + message={props.message} + diffComponent={props.diffComponent} + hideDetails={props.hideDetails} + /> </Show> ) } @@ -96,6 +113,7 @@ export interface ToolProps { input: Record<string, any> metadata: Record<string, any> tool: string + diffComponent: ValidComponent output?: string hideDetails?: boolean } @@ -162,6 +180,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { component={render} input={input} tool={part.tool} + diffComponent={props.diffComponent} metadata={metadata} output={part.state.status === "completed" ? part.state.output : undefined} hideDetails={props.hideDetails} @@ -361,7 +380,8 @@ ToolRegistry.register({ > <Show when={props.metadata.filediff}> <div data-component="edit-content"> - <Diff + <Dynamic + component={props.diffComponent} before={{ name: getFilename(props.metadata.filediff.path), contents: props.metadata.filediff.before, diff --git a/packages/ui/src/components/message-progress.tsx b/packages/ui/src/components/message-progress.tsx index efe4dfd8f..afd7f754a 100644 --- a/packages/ui/src/components/message-progress.tsx +++ b/packages/ui/src/components/message-progress.tsx @@ -1,10 +1,27 @@ -import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { + For, + JSXElement, + Match, + Show, + Switch, + ValidComponent, + createEffect, + createMemo, + createSignal, + onCleanup, +} from "solid-js" import { Part } from "./message-part" import { Spinner } from "./spinner" import { useData } from "../context/data" import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk" -export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) { +export interface MessageProgressProps { + assistantMessages: () => AssistantMessageType[] + diffComponent: ValidComponent + done?: boolean +} + +export function MessageProgress(props: MessageProgressProps) { const data = useData() const sanitizer = createMemo(() => (data.directory ? new RegExp(`${data.directory}/`, "g") : undefined)) const parts = createMemo(() => props.assistantMessages().flatMap((m) => data.store.part[m.id])) @@ -155,7 +172,12 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa ) return ( <div data-slot="message-progress-item"> - <Part message={message()!} part={part} sanitize={sanitizer()} /> + <Part + message={message()!} + part={part} + sanitize={sanitizer()} + diffComponent={props.diffComponent} + /> </div> ) }} diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 376317e1b..ea5871b95 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -1,15 +1,15 @@ import { Accordion } from "./accordion" import { Button } from "./button" -import { Diff } from "./diff" import { DiffChanges } from "./diff-changes" import { FileIcon } from "./file-icon" import { Icon } from "./icon" import { StickyAccordionHeader } from "./sticky-accordion-header" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { For, Match, Show, Switch, type JSX } from "solid-js" +import { For, Match, Show, Switch, ValidComponent, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { type FileDiff } from "@opencode-ai/sdk" import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr" +import { Dynamic } from "solid-js/web" export interface SessionReviewProps { split?: boolean @@ -18,6 +18,7 @@ export interface SessionReviewProps { classes?: { root?: string; header?: string; container?: string } actions?: JSX.Element diffs: (FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> })[] + diffComponent: ValidComponent } export const SessionReview = (props: SessionReviewProps) => { @@ -96,7 +97,8 @@ export const SessionReview = (props: SessionReviewProps) => { </Accordion.Trigger> </StickyAccordionHeader> <Accordion.Content data-slot="session-review-accordion-content"> - <Diff + <Dynamic + component={props.diffComponent} preloadedDiff={diff.preloaded} diffStyle={props.split ? "split" : "unified"} before={{ diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index c61b23068..963713a22 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -2,7 +2,18 @@ import { AssistantMessage } from "@opencode-ai/sdk" import { useData } from "../context" import { Binary } from "@opencode-ai/util/binary" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js" +import { + createEffect, + createMemo, + createSignal, + For, + Match, + onMount, + ParentProps, + Show, + Switch, + ValidComponent, +} from "solid-js" import { DiffChanges } from "./diff-changes" import { Typewriter } from "./typewriter" import { Message } from "./message-part" @@ -11,10 +22,10 @@ import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { FileIcon } from "./file-icon" import { Icon } from "./icon" -import { Diff } from "./diff" import { Card } from "./card" import { MessageProgress } from "./message-progress" import { Collapsible } from "./collapsible" +import { Dynamic } from "solid-js/web" export function SessionTurn( props: ParentProps<{ @@ -25,6 +36,7 @@ export function SessionTurn( content?: string container?: string } + diffComponent: ValidComponent }>, ) { const data = useData() @@ -117,7 +129,7 @@ export function SessionTurn( </div> </div> <div data-slot="session-turn-message-content"> - <Message message={msg()} parts={parts()} sanitize={sanitizer()} /> + <Message message={msg()} parts={parts()} sanitize={sanitizer()} diffComponent={props.diffComponent} /> </div> {/* Summary */} <Show when={completed()}> @@ -167,7 +179,8 @@ export function SessionTurn( </Accordion.Trigger> </StickyAccordionHeader> <Accordion.Content data-slot="session-turn-accordion-content"> - <Diff + <Dynamic + component={props.diffComponent} before={{ name: diff.file!, contents: diff.before!, @@ -193,7 +206,11 @@ export function SessionTurn( <div data-slot="session-turn-response-section"> <Switch> <Match when={!completed()}> - <MessageProgress assistantMessages={assistantMessages} done={!messageWorking()} /> + <MessageProgress + assistantMessages={assistantMessages} + done={!messageWorking()} + diffComponent={props.diffComponent} + /> </Match> <Match when={completed() && hasToolPart()}> <Collapsible variant="ghost" open={detailsExpanded()} onOpenChange={setDetailsExpanded}> @@ -224,10 +241,18 @@ export function SessionTurn( message={assistantMessage} parts={parts().filter((p) => p?.id !== last()?.id)} sanitize={sanitizer()} + diffComponent={props.diffComponent} /> ) } - return <Message message={assistantMessage} parts={parts()} sanitize={sanitizer()} /> + return ( + <Message + message={assistantMessage} + parts={parts()} + sanitize={sanitizer()} + diffComponent={props.diffComponent} + /> + ) }} </For> <Show when={error()}> diff --git a/packages/ui/src/components/pierre.ts b/packages/ui/src/pierre/index.ts index 5821697c7..26e902d05 100644 --- a/packages/ui/src/components/pierre.ts +++ b/packages/ui/src/pierre/index.ts @@ -1,4 +1,13 @@ -import { FileDiffOptions } from "@pierre/precision-diffs" +import { DiffLineAnnotation, FileContents, FileDiffOptions } from "@pierre/precision-diffs" +import { ComponentProps } from "solid-js" + +export type DiffProps<T = {}> = FileDiffOptions<T> & { + before: FileContents + after: FileContents + annotations?: DiffLineAnnotation<T>[] + class?: string + classList?: ComponentProps<"div">["classList"] +} export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"]) { return { diff --git a/packages/ui/src/pierre/worker.ts b/packages/ui/src/pierre/worker.ts new file mode 100644 index 000000000..de5fd625a --- /dev/null +++ b/packages/ui/src/pierre/worker.ts @@ -0,0 +1,5 @@ +import ShikiWorkerUrl from "@pierre/precision-diffs/worker/shiki-worker.js?worker&url" + +export function workerFactory(): Worker { + return new Worker(ShikiWorkerUrl, { type: "module" }) +} diff --git a/packages/util/package.json b/packages/util/package.json index dcdda3b51..83dd25c22 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.126", + "version": "1.0.127", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 528f8abcb..38ba08b4f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.126", + "version": "1.0.127", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index edf3f7274..ffd493859 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.0.126", + "version": "1.0.127", "publisher": "sst-dev", "repository": { "type": "git", |
