소스 검색

feat: better code and diff rendering performance

Adam 4 달 전
부모
커밋
c0a35141e6

+ 10 - 5
.opencode/opencode.jsonc

@@ -1,9 +1,11 @@
 {
 {
   "$schema": "https://opencode.ai/config.json",
   "$schema": "https://opencode.ai/config.json",
-  "plugin": ["opencode-openai-codex-auth"],
-  "enterprise": {
-    "url": "https://enterprise.dev.opencode.ai",
-  },
+  "plugin": [
+    "opencode-openai-codex-auth"
+  ],
+  // "enterprise": {
+  //   "url": "https://enterprise.dev.opencode.ai",
+  // },
   "provider": {
   "provider": {
     "opencode": {
     "opencode": {
       "options": {
       "options": {
@@ -18,7 +20,10 @@
     },
     },
     "morph": {
     "morph": {
       "type": "local",
       "type": "local",
-      "command": ["bunx", "@morphllm/morphmcp"],
+      "command": [
+        "bunx",
+        "@morphllm/morphmcp"
+      ],
       "environment": {
       "environment": {
         "ENABLED_TOOLS": "warp_grep",
         "ENABLED_TOOLS": "warp_grep",
       },
       },

+ 2 - 2
bun.lock

@@ -443,7 +443,7 @@
     "@hono/zod-validator": "0.4.2",
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
     "@kobalte/core": "0.13.11",
     "@openauthjs/openauth": "0.0.0-20250322224806",
     "@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/meta": "0.29.4",
     "@solidjs/router": "0.15.4",
     "@solidjs/router": "0.15.4",
     "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
     "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
@@ -1218,7 +1218,7 @@
 
 
     "@petamoriken/float16": ["@petamoriken/[email protected]", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
     "@petamoriken/float16": ["@petamoriken/[email protected]", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
 
 
-    "@pierre/precision-diffs": ["@pierre/precision-diffs@0.5.7", "", { "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/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=="],
 
 
     "@pkgjs/parseargs": ["@pkgjs/[email protected]", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
     "@pkgjs/parseargs": ["@pkgjs/[email protected]", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
 
 

+ 1 - 1
package.json

@@ -30,7 +30,7 @@
       "@tsconfig/bun": "1.0.9",
       "@tsconfig/bun": "1.0.9",
       "@cloudflare/workers-types": "4.20251008.0",
       "@cloudflare/workers-types": "4.20251008.0",
       "@openauthjs/openauth": "0.0.0-20250322224806",
       "@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",
       "@tailwindcss/vite": "4.1.11",
       "diff": "8.0.2",
       "diff": "8.0.2",
       "ai": "5.0.97",
       "ai": "5.0.97",

+ 4 - 0
packages/desktop/src/pages/session.tsx

@@ -30,6 +30,7 @@ import { useSync } from "@/context/sync"
 import { useSession } from "@/context/session"
 import { useSession } from "@/context/session"
 import { useLayout } from "@/context/layout"
 import { useLayout } from "@/context/layout"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { Diff } from "@opencode-ai/ui/diff"
 
 
 export default function Page() {
 export default function Page() {
   const layout = useLayout()
   const layout = useLayout()
@@ -357,6 +358,7 @@ export default function Page() {
                           content: "pb-20",
                           content: "pb-20",
                           container: "w-full " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"),
                           container: "w-full " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"),
                         }}
                         }}
+                        diffComponent={Diff}
                       />
                       />
                     </div>
                     </div>
                   </Match>
                   </Match>
@@ -405,6 +407,7 @@ export default function Page() {
                       container: "px-6",
                       container: "px-6",
                     }}
                     }}
                     diffs={session.diffs()}
                     diffs={session.diffs()}
+                    diffComponent={Diff}
                     actions={
                     actions={
                       <Tooltip value="Open in tab">
                       <Tooltip value="Open in tab">
                         <IconButton
                         <IconButton
@@ -436,6 +439,7 @@ export default function Page() {
                     container: "px-6",
                     container: "px-6",
                   }}
                   }}
                   diffs={session.diffs()}
                   diffs={session.diffs()}
+                  diffComponent={Diff}
                   split
                   split
                 />
                 />
               </div>
               </div>

+ 10 - 1
packages/enterprise/src/routes/share/[shareID].tsx

@@ -18,6 +18,10 @@ import z from "zod"
 import NotFound from "../[...404]"
 import NotFound from "../[...404]"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
 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(
 const SessionDataMissingError = NamedError.create(
   "SessionDataMissingError",
   "SessionDataMissingError",
@@ -230,6 +234,7 @@ export default function () {
                                 "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
                                 "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
                               container: "px-4",
                               container: "px-4",
                             }}
                             }}
+                            diffComponent={ClientOnlyDiff}
                           />
                           />
                         )}
                         )}
                       </For>
                       </For>
@@ -299,6 +304,7 @@ export default function () {
                                 content: "flex flex-col justify-between items-start",
                                 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"),
                                 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 }}>
                               <div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
                                 <Logo class="w-58.5 opacity-12" />
                                 <Logo class="w-58.5 opacity-12" />
@@ -311,6 +317,7 @@ export default function () {
                             <SessionReview
                             <SessionReview
                               class="@4xl:hidden"
                               class="@4xl:hidden"
                               diffs={diffs()}
                               diffs={diffs()}
+                              diffComponent={Diff}
                               classes={{
                               classes={{
                                 root: "pb-20",
                                 root: "pb-20",
                                 header: "px-6",
                                 header: "px-6",
@@ -318,9 +325,10 @@ export default function () {
                               }}
                               }}
                             />
                             />
                             <SessionReview
                             <SessionReview
-                              class="hidden @4xl:flex"
                               split
                               split
+                              class="hidden @4xl:flex"
                               diffs={splitDiffs()}
                               diffs={splitDiffs()}
+                              diffComponent={Diff}
                               classes={{
                               classes={{
                                 root: "pb-20",
                                 root: "pb-20",
                                 header: "px-6",
                                 header: "px-6",
@@ -352,6 +360,7 @@ export default function () {
                               <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
                               <div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
                                 <SessionReview
                                 <SessionReview
                                   diffs={diffs()}
                                   diffs={diffs()}
+                                  diffComponent={Diff}
                                   classes={{
                                   classes={{
                                     root: "pb-20",
                                     root: "pb-20",
                                     header: "px-4",
                                     header: "px-4",

+ 1 - 1
packages/ui/package.json

@@ -4,7 +4,7 @@
   "type": "module",
   "type": "module",
   "exports": {
   "exports": {
     "./*": "./src/components/*.tsx",
     "./*": "./src/components/*.tsx",
-    "./pierre": "./src/components/pierre.ts",
+    "./pierre": "./src/pierre/index.ts",
     "./hooks": "./src/hooks/index.ts",
     "./hooks": "./src/hooks/index.ts",
     "./context": "./src/context/index.ts",
     "./context": "./src/context/index.ts",
     "./context/*": "./src/context/*.tsx",
     "./context/*": "./src/context/*.tsx",

+ 24 - 5
packages/ui/src/components/code.tsx

@@ -1,6 +1,22 @@
 import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/precision-diffs"
 import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/precision-diffs"
 import { ComponentProps, createEffect, splitProps } from "solid-js"
 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> & {
 export type CodeProps<T = {}> = FileOptions<T> & {
   file: FileContents
   file: FileContents
@@ -14,10 +30,13 @@ export function Code<T>(props: CodeProps<T>) {
   const [local, others] = splitProps(props, ["file", "class", "classList", "annotations"])
   const [local, others] = splitProps(props, ["file", "class", "classList", "annotations"])
 
 
   createEffect(() => {
   createEffect(() => {
-    const instance = new File<T>({
-      ...createDefaultOptions<T>("unified"),
-      ...others,
-    })
+    const instance = new File<T>(
+      {
+        ...createDefaultOptions<T>("unified"),
+        ...others,
+      },
+      workerPool,
+    )
 
 
     container.innerHTML = ""
     container.innerHTML = ""
     instance.render({
     instance.render({

+ 75 - 0
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>
+  )
+}

+ 27 - 71
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 {
 // interface ThreadMetadata {
 //   threadId: string
 //   threadId: string
@@ -21,21 +26,21 @@ export type DiffProps<T = {}> = FileDiffOptions<T> & {
 
 
 export function Diff<T>(props: DiffProps<T>) {
 export function Diff<T>(props: DiffProps<T>) {
   let container!: HTMLDivElement
   let container!: HTMLDivElement
-  let fileDiffRef!: HTMLElement
   const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
   const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
 
 
   let fileDiffInstance: FileDiff<T> | undefined
   let fileDiffInstance: FileDiff<T> | undefined
   const cleanupFunctions: Array<() => void> = []
   const cleanupFunctions: Array<() => void> = []
 
 
   createEffect(() => {
   createEffect(() => {
-    if (props.preloadedDiff) return
     container.innerHTML = ""
     container.innerHTML = ""
     if (!fileDiffInstance) {
     if (!fileDiffInstance) {
-      fileDiffInstance = new FileDiff<T>({
-        ...createDefaultOptions(props.diffStyle),
-        ...others,
-        ...(props.preloadedDiff ?? {}),
-      })
+      fileDiffInstance = new FileDiff<T>(
+        {
+          ...createDefaultOptions(props.diffStyle),
+          ...others,
+        },
+        workerPool,
+      )
     }
     }
     fileDiffInstance.render({
     fileDiffInstance.render({
       oldFile: local.before,
       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(() => {
   onCleanup(() => {
     // Clean up FileDiff event handlers and dispose SolidJS components
     // Clean up FileDiff event handlers and dispose SolidJS components
     fileDiffInstance?.cleanUp()
     fileDiffInstance?.cleanUp()
     cleanupFunctions.forEach((dispose) => dispose())
     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} />
 }
 }

+ 26 - 6
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 { Dynamic } from "solid-js/web"
 import {
 import {
   AssistantMessage,
   AssistantMessage,
@@ -13,7 +13,6 @@ import { GenericTool } from "./basic-tool"
 import { Card } from "./card"
 import { Card } from "./card"
 import { Icon } from "./icon"
 import { Icon } from "./icon"
 import { Checkbox } from "./checkbox"
 import { Checkbox } from "./checkbox"
-import { Diff } from "./diff"
 import { DiffChanges } from "./diff-changes"
 import { DiffChanges } from "./diff-changes"
 import { Markdown } from "./markdown"
 import { Markdown } from "./markdown"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
@@ -23,12 +22,14 @@ import { unwrap } from "solid-js/store"
 export interface MessageProps {
 export interface MessageProps {
   message: MessageType
   message: MessageType
   parts: PartType[]
   parts: PartType[]
+  diffComponent: ValidComponent
   sanitize?: RegExp
   sanitize?: RegExp
 }
 }
 
 
 export interface MessagePartProps {
 export interface MessagePartProps {
   part: PartType
   part: PartType
   message: MessageType
   message: MessageType
+  diffComponent: ValidComponent
   hideDetails?: boolean
   hideDetails?: boolean
   sanitize?: RegExp
   sanitize?: RegExp
 }
 }
@@ -53,6 +54,7 @@ export function Message(props: MessageProps) {
             message={assistantMessage() as AssistantMessage}
             message={assistantMessage() as AssistantMessage}
             parts={props.parts}
             parts={props.parts}
             sanitize={props.sanitize}
             sanitize={props.sanitize}
+            diffComponent={props.diffComponent}
           />
           />
         )}
         )}
       </Match>
       </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(() => {
   const filteredParts = createMemo(() => {
     return props.parts?.filter((x) => {
     return props.parts?.filter((x) => {
       if (x.type === "reasoning") return false
       if (x.type === "reasoning") return false
@@ -68,7 +75,11 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
     })
     })
   })
   })
   return (
   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))
   const part = createMemo(() => sanitizePart(unwrap(props.part), props.sanitize))
   return (
   return (
     <Show when={component()}>
     <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>
     </Show>
   )
   )
 }
 }
@@ -96,6 +113,7 @@ export interface ToolProps {
   input: Record<string, any>
   input: Record<string, any>
   metadata: Record<string, any>
   metadata: Record<string, any>
   tool: string
   tool: string
+  diffComponent: ValidComponent
   output?: string
   output?: string
   hideDetails?: boolean
   hideDetails?: boolean
 }
 }
@@ -162,6 +180,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
             component={render}
             component={render}
             input={input}
             input={input}
             tool={part.tool}
             tool={part.tool}
+            diffComponent={props.diffComponent}
             metadata={metadata}
             metadata={metadata}
             output={part.state.status === "completed" ? part.state.output : undefined}
             output={part.state.status === "completed" ? part.state.output : undefined}
             hideDetails={props.hideDetails}
             hideDetails={props.hideDetails}
@@ -361,7 +380,8 @@ ToolRegistry.register({
       >
       >
         <Show when={props.metadata.filediff}>
         <Show when={props.metadata.filediff}>
           <div data-component="edit-content">
           <div data-component="edit-content">
-            <Diff
+            <Dynamic
+              component={props.diffComponent}
               before={{
               before={{
                 name: getFilename(props.metadata.filediff.path),
                 name: getFilename(props.metadata.filediff.path),
                 contents: props.metadata.filediff.before,
                 contents: props.metadata.filediff.before,

+ 5 - 3
packages/ui/src/components/session-review.tsx

@@ -1,15 +1,15 @@
 import { Accordion } from "./accordion"
 import { Accordion } from "./accordion"
 import { Button } from "./button"
 import { Button } from "./button"
-import { Diff } from "./diff"
 import { DiffChanges } from "./diff-changes"
 import { DiffChanges } from "./diff-changes"
 import { FileIcon } from "./file-icon"
 import { FileIcon } from "./file-icon"
 import { Icon } from "./icon"
 import { Icon } from "./icon"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 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 { createStore } from "solid-js/store"
 import { type FileDiff } from "@opencode-ai/sdk"
 import { type FileDiff } from "@opencode-ai/sdk"
 import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
 import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
+import { Dynamic } from "solid-js/web"
 
 
 export interface SessionReviewProps {
 export interface SessionReviewProps {
   split?: boolean
   split?: boolean
@@ -18,6 +18,7 @@ export interface SessionReviewProps {
   classes?: { root?: string; header?: string; container?: string }
   classes?: { root?: string; header?: string; container?: string }
   actions?: JSX.Element
   actions?: JSX.Element
   diffs: (FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> })[]
   diffs: (FileDiff & { preloaded?: PreloadMultiFileDiffResult<any> })[]
+  diffComponent: ValidComponent
 }
 }
 
 
 export const SessionReview = (props: SessionReviewProps) => {
 export const SessionReview = (props: SessionReviewProps) => {
@@ -96,7 +97,8 @@ export const SessionReview = (props: SessionReviewProps) => {
                   </Accordion.Trigger>
                   </Accordion.Trigger>
                 </StickyAccordionHeader>
                 </StickyAccordionHeader>
                 <Accordion.Content data-slot="session-review-accordion-content">
                 <Accordion.Content data-slot="session-review-accordion-content">
-                  <Diff
+                  <Dynamic
+                    component={props.diffComponent}
                     preloadedDiff={diff.preloaded}
                     preloadedDiff={diff.preloaded}
                     diffStyle={props.split ? "split" : "unified"}
                     diffStyle={props.split ? "split" : "unified"}
                     before={{
                     before={{

+ 26 - 5
packages/ui/src/components/session-turn.tsx

@@ -2,7 +2,18 @@ import { AssistantMessage } from "@opencode-ai/sdk"
 import { useData } from "../context"
 import { useData } from "../context"
 import { Binary } from "@opencode-ai/util/binary"
 import { Binary } from "@opencode-ai/util/binary"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 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 { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
 import { Typewriter } from "./typewriter"
 import { Message } from "./message-part"
 import { Message } from "./message-part"
@@ -11,10 +22,10 @@ import { Accordion } from "./accordion"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { FileIcon } from "./file-icon"
 import { FileIcon } from "./file-icon"
 import { Icon } from "./icon"
 import { Icon } from "./icon"
-import { Diff } from "./diff"
 import { Card } from "./card"
 import { Card } from "./card"
 import { MessageProgress } from "./message-progress"
 import { MessageProgress } from "./message-progress"
 import { Collapsible } from "./collapsible"
 import { Collapsible } from "./collapsible"
+import { Dynamic } from "solid-js/web"
 
 
 export function SessionTurn(
 export function SessionTurn(
   props: ParentProps<{
   props: ParentProps<{
@@ -25,6 +36,7 @@ export function SessionTurn(
       content?: string
       content?: string
       container?: string
       container?: string
     }
     }
+    diffComponent: ValidComponent
   }>,
   }>,
 ) {
 ) {
   const data = useData()
   const data = useData()
@@ -117,7 +129,7 @@ export function SessionTurn(
                   </div>
                   </div>
                 </div>
                 </div>
                 <div data-slot="session-turn-message-content">
                 <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>
                 </div>
                 {/* Summary */}
                 {/* Summary */}
                 <Show when={completed()}>
                 <Show when={completed()}>
@@ -167,7 +179,8 @@ export function SessionTurn(
                               </Accordion.Trigger>
                               </Accordion.Trigger>
                             </StickyAccordionHeader>
                             </StickyAccordionHeader>
                             <Accordion.Content data-slot="session-turn-accordion-content">
                             <Accordion.Content data-slot="session-turn-accordion-content">
-                              <Diff
+                              <Dynamic
+                                component={props.diffComponent}
                                 before={{
                                 before={{
                                   name: diff.file!,
                                   name: diff.file!,
                                   contents: diff.before!,
                                   contents: diff.before!,
@@ -224,10 +237,18 @@ export function SessionTurn(
                                       message={assistantMessage}
                                       message={assistantMessage}
                                       parts={parts().filter((p) => p?.id !== last()?.id)}
                                       parts={parts().filter((p) => p?.id !== last()?.id)}
                                       sanitize={sanitizer()}
                                       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>
                             </For>
                             <Show when={error()}>
                             <Show when={error()}>

+ 10 - 1
packages/ui/src/components/pierre.ts → 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"]) {
 export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"]) {
   return {
   return {

+ 5 - 0
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" })
+}