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:
@@ -422,8 +422,13 @@ function renderReviewOp(
|
||||
* On-state body (INV-33): the F7 baseline diff — added/changed PROSE author-colored
|
||||
* via colorByAuthor (F9 sentinels), deletions struck — overlaid with F4 pending
|
||||
* proposals as blue cw-proposal blocks (✓/✗). One pass, pure, vscode-free.
|
||||
* Resolved proposals append after the diff body; unresolved ones append as trailing
|
||||
* cw-proposal-unanchored blocks (never dropped — INV-34).
|
||||
*
|
||||
* A resolved proposal renders INLINE, right after the current-side block its anchor
|
||||
* falls in (#31) — so "what is Claude proposing, and where?" is answered by
|
||||
* position. A proposal whose anchor does not resolve (or falls before the first
|
||||
* block) renders as a trailing cw-proposal-unanchored block (never dropped —
|
||||
* INV-34). Deterministic: proposals in the same block are ordered by anchorStart
|
||||
* then id; trailing proposals keep input order.
|
||||
*/
|
||||
export function renderReview(
|
||||
baselineText: string,
|
||||
@@ -435,17 +440,43 @@ export function renderReview(
|
||||
const render = opts.render ?? defaultRender;
|
||||
const ranges = splitBlocksWithRanges(currentText);
|
||||
const ops = diffBlocks(baselineText, currentText);
|
||||
|
||||
// Associate each resolved proposal with the current-side block index whose range
|
||||
// it anchors into: the largest block with start <= anchorStart (the containing
|
||||
// block, or the nearest preceding block when the anchor sits in a gap). A
|
||||
// resolved anchor before all blocks, and every unresolved proposal, trails.
|
||||
const blockOf = (a: number): number => {
|
||||
let j = -1;
|
||||
for (let k = 0; k < ranges.length && ranges[k].start <= a; k++) j = k;
|
||||
return j;
|
||||
};
|
||||
const byBlock = new Map<number, ProposalView[]>();
|
||||
const trailing: ProposalView[] = [];
|
||||
for (const p of proposals) {
|
||||
const j = p.anchorStart === null ? -1 : blockOf(p.anchorStart);
|
||||
if (j < 0) {
|
||||
trailing.push(p);
|
||||
continue;
|
||||
}
|
||||
(byBlock.get(j) ?? byBlock.set(j, []).get(j)!).push(p);
|
||||
}
|
||||
for (const arr of byBlock.values()) {
|
||||
arr.sort((a, b) => a.anchorStart! - b.anchorStart! || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
||||
}
|
||||
|
||||
let ci = 0; // pointer into ranges; advances for every op with a current-side block
|
||||
const bodyParts = ops.map((op) => {
|
||||
const bodyParts: string[] = [];
|
||||
for (const op of ops) {
|
||||
const blockIndex = op.kind === "removed" ? -1 : ci;
|
||||
const blk = op.kind === "removed" ? undefined : ranges[ci++];
|
||||
const colored = (raw: string): string =>
|
||||
blk ? colorByAuthor(raw, blk.start, authorSpans, render) : render(raw);
|
||||
return renderReviewOp(op, render, colored);
|
||||
});
|
||||
const anchored = proposals.filter((p) => p.anchorStart !== null);
|
||||
const unanchored = proposals.filter((p) => p.anchorStart === null);
|
||||
const proposalParts = [...anchored, ...unanchored].map((p) => proposalBlockHtml(p, render));
|
||||
return [...bodyParts, ...proposalParts].join("\n");
|
||||
bodyParts.push(renderReviewOp(op, render, colored));
|
||||
const here = blockIndex >= 0 ? byBlock.get(blockIndex) : undefined;
|
||||
if (here) for (const p of here) bodyParts.push(proposalBlockHtml(p, render));
|
||||
}
|
||||
for (const p of trailing) bodyParts.push(proposalBlockHtml(p, render));
|
||||
return bodyParts.join("\n");
|
||||
}
|
||||
|
||||
/** Pure entry point: annotated HTML body for the preview (INV-22). */
|
||||
|
||||
Reference in New Issue
Block a user