Skip to content

Commit 7cccd6c

Browse files
authored
fix(trajectory): tag a pending deep-link step with the trial it arrived for (#1204)
1 parent 2d6505d commit 7cccd6c

1 file changed

Lines changed: 25 additions & 8 deletions

File tree

frontend/src/components/trajectory-viewer.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -718,8 +718,21 @@ export function TrajectoryViewer({
718718
const stepRefs = useRef<(HTMLDivElement | null)[]>([]);
719719
const stepReset = useRef<string | null>(null);
720720
// The step a #step-<id> address asked for, held here once the address has
721-
// been relieved of it (see the capture effect below).
722-
const [pendingStep, setPendingStep] = useState<number | null>(null);
721+
// been relieved of it (see the capture effect below), TAGGED WITH THE TRIAL
722+
// it arrived for. The tag is the correctness property, not the reset below:
723+
// clearing on a trial switch is a state update, so the honour effect still
724+
// runs this flush with the old step, and re-runs because `trajectory` just
725+
// changed -- handing the step being left to the trial arriving. With the
726+
// tag, honouring is conditional on the trial matching, so no ordering
727+
// between the two effects can apply a step to the wrong run.
728+
const [pendingStep, setPendingStep] = useState<{
729+
trial: string;
730+
step: number;
731+
} | null>(null);
732+
// Read by the capture effect, which mounts once and would otherwise close
733+
// over the mount-time trial for every later hashchange.
734+
const trialIdRef = useRef(trialId);
735+
trialIdRef.current = trialId;
723736

724737
// Reset expanded steps and search when switching to a different trial
725738
useEffect(() => {
@@ -763,7 +776,7 @@ export function TrajectoryViewer({
763776
const take = () => {
764777
const m = /^#step-(\d+)$/.exec(window.location.hash);
765778
if (!m) return;
766-
setPendingStep(Number(m[1]));
779+
setPendingStep({ trial: trialIdRef.current, step: Number(m[1]) });
767780
const spent = `${window.location.pathname}${window.location.search}`;
768781
window.history.replaceState(window.history.state, "", spent);
769782
};
@@ -878,19 +891,23 @@ export function TrajectoryViewer({
878891
useEffect(() => {
879892
const steps = trajectory?.steps;
880893
if (pendingStep === null || !steps?.length) return;
894+
// Not this trial's step: leave it alone. It belongs to the run the reader
895+
// left, and the reset effect clears it on the next render.
896+
if (pendingStep.trial !== trialId) return;
897+
const step = pendingStep.step;
881898
setPendingStep(null);
882-
const idx = stepIdToIndex(pendingStep);
899+
const idx = stepIdToIndex(step);
883900
if (idx >= 0) {
884901
setDeepLinkError(null);
885902
handleStepClick(idx);
886-
} else if (steps.some((s) => Number(s.step_id) === pendingStep)) {
903+
} else if (steps.some((s) => Number(s.step_id) === step)) {
887904
// Present but empty: the list never draws it, so expanding the item
888905
// would scroll to nothing.
889-
setDeepLinkError(`Step ${pendingStep} is empty and is not shown.`);
906+
setDeepLinkError(`Step ${step} is empty and is not shown.`);
890907
} else {
891-
setDeepLinkError(`Step ${pendingStep} is not in this trajectory.`);
908+
setDeepLinkError(`Step ${step} is not in this trajectory.`);
892909
}
893-
}, [pendingStep, trajectory, handleStepClick, stepIdToIndex]);
910+
}, [pendingStep, trialId, trajectory, handleStepClick, stepIdToIndex]);
894911

895912
if (isLoading) {
896913
return (

0 commit comments

Comments
 (0)