From bea9fd5148df148b45da034c96818348888a5336 Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Fri, 26 Jun 2026 06:15:36 -0700 Subject: [PATCH] feat(model): add Proposal.original for F12 optimistic-apply revert (#64) --- src/model.ts | 10 ++++++++++ test/model.test.ts | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/model.ts b/src/model.ts index c24f133..c865d83 100644 --- a/src/model.ts +++ b/src/model.ts @@ -97,6 +97,14 @@ export interface Proposal { * back-compat with older sidecars) ⇒ a single-range proposal accepted whole. */ granularity?: "block" | "single"; + /** + * F12/#64 (INV-48): the pre-apply text, captured when a proposal is + * optimistically applied to the buffer. `replacement` is now in the buffer and + * `fp.text` re-anchors to it, so `original` is the only record of what to revert + * to (revert-in-place) and what to show struck in the `` half. Absent on a + * proposal created but not yet optimistically applied (or older sidecars). + */ + original?: string; } export interface Artifact { @@ -252,6 +260,8 @@ export function serializeArtifact(a: Artifact): string { createdAt: p.createdAt, ...(p.turnId !== undefined ? { turnId: p.turnId } : {}), ...(p.instruction !== undefined ? { instruction: p.instruction } : {}), + ...(p.original !== undefined ? { original: p.original } : {}), + ...(p.granularity !== undefined ? { granularity: p.granularity } : {}), }, p, ), diff --git a/test/model.test.ts b/test/model.test.ts index 7baf525..303c6be 100644 --- a/test/model.test.ts +++ b/test/model.test.ts @@ -1,4 +1,5 @@ -import { describe, it, expect } from "vitest"; +import assert from "node:assert"; +import { describe, it, test, expect } from "vitest"; import { SCHEMA_VERSION, emptyArtifact, @@ -179,6 +180,19 @@ describe("unknown-field preservation (F5 SLICE-2, INV-15)", () => { }); }); +test("serializeArtifact round-trips Proposal.original", () => { + const a = emptyArtifact("doc.md"); + a.anchors["a_1"] = { fingerprint: { text: "new", before: "", after: "", lineHint: 0 } }; + a.proposals.push({ + id: "pr_1", anchorId: "a_1", replacement: "new", + author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "s" } }, + createdAt: "2026-06-26T00:00:00.000Z", original: "old", granularity: "block", + }); + const json = serializeArtifact(a); + assert.match(json, /"original": "old"/); + assert.match(json, /"granularity": "block"/); +}); + describe("attributions[] (F3 SLICE-1)", () => { it("round-trips an attribution record through serialize → parse", () => { const a = emptyArtifact("docs/x.md");