Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-patches-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Render requested patch bodies in human-readable `hunk session review --include-patch` output and omit the empty `Notes:` section when a review has no notes.
68 changes: 68 additions & 0 deletions src/session/agent/cliClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ describe("Hunk session CLI formatters", () => {
additions: 2,
deletions: 1,
hunkCount: 2,
patch: undefined,
hunks: [
createTestSessionReviewHunk({ index: 0, header: "@@ -1,1 +1,2 @@" }),
createTestSessionReviewHunk({ index: 1, header: "@@ -10,1 +11,1 @@" }),
Expand All @@ -533,6 +534,7 @@ describe("Hunk session CLI formatters", () => {
additions: 0,
deletions: 1,
hunkCount: 1,
patch: undefined,
});

expect(
Expand Down Expand Up @@ -570,6 +572,72 @@ describe("Hunk session CLI formatters", () => {
);
});

test("review output includes resolved patch bodies without changing their diff lines", () => {
const output = formatReviewOutput(
createTestSessionReview({
files: [
createTestSessionReviewFile({
path: "added.txt",
patch: [
"diff --git a/added.txt b/added.txt",
"--- /dev/null",
"+++ b/added.txt",
"@@ -0,0 +1,2 @@",
"+first",
"+second",
].join("\n"),
}),
],
}),
);

expect(output).toContain(
" patch:\ndiff --git a/added.txt b/added.txt\n--- /dev/null\n+++ b/added.txt\n@@ -0,0 +1,2 @@\n+first\n+second\n",
);
});

test("review output neutralizes patch controls while preserving diff layout", () => {
const output = formatReviewOutput(
createTestSessionReview({
files: [
createTestSessionReviewFile({
path: "unsafe.txt",
patch: "@@ -0,0 +1 @@\n+before\x1b[2Jafter\n+\tindented",
}),
],
}),
);

expect(output).not.toContain("\x1b");
expect(output).toContain("@@ -0,0 +1 @@\n+beforeafter\n+\tindented\n");
});

test("review output renders populated notes but omits an empty Notes section", () => {
const withoutNotes = formatReviewOutput(createTestSessionReview({ reviewNotes: [] }));
const withNotes = formatReviewOutput(
createTestSessionReview({
reviewNoteCount: 1,
reviewNotes: [
{
noteId: "user:1",
source: "user",
filePath: "src/app.ts",
body: "Please simplify this.",
author: "user",
createdAt: "2026-05-10T00:00:00.000Z",
editable: true,
},
],
}),
);

expect(withoutNotes).toContain("Review notes: 0\nFiles:");
expect(withoutNotes).not.toContain("Notes:");
expect(withNotes).toContain(
"Review notes: 1\nNotes:\n - user:1 [user] src/app.ts: Please simplify this.\nFiles:",
);
});

test("command result formatters describe comment and navigation side effects", () => {
expect(
formatNavigationOutput(selector, {
Expand Down
5 changes: 3 additions & 2 deletions src/session/agent/cliClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,10 +496,10 @@ export function formatReviewOutput(review: SessionReview) {
`Agent notes visible: ${review.showAgentNotes ? "yes" : "no"}`,
`Live comments: ${review.liveCommentCount}`,
`Review notes: ${review.reviewNoteCount ?? review.reviewNotes?.length ?? 0}`,
...(review.reviewNotes
...((review.reviewNotes?.length ?? 0) > 0
? [
"Notes:",
...review.reviewNotes.map(
...review.reviewNotes!.map(
(note) =>
` - ${note.noteId} [${note.source}] ${formatSessionPath(note.filePath)}: ${note.body}`,
),
Expand All @@ -509,6 +509,7 @@ export function formatReviewOutput(review: SessionReview) {
...review.files.flatMap((file) => [
` - ${formatSessionPath(file.path)} (+${file.additions} -${file.deletions}, hunks: ${file.hunkCount})`,
...file.hunks.map((hunk) => ` hunk ${hunk.index + 1}: ${hunk.header}`),
...(file.patch === undefined ? [] : [" patch:", sanitizeTerminalText(file.patch)]),
]),
"",
].join("\n");
Expand Down
Loading