/** * workspacePath — pure, vscode-free helpers for the "is this document one * Cowriting can anchor to?" decision (F2/F3/F4 require a SAVED file under the * workspace folder, because threads/attribution/proposals persist to a * `.threads/` sidecar beside that file). * * Split out so the membership test and the per-condition warning are * deterministic and unit-testable with no editor — see `test/workspacePath.test.ts`. */ import * as path from "node:path"; /** * Is `fsPath` the workspace root or a path strictly inside it? * * Uses a path-separator boundary, NOT a bare `startsWith(root)` — otherwise a * sibling whose name merely begins with the root's name would falsely match * (e.g. `.../vscode-cowriting-plugin-content/x` vs root `.../vscode-cowriting-plugin`). */ export function isUnderRoot(fsPath: string, root: string): boolean { 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 { /** Is there an active text editor at all? */ hasEditor: boolean; /** Is the active editor's selection empty (no highlight)? */ selectionEmpty: boolean; /** The active document's URI scheme (`file`, `untitled`, …). */ scheme: string; } /** * Why a selection can't be sent to Claude — or `null` if it can. F8 widened the * membership: any `file:` or `untitled:` document is authorable (in-workspace * files persist to the repo `.threads/` sidecar, out-of-workspace/untitled to * 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 { if (!ctx.hasEditor) { return "Cowriting: focus a text editor with a selection first."; } if (ctx.selectionEmpty) { return "Cowriting: select some text to send to Claude first."; } if (!isAuthorable(ctx.scheme)) { return "Cowriting: this kind of document can't be edited — Cowriting authors on a file or an untitled buffer, not a read-only view."; } return null; }