feat(f8): isAuthorable + widened selectionRejection (file|untitled)
Spec §6.4: replace the scheme!=file / !isUnderRoot rejections with a single !isAuthorable branch. isUnderRoot retained as a routing input. Per-condition messaging (#24) kept for no-editor / empty-selection / non-{file,untitled}. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-13
@@ -20,6 +20,17 @@ export function isUnderRoot(fsPath: string, root: string): boolean {
|
|||||||
return fsPath === root || fsPath.startsWith(root + path.sep);
|
return fsPath === root || fsPath.startsWith(root + path.sep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Documents Cowriting can author on: a saved file OR an unsaved buffer (F8 §6.2).
|
||||||
|
* F8 widened membership from "saved file under the workspace folder" to any
|
||||||
|
* `file:`/`untitled:` doc — the SidecarRouter then routes where its artifact is
|
||||||
|
* stored (repo `.threads/` in-workspace, global storage otherwise). `isUnderRoot`
|
||||||
|
* (above) is retained as that ROUTING input, no longer an authoring gate.
|
||||||
|
*/
|
||||||
|
export function isAuthorable(scheme: string): boolean {
|
||||||
|
return scheme === "file" || scheme === "untitled";
|
||||||
|
}
|
||||||
|
|
||||||
export interface SelectionContext {
|
export interface SelectionContext {
|
||||||
/** Is there an active text editor at all? */
|
/** Is there an active text editor at all? */
|
||||||
hasEditor: boolean;
|
hasEditor: boolean;
|
||||||
@@ -27,17 +38,15 @@ export interface SelectionContext {
|
|||||||
selectionEmpty: boolean;
|
selectionEmpty: boolean;
|
||||||
/** The active document's URI scheme (`file`, `untitled`, …). */
|
/** The active document's URI scheme (`file`, `untitled`, …). */
|
||||||
scheme: string;
|
scheme: string;
|
||||||
/** The active document's filesystem path (empty if none). */
|
|
||||||
fsPath: string;
|
|
||||||
/** The workspace folder Cowriting is anchored to. */
|
|
||||||
root: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Why a selection can't be sent to Claude — or `null` if it can. Each condition
|
* Why a selection can't be sent to Claude — or `null` if it can. F8 widened the
|
||||||
* gets its OWN message so the user learns the real reason instead of always
|
* membership: any `file:` or `untitled:` document is authorable (in-workspace
|
||||||
* being told to "select some text" (the reported bug: an out-of-workspace file
|
* files persist to the repo `.threads/` sidecar, out-of-workspace/untitled to
|
||||||
* was rejected with the no-selection message even though text was selected).
|
* global storage — the router decides). Only a non-{file,untitled} scheme (a
|
||||||
|
* read-only `git:`/`output:` view), a missing editor, or an empty selection is
|
||||||
|
* refused — each with its OWN message (#24's per-condition messaging).
|
||||||
*/
|
*/
|
||||||
export function selectionRejection(ctx: SelectionContext): string | null {
|
export function selectionRejection(ctx: SelectionContext): string | null {
|
||||||
if (!ctx.hasEditor) {
|
if (!ctx.hasEditor) {
|
||||||
@@ -46,11 +55,8 @@ export function selectionRejection(ctx: SelectionContext): string | null {
|
|||||||
if (ctx.selectionEmpty) {
|
if (ctx.selectionEmpty) {
|
||||||
return "Cowriting: select some text to send to Claude first.";
|
return "Cowriting: select some text to send to Claude first.";
|
||||||
}
|
}
|
||||||
if (ctx.scheme !== "file") {
|
if (!isAuthorable(ctx.scheme)) {
|
||||||
return "Cowriting: save this document to a file first — Cowriting anchors edits to a saved workspace file.";
|
return "Cowriting: this kind of document can't be edited — Cowriting authors on a file or an untitled buffer, not a read-only view.";
|
||||||
}
|
|
||||||
if (!isUnderRoot(ctx.fsPath, ctx.root)) {
|
|
||||||
return "Cowriting: this file is outside your workspace folder — open a file inside it to ask Claude to edit (Cowriting anchors edits and attribution to files in the workspace).";
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-21
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { isUnderRoot, selectionRejection } from "../src/workspacePath";
|
import { isAuthorable, isUnderRoot, selectionRejection } from "../src/workspacePath";
|
||||||
|
|
||||||
describe("isUnderRoot", () => {
|
describe("isUnderRoot", () => {
|
||||||
const root = "/a/vscode-cowriting-plugin/sandbox";
|
const root = "/a/vscode-cowriting-plugin/sandbox";
|
||||||
@@ -24,32 +24,37 @@ describe("isUnderRoot", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("selectionRejection", () => {
|
describe("isAuthorable", () => {
|
||||||
const root = "/ws";
|
it("accepts file: and untitled: schemes", () => {
|
||||||
const ok = { hasEditor: true, selectionEmpty: false, scheme: "file", fsPath: "/ws/a.md", root };
|
expect(isAuthorable("file")).toBe(true);
|
||||||
|
expect(isAuthorable("untitled")).toBe(true);
|
||||||
it("returns null when everything is valid", () => {
|
|
||||||
expect(selectionRejection(ok)).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
it("rejects read-only / virtual schemes", () => {
|
||||||
|
expect(isAuthorable("git")).toBe(false);
|
||||||
|
expect(isAuthorable("output")).toBe(false);
|
||||||
|
expect(isAuthorable("cowriting-baseline")).toBe(false);
|
||||||
|
expect(isAuthorable("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("selectionRejection — F8 widened (accepts out-of-folder + untitled)", () => {
|
||||||
|
const ok = { hasEditor: true, selectionEmpty: false, scheme: "file" };
|
||||||
|
|
||||||
|
it("accepts an in-folder / out-of-folder file (file: scheme)", () => {
|
||||||
|
expect(selectionRejection({ ...ok, scheme: "file" })).toBeNull();
|
||||||
|
});
|
||||||
|
it("accepts an untitled buffer (no longer rejected)", () => {
|
||||||
|
expect(selectionRejection({ ...ok, scheme: "untitled" })).toBeNull();
|
||||||
|
});
|
||||||
it("names the missing editor", () => {
|
it("names the missing editor", () => {
|
||||||
const msg = selectionRejection({ ...ok, hasEditor: false });
|
expect(selectionRejection({ ...ok, hasEditor: false })).toMatch(/focus a text editor/i);
|
||||||
expect(msg).toMatch(/focus a text editor/i);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("names the empty selection (only when there IS an editor)", () => {
|
it("names the empty selection (only when there IS an editor)", () => {
|
||||||
const msg = selectionRejection({ ...ok, selectionEmpty: true });
|
expect(selectionRejection({ ...ok, selectionEmpty: true })).toMatch(/select some text/i);
|
||||||
expect(msg).toMatch(/select some text/i);
|
|
||||||
});
|
});
|
||||||
|
it("rejects a non-{file,untitled} scheme with its own message (not 'select some text')", () => {
|
||||||
it("names an unsaved/non-file document", () => {
|
const msg = selectionRejection({ ...ok, scheme: "git" });
|
||||||
const msg = selectionRejection({ ...ok, scheme: "untitled" });
|
expect(msg).toMatch(/can.?t be edited|read-only|not a file/i);
|
||||||
expect(msg).toMatch(/save this document/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("names an out-of-workspace file (the reported bug — NOT a selection problem)", () => {
|
|
||||||
const msg = selectionRejection({ ...ok, fsPath: "/elsewhere/a.md" });
|
|
||||||
expect(msg).toMatch(/outside your workspace folder/i);
|
|
||||||
expect(msg).not.toMatch(/select some text/i);
|
expect(msg).not.toMatch(/select some text/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user