From 9363c15b4a1fafca2d30063ebcb166f36ba35235 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 8 Dec 2025 06:24:19 -0600 Subject: [PATCH] feat: better code and diff perf --- bun.lock | 6 ++- package.json | 2 +- packages/desktop/src/DesktopInterface.tsx | 2 +- packages/desktop/src/context/local.tsx | 2 +- packages/desktop/src/context/session.tsx | 2 +- .../desktop/src/pages/directory-layout.tsx | 2 +- packages/desktop/src/pages/home.tsx | 2 +- packages/desktop/src/pages/layout.tsx | 2 +- packages/desktop/src/pages/session.tsx | 7 +++- packages/desktop/src/utils/encode.ts | 7 ---- packages/desktop/src/utils/index.ts | 1 - .../enterprise/src/routes/share/[shareID].tsx | 2 +- packages/ui/src/components/code.tsx | 40 +++++++----------- packages/ui/src/components/diff.tsx | 42 +++++++------------ packages/ui/src/components/session-review.tsx | 3 ++ packages/ui/src/components/session-turn.tsx | 3 ++ packages/ui/src/pierre/index.ts | 34 ++++++++------- packages/ui/src/pierre/worker.ts | 17 +++++++- packages/util/src/encode.ts | 25 +++++++++++ 19 files changed, 111 insertions(+), 90 deletions(-) delete mode 100644 packages/desktop/src/utils/encode.ts create mode 100644 packages/util/src/encode.ts diff --git a/bun.lock b/bun.lock index 8feff3ae8c..2350e0b14d 100644 --- a/bun.lock +++ b/bun.lock @@ -458,7 +458,7 @@ "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.6.0-beta.3", + "@pierre/precision-diffs": "0.6.0-beta.10", "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", @@ -1273,7 +1273,7 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], - "@pierre/precision-diffs": ["@pierre/precision-diffs@0.6.0-beta.3", "", { "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=="], + "@pierre/precision-diffs": ["@pierre/precision-diffs@0.6.0-beta.10", "", { "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", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-2rdd1Q1xJbB0Z4oUbm0Ybrr2gLFEdvNetZLadJboZSFL7Q4gFujdQZfXfV3vB9X+esjt++v0nzb3mioW25BOTA=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -2819,6 +2819,8 @@ "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], diff --git a/package.json b/package.json index c7ea47072d..b866c9bdf0 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.6.0-beta.3", + "@pierre/precision-diffs": "0.6.0-beta.10", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "ai": "5.0.97", diff --git a/packages/desktop/src/DesktopInterface.tsx b/packages/desktop/src/DesktopInterface.tsx index c4de3e089c..1979308e46 100644 --- a/packages/desktop/src/DesktopInterface.tsx +++ b/packages/desktop/src/DesktopInterface.tsx @@ -13,7 +13,7 @@ import Session from "@/pages/session" import { LayoutProvider } from "./context/layout" import { GlobalSDKProvider } from "./context/global-sdk" import { SessionProvider } from "./context/session" -import { base64Encode } from "./utils" +import { base64Encode } from "@opencode-ai/util/encode" import { createMemo, Show } from "solid-js" const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1" diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index de4202d9a0..a9f48ec673 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -5,7 +5,7 @@ import type { FileContent, FileNode, Model, Provider, File as FileStatus } from import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" -import { base64Encode } from "@/utils" +import { base64Encode } from "@opencode-ai/util/encode" export type LocalFile = FileNode & Partial<{ diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 5c0bad98a3..8275ce12a9 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -7,7 +7,7 @@ import { TextSelection } from "./local" import { pipe, sumBy } from "remeda" import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2" import { useParams } from "@solidjs/router" -import { base64Encode } from "@/utils" +import { base64Encode } from "@opencode-ai/util/encode" import { useSDK } from "./sdk" export type LocalPTY = { diff --git a/packages/desktop/src/pages/directory-layout.tsx b/packages/desktop/src/pages/directory-layout.tsx index de16eff301..20467c066b 100644 --- a/packages/desktop/src/pages/directory-layout.tsx +++ b/packages/desktop/src/pages/directory-layout.tsx @@ -4,7 +4,7 @@ import { SDKProvider } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" import { useGlobalSync } from "@/context/global-sync" -import { base64Decode } from "@/utils" +import { base64Decode } from "@opencode-ai/util/encode" import { DataProvider } from "@opencode-ai/ui/context" import { iife } from "@opencode-ai/util/iife" diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx index 58fcb20cec..c35d5754e0 100644 --- a/packages/desktop/src/pages/home.tsx +++ b/packages/desktop/src/pages/home.tsx @@ -1,5 +1,5 @@ import { useGlobalSync } from "@/context/global-sync" -import { base64Encode } from "@/utils" +import { base64Encode } from "@opencode-ai/util/encode" import { For } from "solid-js" import { A } from "@solidjs/router" import { Button } from "@opencode-ai/ui/button" diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 94ff4dd04f..a1b575ee58 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -3,7 +3,7 @@ import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" import { useLayout } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" -import { base64Decode, base64Encode } from "@/utils" +import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { Mark } from "@opencode-ai/ui/logo" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 3cc6d307ca..81f4dc1cbc 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -31,6 +31,7 @@ import { useSession } from "@/context/session" import { useLayout } from "@/context/layout" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Terminal } from "@/components/terminal" +import { checksum } from "@opencode-ai/util/encode" export default function Page() { const layout = useLayout() @@ -489,7 +490,11 @@ export default function Page() { {(f) => ( diff --git a/packages/desktop/src/utils/encode.ts b/packages/desktop/src/utils/encode.ts deleted file mode 100644 index 265bba5c43..0000000000 --- a/packages/desktop/src/utils/encode.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function base64Encode(value: string) { - return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") -} - -export function base64Decode(value: string) { - return atob(value.replace(/-/g, "+").replace(/_/g, "/")) -} diff --git a/packages/desktop/src/utils/index.ts b/packages/desktop/src/utils/index.ts index e50efe837a..d87053269d 100644 --- a/packages/desktop/src/utils/index.ts +++ b/packages/desktop/src/utils/index.ts @@ -1,2 +1 @@ export * from "./dom" -export * from "./encode" diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index c3392489d3..a8b0ecf694 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -354,7 +354,7 @@ export default function () { Session - 5 Files Changed + {diffs().length} Files Changed diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx index b4b7728169..c80f0987f2 100644 --- a/packages/ui/src/components/code.tsx +++ b/packages/ui/src/components/code.tsx @@ -1,22 +1,7 @@ import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/precision-diffs" -import { ComponentProps, createEffect, splitProps } from "solid-js" +import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js" 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"], - }, -}) +import { workerPool } from "../pierre/worker" export type CodeProps = FileOptions & { file: FileContents @@ -29,17 +14,20 @@ export function Code(props: CodeProps) { let container!: HTMLDivElement const [local, others] = splitProps(props, ["file", "class", "classList", "annotations"]) - createEffect(() => { - const instance = new File( - { - ...createDefaultOptions("unified"), - ...others, - }, - workerPool, - ) + const file = createMemo( + () => + new File( + { + ...createDefaultOptions("unified"), + ...others, + }, + workerPool, + ), + ) + createEffect(() => { container.innerHTML = "" - instance.render({ + file().render({ file: local.file, lineAnnotations: local.annotations, containerWrapper: container, diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx index 8e19c3172c..703043f4c3 100644 --- a/packages/ui/src/components/diff.tsx +++ b/packages/ui/src/components/diff.tsx @@ -1,22 +1,7 @@ import { FileDiff } from "@pierre/precision-diffs" -import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker" -import { createEffect, onCleanup, splitProps } from "solid-js" +import { createEffect, createMemo, 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"], - }, -}) +import { workerPool } from "../pierre/worker" // interface ThreadMetadata { // threadId: string @@ -28,21 +13,22 @@ export function Diff(props: DiffProps) { let container!: HTMLDivElement const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"]) - let fileDiffInstance: FileDiff | undefined - const cleanupFunctions: Array<() => void> = [] - - createEffect(() => { - container.innerHTML = "" - if (!fileDiffInstance) { - fileDiffInstance = new FileDiff( + const fileDiff = createMemo( + () => + new FileDiff( { ...createDefaultOptions(props.diffStyle), ...others, }, workerPool, - ) - } - fileDiffInstance.render({ + ), + ) + + const cleanupFunctions: Array<() => void> = [] + + createEffect(() => { + container.innerHTML = "" + fileDiff().render({ oldFile: local.before, newFile: local.after, lineAnnotations: local.annotations, @@ -52,7 +38,7 @@ export function Diff(props: DiffProps) { onCleanup(() => { // Clean up FileDiff event handlers and dispose SolidJS components - fileDiffInstance?.cleanUp() + fileDiff()?.cleanUp() cleanupFunctions.forEach((dispose) => dispose()) }) diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 6dbaaec189..cc82dcbd49 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -11,6 +11,7 @@ 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" +import { checksum } from "@opencode-ai/util/encode" export interface SessionReviewProps { split?: boolean @@ -105,10 +106,12 @@ export const SessionReview = (props: SessionReviewProps) => { before={{ name: diff.file!, contents: diff.before!, + cacheKey: checksum(diff.before), }} after={{ name: diff.file!, contents: diff.after!, + cacheKey: checksum(diff.after), }} /> diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 44ecc3a142..47d4cd22da 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -3,6 +3,7 @@ import { useData } from "../context" import { useDiffComponent } from "../context/diff" import { Binary } from "@opencode-ai/util/binary" import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { checksum } from "@opencode-ai/util/encode" import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js" import { DiffChanges } from "./diff-changes" import { Typewriter } from "./typewriter" @@ -174,10 +175,12 @@ export function SessionTurn( before={{ name: diff.file!, contents: diff.before!, + cacheKey: checksum(diff.before!), }} after={{ name: diff.file!, contents: diff.after!, + cacheKey: checksum(diff.after!), }} /> diff --git a/packages/ui/src/pierre/index.ts b/packages/ui/src/pierre/index.ts index 26e902d057..ddbd1b82de 100644 --- a/packages/ui/src/pierre/index.ts +++ b/packages/ui/src/pierre/index.ts @@ -9,21 +9,7 @@ export type DiffProps = FileDiffOptions & { classList?: ComponentProps<"div">["classList"] } -export function createDefaultOptions(style: FileDiffOptions["diffStyle"]) { - return { - theme: "OpenCode", - themeType: "system", - disableLineNumbers: false, - overflow: "wrap", - diffStyle: style ?? "unified", - diffIndicators: "bars", - disableBackground: false, - expansionLineCount: 20, - lineDiffType: style === "split" ? "word-alt" : "none", - maxLineDiffLength: 1000, - maxLineLengthForHighlighting: 1000, - disableFileHeader: true, - unsafeCSS: ` +const unsafeCSS = ` [data-pjs-header], [data-pjs] { [data-separator-wrapper] { @@ -46,7 +32,23 @@ export function createDefaultOptions(style: FileDiffOptions["diffStyle"]) [data-separator-content] { height: 24px !important; } -}`, +}` + +export function createDefaultOptions(style: FileDiffOptions["diffStyle"]) { + return { + theme: "OpenCode", + themeType: "system", + disableLineNumbers: false, + overflow: "wrap", + diffStyle: style ?? "unified", + diffIndicators: "bars", + disableBackground: false, + expansionLineCount: 20, + lineDiffType: style === "split" ? "word-alt" : "none", + maxLineDiffLength: 1000, + maxLineLengthForHighlighting: 1000, + disableFileHeader: true, + unsafeCSS, // hunkSeparators(hunkData: HunkData) { // const fragment = document.createDocumentFragment() // const numCol = document.createElement("div") diff --git a/packages/ui/src/pierre/worker.ts b/packages/ui/src/pierre/worker.ts index de5fd625a0..2b2da1f09d 100644 --- a/packages/ui/src/pierre/worker.ts +++ b/packages/ui/src/pierre/worker.ts @@ -1,5 +1,20 @@ -import ShikiWorkerUrl from "@pierre/precision-diffs/worker/shiki-worker.js?worker&url" +import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker" +import ShikiWorkerUrl from "@pierre/precision-diffs/worker/worker.js?worker&url" export function workerFactory(): Worker { return new Worker(ShikiWorkerUrl, { type: "module" }) } + +export 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"], + }, +}) diff --git a/packages/util/src/encode.ts b/packages/util/src/encode.ts new file mode 100644 index 0000000000..cc40fbe9d0 --- /dev/null +++ b/packages/util/src/encode.ts @@ -0,0 +1,25 @@ +export function base64Encode(value: string) { + return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") +} + +export function base64Decode(value: string) { + return atob(value.replace(/-/g, "+").replace(/_/g, "/")) +} + +export async function hash(content: string, algorithm = "SHA-256"): Promise { + const encoder = new TextEncoder() + const data = encoder.encode(content) + const hashBuffer = await crypto.subtle.digest(algorithm, data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("") + return hashHex +} + +export function checksum(content: string): string { + let hash = 0x811c9dc5 + for (let i = 0; i < content.length; i++) { + hash ^= content.charCodeAt(i) + hash = Math.imul(hash, 0x01000193) + } + return (hash >>> 0).toString(36) +}