Out-of-workspace authoring (global-storage sidecar fallback) #25

Closed
opened 2026-06-11 19:29:56 +00:00 by benstull · 1 comment
Owner

Out-of-workspace authoring (global-storage sidecar fallback)

Summary

Let the human ask Claude to author into any file, not just files under the
opened workspace folder — extending threads, live attribution, and propose/accept
(F2/F3/F4) to out-of-workspace and untitled documents by falling back to a
global-storage sidecar (keyed by URI hash, like F6's baseline) when there is no
committable .threads/ home.

Problem / pain

The diff view (F6, #19) and the rendered preview (F7, #21) were broadened to work
on any file — in-folder, out-of-folder, or untitled — because they persist
nothing in the repo (F6's baseline lives in VS Code global storage keyed by URI
hash; F7 persists nothing). But the authoring surfaces — F2 threads, F3
attribution, F4 "Ask Claude to Edit Selection" — still refuse any document that
isn't a saved file: under workspaceFolders[0], because they persist a
git-native .threads/<repo-relative-path>.json sidecar under the workspace root
(store.ts sidecarPath), which has no home for an out-of-workspace file.

The result is a surprising asymmetry: you can diff/preview a file the extension
isn't anchored to, but you cannot ask Claude to edit it — and until the
session-0022 fix the refusal even misreported the reason ("select some text"
instead of "this file is outside your workspace folder").

Who feels it

  • PP-1 Inner-loop coauthor — the human writer/engineer, when working on a file
    opened outside the workspace folder (a doc from a sibling repo, a scratch file)
    or an untitled buffer, and expecting the same "ask Claude to edit" affordance the
    diff/preview already offer there.

Desired outcome / value

The human can select text and ask Claude to edit it in any document the editor
will show — so the authoring loop matches the (already universal) diff/preview
loop, removing the "why does it work here but not there?" friction. Authoring on a
workspace file is unchanged; authoring on an out-of-workspace/untitled file simply
works, with its state kept where a repo sidecar can't live.

Acceptance — "done right"

  • "Ask Claude to Edit Selection" (and threads/attribution) succeed on a saved file
    outside the workspace folder and on an untitled buffer, not just
    in-workspace files.
  • In-workspace files are byte-for-byte unchanged: they keep their committable
    .threads/ git-native sidecar (INV-2 preserved) — no migration, no behavior
    change for the existing flow.
  • Out-of-workspace / untitled documents persist their artifact (threads /
    attributions / proposals) in global storage keyed by a hash of the document
    URI
    , mirroring F6's baseline storage and INV-19 (never the repo).
  • Re-anchoring still works (anchoring is content-based, F2), and reload restores
    spans/threads for a global-storage doc the same way it does for a sidecar doc.
  • The behavior is covered by host E2E (no LLM), reusing the F2–F6 programmatic
    seam pattern.

Scope / non-goals

In scope: a hybrid persistence model — in-workspace file: docs keep the
repo .threads/ sidecar; out-of-workspace file: docs and untitled: buffers use
a global-storage sidecar keyed by URI hash. One storage abstraction the three
authoring controllers (thread / attribution / proposal) share. Accurate scope in
the editSelection guard (the session-0022 messaging already distinguishes the
conditions; this widens what's accepted).

Out of scope / non-goals: uniform global storage (moving in-workspace docs
off the committable sidecar — explicitly rejected: it would lose committable
threads/attribution, the point of INV-2); migrating existing repo sidecars;
cross-rung sharing of global-storage artifacts (see below); per-author coloring or
any UI change beyond accepting more documents.

Assumptions · constraints · dependencies

  • Builds on F2 #4 (threads), F3 #6 (attribution), F4 #12
    (propose/accept), and the F6 #17/#19 global-storage-by-URI-hash precedent
    (BaselineStore, INV-19). Parent: Epic #1.
  • F5 cross-rung interaction (#14): the cross-rung contract shares the artifact
    as a committed file. A global-storage sidecar is not a committed file, so
    out-of-workspace authoring artifacts are not cross-rung-shareable — which is
    acceptable (an out-of-repo file isn't shared via the repo anyway), but the design
    must state it explicitly.
  • Identity caveat: global-storage keying is by URI hash, so renaming/moving an
    out-of-workspace file orphans its artifact (the repo sidecar, by contrast, moves
    with the file in git). The design should call this out.
  • Touches INV-2 (git-native sidecar) by adding an alternative persistence path — a
    load-bearing change, so this Feature needs a Solution Design before
    implementation (handbook §3.4 R3).
  • No LLM/network/credential surface added (INV-8 untouched); SCHEMA_VERSION and
    the cross-rung contract format are unchanged (the artifact shape is identical —
    only where it is stored differs).

Source / signal

Capture session vscode-cowriting-plugin-0023. Originated during F7 manual
testing
(sessions 0021 / 0022): "Ask Claude to Edit Selection" was refused on
vscode-cowriting-plugin-content/issues/diff-view-toggle.md (outside the EDH
sandbox/ root). Session 0022 (PR #24) fixed the misleading message and the
startsWith prefix-collision; this Feature is the follow-on the operator chose —
extend authoring to any file via global storage (the "global storage like F6"
fork). See memory editselection-workspace-membership.md.

Priority (WSJF)

Provisional: Value 3 · Time-criticality 2 · Opportunity-enablement 2 ÷ Size 3 ≈
2.3. priority/P2 — a real UX consistency win, not urgent; sequence after the
open F7 follow-up #22 unless the operator reprioritizes.

Decomposition (Feature → slice checklist)

  • Storage abstraction: a sidecar store that resolves repo .threads/ for
    in-workspace file: docs and global storage (URI-hash key) otherwise — shared by
    the thread / attribution / proposal controllers (today CoauthorStore is
    workspace-root-bound).
  • Widen the authoring gate: selectionRejection / controller membership
    accept out-of-workspace file: + untitled: docs, routing them to the
    global-storage store.
  • Untitled identity: key untitled buffers like F6 does (in-memory / URI),
    documenting the move/rename orphaning caveat.
  • Host E2E (no LLM): authoring on an out-of-workspace file + an untitled
    buffer (open → propose via seam → accept → reload restores), reusing the F2–F6
    harness.
  • Docs: README + a note on the hybrid model and the F5 non-shareability of
    global-storage artifacts.

Solution notes (optional — NOT a design)

Non-binding: the cleanest shape is likely an interface the controllers depend on
(e.g. SidecarStore) with two implementations — the existing repo-rooted
CoauthorStore and a GlobalSidecarStore mirroring BaselineStore's URI-hash
keying — selected per document by the same isUnderRoot predicate added in #24.
The artifact JSON is identical either way, so mergeArtifacts / the cross-rung
format (F5) need no change. To be settled in the Solution Design.

<!-- Set the type/* label: feature --> # Out-of-workspace authoring (global-storage sidecar fallback) ## Summary Let the human ask Claude to author into **any file**, not just files under the opened workspace folder — extending threads, live attribution, and propose/accept (F2/F3/F4) to out-of-workspace and untitled documents by falling back to a **global-storage sidecar** (keyed by URI hash, like F6's baseline) when there is no committable `.threads/` home. ## Problem / pain The diff view (F6, #19) and the rendered preview (F7, #21) were broadened to work on **any** file — in-folder, out-of-folder, or untitled — because they persist nothing in the repo (F6's baseline lives in VS Code global storage keyed by URI hash; F7 persists nothing). But the **authoring** surfaces — F2 threads, F3 attribution, F4 "Ask Claude to Edit Selection" — still refuse any document that isn't a saved `file:` under `workspaceFolders[0]`, because they persist a git-native `.threads/<repo-relative-path>.json` sidecar under the workspace root (`store.ts` `sidecarPath`), which has no home for an out-of-workspace file. The result is a surprising asymmetry: you can **diff/preview** a file the extension isn't anchored to, but you **cannot ask Claude to edit** it — and until the session-0022 fix the refusal even misreported the reason ("select some text" instead of "this file is outside your workspace folder"). ## Who feels it - **PP-1 Inner-loop coauthor** — the human writer/engineer, when working on a file opened outside the workspace folder (a doc from a sibling repo, a scratch file) or an untitled buffer, and expecting the same "ask Claude to edit" affordance the diff/preview already offer there. ## Desired outcome / value The human can select text and ask Claude to edit it in **any** document the editor will show — so the authoring loop matches the (already universal) diff/preview loop, removing the "why does it work here but not there?" friction. Authoring on a workspace file is unchanged; authoring on an out-of-workspace/untitled file simply works, with its state kept where a repo sidecar can't live. ## Acceptance — "done right" - "Ask Claude to Edit Selection" (and threads/attribution) succeed on a saved file **outside** the workspace folder and on an **untitled** buffer, not just in-workspace files. - **In-workspace files are byte-for-byte unchanged**: they keep their committable `.threads/` git-native sidecar (INV-2 preserved) — no migration, no behavior change for the existing flow. - Out-of-workspace / untitled documents persist their artifact (threads / attributions / proposals) in **global storage keyed by a hash of the document URI**, mirroring F6's baseline storage and INV-19 (never the repo). - Re-anchoring still works (anchoring is content-based, F2), and reload restores spans/threads for a global-storage doc the same way it does for a sidecar doc. - The behavior is covered by host E2E (no LLM), reusing the F2–F6 programmatic seam pattern. ## Scope / non-goals **In scope:** a **hybrid** persistence model — in-workspace `file:` docs keep the repo `.threads/` sidecar; out-of-workspace `file:` docs and `untitled:` buffers use a global-storage sidecar keyed by URI hash. One storage abstraction the three authoring controllers (thread / attribution / proposal) share. Accurate scope in the `editSelection` guard (the session-0022 messaging already distinguishes the conditions; this widens what's *accepted*). **Out of scope / non-goals:** **uniform** global storage (moving in-workspace docs off the committable sidecar — explicitly rejected: it would lose committable threads/attribution, the point of INV-2); migrating existing repo sidecars; cross-rung sharing of global-storage artifacts (see below); per-author coloring or any UI change beyond accepting more documents. ## Assumptions · constraints · dependencies - Builds on **F2 #4** (threads), **F3 #6** (attribution), **F4 #12** (propose/accept), and the **F6 #17/#19** global-storage-by-URI-hash precedent (`BaselineStore`, INV-19). Parent: Epic **#1**. - **F5 cross-rung interaction (#14):** the cross-rung contract shares the artifact *as a committed file*. A global-storage sidecar is **not** a committed file, so out-of-workspace authoring artifacts are **not cross-rung-shareable** — which is acceptable (an out-of-repo file isn't shared via the repo anyway), but the design must state it explicitly. - **Identity caveat:** global-storage keying is by URI hash, so renaming/moving an out-of-workspace file orphans its artifact (the repo sidecar, by contrast, moves with the file in git). The design should call this out. - Touches INV-2 (git-native sidecar) by adding an alternative persistence path — a **load-bearing** change, so this Feature needs a **Solution Design** before implementation (handbook §3.4 R3). - No LLM/network/credential surface added (INV-8 untouched); `SCHEMA_VERSION` and the cross-rung contract format are unchanged (the artifact shape is identical — only *where* it is stored differs). ## Source / signal Capture session **vscode-cowriting-plugin-0023**. Originated during **F7 manual testing** (sessions 0021 / 0022): "Ask Claude to Edit Selection" was refused on `vscode-cowriting-plugin-content/issues/diff-view-toggle.md` (outside the EDH `sandbox/` root). Session 0022 (PR #24) fixed the *misleading message* and the `startsWith` prefix-collision; this Feature is the follow-on the operator chose — **extend authoring to any file via global storage** (the "global storage like F6" fork). See memory `editselection-workspace-membership.md`. ## Priority (WSJF) Provisional: Value 3 · Time-criticality 2 · Opportunity-enablement 2 ÷ Size 3 ≈ **2.3**. `priority/P2` — a real UX consistency win, not urgent; sequence after the open F7 follow-up #22 unless the operator reprioritizes. ## Decomposition (Feature → slice checklist) - [ ] **Storage abstraction:** a sidecar store that resolves repo `.threads/` for in-workspace `file:` docs and global storage (URI-hash key) otherwise — shared by the thread / attribution / proposal controllers (today `CoauthorStore` is workspace-root-bound). - [ ] **Widen the authoring gate:** `selectionRejection` / controller membership accept out-of-workspace `file:` + `untitled:` docs, routing them to the global-storage store. - [ ] **Untitled identity:** key untitled buffers like F6 does (in-memory / URI), documenting the move/rename orphaning caveat. - [ ] **Host E2E (no LLM):** authoring on an out-of-workspace file + an untitled buffer (open → propose via seam → accept → reload restores), reusing the F2–F6 harness. - [ ] **Docs:** README + a note on the hybrid model and the F5 non-shareability of global-storage artifacts. ## Solution notes (optional — NOT a design) Non-binding: the cleanest shape is likely an interface the controllers depend on (e.g. `SidecarStore`) with two implementations — the existing repo-rooted `CoauthorStore` and a `GlobalSidecarStore` mirroring `BaselineStore`'s URI-hash keying — selected per document by the same `isUnderRoot` predicate added in #24. The artifact JSON is identical either way, so `mergeArtifacts` / the cross-rung format (F5) need no change. To be settled in the Solution Design.
benstull added the type/featurepriority/P2 labels 2026-06-11 19:29:56 +00:00
Author
Owner

Shipped to main in PR #26 (session 0025). Hybrid persistence: in-workspace files keep the committable .threads/ sidecar byte-for-byte (INV-2); out-of-workspace file: + untitled: route to a global-storage sidecar keyed by sha256(uri) (INV-19/24/25). 150 unit + 41 host-E2E tests green. Non-shippable (no marketplace publish) — done = merge + tests green.

Shipped to `main` in PR #26 (session 0025). Hybrid persistence: in-workspace files keep the committable `.threads/` sidecar byte-for-byte (INV-2); out-of-workspace `file:` + `untitled:` route to a global-storage sidecar keyed by `sha256(uri)` (INV-19/24/25). 150 unit + 41 host-E2E tests green. Non-shippable (no marketplace publish) — done = merge + tests green.
Sign in to join this conversation.