Преглед изворни кода

feat(desktop): unified diff toggle

Adam пре 1 месец
родитељ
комит
2ec6a21cc0

+ 11 - 0
packages/app/src/context/layout.tsx

@@ -30,6 +30,8 @@ type SessionTabs = {
 
 
 export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
 export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
 
 
+export type ReviewDiffStyle = "unified" | "split"
+
 export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
 export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
   name: "Layout",
   name: "Layout",
   init: () => {
   init: () => {
@@ -49,6 +51,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         },
         },
         review: {
         review: {
           opened: true,
           opened: true,
+          diffStyle: "split" as ReviewDiffStyle,
         },
         },
         session: {
         session: {
           width: 600,
           width: 600,
@@ -156,6 +159,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       },
       },
       review: {
       review: {
         opened: createMemo(() => store.review?.opened ?? true),
         opened: createMemo(() => store.review?.opened ?? true),
+        diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
+        setDiffStyle(diffStyle: ReviewDiffStyle) {
+          if (!store.review) {
+            setStore("review", { opened: true, diffStyle })
+            return
+          }
+          setStore("review", "diffStyle", diffStyle)
+        },
         open() {
         open() {
           setStore("review", "opened", true)
           setStore("review", "opened", true)
         },
         },

+ 4 - 1
packages/app/src/pages/session.tsx

@@ -742,6 +742,8 @@ export default function Page() {
                 <div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
                 <div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
                   <SessionReview
                   <SessionReview
                     diffs={diffs()}
                     diffs={diffs()}
+                    diffStyle={layout.review.diffStyle()}
+                    onDiffStyleChange={layout.review.setDiffStyle}
                     classes={{
                     classes={{
                       root: "pb-32",
                       root: "pb-32",
                       header: "px-4",
                       header: "px-4",
@@ -867,7 +869,8 @@ export default function Page() {
                           container: "px-6",
                           container: "px-6",
                         }}
                         }}
                         diffs={diffs()}
                         diffs={diffs()}
-                        split
+                        diffStyle={layout.review.diffStyle()}
+                        onDiffStyleChange={layout.review.setDiffStyle}
                       />
                       />
                     </div>
                     </div>
                   </Tabs.Content>
                   </Tabs.Content>

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

@@ -33,7 +33,7 @@ const ClientOnlyCode = clientOnly(() => import("@opencode-ai/ui/code").then((m)
 const ClientOnlyWorkerPoolProvider = clientOnly(() =>
 const ClientOnlyWorkerPoolProvider = clientOnly(() =>
   import("@opencode-ai/ui/pierre/worker").then((m) => ({
   import("@opencode-ai/ui/pierre/worker").then((m) => ({
     default: (props: { children: any }) => (
     default: (props: { children: any }) => (
-      <WorkerPoolProvider pool={m.workerPool}>{props.children}</WorkerPoolProvider>
+      <WorkerPoolProvider pools={m.getWorkerPools()}>{props.children}</WorkerPoolProvider>
     ),
     ),
   })),
   })),
 )
 )

+ 2 - 2
packages/ui/src/components/code.tsx

@@ -1,7 +1,7 @@
 import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/diffs"
 import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/diffs"
 import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js"
 import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js"
 import { createDefaultOptions, styleVariables } from "../pierre"
 import { createDefaultOptions, styleVariables } from "../pierre"
-import { workerPool } from "../pierre/worker"
+import { getWorkerPool } from "../pierre/worker"
 
 
 export type CodeProps<T = {}> = FileOptions<T> & {
 export type CodeProps<T = {}> = FileOptions<T> & {
   file: FileContents
   file: FileContents
@@ -21,7 +21,7 @@ export function Code<T>(props: CodeProps<T>) {
           ...createDefaultOptions<T>("unified"),
           ...createDefaultOptions<T>("unified"),
           ...others,
           ...others,
         },
         },
-        workerPool,
+        getWorkerPool("unified"),
       ),
       ),
   )
   )
 
 

+ 1 - 1
packages/ui/src/components/diff-ssr.tsx

@@ -13,7 +13,7 @@ export function Diff<T>(props: SSRDiffProps<T>) {
   let container!: HTMLDivElement
   let container!: HTMLDivElement
   let fileDiffRef!: HTMLElement
   let fileDiffRef!: HTMLElement
   const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
   const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
-  const workerPool = useWorkerPool()
+  const workerPool = useWorkerPool(props.diffStyle)
 
 
   let fileDiffInstance: FileDiff<T> | undefined
   let fileDiffInstance: FileDiff<T> | undefined
   const cleanupFunctions: Array<() => void> = []
   const cleanupFunctions: Array<() => void> = []

+ 7 - 10
packages/ui/src/components/diff.tsx

@@ -1,7 +1,7 @@
 import { FileDiff } from "@pierre/diffs"
 import { FileDiff } from "@pierre/diffs"
 import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
 import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
 import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
 import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
-import { workerPool } from "../pierre/worker"
+import { getWorkerPool } from "../pierre/worker"
 
 
 // interface ThreadMetadata {
 // interface ThreadMetadata {
 //   threadId: string
 //   threadId: string
@@ -20,26 +20,23 @@ export function Diff<T>(props: DiffProps<T>) {
           ...createDefaultOptions(props.diffStyle),
           ...createDefaultOptions(props.diffStyle),
           ...others,
           ...others,
         },
         },
-        workerPool,
+        getWorkerPool(props.diffStyle),
       ),
       ),
   )
   )
 
 
-  const cleanupFunctions: Array<() => void> = []
-
   createEffect(() => {
   createEffect(() => {
+    const diff = fileDiff()
     container.innerHTML = ""
     container.innerHTML = ""
-    fileDiff().render({
+    diff.render({
       oldFile: local.before,
       oldFile: local.before,
       newFile: local.after,
       newFile: local.after,
       lineAnnotations: local.annotations,
       lineAnnotations: local.annotations,
       containerWrapper: container,
       containerWrapper: container,
     })
     })
-  })
 
 
-  onCleanup(() => {
-    // Clean up FileDiff event handlers and dispose SolidJS components
-    fileDiff()?.cleanUp()
-    cleanupFunctions.forEach((dispose) => dispose())
+    onCleanup(() => {
+      diff.cleanUp()
+    })
   })
   })
 
 
   return <div data-component="diff" style={styleVariables} ref={container} />
   return <div data-component="diff" style={styleVariables} ref={container} />

+ 17 - 1
packages/ui/src/components/session-review.tsx

@@ -1,5 +1,6 @@
 import { Accordion } from "./accordion"
 import { Accordion } from "./accordion"
 import { Button } from "./button"
 import { Button } from "./button"
+import { RadioGroup } from "./radio-group"
 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"
@@ -13,8 +14,12 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
 import { Dynamic } from "solid-js/web"
 import { Dynamic } from "solid-js/web"
 import { checksum } from "@opencode-ai/util/encode"
 import { checksum } from "@opencode-ai/util/encode"
 
 
+export type SessionReviewDiffStyle = "unified" | "split"
+
 export interface SessionReviewProps {
 export interface SessionReviewProps {
   split?: boolean
   split?: boolean
+  diffStyle?: SessionReviewDiffStyle
+  onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
   class?: string
   class?: string
   classList?: Record<string, boolean | undefined>
   classList?: Record<string, boolean | undefined>
   classes?: { root?: string; header?: string; container?: string }
   classes?: { root?: string; header?: string; container?: string }
@@ -28,6 +33,8 @@ export const SessionReview = (props: SessionReviewProps) => {
     open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
     open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
   })
   })
 
 
+  const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
+
   const handleChange = (open: string[]) => {
   const handleChange = (open: string[]) => {
     setStore("open", open)
     setStore("open", open)
   }
   }
@@ -60,6 +67,15 @@ export const SessionReview = (props: SessionReviewProps) => {
       >
       >
         <div data-slot="session-review-title">Session changes</div>
         <div data-slot="session-review-title">Session changes</div>
         <div data-slot="session-review-actions">
         <div data-slot="session-review-actions">
+          <Show when={props.onDiffStyleChange}>
+            <RadioGroup
+              options={["unified", "split"] as const}
+              current={diffStyle()}
+              value={(style) => style}
+              label={(style) => (style === "unified" ? "Unified" : "Split")}
+              onSelect={(style) => style && props.onDiffStyleChange?.(style)}
+            />
+          </Show>
           <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
           <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
             <Switch>
             <Switch>
               <Match when={store.open.length > 0}>Collapse all</Match>
               <Match when={store.open.length > 0}>Collapse all</Match>
@@ -102,7 +118,7 @@ export const SessionReview = (props: SessionReviewProps) => {
                   <Dynamic
                   <Dynamic
                     component={diffComponent}
                     component={diffComponent}
                     preloadedDiff={diff.preloaded}
                     preloadedDiff={diff.preloaded}
-                    diffStyle={props.split ? "split" : "unified"}
+                    diffStyle={diffStyle()}
                     before={{
                     before={{
                       name: diff.file!,
                       name: diff.file!,
                       contents: diff.before!,
                       contents: diff.before!,

+ 13 - 3
packages/ui/src/context/worker-pool.tsx

@@ -1,10 +1,20 @@
 import type { WorkerPoolManager } from "@pierre/diffs/worker"
 import type { WorkerPoolManager } from "@pierre/diffs/worker"
 import { createSimpleContext } from "./helper"
 import { createSimpleContext } from "./helper"
 
 
-const ctx = createSimpleContext<WorkerPoolManager | undefined, { pool: WorkerPoolManager | undefined }>({
+export type WorkerPools = {
+  unified: WorkerPoolManager | undefined
+  split: WorkerPoolManager | undefined
+}
+
+const ctx = createSimpleContext<WorkerPools, { pools: WorkerPools }>({
   name: "WorkerPool",
   name: "WorkerPool",
-  init: (props) => props.pool,
+  init: (props) => props.pools,
 })
 })
 
 
 export const WorkerPoolProvider = ctx.provider
 export const WorkerPoolProvider = ctx.provider
-export const useWorkerPool = ctx.use
+
+export function useWorkerPool(diffStyle: "unified" | "split" | undefined) {
+  const pools = ctx.use()
+  if (diffStyle === "split") return pools.split
+  return pools.unified
+}

+ 35 - 12
packages/ui/src/pierre/worker.ts

@@ -1,16 +1,15 @@
-import { getOrCreateWorkerPoolSingleton, WorkerPoolManager } from "@pierre/diffs/worker"
+import { WorkerPoolManager } from "@pierre/diffs/worker"
 import ShikiWorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"
 import ShikiWorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"
 
 
+export type WorkerPoolStyle = "unified" | "split"
+
 export function workerFactory(): Worker {
 export function workerFactory(): Worker {
   return new Worker(ShikiWorkerUrl, { type: "module" })
   return new Worker(ShikiWorkerUrl, { type: "module" })
 }
 }
 
 
-export const workerPool: WorkerPoolManager | undefined = (() => {
-  if (typeof window === "undefined") {
-    return undefined
-  }
-  return getOrCreateWorkerPoolSingleton({
-    poolOptions: {
+function createPool(lineDiffType: "none" | "word-alt") {
+  const pool = new WorkerPoolManager(
+    {
       workerFactory,
       workerFactory,
       // poolSize defaults to 8. More workers = more parallelism but
       // poolSize defaults to 8. More workers = more parallelism but
       // also more memory. Too many can actually slow things down.
       // also more memory. Too many can actually slow things down.
@@ -19,10 +18,34 @@ export const workerPool: WorkerPoolManager | undefined = (() => {
       // boot up time for workers
       // boot up time for workers
       poolSize: 2,
       poolSize: 2,
     },
     },
-    highlighterOptions: {
+    {
       theme: "OpenCode",
       theme: "OpenCode",
-      // Optionally preload languages to avoid lazy-loading delays
-      // langs: ["typescript", "javascript", "css", "html"],
+      lineDiffType,
     },
     },
-  })
-})()
+  )
+
+  pool.initialize()
+  return pool
+}
+
+let unified: WorkerPoolManager | undefined
+let split: WorkerPoolManager | undefined
+
+export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolManager | undefined {
+  if (typeof window === "undefined") return
+
+  if (style === "split") {
+    if (!split) split = createPool("word-alt")
+    return split
+  }
+
+  if (!unified) unified = createPool("none")
+  return unified
+}
+
+export function getWorkerPools() {
+  return {
+    unified: getWorkerPool("unified"),
+    split: getWorkerPool("split"),
+  }
+}