diff --git a/src/proposalController.ts b/src/proposalController.ts index 139c096..6ae714b 100644 --- a/src/proposalController.ts +++ b/src/proposalController.ts @@ -22,6 +22,8 @@ export interface RenderedProposal { id: string; /** true → resolves exactly, decidable; false → stale/orphaned (INV-11). */ pending: boolean; + /** always false — proposals are decide-only, no reply input (INV-12). */ + canReply: boolean; turnId?: string; range: { start: number; end: number }; } @@ -252,6 +254,11 @@ export class ProposalController implements vscode.Disposable { author: { name: proposal.author.id }, }, ]); + // Proposals are decide-only (INV-12): ✓ accept / ✗ reject in the title + // bar. Without this, VS Code renders its default "Reply…" input with no + // submit command wired — a dead end. Discussion belongs in a regular + // coauthoring thread; proposal discussion trails are deferred (spec §1.7). + vsThread.canReply = false; vsThread.label = pending ? "Pending proposal" : "⚠ Stale proposal (target text changed or missing) — accept disabled"; @@ -315,12 +322,13 @@ export class ProposalController implements vscode.Disposable { const state = this.docs.get(docPath); if (!state) return []; const out: RenderedProposal[] = []; - for (const [id] of state.vsThreads) { + for (const [id, vsThread] of state.vsThreads) { const p = state.artifact.proposals.find((x) => x.id === id)!; const off = state.live.get(id)!; out.push({ id, pending: !state.unresolved.has(id), + canReply: vsThread.canReply !== false, turnId: p.turnId, range: { start: off.start, end: off.end }, }); diff --git a/test/e2e/suite/proposals.test.ts b/test/e2e/suite/proposals.test.ts index 0ba7c9c..19d4e61 100644 --- a/test/e2e/suite/proposals.test.ts +++ b/test/e2e/suite/proposals.test.ts @@ -74,6 +74,7 @@ suite("F4 propose/accept (host E2E — programmatic ingress, no LLM)", () => { assert.strictEqual(rendered.length, 1); assert.strictEqual(rendered[0].pending, true); assert.strictEqual(rendered[0].turnId, "turn-p1"); + assert.strictEqual(rendered[0].canReply, false, "proposals are decide-only — no dead reply input (INV-12)"); const art = readSidecar(); assert.strictEqual(art.proposals.length, 1, "proposal persisted at propose time"); assert.strictEqual(art.anchors[art.proposals[0].anchorId].fingerprint.text, TARGET);