Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3ea6c65ca | |||
| 21bbf6b114 | |||
| 0743cf9a8a | |||
| 946625e899 | |||
| 20e13bba4d | |||
| d2405a2ca8 | |||
| 5d9f7ddaaf |
+15
-53
@@ -59,21 +59,6 @@
|
|||||||
"title": "Apply Agent Edit (internal seam)",
|
"title": "Apply Agent Edit (internal seam)",
|
||||||
"category": "Cowriting"
|
"category": "Cowriting"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"command": "cowriting.edit",
|
|
||||||
"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",
|
"command": "cowriting.editSelection",
|
||||||
"title": "Ask Claude to Edit Selection",
|
"title": "Ask Claude to Edit Selection",
|
||||||
@@ -133,29 +118,13 @@
|
|||||||
"command": "cowriting.rejectProposal",
|
"command": "cowriting.rejectProposal",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"command": "cowriting.askClaude.submit",
|
|
||||||
"when": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "cowriting.askClaude.cancel",
|
|
||||||
"when": "false"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"command": "cowriting.pinDiffBaseline",
|
"command": "cowriting.pinDiffBaseline",
|
||||||
"when": "editorLangId == markdown"
|
"when": "editorLangId == markdown"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"command": "cowriting.edit",
|
|
||||||
"when": "editorLangId == markdown"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "cowriting.editSelection",
|
|
||||||
"when": "false"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"command": "cowriting.editDocument",
|
"command": "cowriting.editDocument",
|
||||||
"when": "false"
|
"when": "editorLangId == markdown"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cowriting.acceptAllProposals",
|
"command": "cowriting.acceptAllProposals",
|
||||||
@@ -171,8 +140,13 @@
|
|||||||
],
|
],
|
||||||
"editor/title/context": [
|
"editor/title/context": [
|
||||||
{
|
{
|
||||||
"command": "cowriting.edit",
|
"command": "cowriting.editSelection",
|
||||||
"when": "resourceLangId == markdown",
|
"when": "editorHasSelection && resourceLangId == markdown",
|
||||||
|
"group": "1_cowriting@1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "cowriting.editDocument",
|
||||||
|
"when": "!editorHasSelection && resourceLangId == markdown",
|
||||||
"group": "1_cowriting@1"
|
"group": "1_cowriting@1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -190,8 +164,13 @@
|
|||||||
],
|
],
|
||||||
"editor/context": [
|
"editor/context": [
|
||||||
{
|
{
|
||||||
"command": "cowriting.edit",
|
"command": "cowriting.editSelection",
|
||||||
"when": "editorLangId == markdown && (resourceScheme == file || resourceScheme == untitled)",
|
"when": "editorHasSelection && editorLangId == markdown && (resourceScheme == file || resourceScheme == untitled)",
|
||||||
|
"group": "1_cowriting@1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "cowriting.editDocument",
|
||||||
|
"when": "!editorHasSelection && editorLangId == markdown && (resourceScheme == file || resourceScheme == untitled)",
|
||||||
"group": "1_cowriting@1"
|
"group": "1_cowriting@1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -205,11 +184,6 @@
|
|||||||
"command": "cowriting.reply",
|
"command": "cowriting.reply",
|
||||||
"group": "inline",
|
"group": "inline",
|
||||||
"when": "commentController == cowriting.threads"
|
"when": "commentController == cowriting.threads"
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "cowriting.askClaude.submit",
|
|
||||||
"group": "inline",
|
|
||||||
"when": "commentController == cowriting.askClaude"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"comments/commentThread/title": [
|
"comments/commentThread/title": [
|
||||||
@@ -222,11 +196,6 @@
|
|||||||
"command": "cowriting.reopenThread",
|
"command": "cowriting.reopenThread",
|
||||||
"group": "inline",
|
"group": "inline",
|
||||||
"when": "commentController == cowriting.threads && commentThread =~ /^resolved$/"
|
"when": "commentController == cowriting.threads && commentThread =~ /^resolved$/"
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "cowriting.askClaude.cancel",
|
|
||||||
"group": "inline",
|
|
||||||
"when": "commentController == cowriting.askClaude"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -234,14 +203,7 @@
|
|||||||
{
|
{
|
||||||
"command": "cowriting.showTrackChangesPreview",
|
"command": "cowriting.showTrackChangesPreview",
|
||||||
"key": "ctrl+alt+r",
|
"key": "ctrl+alt+r",
|
||||||
"mac": "cmd+alt+r",
|
|
||||||
"when": "editorLangId == markdown"
|
"when": "editorLangId == markdown"
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "cowriting.edit",
|
|
||||||
"key": "ctrl+alt+e",
|
|
||||||
"mac": "cmd+alt+e",
|
|
||||||
"when": "editorTextFocus && editorLangId == markdown"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
# Session 0056.0 — Transcript
|
|
||||||
|
|
||||||
> App: vscode-cowriting-plugin
|
|
||||||
> Start: 2026-06-26T04-24 (PST)
|
|
||||||
> End: 2026-06-26T04-54 (PST)
|
|
||||||
> Type: planning-and-executing
|
|
||||||
> Posture: autonomous (yolo)
|
|
||||||
> Status: **FINALIZED**
|
|
||||||
|
|
||||||
## Launch prompt
|
|
||||||
|
|
||||||
`/wgl-planning-and-executing implement #60 (live turn progress) from coauthoring-live-progress.md`
|
|
||||||
|
|
||||||
(Continuation of brainstorming session 0055, which graduated the spec; the
|
|
||||||
operator said "implement it" → this coding session.)
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
|
|
||||||
Plan + execute **#60** (P1 feature — live turn progress) from the graduated
|
|
||||||
Solution Design `coauthoring-live-progress.md`. Four cuts: (1) pure
|
|
||||||
`turnProgress.ts` reducer + tests; (2) `runEditTurn` + `onProgress`/`AbortSignal`;
|
|
||||||
(3) `liveProgressUi` + setting + both call sites; (4) host-E2E + smoke.
|
|
||||||
Branch `s60-live-progress` → PR → main.
|
|
||||||
|
|
||||||
## Pre-state
|
|
||||||
|
|
||||||
- Clean `main` at session start (fast-forwarded 13 commits — the sessions/ repo
|
|
||||||
is the code repo itself). Baseline: 222 unit green.
|
|
||||||
- #60 has a graduated Solution Design → §4.3 R2(b) satisfied.
|
|
||||||
|
|
||||||
## Session arc
|
|
||||||
|
|
||||||
1. **Claim + plan.** Claimed 0056 (no in-flight). Wrote the implementation plan
|
|
||||||
with `superpowers:writing-plans` → `docs/superpowers/plans/2026-06-26-live-turn-progress.md`
|
|
||||||
(self-reviewed: full spec coverage, no placeholders). Grounded in the exact SDK
|
|
||||||
types (`AgentToolCallPart.toolName`, `AgentUsage.inputTokens+outputTokens`), the
|
|
||||||
vitest/host-E2E patterns, and the injectable `editTurn` seam. Branch
|
|
||||||
`s60-live-progress`.
|
|
||||||
2. **Task 1 — pure reducer.** `src/turnProgress.ts` + `test/turnProgress.test.ts`
|
|
||||||
(13 cases). Fixed the type-only event import: `AgentRuntimeEvent` is exported
|
|
||||||
from **`@cline/shared`**, not `@cline/sdk` (which doesn't re-export it) — caught
|
|
||||||
by typecheck; amended the commit.
|
|
||||||
3. **Task 2 — `runEditTurn`.** Added `RunEditTurnOptions {onProgress, signal}`;
|
|
||||||
subscribes to the agent, folds events through the reducer, wires `signal` →
|
|
||||||
`agent.abort()`; `finally` unsubscribes. Mocked-agent unit tests (vitest
|
|
||||||
`doMock` intercepts the dynamic import).
|
|
||||||
4. **Task 3 — host relay.** `src/liveProgressUi.ts` (notification line +
|
|
||||||
"Cowriting: Claude" OutputChannel, reveal gated by setting) + `package.json`
|
|
||||||
`contributes.configuration` `cowriting.liveProgress.revealOutput`.
|
|
||||||
5. **Tasks 4+5 — both call sites.** `editSelection` (extension.ts) and preview
|
|
||||||
`askClaude`/`runEditAndPropose` (trackChangesPreview.ts) made cancellable,
|
|
||||||
relaying via the shared `liveProgressUi`; widened the `EditTurn` seam to accept
|
|
||||||
`opts?` (back-compat). Added `liveProgressUi` to `CowritingApi`.
|
|
||||||
6. **Task 6 — host E2E.** `test/e2e/suite/liveProgress.test.ts`: progress is
|
|
||||||
additive (INV-44), aborted turn proposes nothing (INV-47).
|
|
||||||
7. **Task 7 — verify + smoke.** typecheck + 237 unit + build green; enhanced
|
|
||||||
`scripts/smoke-live-turn.mjs` to log progress.
|
|
||||||
8. **Subagent review.** Found a **Medium** (a pre-aborted `AbortSignal` was a
|
|
||||||
no-op — `agent.abort()` runs before the SDK's AbortController exists, so the
|
|
||||||
turn ran to completion) + a **Low** (a throwing `onProgress` relay could fail
|
|
||||||
the turn). Fixed both: short-circuit (throw) before `run()` on a pre-aborted
|
|
||||||
signal; wrap the relay in try/catch. Strengthened the unit + E2E to prove it.
|
|
||||||
9. **Ship.** Pushed; opened PR #61; operator approved merge despite the
|
|
||||||
concurrency (below). Squash-merged `644885c`; issue #60 auto-closed.
|
|
||||||
|
|
||||||
## ⚠️ Concurrency event (mid-session)
|
|
||||||
|
|
||||||
Partway through, the shared working tree gained **uncommitted changes this session
|
|
||||||
did NOT make** — a `cowriting.edit` / `routeEdit` reach refactor across
|
|
||||||
`src/workspacePath.ts`, `test/workspacePath.test.ts`, and the menu/`routeEdit`
|
|
||||||
parts of `package.json` + `src/extension.ts`. It broke 3 E2E (`f12Reach` ×2,
|
|
||||||
`f11Toolbar` ×1) that still expect the old `editSelection`/`editDocument` command
|
|
||||||
surface. Handling: committed **nothing** of it, discarded **nothing**; verified
|
|
||||||
#60 green in isolation by stashing it (by path, reversible) — 237 unit + both #60
|
|
||||||
E2E pass, only the documented `F10 #38` undo-sandbox flake remains — then popped
|
|
||||||
the stash to restore it. Surfaced to the operator, who chose to merge #60 (its 7
|
|
||||||
commits are isolated; the PR diff contains none of the foreign WIP).
|
|
||||||
|
|
||||||
## Cut state (at finalize)
|
|
||||||
|
|
||||||
- **#60 merged** to main (PR #61 squash `644885c`); issue #60 closed.
|
|
||||||
- **Plan archived:** `submit-plan.sh` → content repo `plans/2026-06-26-live-turn-progress.md` (`22ec57e`).
|
|
||||||
- **Foreign refactor WIP:** still uncommitted in the working tree, untouched —
|
|
||||||
for whoever is doing it to continue/commit (it rebases onto the new main).
|
|
||||||
- **Local tree:** left on branch `s60-live-progress` (merged) DELIBERATELY — a
|
|
||||||
`checkout main`/`pull` would have disturbed the foreign uncommitted WIP. Local
|
|
||||||
main not synced; origin/main has #60. Operator should reconcile the local tree
|
|
||||||
+ the in-flight refactor.
|
|
||||||
- **Memory:** added `session-0056-60-live-progress-shipped.md` + index line.
|
|
||||||
|
|
||||||
## Next-session prompt
|
|
||||||
|
|
||||||
```
|
|
||||||
/wgl-planning-and-executing reconcile the local tree (sync main; land/branch the in-flight cowriting.edit/routeEdit refactor), then pick the next item (open: #59 P1 bug, #57, #58, #32, #35, #40, OQ-2)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deferred decisions
|
|
||||||
|
|
||||||
- **Left local on `s60-live-progress` (did not sync main):** to avoid disturbing
|
|
||||||
the concurrent uncommitted refactor in the shared tree. Low confidence this is
|
|
||||||
the tidiest end state, but it is the safest for the foreign WIP. Operator to
|
|
||||||
reconcile.
|
|
||||||
- **Token field for the activity line:** used `inputTokens + outputTokens` from
|
|
||||||
`AgentUsage` (no `totalTokens` field exists). Cache tokens excluded.
|
|
||||||
- **OutputChannel append-not-clear + auto-reveal gating:** per spec §3.5; carried
|
|
||||||
from session 0055's confirmed sub-decisions.
|
|
||||||
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
# Session 0059.0 — Transcript
|
# Session 0056.0 — Transcript
|
||||||
|
|
||||||
> App: vscode-cowriting-plugin
|
> App: vscode-cowriting-plugin
|
||||||
> Start: 2026-06-26T06-01 (PST)
|
> Start: 2026-06-26T04-24 (PST)
|
||||||
> Type: planning-and-executing
|
> Type: planning-and-executing
|
||||||
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
|
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
|
||||||
>
|
>
|
||||||
> This file reserves session ID 0059 for vscode-cowriting-plugin. The driver replaces this
|
> This file reserves session ID 0056 for vscode-cowriting-plugin. The driver replaces this
|
||||||
> body with the full transcript and renames the file to its final
|
> body with the full transcript and renames the file to its final
|
||||||
> SESSION-0059.0-TRANSCRIPT-2026-06-26T06-01--<end>.md form at session end.
|
> SESSION-0056.0-TRANSCRIPT-2026-06-26T04-24--<end>.md form at session end.
|
||||||
|
|
||||||
## Launch prompt
|
## Launch prompt
|
||||||
|
|
||||||
@@ -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._
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# Session 0058.0 — Transcript
|
|
||||||
|
|
||||||
> App: vscode-cowriting-plugin
|
|
||||||
> Start: 2026-06-26T05-12 (PST)
|
|
||||||
> End: 2026-06-26T05-58 (PST)
|
|
||||||
> Type: brainstorming
|
|
||||||
> Posture: autonomous (yolo)
|
|
||||||
> Status: **FINALIZED**
|
|
||||||
|
|
||||||
## Launch prompt
|
|
||||||
|
|
||||||
Operator opened by asking what was in the Gitea backlog, then directed a
|
|
||||||
brainstorming session for a new feature: *"We should also show the proposed change
|
|
||||||
in the Markdown file in the same way. The proposed change in the markdown file
|
|
||||||
should be human-editable, show the diff just like it would after the change is
|
|
||||||
completed in both the markdown file and the webview as the change is made. The
|
|
||||||
diff in both the markdown file and preview webview should be exactly as they will
|
|
||||||
be shown if the edit is accepted. Create a new working tree in a directory other
|
|
||||||
than main for this work."* — extended mid-session with: *"replace the checkbox
|
|
||||||
icon and 'x' icon on the proposed change as 'Accept' and 'Reject'. Each should
|
|
||||||
have an arrow next to them for a dropdown that is 'Accept All' or 'Reject All'.
|
|
||||||
One should be able to accept, reject, accept all, or reject all from either the
|
|
||||||
markdown file or the web preview file."*
|
|
||||||
|
|
||||||
## Pre-state
|
|
||||||
|
|
||||||
- Branch `main`, clean/pushed baseline (after FF). Latest shipped work: #60 live
|
|
||||||
turn progress (session 0056, PR #61). Session 0057 (a `cowriting.edit`/`routeEdit`
|
|
||||||
refactor) still **in-flight** sharing the checkout.
|
|
||||||
- Backlog at session start (8 open): #59 (P1 bug, Apple-Events prompt), #54/#57/#58
|
|
||||||
(P2), #32/#35/#40 (P3), #62 (untriaged).
|
|
||||||
|
|
||||||
## Turn-by-turn arc
|
|
||||||
|
|
||||||
1. **Backlog query** — listed the 8 open Gitea issues for `benstull/vscode-cowriting-plugin`
|
|
||||||
by priority; flagged #62 as the only untriaged item.
|
|
||||||
2. **Session routing** — operator's feature request was feature-shaped *and*
|
|
||||||
reversed a locked invariant (F10/INV-32 "clean editor"), so the session type
|
|
||||||
was ambiguous. Asked; operator chose **brainstorm a spec first**. Routed via
|
|
||||||
`wgl-brainstorming`; claimed session **0058**.
|
|
||||||
3. **Isolation** — session 0057 still in-flight on the shared checkout → created an
|
|
||||||
isolated worktree `vscode-cowriting-plugin-s58` on branch
|
|
||||||
`s58-inline-editor-diff` off `origin/main` (§5.4).
|
|
||||||
4. **Exploration** — dispatched an Explore agent that mapped five areas: webview
|
|
||||||
render engine (`trackChangesModel.ts`), F4 proposal model + seam, the F10
|
|
||||||
clean-editor decision (zero editor decorations remain), controllers/wiring, and
|
|
||||||
commands/toolbar. Key constraint surfaced: **VS Code text editors cannot host
|
|
||||||
editable phantom text** — anything editable in the editor is buffer content;
|
|
||||||
decorations only style existing text or inject *non-editable* before/after
|
|
||||||
content.
|
|
||||||
5. **Four design forks (AskUserQuestion, with ASCII previews):**
|
|
||||||
- Editor model → **optimistic apply + decorations**.
|
|
||||||
- Timing → **on proposal** (not live token-stream into the editor).
|
|
||||||
- Editor affordance → **CodeLens `Accept ▾ / Reject ▾` + QuickPick dropdown**.
|
|
||||||
- (Operator-added) Control parity → Accept/Reject/Accept-all/Reject-all from
|
|
||||||
both surfaces; ✓/✗ superseded.
|
|
||||||
6. **Design presented** section-by-section; operator approved with "keep going"
|
|
||||||
(nodded the two flagged sub-decisions: dedicated `EditorProposalController`;
|
|
||||||
allow save of pending text).
|
|
||||||
7. **Anchor + spec** — filed Feature **#64** (`type/feature`, `priority/P2`) since
|
|
||||||
no tracker anchor existed; wrote `specs/coauthoring-inline-editor-diff.md`
|
|
||||||
(INV-48..54, reversing INV-10/INV-32). Self-review clean. Operator **approved**.
|
|
||||||
|
|
||||||
## Cut state
|
|
||||||
|
|
||||||
- Spec **graduated** → content repo `vscode-cowriting-plugin-content` at `80d5d1a`
|
|
||||||
(`specs/coauthoring-inline-editor-diff.md`).
|
|
||||||
- Feature **#64** filed + labelled.
|
|
||||||
- Worktree `vscode-cowriting-plugin-s58` (branch `s58-inline-editor-diff`) stands
|
|
||||||
ready for the downstream planning-and-executing session; no code committed this
|
|
||||||
session (brainstorming output is the spec only).
|
|
||||||
- Memory updated: `f12-inline-editor-diff-spec-graduated.md` + MEMORY.md index.
|
|
||||||
|
|
||||||
## Deferred decisions
|
|
||||||
|
|
||||||
No autonomous low-confidence calls — every fork was decided live with the operator
|
|
||||||
via AskUserQuestion, and the spec was operator-approved before submission. (The
|
|
||||||
two design sub-decisions — dedicated controller, allow-save-of-pending — were
|
|
||||||
flagged in the presented design and nodded by the operator.)
|
|
||||||
|
|
||||||
## Next-session prompt
|
|
||||||
|
|
||||||
```
|
|
||||||
/goal Plan-and-execute #64 (F12 inline editable proposed-change diff in the Markdown editor) from specs/coauthoring-inline-editor-diff.md, in worktree vscode-cowriting-plugin-s58 (branch s58-inline-editor-diff); follow the spec's 5-slice Delivery Plan
|
|
||||||
```
|
|
||||||
@@ -166,14 +166,5 @@
|
|||||||
},
|
},
|
||||||
"0056": {
|
"0056": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
|
||||||
"0057": {
|
|
||||||
"title": ""
|
|
||||||
},
|
|
||||||
"0058": {
|
|
||||||
"title": ""
|
|
||||||
},
|
|
||||||
"0059": {
|
|
||||||
"title": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-37
@@ -12,8 +12,7 @@ import { SidecarRouter } from "./sidecarRouter";
|
|||||||
import { DiffViewController } from "./diffViewController";
|
import { DiffViewController } from "./diffViewController";
|
||||||
import { TrackChangesPreviewController } from "./trackChangesPreview";
|
import { TrackChangesPreviewController } from "./trackChangesPreview";
|
||||||
import { LiveProgressUi } from "./liveProgressUi";
|
import { LiveProgressUi } from "./liveProgressUi";
|
||||||
import { InlineAskController } from "./inlineAsk";
|
import { isAuthorable, selectionRejection } from "./workspacePath";
|
||||||
import { isAuthorable, routeEdit, selectionRejection } from "./workspacePath";
|
|
||||||
|
|
||||||
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
||||||
|
|
||||||
@@ -26,7 +25,6 @@ export interface CowritingApi {
|
|||||||
trackChangesPreviewController: TrackChangesPreviewController;
|
trackChangesPreviewController: TrackChangesPreviewController;
|
||||||
sidecarRouter: SidecarRouter;
|
sidecarRouter: SidecarRouter;
|
||||||
liveProgressUi: LiveProgressUi;
|
liveProgressUi: LiveProgressUi;
|
||||||
inlineAsk: InlineAskController;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
|
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
|
||||||
@@ -38,11 +36,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
// OutputChannel) for both Ask-Claude entry points.
|
// OutputChannel) for both Ask-Claude entry points.
|
||||||
const liveProgressUi = new LiveProgressUi();
|
const liveProgressUi = new LiveProgressUi();
|
||||||
context.subscriptions.push(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(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand("cowriting.showClineSdkInfo", async () => {
|
vscode.commands.registerCommand("cowriting.showClineSdkInfo", async () => {
|
||||||
try {
|
try {
|
||||||
@@ -119,7 +112,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
attributionController,
|
attributionController,
|
||||||
proposalController,
|
proposalController,
|
||||||
liveProgressUi,
|
liveProgressUi,
|
||||||
inlineAsk,
|
|
||||||
);
|
);
|
||||||
context.subscriptions.push(trackChangesPreviewController);
|
context.subscriptions.push(trackChangesPreviewController);
|
||||||
|
|
||||||
@@ -225,12 +217,17 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!editor) return; // unreachable once reason is null, but narrows the type
|
if (!editor) return; // unreachable once reason is null, but narrows the type
|
||||||
const document = editor.document;
|
const instruction = await vscode.window.showInputBox({
|
||||||
const selection = editor.selection; // non-empty (selectionRejection guaranteed it)
|
prompt: "What should Claude do with the selection?",
|
||||||
// The instruction prompt opens INLINE at the selection (inlineAsk), not the
|
placeHolder: "e.g. tighten this paragraph",
|
||||||
// top-center QuickInput — anchored to the text Claude will edit.
|
});
|
||||||
const instruction = await inlineAsk.prompt(document.uri, selection);
|
|
||||||
if (!instruction) return;
|
if (!instruction) return;
|
||||||
|
if (editor.selection.isEmpty) {
|
||||||
|
void vscode.window.showWarningMessage("Cowriting: select some text to send to Claude first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const document = editor.document;
|
||||||
|
const selection = editor.selection;
|
||||||
const selectedText = document.getText(selection);
|
const selectedText = document.getText(selection);
|
||||||
// Capture the anchor BEFORE the turn (spec §6.5 PUC-1): mid-turn edits
|
// 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.
|
// can't skew it — the proposal renders wherever the target re-resolves.
|
||||||
@@ -302,28 +299,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The single user-facing "Ask Claude to Edit" gesture (one command, one
|
|
||||||
// keybinding, one menu entry). It routes to the selection flow (editSelection)
|
|
||||||
// or the whole-document flow (editDocument) by selection/context — see
|
|
||||||
// routeEdit. The two underlying commands stay registered for the host E2E
|
|
||||||
// harness and the internal seams, but are hidden from the palette.
|
|
||||||
context.subscriptions.push(
|
|
||||||
vscode.commands.registerCommand("cowriting.edit", async (uri?: vscode.Uri) => {
|
|
||||||
const editor = vscode.window.activeTextEditor;
|
|
||||||
const route = routeEdit({
|
|
||||||
hasUri: !!uri,
|
|
||||||
uriMatchesActiveEditor: !!uri && editor?.document.uri.toString() === uri.toString(),
|
|
||||||
hasActiveEditor: !!editor,
|
|
||||||
selectionEmpty: editor?.selection.isEmpty ?? true,
|
|
||||||
});
|
|
||||||
if (route === "selection") {
|
|
||||||
await vscode.commands.executeCommand("cowriting.editSelection");
|
|
||||||
} else {
|
|
||||||
await vscode.commands.executeCommand("cowriting.editDocument", uri);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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 (isAuthorable(doc.uri.scheme)) {
|
if (isAuthorable(doc.uri.scheme)) {
|
||||||
@@ -344,7 +319,6 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
trackChangesPreviewController,
|
trackChangesPreviewController,
|
||||||
sidecarRouter,
|
sidecarRouter,
|
||||||
liveProgressUi,
|
liveProgressUi,
|
||||||
inlineAsk,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -97,14 +97,6 @@ export interface Proposal {
|
|||||||
* back-compat with older sidecars) ⇒ a single-range proposal accepted whole.
|
* back-compat with older sidecars) ⇒ a single-range proposal accepted whole.
|
||||||
*/
|
*/
|
||||||
granularity?: "block" | "single";
|
granularity?: "block" | "single";
|
||||||
/**
|
|
||||||
* F12/#64 (INV-48): the pre-apply text, captured when a proposal is
|
|
||||||
* optimistically applied to the buffer. `replacement` is now in the buffer and
|
|
||||||
* `fp.text` re-anchors to it, so `original` is the only record of what to revert
|
|
||||||
* to (revert-in-place) and what to show struck in the `<del>` half. Absent on a
|
|
||||||
* proposal created but not yet optimistically applied (or older sidecars).
|
|
||||||
*/
|
|
||||||
original?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Artifact {
|
export interface Artifact {
|
||||||
@@ -260,8 +252,6 @@ export function serializeArtifact(a: Artifact): string {
|
|||||||
createdAt: p.createdAt,
|
createdAt: p.createdAt,
|
||||||
...(p.turnId !== undefined ? { turnId: p.turnId } : {}),
|
...(p.turnId !== undefined ? { turnId: p.turnId } : {}),
|
||||||
...(p.instruction !== undefined ? { instruction: p.instruction } : {}),
|
...(p.instruction !== undefined ? { instruction: p.instruction } : {}),
|
||||||
...(p.original !== undefined ? { original: p.original } : {}),
|
|
||||||
...(p.granularity !== undefined ? { granularity: p.granularity } : {}),
|
|
||||||
},
|
},
|
||||||
p,
|
p,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { buildFingerprint } from "./anchorer";
|
|||||||
import { isAuthorable } from "./workspacePath";
|
import { isAuthorable } from "./workspacePath";
|
||||||
import type { EditTurnResult, RunEditTurnOptions } from "./liveTurn";
|
import type { EditTurnResult, RunEditTurnOptions } from "./liveTurn";
|
||||||
import type { LiveProgressUi } from "./liveProgressUi";
|
import type { LiveProgressUi } from "./liveProgressUi";
|
||||||
import type { InlineAskController } from "./inlineAsk";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* F11: a host edit turn (selection/document text + instruction → rewrite).
|
* F11: a host edit turn (selection/document text + instruction → rewrite).
|
||||||
@@ -75,7 +74,6 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
private readonly attribution: AttributionController,
|
private readonly attribution: AttributionController,
|
||||||
private readonly proposals: ProposalController,
|
private readonly proposals: ProposalController,
|
||||||
private readonly liveProgressUi: LiveProgressUi,
|
private readonly liveProgressUi: LiveProgressUi,
|
||||||
private readonly inlineAsk: InlineAskController,
|
|
||||||
) {
|
) {
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
|
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
|
||||||
@@ -222,34 +220,19 @@ 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
|
* 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
|
* 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`.
|
* the result as F4 proposal(s). UI wrapper around `runEditAndPropose`.
|
||||||
*/
|
*/
|
||||||
private async askClaude(document: vscode.TextDocument, target: EditTarget): Promise<void> {
|
private async askClaude(document: vscode.TextDocument, target: EditTarget): Promise<void> {
|
||||||
// The instruction prompt opens INLINE (inlineAsk) at the target: the selected
|
const instruction = await vscode.window.showInputBox({
|
||||||
// range for a range edit, or the editor's cursor line / the document start for
|
prompt:
|
||||||
// a whole-document edit — never the top-center QuickInput.
|
target.kind === "document"
|
||||||
const anchor = this.editTargetAnchor(document, target);
|
? "What should Claude do with the document?"
|
||||||
const instruction = await this.inlineAsk.prompt(document.uri, anchor);
|
: "What should Claude do with the selection?",
|
||||||
|
placeHolder: "e.g. tighten the prose",
|
||||||
|
});
|
||||||
if (!instruction) return;
|
if (!instruction) return;
|
||||||
try {
|
try {
|
||||||
const ids = await vscode.window.withProgress(
|
const ids = await vscode.window.withProgress(
|
||||||
|
|||||||
@@ -60,29 +60,3 @@ export function selectionRejection(ctx: SelectionContext): string | null {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditRouteContext {
|
|
||||||
/** Was the command invoked on a specific resource (a tab right-click)? */
|
|
||||||
hasUri: boolean;
|
|
||||||
/** Does that resource match the focused editor's document? */
|
|
||||||
uriMatchesActiveEditor: boolean;
|
|
||||||
/** Is there a focused text editor at all? */
|
|
||||||
hasActiveEditor: boolean;
|
|
||||||
/** Is the focused editor's selection empty (no highlight)? */
|
|
||||||
selectionEmpty: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route the single "Ask Claude to Edit" gesture (`cowriting.edit`) to the
|
|
||||||
* selection or whole-document flow. One conceptual command, two destinations:
|
|
||||||
*
|
|
||||||
* - A tab right-click on a document that ISN'T the focused editor has no
|
|
||||||
* selection to act on → edit the whole document.
|
|
||||||
* - Otherwise a non-empty selection in the focused editor → edit the selection;
|
|
||||||
* an empty selection (or no editor) → edit the whole document.
|
|
||||||
*/
|
|
||||||
export function routeEdit(ctx: EditRouteContext): "selection" | "document" {
|
|
||||||
if (ctx.hasUri && !ctx.uriMatchesActiveEditor) return "document";
|
|
||||||
if (ctx.hasActiveEditor && !ctx.selectionEmpty) return "selection";
|
|
||||||
return "document";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ suite("no-workspace authoring (F8 — real folder-less, #8 lineage)", () => {
|
|||||||
"cowriting.reply",
|
"cowriting.reply",
|
||||||
"cowriting.resolveThread",
|
"cowriting.resolveThread",
|
||||||
"cowriting.reopenThread",
|
"cowriting.reopenThread",
|
||||||
"cowriting.edit",
|
|
||||||
"cowriting.editSelection",
|
"cowriting.editSelection",
|
||||||
"cowriting.askClaude.submit",
|
|
||||||
"cowriting.askClaude.cancel",
|
|
||||||
"cowriting.applyAgentEdit",
|
"cowriting.applyAgentEdit",
|
||||||
"cowriting.proposeAgentEdit",
|
"cowriting.proposeAgentEdit",
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -174,22 +174,16 @@ suite("F11 preview toolbar (host E2E — message → seam wiring, no LLM)", () =
|
|||||||
assert.strictEqual(api.proposalController.listProposals(doc).length, 0, "no proposals left pending");
|
assert.strictEqual(api.proposalController.listProposals(doc).length, 0, "no proposals left pending");
|
||||||
});
|
});
|
||||||
|
|
||||||
// SLICE-3: the document-scoped command stays registered for #42 reuse (now behind
|
// SLICE-3: the document-scoped command exists for #42 reuse, guarded on markdown.
|
||||||
// the unified `cowriting.edit`). editDocument is hidden from the palette; the
|
test("cowriting.editDocument is a registered command, palette-guarded on markdown", async () => {
|
||||||
// markdown-guarded user-facing palette command is `cowriting.edit`.
|
|
||||||
test("cowriting.editDocument stays registered; cowriting.edit is the markdown-guarded palette command", async () => {
|
|
||||||
const all = await vscode.commands.getCommands(true);
|
const all = await vscode.commands.getCommands(true);
|
||||||
assert.ok(all.includes("cowriting.editDocument"), "editDocument command registered");
|
assert.ok(all.includes("cowriting.editDocument"), "editDocument command registered");
|
||||||
assert.ok(all.includes("cowriting.edit"), "unified edit command registered");
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8"));
|
||||||
const palette = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8")).contributes
|
const entry = (pkg.contributes.menus.commandPalette as Array<{ command: string; when?: string }>).find(
|
||||||
.menus.commandPalette as Array<{ command: string; when?: string }>;
|
(m) => m.command === "cowriting.editDocument",
|
||||||
// editDocument is hidden (routed via cowriting.edit).
|
);
|
||||||
const docEntry = palette.find((m) => m.command === "cowriting.editDocument");
|
assert.ok(entry, "editDocument has a commandPalette entry");
|
||||||
assert.strictEqual(docEntry?.when, "false", "editDocument hidden from the palette");
|
assert.match(entry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
||||||
// cowriting.edit is the user-facing, markdown-guarded entry.
|
|
||||||
const editEntry = palette.find((m) => m.command === "cowriting.edit");
|
|
||||||
assert.ok(editEntry, "cowriting.edit has a commandPalette entry");
|
|
||||||
assert.match(editEntry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// SLICE-5: the minimal right-click gateway lives in editor/title (markdown only).
|
// SLICE-5: the minimal right-click gateway lives in editor/title (markdown only).
|
||||||
|
|||||||
@@ -31,37 +31,43 @@ function menu(id: string): Array<{ command: string; when?: string; group?: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SLICE-1 / #42 (reach): "Ask Claude to Edit" reachable from the editor BODY and
|
// SLICE-1 / #42 (reach): "Ask Claude to Edit" reachable from the editor BODY and
|
||||||
// the editor TAB. The two split commands (editSelection / editDocument) were
|
// the editor TAB, selection-aware (selection → editSelection; no selection →
|
||||||
// unified behind ONE user-facing command `cowriting.edit` that auto-routes by
|
// editDocument), both markdown/authorable-gated, both routing through the single
|
||||||
// selection at runtime (routeEdit: selection → editSelection, none → editDocument)
|
// runEditAndPropose path (INV-38). The menu `when` clauses are declarative, so we
|
||||||
// — so the menus carry a SINGLE selection-agnostic entry, both markdown/authorable
|
// assert them directly; the tab-targeting behavior is exercised through the command.
|
||||||
// -gated. The routing itself is unit-tested (routeEdit) and exercised through the
|
|
||||||
// command below; here we assert the declarative menu `when` clauses.
|
|
||||||
suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
||||||
// PUC-1/2: editor BODY (editor/context) offers the single unified entry,
|
// PUC-1/2: editor BODY (editor/context) is selection-aware + markdown-gated.
|
||||||
// markdown + authorable gated, NOT selection-gated (it routes both cases).
|
test("editor/context offers editSelection (with selection) and editDocument (without), markdown+authorable", () => {
|
||||||
test("editor/context offers a single 'Ask Claude to Edit' (cowriting.edit), markdown+authorable", () => {
|
|
||||||
const m = menu("editor/context");
|
const m = menu("editor/context");
|
||||||
const edit = m.find((e) => e.command === "cowriting.edit");
|
const sel = m.find((e) => e.command === "cowriting.editSelection");
|
||||||
assert.ok(edit, "cowriting.edit is in editor/context");
|
const doc = m.find((e) => e.command === "cowriting.editDocument");
|
||||||
assert.match(edit!.when ?? "", /editorLangId == markdown/, "gated on markdown");
|
assert.ok(sel, "editSelection is in editor/context");
|
||||||
assert.match(edit!.when ?? "", /resourceScheme == file|resourceScheme == untitled/, "gated authorable");
|
assert.ok(doc, "editDocument is in editor/context");
|
||||||
assert.ok(!/editorHasSelection/.test(edit!.when ?? ""), "not selection-gated — one entry routes both cases");
|
|
||||||
// The old split pair is gone from the menu (the commands stay registered/hidden).
|
assert.match(sel!.when ?? "", /editorHasSelection/, "editSelection shows only with a selection");
|
||||||
assert.ok(!m.some((e) => e.command === "cowriting.editSelection"), "old editSelection menu entry removed");
|
assert.ok(!/!\s*editorHasSelection/.test(sel!.when ?? ""), "editSelection is not gated on NO selection");
|
||||||
assert.ok(!m.some((e) => e.command === "cowriting.editDocument"), "old editDocument menu entry removed");
|
assert.match(sel!.when ?? "", /editorLangId == markdown/, "editSelection gated on markdown");
|
||||||
|
assert.match(sel!.when ?? "", /resourceScheme == file|resourceScheme == untitled/, "editSelection gated authorable");
|
||||||
|
|
||||||
|
assert.match(doc!.when ?? "", /!\s*editorHasSelection/, "editDocument shows only without a selection");
|
||||||
|
assert.match(doc!.when ?? "", /editorLangId == markdown/, "editDocument gated on markdown");
|
||||||
|
assert.match(doc!.when ?? "", /resourceScheme == file|resourceScheme == untitled/, "editDocument gated authorable");
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUC-3: editor TAB (editor/title/context) carries the same single entry.
|
// PUC-3: editor TAB (editor/title/context) carries the same selection-aware pair.
|
||||||
test("editor/title/context offers a single 'Ask Claude to Edit' (cowriting.edit), markdown-gated", () => {
|
test("editor/title/context offers editSelection (with selection) and editDocument (without), markdown-gated", () => {
|
||||||
const m = menu("editor/title/context");
|
const m = menu("editor/title/context");
|
||||||
const edit = m.find((e) => e.command === "cowriting.edit");
|
const sel = m.find((e) => e.command === "cowriting.editSelection");
|
||||||
assert.ok(edit, "cowriting.edit is in editor/title/context");
|
const doc = m.find((e) => e.command === "cowriting.editDocument");
|
||||||
assert.match(edit!.when ?? "", /resourceLangId == markdown/, "tab entry gated on markdown");
|
assert.ok(sel, "editSelection is in editor/title/context");
|
||||||
assert.ok(
|
assert.ok(doc, "editDocument is in editor/title/context");
|
||||||
!m.some((e) => e.command === "cowriting.editSelection" || e.command === "cowriting.editDocument"),
|
|
||||||
"old split pair removed from the tab menu",
|
assert.match(sel!.when ?? "", /editorHasSelection/, "tab editSelection shows only with a selection");
|
||||||
);
|
assert.ok(!/!\s*editorHasSelection/.test(sel!.when ?? ""), "tab editSelection is not gated on NO selection");
|
||||||
|
assert.match(sel!.when ?? "", /resourceLangId == markdown/, "tab editSelection gated on markdown");
|
||||||
|
|
||||||
|
assert.match(doc!.when ?? "", /!\s*editorHasSelection/, "tab editDocument shows only without a selection");
|
||||||
|
assert.match(doc!.when ?? "", /resourceLangId == markdown/, "tab editDocument gated on markdown");
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUC-3 behavior: editDocument invoked with a tab URI targets THAT document,
|
// PUC-3 behavior: editDocument invoked with a tab URI targets THAT document,
|
||||||
@@ -77,8 +83,8 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
|||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
// Stub the instruction prompt (sealed input box can't run in CI) + the LLM turn.
|
// Stub the instruction prompt (sealed input box can't run in CI) + the LLM turn.
|
||||||
const origPrompt = api.inlineAsk.prompt;
|
const origInput = vscode.window.showInputBox;
|
||||||
(api.inlineAsk as any).prompt = async () => "rewrite it";
|
(vscode.window as any).showInputBox = async () => "rewrite it";
|
||||||
ctl.setEditTurnForTest(async () => ({
|
ctl.setEditTurnForTest(async () => ({
|
||||||
replacement: "# Tab\n\nThe REWRITTEN tab paragraph.\n",
|
replacement: "# Tab\n\nThe REWRITTEN tab paragraph.\n",
|
||||||
model: "sonnet",
|
model: "sonnet",
|
||||||
@@ -88,7 +94,7 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
|||||||
await vscode.commands.executeCommand("cowriting.editDocument", b.doc.uri);
|
await vscode.commands.executeCommand("cowriting.editDocument", b.doc.uri);
|
||||||
await settle();
|
await settle();
|
||||||
} finally {
|
} finally {
|
||||||
(api.inlineAsk as any).prompt = origPrompt;
|
(vscode.window as any).showInputBox = origInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The proposal(s) landed on the TAB doc (B), and the ACTIVE doc (A) has none.
|
// The proposal(s) landed on the TAB doc (B), and the ACTIVE doc (A) has none.
|
||||||
@@ -110,8 +116,8 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
|||||||
await vscode.window.showTextDocument(a.doc);
|
await vscode.window.showTextDocument(a.doc);
|
||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
const origPrompt = api.inlineAsk.prompt;
|
const origInput = vscode.window.showInputBox;
|
||||||
(api.inlineAsk as any).prompt = async () => "rewrite it";
|
(vscode.window as any).showInputBox = async () => "rewrite it";
|
||||||
ctl.setEditTurnForTest(async () => ({
|
ctl.setEditTurnForTest(async () => ({
|
||||||
replacement: "# No arg\n\nThe REWRITTEN active doc paragraph.\n",
|
replacement: "# No arg\n\nThe REWRITTEN active doc paragraph.\n",
|
||||||
model: "sonnet",
|
model: "sonnet",
|
||||||
@@ -121,7 +127,7 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
|||||||
await vscode.commands.executeCommand("cowriting.editDocument");
|
await vscode.commands.executeCommand("cowriting.editDocument");
|
||||||
await settle();
|
await settle();
|
||||||
} finally {
|
} finally {
|
||||||
(api.inlineAsk as any).prompt = origPrompt;
|
(vscode.window as any).showInputBox = origInput;
|
||||||
}
|
}
|
||||||
assert.ok(api.proposalController.listProposals(a.doc).length >= 1, "active doc received the proposal(s) on no-arg");
|
assert.ok(api.proposalController.listProposals(a.doc).length >= 1, "active doc received the proposal(s) on no-arg");
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-15
@@ -1,5 +1,4 @@
|
|||||||
import assert from "node:assert";
|
import { describe, it, expect } from "vitest";
|
||||||
import { describe, it, test, expect } from "vitest";
|
|
||||||
import {
|
import {
|
||||||
SCHEMA_VERSION,
|
SCHEMA_VERSION,
|
||||||
emptyArtifact,
|
emptyArtifact,
|
||||||
@@ -180,19 +179,6 @@ describe("unknown-field preservation (F5 SLICE-2, INV-15)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("serializeArtifact round-trips Proposal.original", () => {
|
|
||||||
const a = emptyArtifact("doc.md");
|
|
||||||
a.anchors["a_1"] = { fingerprint: { text: "new", before: "", after: "", lineHint: 0 } };
|
|
||||||
a.proposals.push({
|
|
||||||
id: "pr_1", anchorId: "a_1", replacement: "new",
|
|
||||||
author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "s" } },
|
|
||||||
createdAt: "2026-06-26T00:00:00.000Z", original: "old", granularity: "block",
|
|
||||||
});
|
|
||||||
const json = serializeArtifact(a);
|
|
||||||
assert.match(json, /"original": "old"/);
|
|
||||||
assert.match(json, /"granularity": "block"/);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("attributions[] (F3 SLICE-1)", () => {
|
describe("attributions[] (F3 SLICE-1)", () => {
|
||||||
it("round-trips an attribution record through serialize → parse", () => {
|
it("round-trips an attribution record through serialize → parse", () => {
|
||||||
const a = emptyArtifact("docs/x.md");
|
const a = emptyArtifact("docs/x.md");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { isAuthorable, isUnderRoot, routeEdit, 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";
|
||||||
@@ -58,27 +58,3 @@ describe("selectionRejection — F8 widened (accepts out-of-folder + untitled)",
|
|||||||
expect(msg).not.toMatch(/select some text/i);
|
expect(msg).not.toMatch(/select some text/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("routeEdit (single Ask-Claude-to-Edit gesture)", () => {
|
|
||||||
const focused = { hasUri: false, uriMatchesActiveEditor: false, hasActiveEditor: true, selectionEmpty: false };
|
|
||||||
|
|
||||||
it("non-empty selection in the focused editor → selection", () => {
|
|
||||||
expect(routeEdit(focused)).toBe("selection");
|
|
||||||
});
|
|
||||||
it("empty selection in the focused editor → document", () => {
|
|
||||||
expect(routeEdit({ ...focused, selectionEmpty: true })).toBe("document");
|
|
||||||
});
|
|
||||||
it("no active editor → document", () => {
|
|
||||||
expect(routeEdit({ ...focused, hasActiveEditor: false, selectionEmpty: true })).toBe("document");
|
|
||||||
});
|
|
||||||
it("tab right-click on a doc that ISN'T the focused editor → document (no selection to act on)", () => {
|
|
||||||
expect(routeEdit({ hasUri: true, uriMatchesActiveEditor: false, hasActiveEditor: true, selectionEmpty: false })).toBe(
|
|
||||||
"document",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("tab right-click on the focused editor with a selection → selection", () => {
|
|
||||||
expect(routeEdit({ hasUri: true, uriMatchesActiveEditor: true, hasActiveEditor: true, selectionEmpty: false })).toBe(
|
|
||||||
"selection",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user