From 0739ec857cb82532df75fcf98549386aa158f5b5 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 8 Apr 2026 20:57:23 +0100 Subject: [PATCH] Reapply "fix(ui): support unified diff patch format in session changes viewer" This reverts commit af6429162f89055526cde70b6133dbcc14909584. --- .../file-viewer/monaco-diff-viewer.tsx | 23 ++++++++-- .../shell/right-panel/tabs/ChangesTab.tsx | 33 +++++++------ packages/ui/src/lib/diff-utils.ts | 46 +++++++++++++++++++ packages/ui/src/types/session.ts | 3 +- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx b/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx index ace89d5f..6856fb24 100644 --- a/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx +++ b/packages/ui/src/components/file-viewer/monaco-diff-viewer.tsx @@ -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 { getOrCreateTextModel } from "../../lib/monaco/model-cache" import { inferMonacoLanguageId } from "../../lib/monaco/language" import { ensureMonacoLanguageLoaded } from "../../lib/monaco/setup" import { useTheme } from "../../lib/theme" +import { parsePatchToBeforeAfter } from "../../lib/diff-utils" interface MonacoDiffViewerProps { scopeKey: string path: string - before: string - after: string + patch?: string + before?: string + after?: string viewMode?: "split" | "unified" contextMode?: "expanded" | "collapsed" wordWrap?: "on" | "off" @@ -23,6 +25,16 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) { let monaco: any = null 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 = () => { try { diffEditor?.setModel(null as any) @@ -115,11 +127,12 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) { createEffect(() => { if (!ready() || !monaco || !diffEditor) return const languageId = inferMonacoLanguageId(monaco, props.path) + const { before, after } = resolvedContent() const beforeKey = `${props.scopeKey}:diff:${props.path}:before` const afterKey = `${props.scopeKey}:diff:${props.path}:after` - const original = getOrCreateTextModel({ monaco, cacheKey: beforeKey, value: props.before, languageId }) - const modified = getOrCreateTextModel({ monaco, cacheKey: afterKey, value: props.after, languageId }) + const original = getOrCreateTextModel({ monaco, cacheKey: beforeKey, value: before, languageId }) + const modified = getOrCreateTextModel({ monaco, cacheKey: afterKey, value: after, languageId }) diffEditor.setModel({ original, modified }) void ensureMonacoLanguageLoaded(languageId).then(() => { diff --git a/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx b/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx index 307c5cce..6f6a5609 100644 --- a/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx +++ b/packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx @@ -115,23 +115,22 @@ const ChangesTab: Component = (props) => { } > {(file) => ( - - {props.t("instanceInfo.loading")} - - } - > - - + + {props.t("instanceInfo.loading")} + + } + > + + )} diff --git a/packages/ui/src/lib/diff-utils.ts b/packages/ui/src/lib/diff-utils.ts index cca3dc9d..e2fc6954 100644 --- a/packages/ui/src/lib/diff-utils.ts +++ b/packages/ui/src/lib/diff-utils.ts @@ -2,6 +2,7 @@ const HUNK_PATTERN = /(^|\n)@@/m const FILE_MARKER_PATTERN = /(^|\n)(diff --git |--- |\+\+\+)/ const BEGIN_PATCH_PATTERN = /^\*\*\* (Begin|End) Patch/ const UPDATE_FILE_PATTERN = /^\*\*\* Update File: (.+)$/ +const HUNK_HEADER_PATTERN = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/ function stripCodeFence(value: string): string { const trimmed = value.trim() @@ -48,3 +49,48 @@ export function isRenderableDiffText(raw?: string | null): raw is string { if (!normalized) return false 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"), + } +} diff --git a/packages/ui/src/types/session.ts b/packages/ui/src/types/session.ts index 179b712a..de1a6e8d 100644 --- a/packages/ui/src/types/session.ts +++ b/packages/ui/src/types/session.ts @@ -4,8 +4,7 @@ import type { Provider as SDKProvider, Model as SDKModel, } from "@opencode-ai/sdk" -import type { SessionStatus as SDKSessionStatus } from "@opencode-ai/sdk/v2/client" -import type { FileDiff } from "@opencode-ai/sdk/v2/client" +import type { SessionStatus as SDKSessionStatus, FileDiff } from "@opencode-ai/sdk/v2/client" // Export SDK types for external use export type {