优化消息里读/写内容显示样式

This commit is contained in:
xintaofei
2026-03-28 14:04:19 +08:00
parent afa67380e7
commit 8bd19738d0
22 changed files with 660 additions and 462 deletions

View File

@@ -1,5 +1,6 @@
import type { MessageTurn } from "./types"
import { normalizeToolName } from "./tool-call-normalization"
import { generateUnifiedDiff } from "./unified-diff-generator"
export type FileOperation = "read" | "edit" | "write" | "apply_patch"
@@ -266,32 +267,12 @@ function countDiffLines(text: string): DiffStat {
return { additions, deletions }
}
function createHunkHeader(oldLineCount: number, newLineCount: number): string {
const oldStart = oldLineCount === 0 ? 0 : 1
const newStart = newLineCount === 0 ? 0 : 1
return `@@ -${oldStart},${oldLineCount} +${newStart},${newLineCount} @@`
}
function buildUnifiedDiff(
filePath: string,
oldText: string,
newText: string
): string | null {
if (!oldText && !newText) return null
const oldLines = oldText ? oldText.split("\n") : []
const newLines = newText ? newText.split("\n") : []
const lines: string[] = [
`--- a/${filePath}`,
`+++ b/${filePath}`,
createHunkHeader(oldLines.length, newLines.length),
]
for (const line of oldLines) lines.push(`-${line}`)
for (const line of newLines) lines.push(`+${line}`)
return lines.join("\n")
return generateUnifiedDiff(oldText, newText, filePath)
}
function parseEditChangeValue(value: unknown): EditChangePreview | null {

View File

@@ -0,0 +1,180 @@
import { computeLineDiff, type DiffHunk } from "@/components/merge/merge-diff"
/**
* Maximum product of line counts before falling back to naive diff.
* Avoids O(n*m) LCS blowup for very large inputs.
*/
const LCS_PAIR_BUDGET = 200_000
/**
* Generate a unified diff string from old and new text.
*
* Uses LCS-based line diff when within budget, falls back to
* simple "all deletions then all additions" for very large inputs.
*/
export function generateUnifiedDiff(
oldText: string,
newText: string,
filePath?: string,
contextLines: number = 3
): string | null {
if (!oldText && !newText) return null
if (oldText === newText) return null
const oldLines = oldText ? splitLines(oldText) : []
const newLines = newText ? splitLines(newText) : []
const path = filePath ?? "file"
const header = `--- a/${path}\n+++ b/${path}`
// Performance gate: fall back to naive diff for large inputs
if (oldLines.length * newLines.length > LCS_PAIR_BUDGET) {
return buildNaiveDiff(header, oldLines, newLines)
}
const hunks = computeLineDiff(oldLines, newLines)
if (hunks.length === 0) return null
const unifiedHunks = buildUnifiedHunks(oldLines, hunks, contextLines)
return `${header}\n${unifiedHunks}`
}
function splitLines(text: string): string[] {
const lines = text.split("\n")
// Remove trailing empty line from trailing newline
if (lines.length > 0 && lines[lines.length - 1] === "") {
lines.pop()
}
return lines
}
/**
* Naive diff: all deletions first, then all additions, with a single hunk header.
* Used as fallback when inputs are too large for LCS.
*/
function buildNaiveDiff(
header: string,
oldLines: string[],
newLines: string[]
): string {
const oldStart = oldLines.length === 0 ? 0 : 1
const newStart = newLines.length === 0 ? 0 : 1
const hunkHeader = `@@ -${oldStart},${oldLines.length} +${newStart},${newLines.length} @@`
const parts = [header, hunkHeader]
for (const line of oldLines) parts.push(`-${line}`)
for (const line of newLines) parts.push(`+${line}`)
return parts.join("\n")
}
/**
* Convert DiffHunk[] into unified diff text with context lines and hunk headers.
*
* Groups nearby hunks that overlap in their context windows into a single
* unified hunk, producing output similar to `diff -u`.
*/
function buildUnifiedHunks(
oldLines: string[],
hunks: DiffHunk[],
contextLines: number
): string {
// Build "change regions" with context, then merge overlapping ones
const regions = hunks.map((hunk) => ({
// Context-expanded range in old lines
ctxOldStart: Math.max(0, hunk.baseStart - contextLines),
ctxOldEnd: Math.min(
oldLines.length,
hunk.baseStart + hunk.baseCount + contextLines
),
hunk,
}))
// Merge overlapping regions
const merged: {
ctxOldStart: number
ctxOldEnd: number
hunks: DiffHunk[]
}[] = []
for (const region of regions) {
const last = merged[merged.length - 1]
if (last && region.ctxOldStart <= last.ctxOldEnd) {
// Overlapping — extend and add hunk
last.ctxOldEnd = Math.max(last.ctxOldEnd, region.ctxOldEnd)
last.hunks.push(region.hunk)
} else {
merged.push({
ctxOldStart: region.ctxOldStart,
ctxOldEnd: region.ctxOldEnd,
hunks: [region.hunk],
})
}
}
// Render each merged region as a unified hunk
const output: string[] = []
for (const group of merged) {
const lines: string[] = []
let oldCursor = group.ctxOldStart
let newLineCount = 0
const oldLineCount = group.ctxOldEnd - group.ctxOldStart
for (const hunk of group.hunks) {
// Context lines before this change
while (oldCursor < hunk.baseStart) {
lines.push(` ${oldLines[oldCursor]}`)
newLineCount++
oldCursor++
}
// Deleted lines
for (let i = 0; i < hunk.baseCount; i++) {
lines.push(`-${oldLines[hunk.baseStart + i]}`)
oldCursor++
}
// Added lines
for (const newLine of hunk.newLines) {
lines.push(`+${newLine}`)
newLineCount++
}
}
// Trailing context
while (oldCursor < group.ctxOldEnd) {
lines.push(` ${oldLines[oldCursor]}`)
newLineCount++
oldCursor++
}
// Compute hunk header
const oldStart = oldLineCount === 0 ? 0 : group.ctxOldStart + 1
const newStart =
newLineCount === 0
? 0
: group.ctxOldStart +
1 +
computeNewOffset(group.hunks, group.ctxOldStart)
output.push(
`@@ -${oldStart},${oldLineCount} +${newStart},${newLineCount} @@`
)
output.push(...lines)
}
return output.join("\n")
}
/**
* Compute the offset applied to new-line numbering by hunks before a given position.
*/
function computeNewOffset(hunks: DiffHunk[], beforeOldLine: number): number {
let offset = 0
for (const hunk of hunks) {
if (hunk.baseStart >= beforeOldLine) break
offset += hunk.newLines.length - hunk.baseCount
}
return offset
}