[backport cloud/1.51] fix: keep Ctrl/Cmd shortcuts from falling through to the browser - #15479
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>
🎭 Playwright: ✅ 1824 passed, 0 failed · 2 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle Size
⚡ Performance Report
Absolute values
Raw data{
"timestamp": "2026-08-20T06:32:17.600Z",
"gitSha": "01f7ab3cc5c2b6c6d992a2684acdc5b6ba2e9e6e",
"branch": "backport-15066-to-cloud-1.51",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2046.3349999999991,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.638999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 490.329,
"heapDeltaBytes": 4980856,
"heapUsedBytes": 70278168,
"domNodes": 14,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 6.763999999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2021.4859999999817,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.139999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 473.799,
"heapDeltaBytes": 5066232,
"heapUsedBytes": 70438772,
"domNodes": 22,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 7.213000000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1841.7720000000202,
"styleRecalcs": 77,
"styleRecalcDurationMs": 38.613,
"layouts": 12,
"layoutDurationMs": 3.575999999999999,
"taskDurationMs": 884.0400000000001,
"heapDeltaBytes": -18085692,
"heapUsedBytes": 47233192,
"domNodes": -281,
"jsHeapTotalBytes": 23171072,
"scriptDurationMs": 116.998,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1790.106000000037,
"styleRecalcs": 76,
"styleRecalcDurationMs": 36.163000000000004,
"layouts": 12,
"layoutDurationMs": 3.507,
"taskDurationMs": 792.2099999999999,
"heapDeltaBytes": -92752,
"heapUsedBytes": 65229872,
"domNodes": 58,
"jsHeapTotalBytes": 24907776,
"scriptDurationMs": 108.641,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1735.9519999999975,
"styleRecalcs": 31,
"styleRecalcDurationMs": 17.227,
"layouts": 6,
"layoutDurationMs": 0.7040000000000002,
"taskDurationMs": 361.44300000000004,
"heapDeltaBytes": 8357180,
"heapUsedBytes": 73552196,
"domNodes": 78,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 9.283999999999999,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1734.5390000000407,
"styleRecalcs": 31,
"styleRecalcDurationMs": 15.722,
"layouts": 6,
"layoutDurationMs": 0.629,
"taskDurationMs": 343.974,
"heapDeltaBytes": 7888204,
"heapUsedBytes": 73223348,
"domNodes": 76,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 8.550999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 605.5200000000127,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.822000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 395.678,
"heapDeltaBytes": -11789956,
"heapUsedBytes": 53614540,
"domNodes": 18,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 57.221,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 575.1589999999851,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.518,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.615,
"heapDeltaBytes": -10795348,
"heapUsedBytes": 54991840,
"domNodes": 18,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 56.128,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2039.9580000000128,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.308999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 613.671,
"heapDeltaBytes": 12904936,
"heapUsedBytes": 73194732,
"domNodes": -281,
"jsHeapTotalBytes": 3506176,
"scriptDurationMs": 16.141999999999996,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-idle",
"durationMs": 2009.0710000000627,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.345000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 601.602,
"heapDeltaBytes": 12600760,
"heapUsedBytes": 72872096,
"domNodes": -281,
"jsHeapTotalBytes": 3506176,
"scriptDurationMs": 14.181999999999997,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2222.623999999996,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.989999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1233.835,
"heapDeltaBytes": 4915172,
"heapUsedBytes": 66287032,
"domNodes": -285,
"jsHeapTotalBytes": 4222976,
"scriptDurationMs": 333.631,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2161.8330000000014,
"styleRecalcs": 70,
"styleRecalcDurationMs": 15.757,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1178.626,
"heapDeltaBytes": 10569544,
"heapUsedBytes": 71154572,
"domNodes": -284,
"jsHeapTotalBytes": 3698688,
"scriptDurationMs": 333.185,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3265.0150000000053,
"styleRecalcs": 65,
"styleRecalcDurationMs": 15.363000000000001,
"layouts": 60,
"layoutDurationMs": 7.528,
"taskDurationMs": 1437.842,
"heapDeltaBytes": -764904,
"heapUsedBytes": 61559052,
"domNodes": -287,
"jsHeapTotalBytes": 3506176,
"scriptDurationMs": 391.843,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3146.6669999999795,
"styleRecalcs": 65,
"styleRecalcDurationMs": 14.582999999999995,
"layouts": 60,
"layoutDurationMs": 7.383000000000001,
"taskDurationMs": 1382.587,
"heapDeltaBytes": -6232396,
"heapUsedBytes": 55759828,
"domNodes": -287,
"jsHeapTotalBytes": 4030464,
"scriptDurationMs": 383.15,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2046.0009999999897,
"styleRecalcs": 7,
"styleRecalcDurationMs": 6.015,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 594.133,
"heapDeltaBytes": 13196384,
"heapUsedBytes": 74135500,
"domNodes": -282,
"jsHeapTotalBytes": 2719744,
"scriptDurationMs": 14.282000000000004,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2046.7630000000554,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.564000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 582.899,
"heapDeltaBytes": 13573640,
"heapUsedBytes": 74922832,
"domNodes": -284,
"jsHeapTotalBytes": 4030464,
"scriptDurationMs": 12.951000000000004,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 592.7369999999996,
"styleRecalcs": 46,
"styleRecalcDurationMs": 9.998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 400.926,
"heapDeltaBytes": -11416364,
"heapUsedBytes": 54134148,
"domNodes": 18,
"jsHeapTotalBytes": 26214400,
"scriptDurationMs": 118.037,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 589.7219999999379,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.475999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 390.35400000000004,
"heapDeltaBytes": -11397024,
"heapUsedBytes": 53995288,
"domNodes": 20,
"jsHeapTotalBytes": 27000832,
"scriptDurationMs": 116.13399999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 1993.6830000000327,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.666000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 453.487,
"heapDeltaBytes": 5066248,
"heapUsedBytes": 70581716,
"domNodes": 20,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 6.747000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 1995.600999999965,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.669,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 430.93800000000005,
"heapDeltaBytes": 5478188,
"heapUsedBytes": 70928780,
"domNodes": 20,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 5.582999999999999,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1717.4630000000093,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.953,
"layouts": 16,
"layoutDurationMs": 4.8999999999999995,
"taskDurationMs": 793.674,
"heapDeltaBytes": -3632332,
"heapUsedBytes": 61840712,
"domNodes": 60,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 87.986,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1689.883000000009,
"styleRecalcs": 76,
"styleRecalcDurationMs": 36.222,
"layouts": 16,
"layoutDurationMs": 4.52,
"taskDurationMs": 766.147,
"heapDeltaBytes": -3788052,
"heapUsedBytes": 61907356,
"domNodes": 61,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 88.791,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1418.86199999999,
"styleRecalcs": 19,
"styleRecalcDurationMs": 31.240000000000006,
"layouts": 14,
"layoutDurationMs": 12.584000000000001,
"taskDurationMs": 989.8969999999998,
"heapDeltaBytes": -5973644,
"heapUsedBytes": 79929400,
"domNodes": 13673,
"jsHeapTotalBytes": 10223616,
"scriptDurationMs": 19.523999999999994,
"eventListeners": 2375,
"totalBlockingTimeMs": 143,
"frameDurationMs": 16.666666666666696,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8426.918000000001,
"styleRecalcs": 249,
"styleRecalcDurationMs": 38.426,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4381.505,
"heapDeltaBytes": 11263992,
"heapUsedBytes": 71528464,
"domNodes": -283,
"jsHeapTotalBytes": 3960832,
"scriptDurationMs": 1047.565,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8215.417000000001,
"styleRecalcs": 251,
"styleRecalcDurationMs": 37.427,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3995.3070000000002,
"heapDeltaBytes": 12765636,
"heapUsedBytes": 73287504,
"domNodes": -280,
"jsHeapTotalBytes": 3960832,
"scriptDurationMs": 1005.007,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 17328.79299999996,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16650.833,
"heapDeltaBytes": -26891952,
"heapUsedBytes": 170688844,
"domNodes": -8312,
"jsHeapTotalBytes": -13045760,
"scriptDurationMs": 119.63400000000001,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 16686.480000000076,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15761.307999999999,
"heapDeltaBytes": -44473116,
"heapUsedBytes": 165959288,
"domNodes": -8312,
"jsHeapTotalBytes": -15405056,
"scriptDurationMs": 102.36799999999998,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 20409.958000000017,
"styleRecalcs": 159,
"styleRecalcDurationMs": 15.654999999999974,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 19804.041999999998,
"heapDeltaBytes": -40424164,
"heapUsedBytes": 170433884,
"domNodes": -8312,
"jsHeapTotalBytes": -13082624,
"scriptDurationMs": 411.048,
"eventListeners": -16381,
"totalBlockingTimeMs": 28,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 20860.184000000005,
"styleRecalcs": 178,
"styleRecalcDurationMs": 17.72299999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20285.723,
"heapDeltaBytes": -20973468,
"heapUsedBytes": 176264248,
"domNodes": -8312,
"jsHeapTotalBytes": -14426112,
"scriptDurationMs": 424.27700000000004,
"eventListeners": -16385,
"totalBlockingTimeMs": 52,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 476.7940000000408,
"styleRecalcs": 17,
"styleRecalcDurationMs": 23.28,
"layouts": 4,
"layoutDurationMs": 1.639,
"taskDurationMs": 120.512,
"heapDeltaBytes": 5068632,
"heapUsedBytes": 69645912,
"domNodes": 150,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 7.355,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 494.4830000000593,
"styleRecalcs": 11,
"styleRecalcDurationMs": 17.447,
"layouts": 3,
"layoutDurationMs": 0.6119999999999999,
"taskDurationMs": 126.16,
"heapDeltaBytes": 4925088,
"heapUsedBytes": 69324480,
"domNodes": 119,
"jsHeapTotalBytes": 4718592,
"scriptDurationMs": 6.696000000000001,
"eventListeners": 97,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
}
]
} |
|
🔗 Commit SHA: 92aaa2c | Docs | View more details | Give us feedback! |
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## cloud/1.51 #15479 +/- ##
=============================================
Coverage ? 81.48%
=============================================
Files ? 1883
Lines ? 117536
Branches ? 37189
=============================================
Hits ? 95771
Misses ? 21289
Partials ? 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
cloud/1.51Automatically created by backport workflow.