F10 #31: render resolved proposals inline at their anchor block
In the F10 review preview, renderReview now emits each pending proposal whose anchor resolves as a cw-proposal block immediately after the current-side block its anchorStart falls in — answering "what is Claude proposing, and where?" by position — instead of appending all proposals as trailing blocks. Proposals in the same block are ordered by anchorStart then id (deterministic, INV-33); a proposal whose anchor doesn't resolve (or precedes all blocks) still trails as a cw-proposal-unanchored block, never dropped (INV-34). This implements the inline-at-anchor placement the graduated F10 Solution Design already specified (coauthoring-interactive-review.md §2/§6.2); the trailing-block behavior shipped in #29 was a recorded v1 deferral. Unit: mid-document placement, two proposals in distinct blocks, same-block ordering determinism, anchored-before-trailing, mixed-set determinism. E2E: the F10 propose test now asserts the proposal renders before the following block (in place); accept still lands + clears. 194 unit + 51 E2E green; typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,11 @@ suite("F10 interactive review (host E2E — preview is the single review surface
|
||||
const html = api.trackChangesPreviewController.renderHtmlFor(key);
|
||||
assert.ok(html.includes(`data-proposal-id="${id}"`), "the preview renders the proposal block by id");
|
||||
assert.match(html, /class="cw-actions"/, "the proposal block carries ✓/✗ actions");
|
||||
// #31: the proposal renders INLINE at its anchor (right after T1's block), not
|
||||
// trailing the whole document — so it appears BEFORE the following T2 block.
|
||||
const pIdx = html.indexOf(`data-proposal-id="${id}"`);
|
||||
const t2Idx = html.indexOf("second claude target");
|
||||
assert.ok(t2Idx >= 0 && pIdx < t2Idx, "the proposal renders in place, before the following block (#31)");
|
||||
// INV-10: proposing never touches the document.
|
||||
assert.ok(doc.getText().includes(T1), "document unchanged by propose");
|
||||
});
|
||||
|
||||
@@ -231,6 +231,63 @@ describe("renderReview", () => {
|
||||
const count = (html.match(/cw-by-human/g) ?? []).length;
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
// #31 — a resolved proposal renders INLINE at its anchor's block, not as a trailing block.
|
||||
test("renderReview: a resolved proposal renders in place after its anchor's block, not trailing", () => {
|
||||
// Two paragraphs; "Alpha here" at 0..10, "Beta there" at 12..22.
|
||||
const doc = "Alpha here\n\nBeta there";
|
||||
const proposals: ProposalView[] = [{ id: "p1", anchorStart: 0, anchorEnd: 5, replaced: "Alpha", replacement: "Omega" }];
|
||||
const html = renderReview(doc, doc, [], proposals);
|
||||
const pIdx = html.indexOf('data-proposal-id="p1"');
|
||||
const alphaIdx = html.indexOf("Alpha here");
|
||||
const betaIdx = html.indexOf("Beta there");
|
||||
expect(pIdx).toBeGreaterThan(alphaIdx); // after its own block
|
||||
expect(pIdx).toBeLessThan(betaIdx); // and BEFORE the next block (in place, not trailing)
|
||||
});
|
||||
test("renderReview: two proposals in distinct blocks each render after their own block", () => {
|
||||
const doc = "Alpha here\n\nBeta there";
|
||||
const proposals: ProposalView[] = [
|
||||
{ id: "pA", anchorStart: 0, anchorEnd: 5, replaced: "Alpha", replacement: "Omega" },
|
||||
{ id: "pB", anchorStart: 12, anchorEnd: 16, replaced: "Beta", replacement: "Gamma" },
|
||||
];
|
||||
const html = renderReview(doc, doc, [], proposals);
|
||||
const betaIdx = html.indexOf("Beta there");
|
||||
expect(html.indexOf('data-proposal-id="pA"')).toBeLessThan(betaIdx); // before second block
|
||||
expect(html.indexOf('data-proposal-id="pB"')).toBeGreaterThan(betaIdx); // after second block
|
||||
});
|
||||
test("renderReview: two proposals in the same block are ordered by anchorStart (deterministic)", () => {
|
||||
// single block "one two three": one@0, two@4, three@8
|
||||
const doc = "one two three";
|
||||
const proposals: ProposalView[] = [
|
||||
{ id: "late", anchorStart: 8, anchorEnd: 13, replaced: "three", replacement: "trois" },
|
||||
{ id: "early", anchorStart: 0, anchorEnd: 3, replaced: "one", replacement: "uno" },
|
||||
];
|
||||
const html = renderReview(doc, doc, [], proposals);
|
||||
expect(html.indexOf('data-proposal-id="early"')).toBeLessThan(html.indexOf('data-proposal-id="late"'));
|
||||
});
|
||||
test("renderReview: a resolved proposal renders before a trailing unanchored one (INV-34)", () => {
|
||||
const doc = "Alpha here\n\nBeta there";
|
||||
const proposals: ProposalView[] = [
|
||||
{ id: "anc", anchorStart: 0, anchorEnd: 5, replaced: "Alpha", replacement: "Omega" },
|
||||
{ id: "unanc", anchorStart: null, anchorEnd: null, replaced: "x", replacement: "y" },
|
||||
];
|
||||
const html = renderReview(doc, doc, [], proposals);
|
||||
const uIdx = html.indexOf('data-proposal-id="unanc"');
|
||||
expect(html.indexOf('data-proposal-id="anc"')).toBeLessThan(uIdx); // resolved in-place first
|
||||
expect(uIdx).toBeGreaterThan(html.indexOf("Beta there")); // unanchored still trails the body
|
||||
expect(html).toContain("cw-proposal-unanchored");
|
||||
});
|
||||
test("renderReview is deterministic with mixed anchored/unanchored proposals", () => {
|
||||
const doc = "one two three";
|
||||
const proposals: ProposalView[] = [
|
||||
{ id: "late", anchorStart: 8, anchorEnd: 13, replaced: "three", replacement: "trois" },
|
||||
{ id: "early", anchorStart: 0, anchorEnd: 3, replaced: "one", replacement: "uno" },
|
||||
{ id: "floating", anchorStart: null, anchorEnd: null, replaced: "z", replacement: "w" },
|
||||
];
|
||||
const a = renderReview(doc, doc, [], proposals);
|
||||
const b = renderReview(doc, doc, [], proposals);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
import { renderTrackChanges as rtc2 } from "../src/trackChangesModel";
|
||||
|
||||
Reference in New Issue
Block a user