Skip to content

Commit 6cdd881

Browse files
samejrclaude
andauthored
fix(webapp): Fix for resizable side panel getting stuck at its min-size (#3538)
## Summary - Run-view inspector panel was glitching out on Firefox: visual flicker on close, locking up at min size, and intermittent `panelHasSpace` invariant errors. Root cause is the underlying `react-window-splitter` library's collapse animation, which uses `@react-spring/rafz` and interacts poorly with Firefox. - Disabled the library's collapse animation on Firefox only, app-wide (every consumer of `RESIZABLE_PANEL_ANIMATION`). Chromium and Safari behaviour is unchanged. ## Changes - **Firefox animation skip** in `RESIZABLE_PANEL_ANIMATION` — UA-detected at module load, resolves to `undefined` for Firefox so the library's animation actor completes in one frame instead of running its rAF loop. - **Inspector min raised 50px → 250px** so dragging can't shrink the panel into a near-useless width. - **`autosaveId` bumped `v2` → `v3`** to invalidate stale persisted snapshots (the library has a `// TODO` branch that ignores prop changes for already-registered panels, so existing users would otherwise still see the old 50px min). - **`react-window-splitter` pinned** to exact `0.4.1` to protect the patch from drifting if line offsets change in a patch release. - **Two hunks added to the existing `@window-splitter/state` patch:** - Removed the library's auto-collapse-on-drag block entirely. Every collapsible panel in the app is parent-controlled, and that block was triggering state-machine deadlocks when handlers were no-ops. Drag-to-collapse is now disabled across the app; collapse is only triggered explicitly (close button, ESC, URL change, etc.). - In `getDeltaForEvent`, fall back to the panel's `default` before its `min` when expanding — so the first ever click on a span opens the inspector at 500px, not 250px. ## Local testing confirmed - [x] Firefox: open a run, click various spans → panel opens instantly at 500px, drags freely between 250px and max, closes instantly to 0. No console errors. - [x] Chrome/Chromium: same flow, but with smooth open/close animation as before. - [x] Safari: same as Chrome. - [x] Reload mid-session → panel restores cleanly to the dragged size. - [x] Other resizable panels in the app (logs, deployments, schedules, batches, bulk-actions, runs index) still animate on Chromium/Safari. ## Notes - Linear: TRI-8584 - Branch contains intermediate commits exploring an unsuccessful snapshot-validator approach; they're reverted by the final commit. Cumulative diff is 6 files. Squash on merge if you'd prefer a clean history. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ead1e5a commit 6cdd881

9 files changed

Lines changed: 521 additions & 1801 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fix the run-view inspector panel glitching out and locking up in Firefox. Disabled the underlying resizable library's collapse animation on Firefox (where its `requestAnimationFrame`-driven actor caused visual glitches and intermittent state-machine errors) while keeping it intact for Chromium and Safari, and bumped the inspector minimum from 50px to 250px so dragging can't shrink the panel into a near-useless width.

apps/webapp/app/components/primitives/Resizable.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import React, { useRef } from "react";
4-
import { PanelGroup, Panel, PanelResizer } from "react-window-splitter";
4+
import { PanelGroup, Panel, PanelResizer } from "@window-splitter/react";
55
import { cn } from "~/utils/cn";
66

77
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof PanelGroup>) => (
@@ -69,10 +69,14 @@ const ResizableHandle = ({
6969
</PanelResizer>
7070
);
7171

72-
const RESIZABLE_PANEL_ANIMATION = {
73-
easing: "ease-in-out" as const,
74-
duration: 200,
75-
};
72+
// react-window-splitter drives the collapse animation through @react-spring/rafz,
73+
// which has timing/interaction issues with Firefox that produce visual glitches
74+
// (alternating frames, panels stuck at min, panelHasSpace invariant violations).
75+
// Disable the animation on Firefox; it works correctly in Chromium and Safari.
76+
const RESIZABLE_PANEL_ANIMATION =
77+
typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent)
78+
? undefined
79+
: ({ easing: "ease-in-out", duration: 300 } as const);
7680

7781
const COLLAPSIBLE_HANDLE_CLASSNAME = "transition-opacity duration-200";
7882

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { DiscordIcon } from "@trigger.dev/companyicons";
1515
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
1616
import type { TaskRunStatus } from "@trigger.dev/database";
1717
import { Fragment, Suspense, useCallback, useEffect, useRef, useState } from "react";
18-
import type { PanelHandle } from "react-window-splitter";
18+
import type { PanelHandle } from "@window-splitter/react";
1919
import { Bar, BarChart, ResponsiveContainer, Tooltip, type TooltipProps } from "recharts";
2020
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
2121
import { ExitIcon } from "~/assets/icons/ExitIcon";

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectP
115115

116116
const resizableSettings = {
117117
parent: {
118-
autosaveId: "panel-run-parent-v2",
118+
autosaveId: "panel-run-parent-v3",
119119
handleId: "parent-handle",
120120
main: {
121121
id: "run",
@@ -124,7 +124,7 @@ const resizableSettings = {
124124
inspector: {
125125
id: "inspector",
126126
default: "500px" as const,
127-
min: "50px" as const,
127+
min: "250px" as const,
128128
},
129129
},
130130
tree: {

apps/webapp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
"@upstash/ratelimit": "^1.1.3",
136136
"@vercel/sdk": "^1.19.1",
137137
"@whatwg-node/fetch": "^0.9.14",
138+
"@window-splitter/react": "1.1.3",
138139
"ai": "^4.3.19",
139140
"assert-never": "^1.2.1",
140141
"aws4fetch": "^1.0.18",
@@ -199,7 +200,6 @@
199200
"react-resizable-panels": "^2.0.9",
200201
"react-stately": "^3.29.1",
201202
"react-use": "17.5.1",
202-
"react-window-splitter": "^0.4.1",
203203
"recharts": "^2.15.2",
204204
"regression": "^2.0.1",
205205
"remix-auth": "^3.6.0",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"@sentry/remix@9.46.0": "patches/@sentry__remix@9.46.0.patch",
8383
"@upstash/ratelimit@1.1.3": "patches/@upstash__ratelimit.patch",
8484
"antlr4ts@0.5.0-alpha.4": "patches/antlr4ts@0.5.0-alpha.4.patch",
85-
"@window-splitter/state@0.4.1": "patches/@window-splitter__state@0.4.1.patch"
85+
"@window-splitter/state@1.1.3": "patches/@window-splitter__state@1.1.3.patch"
8686
},
8787
"overrides": {
8888
"typescript": "5.5.4",

patches/@window-splitter__state@0.4.1.patch

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
diff --git a/dist/commonjs/index.js b/dist/commonjs/index.js
2+
index e3bdcf702702392e9a06c981545f659ee7c5970e..d88ae6b2dc5b4cf1970cb693f58a926bd12a8f45 100644
3+
--- a/dist/commonjs/index.js
4+
+++ b/dist/commonjs/index.js
5+
@@ -757,30 +757,14 @@ function updateLayout(context, dragEvent) {
6+
panelAfter.onCollapseChange.current(false);
7+
}
8+
}
9+
- const panelBeforeIsAboutToCollapse = panelBefore.currentValue.value.eq(getUnitPixelValue(context, panelBefore.min));
10+
- // If the panel was expanded and now is at it's min size, collapse it
11+
- if (!dragEvent.disregardCollapseBuffer &&
12+
- panelBefore.collapsible &&
13+
- panelBeforeIsAboutToCollapse) {
14+
- if (panelBefore.onCollapseChange?.current &&
15+
- panelBefore.collapseIsControlled &&
16+
- !dragEvent.controlled &&
17+
- !dragEvent.isVirtual) {
18+
- panelBefore.onCollapseChange.current(true);
19+
- return { dragOvershoot: newDragOvershoot };
20+
- }
21+
- // Make it collapsed
22+
- panelBefore.collapsed = true;
23+
- panelBeforeNewValue = getUnitPixelValue(context, panelBefore.collapsedSize);
24+
- // Add the extra space created to the before panel
25+
- panelAfterNewValue = panelAfter.currentValue.value.add(panelBeforePreviousValue.minus(panelBeforeNewValue));
26+
- if (panelBefore.onCollapseChange?.current &&
27+
- !panelBefore.collapseIsControlled &&
28+
- !dragEvent.controlled &&
29+
- !dragEvent.isVirtual) {
30+
- panelBefore.onCollapseChange.current(true);
31+
- }
32+
- }
33+
+ // Drag-to-collapse is disabled in this fork: every consumer of the
34+
+ // library uses controlled `collapsed` props and triggers collapse
35+
+ // explicitly (close button, ESC, URL change, etc.). The original auto-
36+
+ // collapse-on-drag logic that lived here would notify the parent when a
37+
+ // collapsible panel reached its min during a drag — keeping it for our
38+
+ // (controlled-only) case caused state-machine deadlocks when handlers
39+
+ // were no-ops, so the block is removed entirely. Panels just clamp at
40+
+ // `min` during drag now.
41+
panelBefore.currentValue = { type: "pixel", value: panelBeforeNewValue };
42+
panelAfter.currentValue = { type: "pixel", value: panelAfterNewValue };
43+
const leftoverSpace = new big_js_1.default(getGroupSize(context)).minus(newItems.reduce((acc, b) => acc.add(isPanelData(b) ? b.currentValue.value : b.size.value), new big_js_1.default(0)));
44+
@@ -940,7 +924,12 @@ function setCookie(name, jsonData) {
45+
function getDeltaForEvent(context, event) {
46+
const panel = getPanelWithId(context, event.panelId);
47+
if (event.type === "expandPanel") {
48+
- return new big_js_1.default(panel.sizeBeforeCollapse ?? getUnitPixelValue(context, panel.min)).minus(panel.currentValue.value);
49+
+ // Fall back to `default` before `min` so the first-ever expand of a
50+
+ // panel that started life collapsed lands at its configured default
51+
+ // size rather than getting stuck at `min`.
52+
+ const defaultPx = panel.default ? getUnitPixelValue(context, panel.default) : undefined;
53+
+ const target = panel.sizeBeforeCollapse ?? defaultPx ?? getUnitPixelValue(context, panel.min);
54+
+ return new big_js_1.default(target).minus(panel.currentValue.value);
55+
}
56+
const collapsedSize = getUnitPixelValue(context, panel.collapsedSize);
57+
return panel.currentValue.value.minus(collapsedSize);
58+
diff --git a/dist/esm/index.js b/dist/esm/index.js
59+
index f8fddd70c0f1aaed29f2fb0ca0d8093d8ce66335..d1dae8beb1447afca47b91e796b8279135f50c36 100644
60+
--- a/dist/esm/index.js
61+
+++ b/dist/esm/index.js
62+
@@ -728,30 +728,14 @@ function updateLayout(context, dragEvent) {
63+
panelAfter.onCollapseChange.current(false);
64+
}
65+
}
66+
- const panelBeforeIsAboutToCollapse = panelBefore.currentValue.value.eq(getUnitPixelValue(context, panelBefore.min));
67+
- // If the panel was expanded and now is at it's min size, collapse it
68+
- if (!dragEvent.disregardCollapseBuffer &&
69+
- panelBefore.collapsible &&
70+
- panelBeforeIsAboutToCollapse) {
71+
- if (panelBefore.onCollapseChange?.current &&
72+
- panelBefore.collapseIsControlled &&
73+
- !dragEvent.controlled &&
74+
- !dragEvent.isVirtual) {
75+
- panelBefore.onCollapseChange.current(true);
76+
- return { dragOvershoot: newDragOvershoot };
77+
- }
78+
- // Make it collapsed
79+
- panelBefore.collapsed = true;
80+
- panelBeforeNewValue = getUnitPixelValue(context, panelBefore.collapsedSize);
81+
- // Add the extra space created to the before panel
82+
- panelAfterNewValue = panelAfter.currentValue.value.add(panelBeforePreviousValue.minus(panelBeforeNewValue));
83+
- if (panelBefore.onCollapseChange?.current &&
84+
- !panelBefore.collapseIsControlled &&
85+
- !dragEvent.controlled &&
86+
- !dragEvent.isVirtual) {
87+
- panelBefore.onCollapseChange.current(true);
88+
- }
89+
- }
90+
+ // Drag-to-collapse is disabled in this fork: every consumer of the
91+
+ // library uses controlled `collapsed` props and triggers collapse
92+
+ // explicitly (close button, ESC, URL change, etc.). The original auto-
93+
+ // collapse-on-drag logic that lived here would notify the parent when a
94+
+ // collapsible panel reached its min during a drag — keeping it for our
95+
+ // (controlled-only) case caused state-machine deadlocks when handlers
96+
+ // were no-ops, so the block is removed entirely. Panels just clamp at
97+
+ // `min` during drag now.
98+
panelBefore.currentValue = { type: "pixel", value: panelBeforeNewValue };
99+
panelAfter.currentValue = { type: "pixel", value: panelAfterNewValue };
100+
const leftoverSpace = new Big(getGroupSize(context)).minus(newItems.reduce((acc, b) => acc.add(isPanelData(b) ? b.currentValue.value : b.size.value), new Big(0)));
101+
@@ -911,7 +895,12 @@ function setCookie(name, jsonData) {
102+
function getDeltaForEvent(context, event) {
103+
const panel = getPanelWithId(context, event.panelId);
104+
if (event.type === "expandPanel") {
105+
- return new Big(panel.sizeBeforeCollapse ?? getUnitPixelValue(context, panel.min)).minus(panel.currentValue.value);
106+
+ // Fall back to `default` before `min` so the first-ever expand of a
107+
+ // panel that started life collapsed lands at its configured default
108+
+ // size rather than getting stuck at `min`.
109+
+ const defaultPx = panel.default ? getUnitPixelValue(context, panel.default) : undefined;
110+
+ const target = panel.sizeBeforeCollapse ?? defaultPx ?? getUnitPixelValue(context, panel.min);
111+
+ return new Big(target).minus(panel.currentValue.value);
112+
}
113+
const collapsedSize = getUnitPixelValue(context, panel.collapsedSize);
114+
return panel.currentValue.value.minus(collapsedSize);

0 commit comments

Comments
 (0)