#40 (WIP, UNVERIFIED): restore exact author attribution on undo/redo
Follow-up to #38. #38 made undo/redo re-inserted text neutral (no false human coloring); #40 restores its EXACT prior attribution so Claude's restored text is blue again, the human's green. Mechanism — text-keyed attribution snapshots: per-doc Map<documentText, spans>, snapshotted after every forward edit + at load; on undo/redo, after the #38 geometry reconcile, if a snapshot's text equals the current buffer the spans are restored exactly (offsets valid — identical text). Robust to VS Code undo coalescing (only the resulting-text-matching event restores); far-back/evicted states fall back to #38 neutral. History bounded (ATTR_HISTORY_MAX, oldest evicted). NOT MERGED / NOT VERIFIED END-TO-END: 222 unit + typecheck green, but the #40 host E2E (and the pre-existing #38 undoMarks E2E) depend on executeCommand("undo"), which is broken in this test environment — the untouched #38 test fails identically on clean main (the known undoMarks flake, now deterministic). Clearing .vscode-test/user-data (the usual remedy) is permission-blocked here. The operator should verify in a working E2E environment before merging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,8 +44,19 @@ interface DocAttribution {
|
||||
* orphans on reload).
|
||||
*/
|
||||
hadAttributions: boolean;
|
||||
/**
|
||||
* #40: text-keyed attribution snapshots for exact provenance restoration on
|
||||
* undo/redo. Maps a document-text state → the live spans at that state (offsets
|
||||
* valid for that exact text). Captured after every forward edit + at load;
|
||||
* consulted on undo/redo to restore the matching state's spans. Bounded (oldest
|
||||
* evicted) — a far-back/evicted state falls back to the #38 neutral reconcile.
|
||||
*/
|
||||
attrHistory: Map<string, LiveSpan[]>;
|
||||
}
|
||||
|
||||
/** #40: cap on the per-doc attribution-snapshot history (oldest evicted). */
|
||||
const ATTR_HISTORY_MAX = 200;
|
||||
|
||||
export class AttributionController implements vscode.Disposable {
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
private readonly docs = new Map<string, DocAttribution>();
|
||||
@@ -90,7 +101,7 @@ export class AttributionController implements vscode.Disposable {
|
||||
private state(docPath: string): DocAttribution {
|
||||
let s = this.docs.get(docPath);
|
||||
if (!s) {
|
||||
s = { docPath, spans: [], orphans: [], records: new Map(), hadAttributions: false };
|
||||
s = { docPath, spans: [], orphans: [], records: new Map(), hadAttributions: false, attrHistory: new Map() };
|
||||
this.docs.set(docPath, s);
|
||||
}
|
||||
return s;
|
||||
@@ -125,6 +136,9 @@ export class AttributionController implements vscode.Disposable {
|
||||
s.spans = coalesce(s.spans);
|
||||
if (artifact.attributions.length > 0) s.hadAttributions = true;
|
||||
}
|
||||
// #40: seed the snapshot history with the loaded state so undoing back to it
|
||||
// restores its exact attribution.
|
||||
this.snapshotAttribution(s, document.getText());
|
||||
this.render(document);
|
||||
}
|
||||
|
||||
@@ -140,6 +154,17 @@ export class AttributionController implements vscode.Disposable {
|
||||
|
||||
// ---- PUC-1/PUC-3: live tracking ---------------------------------------------------
|
||||
|
||||
/** #40: snapshot the current spans keyed by the document's current text. */
|
||||
private snapshotAttribution(s: DocAttribution, text: string): void {
|
||||
s.attrHistory.delete(text); // re-insert at the end (recency order)
|
||||
s.attrHistory.set(text, s.spans.map((sp) => ({ ...sp })));
|
||||
while (s.attrHistory.size > ATTR_HISTORY_MAX) {
|
||||
const oldest = s.attrHistory.keys().next().value as string | undefined;
|
||||
if (oldest === undefined) break;
|
||||
s.attrHistory.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidChange(e: vscode.TextDocumentChangeEvent): void {
|
||||
if (!this.isTracked(e.document) || e.contentChanges.length === 0) return;
|
||||
const docPath = this.keyOf(e.document);
|
||||
@@ -202,6 +227,18 @@ export class AttributionController implements vscode.Disposable {
|
||||
);
|
||||
}
|
||||
}
|
||||
// #40: exact provenance across history navigation. After a FORWARD edit,
|
||||
// snapshot the new state's spans. On UNDO/REDO, the geometry reconcile above
|
||||
// left re-inserted text neutral (#38); if a snapshot's text equals the current
|
||||
// buffer, restore that state's spans exactly (offsets are valid — identical
|
||||
// text) so Claude's restored text is blue again, the human's green. No match
|
||||
// (far-back/evicted state) keeps the #38 neutral fallback.
|
||||
if (isUndoRedo) {
|
||||
const restored = s.attrHistory.get(e.document.getText());
|
||||
if (restored) s.spans = restored.map((sp) => ({ ...sp }));
|
||||
} else {
|
||||
this.snapshotAttribution(s, e.document.getText());
|
||||
}
|
||||
if (s.spans.length > 0) s.hadAttributions = true;
|
||||
this.render(e.document);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user