Reapply "fix(ui): support unified diff patch format in session changes viewer"

This reverts commit af6429162f.
This commit is contained in:
Shantur Rathore
2026-04-08 20:57:23 +01:00
parent b060ab45ff
commit 0739ec857c
4 changed files with 81 additions and 24 deletions

View File

@@ -1,15 +1,17 @@
import { createEffect, createSignal, onCleanup, onMount } from "solid-js" import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
import { loadMonaco } from "../../lib/monaco/setup" import { loadMonaco } from "../../lib/monaco/setup"
import { getOrCreateTextModel } from "../../lib/monaco/model-cache" import { getOrCreateTextModel } from "../../lib/monaco/model-cache"
import { inferMonacoLanguageId } from "../../lib/monaco/language" import { inferMonacoLanguageId } from "../../lib/monaco/language"
import { ensureMonacoLanguageLoaded } from "../../lib/monaco/setup" import { ensureMonacoLanguageLoaded } from "../../lib/monaco/setup"
import { useTheme } from "../../lib/theme" import { useTheme } from "../../lib/theme"
import { parsePatchToBeforeAfter } from "../../lib/diff-utils"
interface MonacoDiffViewerProps { interface MonacoDiffViewerProps {
scopeKey: string scopeKey: string
path: string path: string
before: string patch?: string
after: string before?: string
after?: string
viewMode?: "split" | "unified" viewMode?: "split" | "unified"
contextMode?: "expanded" | "collapsed" contextMode?: "expanded" | "collapsed"
wordWrap?: "on" | "off" wordWrap?: "on" | "off"
@@ -23,6 +25,16 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
let monaco: any = null let monaco: any = null
const [ready, setReady] = createSignal(false) const [ready, setReady] = createSignal(false)
const resolvedContent = createMemo(() => {
if (props.patch !== undefined && props.patch !== null) {
return parsePatchToBeforeAfter(props.patch)
}
return {
before: props.before ?? "",
after: props.after ?? "",
}
})
const disposeEditor = () => { const disposeEditor = () => {
try { try {
diffEditor?.setModel(null as any) diffEditor?.setModel(null as any)
@@ -115,11 +127,12 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
createEffect(() => { createEffect(() => {
if (!ready() || !monaco || !diffEditor) return if (!ready() || !monaco || !diffEditor) return
const languageId = inferMonacoLanguageId(monaco, props.path) const languageId = inferMonacoLanguageId(monaco, props.path)
const { before, after } = resolvedContent()
const beforeKey = `${props.scopeKey}:diff:${props.path}:before` const beforeKey = `${props.scopeKey}:diff:${props.path}:before`
const afterKey = `${props.scopeKey}:diff:${props.path}:after` const afterKey = `${props.scopeKey}:diff:${props.path}:after`
const original = getOrCreateTextModel({ monaco, cacheKey: beforeKey, value: props.before, languageId }) const original = getOrCreateTextModel({ monaco, cacheKey: beforeKey, value: before, languageId })
const modified = getOrCreateTextModel({ monaco, cacheKey: afterKey, value: props.after, languageId }) const modified = getOrCreateTextModel({ monaco, cacheKey: afterKey, value: after, languageId })
diffEditor.setModel({ original, modified }) diffEditor.setModel({ original, modified })
void ensureMonacoLanguageLoaded(languageId).then(() => { void ensureMonacoLanguageLoaded(languageId).then(() => {

View File

@@ -115,23 +115,22 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
} }
> >
{(file) => ( {(file) => (
<Suspense <Suspense
fallback={ fallback={
<div class="file-viewer-empty"> <div class="file-viewer-empty">
<span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span> <span class="file-viewer-empty-text">{props.t("instanceInfo.loading")}</span>
</div> </div>
} }
> >
<LazyMonacoDiffViewer <LazyMonacoDiffViewer
scopeKey={scopeKey()} scopeKey={scopeKey()}
path={String(file().file || "")} path={String(file().file || "")}
before={String((file() as any).before || "")} patch={String((file() as any).patch || "")}
after={String((file() as any).after || "")} viewMode={props.diffViewMode()}
viewMode={props.diffViewMode()} contextMode={props.diffContextMode()}
contextMode={props.diffContextMode()} wordWrap={props.diffWordWrapMode()}
wordWrap={props.diffWordWrapMode()} />
/> </Suspense>
</Suspense>
)} )}
</Show> </Show>
</div> </div>

View File

@@ -2,6 +2,7 @@ const HUNK_PATTERN = /(^|\n)@@/m
const FILE_MARKER_PATTERN = /(^|\n)(diff --git |--- |\+\+\+)/ const FILE_MARKER_PATTERN = /(^|\n)(diff --git |--- |\+\+\+)/
const BEGIN_PATCH_PATTERN = /^\*\*\* (Begin|End) Patch/ const BEGIN_PATCH_PATTERN = /^\*\*\* (Begin|End) Patch/
const UPDATE_FILE_PATTERN = /^\*\*\* Update File: (.+)$/ const UPDATE_FILE_PATTERN = /^\*\*\* Update File: (.+)$/
const HUNK_HEADER_PATTERN = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
function stripCodeFence(value: string): string { function stripCodeFence(value: string): string {
const trimmed = value.trim() const trimmed = value.trim()
@@ -48,3 +49,48 @@ export function isRenderableDiffText(raw?: string | null): raw is string {
if (!normalized) return false if (!normalized) return false
return HUNK_PATTERN.test(normalized) return HUNK_PATTERN.test(normalized)
} }
export function parsePatchToBeforeAfter(patch: string): { before: string; after: string } {
if (!patch || patch.trim().length === 0) {
return { before: "", after: "" }
}
const lines = patch.replace(/\r\n/g, "\n").split("\n")
const beforeLines: string[] = []
const afterLines: string[] = []
for (const line of lines) {
if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff --git")) {
continue
}
if (HUNK_HEADER_PATTERN.test(line)) {
continue
}
if (line.startsWith("-") && !line.startsWith("---")) {
beforeLines.push(line.slice(1))
} else if (line.startsWith("+") && !line.startsWith("+++")) {
afterLines.push(line.slice(1))
} else if (line.startsWith(" ")) {
beforeLines.push(line.slice(1))
afterLines.push(line.slice(1))
} else if (line === "") {
beforeLines.push("")
afterLines.push("")
} else {
beforeLines.push(line)
afterLines.push(line)
}
}
while (beforeLines.length > 0 && beforeLines[beforeLines.length - 1] === "") {
beforeLines.pop()
}
while (afterLines.length > 0 && afterLines[afterLines.length - 1] === "") {
afterLines.pop()
}
return {
before: beforeLines.join("\n"),
after: afterLines.join("\n"),
}
}

View File

@@ -4,8 +4,7 @@ import type {
Provider as SDKProvider, Provider as SDKProvider,
Model as SDKModel, Model as SDKModel,
} from "@opencode-ai/sdk" } from "@opencode-ai/sdk"
import type { SessionStatus as SDKSessionStatus } from "@opencode-ai/sdk/v2/client" import type { SessionStatus as SDKSessionStatus, FileDiff } from "@opencode-ai/sdk/v2/client"
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
// Export SDK types for external use // Export SDK types for external use
export type { export type {