CMS-241: post-accept Undo on the chat-assistant Applied banner#148
Conversation
…t theme Lime text on a near-white lime tint failed contrast when the launcher was open. Use the foreground color for the label and a stronger lime border so the active state stays legible while still reading as "on".
Adds the spec delta for CMS-241: a 6-second undo window that opens after apply succeeds, the new POST /api/v1/ai/proposals/:id/undo endpoint, the priorDraft addition to the apply response for body/frontmatter kinds, the undone audit outcome, and the keyboard/hover/tab-hidden behaviour. No implementation change in this commit.
Adds the server side of the post-accept undo window for AI proposals:
- `applyAiProposal` now returns `{ document, priorDraft? }`. For
`replace_selection`, `insert_block`, and `update_frontmatter` kinds
the function snapshots the pre-apply body and frontmatter so the
client can replay it on undo without server-side storage.
- New `applyAiProposalUndo` reverses an applied proposal per kind:
soft-delete for `create_document`, restore-from-trash for
`delete_document`, and a body/frontmatter replay for the three edit
kinds. Rejects with `AI_PROPOSAL_CONFLICT` on a concurrent edit
inside the undo window.
- New POST `/api/v1/ai/proposals/:proposalId/undo` route mounts the
undo path; authorization mirrors the action being reverted.
- Audit outcome enum gains `undone` and `undo_failed`, emitted from
the new handler.
- Content store wiring threads `restore` through to the AI module so
the `delete_document` undo path works against the database store.
Connects the existing AppliedBanner countdown to a working undo path: - `StudioAiRouteApi.undoProposal` calls the new server endpoint and surfaces `priorDraft` from the apply response in `StudioAiApplyResult`. - `AssistantProposal` gains `acceptedDocumentId`, `priorDraft`, and `postApplyDraftRevision` — all captured on apply success so undo knows which document to target and what to replay. - `assistant-context` gets an `undoProposal` action, a `mark-proposal-undone` reducer case (strip the proposal row, append a hidden side-channel turn so the model sees the reversal), and the `describeUndoForAgent` helper. - `AppliedBanner` now exposes Undo when a handler is wired AND the proposal carries the per-kind metadata. Banners register their undo callback in a panel-scoped LIFO so ⌘Z / Ctrl+Z fires the most recently still-open window. - `AssistantPanel` listens for ⌘Z / Ctrl+Z, ignores the chord unless focus is inside the panel root, and pops the top of the undo stack. Inline AI hook test stub gains an `undoProposal` no-op so the StudioAiRouteApi contract is still satisfied.
- applyAiProposal now returns priorDraft for replace_selection; asserted. - applyAiProposalUndo: create_document soft-delete, delete_document restore (and refuse-if-not-deleted), replace_selection replay, concurrent-edit conflict, missing-priorDraft guard. - POST /api/v1/ai/proposals/:id/undo: happy path emits undone audit, missing proposal body returns 400, schema hash mismatch emits undo_failed audit. - ai-route-api: undoProposal posts the right URL with documentId, priorDraft, and postApplyDraftRevision. - applied-undo-stack: LIFO trigger order, unsubscribe removes entries.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR introduces an undo window for accepted AI proposals in the Studio chat assistant. After an accept succeeds, a 6-second lime banner displays an Undo button and keyboard shortcut (⌘Z / Ctrl+Z) to reverse the change. The undo endpoint applies kind-specific reversals: soft-delete for new documents, restore for deleted documents, and snapshot replay for text edits, with conflict detection when concurrent edits occur. ChangesPost-Accept Undo Window
🎯 4 (Complex) | ⏱️ ~75 minutes Possibly Related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Four review findings from PR #148: - Require postApplyDraftRevision on the undo route for body/frontmatter kinds — the apply.ts guard only fired when the field was supplied, so a missing field would silently overwrite a concurrent edit. Returns INVALID_INPUT at the route boundary. - Thread the operated documentId into both undone and undo_failed audit records via a new optional override on buildLifecycleAudit. Without this, create_document undo records had no documentId (proposal.documentId is undefined for that kind by schema). - TurnGroup multi-proposal turns now thread onUndo into every accepted child so each one opens its own independent undo window with ⌘Z support, matching the single-card path. - Pessimistic banner: AcceptedView awaits the undo round-trip before dismissing. Pending state pauses the countdown and disables the button; failures keep the banner mounted with an inline error (no separate chat error turn) per SPEC-014. undoProposal context action now returns Promise<void> and rejects with the underlying error so the banner can render it.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/modules/core.ai/src/server/apply.ts`:
- Around line 480-509: When handling body/frontmatter undos in
applyAiProposalUndo, enforce that input.postApplyDraftRevision is provided:
after confirming input.priorDraft (and before fetching existing), add a guard
that if proposal.kind === "body" || proposal.kind === "frontmatter" and typeof
input.postApplyDraftRevision !== "number" then throw aiOutputInvalid (include
proposalId and documentId) so the core logic refuses to replay undos without an
explicit postApplyDraftRevision.
In `@packages/modules/core.ai/src/server/routes.ts`:
- Around line 949-975: The handler currently trusts the request body documentId
separately from proposal.documentId; add a validation after parsedProposal is
set and before authorization (i.e., before calling options.authorize) that for
any proposal.kind !== "create_document" the request body documentId (the
variable used to identify the target doc) must strictly equal
proposal.documentId, and if not throw invalidInput (or the same error shape used
elsewhere) indicating the mismatched documentId and include proposal.kind;
ensure this check lives alongside the existing postApplyDraftRevision validation
so it prevents undo operations against the wrong document for edit/delete kinds.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 458a5062-4abb-451a-81af-54a1d2a3fcd5
📒 Files selected for processing (19)
.changeset/swift-seas-press.mdapps/server/src/lib/runtime-with-modules.tsdocs/specs/SPEC-014-ai-assisted-studio-editing.mdpackages/modules/core.ai/src/index.tspackages/modules/core.ai/src/server/apply.test.tspackages/modules/core.ai/src/server/apply.tspackages/modules/core.ai/src/server/audit.tspackages/modules/core.ai/src/server/routes.test.tspackages/modules/core.ai/src/server/routes.tspackages/studio/src/lib/ai-route-api.test.tspackages/studio/src/lib/ai-route-api.tspackages/studio/src/lib/runtime-ui/components/assistant/applied-undo-stack.test.tspackages/studio/src/lib/runtime-ui/components/assistant/applied-undo-stack.tspackages/studio/src/lib/runtime-ui/components/assistant/assistant-context.tsxpackages/studio/src/lib/runtime-ui/components/assistant/assistant-launcher.tsxpackages/studio/src/lib/runtime-ui/components/assistant/assistant-panel.tsxpackages/studio/src/lib/runtime-ui/components/assistant/assistant-types.tspackages/studio/src/lib/runtime-ui/components/assistant/proposal-card.tsxpackages/studio/src/lib/runtime-ui/hooks/use-inline-ai-transform.test.ts
- applyAiProposalUndo now refuses body/frontmatter undo when postApplyDraftRevision is missing, not just when it disagrees with the live revision. The route already enforces this, but direct callers of the exported function were able to skip the guard and silently overwrite concurrent edits. - The undo route now rejects requests where the body documentId diverges from the proposal's target document (every kind except create_document, which has no documentId by schema). Without this a caller could replay a captured priorDraft onto an unrelated document they happen to have write access to. Tests added for both guards.
Summary
Wires functional Undo onto the 6-second "Applied" banner the chat assistant renders in place of the proposal card after Accept succeeds. Closes CMS-241.
Today the banner ships visual scaffolding only — the Undo button is suppressed because cosmetically clearing the accepted state without server-side rollback would lie. This PR adds the real revert paths for all five proposal kinds and turns the button on.
What lands
Spec —
docs/specs/SPEC-014priorDraft: { body, frontmatter }for body/frontmatter mutating kinds.POST /api/v1/ai/proposals/:proposalId/undo.outcomeenum gainsundone(and anundo_failedcounterpart on the failure audit path).Server —
core.aiapplyAiProposalnow returns{ document, priorDraft? }. Forreplace_selection/insert_block/update_frontmatterthe function snapshots the pre-apply body and frontmatter so the client can replay it on undo without server-side state.applyAiProposalUndoreverses an applied proposal per kind:create_document→ soft-delete the newly created documentdelete_document→ restore from trashAI_PROPOSAL_CONFLICTon a concurrent edit inside the undo window.POST /api/v1/ai/proposals/:proposalId/undoroute mounts the undo path; authorization mirrors the action being reverted (content:deletefor create-doc undo,content:writeotherwise).outcome: "undone"on success,"undo_failed"with the underlying error code on failure.restorethrough to the AI module so delete_document undo works against the database store.Studio
StudioAiRouteApi.undoProposalcalls the new endpoint;StudioAiApplyResultnow surfacespriorDraftfor body/frontmatter kinds.AssistantProposalgainsacceptedDocumentId,priorDraft, andpostApplyDraftRevision, all captured on apply success.assistant-contextgains anundoProposalaction, amark-proposal-undonereducer case (strip the proposal row, append a hidden side-channel turn so the model sees the reversal), anddescribeUndoForAgent.AppliedBannernow exposes Undo when a handler is wired AND the proposal carries the per-kind metadata. Banners register their undo callback in a panel-scoped LIFO so ⌘Z / Ctrl+Z fires the most recently still-open window.AssistantPanellistens for ⌘Z / Ctrl+Z, ignores the chord unless focus is inside the panel root, and pops the top of the undo stack.Tests
applyAiProposalUndoper kind + concurrent-edit conflict + missing-priorDraft guard.POST /api/v1/ai/proposals/:id/undohappy path / 400 missing body / schema-hash mismatch withundo_failedaudit.applyAiProposalhappy-path now assertspriorDraftis present.StudioAiRouteApi.undoProposalposts to the right URL withpriorDraft+postApplyDraftRevision.applied-undo-stackLIFO + unsubscribe.Changeset:
@mdcms/studiominor,@mdcms/sharedpatch.Test plan
bun run check— build + typecheck passbun test packages/modules/core.ai— 43 / 43 pass (10 existing + 6 new undo unit + new route tests)bun test packages/studio/src/lib/ai-route-api.test.ts ./src/lib/runtime-ui/components/assistant/applied-undo-stack.test.ts— passcreate_documentproposal → click Undo within 6s → verify document soft-deletes and the banner dismissesreplace_selectionproposal → press ⌘Z while focus is inside the panel → verify body reverts and a hidden side-channel turn appears in subsequent conversation historyAI_PROPOSAL_CONFLICTPre-existing test failures (unchanged on this branch)
5 tests fail on
mainand continue to fail here — none touched by this PR: 4 CLI login authorize tests + 1demo:seedtest. Verified by runningbun run unitonmainbefore pushing.Summary by CodeRabbit
Release Notes
New Features
Documentation