feat: baseline router — git HEAD + snapshot per INV-7/D13/D14; retire machine-landing advance (spec §6.4)

Task 2 of the native-surfaces migration plan. GitBaselineAdapter resolves a
document's HEAD blob via the built-in vscode.git extension (hardened with a
.git/logs/HEAD watch that nudges repo.status(), since the git extension's own
watcher doesn't reliably notice out-of-band commits on non-workspace-folder
repos in the E2E host). DiffViewController is reworked into a baseline router:
head mode (git-tracked, never persisted, re-read on commit) vs snapshot mode
(captured on CoeditingRegistry entry, re-pinned by "Mark Changes as Reviewed"
— renamed from cowriting.pinDiffBaseline, D14). The shipped machine-landing
baseline advance (#48/INV-18) is retired: a landed Claude edit now stays a
visible change-since-baseline until commit or review (INV-7/D21). Legacy
on-disk reasons ("opened"/"machine-landing") migrate to "entered" via
BaselineStore.normalizeReason on load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-07-02 07:09:55 -07:00
parent de83757754
commit 447a1170ce
23 changed files with 558 additions and 140 deletions
+13 -7
View File
@@ -56,8 +56,11 @@ export class AttributionController implements vscode.Disposable {
/**
* F6 (§6.2/§6.4): the single machine-landing signal. Fired after a real
* (non-no-op) seam apply succeeds. INV-9 makes the seam the sole machine-edit
* ingress, so this is the sole signal — DiffViewController subscribes to
* advance the baseline; no call-site wiring, no future driver can forget it.
* ingress, so this is the sole signal. Native-surfaces migration (INV-7/
* INV-18 retirement, spec §6.4): DiffViewController no longer subscribes
* here to advance the baseline — a landed edit stays a visible
* change-since-baseline until commit (head mode) or "Mark Changes as
* Reviewed" (snapshot mode). Other consumers may still subscribe.
*/
private readonly applyEmitter = new vscode.EventEmitter<{ document: vscode.TextDocument }>();
readonly onDidApplyAgentEdit: vscode.Event<{ document: vscode.TextDocument }> = this.applyEmitter.event;
@@ -291,9 +294,10 @@ export class AttributionController implements vscode.Disposable {
);
}
if (ok && opts?.landBaseline !== false) {
// F6 (INV-18): a real machine landing — advance the baseline. F12 (INV-48)
// suppresses this for optimistic apply: the proposed text is in the buffer
// but the change stays PENDING (baseline at pre-proposal) until accept.
// F6 (INV-18, retired as a baseline advance — spec §6.4/INV-7): fire the
// machine-landing signal. F12 (INV-48) suppresses this for optimistic
// apply: the proposed text is in the buffer but the change stays PENDING
// until accept.
this.applyEmitter.fire({ document });
}
return ok;
@@ -302,8 +306,10 @@ export class AttributionController implements vscode.Disposable {
/**
* F12/#64 (INV-51): fire the machine-landing signal WITHOUT applying text — used
* by finalize-in-place, where the proposed text already landed in the buffer via
* optimistic apply (`landBaseline:false`) and accept only needs to advance the
* F6 baseline so the now-accepted change stops reading as pending.
* optimistic apply (`landBaseline:false`) and accept marks the landing so the
* now-accepted change is recorded (INV-7: the baseline itself no longer
* advances on landing — it advances only on commit / "Mark Changes as
* Reviewed").
*/
signalLanded(document: vscode.TextDocument): void {
if (this.isTracked(document)) this.applyEmitter.fire({ document });
+17 -2
View File
@@ -7,11 +7,19 @@
* key is a hash of the document URI (the controller derives it), not a
* workspace-relative path; an untitled buffer has no durable identity and is
* never persisted here (in-memory only — see DiffViewController).
*
* INV-7 (native-surfaces migration, spec §6.4): the baseline is now either the
* git-HEAD blob (never persisted — see GitBaselineAdapter/DiffViewController)
* or a snapshot captured on coediting-entry / re-pinned by "Mark Changes as
* Reviewed". `"opened"`/`"machine-landing"` are legacy reasons from the
* shipped #17/#48 baseline; `normalizeReason` migrates them on load so old
* on-disk baselines keep round-tripping under the new vocabulary.
*/
import * as fs from "node:fs";
import * as path from "node:path";
export type BaselineReason = "opened" | "machine-landing" | "pinned";
export type BaselineReason = "entered" | "pinned" | "head";
type LegacyBaselineReason = BaselineReason | "opened" | "machine-landing";
export interface Baseline {
/** the document URI (the identity the key is derived from) — for round-trip/debug. */
@@ -22,6 +30,12 @@ export interface Baseline {
reason: BaselineReason;
}
/** Migrate legacy on-disk reasons to the current vocabulary (§6.4 INV-7). */
export function normalizeReason(reason: LegacyBaselineReason): BaselineReason {
if (reason === "opened" || reason === "machine-landing") return "entered";
return reason;
}
export class BaselineStore {
/** @param storageDir absolute VS Code GLOBAL-storage dir (context.globalStorageUri.fsPath). */
constructor(private readonly storageDir: string) {}
@@ -34,7 +48,8 @@ export class BaselineStore {
load(key: string): Baseline | null {
const p = this.baselinePath(key);
if (!fs.existsSync(p)) return null;
return JSON.parse(fs.readFileSync(p, "utf8")) as Baseline;
const raw = JSON.parse(fs.readFileSync(p, "utf8")) as Baseline;
return { ...raw, reason: normalizeReason(raw.reason) };
}
/** Newest epoch wins — overwrite in place, no history (state-not-history, the F3/F4 precedent). */
+126 -43
View File
@@ -1,9 +1,16 @@
/**
* DiffViewController F6 baseline data layer (spec §6.4/§6.7). Owns the baseline
* lifecycle (initialize at first sight / advance at every machine landing / pin
* on demand) and serves it to F7/F10 via `getBaseline` + the additive
* `onDidChangeBaseline` event. A pure data layer: never mutates the document,
* sidecar, or attribution state (INV-19).
* DiffViewController the baseline router (native-surfaces migration, spec
* §6.4, INV-7). Resolves each coedited document's review baseline to one of two
* modes: **head** (git-tracked the baseline is the `HEAD` blob, re-read on
* every commit, never persisted) or **snapshot** (untracked/no-repo captured
* on coediting-entry, re-pinned on demand by "Mark Changes as Reviewed").
* Baselines are established ONLY for documents the CoeditingRegistry has
* entered (INV-10) opening a file no longer captures one (the retired
* `ensureBaseline`/#17 behavior). The shipped machine-landing advance (#48,
* INV-18) is retired too: a landed Claude edit stays a visible change-since-
* baseline until the file is committed (head mode) or reviewed (snapshot
* mode) spec INV-7's note, D21. A pure data layer: never mutates the
* document, sidecar, or attribution state (INV-19).
*
* The F6 two-pane `vscode.diff` *view* (toggle UI + the `cowriting-baseline:`
* virtual document) was removed in #34 once F10 made the rendered preview the
@@ -12,37 +19,57 @@
* The baseline works on ANY text document, not just workspace files it needs no
* `.threads/` sidecar, only a stable doc identity + a storage home. So it has
* its OWN diffable predicate (any `file:` or `untitled:` doc), decoupled from
* F2's workspace `isTracked`. Persistable docs (`file:`) keep their baseline in
* VS Code's per-extension GLOBAL storage keyed by a hash of the document URI;
* untitled buffers have no durable identity, so their baseline is in-memory
* only (lost on reload) the same degrade the storage-unavailable path uses.
* F2's workspace `isTracked`. Persistable docs (`file:`) keep their SNAPSHOT
* baseline in VS Code's per-extension GLOBAL storage keyed by a hash of the
* document URI (INV-7: head-mode baselines are storage-free never
* persisted); untitled buffers have no durable identity, so their baseline is
* in-memory only (lost on reload) the same degrade the storage-unavailable
* path uses.
*/
import { createHash } from "node:crypto";
import * as vscode from "vscode";
import { BaselineStore, type Baseline, type BaselineReason } from "./baselineStore";
import type { GitBaselineAdapter } from "./gitBaseline";
import type { CoeditingRegistry } from "./coeditingRegistry";
export class DiffViewController implements vscode.Disposable {
private readonly disposables: vscode.Disposable[] = [];
/** Source of truth for the baseline, keyed by `document.uri.toString()`. */
private readonly baselines = new Map<string, Baseline>();
/** F7 (additive): fires on every baseline capture (open / advance / pin) so
* the track-changes preview refreshes without polling. Carries the real
* document URI. */
/** Which mode each established document resolved to (INV-7). */
private readonly modes = new Map<string, "head" | "snapshot">();
/** F7 (additive): fires on every baseline capture (establish / head-refresh /
* pin) so the track-changes preview refreshes without polling. Carries the
* real document URI. */
private readonly onDidChangeBaselineEmitter = new vscode.EventEmitter<{ uri: string }>();
readonly onDidChangeBaseline = this.onDidChangeBaselineEmitter.event;
private storageWarned = false;
constructor(private readonly store: BaselineStore | null) {
constructor(
private readonly store: BaselineStore | null,
private readonly git: GitBaselineAdapter,
registry: CoeditingRegistry,
) {
this.disposables.push(
this.onDidChangeBaselineEmitter,
vscode.commands.registerCommand("cowriting.pinDiffBaseline", () =>
vscode.commands.registerCommand("cowriting.markReviewed", () =>
this.pinCommand(vscode.window.activeTextEditor),
),
// F6 captures a baseline for any diffable doc the moment it is first seen,
// independent of the workspace gate (so "opened" is the open-time text).
vscode.workspace.onDidOpenTextDocument((d) => this.ensureBaseline(d)),
// INV-10: a baseline is established only once its document ENTERS
// coediting; exiting keeps the stored baseline as-is (PUC-7).
registry.onDidChange(({ uri, coediting }) => {
if (!coediting) return;
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri);
if (doc) void this.establish(doc);
}),
// A commit on a watched repo may advance a head-mode baseline (D13).
git.onDidChangeHead(() => void this.refreshHeadBaselines()),
);
for (const d of vscode.workspace.textDocuments) this.ensureBaseline(d);
// Restore already-coediting documents (e.g. after an extension host reload).
for (const key of registry.list()) {
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === key);
if (doc) void this.establish(doc);
}
}
// ---- diffability / identity --------------------------------------------------------
@@ -51,7 +78,7 @@ export class DiffViewController implements vscode.Disposable {
private isDiffable(document: vscode.TextDocument): boolean {
return document.uri.scheme === "file" || document.uri.scheme === "untitled";
}
/** Only `file:` docs have a durable identity to persist a baseline against. */
/** Only `file:` docs have a durable identity to persist a SNAPSHOT baseline against. */
private isPersistable(document: vscode.TextDocument): boolean {
return document.uri.scheme === "file";
}
@@ -64,36 +91,66 @@ export class DiffViewController implements vscode.Disposable {
return createHash("sha256").update(uriKey).digest("hex");
}
// ---- baseline lifecycle (§6.4) -----------------------------------------------------
// ---- baseline lifecycle (§6.4, INV-7) -----------------------------------------------
/** First sight of a diffable doc: load the stored baseline, else capture `opened`. */
ensureBaseline(document: vscode.TextDocument): void {
/**
* Resolve this document's baseline mode and (re)establish its baseline.
* git-tracked (a HEAD blob resolves) **head** mode, baseline = HEAD, never
* persisted. Otherwise **snapshot** mode: load a previously-stored
* snapshot, else capture the buffer now (`"entered"`). Called once when a
* document enters coediting (registry.enter) and at startup for documents
* already coediting.
*/
async establish(document: vscode.TextDocument): Promise<void> {
if (!this.isDiffable(document)) return;
const key = this.uriKey(document);
if (this.baselines.has(key)) return;
const head = await this.git.headFor(document.uri);
if (head) {
this.modes.set(key, "head");
this.setBaseline(key, head.text, "head");
return;
}
this.modes.set(key, "snapshot");
if (this.store && this.isPersistable(document)) {
try {
const stored = this.store.load(this.storageKey(key));
if (stored) {
this.baselines.set(key, stored);
return;
}
} catch {
this.warnStorageOnce();
const stored = this.tryLoad(key); // existing load path, legacy reasons normalized
if (stored) {
this.baselines.set(key, stored);
this.fire(key);
return;
}
}
this.capture(document, "opened");
this.capture(document, "entered");
}
/** Machine landing (INV-18): re-capture so landed text never shows as a change. */
advance(document: vscode.TextDocument): void {
if (!this.isDiffable(document)) return;
this.capture(document, "machine-landing");
/** The mode a document's baseline resolved to, or undefined before `establish`. */
modeOf(uriString: string): "head" | "snapshot" | undefined {
return this.modes.get(uriString);
}
/** Human pin: baseline := now; the preview's change-marks empty (left = right). */
/** Re-read HEAD for every head-mode document; a commit advances the baseline (D13). */
private async refreshHeadBaselines(): Promise<void> {
for (const [key, mode] of this.modes) {
if (mode !== "head") continue;
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === key);
if (!doc) continue;
const head = await this.git.headFor(doc.uri);
if (head && head.text !== this.baselines.get(key)?.text) {
this.setBaseline(key, head.text, "head"); // commit advanced the baseline (D13)
}
}
}
/** Human pin ("Mark Changes as Reviewed", D14): snapshot mode only a
* git-tracked (head-mode) baseline advances only by commit. */
pin(document: vscode.TextDocument): void {
if (!this.isDiffable(document)) return;
const key = this.uriKey(document);
if (this.modes.get(key) !== "snapshot") {
void vscode.window.showWarningMessage(
"Cowriting: this document is git-tracked — commit to advance the baseline.",
);
return;
}
this.capture(document, "pinned");
}
@@ -103,12 +160,38 @@ export class DiffViewController implements vscode.Disposable {
const baseline: Baseline = { uri: key, text: document.getText(), capturedAt: new Date().toISOString(), reason };
this.baselines.set(key, baseline);
if (this.store && this.isPersistable(document)) {
try {
this.store.save(this.storageKey(key), baseline);
} catch {
this.warnStorageOnce();
}
this.trySave(key, baseline);
}
this.fire(key);
}
/** Set a baseline from resolved text directly (head mode — no document buffer read). */
private setBaseline(key: string, text: string, reason: BaselineReason): void {
const baseline: Baseline = { uri: key, text, capturedAt: new Date().toISOString(), reason };
this.baselines.set(key, baseline);
// head-mode baselines are storage-free (INV-7); persist only snapshots.
if (reason !== "head" && this.store) this.trySave(key, baseline);
this.fire(key);
}
private tryLoad(key: string): Baseline | null {
try {
return this.store!.load(this.storageKey(key));
} catch {
this.warnStorageOnce();
return null;
}
}
private trySave(key: string, baseline: Baseline): void {
try {
this.store!.save(this.storageKey(key), baseline);
} catch {
this.warnStorageOnce();
}
}
private fire(key: string): void {
this.onDidChangeBaselineEmitter.fire({ uri: key });
}
@@ -122,7 +205,7 @@ export class DiffViewController implements vscode.Disposable {
private pinCommand(editor: vscode.TextEditor | undefined): void {
if (!editor || !this.isDiffable(editor.document)) {
void vscode.window.showWarningMessage("Cowriting: focus a text editor to pin its review baseline.");
void vscode.window.showWarningMessage("Cowriting: focus a text editor to mark its changes as reviewed.");
return;
}
this.pin(editor.document);
+1 -1
View File
@@ -57,7 +57,7 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL
vscode.languages.registerCodeLensProvider({ language: "markdown" }, this),
this.proposals.onDidChangeProposals(({ uri }) => this.scheduleApply(uri)),
vscode.window.onDidChangeActiveTextEditor((ed) => ed && this.renderEditor(ed)),
// Refresh committed decorations when the baseline advances (pin / machine-landing / open).
// Refresh committed decorations when the baseline changes (establish / pin / head-refresh).
this.diffView.onDidChangeBaseline(({ uri }) => {
const ed = vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() === uri);
if (ed) this.renderEditor(ed);
+20 -15
View File
@@ -10,6 +10,7 @@ import { BaselineStore } from "./baselineStore";
import { GlobalSidecarStore } from "./globalSidecarStore";
import { SidecarRouter } from "./sidecarRouter";
import { DiffViewController } from "./diffViewController";
import { GitBaselineAdapter } from "./gitBaseline";
import { TrackChangesPreviewController } from "./trackChangesPreview";
import { LiveProgressUi } from "./liveProgressUi";
import { EditorProposalController } from "./editorProposalController";
@@ -87,17 +88,21 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
);
coeditingRegistry.syncContext(vscode.window.activeTextEditor);
// --- F6: baseline data layer (Features #17 + #19) — workspace-INDEPENDENT ---
// The two-pane vscode.diff VIEW was removed in #34 (F10's rendered preview is
// the single review surface); only the baseline store survives, consumed by
// F7/F10. It tracks a baseline for ANY diffable doc (file or untitled), so it
// is constructed regardless of whether a folder is open. Baseline lives in VS
// Code's per-extension GLOBAL storage (always present), never the repo
// (INV-19). The machine-landing advance is wired below. The controller
// self-wires baseline capture on open.
// --- Baseline router (native-surfaces migration, spec §6.4, INV-7) — workspace-
// INDEPENDENT --- The two-pane vscode.diff VIEW was removed in #34 (F10's
// rendered preview is the single review surface); only the baseline data
// layer survives, consumed by F7/F10. It tracks a baseline for ANY diffable
// doc (file or untitled), so it is constructed regardless of whether a folder
// is open. A SNAPSHOT baseline lives in VS Code's per-extension GLOBAL
// storage (always present), never the repo (INV-19); a git-tracked doc's HEAD
// baseline is never persisted (INV-7). Baselines are established only for
// documents the CoeditingRegistry has entered — see the registry wiring
// inside DiffViewController's constructor.
const gitBaseline = new GitBaselineAdapter();
context.subscriptions.push(gitBaseline);
const baselineStorageDir = context.globalStorageUri?.fsPath;
const baselineStore = baselineStorageDir ? new BaselineStore(baselineStorageDir) : null;
const diffViewController = new DiffViewController(baselineStore);
const diffViewController = new DiffViewController(baselineStore, gitBaseline, coeditingRegistry);
context.subscriptions.push(diffViewController);
// F8: the out-of-workspace/untitled authoring sidecar — same GLOBAL storage
@@ -176,12 +181,12 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
}),
);
// --- F6 machine-landing wiring — now for ANY authorable doc ---
// The seam's single machine-landing signal advances the F6 baseline (INV-18);
// the seam can now fire on out-of-folder files too, so wire it unconditionally.
context.subscriptions.push(
attributionController.onDidApplyAgentEdit((e) => diffViewController.advance(e.document)),
);
// NOTE (native-surfaces migration, INV-7/INV-18 retirement): the shipped
// machine-landing baseline advance (#48) — wiring
// attributionController.onDidApplyAgentEdit → diffViewController.advance —
// was removed here. A landed Claude edit now stays a visible change-since-
// baseline until the file is committed (head mode) or reviewed via
// "Mark Changes as Reviewed" (snapshot mode).
// One SHARED sidecar watcher for both controllers; self-writes are suppressed
// centrally in the repo store (only repo `.threads/` sidecars are watched —
+131
View File
@@ -0,0 +1,131 @@
/**
* GitBaselineAdapter resolves a document's git-HEAD baseline via the built-in
* Git extension API (spec §6.4, INV-7). Spike-verified: Repository.show('HEAD',
* path) + state.onDidChange work as assumed; the change event is coarse, so
* re-reads key off state.HEAD.commit; a nested repo may need openRepository().
*
* Hardening (Task 2 E2E, host-verified): for a repo that is NOT a workspace
* folder (the common case here F8/#19 precedent, coediting works on any
* file), the git extension's own filesystem watcher does not reliably notice
* an out-of-band commit (e.g. a `git commit` run from a terminal, or in the
* host E2E) `repo.state.onDidChange` can simply never fire. We additionally
* watch `.git/logs/HEAD` (the reflog VS Code itself also watches for
* in-workspace repos) with Node's own `fs.watch` and nudge `repo.status()`,
* which reliably re-syncs `state.HEAD` and lets the existing onDidChange path
* do the rest. Best-effort: any failure here just falls back to the passive
* listener.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import * as vscode from "vscode";
interface GitRepository {
rootUri: vscode.Uri;
state: { HEAD?: { commit?: string }; onDidChange: vscode.Event<void> };
show(ref: string, path: string): Promise<string>;
status(): Promise<void>;
}
interface GitAPI {
repositories: GitRepository[];
getRepository(uri: vscode.Uri): GitRepository | null;
openRepository(root: vscode.Uri): Promise<GitRepository | null>;
onDidOpenRepository: vscode.Event<GitRepository>;
}
export interface HeadBaseline { text: string; commit: string }
export class GitBaselineAdapter implements vscode.Disposable {
private api: GitAPI | null | undefined; // undefined = not resolved yet; null = unavailable
private readonly hooked = new WeakSet<GitRepository>();
private readonly lastCommit = new Map<string, string>(); // repo root → HEAD commit
private readonly emitter = new vscode.EventEmitter<void>();
readonly onDidChangeHead = this.emitter.event;
private readonly disposables: vscode.Disposable[] = [this.emitter];
private async gitApi(): Promise<GitAPI | null> {
if (this.api !== undefined) return this.api;
try {
const ext = vscode.extensions.getExtension("vscode.git");
if (!ext) return (this.api = null);
const exports = ext.isActive ? ext.exports : await ext.activate();
this.api = exports.getAPI(1) as GitAPI;
this.disposables.push(this.api.onDidOpenRepository((r) => this.hook(r)));
for (const r of this.api.repositories) this.hook(r);
} catch {
this.api = null;
}
return this.api;
}
/** HEAD blob text + commit for a tracked file; null when untracked / no repo / no HEAD. */
async headFor(uri: vscode.Uri): Promise<HeadBaseline | null> {
if (uri.scheme !== "file") return null;
const git = await this.gitApi();
if (!git) return null;
let repo = git.getRepository(uri);
if (!repo) repo = await this.discoverNested(git, uri);
if (!repo) return null;
this.hook(repo);
try {
const text = await repo.show("HEAD", uri.fsPath);
return { text, commit: repo.state.HEAD?.commit ?? "" };
} catch {
return null; // untracked, ignored, or no commit yet → snapshot mode
}
}
/** Spike finding: a nested repo may not be auto-opened — walk up for .git, then openRepository. */
private async discoverNested(git: GitAPI, uri: vscode.Uri): Promise<GitRepository | null> {
let dir = path.dirname(uri.fsPath);
for (let i = 0; i < 32; i++) {
if (fs.existsSync(path.join(dir, ".git"))) {
await git.openRepository(vscode.Uri.file(dir));
return git.getRepository(uri);
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}
/** Watch a repo; fire onDidChangeHead only when HEAD's commit actually moves. */
private hook(repo: GitRepository): void {
if (this.hooked.has(repo)) return;
this.hooked.add(repo);
this.lastCommit.set(repo.rootUri.toString(), repo.state.HEAD?.commit ?? "");
this.disposables.push(
repo.state.onDidChange(() => {
const key = repo.rootUri.toString();
const commit = repo.state.HEAD?.commit ?? "";
if (this.lastCommit.get(key) !== commit) {
this.lastCommit.set(key, commit);
this.emitter.fire();
}
}),
);
this.watchReflog(repo);
}
/** Best-effort nudge: watch `.git/logs/HEAD` directly and force `repo.status()`
* on any write, so an out-of-band commit is picked up even when the git
* extension's own watcher doesn't fire (see the class doc). */
private watchReflog(repo: GitRepository): void {
try {
const reflog = path.join(repo.rootUri.fsPath, ".git", "logs", "HEAD");
if (!fs.existsSync(reflog)) return;
let pending: ReturnType<typeof setTimeout> | undefined;
const watcher = fs.watch(reflog, () => {
if (pending) clearTimeout(pending);
pending = setTimeout(() => void repo.status().catch(() => {}), 150);
});
this.disposables.push({ dispose: () => watcher.close() });
} catch {
// best-effort only — the passive repo.state.onDidChange listener still applies.
}
}
dispose(): void {
for (const d of this.disposables) d.dispose();
}
}
+7 -2
View File
@@ -380,8 +380,13 @@ export class ProposalController implements vscode.Disposable {
/**
* F12/#64 (INV-51): ACCEPT an optimistically-applied proposal the text is
* already in the buffer, so this only advances the F6 baseline (machine-landing,
* via `attribution.signalLanded`) and clears the proposal. No re-application.
* already in the buffer, so this only marks the attribution landing (via
* `attribution.signalLanded`) and clears the proposal. No re-application.
* Native-surfaces migration (INV-7/INV-18 retirement, spec §6.4): this no
* longer advances the baseline a landed proposal stays a visible change-
* since-baseline until commit (head mode) or "Mark Changes as Reviewed"
* (snapshot mode); nothing here subscribes to `signalLanded` for baseline
* purposes anymore (the `onDidApplyAgentEdit → advance` wire was removed).
*/
async finalizeInPlace(docPath: string, proposalId: string): Promise<boolean> {
const hit = this.byId(docPath, proposalId);
+8 -5
View File
@@ -548,11 +548,14 @@ export interface RenderOptions {
/** Per-block markdown→HTML renderer (test seam). Defaults to the bundled markdown-it. */
render?: (src: string) => string;
/**
* #48: the baseline was just PINNED (`reason === "pinned"`). With zero changes
* since that pin, the on-render is fully clean no authorship coloring so a
* pin reads as "this is my clean starting point". Distinct from a baseline
* advanced by a machine-landing (accept), which keeps its authorship coloring
* (F10 INV-33). Only consulted by `renderReview`.
* #48: the baseline was just PINNED (`reason === "pinned"` "Mark Changes as
* Reviewed" in snapshot mode). With zero changes since that pin, the
* on-render is fully clean no authorship coloring so a pin reads as
* "this is my clean starting point". Distinct from an ordinary accepted
* proposal, which since the native-surfaces migration retired the
* machine-landing baseline advance (INV-18/INV-7) stays a visible change-
* since-baseline and keeps its authorship coloring (F10 INV-33). Only
* consulted by `renderReview`.
*/
pinned?: boolean;
}
+5 -5
View File
@@ -418,15 +418,15 @@ export class TrackChangesPreviewController implements vscode.Disposable {
}
private epochLabel(baseline: { reason: string; capturedAt: string } | undefined): string {
if (!baseline) return "opened (no baseline yet)";
if (!baseline) return "not coediting (no baseline yet)";
const time = new Date(baseline.capturedAt).toLocaleTimeString();
switch (baseline.reason) {
case "machine-landing":
return `Claude landed ${time}`;
case "head":
return `HEAD ${time}`;
case "pinned":
return `pinned ${time}`;
return `reviewed ${time}`;
default:
return `opened ${time}`;
return `entered ${time}`;
}
}