[backport core/1.51] fix: keep Ctrl/Cmd shortcuts from falling through to the browser - #15478
Conversation
) ## Summary Ctrl+S sometimes opened the browser's own "save page" dialog instead of saving the workflow. Root cause: the global keybinding handler's modal gate returns before `event.preventDefault()`, so any combo the app owns is handed back to the browser whenever a dialog is open. `src/platform/keybindings/keybindingService.ts:53-57` ```ts if (isModalOpen(dialogStore.dialogStack.length)) { return // <-- before event.preventDefault() on line 57 } event.preventDefault() ``` `isModalOpen` is true for any managed dialog, any `[role="dialog"][aria-modal="true"]`, any open Reka dialog, any `<dialog open>`, and any visible `.comfy-modal` — so this fires often, and intermittently, which is why the report is "Ctrl+S doesn't *always* save". ## Changes - **What (Before/After)**: when the modal gate declines to run a keybinding, the browser default is now suppressed for Ctrl/Cmd combos. Before: browser save-page dialog. After: no-op. Bare keys are untouched, so typing inside a dialog still works — that is what the gate was added to protect (#12184), and the existing tests asserting `preventDefault` is *not* called for bare-key bindings still pass unchanged. - **Breaking**: none. - **Dependencies**: none. ## Review Focus - Scope of the guard is `keyCombo.ctrl`, which `KeyComboImpl.fromEvent` sets from `ctrlKey || metaKey`, so Cmd on macOS is covered. - Combos in `RESERVED_BY_TEXT_INPUT` (Ctrl+A/C/V/X/Z/…) already return earlier when focus is in an input, so this does not intercept text editing. - **Deliberately out of scope**: whether Ctrl+S should *save* while a dialog is open. That is a product decision (re-entrancy against the Save-As prompt is real), and this PR only stops the browser from hijacking the combo. Tracked separately. ## How - `keybindingService.ts`: suppress the default for Ctrl/Cmd combos inside the modal-gate early return. - `keybindingService.dialog.test.ts`: failing repro committed first (`test:` commit), then the fix — the diff proves the bug existed. - `browser_tests/tests/defaultKeybindings.spec.ts`: e2e opens the Save-As dialog with Ctrl+S, then asserts a second Ctrl+S is `defaultPrevented`. - Fixes FE-1188 (https://linear.app/comfyorg/issue/FE-1188/bug-ctrls-triggers-browser-save-as-html-instead-of-comfyui-native-save) - Part of FE-1586 (reported by Pablo in #comfy-creatives, 2026-08-11: "When I hit ctrl+S it doesnt always save") --------- Co-authored-by: Connor Byrne <c.byrne@comfy.org> Co-authored-by: Alexander Brown <drjkl@comfy.org>
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1820 passed, 0 failed · 4 flaky📊 Browser Reports
📦 Bundle Size
⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-20T06:32:40.380Z",
"gitSha": "e6684dd7e196eb79fa2d9e090a21c1849749504f",
"branch": "backport-15066-to-core-1.51",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2031.9700000000012,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.640000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 453.931,
"heapDeltaBytes": 5177200,
"heapUsedBytes": 70599264,
"domNodes": 18,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 6.711000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2046.779000000015,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.812000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 448.62,
"heapDeltaBytes": 5085828,
"heapUsedBytes": 70497396,
"domNodes": 18,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 6.796,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2080.155999999988,
"styleRecalcs": 80,
"styleRecalcDurationMs": 53.019999999999996,
"layouts": 12,
"layoutDurationMs": 3.766,
"taskDurationMs": 1068.293,
"heapDeltaBytes": -18626364,
"heapUsedBytes": 46700316,
"domNodes": 63,
"jsHeapTotalBytes": 23957504,
"scriptDurationMs": 127.09199999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1809.1759999999795,
"styleRecalcs": 76,
"styleRecalcDurationMs": 35.829,
"layouts": 12,
"layoutDurationMs": 3.202,
"taskDurationMs": 867.5310000000001,
"heapDeltaBytes": -19300612,
"heapUsedBytes": 46232104,
"domNodes": 5,
"jsHeapTotalBytes": 22908928,
"scriptDurationMs": 112.371,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1727.3050000000012,
"styleRecalcs": 30,
"styleRecalcDurationMs": 18.328000000000003,
"layouts": 6,
"layoutDurationMs": 0.7679999999999998,
"taskDurationMs": 389.334,
"heapDeltaBytes": 7691160,
"heapUsedBytes": 73079736,
"domNodes": 75,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 11.345,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1737.594999999942,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.820999999999998,
"layouts": 6,
"layoutDurationMs": 0.7260000000000001,
"taskDurationMs": 386.235,
"heapDeltaBytes": 7811752,
"heapUsedBytes": 73086308,
"domNodes": 77,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 10.618999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 563.2339999999658,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.235000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 374.39199999999994,
"heapDeltaBytes": -10815500,
"heapUsedBytes": 55067616,
"domNodes": 18,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 56.079,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 588.9400000000933,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.856,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 391.752,
"heapDeltaBytes": -11850708,
"heapUsedBytes": 53579392,
"domNodes": 18,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 58.616,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2008.8230000000067,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.294000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 597.389,
"heapDeltaBytes": 9420000,
"heapUsedBytes": 69787064,
"domNodes": -283,
"jsHeapTotalBytes": 2981888,
"scriptDurationMs": 15.719999999999997,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2051.7489999999725,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.668999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 618.2940000000001,
"heapDeltaBytes": 12684688,
"heapUsedBytes": 72453396,
"domNodes": -283,
"jsHeapTotalBytes": 3506176,
"scriptDurationMs": 14.936000000000005,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2143.9260000000218,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.561,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1200.244,
"heapDeltaBytes": 7927208,
"heapUsedBytes": 68703764,
"domNodes": -284,
"jsHeapTotalBytes": 3698688,
"scriptDurationMs": 336.80100000000004,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2140.6880000000683,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.234,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1206.329,
"heapDeltaBytes": -6724344,
"heapUsedBytes": 54818336,
"domNodes": -283,
"jsHeapTotalBytes": 4292608,
"scriptDurationMs": 330.23199999999997,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3281.8829999999934,
"styleRecalcs": 64,
"styleRecalcDurationMs": 14.672,
"layouts": 60,
"layoutDurationMs": 7.362,
"taskDurationMs": 1432.824,
"heapDeltaBytes": -1243572,
"heapUsedBytes": 61565140,
"domNodes": -286,
"jsHeapTotalBytes": 3506176,
"scriptDurationMs": 399.026,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3232.2849999999335,
"styleRecalcs": 64,
"styleRecalcDurationMs": 14.161,
"layouts": 60,
"layoutDurationMs": 7.142999999999999,
"taskDurationMs": 1399.752,
"heapDeltaBytes": -1256024,
"heapUsedBytes": 61488952,
"domNodes": -287,
"jsHeapTotalBytes": 2981888,
"scriptDurationMs": 379.28999999999996,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2013.7420000000361,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.317999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 588.707,
"heapDeltaBytes": 12378860,
"heapUsedBytes": 72952632,
"domNodes": -283,
"jsHeapTotalBytes": 3506176,
"scriptDurationMs": 13.415999999999997,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2006.4760000000206,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.454999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 611.642,
"heapDeltaBytes": 12298408,
"heapUsedBytes": 73800964,
"domNodes": -286,
"jsHeapTotalBytes": 3244032,
"scriptDurationMs": 15.869999999999996,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 608.2770000000437,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.78,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 407.59499999999997,
"heapDeltaBytes": -11333420,
"heapUsedBytes": 54052656,
"domNodes": 20,
"jsHeapTotalBytes": 26214400,
"scriptDurationMs": 121.74499999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 622.1590000000106,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.76,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 424.59000000000003,
"heapDeltaBytes": -10184140,
"heapUsedBytes": 55582800,
"domNodes": 20,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 128.737,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 1996.5700000000197,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.57,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 433.77199999999993,
"heapDeltaBytes": 5389648,
"heapUsedBytes": 70727536,
"domNodes": 18,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 6.303,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2020.9220000000414,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.854,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 473.084,
"heapDeltaBytes": 5370948,
"heapUsedBytes": 71229280,
"domNodes": 22,
"jsHeapTotalBytes": 23592960,
"scriptDurationMs": 6.694999999999999,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1698.0649999999855,
"styleRecalcs": 77,
"styleRecalcDurationMs": 35.439,
"layouts": 16,
"layoutDurationMs": 3.9960000000000004,
"taskDurationMs": 743.7479999999999,
"heapDeltaBytes": -3617876,
"heapUsedBytes": 61833804,
"domNodes": 66,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 83.01400000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1722.5650000000314,
"styleRecalcs": 79,
"styleRecalcDurationMs": 41.769,
"layouts": 16,
"layoutDurationMs": 4.592,
"taskDurationMs": 812.366,
"heapDeltaBytes": -3612568,
"heapUsedBytes": 61776768,
"domNodes": 68,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 89.152,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-transition-enter",
"durationMs": 1400.1430000000141,
"styleRecalcs": 19,
"styleRecalcDurationMs": 30.942999999999998,
"layouts": 14,
"layoutDurationMs": 12.803000000000003,
"taskDurationMs": 945.8629999999999,
"heapDeltaBytes": -5976808,
"heapUsedBytes": 79361504,
"domNodes": 13673,
"jsHeapTotalBytes": 10223616,
"scriptDurationMs": 16.725999999999992,
"eventListeners": 2375,
"totalBlockingTimeMs": 143,
"frameDurationMs": 16.666666666666696,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8355.33399999997,
"styleRecalcs": 249,
"styleRecalcDurationMs": 38.462,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4239.506,
"heapDeltaBytes": 13230684,
"heapUsedBytes": 73282876,
"domNodes": -282,
"jsHeapTotalBytes": 3960832,
"scriptDurationMs": 1034.2420000000002,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8318.143000000076,
"styleRecalcs": 248,
"styleRecalcDurationMs": 35.416000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4182.726,
"heapDeltaBytes": -4664100,
"heapUsedBytes": 55575980,
"domNodes": -282,
"jsHeapTotalBytes": 4292608,
"scriptDurationMs": 1016.1520000000002,
"eventListeners": -163,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 17274.103000000025,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16629.340999999997,
"heapDeltaBytes": -39823132,
"heapUsedBytes": 171059376,
"domNodes": -8316,
"jsHeapTotalBytes": -13012992,
"scriptDurationMs": 119.687,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 17468.75,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16668.99,
"heapDeltaBytes": -39775964,
"heapUsedBytes": 170611356,
"domNodes": -8312,
"jsHeapTotalBytes": -11997184,
"scriptDurationMs": 109.24,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 20421.45499999998,
"styleRecalcs": 172,
"styleRecalcDurationMs": 17.52300000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 19954.943999999996,
"heapDeltaBytes": -32775332,
"heapUsedBytes": 164432284,
"domNodes": -8312,
"jsHeapTotalBytes": -13570048,
"scriptDurationMs": 422.285,
"eventListeners": -16383,
"totalBlockingTimeMs": 51,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 21133.445999999934,
"styleRecalcs": 177,
"styleRecalcDurationMs": 20.136000000000042,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20608.474,
"heapDeltaBytes": -19927456,
"heapUsedBytes": 176205144,
"domNodes": -8312,
"jsHeapTotalBytes": -13639680,
"scriptDurationMs": 425.68800000000005,
"eventListeners": -16385,
"totalBlockingTimeMs": 147,
"frameDurationMs": 17.779999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 475.47000000008666,
"styleRecalcs": 18,
"styleRecalcDurationMs": 25.68,
"layouts": 4,
"layoutDurationMs": 1.605,
"taskDurationMs": 123.70700000000001,
"heapDeltaBytes": 5133084,
"heapUsedBytes": 69617336,
"domNodes": 148,
"jsHeapTotalBytes": 5505024,
"scriptDurationMs": 8.841,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 482.4019999999791,
"styleRecalcs": 10,
"styleRecalcDurationMs": 26.008000000000003,
"layouts": 3,
"layoutDurationMs": 0.7010000000000001,
"taskDurationMs": 127.91199999999999,
"heapDeltaBytes": 4926468,
"heapUsedBytes": 69460064,
"domNodes": 119,
"jsHeapTotalBytes": 4718592,
"scriptDurationMs": 7.276,
"eventListeners": 97,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## core/1.51 #15478 +/- ##
==========================================
Coverage 81.48% 81.48%
==========================================
Files 1883 1883
Lines 117534 117536 +2
Branches 36717 36718 +1
==========================================
+ Hits 95769 95771 +2
Misses 21289 21289
Partials 476 476
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Backport of #15066 to
core/1.51Automatically created by backport workflow.