Skip to content

Conversation

@sid597
Copy link
Collaborator

@sid597 sid597 commented Aug 10, 2025

Summary by CodeRabbit

  • New Features

    • Customizable left sidebar with collapsible Global and Personal sections, clickable references, truncation, and per-section open state.
    • New Settings panels to add, remove, and configure global and personal sidebar sections and their children.
    • Sidebar auto-mounts into the app UI.
  • Improvements

    • Dynamic config loading and synchronization with persistent settings and error handling.

For Reviewing:

The settings are going to be saved in roam/js/discourse-graph page, the structure is going to be as in following example:

- Left Sidebar
    - siddharth yadav/Personal Section
        - CLM
        - qa
            - Settings
                - Type
                    - blocks-and-pages
                - Collapsable?
                    - true
                - Open?
                    - true
                - Truncate-result?
                    - 75
            - Children
                - [[QUE]] - st test te
                - May 13th, 2025
                - Test/Home
        - July 16th, 2025
            - Settings
                - Type
                    - blocks-and-pages
                - Collapsable?
                    - true
                - Open?
                    - true
                - Truncate-result?
                    - 75
            - Children
                - Test/Home
                - July 16th, 2025
    - Global Section
        - Open
        - Children
            - Canvas
            - May 13th, 2025
            - CLM
            - [[ISS]] - clicking to open nodes in sidebar from canvas was inconsistent or undiscoverable for Ben
    - Matt/Personal Section
        - Test/Home
        - a test page with Trang

We extract the rendering data using the logic:

  • Find Left Sidebar block
  • Find Global Section
    • extract the pages and open condition
  • Find {username}/Personal Section
    • For all the children, if it has Settings and Children tab use the subgroup rendering otherwise normal

On a section header user can shift+click to open in sidebar, double click to open page directly,

@linear
Copy link

linear bot commented Aug 10, 2025

ENG-678 Create MVP0

@supabase
Copy link

supabase bot commented Aug 10, 2025

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@sid597
Copy link
Collaborator Author

sid597 commented Aug 10, 2025

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 10, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 10, 2025

Caution

Review failed

An error occurred during the review process. Please try again later.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 Nitpick comments (11)
apps/roam/src/components/settings/Settings.tsx (1)

169-181: Make Left Sidebar header styling consistent with other sections

Other section headers use “text-lg font-semibold text-neutral-dark”. Mirror that here for visual consistency.

-          <SectionHeader>Left Sidebar</SectionHeader>
+          <SectionHeader className="text-lg font-semibold text-neutral-dark">
+            Left Sidebar
+          </SectionHeader>
apps/roam/src/components/LeftSidebarView.tsx (2)

146-174: Simplify single/double-click detection

Manual timers work but are brittle. Consider using onDoubleClick for the “open” action and onClick for “toggle collapse.” Keep shift-click behavior for opening in sidebar.

Example:

<div
  className="sidebar-title-button"
  onClick={() => collapsable && setIsOpen((p) => !p)}
  onDoubleClick={(e) => {
    e.preventDefault();
    if (titleRef.type === "page")
      void window.roamAlphaAPI.ui.mainWindow.openPage({
        page: { uid: getPageUidByPageTitle(titleRef.title) },
      });
    else if (titleRef.type === "block")
      void window.roamAlphaAPI.ui.mainWindow.openBlock({
        block: { uid: titleRef.uid },
      });
  }}
  onMouseDown={(e) => {
    if (e.shiftKey) {
      // preserve shift-click open behavior
      e.preventDefault();
      if (titleRef.type === "page")
        void openTarget(e as any, { kind: "page", title: titleRef.title });
      else if (titleRef.type === "block")
        void openTarget(e as any, { kind: "block", uid: titleRef.uid });
    }
  }}
/>

59-95: Optional: improve accessibility of clickable rows

Clickable divs should expose role and keyboard handlers.

-      <div key={child.uid} style={{ padding: "4px 0 4px 4px" }}>
-        <div
+      <div key={child.uid} style={{ padding: "4px 0 4px 4px" }}>
+        <div
           className={`section-child-item ${isTodo ? "todo-item" : "page"}`}
           style={{
             color: "#495057",
             lineHeight: 1.5,
             borderRadius: 3,
             cursor: "pointer",
           }}
-          onClick={onClick}
+          role="button"
+          tabIndex={0}
+          onClick={onClick}
+          onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && onClick(e as any)}
         >
           {label}
         </div>
       </div>

Apply similarly to the simple personal section item.

apps/roam/src/components/settings/LeftSidebar.tsx (8)

3-3: Avoid recomputing all page names on every render; memoize getAllPageNames().

getAllPageNames() can be expensive in large graphs. Memoize once per mount and reuse.

-import React, { useCallback, useEffect, useState } from "react";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
 return (
   <div className="flex flex-col gap-4 p-1">
+    { /* Compute once */ }
+    { /* eslint-disable react-hooks/rules-of-hooks */ }
+    const allPageNames = useMemo(() => getAllPageNames(), []);
-        <AutocompleteInput
+        <AutocompleteInput
           key={getAutocompleteKey()}
           value={getPageInput()}
           setValue={setPageInput}
           placeholder="Add page…"
-          options={getAllPageNames()}
+          options={allPageNames}
           maxItemsDisplayed={50}
         />

If lint complains about hooks in JSX, move the useMemo to the top of the component body before return.

Also applies to: 127-129, 149-156


114-125: Remove setTimeout(…, 0) in controlled setters.

This anti-pattern risks state updates after unmount and complicates reasoning. Directly set state.

-const setPageInput = useCallback(
-  (value: string) => {
-    setTimeout(() => {
-      setNewPageInputs((prev) => ({
-        ...prev,
-        [globalSection.childrenUid]: value,
-      }));
-    }, 0);
-  },
+const setPageInput = useCallback(
+  (value: string) => {
+    setNewPageInputs((prev) => ({
+      ...prev,
+      [globalSection.childrenUid]: value,
+    }));
+  },
   [globalSection.childrenUid],
 );

If this was a workaround for AutocompleteInput focus/blur behavior, call it out in a comment or keep a minimal debounce with useRef instead of setTimeout.


175-179: Add accessible titles to icon-only buttons.

Improves a11y and UX.

-  <Button
+  <Button
     icon="trash"
     minimal
     small
-    onClick={() => void removePage(p)}
+    title="Remove page"
+    onClick={() => void removePage(p)}
   />

424-431: Memoize page list in Personal component too.

Avoid recomputing getAllPageNames() on every render for both the “Add child” and “Add section or page” inputs.

-  const [settingsDialogSectionUid, setSettingsDialogSectionUid] = useState<
+  const [settingsDialogSectionUid, setSettingsDialogSectionUid] = useState<
     string | null
   >(null);
   const [expandedChildLists, setExpandedChildLists] = useState<Set<string>>(
     new Set(),
   );
+  const allPageNames = useMemo(() => getAllPageNames(), []);
-          <AutocompleteInput
+          <AutocompleteInput
             key={childAutocompleteKeys[inputKey] || 0}
             value={childInput}
             setValue={setChildInput}
             placeholder="Add child page…"
-            options={getAllPageNames()}
+            options={allPageNames}
             maxItemsDisplayed={50}
           />
-          <AutocompleteInput
+          <AutocompleteInput
             key={autocompleteKey}
             value={newSectionInput}
             setValue={setNewSectionInput}
             placeholder="Add section or page…"
-            options={getAllPageNames()}
+            options={allPageNames}
             maxItemsDisplayed={50}
           />

Also applies to: 482-488, 196-209


497-498: Avoid returning Promises from event handlers; prefix with void.

Prevents unhandled Promise warnings and clarifies intent.

-            onClick={() => addSection(newSectionInput)}
+            onClick={() => void addSection(newSectionInput)}
-            onClick={() =>
-              addChildToSection(section.childrenUid!, childInput)
-            }
+            onClick={() => void addChildToSection(section.childrenUid!, childInput)}
-                onClick={() => removeChild(child)}
+                onClick={() => void removeChild(child)}

Also applies to: 433-440, 453-454


210-250: Centralize skeleton creation (single source-of-truth).

UI components shouldn’t mutate config shape. Prefer moving “ensure Left Sidebar skeleton” into a shared utility (e.g., in ~/utils/getLeftSidebarSettings or ~/utils/discourseConfigRef) so:

  • It runs once, deterministically.
  • All consumers see a consistent, validated tree.
  • UI code focuses on edits, not initialization.

I can draft ensureLeftSidebarSkeleton() and wire both components to call it. Say the word.


129-185: UX polish: surface errors to users, not just console.

Where create/delete operations fail, notify users (toaster/snackbar) so they understand that a change didn’t persist.

Examples:

  • add/remove page
  • add/remove section
  • add/remove child
  • convert to complex section

I can wire Blueprint Toaster or an existing notification helper here.

Also applies to: 466-597


131-138: Labels consistency (minor).

“Open” vs “Open?” and “Truncate-result?” style differs between Global and Personal. If these strings are only labels (not used for parsing in getLeftSidebarSettings), consider making them consistent for a cleaner UX.

Please confirm whether getLeftSidebarSettings parses by label text or by structure/UID only. If by label text, keep as-is to avoid breaking parsing.

Also applies to: 370-394

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8b3c8f and 1fe2bee.

📒 Files selected for processing (6)
  • apps/roam/src/components/LeftSidebarView.tsx (1 hunks)
  • apps/roam/src/components/settings/LeftSidebar.tsx (1 hunks)
  • apps/roam/src/components/settings/Settings.tsx (3 hunks)
  • apps/roam/src/utils/discourseConfigRef.ts (3 hunks)
  • apps/roam/src/utils/getLeftSidebarSettings.ts (1 hunks)
  • apps/roam/src/utils/initializeObserversAndListeners.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-19T22:34:23.619Z
Learnt from: CR
PR: DiscourseGraphs/discourse-graph#0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-07-19T22:34:23.619Z
Learning: Applies to apps/roam/**/*.{js,jsx,ts,tsx} : Use BlueprintJS 3 components and Tailwind CSS for platform-native UI in the Roam Research extension

Applied to files:

  • apps/roam/src/components/settings/Settings.tsx
🧬 Code Graph Analysis (5)
apps/roam/src/components/settings/Settings.tsx (1)
apps/roam/src/components/settings/LeftSidebar.tsx (2)
  • LeftSidebarGlobalSections (17-185)
  • LeftSidebarPersonalSections (187-599)
apps/roam/src/utils/discourseConfigRef.ts (1)
apps/roam/src/utils/getLeftSidebarSettings.ts (2)
  • LeftSidebarConfig (31-37)
  • getLeftSidebarSettings (39-52)
apps/roam/src/utils/initializeObserversAndListeners.ts (1)
apps/roam/src/components/LeftSidebarView.tsx (1)
  • mountLeftSidebarInto (353-395)
apps/roam/src/components/settings/LeftSidebar.tsx (3)
apps/roam/src/utils/discourseConfigRef.ts (1)
  • getFormattedConfigTree (31-57)
apps/roam/src/utils/renderNodeConfigPage.ts (1)
  • DISCOURSE_CONFIG_PAGE_TITLE (28-28)
apps/roam/src/utils/getLeftSidebarSettings.ts (1)
  • LeftSidebarPersonalSectionConfig (15-22)
apps/roam/src/utils/getLeftSidebarSettings.ts (1)
apps/roam/src/utils/getExportSettings.ts (4)
  • IntSetting (13-13)
  • BooleanSetting (12-12)
  • getUidAndBooleanSetting (56-62)
  • getUidAndIntSetting (49-55)
🔇 Additional comments (9)
apps/roam/src/utils/getLeftSidebarSettings.ts (3)

39-52: Solid top-level extraction flow

Locating "Left Sidebar" and delegating to global/personal parsers is clean and resilient to missing nodes via safe fallbacks.


54-73: Global section parsing looks correct

  • Presence-based boolean for "Open" aligns with existing getUidAndBooleanSetting contract.
  • Children and UIDs default safely when missing.

No changes needed.


75-116: Personal sections: good simple/complex split and safe fallbacks

  • isSimple derivation is clear.
  • Complex branch returns settings and children safely.

No changes needed here.

apps/roam/src/utils/discourseConfigRef.ts (1)

10-13: Left Sidebar config integrated correctly

  • Type FormattedConfigTree extended with leftSidebar
  • getFormattedConfigTree returns leftSidebar via getLeftSidebarSettings
  • Import path and types align with the new utility

Looks good.

Also applies to: 28-29, 55-56

apps/roam/src/components/settings/Settings.tsx (1)

29-33: Imports for Left Sidebar settings are correct

Components are co-located and namespacing matches.

apps/roam/src/utils/initializeObserversAndListeners.ts (3)

44-45: Correct dependency added

Importing mountLeftSidebarInto here is the right place for DOM integration.


106-116: Immediate mount is useful; ensure it’s coordinated with readiness

This pre-empts waiting for the observer. With the proposed onReady flow in LeftSidebarView, this is fine and avoids blank “starred” content removal until the sidebar has content.

Please validate end-to-end after applying the LeftSidebarView onReady change to ensure the default starred pages aren’t removed until the custom sidebar is ready.


118-131: Observer-based mount is appropriate and idempotent

Using className filter and wrapper scoping is sufficient. mountLeftSidebarInto guards via a fixed root id.

apps/roam/src/components/settings/LeftSidebar.tsx (1)

18-25: Default fallbacks cover missing config—no null-guards needed

The getFormattedConfigTreegetLeftSidebarSettings path always supplies defaults when nodes are absent:

  • leftSidebarNode?.children and childrenNode?.children both fall back to []
  • UIDs default to "" and BooleanSettings default to false
  • Personal sections likewise return empty arrays and default values

As a result, on initial render settings.leftSidebar.global.children is always an array (never undefined), and uid/value fields are non-null. No additional guards or early returns are required here.

@sid597
Copy link
Collaborator Author

sid597 commented Aug 11, 2025

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

✅ Actions performed

Full review triggered.

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: 5

♻️ Duplicate comments (5)
apps/roam/src/components/settings/LeftSidebar.tsx (2)

91-113: Harden addPage: trim, case-insensitive dedupe, and guard missing childrenUid

Prevents whitespace-only entries, case-variant duplicates, and failures if Children is missing.

-  const addPage = async (page: string) => {
-    if (!page || pages.some((p) => p.text === page)) {
-      return;
-    }
+  const addPage = async (page: string) => {
+    const name = page.trim();
+    if (!name) return;
+    if (pages.some((p) => p.text.toLowerCase() === name.toLowerCase())) return;
+    if (!globalSection.childrenUid) {
+      console.error("Global Section 'Children' UID missing.");
+      return;
+    }
     try {
       await createBlock({
         parentUid: globalSection.childrenUid,
         order: "last",
-        node: { text: page },
+        node: { text: name },
       });

386-446: Avoid non-null assertions on childrenUid; guard and handle missing

Prevents runtime crashes if parsing missed the Children node.

-    const inputKey = section.childrenUid!;
+    const inputKey = section.childrenUid;
+    if (!inputKey) return null;
     const childInput = sectionChildInputs[inputKey] || "";
@@
-              addChildToSection(section.childrenUid!, childInput);
+              addChildToSection(inputKey, childInput);
@@
-          <Button
+          <Button
             icon="plus"
             small
             minimal
             disabled={!childInput}
-            onClick={() => addChildToSection(section.childrenUid!, childInput)}
+            onClick={() => void addChildToSection(inputKey, childInput)}
           />
apps/roam/src/components/LeftSidebarView.tsx (2)

336-355: Avoid rendering with empty config

The component renders immediately even if the config is empty, which could cause the blank sidebar issue mentioned in past reviews.


360-372: Defer removal of default starred list until ready

The function removes the default starred-pages immediately, which can cause a blank sidebar if the LeftSidebarView isn't ready with content.

apps/roam/src/utils/getLeftSidebarSettings.ts (1)

138-151: Robustify defaults for invalid setting values

The default values only apply when the setting node or UID is missing, but not when the value is invalid (e.g., non-numeric for truncateResult).

🧹 Nitpick comments (7)
apps/roam/src/components/settings/Settings.tsx (1)

168-180: Align SectionHeader styling with other headers

For visual consistency, use the same typography as “Personal Settings” and “Global Settings”.

-          <SectionHeader>Left Sidebar</SectionHeader>
+          <SectionHeader className="text-lg font-semibold text-neutral-dark">
+            Left Sidebar
+          </SectionHeader>
apps/roam/src/utils/discourseConfigRef.ts (2)

10-13: Optional: import defaults-aware helper to harden parsing

To avoid undefined UIDs or missing children when the skeleton hasn’t been ensured, prefer the defaults-aware accessor.

-import {
-  LeftSidebarConfig,
-  getLeftSidebarSettings,
-} from "./getLeftSidebarSettings";
+import { LeftSidebarConfig, getLeftSidebarSettings } from "./getLeftSidebarSettings";
+import { getLeftSidebarSettingsWithDefaults } from "./ensureLeftSidebarStructure";

55-56: Optional: return leftSidebar with defaults to improve resilience

If the config tree is partially populated, using defaults avoids null-ish values.

-    leftSidebar: getLeftSidebarSettings(configTreeRef.tree),
+    leftSidebar: getLeftSidebarSettingsWithDefaults(configTreeRef.tree),
apps/roam/src/utils/initializeObserversAndListeners.ts (1)

118-131: Skip redundant mounts if root already exists

Minor optimization to avoid unnecessary ReactDOM.render calls when the observer fires multiple times for the same container.

   const leftSidebarObserver = createHTMLObserver({
     tag: "DIV",
     useBody: true,
     className: "starred-pages-wrapper",
     callback: (el) => {
       try {
         const container = el as HTMLDivElement;
-        mountLeftSidebarInto(container);
+        if (container.querySelector("#dg-left-sidebar-root")) return;
+        mountLeftSidebarInto(container);
       } catch (e) {
         console.error("[DG][LeftSidebar] leftSidebarObserver error", e);
       }
     },
   });
apps/roam/src/components/settings/LeftSidebar.tsx (1)

38-54: Remove debug log from initialization

Avoid noisy logs in production.

       try {
         await ensureLeftSidebarReady();
         const config = getFormattedConfigTree();
-        console.log("config", config);
         setSettings(config);
         setIsInitialized(true);
apps/roam/src/components/LeftSidebarView.tsx (1)

285-285: Consider persisting Global Section open state

The Global Section uses local React state for open/closed, unlike Personal Sections which persist to Roam blocks. This means the Global Section state won't survive page reloads.

Consider using the same persistence mechanism as Personal Sections for consistency, or document why this difference is intentional.

apps/roam/src/utils/getLeftSidebarSettings.ts (1)

144-151: Reconsider default values for collapsable and open settings

The defaults set both collapsable and open to false. This means sections are non-collapsible and closed by default, which seems inconsistent. Consider:

  • If not collapsible, the open state is irrelevant
  • If collapsible, defaulting to closed might hurt discoverability

Consider more intuitive defaults:

   const collapsableSetting = getBoolean("Collapsable?");
   if (!settingsNode?.uid || !collapsableSetting.uid) {
-    collapsableSetting.value = false;
+    collapsableSetting.value = true;
   }

   const openSetting = getBoolean("Open?");
   if (!settingsNode?.uid || !openSetting.uid) {
-    openSetting.value = false;
+    openSetting.value = true;
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8b3c8f and 14ee691.

📒 Files selected for processing (7)
  • apps/roam/src/components/LeftSidebarView.tsx (1 hunks)
  • apps/roam/src/components/settings/LeftSidebar.tsx (1 hunks)
  • apps/roam/src/components/settings/Settings.tsx (2 hunks)
  • apps/roam/src/utils/discourseConfigRef.ts (2 hunks)
  • apps/roam/src/utils/ensureLeftSidebarStructure.ts (1 hunks)
  • apps/roam/src/utils/getLeftSidebarSettings.ts (1 hunks)
  • apps/roam/src/utils/initializeObserversAndListeners.ts (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
apps/roam/src/utils/ensureLeftSidebarStructure.ts (2)
apps/roam/src/utils/renderNodeConfigPage.ts (1)
  • DISCOURSE_CONFIG_PAGE_TITLE (28-28)
apps/roam/src/utils/getLeftSidebarSettings.ts (3)
  • LeftSidebarConfig (34-40)
  • getLeftSidebarGlobalSectionConfig (65-84)
  • getLeftSidebarPersonalSectionConfig (86-123)
apps/roam/src/utils/discourseConfigRef.ts (1)
apps/roam/src/utils/getLeftSidebarSettings.ts (2)
  • LeftSidebarConfig (34-40)
  • getLeftSidebarSettings (42-63)
apps/roam/src/components/settings/Settings.tsx (1)
apps/roam/src/components/settings/LeftSidebar.tsx (2)
  • LeftSidebarGlobalSections (200-218)
  • LeftSidebarPersonalSections (587-605)
apps/roam/src/components/settings/LeftSidebar.tsx (3)
apps/roam/src/utils/getLeftSidebarSettings.ts (2)
  • LeftSidebarPersonalSectionConfig (18-25)
  • LeftSidebarGlobalSectionConfig (27-32)
apps/roam/src/utils/discourseConfigRef.ts (2)
  • FormattedConfigTree (20-29)
  • getFormattedConfigTree (31-57)
apps/roam/src/utils/ensureLeftSidebarStructure.ts (1)
  • ensureLeftSidebarReady (72-83)
apps/roam/src/utils/getLeftSidebarSettings.ts (1)
apps/roam/src/utils/getExportSettings.ts (4)
  • IntSetting (13-13)
  • BooleanSetting (12-12)
  • getUidAndBooleanSetting (56-62)
  • getUidAndIntSetting (49-55)
apps/roam/src/components/LeftSidebarView.tsx (2)
apps/roam/src/utils/getLeftSidebarSettings.ts (2)
  • LeftSidebarPersonalSectionConfig (18-25)
  • LeftSidebarConfig (34-40)
apps/roam/src/utils/discourseConfigRef.ts (1)
  • getFormattedConfigTree (31-57)
apps/roam/src/utils/initializeObserversAndListeners.ts (1)
apps/roam/src/components/LeftSidebarView.tsx (1)
  • mountLeftSidebarInto (357-397)
🔇 Additional comments (7)
apps/roam/src/components/settings/Settings.tsx (1)

28-31: Imports look correct and scoped

The Left Sidebar settings components are imported from the right location and named exports match.

apps/roam/src/utils/discourseConfigRef.ts (1)

20-29: Good extension of the formatted config tree

Adding leftSidebar to FormattedConfigTree is a clean extension and keeps existing fields intact.

apps/roam/src/utils/initializeObserversAndListeners.ts (3)

44-45: Import is correct and localized

The new mount function is imported from the right module.


106-117: Immediate mount is a good UX improvement

Proactively mounting if the wrapper already exists avoids waiting for the observer tick.


340-341: Observer correctly added to the list

Ensures teardown via the returned observers array.

apps/roam/src/components/LeftSidebarView.tsx (2)

50-58: Add null check before using page UID

The function gets a page UID but only checks for null on line 51, yet still attempts to use it on line 57 even when it's undefined.

Apply this diff to handle the null case properly:

   const uid = getPageUidByPageTitle(target.title);
   if (!uid) return;
   if (e.shiftKey) {
     await window.roamAlphaAPI.ui.rightSidebar.addWindow({
       window: { type: "outline", "block-uid": uid },
     });
   } else {
-    await window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid } });
+    if (uid) {
+      await window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid } });
+    }
   }

Likely an incorrect or invalid review comment.


176-176: Client-side generateUID is supported in Roam
The call to window.roamAlphaAPI.util.generateUID() is part of Roam’s in-browser API and is widely used by community extensions and roam/js scripts. If this component only runs inside the Roam app (e.g. in a roam/js block or Depot extension), no fallback is necessary.
Optionally, if you ever expect this code to execute outside of the Roam context, you can guard the call with a check for window.roamAlphaAPI?.util?.generateUID and throw or log an error—but there’s no need to supply a temporary UID in normal in-app use.

Comment on lines +390 to +396
setTimeout(() => {
try {
ReactDOM.render(<LeftSidebarView />, root);
} catch (e) {
console.error("[DG][LeftSidebar] render error", e);
}
}, 500);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace fixed timeout with proper initialization check

Using a fixed 500ms timeout is unreliable - it might be too short in some cases or unnecessarily long in others.

Consider using a more robust initialization check:

-  setTimeout(() => {
-    try {
-      ReactDOM.render(<LeftSidebarView />, root);
-    } catch (e) {
-      console.error("[DG][LeftSidebar] render error", e);
-    }
-  }, 500);
+  const waitForConfig = (retries = 10) => {
+    const config = getFormattedConfigTree().leftSidebar;
+    const hasContent = (config.global.children?.length || 0) > 0 || 
+                      (config.personal.sections?.length || 0) > 0;
+    
+    if (hasContent || retries <= 0) {
+      try {
+        ReactDOM.render(<LeftSidebarView />, root);
+      } catch (e) {
+        console.error("[DG][LeftSidebar] render error", e);
+      }
+    } else {
+      setTimeout(() => waitForConfig(retries - 1), 100);
+    }
+  };
+  
+  waitForConfig();
📝 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
setTimeout(() => {
try {
ReactDOM.render(<LeftSidebarView />, root);
} catch (e) {
console.error("[DG][LeftSidebar] render error", e);
}
}, 500);
const waitForConfig = (retries = 10) => {
const config = getFormattedConfigTree().leftSidebar;
const hasContent =
(config.global.children?.length || 0) > 0 ||
(config.personal.sections?.length || 0) > 0;
if (hasContent || retries <= 0) {
try {
ReactDOM.render(<LeftSidebarView />, root);
} catch (e) {
console.error("[DG][LeftSidebar] render error", e);
}
} else {
setTimeout(() => waitForConfig(retries - 1), 100);
}
};
waitForConfig();
🤖 Prompt for AI Agents
In apps/roam/src/components/LeftSidebarView.tsx around lines 390 to 396, replace
the fixed setTimeout render delay with a robust initialization check that
ensures the target root element exists and the app is ready before rendering;
implement either waiting for document.readyState/DOMContentLoaded or use a
short, cancellable poll (or a MutationObserver) to detect the root element, then
call ReactDOM.render (or createRoot) once and only once, and handle errors
within that single render attempt; ensure you clear any observers/timeouts after
success and retain the existing console.error handling for render exceptions.

Comment on lines +175 to +176
!getPageInput() || pages.some((p) => p.text === getPageInput())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Match disabled condition to normalization logic

Use trimmed input and case-insensitive duplicate check to mirror addPage.

-          disabled={
-            !getPageInput() || pages.some((p) => p.text === getPageInput())
-          }
+          disabled={
+            !getPageInput().trim() ||
+            pages.some(
+              (p) => p.text.toLowerCase() === getPageInput().trim().toLowerCase(),
+            )
+          }
📝 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
!getPageInput() || pages.some((p) => p.text === getPageInput())
}
disabled={
- !getPageInput() || pages.some((p) => p.text === getPageInput())
+ !getPageInput().trim() ||
+ pages.some(
+ (p) =>
+ p.text.toLowerCase() === getPageInput().trim().toLowerCase(),
+ )
}
🤖 Prompt for AI Agents
In apps/roam/src/components/settings/LeftSidebar.tsx around lines 175 to 176,
the disabled condition uses the raw input and a case-sensitive check but addPage
normalizes (trims and lowercases) before comparing; change the condition to use
a normalized value (const normalized = getPageInput()?.trim().toLowerCase()) and
disable when normalized is empty or pages.some(p => p.text.trim().toLowerCase()
=== normalized) so the button matches the same trimmed, case-insensitive
duplicate logic as addPage.

Comment on lines +254 to +271
const addSection = async (sectionName: string) => {
if (!sectionName || sections.some((s) => s.text === sectionName)) {
return;
}

try {
await createBlock({
parentUid: personalSection.uid,
order: "last",
node: { text: sectionName },
});
refreshSections();
setNewSectionInput("");
setAutocompleteKey((prev) => prev + 1);
} catch (error) {
console.error("Failed to add section:", error);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden addSection: trim and avoid case-variant duplicates

Avoids accidental duplicates and whitespace-only sections.

-  const addSection = async (sectionName: string) => {
-    if (!sectionName || sections.some((s) => s.text === sectionName)) {
-      return;
-    }
+  const addSection = async (sectionName: string) => {
+    const name = sectionName.trim();
+    if (!name) return;
+    if (sections.some((s) => s.text.toLowerCase() === name.toLowerCase())) {
+      return;
+    }
     try {
       await createBlock({
         parentUid: personalSection.uid,
         order: "last",
-        node: { text: sectionName },
+        node: { text: name },
       });
📝 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
const addSection = async (sectionName: string) => {
if (!sectionName || sections.some((s) => s.text === sectionName)) {
return;
}
try {
await createBlock({
parentUid: personalSection.uid,
order: "last",
node: { text: sectionName },
});
refreshSections();
setNewSectionInput("");
setAutocompleteKey((prev) => prev + 1);
} catch (error) {
console.error("Failed to add section:", error);
}
};
const addSection = async (sectionName: string) => {
const name = sectionName.trim();
if (!name) return;
if (sections.some((s) => s.text.toLowerCase() === name.toLowerCase())) {
return;
}
try {
await createBlock({
parentUid: personalSection.uid,
order: "last",
node: { text: name },
});
refreshSections();
setNewSectionInput("");
setAutocompleteKey((prev) => prev + 1);
} catch (error) {
console.error("Failed to add section:", error);
}
};
🤖 Prompt for AI Agents
In apps/roam/src/components/settings/LeftSidebar.tsx around lines 254 to 271,
the addSection handler should reject whitespace-only input and avoid duplicates
that differ only by leading/trailing spaces or case; fix by trimming the
incoming sectionName into a const (e.g., const name = sectionName.trim()),
return early if name === "" or if sections.some(s => s.text.trim().toLowerCase()
=== name.toLowerCase()), use the trimmed name when creating the block node ({
text: name }), and keep the existing refreshSections(), setNewSectionInput(""),
and autocomplete key update inside the try/catch.

Comment on lines +39 to +66
} else {
const hasGlobalSection = sidebarNode.children?.some(
(n) => n.text === "Global Section",
);
const hasPersonalSection = sidebarNode.children?.some(
(n) => n.text === userName + "/Personal Section",
);

if (!hasGlobalSection) {
await createBlock({
parentUid: sidebarNode.uid,
order: 0,
node: {
text: "Global Section",
children: [
{ text: "Open", children: [{ text: "false" }] },
{ text: "Children" },
],
},
});
}

if (!hasPersonalSection) {
await createBlock({
parentUid: sidebarNode.uid,
node: { text: userName + "/Personal Section" },
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure Global Section’s Open/Children even when Global exists

Currently, if “Global Section” exists but is missing “Open” and/or “Children,” they remain absent. Make the ensure step idempotent and complete.

   } else {
-    const hasGlobalSection = sidebarNode.children?.some(
-      (n) => n.text === "Global Section",
-    );
+    const hasGlobalSection = sidebarNode.children?.some(
+      (n) => n.text === "Global Section",
+    );
     const hasPersonalSection = sidebarNode.children?.some(
       (n) => n.text === userName + "/Personal Section",
     );
 
     if (!hasGlobalSection) {
       await createBlock({
         parentUid: sidebarNode.uid,
         order: 0,
         node: {
           text: "Global Section",
           children: [
             { text: "Open", children: [{ text: "false" }] },
             { text: "Children" },
           ],
         },
       });
     }
 
+    // Re-read to get the latest children and ensure nested nodes exist
+    const refreshed = getBasicTreeByParentUid(configPageUid).find(
+      (n) => n.text === "Left Sidebar",
+    );
+    const globalNode = refreshed?.children?.find((n) => n.text === "Global Section");
+    if (globalNode) {
+      const hasOpen = globalNode.children?.some((c) => c.text === "Open");
+      const hasChildren = globalNode.children?.some((c) => c.text === "Children");
+      if (!hasOpen) {
+        await createBlock({
+          parentUid: globalNode.uid,
+          order: 0,
+          node: { text: "Open", children: [{ text: "false" }] },
+        });
+      }
+      if (!hasChildren) {
+        await createBlock({
+          parentUid: globalNode.uid,
+          order: 1,
+          node: { text: "Children" },
+        });
+      }
+    }
+
     if (!hasPersonalSection) {
       await createBlock({
         parentUid: sidebarNode.uid,
         node: { text: userName + "/Personal Section" },
       });
     }
   }
📝 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
} else {
const hasGlobalSection = sidebarNode.children?.some(
(n) => n.text === "Global Section",
);
const hasPersonalSection = sidebarNode.children?.some(
(n) => n.text === userName + "/Personal Section",
);
if (!hasGlobalSection) {
await createBlock({
parentUid: sidebarNode.uid,
order: 0,
node: {
text: "Global Section",
children: [
{ text: "Open", children: [{ text: "false" }] },
{ text: "Children" },
],
},
});
}
if (!hasPersonalSection) {
await createBlock({
parentUid: sidebarNode.uid,
node: { text: userName + "/Personal Section" },
});
}
} else {
const hasGlobalSection = sidebarNode.children?.some(
(n) => n.text === "Global Section",
);
const hasPersonalSection = sidebarNode.children?.some(
(n) => n.text === userName + "/Personal Section",
);
if (!hasGlobalSection) {
await createBlock({
parentUid: sidebarNode.uid,
order: 0,
node: {
text: "Global Section",
children: [
{ text: "Open", children: [{ text: "false" }] },
{ text: "Children" },
],
},
});
}
// Re-read to get the latest children and ensure nested nodes exist
const refreshed = getBasicTreeByParentUid(configPageUid).find(
(n) => n.text === "Left Sidebar",
);
const globalNode = refreshed?.children?.find(
(n) => n.text === "Global Section"
);
if (globalNode) {
const hasOpen = globalNode.children?.some((c) => c.text === "Open");
const hasChildren = globalNode.children?.some((c) => c.text === "Children");
if (!hasOpen) {
await createBlock({
parentUid: globalNode.uid,
order: 0,
node: { text: "Open", children: [{ text: "false" }] },
});
}
if (!hasChildren) {
await createBlock({
parentUid: globalNode.uid,
order: 1,
node: { text: "Children" },
});
}
}
if (!hasPersonalSection) {
await createBlock({
parentUid: sidebarNode.uid,
node: { text: userName + "/Personal Section" },
});
}
}
🤖 Prompt for AI Agents
In apps/roam/src/utils/ensureLeftSidebarStructure.ts around lines 39 to 66, the
code only creates a new "Global Section" when missing but does not ensure that
an existing "Global Section" has the required "Open" (with a child "false") and
"Children" nodes; make the operation idempotent by locating the existing "Global
Section" node when present, examine its children for an "Open" child (and that
Open child has a child "false") and a "Children" child, and call createBlock to
add any missing child blocks with the correct parentUid set to the Global
Section node's uid and appropriate order/structure so that running the function
multiple times leaves no gaps.

Comment on lines +106 to +113
const personal = getLeftSidebarPersonalSectionConfig(
personalLeftSidebarNode,
) || {
uid: "",
text: "",
isSimple: true,
sections: [],
};
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

Fix type of personal default: remove extraneous fields

The fallback object includes properties not in the expected type ({ uid; sections }). This will fail excess property checks.

-  const personal = getLeftSidebarPersonalSectionConfig(
-    personalLeftSidebarNode,
-  ) || {
-    uid: "",
-    text: "",
-    isSimple: true,
-    sections: [],
-  };
+  const personal =
+    getLeftSidebarPersonalSectionConfig(personalLeftSidebarNode) || {
+      uid: "",
+      sections: [],
+    };
📝 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
const personal = getLeftSidebarPersonalSectionConfig(
personalLeftSidebarNode,
) || {
uid: "",
text: "",
isSimple: true,
sections: [],
};
const personal =
getLeftSidebarPersonalSectionConfig(personalLeftSidebarNode) || {
uid: "",
sections: [],
};
🤖 Prompt for AI Agents
In apps/roam/src/utils/ensureLeftSidebarStructure.ts around lines 106 to 113,
the fallback object for personal includes extraneous fields (text, isSimple) not
present on the expected type ({ uid; sections }), causing excess property check
errors; replace the fallback with an object matching the type (e.g., { uid: "",
sections: [] }) or adjust the function call to allow a broader type (or use a
safe type assertion) so the default value exactly matches the expected shape.

@sid597 sid597 closed this Sep 29, 2025
@github-project-automation github-project-automation bot moved this to Done in General Sep 29, 2025
@sid597
Copy link
Collaborator Author

sid597 commented Sep 29, 2025

ngmi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants