Skip to content

Conversation

@chmurson
Copy link
Collaborator

@chmurson chmurson commented Aug 27, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Improved code-to-PDF sync by filtering out malformed/oversized layout blocks, reducing misaligned highlights and unexpected jumps.
    • Made end-of-content markers and stray breaks non-interactive to prevent accidental clicks/selections in the PDF text layer.
  • New Features

    • Added temporary on-screen debug overlays that highlight matched PDF blocks for visual verification; overlays auto-revert after a short interval.
  • Developer Tools

    • Added a development-only feature-flag system and devtools initializer to list, toggle, and manage flags for dev-only debugging.

@netlify
Copy link

netlify bot commented Aug 27, 2025

Deploy Preview for graypaper-reader ready!

Name Link
🔨 Latest commit 1fa5547
🔍 Latest deploy log https://app.netlify.com/projects/graypaper-reader/deploys/68af28a2b0440900085e760b
😎 Deploy Preview https://deploy-preview-297--graypaper-reader.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Aug 27, 2025

Walkthrough

Adds size-based filtering for “pathological” Synctex blocks in lookup and range APIs, a dev-only debug rectangle drawer, minor Synctex-load refactor, a devtools feature-flag system (including DEBUG_DRAW_BLOCKS), and disables pointer events on PDF text-layer end-of-content/br elements.

Changes

Cohort / File(s) Summary
Synctex block filtering & control flow
src/components/CodeSyncProvider/CodeSyncProvider.tsx
Adds module-scoped isPathologicalBlock(block) and applies it to skip/filter pathological blocks in getSynctexBlockAtLocation and getSynctexBlockRange; updates Synctex load to assign loaded synctex to a local variable before setting state.
Debug drawing helper
src/components/CodeSyncProvider/debugDrawBlock.ts
New exported debugDrawBlock(page, rectParams) that locates per-page canvas, caches image data, draws a temporary red outline rectangle from normalized coords, and reverts the drawing after a timeout.
PDF text layer interaction behavior
src/components/PdfViewer/PdfViewer.css
Adds rule disabling pointer events on .textLayer .endOfContent and .textLayer br so those elements are non-interactive.
Devtools feature-flag infra
src/devtools/featureFlags.ts, src/devtools/index.ts, src/devtools/initDevTools.ts, src/devtools/useFeatureFlag.tsx
Adds development-only feature-flag system: FEATURE_FLAGS (includes DEBUG_DRAW_BLOCKS), FeatureFlagsManager singleton (featureFlags), isFeatureEnabled helper, useFeatureFlag hook and withFeatureFlag HOC, initDevTools() to expose window.gpDev on localhost, and a devtools index re-exporting APIs and isDevelopment().
App bootstrap change
src/main.tsx
Calls initDevTools() during startup and nests App inside LocationProvider within MetadataProvider.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant Pdf as PdfViewer
    participant CodeSync as CodeSyncProvider
    participant Synctex as SynctexData
    participant Debug as debugDrawBlock
    participant IsPath as isPathologicalBlock

    User->>Pdf: click / request location/range
    Pdf->>CodeSync: getSynctexBlockAtLocation(point)
    CodeSync->>Synctex: fetch candidate blocks
    loop per candidate
        CodeSync->>IsPath: isPathologicalBlock(block)
        IsPath-->>CodeSync: true/false
        alt non-pathological
            CodeSync->>Debug: debugDrawBlock(page, rect) [if DEBUG_DRAW_BLOCKS]
            Debug-->>CodeSync: (draw + auto-undo)
            CodeSync-->>Pdf: return matching block
        else pathological
            Note right of CodeSync: skip block
        end
    end

    Pdf->>CodeSync: getSynctexBlockRange(range)
    CodeSync->>Synctex: compute raw rangeOfBlocks
    CodeSync->>IsPath: filter pathological -> goodBlocks
    CodeSync->>Debug: draw each goodBlock (if feature enabled)
    CodeSync-->>Pdf: return goodBlocks

    Note over CodeSync,Synctex: On synctex load: assign to local var, perform transient pass, then set synctex state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • DrEverr
  • mateuszsikora

Poem

A rabbit drew a tiny red line,
Skipped the lumps that broke the sign.
It marked the good, then wiped away,
Synced the hops and went to play.
Hop, debug, and tidy day 🐇✨


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe52fb and 1fa5547.

📒 Files selected for processing (1)
  • src/components/CodeSyncProvider/CodeSyncProvider.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/CodeSyncProvider/CodeSyncProvider.tsx
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-pathological-selection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 0

🧹 Nitpick comments (4)
src/components/CodeSyncProvider/CodeSyncProvider.tsx (4)

31-34: Tighten type and clarify comment; consider extracting thresholds.

Use the project’s ISynctexBlock type (or a Pick) and fix the comment grammar. Optionally extract the “magic numbers” to named constants for easier tuning and A/B experiments.

Apply within this hunk:

-const isPathologicalBlock = (block: { width: number; height: number }) => {
-  /* checking simple for dimension is an naive attempt to find too big blocks; there is a risk that it matches healthy big blocks */
+const isPathologicalBlock = (block: Pick<ISynctexBlock, "width" | "height">) => {
+  /* Simple dimension check to catch oversized blocks; may misclassify legitimately large blocks. */
   return (block.height > 0.1 && block.width > 0.1) || block.height > 0.5 || block.width > 0.5;
 };

Optionally (outside this hunk), define:

const PATHOLOGICAL_MAX_DIM = 0.5;
const PATHOLOGICAL_BOTH_DIM = 0.1;

and use them in the predicate.


65-67: Skipping pathological blocks: confirm behavior when all candidates are filtered.

If every block on a page (rare but possible) is flagged pathological, lastMatch remains null. Verify downstream UX (e.g., cursor feedback) degrades gracefully; consider a fallback to nearest non-pathological neighbor or relaxing thresholds in that edge case.


112-116: Range filtering may return an empty selection; consider a small fallback.

Filtering after slice can drop both ends and yield an empty array, which can look like a lost selection. Optionally add a bounded expansion to find nearby non-pathological blocks.

Apply within this hunk:

-        synctexData.blocksByPage
-          .get(startBlockId.pageNumber)
-          ?.slice(startBlockId.index, endBlockId.index + 1)
-          .filter((block) => !isPathologicalBlock(block)) || []
+        (() => {
+          const pageBlocks = synctexData.blocksByPage.get(startBlockId.pageNumber) ?? [];
+          const slice = pageBlocks.slice(startBlockId.index, endBlockId.index + 1);
+          const filtered = slice.filter((b) => !isPathologicalBlock(b));
+          if (filtered.length) return filtered;
+          const MAX_EXPAND = 2;
+          for (let d = 1; d <= MAX_EXPAND; d++) {
+            const left = Math.max(startBlockId.index - d, 0);
+            const right = Math.min(endBlockId.index + d, pageBlocks.length - 1);
+            const attempt = pageBlocks.slice(left, right + 1).filter((b) => !isPathologicalBlock(b));
+            if (attempt.length) return attempt;
+          }
+          return [];
+        })()

55-55: Remove or gate stray debug logs

Avoid noisy logs in hot paths. Found the following occurrences:

  • src/components/CodeSyncProvider/CodeSyncProvider.tsx (line 55)
  •   console.log("start getSynctexBlockAtLocation");
    
  •   // console.debug("getSynctexBlockAtLocation"); // optionally gate behind DEV
    
    
    
  • src/components/PdfViewer/SelectionRenderer/SelectionRenderer.tsx (line 29)
  • console.debug("scroll: checking if we need to scroll");
  • // console.debug("scroll: checking if we need to scroll"); // optionally gate behind DEV

To ensure no other stray logs remain, you can re-run:  
```shell
rg -nP '\bconsole\.(log|debug)\(' -g '*.ts' -g '*.tsx' -C2 src/
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e13b7ca and c9561cb.

📒 Files selected for processing (2)
  • src/components/CodeSyncProvider/CodeSyncProvider.tsx (4 hunks)
  • src/components/PdfViewer/PdfViewer.css (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.css

⚙️ CodeRabbit configuration file

Review CSS code with focus on Tailwind CSS 4.x best practices and conventions. Ensure compatibility with Tailwind 4.x features and syntax.

Files:

  • src/components/PdfViewer/PdfViewer.css
**/*.{js,jsx,ts,tsx}

⚙️ CodeRabbit configuration file

When reviewing Tailwind CSS classes, ensure they follow Tailwind 4.x conventions and suggest modern 4.x alternatives for deprecated patterns.

Files:

  • src/components/CodeSyncProvider/CodeSyncProvider.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - graypaper-reader
  • GitHub Check: Header rules - graypaper-reader
  • GitHub Check: Pages changed - graypaper-reader
🔇 Additional comments (1)
src/components/PdfViewer/PdfViewer.css (1)

52-54: Good fix: prevent non-text nodes from intercepting clicks in the text layer.

Disabling pointer events on .endOfContent and br will reduce spurious hit-testing during selection. Looks safe given PDF.js’ structure.

Copy link

@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: 8

🧹 Nitpick comments (4)
src/components/CodeSyncProvider/debugDrawBlock.ts (2)

6-7: Add proper return type annotation for early returns

The function returns void | (() => void), but early returns don't explicitly return undefined. While JavaScript handles this, it's better to be explicit for consistency with the undo function return.

-  if (!canvas) return;
+  if (!canvas) return undefined;

   const context = canvas.getContext("2d");

-  if (!context) return;
+  if (!context) return undefined;

Also applies to: 10-11


51-59: Consider making the timeout duration configurable

The 2500ms timeout is hardcoded. Consider making this configurable for different debugging scenarios.

-export function debugDrawBlock(page: number, rectParams: { width: number; height: number; top: number; left: number }) {
+export function debugDrawBlock(page: number, rectParams: { width: number; height: number; top: number; left: number }, duration = 2500) {
   const removeCallback = drawRectangle(page, rectParams);

   if (!removeCallback) return;

   setTimeout(() => {
     removeCallback();
-  }, 2500);
+  }, duration);
}
src/components/CodeSyncProvider/CodeSyncProvider.tsx (2)

137-138: Remove unnecessary || [] fallbacks

The filter method always returns an array, so the || [] fallback is redundant.

-  const badBlocks = rangeOfBlocks.filter((block) => isPathologicalBlock(block)) || [];
-  const goodBlocks = rangeOfBlocks.filter((block) => !isPathologicalBlock(block)) || [];
+  const badBlocks = rangeOfBlocks.filter((block) => isPathologicalBlock(block));
+  const goodBlocks = rangeOfBlocks.filter((block) => !isPathologicalBlock(block));

137-144: Consider performance optimization for block filtering

You're iterating through rangeOfBlocks twice to separate good and bad blocks. Consider using a single pass with reduce or a simple loop.

-  const badBlocks = rangeOfBlocks.filter((block) => isPathologicalBlock(block));
-  const goodBlocks = rangeOfBlocks.filter((block) => !isPathologicalBlock(block));
+  const goodBlocks: ISynctexBlock[] = [];
+  const badBlocks: ISynctexBlock[] = [];
+  for (const block of rangeOfBlocks) {
+    if (isPathologicalBlock(block)) {
+      badBlocks.push(block);
+    } else {
+      goodBlocks.push(block);
+    }
+  }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c9561cb and 98f42a0.

📒 Files selected for processing (2)
  • src/components/CodeSyncProvider/CodeSyncProvider.tsx (6 hunks)
  • src/components/CodeSyncProvider/debugDrawBlock.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,jsx,ts,tsx}

⚙️ CodeRabbit configuration file

When reviewing Tailwind CSS classes, ensure they follow Tailwind 4.x conventions and suggest modern 4.x alternatives for deprecated patterns.

Files:

  • src/components/CodeSyncProvider/debugDrawBlock.ts
  • src/components/CodeSyncProvider/CodeSyncProvider.tsx
🧬 Code graph analysis (1)
src/components/CodeSyncProvider/CodeSyncProvider.tsx (1)
src/components/CodeSyncProvider/debugDrawBlock.ts (1)
  • debugDrawBlock (51-59)
🪛 GitHub Actions: Node.js CI
src/components/CodeSyncProvider/CodeSyncProvider.tsx

[error] 48-48: Biome linter: 'forEach' usage detected. Prefer 'for...of' instead of 'forEach'. (lint/complexity/noForEach)


[error] 49-49: Biome linter: 'forEach' usage detected. Prefer 'for...of' instead of 'forEach'. (lint/complexity/noForEach)


[error] 50-50: Biome linter: 'forEach' usage detected. Prefer 'for...of' instead of 'forEach'. (lint/complexity/noForEach)


[error] 59-59: Biome linter: 'forEach' usage detected. Prefer 'for...of' instead of 'forEach'. (lint/complexity/noForEach)


[error] 60-60: Biome linter: 'forEach' usage detected. Prefer 'for...of' instead of 'forEach'. (lint/complexity/noForEach)


[error] 142-142: Biome linter: 'forEach' usage detected. Prefer 'for...of' instead of 'forEach'. (lint/complexity/noForEach)

🔇 Additional comments (2)
src/components/CodeSyncProvider/debugDrawBlock.ts (1)

37-37: Good catch on the rectangle drawing fix!

The comment indicates this was a bug fix where the bottom line calculation was corrected from subtraction to addition. This ensures the rectangle is drawn correctly.

src/components/CodeSyncProvider/CodeSyncProvider.tsx (1)

88-90: Good implementation of pathological block filtering

The filtering logic is correctly applied to skip pathological blocks during location matching, which should prevent selection issues with oversized blocks.

Copy link

@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: 0

♻️ Duplicate comments (2)
src/components/CodeSyncProvider/debugDrawBlock.ts (2)

1-1: Provide cleanup API for cached ImageData to avoid leaks

The per-page Map grows unbounded and never clears. Add an explicit cleanup.

Apply:

+export function clearDebugState(page?: number) {
+  if (page !== undefined) {
+    statePerPage.delete(page);
+  } else {
+    statePerPage.clear();
+  }
+}

42-47: Delete cached snapshot after undo to free memory

After restoration, the entry can be removed safely.

   return function undo() {
     const savedState = statePerPage.get(page);
     if (savedState) {
       context.putImageData(savedState, 0, 0);
+      statePerPage.delete(page);
     }
   };
🧹 Nitpick comments (14)
src/components/CodeSyncProvider/debugDrawBlock.ts (3)

12-15: Handle canvas resize: refresh snapshot if dimensions changed

If the canvas is re-rendered/zoomed, the cached ImageData may no longer match.

-  if (!statePerPage.has(page)) {
-    const savedState = context.getImageData(0, 0, canvas.width, canvas.height);
-    statePerPage.set(page, savedState);
-  }
+  const prev = statePerPage.get(page);
+  if (!prev || prev.width !== canvas.width || prev.height !== canvas.height) {
+    const savedState = context.getImageData(0, 0, canvas.width, canvas.height);
+    statePerPage.set(page, savedState);
+  }

24-27: Clamp normalized rect to [0,1] to avoid out-of-bounds drawing

Protect against pathological inputs or rounding.

-  const x1 = left * canvasWidth;
-  const y1 = top * canvasHeight;
-  const rectWidth = width * canvasWidth;
-  const rectHeight = height * canvasHeight;
+  const clamp = (v: number, min = 0, max = 1) => Math.min(max, Math.max(min, v));
+  const x1 = clamp(left) * canvasWidth;
+  const y1 = clamp(top) * canvasHeight;
+  const rectWidth = clamp(width) * canvasWidth;
+  const rectHeight = clamp(height) * canvasHeight;

4-4: Prefer typed querySelector for better type-safety

Avoid the cast; let TS infer the element type.

-  const canvas = document.querySelector(`[data-page-number="${page}"] canvas`) as HTMLCanvasElement;
+  const canvas = document.querySelector<HTMLCanvasElement>(`[data-page-number="${page}"] canvas`);
src/devtools/featureFlags.ts (3)

22-24: Make isDevelopment() SSR-safe and DRY with central helper

Direct window access can break in SSR/tests; also duplicated in devtools/index.ts.

-  private isDevelopment(): boolean {
-    return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
-  }
+  private isDevelopment(): boolean {
+    return typeof window !== "undefined" &&
+      (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
+  }

Follow-up: consider extracting to a single shared util (e.g., devtools/env.ts) and reusing here and in index.ts.


30-43: Defensive access to sessionStorage

Some environments (e.g., privacy modes) throw on sessionStorage access.

-  isEnabled(flagKey: FeatureFlagKey): boolean {
+  isEnabled(flagKey: FeatureFlagKey): boolean {
     if (!this.isDevelopment()) {
       return false;
     }
 
     const flag = FEATURE_FLAGS[flagKey];
-    const storedValue = sessionStorage.getItem(this.getStorageKey(flag.key));
+    let storedValue: string | null = null;
+    try {
+      storedValue = typeof sessionStorage !== "undefined"
+        ? sessionStorage.getItem(this.getStorageKey(flag.key))
+        : null;
+    } catch {
+      // ignore and fall back to default
+    }

45-61: Emit a CustomEvent on flag changes; let hooks subscribe instead of polling

Improves responsiveness and removes reliance on sessionStorage 'storage' events.

+  private emitChange(flagKey: FeatureFlagKey, value?: boolean): void {
+    try {
+      window.dispatchEvent(
+        new CustomEvent("gp:featureFlagChanged", { detail: { flagKey, value } }),
+      );
+    } catch {
+      // no-op
+    }
+  }

Then after each set/remove in:

  • toggle(): call this.emitChange(flagKey, newValue)
  • enable(): this.emitChange(flagKey, true)
  • disable(): this.emitChange(flagKey, false)
  • reset(): this.emitChange(flagKey)
  • resetAll(): after loop, this.emitChange("DEBUG_DRAW_BLOCKS" as FeatureFlagKey) or emit per flag in loop.

Also applies to: 62-72, 73-83, 84-94, 95-107

src/devtools/useFeatureFlag.tsx (2)

7-29: Stabilize effect and subscribe to explicit flag-change event

Avoid restarting interval on each state flip; listen to gp:featureFlagChanged.

-  useEffect(() => {
+  useEffect(() => {
     const handleStorageChange = (e: StorageEvent) => {
       if (e.storageArea === sessionStorage && e.key?.startsWith("gp_feature_flag_")) {
-        setEnabled(isFeatureEnabled(flagKey));
+        setEnabled(isFeatureEnabled(flagKey));
       }
     };
 
     const checkFlagState = () => {
-      const currentState = isFeatureEnabled(flagKey);
-      if (currentState !== enabled) {
-        setEnabled(currentState);
-      }
+      setEnabled(isFeatureEnabled(flagKey));
     };
 
     const intervalId = setInterval(checkFlagState, 1000);
 
+    const handleCustomEvent = () => setEnabled(isFeatureEnabled(flagKey));
+    window.addEventListener("gp:featureFlagChanged", handleCustomEvent as EventListener);
     window.addEventListener("storage", handleStorageChange);
 
     return () => {
       clearInterval(intervalId);
+      window.removeEventListener("gp:featureFlagChanged", handleCustomEvent as EventListener);
       window.removeEventListener("storage", handleStorageChange);
     };
-  }, [flagKey, enabled]);
+  }, [flagKey]);

Note: sessionStorage typically does not fire storage events within the same tab; the custom event closes that gap.


34-47: Preserve component name in HOC for better DevTools DX

 export function withFeatureFlag<P extends object>(
   flagKey: FeatureFlagKey,
   Component: React.ComponentType<P>,
 ): React.ComponentType<P> {
-  return function FeatureFlagWrapper(props: P) {
+  function FeatureFlagWrapper(props: P) {
     const isEnabled = useFeatureFlag(flagKey);
 
     if (!isEnabled) {
       return null;
     }
 
     return <Component {...props} />;
-  };
+  }
+  FeatureFlagWrapper.displayName = `WithFeatureFlag(${Component.displayName || Component.name || "Component"})`;
+  return FeatureFlagWrapper;
 }
src/main.tsx (1)

7-7: Avoid bundling devtools in production builds via dynamic import

Tree-shakes dev-only code; keeps gpDev out of prod bundles.

-import { initDevTools } from "./devtools/initDevTools";
+// Load devtools only in development
+if (import.meta.env.DEV) {
+  import("./devtools/initDevTools").then((m) => m.initDevTools());
+}
@@
-initDevTools();

Also applies to: 17-18

src/devtools/index.ts (1)

8-10: SSR-safe isDevelopment() and eliminate duplication with featureFlags

Guard window access and consider centralizing this helper.

 export function isDevelopment(): boolean {
-  return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
+  return typeof window !== "undefined" &&
+    (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
 }

Follow-up: extract to devtools/env.ts and reuse here and in FeatureFlagsManager.

src/devtools/initDevTools.ts (4)

1-1: Import and use central isDevelopment to enable tree-shaking and consistent gating

Leverage the shared devtools helper so bundlers can drop this code in production.

-import { type FeatureFlagKey, featureFlags } from "./featureFlags";
+import { type FeatureFlagKey, featureFlags } from "./featureFlags";
+import { isDevelopment } from ".";

3-15: Avoid type/runtime mismatch: make gpDev optional (or always stub it) and factor a reusable API type

Right now the type claims gpDev always exists, but the function early-returns without assigning it off-localhost. Make it optional (or always define a stub); factoring a type also tightens reuse.

 declare global {
   interface Window {
-    gpDev: {
-      listFlags: () => void;
-      toggle: (flagKey: FeatureFlagKey) => boolean;
-      enable: (flagKey: FeatureFlagKey) => void;
-      disable: (flagKey: FeatureFlagKey) => void;
-      reset: (flagKey: FeatureFlagKey) => void;
-      resetAll: () => void;
-      help: () => void;
-    };
+    gpDev?: GpDevAPI;
   }
 }

Add this type near the imports:

type GpDevAPI = Readonly<{
  listFlags: () => void;
  toggle: (flagKey: FeatureFlagKey) => boolean;
  enable: (flagKey: FeatureFlagKey) => void;
  disable: (flagKey: FeatureFlagKey) => void;
  reset: (flagKey: FeatureFlagKey) => void;
  resetAll: () => void;
  help: () => void;
}>;

17-21: Strengthen environment/host gating and SSR-safety; add IPv6/0.0.0.0; allow HMR idempotency

Prevents server-side crashes, avoids accidental exposure on custom dev hosts, and dedupes re-inits.

 export function initDevTools(): void {
-  if (window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") {
-    return;
-  }
+  if (typeof window === "undefined") return; // SSR-safety
+  if (!isDevelopment) return; // build-time tree-shake
+  const host = window.location.hostname;
+  const isLocalhost =
+    host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0";
+  if (!isLocalhost) return;
+  if (window.gpDev) return; // idempotent (HMR)

22-30: Harden gpDev: freeze API and expose as non-enumerable, read-only property

Prevents accidental mutation in console sessions and hides it from object iterations while keeping DX.

-  window.gpDev = {
-    listFlags: () => featureFlags.listFlags(),
-    toggle: (flagKey: FeatureFlagKey) => featureFlags.toggle(flagKey),
-    enable: (flagKey: FeatureFlagKey) => featureFlags.enable(flagKey),
-    disable: (flagKey: FeatureFlagKey) => featureFlags.disable(flagKey),
-    reset: (flagKey: FeatureFlagKey) => featureFlags.reset(flagKey),
-    resetAll: () => featureFlags.resetAll(),
-    help: () => featureFlags.help(),
-  };
+  const api: GpDevAPI = Object.freeze({
+    listFlags: () => featureFlags.listFlags(),
+    toggle: (flagKey: FeatureFlagKey) => featureFlags.toggle(flagKey),
+    enable: (flagKey: FeatureFlagKey) => featureFlags.enable(flagKey),
+    disable: (flagKey: FeatureFlagKey) => featureFlags.disable(flagKey),
+    reset: (flagKey: FeatureFlagKey) => featureFlags.reset(flagKey),
+    resetAll: () => featureFlags.resetAll(),
+    help: () => featureFlags.help(),
+  });
+  Object.defineProperty(window, "gpDev", {
+    value: api,
+    writable: false,
+    configurable: false,
+    enumerable: false,
+  });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 98f42a0 and 9fe52fb.

📒 Files selected for processing (7)
  • src/components/CodeSyncProvider/CodeSyncProvider.tsx (5 hunks)
  • src/components/CodeSyncProvider/debugDrawBlock.ts (1 hunks)
  • src/devtools/featureFlags.ts (1 hunks)
  • src/devtools/index.ts (1 hunks)
  • src/devtools/initDevTools.ts (1 hunks)
  • src/devtools/useFeatureFlag.tsx (1 hunks)
  • src/main.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/CodeSyncProvider/CodeSyncProvider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,jsx,ts,tsx}

⚙️ CodeRabbit configuration file

When reviewing Tailwind CSS classes, ensure they follow Tailwind 4.x conventions and suggest modern 4.x alternatives for deprecated patterns.

Files:

  • src/devtools/featureFlags.ts
  • src/devtools/initDevTools.ts
  • src/components/CodeSyncProvider/debugDrawBlock.ts
  • src/devtools/index.ts
  • src/main.tsx
  • src/devtools/useFeatureFlag.tsx
🧬 Code graph analysis (4)
src/devtools/initDevTools.ts (1)
src/devtools/featureFlags.ts (2)
  • FeatureFlagKey (17-17)
  • featureFlags (158-158)
src/devtools/index.ts (1)
src/devtools/featureFlags.ts (1)
  • isDevelopment (22-24)
src/main.tsx (1)
src/devtools/initDevTools.ts (1)
  • initDevTools (17-46)
src/devtools/useFeatureFlag.tsx (1)
src/devtools/featureFlags.ts (3)
  • FeatureFlagKey (17-17)
  • isFeatureEnabled (160-162)
  • isEnabled (30-43)
🔇 Additional comments (3)
src/components/CodeSyncProvider/debugDrawBlock.ts (1)

36-36: LGTM: corrected rectangle path

The switch to addition on the bottom-right corner is correct.

src/devtools/initDevTools.ts (2)

32-46: LGTM on developer-facing console UX

Concise banner and actionable hints; non-intrusive. Good.


22-25: No action needed: toggle() return type is correct

I’ve verified that in src/devtools/featureFlags.ts the toggle(flagKey: FeatureFlagKey) method is explicitly declared to return a boolean (line 45). Therefore, the passthrough in initDevTools.ts correctly preserves the boolean return type and needs no change.

@chmurson chmurson merged commit 746a7ae into main Aug 27, 2025
8 checks passed
@chmurson chmurson deleted the fix-pathological-selection branch August 27, 2025 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant