Skip to content

Conversation

@mcdoh
Copy link
Contributor

@mcdoh mcdoh commented Oct 27, 2025

What changes are proposed in this pull request?

Updated code to address errors logged to the dev console.

How is this patch tested? If it is not, please explain why.

  • unit tests
  • manually

Release Notes

Is this a user-facing change that should be mentioned in the release notes?

  • No. You can skip the rest of this section.
  • Yes. Give a description of this change to be included in the release
    notes for FiftyOne users.

(Details in 1-2 sentences. You can just refer to another PR with a description
if this PR is part of a larger change.)

What areas of FiftyOne does this PR affect?

  • App: FiftyOne application changes
  • Build: Build and test infrastructure changes
  • Core: Core fiftyone Python library changes
  • Documentation: FiftyOne documentation changes
  • Other

Summary by CodeRabbit

  • New Features

    • Expanded 3D annotation: polyline workflows, vertex tooltips, field selector, coordinate inputs, annotation gizmo, tips, and mock renderers for testing.
    • New form helpers and improved select/dropdown UX; ImportSchema gains disabled messaging; operator menus accept modal positioning props.
  • Bug Fixes

    • Updated font loading with JS-disabled fallback; hotkey and modal keyboard guards ignore text inputs/areas.
  • Refactor

    • Standardized prop forwarding/animation APIs and consolidated 3D state and transform model.
    • Selection events now support ignore-side-effect semantics; stability/timing utilities added.
  • Tests

    • Many new unit/integration tests for UI, 3D utilities, state, and timing utilities.
Gavin McDonald and others added 30 commits October 23, 2025 17:18
fix overlay initialization on scene recreation
fix sample delta generation for ambiguous label types
* don't re-render inputs

* tests

* update via overlay after user input

* tests

* lint

---------

Co-authored-by: Gavin McDonald <gavin@mcmorgans.us>
Co-authored-by: Benjamin Kane <ben@voxel51.com>
* fix disacrd changes and bounds updates

* linting

* bugs

* linting
Gavin McDonald and others added 27 commits October 30, 2025 11:42
FOEPD-2447: lighter: fix selection + drag bug
* fix ignoreSideEffects

* Update app/packages/core/src/components/Modal/Sidebar/Annotate/Edit/AnnotationSchema.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* add test

* lint

* remove isBridgeLogicHandled

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
[HA] add discard action to annotation footer
* fix undo/redo and selection for classifications, clear undo/redo stack on exit

* add label to classification creation
Release notes for OSS 1.10.0 / Enterprise 2.13.0
Changing wait interval for waitForSampleLoadDomAttribute
@mcdoh mcdoh requested review from a team as code owners October 31, 2025 14:41
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 24

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
app/packages/operators/src/components/OperatorExecutionMenu/index.tsx (1)

70-94: Add TypeScript types to Item component parameters.

The Item function component's parameters are not typed, which reduces type safety and makes the code harder to maintain.

Apply this diff to add proper types:

-function Item({ target, disabled, onClose, onOptionClick }) {
+function Item({ 
+  target, 
+  disabled, 
+  onClose, 
+  onOptionClick 
+}: {
+  target: OperatorExecutionOption;
+  disabled: boolean;
+  onClose: () => void;
+  onOptionClick?: (option: OperatorExecutionOption) => void;
+}) {
   return (
app/packages/lighter/src/overlay/ClassificationOverlay.ts (1)

42-97: Restore rendering instead of returning early.
With the new return; the overlay never disposes previous elements, never draws the classification label, and never calls emitLoaded(), so classification overlays vanish and selection state changes have no visible effect. Please drop the guard so the original rendering path runs again.

Apply this diff to remove the regression:

   protected renderImpl(renderer: Renderer2D): void {
-    return;
app/packages/core/src/components/Modal/Lighter/LighterSampleRenderer.tsx (1)

56-78: Remove the sample dependency concern—this is actually a bug requiring a fix.

The analysis confirms the original review comment identified a genuine issue. When users navigate to a different sample:

  • sampleRef.current updates to the new sample (line 51)
  • The overlay effect (lines 56-78) does NOT re-run because sample is not in its dependency array
  • The sceneId effect (lines 80-86) runs only once (empty dependencies), so sceneId never changes when sample changes
  • Result: old overlays persist instead of reloading for the new sample

The component needs to either:

  1. Add sample to the sceneId effect dependencies so it regenerates when sample changes, triggering a scene/component remount, OR
  2. Add sample to the overlay effect dependencies so overlays reload when sample changes, OR
  3. Clear existing overlays before loading new ones when sample changes

This is not a question of whether media "needs" to reload—it objectively should when the sample prop changes.

app/packages/looker-3d/src/annotation/annotation-toolbar/useAnnotationActions.tsx (1)

153-160: Remove unused dependencies currentSampleId and currentActiveField from the handleDeleteSelectedPoint dependency array.

The function body does not reference these variables. They should be removed from the dependency array (lines 154-155) to avoid unnecessary re-renders when their values change. The current dependencies should be: selectedPoint, setPolylinePointTransforms, setSelectedPoint, and setCurrentArchetypeSelectedForTransform.

app/packages/looker-3d/src/annotation/annotation-toolbar/CoordinateInputs.tsx (1)

239-315: Normalize polylinePointTransforms usage before indexing.

Both the memoized read and the task setter assume polylinePointTransforms is an object. Right after deselecting a label it’s null, so polylinePointTransforms[selectedPoint.labelId] (and later prev[labelId]) will throw. Normalize the container once and reuse that normalized value when you build the next state.

-    const segments =
-      polylinePointTransforms[selectedPoint.labelId]?.segments || [];
+    const segments =
+      polylinePointTransforms?.[selectedPoint.labelId]?.segments || [];
@@
-        setPolylinePointTransforms((prev) => {
-          const currentSegments = prev[labelId]?.segments || [];
+        setPolylinePointTransforms((prev) => {
+          const base = prev ?? {};
+          const existing = base[labelId] ?? {};
+          const currentSegments = existing.segments ?? [];
@@
-          const existingLabelData = prev[labelId];
-          const path = existingLabelData?.path || currentActiveField || "";
-          const sampleId = existingLabelData?.sampleId || currentSampleId;
+          const path = existing.path || currentActiveField || "";
+          const sampleId = existing.sampleId || currentSampleId;
@@
-            ...prev,
+            ...base,
             [labelId]: {
-              ...(existingLabelData ?? {}),
+              ...existing,
               segments: newSegments,
               path,
               sampleId,
             },
           };
♻️ Duplicate comments (11)
app/packages/core/src/components/Modal/Sidebar/Annotate/useFocus.ts (2)

15-15: Critical: Invalid hook usage - pass callbacks, not hook functions

You're still passing hook functions (useExit, useSave) directly to useConfirmExit, which expects plain callbacks. This violates the Rules of Hooks and will cause a runtime type mismatch.

Call the hooks first to obtain their callbacks, then pass those to useConfirmExit:

+  const exit = useExit();
+  const save = useSave();
-  const { confirmExit } = useConfirmExit(useExit, useSave);
+  const { confirmExit } = useConfirmExit(exit, save);
   const selectId = useRef<string | null>(null);
   const onExit = useExit(false);

42-46: Intended overlay selection is lost when no unsaved changes exist

When there are no unsaved changes, the handler exits without calling select(). If the user previously attempted to select another overlay (stored in selectId.current by the SELECT handler), that selection is never applied.

       if (!current || !STORE.get(hasChanges)) {
         // no unsaved changes, allow the exit
         onExit();
+        select();
         return;
       }
app/packages/state/src/jotai/modal.ts (1)

17-43: Critical: SSR and storage safety guards still missing.

Direct localStorage access in getItem, setItem, and removeItem will throw in SSR, Safari Private Mode, or sandboxed contexts. This directly contradicts the PR objective to address console errors.

Apply this diff to add the necessary guards:

     getItem: (key: string, initialValue: T): T => {
       const datasetName = getDatasetName();
       const prefixedKey = datasetName ? `${datasetName}_${key}` : key;
-      const storedValue = localStorage.getItem(prefixedKey);
-
-      if (storedValue === null) {
-        return initialValue;
-      }
-
+      if (typeof window === "undefined") return initialValue;
       try {
+        const storedValue = window.localStorage.getItem(prefixedKey);
+        if (storedValue === null) return initialValue;
         return JSON.parse(storedValue) as T;
       } catch {
         return initialValue;
       }
     },
 
     setItem: (key: string, value: T): void => {
       const datasetName = getDatasetName();
       const prefixedKey = datasetName ? `${datasetName}_${key}` : key;
-      localStorage.setItem(prefixedKey, JSON.stringify(value));
+      if (typeof window === "undefined") return;
+      try {
+        window.localStorage.setItem(prefixedKey, JSON.stringify(value));
+      } catch {
+        // ignore write errors (quota/private-mode)
+      }
     },
 
     removeItem: (key: string): void => {
       const datasetName = getDatasetName();
       const prefixedKey = datasetName ? `${datasetName}_${key}` : key;
-      localStorage.removeItem(prefixedKey);
+      if (typeof window === "undefined") return;
+      try {
+        window.localStorage.removeItem(prefixedKey);
+      } catch {
+        // ignore remove errors
+      }
     },
app/packages/core/src/components/Modal/Sidebar/Annotate/Edit/useDelete.ts (1)

34-48: Stop dispatching when schema/scene are missing

getFieldSchema still returns Field | null, so the non-null assertion will crash when the schema lookup fails (this was already flagged earlier). On top of that we flip isSaving to true even when scene is undefined, leaving the UI stuck because no dispatch runs to reset it. Please guard both conditions before dispatching so we either exit cleanly or dispatch with a real Field.

-    setSaving(true);
-    scene?.dispatchSafely({
+    if (!scene) {
+      return;
+    }
+
+    const fieldSchema = getFieldSchema(schema, label.path);
+    if (!fieldSchema) {
+      return;
+    }
+
+    setSaving(true);
+    scene.dispatchSafely({
       type: LIGHTER_EVENTS.DO_REMOVE_OVERLAY,
       detail: {
         label,
-        schema: getFieldSchema(schema, label?.path)!,
+        schema: fieldSchema,
         onSuccess: () => {
app/packages/looker-3d/src/annotation/annotation-toolbar/useAnnotationActions.tsx (1)

137-140: Still filtering only empty segments; polylines need ≥2 vertices.

As noted in the previous review, filtering segment.points.length > 0 can leave single-vertex segments, which are invalid for polylines.

Apply this diff to filter degenerate segments:

-      // Remove empty segments
       const filteredSegments = newSegments.filter(
-        (segment) => segment.points.length > 0
+        (segment) => segment.points.length >= 2
       );
app/packages/core/src/components/Modal/Sidebar/Annotate/Edit/useExit.ts (1)

59-72: Unsaved label cleanup is still bypassed.

The early if (!label || !revertLabel) guard runs before the unsaved?.isNew branch, so new overlays never hit removeOverlay(...)—exactly the critical bug called out earlier. Please restore the prior ordering so the unsaved-removal logic runs first.

-    if (!label || !revertLabel) {
-      setSaved(null);
-      setEditing(null);
-      return;
-    }
-
-    // label has not been persisted, so remove it
-    if (unsaved?.isNew) {
+    // label has not been persisted, so remove it
+    if (unsaved?.isNew) {
       removeOverlay(unsaved?.overlay.id);
       scene?.exitInteractiveMode();
       setEditing(null);
       setSaved(null);
       return;
     }
 
+    if (!label || !revertLabel) {
+      setSaved(null);
+      setEditing(null);
+      return;
+    }
app/packages/core/src/components/Modal/Sidebar/Annotate/Edit/Position.tsx (1)

54-87: Compare overlay ids and guard event.detail safely.

The bounds events fire with the overlay id, not the label’s _id, so event.detail.id !== data?._id still filters out legitimate updates. It also blows up when event.detail is missing. Please key off overlay.id (as previously suggested) and null-check the detail before reading id.

-    const handler = (event) => {
+    const handler = (event: CustomEvent<{ id?: string }>) => {
       if (
         !(overlay instanceof BoundingBoxOverlay) ||
-        !overlay.hasValidBounds() ||
-        event.detail.id !== data?._id
+        !overlay.hasValidBounds() ||
+        event.detail?.id !== overlay?.id
       ) {
         return;
       }
@@
-  }, [data?._id, overlay, scene, setData]);
+  }, [overlay?.id, overlay, scene, setData]);
app/packages/looker-3d/src/annotation/useSetEditingToNewPolyline.tsx (1)

92-101: Ensure new polyline path is always a string.

This still uses transformData.path ?? currentActiveField, so when both are undefined/null we end up with a non-string path — exactly what the earlier review flagged. Please fall back to an empty string to keep downstream lookups and typing happy.

-        path: transformData.path ?? currentActiveField,
+        path: transformData.path ?? currentActiveField ?? "",
app/packages/looker-3d/src/annotation/usePolylineAnnotation.tsx (2)

118-141: Harden transform updates against null state (and keep path as string).

setPolylinePointTransforms(null) leaves this setter receiving prev === null, so prev[labelId] throws before optional chaining runs. At the same time, the path fallback still omits the empty-string guard that was previously requested. Reusing a normalized base/existing structure avoids the crash and lets you reuse the same object for both the segment data and the string fallback.

-      setPolylinePointTransforms((prev) => {
-        const labelId = label._id;
-        const currentSegments = prev[labelId]?.segments || [];
+      setPolylinePointTransforms((prev) => {
+        const labelId = label._id;
+        const base = prev ?? {};
+        const existing = base[labelId] ?? { segments: [] };
+        const currentSegments = existing.segments ?? [];
@@
-        return {
-          ...prev,
-          [labelId]: {
-            segments: newSegments,
-            path: prev[labelId]?.path ?? currentActiveField,
-            sampleId: prev[labelId]?.sampleId ?? currentSampleId,
-          },
-        };
+        return {
+          ...base,
+          [labelId]: {
+            ...existing,
+            segments: newSegments,
+            path: existing.path ?? currentActiveField ?? "",
+            sampleId: existing.sampleId ?? currentSampleId,
+          },
+        };
       });

336-358: Fix null handling when inserting vertices (and keep path string-safe).

Same crash vector exists here: prev may be null, and prev[labelId] (plus the .path access) will blow up. Please normalize prev before reading and reuse that when building the return payload, while also applying the empty-string path fallback that was requested earlier.

-        setPolylinePointTransforms((prev) => {
-          const labelId = label._id;
-          const currentSegments = prev[labelId]?.segments || [];
+        setPolylinePointTransforms((prev) => {
+          const labelId = label._id;
+          const base = prev ?? {};
+          const existing = base[labelId] ?? {};
+          const currentSegments = existing.segments ?? [];
@@
-          return {
-            ...prev,
-            [labelId]: {
-              segments: newSegments,
-              path: prev[labelId].path ?? currentActiveField,
-              sampleId: prev[labelId].sampleId ?? currentSampleId,
-            },
-          };
+          return {
+            ...base,
+            [labelId]: {
+              ...existing,
+              segments: newSegments,
+              path: existing.path ?? currentActiveField ?? "",
+              sampleId: existing.sampleId ?? currentSampleId,
+            },
+          };
         });
app/packages/looker-3d/src/labels/index.tsx (1)

303-323: Stop spreading transform misc onto Polyline props

This reintroduces the same prop-leak we called out before: the result of coerceStringBooleans(...) gets merged into overlayCombined / overlayLabel and then spread straight into <Polyline />. React ends up seeing arbitrary keys like closed on the underlying <group>, which is exactly the console noise this PR is trying to remove. Keep the misc payload on the label metadata (e.g. label.misc) and pass the supported props (closed, etc.) explicitly when rendering the Polyline. Based on prior feedback.

Apply something along these lines:

-        let finalPoints3d = maybeExistingTransformData?.segments
-          ? maybeExistingTransformData.segments.map((seg) => seg.points)
-          : (overlay as PolyLineProps).points3d;
-
-        if (finalPoints3d) {
-          finalPoints3d = finalPoints3d.filter(isValidPolylineSegment);
-        }
-
-        const overlayCombined = {
-          ...overlay,
-          ...coerceStringBooleans(maybeExistingTransformData?.misc ?? {}),
-          points3d: finalPoints3d,
-        };
+        let finalPoints3d = maybeExistingTransformData?.segments
+          ? maybeExistingTransformData.segments.map((seg) => seg.points)
+          : (overlay as PolyLineProps).points3d;
+
+        if (finalPoints3d) {
+          finalPoints3d = finalPoints3d.filter(isValidPolylineSegment);
+        }
+
+        const sanitizedMisc = coerceStringBooleans(
+          maybeExistingTransformData?.misc ?? {}
+        );
+        const overlayLabelWithMisc = {
+          ...overlay,
+          points3d: finalPoints3d,
+          misc: sanitizedMisc,
+        };
+        const isClosed = sanitizedMisc.closed === true;
...
-          newPolylineOverlays.push(
-            <Polyline
-              key={`polyline-${overlay._id ?? overlay.id}-${overlay.sampleId}`}
-              rotation={overlayRotation}
-              opacity={labelAlpha}
-              lineWidth={polylineWidth}
-              {...(overlayCombined as PolyLineProps)}
-              label={overlayCombined}
-              onClick={(e) => handleSelect(overlay, "polyline", e)}
-              tooltip={tooltip}
-            />
-          );
+          newPolylineOverlays.push(
+            <Polyline
+              key={`polyline-${overlay._id ?? overlay.id}-${overlay.sampleId}`}
+              rotation={overlayRotation}
+              opacity={labelAlpha}
+              lineWidth={polylineWidth}
+              {...(overlay as PolyLineProps)}
+              points3d={finalPoints3d}
+              closed={isClosed}
+              label={overlayLabelWithMisc}
+              onClick={(e) => handleSelect(overlay, "polyline", e)}
+              tooltip={tooltip}
+            />
+          );
...
-        const overlayLabel = {
+        const sanitizedMisc = coerceStringBooleans(transformData.misc ?? {});
+        const isClosed = sanitizedMisc.closed === true;
+        const overlayLabel = {
           _id: labelId,
           _cls: "Polyline",
           type: "Polyline",
           path: transformData.path,
           label: transformData.label,
           selected: false,
           sampleId: currentSampleId,
           tags: [],
           points3d,
-          ...coerceStringBooleans(transformData.misc ?? {}),
+          misc: sanitizedMisc,
         };
+        const { misc: _unused, ...polylineProps } = overlayLabel;
...
-        newPolylineOverlays.push(
-          <Polyline
-            key={`polyline-${labelId}-${transformData.sampleId}`}
-            rotation={overlayRotation}
-            opacity={labelAlpha}
-            lineWidth={polylineWidth}
-            {...(overlayLabel as unknown as PolyLineProps)}
-            label={overlayLabel}
-            onClick={(e) => handleSelect(overlayLabel, "polyline", e)}
-            tooltip={tooltip}
-            color={getLabelColor({
+        newPolylineOverlays.push(
+          <Polyline
+            key={`polyline-${labelId}-${transformData.sampleId}`}
+            rotation={overlayRotation}
+            opacity={labelAlpha}
+            lineWidth={polylineWidth}
+            {...(polylineProps as unknown as PolyLineProps)}
+            closed={isClosed}
+            label={overlayLabel}
+            onClick={(e) => handleSelect(overlayLabel, "polyline", e)}
+            tooltip={tooltip}
+            color={getLabelColor({
               coloring,
               path: transformData.path,
               isTagged: false,
               labelTagColors,
               customizeColorSetting,
               label: overlayLabel,
               embeddedDocType: overlayLabel._cls,
             })}
           />
         );

Also applies to: 355-386

🧹 Nitpick comments (16)
app/packages/core/src/components/Modal/Sidebar/Annotate/Icons.tsx (2)

5-5: Icon substitution addresses missing/unsuitable icon.

The import change from PolylineOutlined to TimelineOutlined (aliased for compatibility) combined with the 90-degree rotation at line 62 is a reasonable solution if PolylineOutlined doesn't exist or isn't suitable in MUI.

Consider adding a brief comment explaining this substitution for maintainability:

+// MUI doesn't provide PolylineOutlined; using rotated TimelineOutlined instead
  TimelineOutlined as PolylineOutlined,

59-65: Console error fix looks good, but consider consistency.

The change from fill prop to sx={{ color: fill }} correctly addresses the console error, as MUI icons don't accept fill as a prop.

For consistency with the Locking and Shown components (lines 73, 84) which use the style prop, consider:

-      <PolylineOutlined sx={{ transform: "rotate(90deg)", color: fill }} />
+      <PolylineOutlined style={{ transform: "rotate(90deg)", color: fill }} />

Both approaches work, but consistency improves maintainability.

app/packages/core/src/components/Actions/ToggleSidebar/index.tsx (1)

9-44: Consider adding a displayName for easier debugging.

The forwardRef component lacks a displayName, which can make debugging in React DevTools less clear.

Apply this diff to add a displayName:

 const ToggleSidebar = React.forwardRef<
   HTMLButtonElement,
   ActionProps & {
     modal: boolean;
   }
 >(({ modal, adaptiveMenuItemProps }, ref) => {
   const [visible, setVisible] = useRecoilState(sidebarVisible(modal));

   return (
     <PillButton
       onClick={() => {
         setVisible(!visible);
       }}
       title={`${visible ? "Hide" : "Show"} sidebar (s)`}
       tooltipPlacement={modal ? "bottom" : "top"}
       open={visible}
       icon={
         visible ? (
           modal ? (
             <KeyboardArrowRight />
           ) : (
             <KeyboardArrowLeft />
           )
         ) : modal ? (
           <KeyboardArrowLeft />
         ) : (
           <KeyboardArrowRight />
         )
       }
       highlight={!visible}
       ref={ref}
       data-cy="action-toggle-sidebar"
       {...(getStringAndNumberProps(adaptiveMenuItemProps) || {})}
     />
   );
 });
+
+ToggleSidebar.displayName = "ToggleSidebar";
app/packages/core/src/components/Modal/ModalNavigation.tsx (1)

125-152: Consider covering additional text-entry input types.

The keyboard guards correctly prevent navigation when TEXTAREA or text INPUT elements are focused. However, the INPUT check only covers type="text" and may miss other text-entry scenarios like email, tel, url, search, password, or number inputs where users might also be typing.

If broader coverage is desired, consider:

 // Prevent navigation when editing text in input fields
 if (active?.tagName === "INPUT") {
-  if ((active as HTMLInputElement).type === "text") {
+  const inputType = (active as HTMLInputElement).type;
+  if (["text", "email", "tel", "url", "search", "password", "number"].includes(inputType)) {
     return;
   }
 }

Alternatively, if the current scope is intentional (e.g., to allow arrow navigation in other input types like checkboxes or range sliders), the implementation is appropriate as-is.

app/packages/core/src/components/Modal/Sample3d.tsx (1)

26-32: LGTM: Conditional logic correctly implements annotation mode support.

The expanded condition properly handles both group media and annotation mode by bypassing the SampleWrapper when appropriate. The boolean logic is sound and the implementation is correct.

Consider adding a brief comment explaining why these modes render Looker3d directly without the wrapper, to improve maintainability:

+        {/* Groups and annotation mode render Looker3d directly without wrapper */}
         {isGroup || (canAnnotate && isInAnnotateMode) ? (
app/packages/components/src/components/LabeledField/index.tsx (1)

10-18: Consider allowing ReactNode for description.

The label prop accepts React.ReactNode while description is limited to string. For consistency and flexibility, consider allowing React.ReactNode for the description as well.

Apply this diff if you'd like to make the types consistent:

 export const LabeledField = ({
   formControl,
   label,
   description,
 }: {
   formControl: React.ReactNode;
   label?: React.ReactNode;
-  description?: string;
+  description?: React.ReactNode;
 }) => {
app/packages/core/src/plugins/SchemaIO/components/OperatorExecutionButtonView.tsx (1)

101-103: Consider validating origin prop structure before forwarding.

The menu_anchor_origin and menu_transform_origin props are forwarded directly from the schema view without validation. If the schema provides malformed values (e.g., incorrect vertical/horizontal values), they'll be passed to the underlying component, potentially causing console warnings or unexpected behavior.

Consider adding validation:

+const validateOrigin = (origin) => {
+  if (!origin) return origin;
+  const validVertical = ["top", "bottom", "center"];
+  const validHorizontal = ["left", "right", "center"];
+  if (validVertical.includes(origin.vertical) && validHorizontal.includes(origin.horizontal)) {
+    return origin;
+  }
+  return undefined;
+};
+
 return (
   <Box {...getComponentProps(props, "container")}>
     <TooltipProvider title={title} {...getComponentProps(props, "tooltip")}>
       <OperatorExecutionButton
         operatorUri={operator}
         onSuccess={handleOnSuccess}
         onError={handleOnError}
         onOptionSelected={handleOnOptionSelected}
         executionParams={computedParams}
         variant={variant}
         disabled={disabled}
         insideModal={inside_modal}
-        menuAnchorOrigin={menu_anchor_origin}
-        menuTransformOrigin={menu_transform_origin}
+        menuAnchorOrigin={validateOrigin(menu_anchor_origin)}
+        menuTransformOrigin={validateOrigin(menu_transform_origin)}
         startIcon={icon_position === "left" ? Icon : undefined}
         endIcon={icon_position === "right" ? Icon : undefined}
         title={description}
         {...getComponentProps(props, "button", getButtonProps(props))}
       >
app/packages/looker-3d/src/AnnotationTips.tsx (3)

36-104: Prefer theme colors over hardcoded values for consistency.

Multiple styled components use hardcoded color values (#e0e0e0, #bdbdbd, #999, #ccc, #666, etc.) instead of leveraging the theme system. While TipsContainer correctly uses theme.primary.main for the border, other components should follow suit for maintainability and consistent theming.

Consider extracting commonly used colors from the theme (e.g., theme.text.primary, theme.text.secondary, etc.) and applying them consistently across all styled components.


108-123: Consider platform-specific keyboard shortcuts.

The tip content uses macOS-specific keyboard shortcuts (⌘) without considering Windows/Linux users who would use Ctrl instead. Consider detecting the user's platform and displaying the appropriate modifier key.

Example approach:

const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const modifierKey = isMac ? '⌘' : 'Ctrl';

Then use {modifierKey} in the tip text.


167-195: Add accessibility attributes for screen readers.

The tips overlay lacks proper ARIA attributes, which could impact users relying on assistive technologies. Consider adding:

  • role="dialog" or role="alert" to the container
  • aria-label or aria-labelledby to describe the dialog
  • aria-describedby to reference the tip content
  • Focus management when the overlay appears

Example enhancements:

  return (
    <TipsContainer
      $border={theme.primary.main}
      $text="#e0e0e0"
      $isMultiviewOn={isMultiviewOn}
+     role="dialog"
+     aria-label="3D Viewer Tips"
+     aria-describedby="tip-content"
    >
      <TipsHeader>
        <TipsTitle>
          <InfoOutlined style={{ fontSize: 16, color: theme.primary.main }} />
          FiftyOne 3D Viewer Tips
        </TipsTitle>
      </TipsHeader>

      <TipsContent>
-       <TipsList>
+       <TipsList id="tip-content">
          <TipsListItem>{currentTip.text}</TipsListItem>
        </TipsList>
      </TipsContent>

      <TipsActions>
-       <TipsButton $variant="primary" onClick={handleCancel}>
+       <TipsButton $variant="primary" onClick={handleCancel} aria-label="Dismiss tips">
          Got it
        </TipsButton>
-       <TipsButton $variant="secondary" onClick={handleDoNotShowAgain}>
+       <TipsButton $variant="secondary" onClick={handleDoNotShowAgain} aria-label="Don't show tips again">
          Don't show again
        </TipsButton>
      </TipsActions>
    </TipsContainer>
  );
app/packages/core/src/utils/time.ts (2)

1-18: Documentation could clarify timeout behavior.

The JSDoc is clear, but could be more precise: the fallback is returned when the function is called after the timeout has elapsed, not automatically when timeout expires. Additionally, once timeout occurs, the function continues returning the fallback value on subsequent calls until the predicate becomes true.


24-25: Remove unused hasTimedOut variable.

The hasTimedOut variable is set on line 44 and reset on line 33, but it's never read or used in any conditional logic. This appears to be dead code.

Apply this diff to remove the unused variable:

 ): () => boolean {
   let startTime: number | null = null;
-  let hasTimedOut = false;
 
   return () => {

And remove the assignments:

     // If predicate is true, reset and return true
     if (predicateResult) {
       startTime = null;
-      hasTimedOut = false;
       return true;
     }
     const elapsed = Date.now() - startTime;
     if (elapsed >= timeoutMs) {
-      hasTimedOut = true;
       return fallbackReturnValue;
     }
app/packages/core/src/utils/time.test.ts (1)

136-158: Test comment is misleading.

The test works correctly, but the comment on line 156 says "returns true (from fallback after timeout)". However, at call 4, callCount becomes 4, so predicate() returns true (4 % 2 === 0), meaning the wrapper returns true because the predicate is true, not because of the timeout fallback.

Consider updating the comment to:

-    // Call 4: returns true (from fallback after timeout)
+    // Call 4: returns true (predicate returns true since callCount=4)
     expect(wrapper()).toBe(true);
app/packages/core/src/components/Modal/Sidebar/Annotate/useFocus.ts (1)

73-80: Consider handling overlay selection when editing new labels

When editing a new label, the handler stores the target overlay id in selectId but returns early without selecting it (line 75). This prevents users from switching overlays during new label creation, which may be intentional to avoid losing work. However:

  1. The stored selectId is never cleared if the user cancels the new label, leaving stale data
  2. Users may find it confusing that clicking another overlay has no effect

Consider either:

  • Clearing selectId.current on the early return to avoid stale data
  • Or prompting the user to save/discard the new label before switching overlays (similar to the existing label flow)
app/packages/core/src/plugins/SchemaIO/components/DropdownView.tsx (1)

202-203: Remove unreachable code.

The if (multiple) branch in renderValue will never execute because when multiple is true, the component returns early at line 164 with <AutocompleteView>. This is dead code.

Apply this diff to remove the unreachable code:

         }
         return placeholder;
       }
-      if (multiple) {
-        return value.map((item) => choiceLabels[item] || item).join(", ");
-      }
       return choiceLabels[value] || value;
     }}
docs/source/release-notes.rst (1)

62-66: Prefer “parameter” over “kwarg” for user‑facing docs

Replace colloquial “kwarg” with “parameter” and simplify phrasing.

- includes a kwarg for a batcher, which can be a
+ includes a parameter for a batcher, which can be a
Comment on lines +81 to +88
<Item
key={
item.isSectionHeader
? `section-${item.sectionKey}-${idx}`
: `{item.shortcut}-${idx}`
}
{...item}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix React key interpolation for non-section help items.

The non-section branch is building the key as the literal string "{item.shortcut}-${idx}", so every row ends up with the same key. React will warn and reconciliation can break when keys collide. Switch this to a template literal so the shortcut and index are actually interpolated.

-                      : `{item.shortcut}-${idx}`
+                      : `${item.shortcut}-${idx}`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Item
key={
item.isSectionHeader
? `section-${item.sectionKey}-${idx}`
: `{item.shortcut}-${idx}`
}
{...item}
/>
<Item
key={
item.isSectionHeader
? `section-${item.sectionKey}-${idx}`
: `${item.shortcut}-${idx}`
}
{...item}
/>
🤖 Prompt for AI Agents
In app/packages/components/src/components/HelpPanel/HelpPanel.tsx around lines
81 to 88, the non-section key branch currently produces the literal string
"{item.shortcut}-${idx}" so keys collide; change that branch to use a JavaScript
template literal that interpolates item.shortcut and idx (e.g. backtick-based
`${item.shortcut}-${idx}`) so each key is unique and React reconciliation works
correctly.
Comment on lines +105 to +117
sx: {
// default dialog z-index is 1300
zIndex: insideModal ? 2400 : undefined,
},
MenuListProps: {
sx: { paddingY: 0, zIndex: 999 },
sx: {
paddingY: 0,
zIndex: 999,
maxHeight: "400px",
width: hideActions ? "100%" : "270px",
overflowY: "auto",
background: (theme) => theme.background.level2,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the z-index value is appropriate for all modal contexts.

The z-index of 2400 for modal contexts should overlay standard MUI Dialogs (z-index 1300), but ensure this doesn't conflict with other high-z-index UI elements like tooltips, popovers, or nested modals.


🏁 Script executed:

#!/bin/bash
# Search for z-index values in the codebase to check for potential conflicts
rg -n --type=tsx --type=ts 'zIndex.*:.*\d{4}' -C 2

Length of output: 27


I'll fix the script syntax and search for z-index values to verify potential conflicts:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for high z-index values (4 digits) in TypeScript and TSX files
rg -n 'zIndex\s*:\s*\d{4,}' -g '*.ts' -g '*.tsx' -C 2

Length of output: 7312


🏁 Script executed:

#!/bin/bash
# Also search for z-index references in the Selection component specifically
rg -n 'zIndex' app/packages/components/src/components/Selection/ -C 1

Length of output: 1221


🏁 Script executed:

#!/bin/bash
# Search for MUI Dialog and Modal z-index related code
rg -n 'z-index.*1300|zIndex.*1300' -g '*.ts' -g '*.tsx'

Length of output: 496


🏁 Script executed:

#!/bin/bash
# Check for tooltip, popover z-index values to identify hierarchy
rg -n '(tooltip|popover).*zIndex|zIndex.*(tooltip|popover)' -g '*.ts' -g '*.tsx' -i

Length of output: 359


🏁 Script executed:

#!/bin/bash
# List all zIndex values found to establish a complete picture
rg -n 'zIndex\s*:\s*[^,}]+' -g '*.ts' -g '*.tsx' | head -30

Length of output: 2980


🏁 Script executed:

#!/bin/bash
# Search for how Selection component is used within modal contexts
rg -n 'Selection' app/packages/core/src/components/Modal/ -C 2

Length of output: 1201


🏁 Script executed:

#!/bin/bash
# Check if there are any Popper or Menu z-index related issues documented
rg -n 'MenuProps|Popper' app/packages/components/src/components/Selection/ -A 3 -B 1

Length of output: 483


🏁 Script executed:

#!/bin/bash
# Check MUI theme z-index configuration in the codebase
rg -n 'zIndex\s*:\s*\{' app/packages/components/src/components/ThemeProvider/ -A 10

Length of output: 1017


🏁 Script executed:

#!/bin/bash
# Search for comments or documentation about the 999 z-index value
rg -n '999|MenuListProps' app/packages/components/src/components/Selection/ -B 2 -A 2

Length of output: 2758


🏁 Script executed:

#!/bin/bash
# Check if there are any git comments or TODOs related to z-index in Selection
rg -n 'TODO|FIXME|BUG|z-index' app/packages/components/src/components/Selection/Selection.tsx -B 1 -A 1

Length of output: 427


🏁 Script executed:

#!/bin/bash
# Verify how MUI Select MenuProps work by checking if there are other similar patterns
rg -n 'MenuProps.*zIndex.*:.*\d+' -A 2 -B 2 -g '*.ts' -g '*.tsx'

Length of output: 0


Fix z-index inconsistency in Selection component and typo in styledComponents.

The z-index value of 2400 for MenuProps correctly overlays MUI Dialogs (z-index 1300), but MenuListProps uses 999, creating an inverted hierarchy. When insideModal=true, the dropdown list content should have a z-index at least as high as its parent to ensure it remains visible above the modal.

Additionally, there's a CSS property typo in styledComponents.ts line 88: zindex should be zIndex.

Required fixes:

  • Selection.tsx line 112: Change MenuListProps z-index from 999 to at least 2400 (or use conditional like parent)
  • styledComponents.ts line 88: Correct zindex: 9999; to zIndex: 9999;
🤖 Prompt for AI Agents
In app/packages/components/src/components/Selection/Selection.tsx around lines
105-117, the MenuListProps currently sets zIndex: 999 which conflicts with the
parent MenuProps zIndex of 2400 when insideModal is true; change
MenuListProps.sx.zIndex to match the parent (e.g., set to 2400 or conditionally
use insideModal ? 2400 : undefined) so the dropdown list remains above MUI
Dialogs. In app/packages/components/src/components/Selection/styledComponents.ts
line 88, fix the CSS typo by replacing the non-standard `zindex: 9999;` with the
camelCase React style `zIndex: 9999;`.
Comment on lines +233 to +242
<MenuItem
key="create-view-option"
value="create-view-option"
label=""
sx={{ padding: 0 }}
onClick={(e) => {
e.preventDefault();
}}
>
{lastFixedOption}
</LastOption>
<LastOption>{lastFixedOption}</LastOption>
</MenuItem>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider potential issue with fixed MenuItem value.

The lastFixedOption is wrapped in a MenuItem with a hardcoded value="create-view-option". While e.preventDefault() is called, MUI Select might still allow this value to be selected through keyboard navigation or other interactions, which could cause the Select to display an unexpected value.

Consider one of these approaches:

  1. Use disableListItem prop if available, or add pointer-events styling:
 <MenuItem
   key="create-view-option"
   value="create-view-option"
-  sx={{ padding: 0 }}
+  sx={{ padding: 0, pointerEvents: 'none' }}
   onClick={(e) => {
     e.preventDefault();
   }}
 >
   <LastOption>{lastFixedOption}</LastOption>
 </MenuItem>
  1. Or use a non-MenuItem wrapper if this is truly a non-selectable action:
-<MenuItem
-  key="create-view-option"
-  value="create-view-option"
-  sx={{ padding: 0 }}
-  onClick={(e) => {
-    e.preventDefault();
-  }}
->
-  <LastOption>{lastFixedOption}</LastOption>
-</MenuItem>
+<ListSubheader sx={{ padding: 0, position: 'sticky', bottom: 0 }}>
+  <LastOption>{lastFixedOption}</LastOption>
+</ListSubheader>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<MenuItem
key="create-view-option"
value="create-view-option"
label=""
sx={{ padding: 0 }}
onClick={(e) => {
e.preventDefault();
}}
>
{lastFixedOption}
</LastOption>
<LastOption>{lastFixedOption}</LastOption>
</MenuItem>
<MenuItem
key="create-view-option"
value="create-view-option"
sx={{ padding: 0, pointerEvents: 'none' }}
onClick={(e) => {
e.preventDefault();
}}
>
<LastOption>{lastFixedOption}</LastOption>
</MenuItem>
🤖 Prompt for AI Agents
In app/packages/components/src/components/Selection/Selection.tsx around lines
233-242, the MenuItem for lastFixedOption has a hardcoded value
("create-view-option") which can still be selected via keyboard; make this item
non-selectable by either replacing the MenuItem with a non-selectable wrapper
(e.g., Box or ListSubheader) so it is not part of the Select options, or if you
must keep MenuItem, remove the value prop and mark it disabled and
non-interactive (set disabled, sx={{ pointerEvents: 'none' }}, tabIndex={-1} and
aria-hidden="true") so it cannot be focused or chosen.
Comment on lines 80 to +86
useEffect(() => {
// sceneId should be deterministic, but unique for a given sample snapshot
const sample = sampleRef.current;
setSceneId(
`${sample?.sample?._id}-${sample?.sample?.last_modified_at?.datetime}`
);
}, [sample]);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: sceneId never updates when sample changes.

The empty dependency array means sceneId is set only once on mount. If the sample prop changes to represent a different sample, the sceneId will remain stale. This creates a mismatch between the scene identifier and the actual sample data, which could lead to:

  • Wrong scene being displayed
  • Stale scene references
  • Data inconsistency bugs

If the intent is to keep the scene stable across sample updates for the same logical sample, consider adding dependencies for the ID fields:

  useEffect(() => {
    // sceneId should be deterministic, but unique for a given sample snapshot
    const sample = sampleRef.current;
    setSceneId(
      `${sample?.sample?._id}-${sample?.sample?.last_modified_at?.datetime}`
    );
- }, []);
+ }, [sample?.sample?._id, sample?.sample?.last_modified_at?.datetime]);

If you want the component to be stable across sample navigation, you'll need a different architecture (e.g., key prop to force remount).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/packages/core/src/components/Modal/Lighter/LighterSampleRenderer.tsx
around lines 80 to 86, the useEffect that sets sceneId runs only on mount (empty
dependency array) so sceneId stays stale when the sample changes; update the
effect to depend on the sample identity/timestamp (e.g.,
sampleRef.current.sample._id and
sampleRef.current.sample.last_modified_at?.datetime or the prop(s) that feed
sampleRef) so sceneId is recalculated whenever the sample or its
last_modified_at changes; alternatively, if you intend to force a full remount
on sample change, move the sceneId logic into a stable key on the component
consumer instead.
Comment on lines +105 to 107
if (annotationSupported && (loading || !scene)) {
return <Loading />;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore 3D annotate loading behavior.

annotationSupported is true for 3D, but those datasets never get a lighterSceneAtom instance, so this guard now keeps the sidebar in an endless spinner. We still need to bypass the scene requirement when mediaType === "3d" like before.

Apply this diff:

-  if (annotationSupported && (loading || !scene)) {
+  if (
+    annotationSupported &&
+    (loading || (!scene && mediaType !== "3d"))
+  ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (annotationSupported && (loading || !scene)) {
return <Loading />;
}
if (
annotationSupported &&
(loading || (!scene && mediaType !== "3d"))
) {
return <Loading />;
}
🤖 Prompt for AI Agents
In app/packages/core/src/components/Modal/Sidebar/Annotate/Annotate.tsx around
lines 105 to 107, the current guard returns Loading whenever annotationSupported
is true and scene is missing, which leaves 3D datasets stuck because they have
annotationSupported true but no lighterSceneAtom; update the condition to bypass
the scene requirement for 3D media by only requiring scene when mediaType !==
"3d" (i.e., change the check to return Loading if annotationSupported &&
(loading || (!scene && mediaType !== "3d"))), ensuring you reference the
existing mediaType variable in the component.
Comment on lines +940 to 941
{mode === "annotate" && isPolylineAnnotateActive && <AnnotationToolbar />}
</Fo3dSceneContext.Provider>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t hide the annotation toolbar by default

Requiring isPolylineAnnotateActive here means annotate mode renders with no toolbar until the polyline flag is already true—users lose the controls they need to activate polyline mode (and any other tools). Drop this extra condition so the toolbar is always available when the modal is in annotate mode.

Apply this diff:

-      {mode === "annotate" && isPolylineAnnotateActive && <AnnotationToolbar />}
+      {mode === "annotate" && <AnnotationToolbar />}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{mode === "annotate" && isPolylineAnnotateActive && <AnnotationToolbar />}
</Fo3dSceneContext.Provider>
{mode === "annotate" && <AnnotationToolbar />}
</Fo3dSceneContext.Provider>
🤖 Prompt for AI Agents
In app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx around lines 940 to 941,
the AnnotationToolbar is currently gated by both mode === "annotate" and
isPolylineAnnotateActive, which hides the toolbar until a polyline flag is true;
remove the isPolylineAnnotateActive condition so the toolbar renders whenever
mode === "annotate". Update the JSX to only check mode === "annotate" (i.e.,
render <AnnotationToolbar /> when annotate mode is active) and ensure no other
logic relies on the removed condition.
Comment on lines +59 to +63
useEffect(() => {
if (isSelectedForAnnotation) {
setIsPolylineAnnotateActive(true);
}
}, [isSelectedForAnnotation]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset annotate-active flag when deselected.

The effect only ever sets the atom to true. Once a label is selected, the flag stays latched even after deselection, so other UI that relies on isPolylineAnnotateActive never turns off. Please reflect the current selection each time and include the setter in the dependency list.

-  useEffect(() => {
-    if (isSelectedForAnnotation) {
-      setIsPolylineAnnotateActive(true);
-    }
-  }, [isSelectedForAnnotation]);
+  useEffect(() => {
+    setIsPolylineAnnotateActive(isSelectedForAnnotation);
+  }, [isSelectedForAnnotation, setIsPolylineAnnotateActive]);
🤖 Prompt for AI Agents
In app/packages/looker-3d/src/labels/polyline.tsx around lines 59 to 63, the
useEffect currently only sets setIsPolylineAnnotateActive(true) when selected,
causing the flag to remain latched after deselection; change the effect to set
the atom to the current isSelectedForAnnotation value (e.g.,
setIsPolylineAnnotateActive(isSelectedForAnnotation)) so it turns off when
deselected, and include setIsPolylineAnnotateActive in the dependency array.
Comment on lines +45 to +75
subscribe: (
key: string,
callback: (value: T) => void,
initialValue: T
): (() => void) | undefined => {
if (
typeof window === "undefined" ||
typeof window.addEventListener === "undefined"
) {
return undefined;
}

const handler = (e: StorageEvent) => {
const datasetName = getDatasetName();
const prefixedKey = datasetName ? `${datasetName}_${key}` : key;

if (e.storageArea === localStorage && e.key === prefixedKey) {
let newValue: T;
try {
newValue = e.newValue ? JSON.parse(e.newValue) : initialValue;
} catch {
newValue = initialValue;
}
callback(newValue);
}
};

window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe localStorage reference in storage event handler.

Line 61 directly references the global localStorage which can throw in restricted contexts even when window exists. Use window.localStorage consistently and guard the comparison.

Apply this diff:

       const handler = (e: StorageEvent) => {
         const datasetName = getDatasetName();
         const prefixedKey = datasetName ? `${datasetName}_${key}` : key;
 
-        if (e.storageArea === localStorage && e.key === prefixedKey) {
+        if (e.storageArea === window.localStorage && e.key === prefixedKey) {
           let newValue: T;
           try {
             newValue = e.newValue ? JSON.parse(e.newValue) : initialValue;
           } catch {
             newValue = initialValue;
           }
           callback(newValue);
         }
       };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subscribe: (
key: string,
callback: (value: T) => void,
initialValue: T
): (() => void) | undefined => {
if (
typeof window === "undefined" ||
typeof window.addEventListener === "undefined"
) {
return undefined;
}
const handler = (e: StorageEvent) => {
const datasetName = getDatasetName();
const prefixedKey = datasetName ? `${datasetName}_${key}` : key;
if (e.storageArea === localStorage && e.key === prefixedKey) {
let newValue: T;
try {
newValue = e.newValue ? JSON.parse(e.newValue) : initialValue;
} catch {
newValue = initialValue;
}
callback(newValue);
}
};
window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
},
};
subscribe: (
key: string,
callback: (value: T) => void,
initialValue: T
): (() => void) | undefined => {
if (
typeof window === "undefined" ||
typeof window.addEventListener === "undefined"
) {
return undefined;
}
const handler = (e: StorageEvent) => {
const datasetName = getDatasetName();
const prefixedKey = datasetName ? `${datasetName}_${key}` : key;
if (e.storageArea === window.localStorage && e.key === prefixedKey) {
let newValue: T;
try {
newValue = e.newValue ? JSON.parse(e.newValue) : initialValue;
} catch {
newValue = initialValue;
}
callback(newValue);
}
};
window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
},
};
🤖 Prompt for AI Agents
In app/packages/state/src/jotai/modal.ts around lines 45 to 75, the storage
event handler references the global localStorage directly which can throw in
restricted environments; update the handler to use window.localStorage
consistently and guard the comparison by checking that window.localStorage is
defined before comparing e.storageArea to it (e.g., get a localStorageRef =
typeof window.localStorage !== "undefined" ? window.localStorage : null and only
perform the e.storageArea === localStorageRef check when localStorageRef is
non-null).
Comment on lines +27 to +31
- Increased maximum length for the name of a dataset or view to over 1000
characters.
- Fixed a bug where the UI would allow exporting media greater than 100 MB, even
though that is not supported, resulting in the download hanging.

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Resolve inconsistency on name length limit and avoid duplication in Enterprise notes

Enterprise says “over 1000” while OS says “1551”. Either align Enterprise to “1551” or drop the Enterprise bullet to avoid duplicating an OS change under “Includes all updates from...”. Suggest:

  • If unique Enterprise behavior: specify exactly what differs (e.g., UI limit vs backend limit) and keep the exact number.
  • If not unique: remove the Enterprise bullet.

Also applies to: 67-69


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Extract the exact content of release-notes.rst to verify the inconsistency claim
cat docs/source/release-notes.rst

Length of output: 327678


Resolve duplicated/inconsistent dataset-name length in release notes

Enterprise 2.13.0 (App) states "over 1000" while FiftyOne 1.10.0 (Core) states the limit was increased "from 100 to 1551" — either change the Enterprise bullet to "1551" if it's the same change, or remove the Enterprise bullet (it's redundant because Enterprise "Includes all updates from FiftyOne 1.10.0"). File: docs/source/release-notes.rst (Enterprise 2.13.0 App section and FiftyOne 1.10.0 Core section).

🤖 Prompt for AI Agents
In docs/source/release-notes.rst around lines 27 to 31, the Enterprise 2.13.0
(App) bullet claims "over 1000" for dataset/view name length which conflicts
with the FiftyOne 1.10.0 (Core) note saying it increased "from 100 to 1551";
update the Enterprise bullet to match the Core change by replacing "over 1000"
with "1551" (or remove the Enterprise bullet entirely if you prefer to avoid
redundancy since Enterprise includes all Core updates) so the two sections are
consistent.
Comment on lines +48 to +59
FiftyOne 1.10.0
--------------
*Released October 31, 2025*

Model Zoo

- Added `FiftyOneTransformerForPoseEstimation` to support transformer-based
keypoint prediction models (
`ViTPose <https://huggingface.co/docs/transformers/model_doc/vitpose#vitpose>`_
) to the fiftyone model zoo.
`#6371 <https://github.com/voxel51/fiftyone/pull/6371>`_

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Capitalize “FiftyOne Model Zoo” and tighten phrasing

Use consistent branding and tidy punctuation. Proposed edit:

- Added `FiftyOneTransformerForPoseEstimation` to support transformer-based
-  keypoint prediction models (
-  `ViTPose <https://huggingface.co/docs/transformers/model_doc/vitpose#vitpose>`_
-  ) to the fiftyone model zoo.
+ Added `FiftyOneTransformerForPoseEstimation` to support transformer-based
+ keypoint prediction models (e.g.,
+ `ViTPose <https://huggingface.co/docs/transformers/model_doc/vitpose#vitpose>`_)
+ in the FiftyOne Model Zoo.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FiftyOne 1.10.0
--------------
*Released October 31, 2025*
Model Zoo
- Added `FiftyOneTransformerForPoseEstimation` to support transformer-based
keypoint prediction models (
`ViTPose <https://huggingface.co/docs/transformers/model_doc/vitpose#vitpose>`_
) to the fiftyone model zoo.
`#6371 <https://github.com/voxel51/fiftyone/pull/6371>`_
FiftyOne 1.10.0
--------------
*Released October 31, 2025*
Model Zoo
- Added `FiftyOneTransformerForPoseEstimation` to support transformer-based
keypoint prediction models (e.g.,
`ViTPose <https://huggingface.co/docs/transformers/model_doc/vitpose#vitpose>`_)
in the FiftyOne Model Zoo.
`#6371 <https://github.com/voxel51/fiftyone/pull/6371>`_
🤖 Prompt for AI Agents
In docs/source/release-notes.rst around lines 48 to 59, the section heading and
phrasing need tightening: change the section title from "Model Zoo" to "FiftyOne
Model Zoo" (capitalized as requested), reword the bullet to a single concise
sentence such as "Added FiftyOneTransformerForPoseEstimation to the FiftyOne
Model Zoo to support transformer-based keypoint prediction models (ViTPose)."
ensure the ViTPose URL remains as the inline reference and the PR link (`#6371`)
stays at the end of the bullet with correct punctuation and balanced parentheses
and periods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

10 participants