fix(session-ui): enable word diffs in unified view (#45833)

This commit is contained in:
Luke Parker 2026-08-28 14:31:18 +10:00 committed by GitHub
parent 92b9eebab2
commit bb390f435c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 130 additions and 17 deletions

View file

@ -0,0 +1,35 @@
import { expect, story } from "../../storybook/playwright/story"
for (const split of [false, true]) {
for (const theme of ["light", "dark"]) {
story(`highlights changed words in ${split ? "split" : "unified"} ${theme} diffs`, async ({ mount }) => {
const root = await mount("components-session-review--inline-changes", { args: { split }, globals: { theme } })
const diffs = root.locator("diffs-container")
await expect(diffs).toHaveCount(2)
for (const diff of await diffs.all()) {
await expect(diff.locator('[data-line] [style*="--syntax-"]')).not.toHaveCount(0)
}
const additions = root.locator('[data-line-type="change-addition"] [data-diff-span]')
await expect(additions).toHaveText(["select-text"])
await expect(root.locator('[data-line-type="context"] [data-diff-span]')).toHaveCount(0)
await expect(root.locator('[data-line-type="change-deletion"] [data-diff-span]')).toHaveText([
'"http" in',
"? error.reason.",
"response?.",
": undefined",
])
await expect(additions).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)")
})
}
for (const source of ["files", "metadata"]) {
story(`skips word diffs for large ${split ? "split" : "unified"} ${source}`, async ({ mount }) => {
const root = await mount("components-session-review--large-file", { args: { split, source } })
const line = root.locator('[data-line][data-line-type="change-addition"]')
await expect(line).toHaveText("export const value = 'after'")
// Plain first paint is not proof that the worker kept inline diffs disabled.
await expect(line.locator('[style*="--syntax-"]')).not.toHaveCount(0)
await expect(root.locator("[data-diff-span]")).toHaveCount(0)
})
}
}

View file

@ -4,7 +4,7 @@ import { expect, story } from "../../storybook/playwright/story"
story("opens the comment editor when code is clicked", async ({ mount }) => {
const root = await mount("components-session-review--interactive-comments")
const review = root.locator('[data-component="session-review"]')
await review.getByText("export const value = 'after'", { exact: true }).click()
await review.locator('[data-line-type="change-addition"] [data-diff-span]').click()
await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 2")
})

View file

@ -906,7 +906,7 @@ function TextViewer<T>(props: TextFileProps<T>) {
createEffect(() => {
const opts = options()
const workerPool = getWorkerPool("unified")
const workerPool = getWorkerPool()
const virtualizer = virtuals.get()
renderViewer({
@ -1111,7 +1111,8 @@ function DiffViewer<T>(props: DiffFileProps<T>) {
createEffect(() => {
const opts = options()
const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle)
// Worker render options override per-viewer options, including the large-file fallback.
const workerPool = getWorkerPool(large() ? "none" : "word-alt")
const virtualizer = virtuals.get()
const beforeContents = typeof local.before?.contents === "string" ? local.before.contents : ""
const afterContents = typeof local.after?.contents === "string" ? local.after.contents : ""

View file

@ -1,6 +1,8 @@
import { createStore } from "solid-js/store"
import { parseDiffFromFile } from "@pierre/diffs"
import { CurrentSessionProviders } from "../storybook/current-session-story"
import { editThenTestDocument, reviewDiffs } from "../storybook/current-session-fixtures"
import { File } from "./file"
import { SessionReview, type SessionReviewComment } from "./session-review"
function ReviewStory(props: { split?: boolean }) {
@ -77,3 +79,79 @@ function InteractiveCommentsStory() {
}
export const InteractiveComments = { render: () => <InteractiveCommentsStory /> }
const gitDiffs = [
{
// OpenCode 93e1f383dd79683af4fc5ad139cea0516603c838, unchanged git-show output.
file: "packages/session-ui/src/components/file.tsx",
additions: 1,
deletions: 1,
patch: `diff --git a/packages/session-ui/src/components/file.tsx b/packages/session-ui/src/components/file.tsx
index 704971b014..4876731cbd 100644
--- a/packages/session-ui/src/components/file.tsx
+++ b/packages/session-ui/src/components/file.tsx
@@ -702,7 +702,7 @@ function ViewerShell(props: {
data-mode={props.mode}
dir="ltr"
style={styleVariables}
- class="relative outline-none"
+ class="relative select-text outline-none"
classList={{
...props.classList,
[props.class ?? ""]: !!props.class,
`,
},
{
// OpenCode 497a24c17d, unchanged git-show output.
file: "packages/core/src/session/runner/retry.ts",
additions: 1,
deletions: 1,
patch: `diff --git a/packages/core/src/session/runner/retry.ts b/packages/core/src/session/runner/retry.ts
index 10f6680097..ef26792ffe 100644
--- a/packages/core/src/session/runner/retry.ts
+++ b/packages/core/src/session/runner/retry.ts
@@ -15,7 +15,7 @@ export interface Input {
}
\x20
export function isRetryable(error: AIError) {
- const override = "http" in error.reason ? error.reason.http?.response?.headers["x-should-retry"] : undefined
+ const override = error.reason.http?.headers["x-should-retry"]
if (override === "true") return true
if (override === "false") return false
switch (error.reason._tag) {
`,
},
]
export const InlineChanges = {
args: { split: false },
render: (args: { split: boolean }) => (
<CurrentSessionProviders document={editThenTestDocument}>
<div class="mx-auto h-screen min-h-[620px] w-full max-w-[1100px] overflow-hidden bg-background-base">
<SessionReview
title="OpenCode Git history"
diffs={gitDiffs}
open={gitDiffs.map((diff) => diff.file)}
split={args.split}
/>
</div>
</CurrentSessionProviders>
),
}
export const LargeFile = {
args: { split: false, source: "files" },
argTypes: { source: { control: "select", options: ["files", "metadata"] } },
render: (args: { split: boolean; source: string }) => {
// Cross the viewer's 500,000-character limit without long changed lines or 1,000 total lines.
const padding = `// ${"unchanged ".repeat(70)}\n`.repeat(800)
const before = { name: "large.ts", contents: `export const value = 'before'\n${padding}` }
const after = { name: "large.ts", contents: `export const value = 'after'\n${padding}` }
const input = args.source === "metadata" ? { fileDiff: parseDiffFromFile(before, after) } : { before, after }
return (
<div class="h-screen overflow-auto bg-background-base">
<File mode="diff" {...input} diffStyle={args.split ? "split" : "unified"} />
</div>
)
},
}

View file

@ -197,7 +197,7 @@ export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"])
disableBackground: false,
expansionLineCount: 20,
hunkSeparators: "line-info-basic",
lineDiffType: style === "split" ? "word-alt" : "none",
lineDiffType: "word-alt",
maxLineDiffLength: 1000,
maxLineLengthForHighlighting: 1000,
disableFileHeader: true,

View file

@ -4,8 +4,6 @@ import { registerOpenCodeTheme } from "@opencode-ai/ui/context/marked-theme-regi
registerOpenCodeTheme()
export type WorkerPoolStyle = "unified" | "split"
export function workerFactory(): Worker {
return new Worker(ShikiWorkerUrl, { type: "module" })
}
@ -32,24 +30,25 @@ function createPool(lineDiffType: "none" | "word-alt") {
return pool
}
let unified: WorkerPoolManager | undefined
let split: WorkerPoolManager | undefined
let plain: WorkerPoolManager | undefined
let diff: WorkerPoolManager | undefined
export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolManager | undefined {
export function getWorkerPool(lineDiffType: "none" | "word-alt" = "word-alt"): WorkerPoolManager | undefined {
if (typeof window === "undefined") return
if (style === "split") {
if (!split) split = createPool("word-alt")
return split
if (lineDiffType === "none") {
if (!plain) plain = createPool("none")
return plain
}
if (!unified) unified = createPool("none")
return unified
if (!diff) diff = createPool("word-alt")
return diff
}
export function getWorkerPools() {
const pool = getWorkerPool()
return {
unified: getWorkerPool("unified"),
split: getWorkerPool("split"),
unified: pool,
split: pool,
}
}

View file

@ -152,7 +152,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
const locale = useLocale()
createEffect(() => {
getWorkerPool(props.diffStyle)
getWorkerPool()
})
const fileIndex = () => {