Skip to content

feat(undo): add optional undo/redo history size limit and disable buttons when unavailable` - #6146

Open
rleisti wants to merge 14 commits into
OHIF:masterfrom
rleisti:feat/undo-redo-limit
Open

rleisti wants to merge 14 commits into
OHIF:masterfrom
rleisti:feat/undo-redo-limit

Conversation

@rleisti

@rleisti rleisti commented Jul 15, 2026 •

Copy link
Copy Markdown

Context

When editing segmentations, each labelmap edit records an undo memo that holds full labelmap buffers. Over a long session the undo/redo history can therefore grow large enough to cause memory pressure / out-of-memory or buffer-allocation failures, especially with large volumes.

Today the history size is fixed at Cornerstone's internal default and cannot be tuned by a deployment. This PR adds an optional maxUndoRedoCacheSize app config so integrators working with large data can bound the history. It is opt-in: when the option is not set, Cornerstone's existing default is used, so there is no behavior change for current users.

While in this area, it also fixes a small UX papercut: the header Undo/Redo buttons were always clickable, so clicking them with an empty history did nothing and made the app feel broken.

This contribution has been donated by the University of Calgary.

Changes & Results

  • New app config option maxUndoRedoCacheSize (platform/core/src/types/AppTypes.ts): optional number on AppTypes.Config. When unset, the Cornerstone default is used.
  • Apply the option on startup (extensions/cornerstone/src/init.tsx): when appConfig.maxUndoRedoCacheSize is provided, set csUtilities.HistoryMemo.DefaultHistoryMemo.size accordingly.
  • Disable the header Undo/Redo buttons when nothing is available (extensions/default/src/ViewerLayout/ViewerHeader.tsx): the buttons are now disabled (greyed out) based on canUndo / canRedo.
  • New useUndoRedoState hook (extensions/default/src/ViewerLayout/useUndoRedoState.ts): tracks DefaultHistoryMemo.canUndo/canRedo and re-evaluates on the relevant Cornerstone events (HISTORY_UNDO/HISTORY_REDO, plus ANNOTATION_COMPLETED/ANNOTATION_REMOVED/SEGMENTATION_DATA_MODIFIED, since a new memo can be pushed without emitting a history event).
  • Docs (platform/docs/docs/configuration/configurationFiles.md): document the new maxUndoRedoCacheSize option.

Effects:

  • Before: undo/redo history size was not configurable; Undo/Redo buttons were always enabled even with an empty history.
  • After: deployments can optionally cap the history via config; Undo/Redo buttons grey out when there is nothing to undo/redo. No change to default behavior when the option is unset.

Testing

The Undo button and the Redo button (no configuration):

  1. Open a study in the viewer.
  2. Make sure that the Undo button and the Redo button in the header are disabled (grey).
  3. Draw a measurement, or paint a segmentation. Make sure that the Undo button becomes enabled.
  4. Click Undo until the history is empty. Make sure that the Undo button becomes disabled and the Redo button becomes enabled.
  5. Draw a new measurement. Make sure that the Redo button becomes disabled.
  6. Drag a handle of an existing measurement, and release the mouse button. Make sure that the Undo button is enabled and the Redo button is disabled.

The history size limit (optional):

The dev servers (pnpm dev, pnpm dev:fast) load platform/app/public/config/dev.js. A production build loads config/default.js. To use a different file, set APP_CONFIG, for example APP_CONFIG=config/default.js pnpm dev:fast.

The viewer applies the value when a mode opens. Therefore, open a study before each check in the console.

  1. In config/dev.js, add this entry to the customizationService array (the legacy form):
    { 'cornerstone.maxUndoRedoCacheSize': { $set: 5 } },
  2. Reload the viewer, and open a study. In the console, make sure that window.cornerstone.utilities.HistoryMemo.DefaultHistoryMemo.size returns 5.
  3. Do 6 or more edits, then click Undo until the Undo button becomes disabled. Make sure that only the 5 most recent edits undo.
  4. Replace the customizationService array with the phased form. Then do step 2 again:
    customizationService: {
      global: {
        'cornerstone.maxUndoRedoCacheSize': { $set: 5 },
      },
    },
  5. Set the value to 0. Reload the viewer, and open a study. Make sure that DefaultHistoryMemo.size returns 50.
  6. Set the value to 10.5. Reload the viewer, and open a study. Make sure that the console shows RangeError: Invalid array length. The mode entry throws this error for an invalid value.
  7. Remove the customization, reload the viewer, and open a study. Make sure that DefaultHistoryMemo.size returns 50, the Cornerstone default. This result shows that the default behavior does not change.

The test steps do not use window.config.maxUndoRedoCacheSize. The option is a customization, and it is not a top-level appConfig key.

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: Ubuntu 22.04.5 LTS
  • Node version: 22.22.2
  • Browser: Firefox 140.12.0esr (64-bit)

Summary by CodeRabbit

  • New Features
    • Undo and Redo controls now reflect whether undo/redo is available, with improved accessibility labeling.
    • Undo/redo button state updates automatically as editing history changes.
    • Added opt-in configurability for undo/redo history retention via cornerstone.maxUndoRedoCacheSize.
  • Bug Fixes
    • Invalid or non-finite history cache settings are ignored to preserve default behavior.
  • Documentation
    • Documented the cornerstone.maxUndoRedoCacheSize configuration option and what happens when it’s unset.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@netlify

netlify Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

❌ Deploy Preview for ohif-dev failed. Why did it fail? →

Name Link
🔨 Latest commit 79b6855
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6abd833de341720008534e57

@rleisti

rleisti commented Jul 15, 2026

Copy link
Copy Markdown
Author

@wayfarer3130 @jbocce please review if you have time

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Undo/redo history size is now configurable through Cornerstone customizations. A React hook tracks history availability, and the viewer header disables unavailable Undo and Redo controls while adding accessible labels.

Changes

Undo/redo history controls

Layer / File(s) Summary
History size configuration
extensions/cornerstone/src/types/AppTypes.ts, extensions/cornerstone/src/init.tsx, platform/docs/docs/configuration/configurationFiles.md
Adds the typed cornerstone.maxUndoRedoCacheSize customization, applies valid non-negative numeric values during initialization, and documents the option.
History availability synchronization
extensions/default/src/ViewerLayout/useUndoRedoState.ts
Adds a hook that reads DefaultHistoryMemo, synchronizes after relevant tool events, defers reads until updates settle, and cleans up listeners and timers.
Undo and Redo header controls
extensions/default/src/ViewerLayout/ViewerHeader.tsx
Uses the hook to disable unavailable actions and adds aria-label attributes to both controls.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CornerstoneToolEvents
  participant useUndoRedoState
  participant DefaultHistoryMemo
  participant ViewerHeader
  CornerstoneToolEvents->>useUndoRedoState: notify history-changing event
  useUndoRedoState->>DefaultHistoryMemo: read availability after timeout
  DefaultHistoryMemo-->>useUndoRedoState: return canUndo and canRedo
  useUndoRedoState-->>ViewerHeader: provide undo/redo state
Loading

Possibly related PRs

  • OHIF/Viewers#6133: Adds typed customization-service access used by the new Cornerstone customization key.

Suggested reviewers: sedghi, wayfarer3130

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the two main changes and follows the semantic-release format.
Description check ✅ Passed The description includes all required sections, explains the motivation and effects, documents testing steps, and completes the checklist. It contains minor inaccuracies about the configuration type a…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread extensions/cornerstone/src/init.tsx Outdated
// Limit the undo/redo history size. Segmentation memos hold full labelmap
// buffers, so a large history can cause out-of-memory / buffer allocation
// issues. Configurable via appConfig.maxUndoRedoCacheSize.
if (appConfig.maxUndoRedoCacheSize != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just appConfig.maxUndoRedoCacheSize >=0

Comment thread platform/core/src/types/AppTypes.ts Outdated
};
useCursors?: boolean;
maxCacheSize?: number;
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be a customization now rather than a config item - it is still possible to set as a config item, but through the customization values.

@wayfarer3130 wayfarer3130 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you make maxUndoRedoCacheSize a customization setting?

@rleisti
rleisti requested a review from wayfarer3130 July 29, 2026 14:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@extensions/cornerstone/src/init.tsx`:
- Around line 162-171: Update the validation guarding the assignment to
csUtilities.HistoryMemo.DefaultHistoryMemo.size in the maxUndoRedoCacheSize
configuration flow. Accept only finite, non-negative integer values, rejecting
Infinity, NaN, and fractional values before mutating the history size.
🪄 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: d89a251e-4669-4484-a0b6-65ed38534dfa

📥 Commits

Reviewing files that changed from the base of the PR and between daccaea and fc8fac1.

📒 Files selected for processing (3)
  • extensions/cornerstone/src/init.tsx
  • extensions/cornerstone/src/types/AppTypes.ts
  • platform/docs/docs/configuration/configurationFiles.md

Comment thread extensions/cornerstone/src/init.tsx Outdated
Resolve the conflict in extensions/default/src/ViewerLayout/ViewerHeader.tsx.
The master branch moved the undo/redo buttons out of ViewerHeader and into
extensions/default/src/ViewerLayout/HeaderUndoRedo.tsx, which the new
`ohif.headerRightSide` customization renders.

This merge keeps the structure of master, and it moves the changes of this
branch into HeaderUndoRedo.tsx: the `useUndoRedoState` hook, the `disabled`
property on each button, the `aria-label` on each button, and the move of the
`cursor-pointer` class from the wrapper element to each button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wayfarer3130

Copy link
Copy Markdown
Contributor

The merge of master

I merged origin/master into this branch, and I pushed the merge. The merge had one conflict, and I resolved that conflict.

The conflict is in extensions/default/src/ViewerLayout/ViewerHeader.tsx. The master branch moved the undo button and the redo button out of ViewerHeader.tsx. The two buttons are now in extensions/default/src/ViewerLayout/HeaderUndoRedo.tsx, and the new ohif.headerRightSide customization renders that component.

I kept the structure of master, and I moved the changes of this branch into HeaderUndoRedo.tsx:

  • the useUndoRedoState hook;
  • the disabled property on each button;
  • the aria-label property on each button;
  • the cursor-pointer class, which moves from the wrapper element to each button.

Please check this resolution. The resolution is a design decision, and the resolution is not a text merge.

The review findings

1. The documented configuration does not work

extensions/cornerstone/src/init.tsx reads the customization in preRegistration.

platform/app/src/appInit.js calls loadAndApplyBootstrapCustomizations at line 121. platform/app/src/appInit.js calls registerExtensions at line 123. platform/app/src/appInit.js calls applyGlobalCustomizations() at line 129.

preRegistration runs during registerExtensions. Therefore getCustomization('cornerstone.maxUndoRedoCacheSize') returns undefined for a value in the global phase, and the limit never applies. Only the bootstrap phase gives a value to preRegistration. A mode customizations map also gives a value to preRegistration.

2. A bad value stops the start of the application

The test in extensions/cornerstone/src/init.tsx is Number.isFinite(maxUndoRedoCacheSize) && maxUndoRedoCacheSize >= 0. That test accepts three values that damage the setter.

The setter of DefaultHistoryMemo.size in @cornerstonejs/core runs this.ring = new Array(newSize):

  • The value 10.5 makes new Array throw RangeError: Invalid array length.
  • The value 1e10 makes new Array throw the same RangeError, because the maximum length of an array is 4294967295.
  • The value 0 makes position become NaN in the push method, because push computes (this.position + 1) % this._size. The line this.ring[this.position] = memo then writes to the property NaN. One labelmap buffer stays in memory forever, and canUndo stays false.

preRegistration throws the RangeError, and the application does not start. Please use Number.isInteger(value) && value >= 1, and please add a maximum value.

3. A drag of a measurement does not update the buttons

HISTORY_CHANGING_EVENTS in extensions/default/src/ViewerLayout/useUndoRedoState.ts does not contain ANNOTATION_MODIFIED.

A user drags an existing measurement. The tool pushes a memo, and the tool fires only ANNOTATION_MODIFIED. triggerAnnotationCompleted runs only for a new annotation. Therefore the undo button stays grey after the drag, and the redo button stays enabled although redoAvailable is 0.

4. The documentation puts the option in the wrong list

platform/docs/docs/configuration/configurationFiles.md adds the bullet to the list of the top-level appConfig keys. The option is a customization identifier, and the option is not a top-level key. No code reads a top-level maxUndoRedoCacheSize.

The test steps of this pull request set maxUndoRedoCacheSize at the top level, and the test steps read window.config.maxUndoRedoCacheSize. Those test steps do not test the code of this pull request.

5. The new labels have no translation keys

HeaderUndoRedo.tsx uses t('Header:Undo') and t('Header:Redo'). platform/i18n/src/locales/en-US/Header.json contains neither key. i18next returns the name of the key, so English shows the correct word. A different language also shows the English word.

A note on the findings

Finding 1, finding 2 and finding 4 all concern the same configuration path. One change can correct all three findings.

What I checked, and found correct

  • The size setter exists on DefaultHistoryMemo, and the setter is writable.
  • The deferred read with setTimeout(0) handles the brush correctly. applyActiveStrategy fires SEGMENTATION_DATA_MODIFIED before doneEditMemo() pushes the memo.
  • The labelmap memo, the annotation memo, the tracking-state memo and the segment-index memo each carry an id. Therefore undo() dispatches HISTORY_UNDO.
  • The Button component supports disabled, with disabled:opacity-50 and disabled:pointer-events-none.
  • The click methods of the Playwright page object wait for the button to become actionable.

🤖 Generated with Claude Code

@cypress

cypress Bot commented Sep 16, 2026

Copy link
Copy Markdown

Viewers    Run #6776

Run Properties:  status check passed Passed #6776  •  git commit 806b62ac9e: Merge remote-tracking branch 'origin/master' into feat/undo-redo-limit
Project Viewers
Branch Review feat/undo-redo-limit
Run status status check passed Passed #6776
Run duration 01m 47s
Commit git commit 806b62ac9e: Merge remote-tracking branch 'origin/master' into feat/undo-redo-limit
Committer Bill Wallace
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 28
View all changes introduced in this branch ↗︎

wayfarer3130 and others added 6 commits September 30, 2026 17:07
…value

The customization was read in preRegistration, before the global phase and
the legacy customizationService form are applied, so the limit never took
effect for those forms. Read it in onModeEnter instead, and again when a
global or mode customization changes, so the mode phase also applies.

Accept only an integer from 1 to 10000. The Cornerstone size setter throws a
RangeError for a fractional or too-large value, and a size of 0 makes push
write to ring[NaN]. An invalid value logs a warning and keeps the default.
The setter clears the history, so the size is assigned only when it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A drag of an existing annotation fires ANNOTATION_MODIFIED only while the
pointer moves. The tool pushes the memo on mouse up and fires no event for
it, so the buttons kept the state from before the drag. Listen for
ANNOTATION_MODIFIED, and re-read the history state after mouseup/touchend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n keys

Move cornerstone.maxUndoRedoCacheSize from the list of top-level appConfig
keys to the customization table, because no code reads a top-level key.
Add the Header:Undo and Header:Redo keys that the button labels use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the subscriptions to the global and mode customization events. The
history size comes from the customization at the time of mode entry only,
and onModeEnter applies it once, after it registers the event handlers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the applyUndoRedoCacheSize helper and its test. onModeEnter sets
DefaultHistoryMemo.size to the customization value, or to 50 when the value
is unset. An invalid value makes the Cornerstone setter throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants