refactor(f8): controllers depend on SidecarRouter + isAuthorable (key via keyOf)

Spec §6.4: constructor store CoauthorStore→SidecarRouter, rootDir→string|undefined;
isInRoot/isTracked gate → isAuthorable(scheme); per-doc key via store.keyOf(docIdentity).
Seam + artifact logic unchanged. currentAuthor omits git email when no root (fail-open).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 13:11:39 -07:00
parent 94aaff71e9
commit 7387366e28
3 changed files with 47 additions and 48 deletions
+15 -14
View File
@@ -10,13 +10,13 @@
* Claude-attributed with zero new attribution code.
*/
import * as vscode from "vscode";
import { CoauthorStore } from "./store";
import { SidecarRouter, docIdentity } from "./sidecarRouter";
import { emptyArtifact, type Artifact, type Fingerprint, type Proposal, type Provenance } from "./model";
import { resolve, shift, type OffsetRange } from "./anchorer";
import { addProposal, proposalBody, removeProposal } from "./proposalModel";
import type { AttributionController } from "./attributionController";
import type { VersionGuard } from "./versionGuard";
import { isUnderRoot } from "./workspacePath";
import { isAuthorable } from "./workspacePath";
/** Test-facing snapshot of what is currently rendered for a document. */
export interface RenderedProposal {
@@ -54,9 +54,9 @@ export class ProposalController implements vscode.Disposable {
private readonly statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 89);
constructor(
private readonly store: CoauthorStore,
private readonly store: SidecarRouter,
private readonly attribution: AttributionController,
private readonly rootDir: string,
private readonly rootDir: string | undefined,
private readonly guard: VersionGuard,
) {
// No commentingRangeProvider: humans never open proposal threads by hand —
@@ -71,13 +71,14 @@ export class ProposalController implements vscode.Disposable {
}
private isTracked(document: vscode.TextDocument): boolean {
return document.uri.scheme === "file" && isUnderRoot(document.uri.fsPath, this.rootDir);
return isAuthorable(document.uri.scheme);
}
private docPathOf(uri: vscode.Uri): string {
return vscode.workspace.asRelativePath(uri, false);
/** The single document key (F8): repo-relative path in-workspace, URI string otherwise. */
private keyOf(document: vscode.TextDocument): string {
return this.store.keyOf(docIdentity(document));
}
private ensureState(document: vscode.TextDocument): DocState {
const docPath = this.docPathOf(document.uri);
const docPath = this.keyOf(document);
let state = this.docs.get(docPath);
if (!state) {
state = {
@@ -108,8 +109,8 @@ export class ProposalController implements vscode.Disposable {
opts?: { turnId?: string; instruction?: string },
): Promise<string | undefined> {
if (!this.isTracked(document)) return undefined;
if (this.guard.isReadOnly(this.docPathOf(document.uri))) return undefined;
const docPath = this.docPathOf(document.uri);
if (this.guard.isReadOnly(this.keyOf(document))) return undefined;
const docPath = this.keyOf(document);
let proposalId: string | undefined;
this.store.update(docPath, (a) => {
proposalId = addProposal(a, fp, replacement, author, opts).proposalId;
@@ -185,7 +186,7 @@ export class ProposalController implements vscode.Disposable {
/** Load + (re)render every pending proposal at its resolved anchor (or flagged). */
renderAll(document: vscode.TextDocument): void {
if (!this.isTracked(document)) return;
const docPath = this.docPathOf(document.uri);
const docPath = this.keyOf(document);
const state = this.ensureState(document);
state.artifact = this.store.load(docPath) ?? emptyArtifact(docPath);
for (const vsThread of state.vsThreads.values()) vsThread.dispose();
@@ -212,14 +213,14 @@ export class ProposalController implements vscode.Disposable {
handleExternalSidecarChange(uri: vscode.Uri): void {
for (const state of this.docs.values()) {
if (this.store.sidecarPath(state.docPath) === uri.fsPath) {
const doc = vscode.workspace.textDocuments.find((d) => this.docPathOf(d.uri) === state.docPath);
const doc = vscode.workspace.textDocuments.find((d) => this.keyOf(d) === state.docPath);
if (doc) this.renderAll(doc);
}
}
}
private onDidChange(e: vscode.TextDocumentChangeEvent): void {
const state = this.docs.get(this.docPathOf(e.document.uri));
const state = this.docs.get(this.keyOf(e.document));
if (!state || state.live.size === 0) return;
for (const change of e.contentChanges) {
const edit = { start: change.rangeOffset, end: change.rangeOffset + change.rangeLength, newLength: change.text.length };
@@ -298,7 +299,7 @@ export class ProposalController implements vscode.Disposable {
// ---- lookups ----------------------------------------------------------------------------
private openDoc(state: DocState): vscode.TextDocument | undefined {
return vscode.workspace.textDocuments.find((d) => this.docPathOf(d.uri) === state.docPath);
return vscode.workspace.textDocuments.find((d) => this.keyOf(d) === state.docPath);
}
private byThread(vsThread: vscode.CommentThread): { state: DocState; proposal: Proposal } | undefined {
for (const state of this.docs.values()) {