F12: inline editable proposed-change diff in the Markdown editor + Accept/Reject control parity (#64) #66

Merged
benstull merged 12 commits from s58-inline-editor-diff into main 2026-06-26 15:28:15 +00:00
11 changed files with 1982 additions and 169 deletions
Showing only changes of commit 1df3d2c154 - Show all commits
File diff suppressed because it is too large Load Diff
-28
View File
@@ -64,16 +64,6 @@
"title": "Ask Claude to Edit",
"category": "Cowriting"
},
{
"command": "cowriting.askClaude.submit",
"title": "Ask Claude to Edit",
"category": "Cowriting"
},
{
"command": "cowriting.askClaude.cancel",
"title": "Cancel",
"category": "Cowriting"
},
{
"command": "cowriting.editSelection",
"title": "Ask Claude to Edit Selection",
@@ -148,14 +138,6 @@
"command": "cowriting.rejectProposal",
"when": "false"
},
{
"command": "cowriting.askClaude.submit",
"when": "false"
},
{
"command": "cowriting.askClaude.cancel",
"when": "false"
},
{
"command": "cowriting.pinDiffBaseline",
"when": "editorLangId == markdown"
@@ -232,11 +214,6 @@
"command": "cowriting.reply",
"group": "inline",
"when": "commentController == cowriting.threads"
},
{
"command": "cowriting.askClaude.submit",
"group": "inline",
"when": "commentController == cowriting.askClaude"
}
],
"comments/commentThread/title": [
@@ -249,11 +226,6 @@
"command": "cowriting.reopenThread",
"group": "inline",
"when": "commentController == cowriting.threads && commentThread =~ /^resolved$/"
},
{
"command": "cowriting.askClaude.cancel",
"group": "inline",
"when": "commentController == cowriting.askClaude"
}
]
},
@@ -0,0 +1,111 @@
# Session 0057.0 — Transcript
> App: vscode-cowriting-plugin
> Start: 2026-06-26T04-35 (PST)
> End: 2026-06-26T06-55 (PST)
> Type: executing-plans
> Posture: yolo
> Claude-Session: 3d66a467-8026-472d-9693-52a37939d493
> Goal: Operator-feedback UX fixes on the Ask-Claude-to-Edit experience (broken
> keybinding, missing/duplicated shortcuts, edit-box placement) — then iterate the
> edit-input UX to its final form.
> Outcome: Shipped two PRs to vscode-cowriting-plugin main (#62, #65); fixed and
> shipped a dev-plugin bug (#134 / PR #135, v0.60.0) surfaced mid-session.
## Plan
Operator opened with three asks on the Ask-Claude UX: the live/review-panel
shortcut didn't work, there was no shortcut for "Ask Claude to Edit
Selection/Document" (wanted one combined shortcut), and the edit box appeared in
a disliked spot (near the command palette) — with a request to recommend a
VS-Code-idiomatic placement. Routed as an executing-plans (yolo) session.
The session became an extended live iterate-and-verify loop on the edit-input UX,
driven by the operator testing each build in an Extension Development Host.
## Pre-session state
- On branch `s60-live-progress`; `#60` (live turn progress) work in flight.
- A **concurrent session (0056)** was live in the SAME working checkout, actively
committing/stashing `#60` — which clobbered this session's uncommitted edits
twice before isolation.
## Turn-by-turn arc
1. **Phase A (keybindings + command unify).** Added one `cowriting.edit` command
that routes selection-vs-document at runtime via a pure `routeEdit` helper
(unit-tested); hid the two underlying commands from the palette; collapsed the
split menus to one entry. Added `⌘⌥E`/`Ctrl+Alt+E` (edit) and `mac` variants
for `⌘⌥R`/`Ctrl+Alt+R` (review panel) — the missing `mac` variant was why the
panel shortcut "didn't work" (Option-key combos are unreliable on macOS).
2. **Concurrent-checkout incident + recovery.** Discovered session 0056 was
clobbering the shared tree (git stash/pop/commit out from under this session).
Exported the edits as a durable patch, created an **isolated git worktree**, and
moved all work there. (§5.4 lesson — should have isolated up front.)
3. **Input-box placement (design Q).** Recommended + implemented an inline input
via the Comments API; then iterated heavily per operator feedback:
inline-at-selection → top-of-document → **finally a multi-line webview in a
split pane below the document for BOTH scopes**. Each iteration was built and
verified live in the EDH.
4. **Dev-plugin bug #134 (the root cause of the clobber).** Filed plugin feedback
#134 (two sessions can share one working tree — isolation was prose, not a
deterministic guard), then implemented the fix: `claim-session-id.sh` now stamps
`> Checkout:` and **refuses a claim when another live session occupies the same
checkout** (self-session excepted; `WGL_ALLOW_SHARED_CHECKOUT=1` override).
Shipped as PR #135 / v0.60.0 and made live in the running install.
5. **Final edit-input form (PR #65).** Unified both scopes on the split-below
multi-line webview (`editInstructionInput.ts` / `promptEditInstruction`):
auto-focus, ⌘↵ send, Esc cancel-with-confirm-if-text, split collapses + focus
restored to the doc on close (no Output/Debug panel popping), selection stays
highlighted above. **Deleted the entire inline-comment mechanism** (`inlineAsk.ts`,
`cowriting.askClaude.submit/cancel`, their menus, the Escape keybinding) — net
deletion. Verified interactively, merged.
## Cut state (end of session)
| Repo | Change | Ref |
| --- | --- | --- |
| vscode-cowriting-plugin | Phase A: unify `cowriting.edit` + `routeEdit` + mac keybindings + inline Comments-API input | PR #62 → main (squash `9432300`) |
| vscode-cowriting-plugin | Final: split-below multi-line webview for both scopes; remove inline-comment mechanism | PR #65 → main (squash `7e42d11`) |
| wiggleverse-dev-claude-plugin | Deterministic shared-checkout guard in `claim-session-id.sh` + Step 6b docs; v0.60.0 | PR #135 → main (merge `ca383c4`); installed live |
| wiggleverse-dev-claude-plugin (tracker) | Filed + closed feedback #134 (shared-checkout hazard) | issue #134 `resolution:done` |
- All worktrees (`s60-edit-ux`, `edit-top-anchor`, dev-plugin `issue134`) removed;
branches deleted; all temporary EDH windows closed.
- 242 unit + typecheck (src + e2e) + build green on the final tip. E2E electron
suite not run (known env-broken locally); webview UX verified interactively.
- Local `main` is 3 behind `origin/main` (sessions 0058/0059 + PR #65 merge) —
cosmetic; a future session pulls.
- Untracked strays `docs/superpowers/plans/2026-06-26-live-turn-progress.md` and
`specs/` predate this session (from the `#60` line / a known `specs/` stray);
left untouched — not this session's to land.
## Deferred decisions
None logged. No silent low-confidence calls — every judgment call (worktree
recovery, each UX fork, cancel/placement/size trade-offs) was surfaced live and
decided by the operator or flagged as a hard VS Code API limit.
## What lands on the operator's plate
- Nothing blocking. The edit-input UX line is complete and shipped.
- The dev-plugin shared-checkout guard (#134/v0.60.0) is **live for new
sessions** (this session's running install was updated; new sessions get it on
start).
- Known VS Code API limits captured in memory ([[ask-claude-edit-input-ux]]): no
multi-line input at the command-palette location; no API to set an editor split
ratio; no API to read bottom-panel visibility.
## Prompt the operator can paste into the next session
This session's thread (Ask-Claude edit UX) is **complete** — there is no single
forced next step. Open backlog items the operator may choose from (each its own
session): `#32` scroll-sync (feature, needs design), `#35` repo rename, `#40` undo
provenance (P3), F11 spec graduation (OQ-2), content-repo draft reconciliation.
No `Next /goal:` is recorded — the next move is an operator pick from the backlog
above.
@@ -1,22 +0,0 @@
# Session 0057.0 — Transcript
> App: vscode-cowriting-plugin
> Start: 2026-06-26T04-35 (PST)
> Type: executing-plans
> Posture: yolo
> Claude-Session: 3d66a467-8026-472d-9693-52a37939d493
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
>
> This file reserves session ID 0057 for vscode-cowriting-plugin. The driver replaces this
> body with the full transcript and renames the file to its final
> SESSION-0057.0-TRANSCRIPT-2026-06-26T04-35--<end>.md form at session end.
## Launch prompt
_(launch prompt not captured at claim time)_
## Deferred decisions
_Autonomous-mode low-confidence calls the driver made and would have
liked operator input on. Appended as the session runs; surfaced at
finalize. Empty if none._
+445
View File
@@ -0,0 +1,445 @@
---
status: graduated
---
# Solution Design: Inline editable proposed-change diff in the Markdown editor (#64)
| | |
| --- | --- |
| **Author(s)** | Ben Stull (with Claude) |
| **Reviewers / approvers** | Ben Stull |
| **Status** | `graduated` |
| **Version** | v0.1.1 |
| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#64` (Inline editable proposed-change diff in the Markdown editor + Accept/Reject controls in both surfaces, `type/feature`, `priority/P2`) · Brainstorming session `vscode-cowriting-plugin-0058` (2026-06-26) · Builds on (all shipped): F4 `#12` (propose/accept seam, INV-9/10/11/13), F6 `#17` (baseline / machine-landing), F7 `#21` (rendered preview, pure render engine INV-22), F10 `#29` (interactive review preview — the **clean-editor** decision INV-32; ✓/✗ accept-reject), F11 `#43` (preview toolbar, `data-src` block→source mapping INV-36), document-edit-flow `#42/#47/#46` (block proposals INV-39/40, accept-all INV-42) · Parent specs (graduated): `coauthoring-propose-accept.md`, `coauthoring-diff-view.md`, `coauthoring-rendered-preview.md`, `coauthoring-interactive-review.md`, `coauthoring-document-edit-flow.md` · Lineage: `ben.stull/rfc-app#48` |
**Change log**
| Date | Version | Change | By |
| --- | --- | --- | --- |
| 2026-06-26 | v0.1.1 | Implementation refinement (planning-and-executing session 0059): made the **re-anchor** explicit. Optimistic apply (§3.2) must store the pre-apply text on a new `Proposal.original` field **and** re-fingerprint the proposal's anchor to the *applied* text — F4's `fp.text` is the *original* target, which leaves the buffer once the replacement lands, so `resolve()` would orphan every applied proposal otherwise. `finalizeInPlace`/`revertInPlace`/decorate all key off the re-anchored fp; revert restores `original`. Reload-safety (INV-51/54): a proposal already carrying `original` is never re-captured (the in-memory applied-set is empty after a window reload), so the revert target survives save+reload. Shipped #64 (PR, session 0059). |
| 2026-06-26 | v0.1.0 | Initial draft — brainstorming session 0058. Four forks locked with the operator: **(1) editor model** = *optimistic apply + decorations* — on propose, the editor buffer becomes the would-be-accepted text (insertions real & editable & tinted; deletions shown as struck-red non-editable hints); accept = finalize-in-place, reject = revert. **(2) timing** = *on proposal* (turn complete), not a live token-stream into the editor (that stays in #60's notification/OutputChannel). **(3) editor affordance** = CodeLens `Accept ▾ / Reject ▾` above each block, `▾` → QuickPick (this / all); the webview keeps HTML buttons with the same dropdown. **(4) controls parity** = Accept / Reject / Accept-all / Reject-all reachable from **both** the editor and the webview. Two sub-decisions confirmed: a **dedicated `EditorProposalController`** owns optimistic-apply + decorations + CodeLens (keeps `ProposalController` the pure F4 state/seam owner); **saving while pending persists** the proposed (accepted-result) text. Reverses INV-32 and INV-10; supersedes the ✓/✗ glyph controls. | Ben Stull + Claude |
---
## 1. Business Context
### 1.1 Executive Summary
Today, when Claude proposes an edit, the change is shown **only in the rendered
review webview** (`<del>old</del><ins>new</ins>` with ✓/✗ controls); the Markdown
**editor stays deliberately clean** (F10 / INV-32 — "the preview is the single
review surface"). The writer who lives in the editor sees nothing: to review what
Claude proposed they must open the preview panel, and they cannot *edit* the
proposed text in place — they can only accept or reject it wholesale in the
webview.
This design brings the proposed change **into the Markdown editor itself**:
editable, with a track-changes diff that matches the webview and the post-accept
result **exactly**. It also replaces the per-proposal `✓`/`✗` glyphs with labelled
**Accept** / **Reject** controls — each carrying a `▾` dropdown for **Accept all**
/ **Reject all** — and makes all four actions reachable from **both** the editor
and the webview. The writer can now read, tweak, and resolve a proposal without
leaving the document.
### 1.2 Background
The plugin's review model (graduated specs `coauthoring-propose-accept.md`,
`coauthoring-rendered-preview.md`, `coauthoring-interactive-review.md`):
- A machine edit becomes an **F4 proposal** — a pending-only record
(`Proposal{ id, anchorId, replacement, granularity }` + a `Fingerprint` anchor
whose `text` is the exact target) that **never mutates the document** (INV-10),
re-resolved against the live text at accept (INV-11) and cleared when
accepted/rejected (INV-13).
- The **render engine** (`trackChangesModel.ts`) is a pure, deterministic,
`vscode`-free unit (INV-22): it diffs at **block** granularity (INV-39), word-
merges prose blocks, and emits webview HTML — `renderReview` (annotated) /
`renderPlain` (clean) — with proposal blocks (`proposalBlockHtml`) carrying the
✓/✗ buttons and `data-src` offsets for selection mapping (INV-36).
- The **F10 decision (INV-32)** removed *all* in-editor decorations
(`grep` confirms zero `TextEditorDecorationType` / `setDecorations` in the tree)
so the webview is the sole review surface; `ProposalController` carries the note
*"no in-editor UI — INV-32 makes the rendered preview the single review surface."*
- The sealed webview is **intent-only** (INV-35): it posts `{accept|reject|
acceptAll|…}` to the host, which routes to `ProposalController.acceptById` /
`rejectById` / `acceptAllProposals` (#46 / INV-42). Accept **applies** the
replacement via the F4 seam (`applyAgentEdit`), which advances the F6 baseline
(machine-landing, INV-18).
This feature **deliberately reverses two of those decisions** for the editor
surface, and supersedes the ✓/✗ glyph controls in both surfaces.
### 1.3 Who feels it & why
The **human coauthor working in the editor** — the writer who asked Claude to edit
and wants to see, adjust, and resolve the result without context-switching to the
preview panel. The pain: the proposed change is invisible in the place they are
actually writing, and it is not editable at all (only accept/reject-able). P2
(issue #64): it materially deepens the inner-loop "edit-in-place" experience and
unifies the review controls, but the webview review path already works, so it is
an enhancement rather than a gap.
---
## 2. Product Design
### 2.1 The experience
When Claude's turn lands a proposal (or, for a document edit, N block proposals),
the **editor** shows the change as track-changes, in place:
```
Accept ▾ Reject ▾ ← CodeLens, above each proposed block
The quick ~~brown~~ red fox jumps over the lazy dog.
└ struck red ┘└ green/blue, editable ┘
```
- **Insertions** are **real, editable buffer text**, tinted by origin
(green = human-origin, blue = LLM) — the writer can click in and edit them like
any other text.
- **Deletions** appear as a **struck-red, non-editable hint** rendered adjacent to
the insertion (a decoration, not buffer text) — visually mirroring the webview's
`<del>…</del>`.
- The buffer content **is exactly the would-be-accepted result**, so what the
writer sees (and can edit) is precisely what accepting produces.
Above each proposed block sit two **CodeLens** actions — **`Accept ▾`** and
**`Reject ▾`**. Clicking opens a small QuickPick:
```
Accept ▾ → ┌───────────────────────┐ Reject ▾ → ┌───────────────────────┐
│ Accept this proposal │ │ Reject this proposal │
│ Accept ALL proposals │ │ Reject ALL proposals │
└───────────────────────┘ └───────────────────────┘
```
The **webview** shows the same proposal with the same diff, and its controls are
the parallel HTML buttons — **`Accept ▾`** / **`Reject ▾`** — where the `▾`
reveals *Accept all* / *Reject all*. The legacy ✓/✗ glyphs are **replaced** by
these labelled controls in both surfaces.
All four actions — **Accept**, **Reject**, **Accept all**, **Reject all** — are
reachable from **either** surface and route to the same controller logic
(INV-53).
### 2.2 Accept / Reject semantics (editable-in-place)
- **Accept** finalizes the change *that is already in the buffer*: it records the
span's attribution as landed, advances the F6 baseline (machine-landing), and
clears the proposal. It does **not** re-apply text (the text is already there) —
see §3.3.
- **Reject** reverts that block's region back to the stored original
(`Fingerprint.text`), and clears the proposal.
- **If the writer edited the inserted text before deciding:** **Accept** keeps
their edited text (their keystrokes layer on as human authorship over the
proposed span); **Reject** still reverts the whole block to the original.
- **Accept all / Reject all** operate on the current document's pending proposals,
in descending anchor order, skipping orphans (the #46 / INV-42 shape; **Reject
all is new**).
### 2.3 Save semantics
Because the proposed text is *in the buffer*, **saving while a proposal is pending
persists the proposed (accepted-result) text** to disk. The deletion hints are
decorations (never buffer text), so the **saved file is clean** — it contains the
accepted-result text, not yet "finalized" only in the sense of attribution /
baseline. "Pending" therefore means *provisional attribution, baseline not yet
advanced* — **not** "document unchanged" (INV-54). We deliberately **allow** the
save rather than block it or strip on save; once the editor shows the live
document, blocking saves would be the surprising behavior.
### 2.4 What this is *not* (non-goals)
- **Not live token-streaming into the editor.** The diff appears when the turn
produces a proposal; watching Claude type into the document is out of scope (the
live token stream is #60's notification + OutputChannel).
- **Not a new review *model*.** F4 proposals, anchoring (INV-11), block
granularity (INV-39/40), accept-all (INV-42) are reused; this adds an editor
*surface* and unifies the controls.
- **Not non-Markdown.** Same Markdown-gated scope as the rest of the review UI.
- **Not intra-diagram mermaid editing.** Atomic fences stay atomic (INV-23); a
mermaid block proposal is editable as its whole fence, not sub-diagram.
- **Not removing the webview.** The rendered preview remains a full review surface;
it gains label/dropdown parity, not a demotion.
### 2.5 Surfaces considered & rejected
| Editor approach | Why not |
| --- | --- |
| **Read-only decoration overlay** (doc unchanged; ghost insertions via decoration) | The inserted text would **not be editable** — fails the core "human-editable" requirement. |
| **Inline track-changes markup in the buffer** (`{~~old~~|++new++}`) | Pollutes the file on disk with markup until resolved; complex; conflicts with clean save. |
| **Native `vscode.diff` two-pane** | The two-pane diff was *removed* in #34 (F10) precisely to make one review surface; a separate diff editor is not "in the Markdown file." |
Chosen: **optimistic apply + decorations** — the only model that yields an
editable diff that is *exactly* the accepted result, in the document itself.
---
## 3. Engineering Design
### 3.1 Architecture
```
┌──────────────────────────────────────────────┐
one F4 turn ───▶ │ ProposalController (F4 state/seam owner) │
(#12 propose) │ propose() · finalizeInPlace() · revertInPlace│
│ · rejectAll() · onDidChangeProposals │
└───────────────┬──────────────────────────────┘
│ proposals (pending records + anchors)
┌─────────────────────┴───────────────────────┐
▼ ▼
┌───────────────────────────┐ ┌────────────────────────────┐
│ EditorProposalController │ shared │ TrackChangesPreview- │
│ (NEW, vscode host) │ pure diff │ Controller (webview) │
│ · optimistic buffer write │◀──────────────▶ │ · renderReview HTML │
│ · insertion tint deco │ trackChangesModel│ · Accept▾/Reject▾ buttons │
│ · deletion-hint deco │ (diff → plan) │ · dropdown → acceptAll/ │
│ · CodeLens Accept▾/Reject▾│ │ rejectAll │
└───────────────────────────┘ └────────────────────────────┘
▲ ▲
└──────────── both route actions to ───────────┘
ProposalController.{finalizeInPlace,revertInPlace,
acceptAll,rejectAll}
```
The decisive rule: **one pure diff is the single source of truth** for both
surfaces (INV-49). The editor renders it as *buffer text + decorations + CodeLens*;
the webview renders it as *HTML*. They cannot diverge because they consume the
same hunks.
### 3.2 Optimistic apply (on propose) — INV-48
`propose()` gains a buffer-write step, owned by `EditorProposalController` (so
`ProposalController` stays the pure state owner):
1. Record the pending proposal(s) + anchor(s) **exactly as today** (the sidecar
record is unchanged: `replacement`, `Fingerprint{text,before,after,lineHint}`,
`granularity`).
2. Write the proposed text into the **live editor buffer**: replace the resolved
anchor span with `replacement` (for a document turn, this makes the buffer
Claude's full proposed document; for a selection turn, the single span). This
write **does not advance the F6 baseline** — the baseline stays at the
pre-proposal text, so the change reads as *pending*, not *landed*.
3. Compute the **decoration plan** from the same per-block word/line diff used by
`acceptBlock` (`wordEditHunks(fp.text, replacement)`): insertion segments →
tinted ranges (buffer coords, offset by the live anchor); deletion segments →
struck-red hint injections (decoration `before`/`after` content) at the segment
boundary.
Critically, the optimistic write must **not** fire the F4 seam's
`onDidApplyAgentEdit` (which would advance the baseline / register a landed edit).
Optimistic apply is a *distinct* buffer-mutation path from accept; only **accept**
advances the baseline (§3.3). This is the load-bearing distinction that keeps the
change *pending* while *present*.
### 3.3 Accept = finalize-in-place; Reject = revert-in-place — INV-51
Because the proposed text is already in the buffer, the old "accept ⇒ apply via
seam" path would **double-apply**. The accept/reject paths are reworked:
- **`finalizeInPlace(id)`** — resolve the proposal's anchor against the live text;
record the span's attribution as **landed** (provenance from the proposal's
`author`, word-precise per INV-40 for block granularity); **advance the F6
baseline** for that region (the machine-landing the seam used to do on apply);
`removeProposal(id)`. No text is re-applied. If the writer edited the inserted
span, the *current* buffer text is what gets finalized (their edits become
layered human authorship).
- **`revertInPlace(id)`** — resolve the proposal's live span; replace it with the
stored original `Fingerprint.text`; `removeProposal(id)`. Reverts the whole
block regardless of any in-place edits.
- **`acceptAll()` / `rejectAll()`** — iterate the current document's pending
proposals in **descending** anchor order (so earlier resolutions don't shift
later offsets), skipping orphans with a tally (INV-42 shape). `acceptAll` reuses
the #46 batch ordering; **`rejectAll` is new** and symmetric.
Both surfaces (editor CodeLens/QuickPick and webview buttons/dropdown) call these
**same four methods** (INV-53). The webview's existing intent messages are renamed/
extended (`accept`/`reject`/`acceptAll` + new `rejectAll`) but stay intent-only
(INV-35).
### 3.4 Single diff source & render-once — INV-49 / INV-50
- **INV-49 (single diff source).** The per-proposal diff (block hunks → word/line
edit hunks) is produced by one pure function in `trackChangesModel.ts` (extends
the existing `diffToBlockHunks` / `wordEditHunks`, INV-39/40). The webview HTML
path and the editor decoration-plan path both consume it. Same proposal ⇒
identical diff in both surfaces (this is the testable parity guarantee).
- **INV-50 (render once).** With optimistic apply, the baseline→buffer delta now
*contains* the proposed change. The renderer must attribute any baseline→buffer
delta that is covered by a **pending proposal** to that proposal (rendered as a
proposal block), and must **not** also render it as an already-landed
machine-diff. Mechanism: the webview render reconciles baseline-diff spans
against pending-proposal anchors; a span covered by a pending proposal is
rendered exactly once, as the proposal. *(This reconciliation is the primary
implementation risk; the implementation plan owns the exact span-mapping, but the
invariant — rendered once — is fixed here.)*
### 3.5 Editor decorations & CodeLens (the editor surface)
`EditorProposalController` (new, `vscode` host module) owns:
- **Two `TextEditorDecorationType`s** — an *insertion tint* (green/blue background,
by provenance) over real buffer ranges, and a *deletion hint* (a
`before`/`after` render-option carrying the struck original text, red,
strikethrough, non-editable). Both are recomputed from the decoration plan on
every `onDidChangeProposals` and on active-editor change.
- **A `CodeLensProvider`** — for each pending proposal whose anchor resolves in the
active document, emit two lenses positioned above the block: `Accept ▾`
(command `cowriting.proposalAcceptMenu` with the proposal id) and `Reject ▾`
(`cowriting.proposalRejectMenu`). Each command opens a `vscode.window
.showQuickPick(["… this proposal","… ALL proposals"])` and dispatches to
`finalizeInPlace`/`acceptAll` or `revertInPlace`/`rejectAll`.
- **Lifecycle wiring** — subscribes to `proposals.onDidChangeProposals` (the same
event the preview uses) and `window.onDidChangeActiveTextEditor`; clears its
decorations + lenses for documents with no pending proposals (so a clean editor
stays clean — INV-32's spirit holds *when there is nothing pending*).
Deletion hints are **decoration-only** (never buffer text, never saved) and
insertion tints decorate **real** buffer text (INV-52).
### 3.6 Webview controls (label + dropdown parity)
`proposalBlockHtml` (`trackChangesModel.ts`) swaps the ✓/✗ glyph buttons for an
**`Accept ▾`** / **`Reject ▾`** control pair. The `▾` opens a small CSS/JS dropdown
in the sealed webview (`preview.ts`/`preview.css`, no network — INV-21) offering
*Accept all* / *Reject all*; selecting posts the corresponding intent
(`acceptAll` / `rejectAll`) or the per-proposal `accept`/`reject`. The host routes
all four to the controller methods of §3.3 (INV-35 preserved).
### 3.7 Components & files
| File | Change |
| --- | --- |
| `src/editorProposalController.ts` | **NEW** — optimistic apply, decoration types, deletion-hint injection, `CodeLensProvider`, QuickPick menus; subscribes to `onDidChangeProposals` + active-editor change. |
| `src/proposalController.ts` | Add `finalizeInPlace(id)`, `revertInPlace(id)`, `rejectAll(docKey)`; `propose()` delegates the optimistic buffer write (so the controller stays state-pure); accept/reject routing now finalizes/reverts in place (no seam re-apply). |
| `src/trackChangesModel.ts` | Factor the shared per-proposal diff into one pure producer feeding both surfaces; reconcile pending-proposal spans in `renderReview` (INV-50); swap ✓/✗ → `Accept ▾`/`Reject ▾` in `proposalBlockHtml`. |
| `src/trackChangesPreview.ts` | Route new `rejectAll` intent; keep `accept`/`reject`/`acceptAll`; relabel toolbar/proposal controls. |
| `src/preview.ts` / `preview.css` | Dropdown control for `Accept ▾`/`Reject ▾`; same diff CSS reused. |
| `src/extension.ts` | Register `EditorProposalController`, its CodeLens provider, and the menu commands; wire it alongside `ProposalController`/`TrackChangesPreviewController`. |
| `package.json` | Add `cowriting.rejectAllProposals`, `cowriting.proposalAcceptMenu`, `cowriting.proposalRejectMenu`; CodeLens contribution; markdown-gated `when` clauses. |
### 3.8 Invariants
- **INV-48 — Optimistic apply (with re-anchor).** On propose, the proposed text is
written into the live editor buffer (the buffer becomes the would-be-accepted
result); this write does **not** advance the F6 baseline and does **not** fire the
F4 machine-landing seam. Because F4's `fp.text` is the *original* target — which
leaves the buffer once the replacement lands — optimistic apply **stores the
pre-apply text on `Proposal.original` and re-fingerprints the anchor to the applied
text**, so `resolve()` keeps finding the proposal in the mutated buffer; finalize /
revert / decorate all key off the re-anchored fp, and revert restores `original`.
`original` is captured **exactly once** (first apply): a proposal that survives a
save+reload already carries it, so a fresh session never re-captures it from the
already-applied buffer (reload-safety, INV-51/54). **Reverses INV-10** (propose no
longer leaves the document untouched) and **INV-32** (the editor is no longer kept
unconditionally clean — it shows pending proposals).
- **INV-49 — Single diff source.** Editor decorations and webview HTML both derive
from one pure, deterministic per-proposal diff (extends `diffToBlockHunks` /
`wordEditHunks`, INV-22/39/40). The same proposal yields the same diff in both
surfaces.
- **INV-50 — Rendered once.** A span covered by a pending proposal is rendered
exactly once — as that proposal — never additionally as an already-landed
baseline diff.
- **INV-51 — Finalize / revert in place.** Accept finalizes the span already in the
buffer (record attribution, advance baseline, clear proposal) without
re-applying text; Reject reverts the block region to the stored
`Fingerprint.text`. No double-apply.
- **INV-52 — Decoration roles.** Deletion hints are decoration-only (non-editable
`before`/`after` content, never buffer text, never saved); insertion tints
decorate real, editable buffer text.
- **INV-53 — Control parity.** Accept / Reject / Accept-all / Reject-all are
reachable from **both** the editor (CodeLens + QuickPick) and the webview
(buttons + dropdown) and route to the same `ProposalController` methods; the ✓/✗
glyph controls are superseded by labelled `Accept ▾`/`Reject ▾`.
- **INV-54 — Save persists pending.** Saving while a proposal is pending persists
the proposed (accepted-result) text; "pending" denotes provisional attribution /
un-advanced baseline, not document divergence. (Decorations are not persisted, so
the saved file is clean text.)
### 3.9 Error & edge handling
- **Orphaned proposal** (anchor no longer resolves) — no editor decorations/lenses
for it; it still surfaces in the webview at end with a dashed border (INV-34);
accept-all/reject-all skip it with a tally (INV-42).
- **Writer edits the inserted span, then accepts** — current buffer text is
finalized (their edits layer as human authorship); **then rejects** — block
reverts to original regardless.
- **External / concurrent edit shifts geometry** — anchors re-resolve on
`onDidChangeProposals` (existing resolve-or-flag); decoration plan + lenses
recompute.
- **Multiple proposals from one turn** — all optimistically applied (buffer = full
proposed document); each block independently finalizable/revertible; descending
order on accept-all/reject-all keeps offsets valid.
- **Reject of an in-buffer span after partial accept of siblings** — each
proposal's revert uses its own live-resolved span, independent of siblings.
- **Non-authorable document** (read-only / not on disk) — controls disabled exactly
as today (`authorable` gating).
---
## 4. Testing & E2E
Per the §9 pipeline and `coauthoring-*` precedent (this is a VS Code extension —
**no flotilla/PPE**; the gate is unit + host-E2E green).
- **Unit (the core):**
- The shared per-proposal diff producer: same hunks → insertion/deletion
segments; parity assertion that the decoration plan and the webview HTML derive
identical add/del spans for the same proposal (INV-49).
- `renderReview` render-once reconciliation: a pending-proposal span is emitted
as a proposal block and **not** as a landed baseline diff (INV-50).
- Decoration-plan computation from `wordEditHunks(fp.text, replacement)`:
insertion ranges in buffer coords; deletion-hint positions (INV-52).
- **Host E2E:**
- Propose (via the injectable `editTurn` stub) → editor shows insertion tint +
deletion hint + `Accept ▾`/`Reject ▾` CodeLens; buffer equals the accepted
result (INV-48).
- Edit the inserted text → **finalizeInPlace** keeps the edited text; baseline
advanced; proposal cleared (INV-51).
- **revertInPlace** restores `Fingerprint.text`; proposal cleared.
- Accept / Reject / Accept-all / Reject-all from **both** the editor command path
and the webview intent path produce the same end state (INV-53); `rejectAll`
clears all pending and reverts all blocks.
- Save while a proposal is pending → file on disk has the proposed text; no
decoration markup in the saved bytes (INV-54).
- Webview/editor parity: same proposal renders the same diff in both (INV-49).
- **Manual smoke:** ask Claude to edit a paragraph; confirm the editor diff is
editable, the deletion hint reads correctly, both surfaces' controls resolve it,
and a clean editor returns once nothing is pending.
---
## 5. Delivery Plan (rollout — not a task list)
One increment, single design-then-build (#64). Ships through branch → PR → `main`;
no migration and no new persisted sidecar shape (the `Proposal` record is
unchanged). The implementation plan (downstream `wgl-planning-and-executing`
session) owns the Task breakdown; a natural cut:
1. **Shared diff + render-once** — factor the pure per-proposal diff producer;
reconcile pending spans in `renderReview` (INV-49/50) + unit tests.
2. **Accept/reject rework**`finalizeInPlace` / `revertInPlace` / `rejectAll` on
`ProposalController` (no seam re-apply); unit tests.
3. **Editor surface**`EditorProposalController`: optimistic apply, decorations,
deletion hints, CodeLens + QuickPick menus; wire in `extension.ts`.
4. **Control parity** — relabel ✓/✗ → `Accept ▾`/`Reject ▾` + dropdown in the
webview; route `rejectAll`; `package.json` commands/`when`.
5. **Host E2E + manual smoke** across both surfaces.
Reversible by reverting the PR (restores the clean-editor / ✓-✗ webview behavior).
---
## 6. Open Questions
- **OQ-1 — Provenance tint exactness.** Insertion tint uses green=human-origin /
blue=LLM by the proposal `author`; whether to additionally word-tint *within* a
block by the F3 attribution of edited-in-place text (vs a single block tint) can
follow the existing F9/F10 coloring; v1 tints the proposed insertion by the
proposal author. *(Not blocking.)*
- **OQ-2 (inherited)** — The F11 design (`#43`) remains un-graduated (lives in code
+ an issue draft); unrelated to #64 but noted in the shared lineage. Track
separately.
- **OQ-3 — Editor dropdown fidelity.** The editor uses a QuickPick to stand in for
the webview's true `▾` dropdown (CodeLens cannot render a caret menu inline); if
a more button-like affordance is wanted later, a webview-style overlay is a
possible increment. *(Accepted for v1.)*
+147
View File
@@ -0,0 +1,147 @@
import * as vscode from "vscode";
import { randomBytes } from "crypto";
/**
* The multi-line instruction input for "Ask Claude to Edit" BOTH the selection
* and the whole-document case. A small focused webview (a tall, resizable
* textarea + Send) opened in a split pane BELOW the document not a comment
* thread, and not the top QuickInput (which is single-line only; VS Code has no
* multi-line input at the command-palette location). For a selection edit the
* document stays in the pane above with the selection still highlighted (VS
* Code's inactive-selection style), so the user can see exactly what Claude will
* edit; the caller has already captured the selection, so we never touch it.
*
* The split collapses when the input is submitted or cancelled, and focus is
* handed back to the document so the collapsing split doesn't reveal the bottom
* panel. `header` names the scope ("Ask Claude to Edit This Selection" /
* "…This Document").
*
* The webview only collects text and posts it to the host no SDK or secret
* surface lives in it (INV-8/35); the sealed CSP allows no network. Because a
* webview CAN read its own textarea, Cancel / Escape confirms ONLY when there is
* text to lose (closing the tab is an explicit dismiss, no prompt). Resolves with
* the typed instruction, or `undefined` if cancelled / closed / left empty.
*/
export async function promptEditInstruction(header: string): Promise<string | undefined> {
// Remember the document editor we came from so we can hand focus back to it
// when the input closes — otherwise, when the empty split group below collapses,
// focus falls into the bottom panel (Output / Debug Console / …) and it pops
// open. Restoring editor focus leaves whatever panel state the user had untouched.
const source = vscode.window.activeTextEditor;
// Open a split editor group BELOW the current one and host the input there, so
// it sits under the document instead of covering it as a tab. The new group is
// empty, so disposing the panel on submit/cancel leaves it empty and VS Code
// collapses the split (the `workbench.editor.closeEmptyGroups` default). Falls
// back to a tab in the active group if the split command is unavailable.
try {
await vscode.commands.executeCommand("workbench.action.newGroupBelow");
} catch {
/* no split — the panel opens as a tab in the active group instead */
}
return new Promise((resolve) => {
const panel = vscode.window.createWebviewPanel(
"cowriting.askClaudeInput",
header,
{ viewColumn: vscode.ViewColumn.Active, preserveFocus: false },
{ enableScripts: true, retainContextWhenHidden: false },
);
let settled = false;
const done = (value: string | undefined): void => {
if (settled) return;
settled = true;
resolve(value);
panel.dispose();
// Hand focus back to the originating document so the collapsing split
// doesn't leave focus in (and reveal) the bottom panel.
if (source) {
void vscode.window.showTextDocument(source.document, {
viewColumn: source.viewColumn ?? vscode.ViewColumn.One,
preserveFocus: false,
});
}
};
panel.webview.onDidReceiveMessage((m: { type?: string; text?: string }) => {
const text = (m?.text ?? "").trim();
if (m?.type === "submit") {
done(text ? text : undefined);
} else if (m?.type === "cancel") {
// Confirm only if there's something to lose (we can read the textarea here).
if (!text) {
done(undefined);
return;
}
void vscode.window
.showWarningMessage("Discard your Ask-Claude instruction?", { modal: true }, "Discard")
.then((pick) => {
if (pick === "Discard") done(undefined);
// else: leave the panel open so the operator can keep editing.
});
}
});
// Closing the tab is an explicit dismiss — cancel without a prompt.
panel.onDidDispose(() => done(undefined));
panel.webview.html = htmlFor(header);
});
}
function escapeHtml(s: string): string {
return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!);
}
function htmlFor(header: string): string {
const nonce = randomBytes(16).toString("base64");
// Sealed CSP: no network; inline style only; the one script is nonce-gated.
const csp = `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';`;
const title = escapeHtml(header);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="${csp}" />
<title>${title}</title>
<style>
body { padding: 14px 16px; font-family: var(--vscode-font-family); color: var(--vscode-foreground); }
h2 { font-size: 13px; font-weight: 600; margin: 0 0 10px; }
textarea {
width: 100%; min-height: 160px; resize: vertical; box-sizing: border-box;
background: var(--vscode-input-background); color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border, transparent); border-radius: 4px; padding: 8px;
font-family: var(--vscode-editor-font-family); font-size: var(--vscode-editor-font-size); line-height: 1.4;
}
textarea::placeholder { color: var(--vscode-input-placeholderForeground); }
textarea:focus { outline: 1px solid var(--vscode-focusBorder); border-color: var(--vscode-focusBorder); }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; gap: 12px; }
.hint { color: var(--vscode-descriptionForeground); font-size: 12px; }
button {
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
border: none; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 13px;
}
button:hover { background: var(--vscode-button-hoverBackground); }
</style>
</head>
<body>
<h2>${title}</h2>
<textarea id="inst" placeholder="e.g. tighten the intro, add a conclusion, and fix the heading levels" autofocus></textarea>
<div class="row">
<span class="hint"> / Ctrl+ to send · Esc to cancel</span>
<button id="send">Send to Claude</button>
</div>
<script nonce="${nonce}">
const api = acquireVsCodeApi();
const ta = document.getElementById('inst');
const focus = () => ta.focus();
focus();
window.addEventListener('focus', focus);
const submit = () => api.postMessage({ type: 'submit', text: ta.value });
const cancel = () => api.postMessage({ type: 'cancel', text: ta.value });
document.getElementById('send').addEventListener('click', submit);
ta.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); submit(); }
else if (e.key === 'Escape') { e.preventDefault(); cancel(); }
});
</script>
</body>
</html>`;
}
+12 -15
View File
@@ -12,7 +12,6 @@ import { SidecarRouter } from "./sidecarRouter";
import { DiffViewController } from "./diffViewController";
import { TrackChangesPreviewController } from "./trackChangesPreview";
import { LiveProgressUi } from "./liveProgressUi";
import { InlineAskController } from "./inlineAsk";
import { EditorProposalController } from "./editorProposalController";
import { isAuthorable, routeEdit, selectionRejection } from "./workspacePath";
@@ -27,7 +26,6 @@ export interface CowritingApi {
trackChangesPreviewController: TrackChangesPreviewController;
sidecarRouter: SidecarRouter;
liveProgressUi: LiveProgressUi;
inlineAsk: InlineAskController;
editorProposalController: EditorProposalController;
}
@@ -40,11 +38,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
// OutputChannel) for both Ask-Claude entry points.
const liveProgressUi = new LiveProgressUi();
context.subscriptions.push(liveProgressUi);
// The inline "Ask Claude to Edit" prompt — rendered at the selection/cursor via
// the Comments API rather than the top-center QuickInput (see inlineAsk.ts).
// Shared by both Ask-Claude entry points (editSelection + the preview's askClaude).
const inlineAsk = new InlineAskController(context.subscriptions);
context.subscriptions.push(
vscode.commands.registerCommand("cowriting.showClineSdkInfo", async () => {
try {
@@ -121,7 +114,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
attributionController,
proposalController,
liveProgressUi,
inlineAsk,
);
context.subscriptions.push(trackChangesPreviewController);
@@ -246,17 +238,23 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
if (!editor) return; // unreachable once reason is null, but narrows the type
const document = editor.document;
const selection = editor.selection; // non-empty (selectionRejection guaranteed it)
// The instruction prompt opens INLINE at the selection (inlineAsk), not the
// top-center QuickInput — anchored to the text Claude will edit.
const instruction = await inlineAsk.prompt(document.uri, selection);
if (!instruction) return;
// Capture the selection text + anchor BEFORE prompting — the inline prompt
// moves the cursor to the document top (where its box anchors), which would
// otherwise collapse the selection we act on (spec §6.5 PUC-1: anchor
// captured pre-turn so mid-turn edits can't skew it).
const selectedText = document.getText(selection);
// Capture the anchor BEFORE the turn (spec §6.5 PUC-1): mid-turn edits
// can't skew it — the proposal renders wherever the target re-resolves.
const fp = buildFingerprint(document.getText(), {
start: document.offsetAt(selection.start),
end: document.offsetAt(selection.end),
});
// The instruction prompt is the multi-line split-below webview box (shared
// with the document case, via the preview controller); the document above
// keeps the selection highlighted while it's open. selectedText/fp were
// captured above, so moving focus to the box doesn't affect what we edit.
const instruction = await trackChangesPreviewController.askEditInstruction(
"Ask Claude to Edit This Selection",
);
if (!instruction) return;
const turnId = `turn-${Date.now().toString(36)}`;
try {
await vscode.window.withProgress(
@@ -363,7 +361,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
trackChangesPreviewController,
sidecarRouter,
liveProgressUi,
inlineAsk,
editorProposalController,
};
}
-71
View File
@@ -1,71 +0,0 @@
import * as vscode from "vscode";
/** Trim the reply text; an empty/whitespace-only instruction is "no instruction". */
export function normalizeInstruction(text: string | undefined): string | undefined {
const t = text?.trim();
return t ? t : undefined;
}
/**
* The "Ask Claude to Edit" prompt, rendered INLINE at the selection (or cursor)
* instead of the top-center QuickInput. VS Code's native pattern for "ask AI to
* edit this" is Inline Chat an input anchored in the editor at the target. The
* stable-API equivalent for a third-party extension is the Comments API: a
* transient comment thread whose reply box renders right at the range. (The
* coauthoring THREADS feature already uses the Comments API via its own
* controller this is a SEPARATE, dedicated `cowriting.askClaude` controller so
* the two never collide.)
*
* One prompt is live at a time; opening a new one cancels the previous. The
* thread carries no comments it is purely the inline input and is disposed
* as soon as the user submits or cancels.
*/
export class InlineAskController {
private readonly controller: vscode.CommentController;
private pending?: { resolve: (v: string | undefined) => void; thread: vscode.CommentThread };
constructor(disposables: vscode.Disposable[]) {
this.controller = vscode.comments.createCommentController("cowriting.askClaude", "Ask Claude to Edit");
// The reply box's prompt + placeholder (Comments API options).
this.controller.options = {
prompt: "Ask Claude to Edit",
placeHolder: "e.g. tighten this paragraph",
};
disposables.push(
this.controller,
vscode.commands.registerCommand("cowriting.askClaude.submit", (r: vscode.CommentReply) =>
this.finish(normalizeInstruction(r?.text)),
),
vscode.commands.registerCommand("cowriting.askClaude.cancel", () => this.finish(undefined)),
// The controller itself disposes the live thread on extension teardown.
new vscode.Disposable(() => this.finish(undefined)),
);
}
/**
* Open the inline input anchored at `range` in `uri`'s editor and resolve with
* the typed instruction (or `undefined` if cancelled / left empty). Replaces
* any prompt already open.
*/
prompt(uri: vscode.Uri, range: vscode.Range): Promise<string | undefined> {
// A fresh prompt supersedes any prior one (resolve it as cancelled).
this.finish(undefined);
return new Promise<string | undefined>((resolve) => {
const thread = this.controller.createCommentThread(uri, range, []);
thread.label = "Ask Claude to Edit";
thread.canReply = true;
thread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
thread.contextValue = "askClaude";
this.pending = { resolve, thread };
});
}
/** Resolve the live prompt (if any) and tear its thread down. */
private finish(value: string | undefined): void {
const p = this.pending;
this.pending = undefined;
if (!p) return;
p.thread.dispose();
p.resolve(value);
}
}
+14 -24
View File
@@ -18,7 +18,7 @@ import { buildFingerprint } from "./anchorer";
import { isAuthorable } from "./workspacePath";
import type { EditTurnResult, RunEditTurnOptions } from "./liveTurn";
import type { LiveProgressUi } from "./liveProgressUi";
import type { InlineAskController } from "./inlineAsk";
import { promptEditInstruction } from "./editInstructionInput";
/**
* F11: a host edit turn (selection/document text + instruction rewrite).
@@ -64,6 +64,13 @@ export class TrackChangesPreviewController implements vscode.Disposable {
const { runEditTurn } = await import("./liveTurn");
return runEditTurn(instruction, text, opts);
};
/**
* The instruction prompt (the multi-line split-below webview box) for BOTH the
* selection and document cases. A field so host E2E can stub it the webview
* DOM can't run in CI (mirrors `editTurn`). Also used by the editor's
* `cowriting.editSelection` command (via this controller).
*/
askEditInstruction: (header: string) => Promise<string | undefined> = promptEditInstruction;
/** Monotonic per-session counter minting a stable turnId for each Ask-Claude gesture. */
private turnSeq = 0;
private nextTurnSeq(): number {
@@ -76,7 +83,6 @@ export class TrackChangesPreviewController implements vscode.Disposable {
private readonly attribution: AttributionController,
private readonly proposals: ProposalController,
private readonly liveProgressUi: LiveProgressUi,
private readonly inlineAsk: InlineAskController,
) {
this.disposables.push(
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
@@ -235,34 +241,18 @@ export class TrackChangesPreviewController implements vscode.Disposable {
}
}
/**
* Where the inline Ask-Claude input anchors: a range edit pins to its selection;
* a whole-document edit pins to the editor's cursor line if this document is the
* active editor, else to the document start.
*/
private editTargetAnchor(document: vscode.TextDocument, target: EditTarget): vscode.Range {
if (target.kind === "range") {
return new vscode.Range(document.positionAt(target.start), document.positionAt(target.end));
}
const active = vscode.window.activeTextEditor;
if (active && active.document.uri.toString() === document.uri.toString()) {
const line = active.selection.active.line;
return new vscode.Range(line, 0, line, 0);
}
return new vscode.Range(0, 0, 0, 0);
}
/**
* F11 (PUC-3/4): prompt host-side for the instruction (keeps the LLM/secret
* surface out of the sealed webview, INV-8/35), run the edit turn, and surface
* the result as F4 proposal(s). UI wrapper around `runEditAndPropose`.
*/
private async askClaude(document: vscode.TextDocument, target: EditTarget): Promise<void> {
// The instruction prompt opens INLINE (inlineAsk) at the target: the selected
// range for a range edit, or the editor's cursor line / the document start for
// a whole-document edit — never the top-center QuickInput.
const anchor = this.editTargetAnchor(document, target);
const instruction = await this.inlineAsk.prompt(document.uri, anchor);
// Both scopes use the same multi-line split-below webview box; only the header
// (and the downstream proposal logic) differs. For a selection the document
// above keeps the selection highlighted while the box is open.
const header =
target.kind === "document" ? "Ask Claude to Edit This Document" : "Ask Claude to Edit This Selection";
const instruction = await this.askEditInstruction(header);
if (!instruction) return;
try {
const ids = await vscode.window.withProgress(
@@ -30,8 +30,6 @@ suite("no-workspace authoring (F8 — real folder-less, #8 lineage)", () => {
"cowriting.reopenThread",
"cowriting.edit",
"cowriting.editSelection",
"cowriting.askClaude.submit",
"cowriting.askClaude.cancel",
"cowriting.applyAgentEdit",
"cowriting.proposeAgentEdit",
]) {
+7 -7
View File
@@ -76,9 +76,9 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
await vscode.window.showTextDocument(a.doc);
await settle();
// Stub the instruction prompt (sealed input box can't run in CI) + the LLM turn.
const origPrompt = api.inlineAsk.prompt;
(api.inlineAsk as any).prompt = async () => "rewrite it";
// Stub the document instruction prompt (the webview can't run in CI) + the LLM turn.
const origPrompt = ctl.askEditInstruction;
ctl.askEditInstruction = async () => "rewrite it";
ctl.setEditTurnForTest(async () => ({
replacement: "# Tab\n\nThe REWRITTEN tab paragraph.\n",
model: "sonnet",
@@ -88,7 +88,7 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
await vscode.commands.executeCommand("cowriting.editDocument", b.doc.uri);
await settle();
} finally {
(api.inlineAsk as any).prompt = origPrompt;
ctl.askEditInstruction = origPrompt;
}
// The proposal(s) landed on the TAB doc (B), and the ACTIVE doc (A) has none.
@@ -112,8 +112,8 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
await vscode.window.showTextDocument(a.doc);
await settle();
const origPrompt = api.inlineAsk.prompt;
(api.inlineAsk as any).prompt = async () => "rewrite it";
const origPrompt = ctl.askEditInstruction;
ctl.askEditInstruction = async () => "rewrite it";
ctl.setEditTurnForTest(async () => ({
replacement: "# No arg\n\nThe REWRITTEN active doc paragraph.\n",
model: "sonnet",
@@ -123,7 +123,7 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
await vscode.commands.executeCommand("cowriting.editDocument");
await settle();
} finally {
(api.inlineAsk as any).prompt = origPrompt;
ctl.askEditInstruction = origPrompt;
}
assert.ok(api.proposalController.listProposals(a.doc).length >= 1, "active doc received the proposal(s) on no-arg");
});