Skip to content

CODAP-1118: implement plugin undo/redo#2388

Merged
kswenson merged 12 commits intomainfrom
CODAP-1118-plugin-undo-redo
Mar 30, 2026
Merged

CODAP-1118: implement plugin undo/redo#2388
kswenson merged 12 commits intomainfrom
CODAP-1118-plugin-undo-redo

Conversation

@kswenson
Copy link
Copy Markdown
Member

@kswenson kswenson commented Feb 13, 2026

Summary

  • Implement the undoChangeNotice handler with all three operations: undoableActionPerformed, undoButtonPress, redoButtonPress
  • Uses a single shared-stack code path for both standalone and external modes, matching V2's mode-agnostic design
  • Plugin undo entries are created via withCustomUndoRedo — no MST state changes, just side-effect callbacks that send undoAction/redoAction messages to the originating plugin
  • Add proactive clearUndo/clearRedo notifications broadcast to all plugins when undo/redo stacks are emptied
  • Fix bug: remove undo strings from all-cases-handler delete, which was the only DI handler that incorrectly created undo entries for plugin-triggered mutations
  • Add architecture documentation (v3/doc/plugin-undo-redo-architecture.md) covering the shared-stack design, the protocol, and the five requirements plugins must satisfy
  • Analysis and implementation plan in v3/doc/plugin-undo-redo.md

Refs CODAP-1118

Test plan

  • Unit tests for all three handler operations (error cases, success, round-trip)
  • Verify undoAction/redoAction messages are sent to the correct plugin tile via broadcastMessage
  • Verify proactive clearUndo/clearRedo notifications are broadcast when stacks are emptied
  • Manual testing with Building Models / SageModeler to validate the shared-stack approach handles interleaving correctly

Generated with Claude Code

@kswenson kswenson added the v3 CODAP v3 label Feb 13, 2026
@cypress
Copy link
Copy Markdown

cypress Bot commented Feb 13, 2026

codap-v3    Run #10917

Run Properties:  status check passed Passed #10917  •  git commit db32f3474f: Increment the build number
Project codap-v3
Branch Review main
Run status status check passed Passed #10917
Run duration 02m 09s
Commit git commit db32f3474f: Increment the build number
Committer github-actions[bot]
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 4
View all changes introduced in this branch ↗︎

@kswenson kswenson requested a review from bfinzer February 13, 2026 17:48
Copy link
Copy Markdown
Contributor

@bfinzer bfinzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻LGTM

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 89.39394% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.28%. Comparing base (67af473) to head (2dd000f).
⚠️ Report is 52 commits behind head on main.

Files with missing lines Patch % Lines
...interactive/handlers/undo-change-notice-handler.ts 90.74% 5 Missing ⚠️
v3/src/models/document/create-document-model.ts 83.33% 2 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (67af473) and HEAD (2dd000f). Click for more details.

HEAD has 15 uploads less than BASE
Flag BASE (67af473) HEAD (2dd000f)
cypress 16 1
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2388       +/-   ##
===========================================
- Coverage   86.35%   70.28%   -16.08%     
===========================================
  Files         768      768               
  Lines       42980    43045       +65     
  Branches    10280    10709      +429     
===========================================
- Hits        37117    30254     -6863     
- Misses       5851    12766     +6915     
- Partials       12       25       +13     
Flag Coverage Δ
cypress 39.33% <16.94%> (-30.42%) ⬇️
jest 58.68% <89.39%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Member

@scytacki scytacki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The part missing from this analysis is the potential issue with id or other state that probably won't be maintained properly when the plugin undoes or replays its actions.

For example if the plugin creates the graph and then the user modifies the graph. If the graph change is undone and then the plugin's creation of the graph is undone. If the plugin's change is then redone, it will very likely create a tile with a new id instead of using the previous id. Now when the graph change is applied it will fail.

Perhaps this kind of failure is OK, but it should be noted here. Since many of the actions plugins take are creating graphs and tables this seems like it will make the undo support not very useful.

@kswenson kswenson force-pushed the CODAP-1118-plugin-undo-redo branch from a6c1e3c to 113a41c Compare March 25, 2026 05:56
@kswenson kswenson changed the title CODAP-1118: plugin undo/redo analysis document CODAP-1118: implement plugin undo/redo Mar 25, 2026
kswenson and others added 10 commits March 24, 2026 23:03
Re-analyze V2 implementation in light of comparison with CODAP-1127 spec.
Key findings:
- V2's handleUndoChangeNotice is mode-agnostic (no standalone vs external branching)
- V3 DI handlers already bypass undo (applyModelChange without undo strings
  routes through withoutUndo), matching V2's retainUndo behavior
- V3 can use a single code path for both modes, same as V2
- Shadow counter approach (from CODAP-1127 spec) is not needed
- Flag all-cases-handler undo strings as a bug

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User-triggered CODAP actions (graph changes, table edits) create undo
entries on the same stack as plugin entries. In standalone mode, this
causes undoButtonPress to potentially undo the wrong action. Revise
analysis to recommend shadow counters for standalone mode (keeping
plugin undo separate from CODAP stack) while retaining CODAP undo
entries for external mode (where interleaving is intentional).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The interleaving of plugin and CODAP undo entries on a shared stack
is actually correct behavior — the most recent action is undone first.
Shift recommendation from shadow counters to single code path (shared
stack) for both standalone and external modes, with shadow counters
as a fallback only if plugin testing reveals issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add test confirming that an MST action producing zero patches but calling
withCustomUndoRedo creates a valid undoable history entry with working
undo/redo callbacks. This resolves the open question about whether the
history system needs extension for plugin undo/redo — it does not.
Update analysis document accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The delete operation in all-cases-handler was the only DI handler that
passed undoStringKey/redoStringKey to applyModelChange, causing it to
create undo entries for plugin-triggered mutations. All other DI handlers
omit these strings so mutations go through withoutUndo(). This aligns
the delete handler with the established pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement the three undoChangeNotice operations:
- undoableActionPerformed: creates a custom undo entry via withCustomUndoRedo
  that sends undoAction/redoAction messages back to the originating plugin
- undoButtonPress: calls document.undoLastAction()
- redoButtonPress: calls document.redoLastAction()

Uses a single shared-stack code path for both standalone and external
modes. All responses include canUndo/canRedo state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a MobX reaction in createDocumentModel that broadcasts clearUndo/
clearRedo notifications to all plugins when the undo or redo stack is
emptied (canUndo/canRedo transitions from true to false). This matches
V2 proactive notification behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use a type guard function for plugin undo/redo patch validation,
following the pattern established in data-set-undo.ts. Extract patcher
into a named constant with explicit ICustomUndoRedoPatcher type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tifications

Test that undo/redo sends the correct undoAction/redoAction message to
the originating plugin tile via broadcastMessage. Test that clearUndo
and clearRedo notifications are broadcast to all plugins when the
undo/redo stacks are emptied.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the shared-stack undo/redo architecture, the protocol between
CODAP and plugins, why DI API mutations bypass undo, and the five
requirements plugins must satisfy to participate correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Data Interactive plugin undo/redo support in V3 by recording plugin-originated undoable actions as custom history entries on CODAP’s shared undo stack, and relaying undo/redo execution back to the originating plugin.

Changes:

  • Implement undoChangeNotice notify handler for undoableActionPerformed, undoButtonPress, and redoButtonPress using a custom undo/redo patcher that sends undoAction/redoAction to the plugin tile.
  • Add a document-level MobX reaction that proactively broadcasts clearUndo/clearRedo when stacks become empty.
  • Align DI “delete all cases” behavior by removing undo/redo strings so plugin-triggered mutations don’t create undo entries; add unit tests and architecture docs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
v3/src/models/history/undo-store.test.ts Adds coverage for history entries consisting only of custom patches (pure side effects).
v3/src/models/document/create-document-model.ts Adds MobX reaction broadcasting clearUndo/clearRedo notifications to plugins.
v3/src/data-interactive/handlers/undo-change-notice-handler.ts Implements plugin undo/redo DI protocol and registers custom undo/redo patcher.
v3/src/data-interactive/handlers/undo-change-notice-handler.test.ts Adds tests for handler operations, targeted undo/redo messaging, and clear notifications.
v3/src/data-interactive/handlers/all-cases-handler.ts Removes undo/redo strings to prevent plugin-triggered “delete all cases” from creating undo entries.
v3/doc/plugin-undo-redo.md Adds detailed analysis/plan documenting the shared-stack approach and rationale.
v3/doc/plugin-undo-redo-architecture.md Adds architecture/protocol documentation and plugin requirements for correct behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread v3/src/models/document/create-document-model.ts Outdated
Comment thread v3/src/data-interactive/handlers/undo-change-notice-handler.ts Outdated
Add optional chaining on prev in undo/redo notification reaction for
defensive safety. Inline unused documentContent variable in handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eation

Add Known Limitation #4 documenting the identity stability issue when
plugin-created tiles are undone and redone: the recreated tile gets a
new ID, breaking subsequent undo entries that reference the original ID.
Note that this limitation also exists in V2 and has not manifested in
practice. Outline two complementary future improvements: ID remapping
in the undo system and optional id parameter in the DI API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kswenson
Copy link
Copy Markdown
Member Author

The part missing from this analysis is the potential issue with id or other state that probably won't be maintained properly when the plugin undoes or replays its actions.

Good point. We investigated this and confirmed the issue exists in V2 as well — graph modification commands store the component ID in closures, and redo fails if the graph has been destroyed and recreated with a new ID.

It hasn't manifested in practice because SageModeler doesn't make tile creation undoable (tile creation is fire-and-forget, and SageModeler's undo/redo operates on its own internal model, not CODAP tiles).

We've documented this as Known Limitation #4 in the architecture doc (v3/doc/plugin-undo-redo-architecture.md) with two complementary future improvements:

  1. ID remapping layer in the undo/redo patch system (general robustness)
  2. Optional id parameter in the DI API component creation (for well-behaved plugins)

Copy link
Copy Markdown
Member

@dougmartin dougmartin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👍 @emcelroy and I reviewed it over Zoom.

@kswenson kswenson merged commit b471433 into main Mar 30, 2026
11 of 12 checks passed
@kswenson kswenson deleted the CODAP-1118-plugin-undo-redo branch March 30, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v3 CODAP v3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants