-
Notifications
You must be signed in to change notification settings - Fork 74
feat(agents): export focused hunk prompts #288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
SalzDevs
wants to merge
1
commit into
modem-dev:main
Choose a base branch
from
SalzDevs:feat/agent-prompt-export
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { buildAgentPrompt, createAgentPromptFile, extractHunkPatch } from "./agentPrompt"; | ||
| import { createTestDiffFile, lines } from "../../test/helpers/diff-helpers"; | ||
|
|
||
| const patch = lines( | ||
| "diff --git a/example.ts b/example.ts", | ||
| "index 1111111..2222222 100644", | ||
| "--- a/example.ts", | ||
| "+++ b/example.ts", | ||
| "@@ -1 +1 @@", | ||
| "-const one = 1;", | ||
| "+const one = 2;", | ||
| "@@ -10 +10 @@", | ||
| "-const ten = 10;", | ||
| "+const ten = 11;", | ||
| ); | ||
|
|
||
| describe("agent prompt export", () => { | ||
| test("extracts one hunk with file headers from a raw patch", () => { | ||
| expect(extractHunkPatch(patch, 1)).toBe( | ||
| lines( | ||
| "diff --git a/example.ts b/example.ts", | ||
| "index 1111111..2222222 100644", | ||
| "--- a/example.ts", | ||
| "+++ b/example.ts", | ||
| "@@ -10 +10 @@", | ||
| "-const ten = 10;", | ||
| "+const ten = 11;", | ||
| ).trimEnd(), | ||
| ); | ||
| }); | ||
|
|
||
| test("builds a paste-ready prompt with comment, selected text, and diff hunk", () => { | ||
| const file = createTestDiffFile({ | ||
| before: "export const value = 1;\n", | ||
| after: "export const value = 2;\n", | ||
| path: "src/example.ts", | ||
| }); | ||
| const prompt = buildAgentPrompt({ | ||
| title: "demo working tree", | ||
| repoRoot: "/repo/demo", | ||
| file: createAgentPromptFile({ ...file, patch }), | ||
| hunkIndex: 0, | ||
| selectedText: "export const value = 2;", | ||
| comment: "Please make this configurable.", | ||
| }); | ||
|
|
||
| expect(prompt).toContain("Please use this Hunk review context"); | ||
| expect(prompt).toContain("- Repo: /repo/demo"); | ||
| expect(prompt).toContain("- File: src/example.ts"); | ||
| expect(prompt).toContain("My comment:\nPlease make this configurable."); | ||
| expect(prompt).toContain("Selected text from Hunk:\n```text\nexport const value = 2;\n```"); | ||
| expect(prompt).toContain("```diff\ndiff --git a/example.ts b/example.ts"); | ||
| expect(prompt).toContain("@@ -1 +1 @@"); | ||
| }); | ||
|
|
||
| test("falls back to the hunk header when raw patch text is unavailable", () => { | ||
| const file = createTestDiffFile({ path: "src/example.ts" }); | ||
| const prompt = buildAgentPrompt({ | ||
| file: createAgentPromptFile(file), | ||
| hunkIndex: 0, | ||
| }); | ||
|
|
||
| expect(prompt).toContain("Diff hunk:"); | ||
| expect(prompt).toContain("@@"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import type { DiffFile } from "./types"; | ||
| import { formatHunkHeader } from "./hunkHeader"; | ||
| import { hunkLineRange } from "./liveComments"; | ||
|
|
||
| export interface AgentPromptHunk { | ||
| index: number; | ||
| header: string; | ||
| oldRange?: [number, number]; | ||
| newRange?: [number, number]; | ||
| } | ||
|
|
||
| export interface AgentPromptFile { | ||
| path: string; | ||
| previousPath?: string; | ||
| patch?: string; | ||
| hunks: AgentPromptHunk[]; | ||
| } | ||
|
|
||
| export interface AgentPromptInput { | ||
| title?: string; | ||
| sourceLabel?: string; | ||
| repoRoot?: string; | ||
| file: AgentPromptFile; | ||
| hunkIndex: number; | ||
| selectedText?: string; | ||
| comment?: string; | ||
| } | ||
|
|
||
| function trimTrailingNewlines(value: string) { | ||
| return value.replace(/\n+$/, ""); | ||
| } | ||
|
|
||
| function codeFence(language: string, value: string) { | ||
| const longestFence = Math.max( | ||
| 2, | ||
| ...Array.from(value.matchAll(/`+/g), (match) => match[0].length), | ||
| ); | ||
| const fence = "`".repeat(longestFence + 1); | ||
| return `${fence}${language}\n${trimTrailingNewlines(value)}\n${fence}`; | ||
| } | ||
|
|
||
| function formatRange(range: [number, number] | undefined) { | ||
| if (!range) { | ||
| return "-"; | ||
| } | ||
|
|
||
| return range[0] === range[1] ? `${range[0]}` : `${range[0]}..${range[1]}`; | ||
| } | ||
|
|
||
| /** Convert one loaded diff file into the generic prompt-export shape. */ | ||
| export function createAgentPromptFile(file: DiffFile): AgentPromptFile { | ||
| return { | ||
| path: file.path, | ||
| previousPath: file.previousPath, | ||
| patch: file.patch, | ||
| hunks: file.metadata.hunks.map((hunk, index) => ({ | ||
| index, | ||
| header: formatHunkHeader(hunk), | ||
| ...hunkLineRange(hunk), | ||
| })), | ||
| }; | ||
| } | ||
|
|
||
| /** Extract one raw unified-diff hunk from a per-file patch, preserving file headers. */ | ||
| export function extractHunkPatch(patch: string | undefined, hunkIndex: number) { | ||
| if (!patch) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const normalizedPatch = patch.replaceAll("\r\n", "\n"); | ||
| const lines = normalizedPatch.split("\n"); | ||
| const hunkLineIndexes = lines.reduce<number[]>((indexes, line, index) => { | ||
| if (line.startsWith("@@ ")) { | ||
| indexes.push(index); | ||
| } | ||
|
|
||
| return indexes; | ||
| }, []); | ||
| const hunkStart = hunkLineIndexes[hunkIndex]; | ||
| if (hunkStart === undefined) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const firstHunkStart = hunkLineIndexes[0] ?? hunkStart; | ||
| const hunkEnd = hunkLineIndexes[hunkIndex + 1] ?? lines.length; | ||
| const headerLines = lines.slice(0, firstHunkStart).filter((line) => line.length > 0); | ||
| const hunkLines = lines.slice(hunkStart, hunkEnd); | ||
| const selectedLines = [...headerLines, ...hunkLines]; | ||
|
|
||
| return trimTrailingNewlines(selectedLines.join("\n")); | ||
| } | ||
|
|
||
| /** Build a paste-ready prompt for sending the focused Hunk context to a coding agent. */ | ||
| export function buildAgentPrompt({ | ||
| title, | ||
| sourceLabel, | ||
| repoRoot, | ||
| file, | ||
| hunkIndex, | ||
| selectedText, | ||
| comment, | ||
| }: AgentPromptInput) { | ||
| const hunk = file.hunks[hunkIndex]; | ||
| if (!hunk) { | ||
| throw new Error(`No hunk ${hunkIndex + 1} exists in ${file.path}.`); | ||
| } | ||
|
|
||
| const normalizedComment = comment?.trim(); | ||
| const normalizedSelection = selectedText?.trim(); | ||
| const diffSnippet = extractHunkPatch(file.patch, hunkIndex) ?? hunk.header; | ||
| const locationLines = [ | ||
| `- Repo: ${repoRoot ?? sourceLabel ?? "(unknown)"}`, | ||
| ...(title ? [`- Review: ${title}`] : []), | ||
| `- File: ${file.path}`, | ||
| ...(file.previousPath ? [`- Previous file: ${file.previousPath}`] : []), | ||
| `- Hunk: ${hunk.index + 1}`, | ||
| `- Old lines: ${formatRange(hunk.oldRange)}`, | ||
| `- New lines: ${formatRange(hunk.newRange)}`, | ||
| ]; | ||
|
|
||
| return trimTrailingNewlines( | ||
| [ | ||
| "Please use this Hunk review context to help me update the code.", | ||
| "", | ||
| "Context:", | ||
| ...locationLines, | ||
| ...(normalizedComment ? ["", "My comment:", normalizedComment] : []), | ||
| ...(normalizedSelection | ||
| ? ["", "Selected text from Hunk:", codeFence("text", normalizedSelection)] | ||
| : []), | ||
| "", | ||
| "Diff hunk:", | ||
| codeFence("diff", diffSnippet), | ||
| "", | ||
| "Please address my comment against this diff. If you need more surrounding code, ask before editing.", | ||
| ].join("\n"), | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pis used to copy a plain hunk prompt, there is no "My comment:" section, so the imperative becomes a dangling reference that may confuse the agent. The instruction should only appear when a comment or selected text is actually present, with a lighter-touch fallback for the context-only case.Prompt To Fix With AI