fix(tui): include surrounding context in edit approval preview (#1410)

This commit is contained in:
liruifengv 2026-07-06 13:50:23 +08:00 committed by GitHub
parent b9258ee07d
commit 1c817df1e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 61 additions and 10 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix the edit approval preview shown by ctrl+e to include surrounding context lines, matching the summary panel.

View file

@ -27,7 +27,7 @@ import {
} from '@moonshot-ai/pi-tui';
import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight';
import { renderDiffLines } from '#/tui/components/media/diff-preview';
import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview';
import type { DiffDisplayBlock, FileContentDisplayBlock } from '#/tui/reverse-rpc/types';
import { currentTheme } from '#/tui/theme';
import { printableChar } from '#/tui/utils/printable-key';
@ -218,17 +218,19 @@ function buildBody(block: ApprovalPreviewBlock): BuiltBody {
}
function buildDiffBody(block: DiffDisplayBlock): BuiltBody {
// renderDiffLines emits a `+N -M path` header on its first line followed
// by every changed line. We pull the header out into the viewer chrome so
// the body is purely scrollable diff content; this also means we don't
// double-render the path.
const rendered = renderDiffLines(
// renderDiffLinesClustered emits a `+N -M path` header on its first line
// followed by every changed line plus surrounding context. We pull the
// header out into the viewer chrome so the body is purely scrollable diff
// content; this also means we don't double-render the path.
const rendered = renderDiffLinesClustered(
block.old_text,
block.new_text,
block.path,
false,
block.old_start ?? 1,
block.new_start ?? 1,
{
contextLines: 3,
oldStart: block.old_start ?? 1,
newStart: block.new_start ?? 1,
},
);
const [header = '', ...rest] = rendered;
return { lines: rest, title: stripLeadingSpace(header) };

View file

@ -156,6 +156,8 @@ export interface ClusteredDiffOptions {
readonly maxLines?: number;
readonly isIncomplete?: boolean;
readonly expandKeyHint?: string;
readonly oldStart?: number;
readonly newStart?: number;
}
interface Cluster {
@ -239,7 +241,13 @@ export function renderDiffLinesClustered(
const s = makeDiffStyles();
const contextLines = opts.contextLines ?? 3;
const maxLines = opts.maxLines;
const diffLines = computeDiffLines(oldText, newText, 1, 1, opts.isIncomplete ?? false);
const diffLines = computeDiffLines(
oldText,
newText,
opts.oldStart ?? 1,
opts.newStart ?? 1,
opts.isIncomplete ?? false,
);
const { clusters, changedCount, addedCount, removedCount } = buildClusters(
diffLines,
contextLines,

View file

@ -138,6 +138,26 @@ describe('ApprovalPreviewViewer', () => {
expect(text).toContain('BETA');
});
it('shows surrounding context lines for a diff block', () => {
const viewer = makeViewer({
block: {
type: 'diff',
path: 'src/foo.ts',
old_text: ['before1', 'before2', 'old', 'after1', 'after2'].join('\n'),
new_text: ['before1', 'before2', 'new', 'after1', 'after2'].join('\n'),
},
rows: 24,
});
const text = strip(viewer.render(100).join('\n'));
expect(text).toContain('before1');
expect(text).toContain('before2');
expect(text).toContain('old');
expect(text).toContain('new');
expect(text).toContain('after1');
expect(text).toContain('after2');
});
// Sanity: rendering is a pure slice — repeated render() calls without
// input changes produce the same output, no incremental state drift.
it('renders deterministically across repeated calls', () => {

View file

@ -155,6 +155,22 @@ describe('renderDiffLinesClustered', () => {
expect(text).toContain('ctrl+o to expand');
});
it('respects oldStart and newStart for line numbers', () => {
const text = stripAnsi(
renderDiffLinesClustered('A\nB\nC', 'A\nX\nC', 'f.ts', {
contextLines: 1,
oldStart: 10,
newStart: 20,
}).join('\n'),
);
// Context lines keep the new (post-edit) line numbers from newStart;
// deleted lines use oldStart; added lines use newStart.
expect(text).toContain(' 20 A');
expect(text).toContain(' 11 - B');
expect(text).toContain(' 21 + X');
expect(text).toContain(' 22 C');
});
it('truncates at cluster boundary and appends the ctrl+o footer when maxLines is set', () => {
const oldLines: string[] = [];
for (let i = 1; i <= 50; i++) oldLines.push(`L${String(i)}`);