F3 SLICE-3: applyAgentEdit seam + live tracking + decorations/toggle (INV-7/INV-9) (#6)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-10 10:01:03 -07:00
parent decdfbf31d
commit d05cb0f9d4
6 changed files with 372 additions and 26 deletions
+7 -18
View File
@@ -1,9 +1,11 @@
/**
* ThreadController — the thin editor-facing layer (spec §6.4). Wires
* CoauthorStore + Anchorer + threadModel to vscode.comments: create-on-
* selection, reply, resolve, render, live-range tracking, reload, and
* FileSystemWatcher-driven external re-anchoring (SLICE-4). Threads are NEVER
* silently moved — an unresolvable anchor renders as an orphaned thread (INV-1).
* selection, reply, resolve, render, live-range tracking, reload, and external
* re-anchoring (SLICE-4) — driven by the SHARED sidecar watcher in extension.ts
* via handleExternalSidecarChange (self-writes are suppressed in CoauthorStore).
* Threads are NEVER silently moved — an unresolvable anchor renders as an
* orphaned thread (INV-1).
*/
import * as vscode from "vscode";
import { CoauthorStore } from "./store";
@@ -35,8 +37,6 @@ export class ThreadController implements vscode.Disposable {
private readonly controller: vscode.CommentController;
private readonly disposables: vscode.Disposable[] = [];
private readonly docs = new Map<string, DocState>(); // keyed by docPath
/** sidecar paths we just wrote, to ignore our own watcher events. */
private readonly selfWrites = new Set<string>();
constructor(private readonly store: CoauthorStore, private readonly rootDir: string) {
this.controller = vscode.comments.createCommentController("cowriting.threads", "Coauthoring Threads");
@@ -60,13 +60,6 @@ export class ThreadController implements vscode.Disposable {
vscode.workspace.onDidChangeTextDocument((e) => this.onDidChange(e)),
vscode.workspace.onDidSaveTextDocument((d) => this.onDidSave(d)),
);
// Re-anchor on external change to any sidecar (a git pull, a manual edit).
const watcher = vscode.workspace.createFileSystemWatcher("**/.threads/**/*.json");
const onSidecar = (uri: vscode.Uri) => this.onExternalSidecarChange(uri);
watcher.onDidChange(onSidecar);
watcher.onDidCreate(onSidecar);
this.disposables.push(watcher);
}
private isInRoot(uri: vscode.Uri): boolean {
@@ -83,7 +76,6 @@ export class ThreadController implements vscode.Disposable {
}
private persist(state: DocState): void {
this.selfWrites.add(this.store.sidecarPath(state.docPath));
this.store.update(state.docPath, (a) => {
a.threads = state.artifact.threads;
for (const t of state.artifact.threads) {
@@ -174,12 +166,9 @@ export class ThreadController implements vscode.Disposable {
}
}
private onExternalSidecarChange(uri: vscode.Uri): void {
/** Shared-watcher entry point (extension.ts): a sidecar changed externally. */
handleExternalSidecarChange(uri: vscode.Uri): void {
const p = uri.fsPath;
if (this.selfWrites.has(p)) {
this.selfWrites.delete(p);
return; // our own write
}
for (const state of this.docs.values()) {
if (this.store.sidecarPath(state.docPath) === p) {
const doc = vscode.workspace.textDocuments.find((d) => this.docPathOf(d.uri) === state.docPath);