mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 22:03:14 +00:00
fix: Simplify review explanation: full-width, plain prose, no diff links (#1547)
* Simplify review explanation: full-width, plain prose, no diff links * Update _build_prompt test for plain diff fences --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
17f0ca585f
commit
e58b609b2f
7 changed files with 33 additions and 190 deletions
|
|
@ -52,10 +52,9 @@ class _DiffGroupModel(BaseModel):
|
|||
)
|
||||
summary: str = Field(
|
||||
description=(
|
||||
"GitHub-flavored markdown walkthrough of this group: a few short "
|
||||
"sentences or bullets, code identifiers in `backticks`, optionally one "
|
||||
"short fenced code block of the key snippet, and concrete locations "
|
||||
"linked as [path:start-end](#loc=path:start-end)."
|
||||
"A short, plain explanation of what this group changes and why, in a "
|
||||
"few sentences a reviewer can skim. Wrap code identifiers, symbols, "
|
||||
"types, flags, and paths in `backticks`. No code blocks or links."
|
||||
)
|
||||
)
|
||||
files: list[str] = Field(
|
||||
|
|
@ -82,24 +81,18 @@ Rules:
|
|||
- Use at most {max_groups} groups. Prefer fewer, larger groups over many tiny ones.
|
||||
- title: a short headline naming the change, roughly 4-10 words, no trailing \
|
||||
punctuation. Wrap code identifiers (symbols, flags, file names) in `backticks`.
|
||||
- summary: GitHub-flavored markdown explaining what the group changes and why, \
|
||||
written as a short walkthrough a reviewer can skim:
|
||||
- Keep it focused — a few short sentences or bullets. Do not restate the diff line by line.
|
||||
- summary: a short, plain explanation of what the group changes and why, that a \
|
||||
reviewer can skim:
|
||||
- Keep it focused — a few short sentences. Do not restate the diff line by line.
|
||||
- Wrap every code identifier, symbol, type, flag, and path in `backticks`.
|
||||
- When one change is central, include at most ONE short fenced code block \
|
||||
(```lang, <= ~8 lines) of the key snippet — not the whole hunk.
|
||||
- Reference concrete locations as markdown links of the EXACT form \
|
||||
[path:start-end](#loc=path:start-end), where path is the verbatim changed-file \
|
||||
path and start/end are line numbers from the "lines X-Y" annotations below \
|
||||
(use [path:line](#loc=path:line) when start == end). Prefer these links over \
|
||||
describing locations in prose — they let the reader jump straight to the hunk.
|
||||
- Plain prose only — no code blocks and no links.
|
||||
- files: the exact file paths (copied verbatim from the list below) in this \
|
||||
group, in the order a reviewer should read them.
|
||||
|
||||
Changed files:
|
||||
{file_list}
|
||||
|
||||
Diffs (each hunk is annotated with its line range in the new file):
|
||||
Diffs:
|
||||
{diffs}
|
||||
"""
|
||||
|
||||
|
|
@ -122,9 +115,7 @@ def _build_prompt(diff_text: str, files: list[str]) -> str:
|
|||
body = hunk.body
|
||||
if len(body) > MAX_FILE_HUNK_CHARS:
|
||||
body = body[:MAX_FILE_HUNK_CHARS] + "\n... (truncated)"
|
||||
# Surface the new-file line range so the model can cite accurate
|
||||
# locations in its summary links.
|
||||
segments.append(f"lines {hunk.new_start}-{hunk.new_end}:\n```diff\n{body}\n```")
|
||||
segments.append(f"```diff\n{body}\n```")
|
||||
block = f"### {file_diff.file}\n" + "\n".join(segments) + "\n"
|
||||
if budget - len(block) < 0:
|
||||
parts.append(f"### {file_diff.file}\n(diff omitted — prompt budget reached)\n")
|
||||
|
|
|
|||
|
|
@ -97,14 +97,13 @@ async def test_generate_diff_groups_llm_failure_returns_none() -> None:
|
|||
assert groups is None
|
||||
|
||||
|
||||
def test_build_prompt_annotates_hunk_line_ranges() -> None:
|
||||
def test_build_prompt_includes_files_and_diffs() -> None:
|
||||
prompt = _build_prompt(_DIFF, ["foo.py", "bar.py"])
|
||||
assert "### foo.py" in prompt
|
||||
assert "### bar.py" in prompt
|
||||
# New-file line ranges from the @@ headers are surfaced so the model can
|
||||
# cite accurate locations: foo.py @@ -1,2 +1,3 @@ and bar.py @@ -1 +1,2 @@.
|
||||
assert "lines 1-3:" in prompt
|
||||
assert "lines 1-2:" in prompt
|
||||
# Hunks are rendered as plain diff fences (no line-range annotations).
|
||||
assert "lines 1-3:" not in prompt
|
||||
assert "```diff" in prompt
|
||||
# The verbatim file list is included so every file can be assigned.
|
||||
assert "- foo.py" in prompt
|
||||
assert "- bar.py" in prompt
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ export interface ReviewSidebarData {
|
|||
view: ReviewSidebarView
|
||||
onViewChange: (view: ReviewSidebarView) => void
|
||||
onSelectGroup: (index: number) => void
|
||||
onLocationClick?: (file: string, startLine: number, endLine: number) => void
|
||||
}
|
||||
|
||||
const ReviewSidebarContext = createContext<{
|
||||
|
|
@ -105,7 +104,6 @@ export function ReviewSidebarPanel({ data }: { data: ReviewSidebarData }) {
|
|||
groups={data.groups ?? []}
|
||||
onSelectGroup={data.onSelectGroup}
|
||||
onSelectFile={data.onSelect}
|
||||
onLocationClick={data.onLocationClick}
|
||||
/>
|
||||
) : !data.files ? (
|
||||
<div className="px-4 pt-1">
|
||||
|
|
@ -183,12 +181,10 @@ function ReviewGroupList({
|
|||
groups,
|
||||
onSelectGroup,
|
||||
onSelectFile,
|
||||
onLocationClick,
|
||||
}: {
|
||||
groups: Array<ReviewSidebarGroup>
|
||||
onSelectGroup: (index: number) => void
|
||||
onSelectFile: (path: string) => void
|
||||
onLocationClick?: (file: string, startLine: number, endLine: number) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-0 flex-1 divide-y divide-[var(--ui-border-subtle)] overflow-y-auto">
|
||||
|
|
@ -198,7 +194,6 @@ function ReviewGroupList({
|
|||
group={group}
|
||||
onSelect={() => onSelectGroup(group.index)}
|
||||
onSelectFile={onSelectFile}
|
||||
onLocationClick={onLocationClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -211,6 +206,12 @@ function splitPath(path: string): { dir: string; base: string } {
|
|||
return { dir: path.slice(0, idx), base: path.slice(idx + 1) }
|
||||
}
|
||||
|
||||
// Older stored summaries embed `[label](#loc=path:line)` diff links. Render the
|
||||
// label as inline code instead so no stale jump-links leak into the explanation.
|
||||
function stripLocationLinks(summary: string): string {
|
||||
return summary.replace(/\[([^\]]+)\]\(#loc=[^)]*\)/g, "`$1`")
|
||||
}
|
||||
|
||||
// Render a title with `backtick`-delimited spans as inline code chips, matching
|
||||
// the Markdown component's inline-code styling, without pulling in the full
|
||||
// block renderer for a single line.
|
||||
|
|
@ -234,12 +235,10 @@ function ReviewGroupRow({
|
|||
group,
|
||||
onSelect,
|
||||
onSelectFile,
|
||||
onLocationClick,
|
||||
}: {
|
||||
group: ReviewSidebarGroup
|
||||
onSelect: () => void
|
||||
onSelectFile: (path: string) => void
|
||||
onLocationClick?: (file: string, startLine: number, endLine: number) => void
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
return (
|
||||
|
|
@ -297,7 +296,7 @@ function ReviewGroupRow({
|
|||
)}
|
||||
|
||||
{group.summary && (
|
||||
<div className="mt-2 pl-7">
|
||||
<div className="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
|
|
@ -313,10 +312,7 @@ function ReviewGroupRow({
|
|||
</button>
|
||||
{expanded && (
|
||||
<div className="mt-1.5">
|
||||
<Markdown
|
||||
content={group.summary}
|
||||
onLocationClick={onLocationClick}
|
||||
/>
|
||||
<Markdown content={stripLocationLinks(group.summary)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { memo, useMemo } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { parseLocationHref } from "./markdownLocation";
|
||||
import type { ReactNode } from "react";
|
||||
import "streamdown/styles.css";
|
||||
|
||||
|
|
@ -8,12 +7,6 @@ interface MarkdownProps {
|
|||
content: string;
|
||||
/** When true, keep Streamdown in streaming mode for the duration of the run. */
|
||||
isLive?: boolean;
|
||||
/**
|
||||
* When set, `#loc=path:start-end` links render as in-page buttons that invoke
|
||||
* this callback instead of navigating. Used by the review view to jump the
|
||||
* diff to a referenced hunk.
|
||||
*/
|
||||
onLocationClick?: (file: string, startLine: number, endLine: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -86,37 +79,22 @@ const SHIKI_THEME: ["github-light", "github-dark"] = ["github-light", "github-da
|
|||
export const Markdown = memo(function Markdown({
|
||||
content,
|
||||
isLive = false,
|
||||
onLocationClick,
|
||||
}: MarkdownProps) {
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
...STREAMDOWN_COMPONENTS,
|
||||
a: ({ href, children }: { href?: string; children?: ReactNode }) => {
|
||||
const loc = onLocationClick ? parseLocationHref(href) : null;
|
||||
if (loc && onLocationClick) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onLocationClick(loc.file, loc.startLine, loc.endLine)}
|
||||
className="font-mono text-[0.85em] text-[color:var(--ui-accent)] underline decoration-[color:var(--ui-accent)]/50 break-words [overflow-wrap:anywhere]"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
className="text-[color:var(--ui-accent)] underline decoration-[color:var(--ui-accent)]/50 break-words [overflow-wrap:anywhere]"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
a: ({ href, children }: { href?: string; children?: ReactNode }) => (
|
||||
<a
|
||||
className="text-[color:var(--ui-accent)] underline decoration-[color:var(--ui-accent)]/50 break-words [overflow-wrap:anywhere]"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}),
|
||||
[onLocationClick]
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseLocationHref } from "./markdownLocation";
|
||||
|
||||
describe("parseLocationHref", () => {
|
||||
it("parses a path with a start-end range", () => {
|
||||
expect(parseLocationHref("#loc=src/foo.rs:231-232")).toEqual({
|
||||
file: "src/foo.rs",
|
||||
startLine: 231,
|
||||
endLine: 232,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a single-line ref (no end)", () => {
|
||||
expect(parseLocationHref("#loc=mod.rs:42")).toEqual({
|
||||
file: "mod.rs",
|
||||
startLine: 42,
|
||||
endLine: 42,
|
||||
});
|
||||
});
|
||||
|
||||
it("tolerates an origin prefix the URL transform may prepend", () => {
|
||||
expect(parseLocationHref("http://localhost/#loc=a/b.ts:5-9")).toEqual({
|
||||
file: "a/b.ts",
|
||||
startLine: 5,
|
||||
endLine: 9,
|
||||
});
|
||||
});
|
||||
|
||||
it("decodes percent-encoded hrefs", () => {
|
||||
expect(parseLocationHref("#loc=a%20b/c.ts:1-2")).toEqual({
|
||||
file: "a b/c.ts",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("clamps an end below the start to a single line", () => {
|
||||
expect(parseLocationHref("#loc=x.ts:10-3")).toEqual({
|
||||
file: "x.ts",
|
||||
startLine: 10,
|
||||
endLine: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for non-location and malformed hrefs", () => {
|
||||
expect(parseLocationHref(undefined)).toBeNull();
|
||||
expect(parseLocationHref("https://example.com")).toBeNull();
|
||||
expect(parseLocationHref("#loc=noline")).toBeNull();
|
||||
expect(parseLocationHref("#loc=:5")).toBeNull();
|
||||
expect(parseLocationHref("#loc=x.ts:abc")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
export interface MarkdownLocation {
|
||||
file: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a `#loc=<path>:<start>[-<end>]` sentinel href emitted by the reviewer
|
||||
* diff-grouping pass into a file + line range. Tolerant of an origin prefix the
|
||||
* markdown URL transform may prepend, and of percent-encoding. Returns null for
|
||||
* any href that isn't a location link.
|
||||
*/
|
||||
export function parseLocationHref(href?: string): MarkdownLocation | null {
|
||||
if (!href) return null;
|
||||
const marker = href.indexOf("#loc=");
|
||||
if (marker === -1) return null;
|
||||
let raw = href.slice(marker + "#loc=".length);
|
||||
try {
|
||||
raw = decodeURIComponent(raw);
|
||||
} catch {
|
||||
// Keep raw as-is if it isn't valid percent-encoding.
|
||||
}
|
||||
const colon = raw.lastIndexOf(":");
|
||||
if (colon <= 0) return null;
|
||||
const file = raw.slice(0, colon);
|
||||
const [startStr, endStr] = raw.slice(colon + 1).split("-");
|
||||
const start = Number.parseInt(startStr ?? "", 10);
|
||||
if (!Number.isFinite(start) || start <= 0) return null;
|
||||
const end = endStr !== undefined ? Number.parseInt(endStr, 10) : start;
|
||||
return {
|
||||
file,
|
||||
startLine: start,
|
||||
endLine: Number.isFinite(end) && end >= start ? end : start,
|
||||
};
|
||||
}
|
||||
|
|
@ -222,11 +222,6 @@ function ReviewBody({
|
|||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null)
|
||||
const groupRefs = useRef<Record<number, HTMLDivElement | null>>({})
|
||||
const [selectedRange, setSelectedRange] = useState<{
|
||||
file: string
|
||||
start: number
|
||||
end: number
|
||||
} | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
void warmDiffHighlighter()
|
||||
|
|
@ -405,21 +400,6 @@ function ReviewBody({
|
|||
})
|
||||
}, [])
|
||||
|
||||
const scrollToLineRange = useCallback(
|
||||
(file: string, start: number, end: number) => {
|
||||
setSelectedFile(file)
|
||||
setSelectedRange({ file, start, end })
|
||||
setExpandedFiles((prev) => ({ ...prev, [file]: true }))
|
||||
requestAnimationFrame(() => {
|
||||
fileRefs.current[file]?.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
})
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const openFinding = useCallback(
|
||||
(finding: ReviewFinding) => {
|
||||
markRead(finding.id)
|
||||
|
|
@ -447,7 +427,6 @@ function ReviewBody({
|
|||
file={file}
|
||||
findings={findingsByFile.get(file.path) ?? []}
|
||||
focused={focused}
|
||||
selectedRange={selectedRange}
|
||||
viewed={viewed.has(file.path)}
|
||||
onToggleViewed={() => {
|
||||
const becomingViewed = !viewed.has(file.path)
|
||||
|
|
@ -485,7 +464,6 @@ function ReviewBody({
|
|||
view,
|
||||
onViewChange: setView,
|
||||
onSelectGroup: scrollToGroup,
|
||||
onLocationClick: scrollToLineRange,
|
||||
}),
|
||||
[
|
||||
detail.number,
|
||||
|
|
@ -497,7 +475,6 @@ function ReviewBody({
|
|||
view,
|
||||
setView,
|
||||
scrollToGroup,
|
||||
scrollToLineRange,
|
||||
]
|
||||
)
|
||||
useRegisterReviewSidebar(sidebarData)
|
||||
|
|
@ -689,7 +666,6 @@ function FileDiffCard({
|
|||
file,
|
||||
findings,
|
||||
focused,
|
||||
selectedRange,
|
||||
viewed,
|
||||
onToggleViewed,
|
||||
expanded,
|
||||
|
|
@ -701,7 +677,6 @@ function FileDiffCard({
|
|||
file: ReviewDiffFile
|
||||
findings: Array<ReviewFinding>
|
||||
focused: ReviewFinding | null
|
||||
selectedRange: { file: string; start: number; end: number } | null
|
||||
viewed: boolean
|
||||
onToggleViewed: () => void
|
||||
expanded: boolean
|
||||
|
|
@ -728,16 +703,8 @@ function FileDiffCard({
|
|||
if (focused?.file === file.path && isAnchored(focused)) {
|
||||
return findingSelectedRange(focused)
|
||||
}
|
||||
if (selectedRange?.file === file.path) {
|
||||
return {
|
||||
start: selectedRange.start,
|
||||
end: selectedRange.end,
|
||||
side: "additions",
|
||||
endSide: "additions",
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [focused, selectedRange, file.path])
|
||||
}, [focused, file.path])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue