feat(f8): wire SidecarRouter; authoring commands live folder-less (#19 precedent)
Spec §6.4: construct GlobalSidecarStore + SidecarRouter (CoauthorStore when root); remove the no-root early-return + command stubs so F2/F3/F4 are real folder-less; renderIfOpen gated by isAuthorable; editor-context menus widened to untitled; export sidecarRouter on CowritingApi. activate now always returns the API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -112,12 +112,12 @@
|
|||||||
"editor/context": [
|
"editor/context": [
|
||||||
{
|
{
|
||||||
"command": "cowriting.editSelection",
|
"command": "cowriting.editSelection",
|
||||||
"when": "editorHasSelection && resourceScheme == file",
|
"when": "editorHasSelection && (resourceScheme == file || resourceScheme == untitled)",
|
||||||
"group": "1_cowriting@1"
|
"group": "1_cowriting@1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cowriting.createThread",
|
"command": "cowriting.createThread",
|
||||||
"when": "editorHasSelection && resourceScheme == file",
|
"when": "editorHasSelection && (resourceScheme == file || resourceScheme == untitled)",
|
||||||
"group": "1_cowriting@2"
|
"group": "1_cowriting@2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+37
-48
@@ -7,9 +7,11 @@ import { ProposalController } from "./proposalController";
|
|||||||
import { buildFingerprint } from "./anchorer";
|
import { buildFingerprint } from "./anchorer";
|
||||||
import { VersionGuard } from "./versionGuard";
|
import { VersionGuard } from "./versionGuard";
|
||||||
import { BaselineStore } from "./baselineStore";
|
import { BaselineStore } from "./baselineStore";
|
||||||
|
import { GlobalSidecarStore } from "./globalSidecarStore";
|
||||||
|
import { SidecarRouter } from "./sidecarRouter";
|
||||||
import { DiffViewController } from "./diffViewController";
|
import { DiffViewController } from "./diffViewController";
|
||||||
import { TrackChangesPreviewController } from "./trackChangesPreview";
|
import { TrackChangesPreviewController } from "./trackChangesPreview";
|
||||||
import { isUnderRoot, selectionRejection } from "./workspacePath";
|
import { isAuthorable, selectionRejection } from "./workspacePath";
|
||||||
|
|
||||||
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ export interface CowritingApi {
|
|||||||
versionGuard: VersionGuard;
|
versionGuard: VersionGuard;
|
||||||
diffViewController: DiffViewController;
|
diffViewController: DiffViewController;
|
||||||
trackChangesPreviewController: TrackChangesPreviewController;
|
trackChangesPreviewController: TrackChangesPreviewController;
|
||||||
|
sidecarRouter: SidecarRouter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
|
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
|
||||||
@@ -59,6 +62,14 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
const diffViewController = new DiffViewController(baselineStore);
|
const diffViewController = new DiffViewController(baselineStore);
|
||||||
context.subscriptions.push(diffViewController);
|
context.subscriptions.push(diffViewController);
|
||||||
|
|
||||||
|
// F8: the out-of-workspace/untitled authoring sidecar — same GLOBAL storage
|
||||||
|
// home as the F6 baseline, keyed by sha256(uri) (INV-19/24). Constructed
|
||||||
|
// workspace-independently; the router falls back to it for any non-in-folder
|
||||||
|
// doc. When globalStorageUri is unavailable, file writes throw (authoring
|
||||||
|
// degrades to errors-on-write, the F6-equivalent edge) but untitled in-memory
|
||||||
|
// still works — PUC-5.
|
||||||
|
const globalSidecarStore = new GlobalSidecarStore(baselineStorageDir ?? "");
|
||||||
|
|
||||||
// --- F7: rendered track-changes preview (Feature #21) — workspace-INDEPENDENT ---
|
// --- F7: rendered track-changes preview (Feature #21) — workspace-INDEPENDENT ---
|
||||||
// Like F6, F7 works on any markdown doc (incl. untitled / out-of-folder), so it
|
// Like F6, F7 works on any markdown doc (incl. untitled / out-of-folder), so it
|
||||||
// is constructed regardless of an open folder and its command is always live.
|
// is constructed regardless of an open folder and its command is always live.
|
||||||
@@ -69,64 +80,42 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
);
|
);
|
||||||
context.subscriptions.push(trackChangesPreviewController);
|
context.subscriptions.push(trackChangesPreviewController);
|
||||||
|
|
||||||
// --- F2: region-anchored threads (Feature #4) ---
|
// --- F8: authoring on ANY document (in-folder, out-of-folder, untitled) ---
|
||||||
|
// The router routes per-document: in-workspace file: → the committable repo
|
||||||
|
// `.threads/` sidecar (CoauthorStore, INV-2); out-of-folder file: + untitled:
|
||||||
|
// → the global-storage sidecar (GlobalSidecarStore). Constructed even with NO
|
||||||
|
// folder open (everything then routes global) — the F6 #19 precedent, now
|
||||||
|
// extended to authoring (F2 threads, F3 attribution, F4 propose/accept).
|
||||||
const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
if (!root) {
|
const coauthorStore = root ? new CoauthorStore(root) : null;
|
||||||
// No folder open → nothing to anchor against, but every contributed
|
const sidecarRouter = new SidecarRouter(coauthorStore, globalSidecarStore, root);
|
||||||
// command must still exist: leave them unregistered and the palette
|
|
||||||
// errors with "command not found" (#8). Register warning stubs instead;
|
|
||||||
// opening a folder reloads the window, re-running activate with a root.
|
|
||||||
const stub = () =>
|
|
||||||
void vscode.window.showWarningMessage(
|
|
||||||
"Cowriting: open a folder first — coauthoring anchors threads and attribution to workspace files.",
|
|
||||||
);
|
|
||||||
for (const command of [
|
|
||||||
"cowriting.createThread",
|
|
||||||
"cowriting.reply",
|
|
||||||
"cowriting.resolveThread",
|
|
||||||
"cowriting.reopenThread",
|
|
||||||
"cowriting.editSelection",
|
|
||||||
"cowriting.toggleAttribution",
|
|
||||||
"cowriting.applyAgentEdit",
|
|
||||||
"cowriting.acceptProposal",
|
|
||||||
"cowriting.rejectProposal",
|
|
||||||
"cowriting.proposeAgentEdit",
|
|
||||||
]) {
|
|
||||||
context.subscriptions.push(vscode.commands.registerCommand(command, stub));
|
|
||||||
}
|
|
||||||
// F6 (toggleDiffView / pinDiffBaseline) is NOT stubbed here — it works
|
|
||||||
// without a folder (on untitled buffers and out-of-folder files); its real
|
|
||||||
// commands were registered above by DiffViewController.
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const store = new CoauthorStore(root);
|
|
||||||
// F5 (INV-16): one shared guard — newer-major sidecars are read-only, one
|
// F5 (INV-16): one shared guard — newer-major sidecars are read-only, one
|
||||||
// warning per doc across the three co-owning controllers.
|
// warning per doc across the three co-owning controllers.
|
||||||
const versionGuard = new VersionGuard(store);
|
const versionGuard = new VersionGuard(sidecarRouter);
|
||||||
const threadController = new ThreadController(store, root, versionGuard);
|
const threadController = new ThreadController(sidecarRouter, root, versionGuard);
|
||||||
context.subscriptions.push(threadController);
|
context.subscriptions.push(threadController);
|
||||||
|
|
||||||
// --- F3: live attribution (Feature #6) ---
|
// --- F3: live attribution (Feature #6) ---
|
||||||
const attributionController = new AttributionController(store, root, versionGuard);
|
const attributionController = new AttributionController(sidecarRouter, root, versionGuard);
|
||||||
context.subscriptions.push(attributionController);
|
context.subscriptions.push(attributionController);
|
||||||
|
|
||||||
// --- F4: propose/accept (Feature #12) ---
|
// --- F4: propose/accept (Feature #12) ---
|
||||||
const proposalController = new ProposalController(store, attributionController, root, versionGuard);
|
const proposalController = new ProposalController(sidecarRouter, attributionController, root, versionGuard);
|
||||||
context.subscriptions.push(proposalController);
|
context.subscriptions.push(proposalController);
|
||||||
|
|
||||||
// --- F6 machine-landing wiring (with-root only) ---
|
// --- F6 machine-landing wiring — now for ANY authorable doc ---
|
||||||
// The seam's single machine-landing signal advances the baseline (INV-18).
|
// The seam's single machine-landing signal advances the F6 baseline (INV-18);
|
||||||
// The seam fires only on workspace files, so this wiring belongs here; the
|
// the seam can now fire on out-of-folder files too, so wire it unconditionally.
|
||||||
// DiffViewController itself was constructed above (workspace-independent).
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
attributionController.onDidApplyAgentEdit((e) => diffViewController.advance(e.document)),
|
attributionController.onDidApplyAgentEdit((e) => diffViewController.advance(e.document)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// One SHARED sidecar watcher for both controllers; self-writes are
|
// One SHARED sidecar watcher for both controllers; self-writes are suppressed
|
||||||
// suppressed centrally in the store (the sidecar is co-owned).
|
// centrally in the repo store (only repo `.threads/` sidecars are watched —
|
||||||
|
// global artifacts live outside the workspace). Harmless when no folder is open.
|
||||||
const watcher = vscode.workspace.createFileSystemWatcher("**/.threads/**/*.json");
|
const watcher = vscode.workspace.createFileSystemWatcher("**/.threads/**/*.json");
|
||||||
const onSidecar = (uri: vscode.Uri) => {
|
const onSidecar = (uri: vscode.Uri) => {
|
||||||
if (store.consumeSelfWrite(uri.fsPath)) return;
|
if (sidecarRouter.consumeSelfWrite(uri.fsPath)) return;
|
||||||
threadController.handleExternalSidecarChange(uri);
|
threadController.handleExternalSidecarChange(uri);
|
||||||
attributionController.handleExternalSidecarChange(uri);
|
attributionController.handleExternalSidecarChange(uri);
|
||||||
proposalController.handleExternalSidecarChange(uri);
|
proposalController.handleExternalSidecarChange(uri);
|
||||||
@@ -188,15 +177,14 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand("cowriting.editSelection", async () => {
|
vscode.commands.registerCommand("cowriting.editSelection", async () => {
|
||||||
const editor = vscode.window.activeTextEditor;
|
const editor = vscode.window.activeTextEditor;
|
||||||
// Each failure names its real reason (no editor / no selection / unsaved /
|
// F8: authoring works on any file: or untitled: doc (the router decides
|
||||||
// outside the workspace folder) — not always "select some text" (#bug:
|
// where its artifact is stored). Each failure still names its real reason
|
||||||
// an out-of-workspace file was rejected with the no-selection message).
|
// (no editor / no selection / a non-{file,untitled} read-only view) — not
|
||||||
|
// always "select some text" (#24's per-condition messaging).
|
||||||
const reason = selectionRejection({
|
const reason = selectionRejection({
|
||||||
hasEditor: !!editor,
|
hasEditor: !!editor,
|
||||||
selectionEmpty: editor?.selection.isEmpty ?? true,
|
selectionEmpty: editor?.selection.isEmpty ?? true,
|
||||||
scheme: editor?.document.uri.scheme ?? "",
|
scheme: editor?.document.uri.scheme ?? "",
|
||||||
fsPath: editor?.document.uri.fsPath ?? "",
|
|
||||||
root,
|
|
||||||
});
|
});
|
||||||
if (reason) {
|
if (reason) {
|
||||||
void vscode.window.showWarningMessage(reason);
|
void vscode.window.showWarningMessage(reason);
|
||||||
@@ -269,7 +257,7 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
|
|
||||||
// Render threads + attributions for already-open editors, and on future opens.
|
// Render threads + attributions for already-open editors, and on future opens.
|
||||||
const renderIfOpen = (doc: vscode.TextDocument) => {
|
const renderIfOpen = (doc: vscode.TextDocument) => {
|
||||||
if (doc.uri.scheme === "file" && isUnderRoot(doc.uri.fsPath, root)) {
|
if (isAuthorable(doc.uri.scheme)) {
|
||||||
threadController.renderAll(doc);
|
threadController.renderAll(doc);
|
||||||
attributionController.loadAll(doc);
|
attributionController.loadAll(doc);
|
||||||
proposalController.renderAll(doc);
|
proposalController.renderAll(doc);
|
||||||
@@ -285,6 +273,7 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
versionGuard,
|
versionGuard,
|
||||||
diffViewController,
|
diffViewController,
|
||||||
trackChangesPreviewController,
|
trackChangesPreviewController,
|
||||||
|
sidecarRouter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user