summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-31 09:23:24 -0600
committerAdam <[email protected]>2025-12-31 09:23:24 -0600
commit2ec6a21cc0018be6677e4cbad6bf48dbf8b37786 (patch)
tree6ad3f27288f660b4bba43772075c5138764f25b3 /packages
parentebf5ad25c5f5cdf42bcb93199d9913f260ebe767 (diff)
downloadopencode-2ec6a21cc0018be6677e4cbad6bf48dbf8b37786.tar.gz
opencode-2ec6a21cc0018be6677e4cbad6bf48dbf8b37786.zip
feat(desktop): unified diff toggle
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/context/layout.tsx11
-rw-r--r--packages/app/src/pages/session.tsx5
-rw-r--r--packages/enterprise/src/routes/share/[shareID].tsx2
-rw-r--r--packages/ui/src/components/code.tsx4
-rw-r--r--packages/ui/src/components/diff-ssr.tsx2
-rw-r--r--packages/ui/src/components/diff.tsx17
-rw-r--r--packages/ui/src/components/session-review.tsx18
-rw-r--r--packages/ui/src/context/worker-pool.tsx16
-rw-r--r--packages/ui/src/pierre/worker.ts47
9 files changed, 91 insertions, 31 deletions
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx
index e57f69f8f..156adc4ff 100644
--- a/packages/app/src/context/layout.tsx
+++ b/packages/app/src/context/layout.tsx
@@ -30,6 +30,8 @@ type SessionTabs = {
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
+export type ReviewDiffStyle = "unified" | "split"
+
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
@@ -49,6 +51,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
opened: true,
+ diffStyle: "split" as ReviewDiffStyle,
},
session: {
width: 600,
@@ -156,6 +159,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
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() {
setStore("review", "opened", true)
},
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 6b9ff9e08..032a8375a 100644
--- a/packages/app/src/pages/session.tsx
+++ b/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">
<SessionReview
diffs={diffs()}
+ diffStyle={layout.review.diffStyle()}
+ onDiffStyleChange={layout.review.setDiffStyle}
classes={{
root: "pb-32",
header: "px-4",
@@ -867,7 +869,8 @@ export default function Page() {
container: "px-6",
}}
diffs={diffs()}
- split
+ diffStyle={layout.review.diffStyle()}
+ onDiffStyleChange={layout.review.setDiffStyle}
/>
</div>
</Tabs.Content>
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index 3a3d580b2..d300f8758 100644
--- a/packages/enterprise/src/routes/share/[shareID].tsx
+++ b/packages/enterprise/src/routes/share/[shareID].tsx
@@ -33,7 +33,7 @@ const ClientOnlyCode = clientOnly(() => import("@opencode-ai/ui/code").then((m)
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
import("@opencode-ai/ui/pierre/worker").then((m) => ({
default: (props: { children: any }) => (
- <WorkerPoolProvider pool={m.workerPool}>{props.children}</WorkerPoolProvider>
+ <WorkerPoolProvider pools={m.getWorkerPools()}>{props.children}</WorkerPoolProvider>
),
})),
)
diff --git a/packages/ui/src/components/code.tsx b/packages/ui/src/components/code.tsx
index 77696faed..fda08260f 100644
--- a/packages/ui/src/components/code.tsx
+++ b/packages/ui/src/components/code.tsx
@@ -1,7 +1,7 @@
import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/diffs"
import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js"
import { createDefaultOptions, styleVariables } from "../pierre"
-import { workerPool } from "../pierre/worker"
+import { getWorkerPool } from "../pierre/worker"
export type CodeProps<T = {}> = FileOptions<T> & {
file: FileContents
@@ -21,7 +21,7 @@ export function Code<T>(props: CodeProps<T>) {
...createDefaultOptions<T>("unified"),
...others,
},
- workerPool,
+ getWorkerPool("unified"),
),
)
diff --git a/packages/ui/src/components/diff-ssr.tsx b/packages/ui/src/components/diff-ssr.tsx
index e367a4fbe..56a12c100 100644
--- a/packages/ui/src/components/diff-ssr.tsx
+++ b/packages/ui/src/components/diff-ssr.tsx
@@ -13,7 +13,7 @@ export function Diff<T>(props: SSRDiffProps<T>) {
let container!: HTMLDivElement
let fileDiffRef!: HTMLElement
const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
- const workerPool = useWorkerPool()
+ const workerPool = useWorkerPool(props.diffStyle)
let fileDiffInstance: FileDiff<T> | undefined
const cleanupFunctions: Array<() => void> = []
diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx
index 75dde0440..ff70cece9 100644
--- a/packages/ui/src/components/diff.tsx
+++ b/packages/ui/src/components/diff.tsx
@@ -1,7 +1,7 @@
import { FileDiff } from "@pierre/diffs"
import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
-import { workerPool } from "../pierre/worker"
+import { getWorkerPool } from "../pierre/worker"
// interface ThreadMetadata {
// threadId: string
@@ -20,26 +20,23 @@ export function Diff<T>(props: DiffProps<T>) {
...createDefaultOptions(props.diffStyle),
...others,
},
- workerPool,
+ getWorkerPool(props.diffStyle),
),
)
- const cleanupFunctions: Array<() => void> = []
-
createEffect(() => {
+ const diff = fileDiff()
container.innerHTML = ""
- fileDiff().render({
+ diff.render({
oldFile: local.before,
newFile: local.after,
lineAnnotations: local.annotations,
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} />
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 9162b5216..9e6c633f4 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -1,5 +1,6 @@
import { Accordion } from "./accordion"
import { Button } from "./button"
+import { RadioGroup } from "./radio-group"
import { DiffChanges } from "./diff-changes"
import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
@@ -13,8 +14,12 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { Dynamic } from "solid-js/web"
import { checksum } from "@opencode-ai/util/encode"
+export type SessionReviewDiffStyle = "unified" | "split"
+
export interface SessionReviewProps {
split?: boolean
+ diffStyle?: SessionReviewDiffStyle
+ onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
class?: string
classList?: Record<string, boolean | undefined>
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),
})
+ const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
+
const handleChange = (open: string[]) => {
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-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}>
<Switch>
<Match when={store.open.length > 0}>Collapse all</Match>
@@ -102,7 +118,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<Dynamic
component={diffComponent}
preloadedDiff={diff.preloaded}
- diffStyle={props.split ? "split" : "unified"}
+ diffStyle={diffStyle()}
before={{
name: diff.file!,
contents: diff.before!,
diff --git a/packages/ui/src/context/worker-pool.tsx b/packages/ui/src/context/worker-pool.tsx
index fc2eecc03..5f788f786 100644
--- a/packages/ui/src/context/worker-pool.tsx
+++ b/packages/ui/src/context/worker-pool.tsx
@@ -1,10 +1,20 @@
import type { WorkerPoolManager } from "@pierre/diffs/worker"
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",
- init: (props) => props.pool,
+ init: (props) => props.pools,
})
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
+}
diff --git a/packages/ui/src/pierre/worker.ts b/packages/ui/src/pierre/worker.ts
index 2d2640674..0d117c368 100644
--- a/packages/ui/src/pierre/worker.ts
+++ b/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"
+export type WorkerPoolStyle = "unified" | "split"
+
export function workerFactory(): Worker {
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,
// poolSize defaults to 8. More workers = more parallelism but
// also more memory. Too many can actually slow things down.
@@ -19,10 +18,34 @@ export const workerPool: WorkerPoolManager | undefined = (() => {
// boot up time for workers
poolSize: 2,
},
- highlighterOptions: {
+ {
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"),
+ }
+}