fix #38: don't mis-attribute undo/redo as fresh human authorship in the preview
Undo in the editor rendered wrong marks in the F10 review preview: restored baseline text (and reverted Claude text) was colored as human-authored. Root cause — attributionController.onDidChange attributed every non-seam document change to currentAuthor() (always human) and ignored e.reason, so an undo/redo that re-inserts text created a fresh human span over it. renderReview is pure; the wrong marks came from these false author spans. Fix: on e.reason === Undo|Redo, reconcile span geometry but do NOT attribute the re-inserted chars — an undo is history navigation, not authorship, so restored text stays neutral (unattributed) rather than falsely claimed by the human. applyChange gains an `attributeInserted` flag (default true; false on undo/redo); seam matching is also skipped on undo/redo (the seam only applies forward edits). Restored text becomes neutral rather than recovering its exact prior provenance (e.g. undoing a deletion of Claude's text shows it unattributed, not blue) — perfect restoration would need an attribution history stack synced to the editor undo stack (fragile due to edit coalescing); filed mentally as a follow-up. The neutral behavior removes the misleading marks, which is the reported defect. Tests: E2E reproduces the mid-edit-undo case (buffer stays dirty so the disk-sync guard doesn't mask it) and asserts restored text is unattributed; +3 unit tests for the geometric-only applyChange path. 197 unit + 50 E2E green; typecheck clean. Closes #38 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,25 @@ describe("multi-edit sequences", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyChange — geometric-only (undo/redo, #38)", () => {
|
||||
it("attributeInserted=false adds NO span for re-inserted text (restored text stays neutral)", () => {
|
||||
// Undo re-inserts 'bravo ' at offset 6 into an empty span list → no span.
|
||||
const r = applyChange([], { start: 6, end: 6, newLength: 6 }, HUMAN, ctx(), false);
|
||||
expect(r).toEqual([]);
|
||||
});
|
||||
it("attributeInserted=false still SHIFTS existing spans by the edit delta", () => {
|
||||
// A real human span sits after the re-insert point; it must shift right by 6,
|
||||
// but the re-inserted chars themselves get no new span.
|
||||
const r = applyChange([span("tail", 20, 30, HUMAN)], { start: 6, end: 6, newLength: 6 }, HUMAN, ctx(), false);
|
||||
expect(ranges(r)).toEqual([[26, 36]]);
|
||||
expect(authors(r)).toEqual(["human"]);
|
||||
});
|
||||
it("attributeInserted=false still reconciles geometry of a deletion (removes a covered span)", () => {
|
||||
const r = applyChange([span("a", 3, 6, AGENT)], { start: 0, end: 10, newLength: 0 }, HUMAN, ctx(), false);
|
||||
expect(r).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("coalesce", () => {
|
||||
it("merges adjacent same-author same-turn spans, keeps earliest createdAt", () => {
|
||||
const a = { ...span("a", 0, 3, HUMAN), createdAt: "2026-06-09T00:00:00.000Z" };
|
||||
|
||||
Reference in New Issue
Block a user