Skip to content

Commit df86c68

Browse files
authored
Show pull request descriptions in review view (#99)
1 parent a239235 commit df86c68

19 files changed

Lines changed: 1075 additions & 58 deletions

bin/walkthrough-guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ choose.
6161
- Avoid generic filler, broad assurance language, and meta-explanatory labels.
6262
- Do not invent bugs, risks, tests, or validation. Describe what the diff and conversation
6363
actually support.
64+
- If a PR/MR description is available, use it only as author intent and orientation. Do not copy it
65+
into the walkthrough JSON; the diff and hunk ids remain the source of truth.
6466

6567
## hunkId format
6668

core/App.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,48 @@ html[data-codiff-platform='darwin'] .sidebar {
15201520
padding: 5px 7px;
15211521
}
15221522

1523+
.codiff-header-toggle.codiff-header-toggle-static {
1524+
cursor: default;
1525+
grid-template-columns: minmax(0, 1fr);
1526+
}
1527+
1528+
.codiff-file-path.source-description-title {
1529+
font-family: var(--font-sans);
1530+
font-size: 16px;
1531+
font-weight: 600;
1532+
}
1533+
1534+
.source-description-author {
1535+
align-items: center;
1536+
color: var(--text);
1537+
display: inline-flex;
1538+
font: 600 13px/1 var(--font-sans);
1539+
gap: 7px;
1540+
max-width: 280px;
1541+
min-width: 0;
1542+
}
1543+
1544+
.source-description-author .gravatar {
1545+
border-radius: 50%;
1546+
corner-shape: round;
1547+
font-size: 9px;
1548+
height: 22px;
1549+
width: 22px;
1550+
}
1551+
1552+
.source-description-author span:last-child {
1553+
overflow: hidden;
1554+
text-overflow: ellipsis;
1555+
white-space: nowrap;
1556+
}
1557+
1558+
.source-description-markdown.codiff-markdown-preview:not(.editable) :is(h1, h2, h3, h4, h5, h6) {
1559+
border-bottom: none;
1560+
font-size: 15px;
1561+
margin: 14px 0 6px;
1562+
padding-bottom: 0;
1563+
}
1564+
15231565
.commit-details-trailers {
15241566
display: grid;
15251567
gap: 6px 10px;
@@ -1651,6 +1693,12 @@ html[data-codiff-platform='darwin'] .sidebar {
16511693
border-radius: 0;
16521694
}
16531695

1696+
.source-description-markdown-editor {
1697+
--mdx-editor-font-size: 13px;
1698+
--mdx-editor-line-height: 1.5;
1699+
--mdx-editor-padding: 0 24px 22px;
1700+
}
1701+
16541702
.codiff-markdown-preview-editor .mdx-editor-content a,
16551703
.review-comment-codex-reply-markdown .mdx-editor-content a {
16561704
color: var(--codiff-readonly-markdown-accent);

core/__tests__/App-render.test.tsx

Lines changed: 302 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type {
2323
ReviewSource,
2424
} from '../types.ts';
2525
import { createChangedFile } from './helpers/fixtures.ts';
26-
import { waitFor } from './helpers/react.tsx';
26+
import { renderReact, waitFor } from './helpers/react.tsx';
2727

2828
const reactActEnvironment = globalThis as typeof globalThis & {
2929
ResizeObserver?: typeof ResizeObserver;
@@ -1059,6 +1059,307 @@ test('commit details render inline in the diff view', async () => {
10591059
}
10601060
});
10611061

1062+
test('pull request descriptions render as provider-aware Markdown source context', async () => {
1063+
const changedFile = createChangedFile('src/app.ts');
1064+
const cases: ReadonlyArray<{
1065+
label: string;
1066+
source: Extract<ReviewSource, { type: 'pull-request' }>;
1067+
}> = [
1068+
{
1069+
label: 'PR description',
1070+
source: {
1071+
author: {
1072+
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
1073+
login: 'octocat',
1074+
url: 'https://github.com/octocat',
1075+
},
1076+
description: '## Intent\n\nShip **review** context.',
1077+
number: 12,
1078+
provider: 'github',
1079+
type: 'pull-request',
1080+
url: 'https://github.com/nkzw-tech/codiff/pull/12',
1081+
},
1082+
},
1083+
{
1084+
label: 'MR description',
1085+
source: {
1086+
description: '## Intent\n\nShip **review** context.',
1087+
number: 13,
1088+
provider: 'gitlab',
1089+
type: 'pull-request',
1090+
url: 'https://gitlab.example.com/group/project/-/merge_requests/13',
1091+
},
1092+
},
1093+
{
1094+
label: 'Description',
1095+
source: {
1096+
description: '## Intent\n\nShip **review** context.',
1097+
number: 14,
1098+
type: 'pull-request',
1099+
url: 'https://example.com/reviews/14',
1100+
},
1101+
},
1102+
];
1103+
1104+
for (const { label, source } of cases) {
1105+
window.codiff = createCodiffMock({
1106+
getRepositoryState: vi.fn(async () => ({
1107+
...repositoryState,
1108+
files: [changedFile],
1109+
source,
1110+
})),
1111+
});
1112+
1113+
const app = await renderReact(<App />);
1114+
1115+
try {
1116+
await waitFor(() => {
1117+
expect(app.container.querySelector('.codiff-source-description-header')).not.toBeNull();
1118+
});
1119+
const header = app.container.querySelector<HTMLElement>('.codiff-source-description-header');
1120+
const body = app.container.querySelector<HTMLElement>('.source-description-markdown');
1121+
expect(body?.textContent).toContain('Intent');
1122+
expect(body?.textContent).toContain('Ship review context.');
1123+
expect(header?.querySelector('.codiff-file-path')?.textContent).toBe(label);
1124+
expect(header?.querySelector('.source-description-title')).toBeNull();
1125+
if (source.author) {
1126+
expect(header?.querySelector('.source-description-author')?.textContent).toContain(
1127+
`@${source.author.login}`,
1128+
);
1129+
} else {
1130+
expect(header?.querySelector('.source-description-author')).toBeNull();
1131+
}
1132+
const toggle = header?.querySelector<HTMLButtonElement>('button.codiff-header-toggle');
1133+
expect(toggle).not.toBeNull();
1134+
expect(toggle?.getAttribute('aria-expanded')).toBe('true');
1135+
expect(toggle?.type).toBe('button');
1136+
} finally {
1137+
await app.cleanup();
1138+
}
1139+
}
1140+
});
1141+
1142+
test('pull request description collapse button toggles the markdown body', async () => {
1143+
const source = {
1144+
description: '## Intent\n\nShip **review** context.',
1145+
number: 12,
1146+
provider: 'github',
1147+
type: 'pull-request',
1148+
url: 'https://github.com/nkzw-tech/codiff/pull/12',
1149+
} satisfies ReviewSource;
1150+
1151+
window.codiff = createCodiffMock({
1152+
getRepositoryState: vi.fn(async () => ({
1153+
...repositoryState,
1154+
files: [createChangedFile('src/app.ts')],
1155+
source,
1156+
})),
1157+
});
1158+
1159+
const app = await renderReact(<App />);
1160+
1161+
try {
1162+
await waitFor(() => {
1163+
expect(app.container.querySelector('.source-description-markdown')).not.toBeNull();
1164+
});
1165+
1166+
let toggle = app.container.querySelector<HTMLButtonElement>('button.codiff-header-toggle');
1167+
expect(toggle?.getAttribute('aria-expanded')).toBe('true');
1168+
expect(app.container.querySelector('.source-description-markdown')?.textContent).toContain(
1169+
'Ship review context.',
1170+
);
1171+
1172+
await act(async () => {
1173+
toggle?.click();
1174+
});
1175+
1176+
await waitFor(() => {
1177+
toggle = app.container.querySelector<HTMLButtonElement>('button.codiff-header-toggle');
1178+
expect(toggle?.getAttribute('aria-expanded')).toBe('false');
1179+
expect(app.container.querySelector('.source-description-markdown')).toBeNull();
1180+
});
1181+
1182+
await act(async () => {
1183+
toggle?.click();
1184+
});
1185+
1186+
await waitFor(() => {
1187+
expect(
1188+
app.container
1189+
.querySelector<HTMLButtonElement>('button.codiff-header-toggle')
1190+
?.getAttribute('aria-expanded'),
1191+
).toBe('true');
1192+
expect(app.container.querySelector('.source-description-markdown')?.textContent).toContain(
1193+
'Ship review context.',
1194+
);
1195+
});
1196+
} finally {
1197+
await app.cleanup();
1198+
}
1199+
});
1200+
1201+
test('missing pull request descriptions do not render placeholder source context', async () => {
1202+
window.codiff = createCodiffMock({
1203+
getRepositoryState: vi.fn(async () => ({
1204+
...repositoryState,
1205+
files: [createChangedFile('src/app.ts')],
1206+
source: {
1207+
description: ' ',
1208+
number: 12,
1209+
provider: 'github',
1210+
type: 'pull-request',
1211+
url: 'https://github.com/nkzw-tech/codiff/pull/12',
1212+
} satisfies ReviewSource,
1213+
})),
1214+
});
1215+
1216+
const app = await renderReact(<App />);
1217+
1218+
try {
1219+
await waitFor(() => {
1220+
expect(app.container.querySelector('.codiff-file-header')).not.toBeNull();
1221+
});
1222+
expect(app.container.querySelector('.codiff-source-description-header')).toBeNull();
1223+
expect(app.container.textContent).not.toContain('PR description');
1224+
} finally {
1225+
await app.cleanup();
1226+
}
1227+
});
1228+
1229+
test('title-only pull request source context renders as a collapsed static header', async () => {
1230+
const cases: ReadonlyArray<Extract<ReviewSource, { type: 'pull-request' }>> = [
1231+
{
1232+
description: ' ',
1233+
number: 12,
1234+
provider: 'github',
1235+
title: 'Title without a body',
1236+
type: 'pull-request',
1237+
url: 'https://github.com/nkzw-tech/codiff/pull/12',
1238+
},
1239+
{
1240+
number: 13,
1241+
provider: 'gitlab',
1242+
title: 'Merge request title only',
1243+
type: 'pull-request',
1244+
url: 'https://gitlab.example.com/group/project/-/merge_requests/13',
1245+
},
1246+
];
1247+
1248+
for (const source of cases) {
1249+
window.codiff = createCodiffMock({
1250+
getRepositoryState: vi.fn(async () => ({
1251+
...repositoryState,
1252+
files: [createChangedFile('src/app.ts')],
1253+
source,
1254+
})),
1255+
});
1256+
1257+
const app = await renderReact(<App />);
1258+
1259+
try {
1260+
await waitFor(() => {
1261+
expect(app.container.querySelector('.codiff-source-description-header')).not.toBeNull();
1262+
});
1263+
const header = app.container.querySelector<HTMLElement>('.codiff-source-description-header');
1264+
expect(header?.classList.contains('collapsed')).toBe(true);
1265+
expect(header?.querySelector('.source-description-title')?.textContent).toBe(source.title);
1266+
expect(header?.querySelector('.codiff-header-toggle-static')).not.toBeNull();
1267+
expect(header?.querySelector('button.codiff-header-toggle')).toBeNull();
1268+
expect(header?.querySelector('.codiff-chevron-box')).toBeNull();
1269+
expect(app.container.querySelector('.source-description-markdown')).toBeNull();
1270+
} finally {
1271+
await app.cleanup();
1272+
}
1273+
}
1274+
});
1275+
1276+
test('pull request descriptions stay visible in walkthrough mode', async () => {
1277+
const changedFile = createChangedFile('src/app.ts');
1278+
const source = {
1279+
description: '## Summary\n\nKeep the PR context visible.',
1280+
number: 12,
1281+
provider: 'github',
1282+
title: 'Keep context visible in walkthrough',
1283+
type: 'pull-request',
1284+
url: 'https://github.com/nkzw-tech/codiff/pull/12',
1285+
} satisfies ReviewSource;
1286+
const narrativeWalkthrough = {
1287+
agent: 'codex',
1288+
chapters: [
1289+
{
1290+
blurb: 'Review the implementation.',
1291+
icon: 'gear',
1292+
id: 'impl',
1293+
stops: [
1294+
{
1295+
added: 1,
1296+
deleted: 1,
1297+
hunkIds: ['src/app.ts:unstaged:h1'],
1298+
hunks: [
1299+
{
1300+
added: 1,
1301+
anchor: { display: 'src/app.ts', sectionId: 'src/app.ts:unstaged', side: 'both' },
1302+
deleted: 1,
1303+
id: 'src/app.ts:unstaged:h1',
1304+
path: 'src/app.ts',
1305+
status: 'modified',
1306+
},
1307+
],
1308+
id: 's1',
1309+
importance: 'critical',
1310+
prose: 'Review this file.',
1311+
title: 'Implementation path',
1312+
},
1313+
],
1314+
title: 'Implementation',
1315+
},
1316+
],
1317+
focus: 'Focus.',
1318+
generatedAt: '2026-06-07T00:00:00.000Z',
1319+
kind: 'narrative',
1320+
repo: { branch: 'main', root: '/repo' },
1321+
source,
1322+
support: [],
1323+
title: 'Narrative',
1324+
version: 4,
1325+
} satisfies NarrativeWalkthrough;
1326+
1327+
window.codiff = createCodiffMock({
1328+
getLaunchOptions: vi.fn(async () => ({
1329+
repositoryPathProvided: true,
1330+
source,
1331+
walkthrough: true,
1332+
walkthroughFile: '/tmp/walkthrough.json',
1333+
})),
1334+
getNarrativeWalkthrough: vi.fn(async () => ({
1335+
status: 'ready' as const,
1336+
walkthrough: narrativeWalkthrough,
1337+
})),
1338+
getRepositoryState: vi.fn(async () => ({
1339+
...repositoryState,
1340+
files: [changedFile],
1341+
source,
1342+
})),
1343+
});
1344+
1345+
const app = await renderReact(<App />);
1346+
1347+
try {
1348+
await waitFor(() => {
1349+
expect(app.container.querySelector('.codiff-source-description-header')).not.toBeNull();
1350+
expect(app.container.querySelector('.wt-stop-block')).not.toBeNull();
1351+
});
1352+
1353+
const header = app.container.querySelector<HTMLElement>('.codiff-source-description-header');
1354+
expect(header?.textContent).toContain('Keep context visible in walkthrough');
1355+
expect(app.container.querySelector('.source-description-markdown')?.textContent).toContain(
1356+
'Keep the PR context visible.',
1357+
);
1358+
} finally {
1359+
await app.cleanup();
1360+
}
1361+
});
1362+
10621363
test('narrative walkthrough stops do not repeat commit details', async () => {
10631364
const changedFile = createChangedFile('src/app.ts');
10641365
const source = { ref: 'abc1234', type: 'commit' } satisfies ReviewSource;

0 commit comments

Comments
 (0)