Skip to content

ENG-1454: Base getter() task#828

Open
sid597 wants to merge 63 commits intomainfrom
migration-block-init-staging-branch
Open

ENG-1454: Base getter() task#828
sid597 wants to merge 63 commits intomainfrom
migration-block-init-staging-branch

Conversation

@sid597
Copy link
Copy Markdown
Collaborator

@sid597 sid597 commented Feb 25, 2026

#DO NOT MERGE - until all the other prs are


Open with Devin

@linear
Copy link
Copy Markdown

linear Bot commented Feb 25, 2026

@supabase
Copy link
Copy Markdown

supabase Bot commented Feb 25, 2026

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 ↗︎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

sid597 added 11 commits March 3, 2026 18:26
* ENG-1454: Enable dual read feature flag

* add caller

* resolve merge conflicts

* review

* example fix

* review
* ENG-1455: Dual-read from old-system settings and from blockprops

* remove console.logs

* address review

* prettier

* add comment describing why we do it like this

* reviewed

* rebase, review

* address review

* explicit throw, now zodschema default reads

* replace throw with warn, throw was too strong, with warn we test if legacy and block props are same
…ult reads (#813)

* ENG-1472: Refactor BlockPropSettingPanels to add accessor-backed default reads (with initialValue fallback)

* review, fix
* ENG-1473: Rebase onto updated eng-1472, resolve conflicts and fix missing import

* ENG-1473: Review fixes — remove dead extensionAPI params, fix type casts

* ENG-1473: Fix restrict-template-expressions warnings in query utils

* rename persistQueryPages → setQueryPages, setQueryPages → setInitialQueryPages

* restore legacy type coercion for query-pages
* ENG-1467: Port global setting consumer reads (→ getGlobalSetting)

* prettier

* rebase onto updated eng-1472, resolve conflicts and fix tsc
* ENG-1479: Port suggestive mode settings to dual-read

Add dual-read routing to getFeatureFlag for flags with legacy counterparts.
FEATURE_FLAG_LEGACY_MAP maps "Suggestive mode enabled" and "Enable left
sidebar" to their config tree reads, gated by isNewSettingsStoreEnabled().
Flags without legacy entries go straight to block props (no behavior change).

Migrate value reads from getFormattedConfigTree() / extensionAPI.settings
to accessors:

- index.ts: getUidAndBooleanSetting → getFeatureFlag (accessor handles
  legacy fallback now)
- AdminPanel: suggestiveModeEnabled.value → getFeatureFlag in both
  FeatureFlagsTab and main component. Removed unused useMemo.
- SuggestiveModeSettings: includePageRelations state init →
  getGlobalSetting. Dropped initialValue from both GlobalFlagPanels.
- hyde.ts: orphan extensionAPI.settings.get() reads →
  getGlobalSetting. Fixes bug where sync config toggles had no
  runtime effect (keys were never written by any code).

Structural UIDs remain with getFormattedConfigTree().
pageGroups.groups NOT migrated — type mismatch (legacy PageGroup has
UIDs, Zod PageGroup does not). Deferred to ENG-1470.

* Align hyde.ts fallback defaults with Zod schema

* Fix eslint naming-convention warnings in FEATURE_FLAG_LEGACY_MAP

* Add TODO(ENG-1484) for suggestive mode reactivity
* Rebase ENG-1468 onto eng-1472 (fresh redo)

* Remove unnecessary lazy initializer from complement useState

* Break circular dep: inline DISCOURSE_CONFIG_PAGE_TITLE

* Address review: case-insensitive attribute lookup, empty-string fallback, move DISCOURSE_CONFIG_PAGE_TITLE to data/constants
…er-based reactivity

- Extract mergeGlobalSectionWithAccessor/mergePersonalSectionsWithAccessor to
  getLeftSidebarSettings.ts (used by LeftSidebarView, GlobalSettings, PersonalSettings)
- Settings panels now actually use accessor values (not fire-and-forget)
- Replace setLeftSidebarFlagHandler with settingsEmitter pub/sub
- useConfig subscribes to emitter instead of old subscribe()
- Wire up global/personal/flag handlers in pullWatchers to emit
- Remove dead newValue===oldValue guard (hasPropChanged already checks)
- Remove dead ?./?? in personal merge (Zod defaults guarantee values)
toggleFoldedState mutates folded.uid in-place then dual-writes to
block props, triggering pull watch → emitter → buildConfig(). Without
refreshConfigTree(), buildConfig() reads stale UIDs from the cached
tree, overwriting the in-place mutation and orphaning Roam blocks.
@sid597 sid597 force-pushed the migration-block-init-staging-branch branch from 21b6000 to 3b4c80a Compare March 3, 2026 13:03
devin-ai-integration[bot]

This comment was marked as resolved.

sid597 added 2 commits March 13, 2026 11:07
…nch' into eng-1484-reactive-settings-some-settings-require-reloading-the-graph

# Conflicts:
#	apps/roam/src/index.ts
#	apps/roam/src/utils/initializeObserversAndListeners.ts
…gs-some-settings-require-reloading-the-graph

ENG-1484: reactive settings for triggers and suggestive mode overlay
devin-ai-integration[bot]

This comment was marked as resolved.

sid597 added 3 commits March 16, 2026 10:43
Resolved 11 conflicts:
- Replaced getFeatureFlag("Reified relation triples") with getStoredRelationsEnabled()
  (main moved reified relations from FeatureFlags to PersonalSettings)
- Removed "Reified relation triples" from FeatureFlagsSchema, kept "Use new settings store"
- Kept toDiscourseNode + added migrateNodeBlockProps in accessors.ts (both needed)
- Dropped MigrationTab from AdminPanel (moved to HomePersonalSettings on main)
- Migrated DiscourseContextOverlay score validation to use accessors instead of
  legacy tree reads (getSubTree/getSettingValueFromTree/getBasicTreeByParentUid)
- Removed dead legacy imports (getSetting, AUTO_CANVAS_RELATIONS_KEY from Tldraw.tsx;
  USE_REIFIED_RELATIONS from accessors.ts)
- Zero net increase in legacy reader call sites
…ing-branch

Resolved 2 import conflicts in canvas files — keep both sides (accessor
imports + new canvas sync/posthog imports).
* ENG-1470: Fix dual-read gaps found during flag-ON validation

Bootstrap legacy config blocks in initSchema so the plugin works on
fresh graphs without createConfigObserver/configPageTabs:
- trigger, grammar/relations, export, Suggestive Mode, Left Sidebar
- Reuses existing ensureBlocksExist/buildBlockMap helpers + DEFAULT_RELATION_VALUES

Fix duplicate block accumulation bugs:
- Page Groups: getSubTree auto-create race (ensureLegacyConfigBlocks pre-creates)
- Folded: lookup-based delete via getBasicTreeByParentUid instead of stale uid
- scratch/enabled: switched getSubTree({ parentUid }) to tree-based reads
- Folded in convertToComplexSection: removed erroneous block creation

Fix dual-read comparison:
- Replace JSON.stringify with deepEqual (handles key order, undefined/empty/false)
- Deterministic async ordering: await legacy write → refreshConfigTree → blockProp write
  (BlockPropSettingPanels, LeftSidebarView toggleFoldedState, AdminPanel suggestive mode)
- Use getPersonalSettings() (raw read) in toggleFoldedState to avoid mid-write comparison

Fix storedRelations import path (renderNodeConfigPage → data/constants)

* Fix dual-read mismatches and ZodError on discourse node parse

* Fix dual-read mismatches: alias timing, key-image re-render, deepEqual null

* Fix prettier formatting

* ENG-1574: Add dual-read console logs to setting setters (#914)

* ENG-1574: Add dual-read console logs to setting setters

Log legacy and block prop values with match/mismatch status
when a setting is changed. Fix broken import in storedRelations.

* ENG-1574: Add init-time dual-read log and window.dgDualReadLog()

Log all legacy vs block prop settings on init. Remove setter
logging. Expose dgDualReadLog() on window for on-demand use.

* ENG-1574: Fix eslint naming-convention warnings in init.ts

* ENG-1574: Use deepEqual instead of JSON.stringify for comparison

JSON.stringify is key-order dependent, causing false mismatches
when legacy and block props return keys in different order.

* ENG-1574: Remove dead code, use deepEqual for comparison

* ENG-1574: Fix review feedback — try-catch, flag exclusion, type guard

* Eng 1616 add getconfigtree equivalent for block pros on init (#944)

* ENG-1616: Bulk-read settings + thread snapshot (with timing logs)

Cut plugin load from ~20925ms to ~1327ms (94%) on a real graph by collapsing
per-call settings accessors into a single bulk read at init and threading
that snapshot through the init chain + observer callbacks.

Key changes:
- accessors.ts: bulkReadSettings() runs ONE pull query against the settings
  page's direct children and returns { featureFlags, globalSettings,
  personalSettings } parsed via Zod. readPathValue exported.
- getDiscourseNodes / getDiscourseRelations / getAllRelations: optional
  snapshot param threaded through, no breaking changes to existing callers.
- initializeDiscourseNodes + refreshConfigTree (+ registerDiscourseDatalog-
  Translators, getDiscourseRelationLabels): accept and forward snapshot.
- index.ts: bulkReadSettings() at the top of init; snapshot threaded into
  initializeDiscourseNodes, refreshConfigTree, initObservers,
  installDiscourseFloatingMenu, setInitialQueryPages, and the 3 sync sites
  inside index.ts itself.
- initializeObserversAndListeners.ts: snapshot threaded into the sync-init
  body; pageTitleObserver + leftSidebarObserver callbacks call
  bulkReadSettings() per fire (fresh, not stale); nodeTagPopupButtonObserver
  uses per-sync-batch memoization via queueMicrotask; hashChangeListener
  and nodeCreationPopoverListener use bulkReadSettings() per fire.
- findDiscourseNode: snapshot param added; getDiscourseNodes() default-arg
  moved inside the cache-miss branch so cache hits don't waste the call.
- isQueryPage / isCanvasPage / QueryPagesPanel.getQueryPages: optional
  snapshot param.
- LeftSidebarView.buildConfig / useConfig / mountLeftSidebar: optional
  initialSnapshot threaded for the first render; emitter-driven updates
  keep using live reads for post-mount reactivity.
- DiscourseFloatingMenu.installDiscourseFloatingMenu: optional snapshot.
- posthog.initPostHog: removed redundant internal getPersonalSetting check
  (caller already guards from the snapshot).
- migrateLegacyToBlockProps.hasGraphMigrationMarker: accepts the existing
  blockMap and does an O(1) lookup instead of a getBlockUidByTextOnPage scan.

Includes per-phase timing console.logs across index.ts, refreshConfigTree,
init.ts, initSettingsPageBlocks, and initObservers. Committed as a
checkpoint so we can reference measurements later; will be removed in the
next commit.

* ENG-1616: Remove plugin-load timing logs

Removes the per-phase console.log instrumentation added in the previous
commit. All the [DG Plugin] / [DG Nav] logs and their `mark()` / `markPhase()`
helpers are gone. Code behavior unchanged.

Dropped in this commit:
- index.ts: mark() closure, load start/done logs, and all phase marks.
- initializeObserversAndListeners.ts: markPhase() closure, per-observer marks,
  pageTitleObserver fire log, hashChangeListener [DG Nav] logs.
- LeftSidebarView.tsx: openTarget [DG Nav] click/resolve logs.
- refreshConfigTree.ts: mark() closure and all phase marks.
- init.ts: mark() closures in initSchema and initSettingsPageBlocks.
- accessors.ts: bulkReadSettings internal timing log.
- index.ts: unused getPluginElapsedTime import.

Previous commit (343dc11) kept as a checkpoint for future drill-downs.

* ENG-1616: Address review — typed indexing, restore dgDualReadLog, optional snapshot

- index.ts: move initPluginTimer() back to its original position (after
  early-return checks) so timing isn't started for graphs that bail out.
- Replace readPathValue + `as T | undefined` casts with direct typed
  indexing on the Zod-derived snapshot types across:
    - index.ts (disallowDiagnostics, isStreamlineStylingEnabled)
    - initializeObserversAndListeners.ts (suggestiveModeOverlay,
      pagePreview, discourseContextOverlay, globalTrigger,
      personalTriggerCombo, customTrigger) — also drops dead `?? "\\"`
      and `?? "@"` fallbacks since Zod defaults already populate them.
    - isCanvasPage.ts (canvasPageFormat)
    - setQueryPages.ts + QueryPagesPanel.tsx (nested [Query][Query pages])
- setQueryPages.setInitialQueryPages: snapshot is now optional with a
  getPersonalSetting fallback, matching the pattern used elsewhere
  (getQueryPages, isCanvasPage, etc.).
- init.ts: restore logDualReadComparison + window.dgDualReadLog so the
  on-demand console helper is available again. NOT auto-called on init —
  invoke window.dgDualReadLog() manually to dump the comparison.

* ENG-1616: Log total plugin load time

Capture performance.now() at the top of runExtension and log the
elapsed milliseconds just before the unload handler is wired, so we
have a single broad measurement of plugin init cost on each load.

* ENG-1616: Tighten init-only leaves to required snapshot, AGENTS.md compliance

Make snapshot required at six init-only leaves where caller audit
showed every site already passed one: installDiscourseFloatingMenu,
initializeDiscourseNodes, setInitialQueryPages, isQueryPage,
isCurrentPageCanvas, isSidebarCanvas. No cascade — only at the leaves.

Drop dead fallback code that was reachable only via the optional path:
- setQueryPages: legacy string|Record coercion ladder (snapshot is Zod-typed string[])
- DiscourseFloatingMenu: getPersonalSetting<boolean> cast site
- DiscourseFloatingMenu: unused props parameter (no caller ever overrode default)
- initializeObserversAndListeners: !== false dead pattern (Zod boolean default)
- initializeObserversAndListeners: as IKeyCombo cast (schema is structurally compatible)

AGENTS.md compliance for >2-arg functions:
- mountLeftSidebar: object-destructured params, both call sites updated
- installDiscourseFloatingMenu: kept at 2 positional via dead-props removal

posthog: collapse doInitPostHog wrapper into initPostHog (caller-side gating).
accessors: revert speculative readPathValue export (no consumer).
LeftSidebarView/DiscourseFloatingMenu: eslint-disable react/no-deprecated on
ReactDOM.render rewritten lines, matching existing codebase convention.

* ENG-1616: Address review — rename snapshot vars, flag-gate bulkRead, move PostHog guards

- Rename settingsSnapshot/callbackSnapshot/snap/navSnapshot → settings
- bulkReadSettings now checks "Use new settings store" flag and falls
  back to legacy reads when off, matching individual getter behavior
- Move encryption/offline guards into initPostHog (diagnostics check
  stays at call site to avoid race with async setSetting in enablePostHog)

* Fix legacy bulk settings fallback

* ENG-1617: se existing 'getConfigTree equivalent functions' for specific setting groups (#946)

* ENG-1616: Bulk-read settings + thread snapshot (with timing logs)

Cut plugin load from ~20925ms to ~1327ms (94%) on a real graph by collapsing
per-call settings accessors into a single bulk read at init and threading
that snapshot through the init chain + observer callbacks.

Key changes:
- accessors.ts: bulkReadSettings() runs ONE pull query against the settings
  page's direct children and returns { featureFlags, globalSettings,
  personalSettings } parsed via Zod. readPathValue exported.
- getDiscourseNodes / getDiscourseRelations / getAllRelations: optional
  snapshot param threaded through, no breaking changes to existing callers.
- initializeDiscourseNodes + refreshConfigTree (+ registerDiscourseDatalog-
  Translators, getDiscourseRelationLabels): accept and forward snapshot.
- index.ts: bulkReadSettings() at the top of init; snapshot threaded into
  initializeDiscourseNodes, refreshConfigTree, initObservers,
  installDiscourseFloatingMenu, setInitialQueryPages, and the 3 sync sites
  inside index.ts itself.
- initializeObserversAndListeners.ts: snapshot threaded into the sync-init
  body; pageTitleObserver + leftSidebarObserver callbacks call
  bulkReadSettings() per fire (fresh, not stale); nodeTagPopupButtonObserver
  uses per-sync-batch memoization via queueMicrotask; hashChangeListener
  and nodeCreationPopoverListener use bulkReadSettings() per fire.
- findDiscourseNode: snapshot param added; getDiscourseNodes() default-arg
  moved inside the cache-miss branch so cache hits don't waste the call.
- isQueryPage / isCanvasPage / QueryPagesPanel.getQueryPages: optional
  snapshot param.
- LeftSidebarView.buildConfig / useConfig / mountLeftSidebar: optional
  initialSnapshot threaded for the first render; emitter-driven updates
  keep using live reads for post-mount reactivity.
- DiscourseFloatingMenu.installDiscourseFloatingMenu: optional snapshot.
- posthog.initPostHog: removed redundant internal getPersonalSetting check
  (caller already guards from the snapshot).
- migrateLegacyToBlockProps.hasGraphMigrationMarker: accepts the existing
  blockMap and does an O(1) lookup instead of a getBlockUidByTextOnPage scan.

Includes per-phase timing console.logs across index.ts, refreshConfigTree,
init.ts, initSettingsPageBlocks, and initObservers. Committed as a
checkpoint so we can reference measurements later; will be removed in the
next commit.

* ENG-1616: Remove plugin-load timing logs

Removes the per-phase console.log instrumentation added in the previous
commit. All the [DG Plugin] / [DG Nav] logs and their `mark()` / `markPhase()`
helpers are gone. Code behavior unchanged.

Dropped in this commit:
- index.ts: mark() closure, load start/done logs, and all phase marks.
- initializeObserversAndListeners.ts: markPhase() closure, per-observer marks,
  pageTitleObserver fire log, hashChangeListener [DG Nav] logs.
- LeftSidebarView.tsx: openTarget [DG Nav] click/resolve logs.
- refreshConfigTree.ts: mark() closure and all phase marks.
- init.ts: mark() closures in initSchema and initSettingsPageBlocks.
- accessors.ts: bulkReadSettings internal timing log.
- index.ts: unused getPluginElapsedTime import.

Previous commit (343dc11) kept as a checkpoint for future drill-downs.

* ENG-1616: Address review — typed indexing, restore dgDualReadLog, optional snapshot

- index.ts: move initPluginTimer() back to its original position (after
  early-return checks) so timing isn't started for graphs that bail out.
- Replace readPathValue + `as T | undefined` casts with direct typed
  indexing on the Zod-derived snapshot types across:
    - index.ts (disallowDiagnostics, isStreamlineStylingEnabled)
    - initializeObserversAndListeners.ts (suggestiveModeOverlay,
      pagePreview, discourseContextOverlay, globalTrigger,
      personalTriggerCombo, customTrigger) — also drops dead `?? "\\"`
      and `?? "@"` fallbacks since Zod defaults already populate them.
    - isCanvasPage.ts (canvasPageFormat)
    - setQueryPages.ts + QueryPagesPanel.tsx (nested [Query][Query pages])
- setQueryPages.setInitialQueryPages: snapshot is now optional with a
  getPersonalSetting fallback, matching the pattern used elsewhere
  (getQueryPages, isCanvasPage, etc.).
- init.ts: restore logDualReadComparison + window.dgDualReadLog so the
  on-demand console helper is available again. NOT auto-called on init —
  invoke window.dgDualReadLog() manually to dump the comparison.

* ENG-1616: Log total plugin load time

Capture performance.now() at the top of runExtension and log the
elapsed milliseconds just before the unload handler is wired, so we
have a single broad measurement of plugin init cost on each load.

* ENG-1616: Tighten init-only leaves to required snapshot, AGENTS.md compliance

Make snapshot required at six init-only leaves where caller audit
showed every site already passed one: installDiscourseFloatingMenu,
initializeDiscourseNodes, setInitialQueryPages, isQueryPage,
isCurrentPageCanvas, isSidebarCanvas. No cascade — only at the leaves.

Drop dead fallback code that was reachable only via the optional path:
- setQueryPages: legacy string|Record coercion ladder (snapshot is Zod-typed string[])
- DiscourseFloatingMenu: getPersonalSetting<boolean> cast site
- DiscourseFloatingMenu: unused props parameter (no caller ever overrode default)
- initializeObserversAndListeners: !== false dead pattern (Zod boolean default)
- initializeObserversAndListeners: as IKeyCombo cast (schema is structurally compatible)

AGENTS.md compliance for >2-arg functions:
- mountLeftSidebar: object-destructured params, both call sites updated
- installDiscourseFloatingMenu: kept at 2 positional via dead-props removal

posthog: collapse doInitPostHog wrapper into initPostHog (caller-side gating).
accessors: revert speculative readPathValue export (no consumer).
LeftSidebarView/DiscourseFloatingMenu: eslint-disable react/no-deprecated on
ReactDOM.render rewritten lines, matching existing codebase convention.

* ENG-1617: Single-pull settings reads + dialog snapshot threading

`getBlockPropBasedSettings` now does one Roam `pull` that returns the
settings page's children with their string + uid + props in one shot,
replacing the `q`-based `getBlockUidByTextOnPage` (~290ms per call) plus
a second `pull` for props. `setBlockPropBasedSettings` reuses the same
helper for the uid lookup so we have a single pull-and-walk pattern.

`SettingsDialog` captures a full settings snapshot once at mount via
`useState(() => bulkReadSettings())` and threads `featureFlags` and
`personalSettings` down to `HomePersonalSettings`. Each child component
(`PersonalFlagPanel`, `NodeMenuTriggerComponent`,
`NodeSearchMenuTriggerSetting`, `KeyboardShortcutInput`) accepts an
`initialValue` prop and seeds its local state from the snapshot instead
of calling `getPersonalSetting` on mount. `PersonalFlagPanel`'s
`initialValue` precedence flips so the prop wins when provided;
`QuerySettings` callers without a prop still hit the existing fallback.

`getDiscourseNodes`, `getDiscourseRelations`, and `getAllRelations`
narrow their snapshot parameter to `Pick<SettingsSnapshot, ...>` to
declare which fields each function actually reads.

Adds a one-line `console.log` in `SettingsDialog` reporting the dialog
open time, kept as an ongoing perf monitor.

* ENG-1617: Refresh snapshot on Home tab nav + reuse readPathValue

CodeRabbit catch: with `renderActiveTabPanelOnly={true}`, the Home tab's
panel unmounts/remounts when the user navigates away and back. Each
re-mount re-runs `useState(() => initialValue ?? false)` in
`BaseFlagPanel`, re-seeding from whatever `initialValue` is currently
passed. Because the dialog held the snapshot in a non-updating
`useState`, that path served stale values, so toggles made earlier in
the same dialog session would visually revert after a tab round-trip.

Fix: hold the snapshot in a stateful slot and refresh it via
`bulkReadSettings()` from the Tabs `onChange` handler when the user
navigates back to Home. The setState batches with `setActiveTabId`, so
the new mount sees the fresh snapshot in the same render.

Also replace the inline reducer in `getBlockPropBasedSettings` with the
existing `readPathValue` util — same traversal but consistent with the
rest of the file and adds array-index handling for free.

* ENG-1617: Per-tab snapshot threading via bulkReadSettings

Replaces the dialog-level snapshot from earlier commits with a per-tab
snapshot model that scales to every tab without per-tab plumbing in the
dialog itself.

In accessors.ts, the three plural getters (getFeatureFlags,
getGlobalSettings, getPersonalSettings) now delegate to the existing
bulkReadSettings, which does one Roam pull on the settings page and
parses all three schemas in a single pass. The slow q-based
getBlockPropBasedSettings is deleted (it was only used by the plural
getters and the set path); setBlockPropBasedSettings goes back to
calling getBlockUidByTextOnPage directly. Writes are infrequent enough
that the q cost is acceptable on the set path.

Each tab container that renders panels at mount captures one snapshot
via useState(() => bulkReadSettings()) and threads the relevant slice as
initialValue down to its panels: HomePersonalSettings, QuerySettings,
GeneralSettings, ExportSettings. The Personal and Global panels in
BlockPropSettingPanels.tsx flip their initialValue precedence to prefer
the prop and fall back to the live read only when the prop is missing,
so callers that don't pass initialValue (e.g. LeftSidebarGlobalSettings,
which already passes its own value) continue to behave the same way.

NodeMenuTriggerComponent, NodeSearchMenuTriggerSetting, and
KeyboardShortcutInput accept an initialValue prop and seed local state
from it instead of calling getPersonalSetting in their useState
initializer.

Settings.tsx wraps getDiscourseNodes() in useState so it doesn't re-run
on every dialog re-render.

The dialog-level snapshot, the snapshot-refresh-on-Home-tab-nav
workaround, and the Pick<SettingsSnapshot, ...> type narrowings are all
gone.

* ENG-1617: Lift settings snapshot to SettingsDialog, thread to all tabs

Move bulkReadSettings() from per-tab useState into a single call at
SettingsDialog mount. Each tab receives its snapshot slice (globalSettings,
personalSettings, featureFlags) as props. Remove dual-read mismatch
console.warn logic from accessors. Make initialValue caller-provided in
BlockPropSettingPanel wrappers. Add TabTiming wrapper for per-tab
commit/paint perf measurement.

* ENG-1617: Remove timing instrumentation, per-call dual-read, flag-aware bulkReadSettings

- Remove TabTiming component and all console.log timing from Settings dialog
- Remove per-call dual-read comparison from getGlobalSetting, getPersonalSetting,
  getDiscourseNodeSetting (keep logDualReadComparison for manual use)
- Make bulkReadSettings flag-aware: reads from legacy when flag is OFF,
  block props when ON
- Remove accessor fallbacks from Global/Personal wrapper panels (initialValue
  now always passed from snapshot)
- Remove getGlobalSetting/getPersonalSetting imports from BlockPropSettingPanels

* ENG-1617: Eliminate double bulkReadSettings calls in accessor functions

getGlobalSetting, getPersonalSetting, getFeatureFlag, getDiscourseNodeSetting
now each do a single bulkReadSettings() call instead of calling
isNewSettingsStoreEnabled() (which triggered a separate bulkReadSettings)
followed by another bulkReadSettings via the getter. bulkReadSettings already
handles the flag check and legacy/block-props routing internally.

* ENG-1617: Re-read snapshot on tab change to prevent stale initialValues

Replace useState with useMemo keyed on activeTabId so bulkReadSettings()
runs fresh each time the user switches tabs. Fixes stale snapshot when
renderActiveTabPanelOnly unmounts/remounts panels.

* ENG-1616: Address review — rename snapshot vars, flag-gate bulkRead, move PostHog guards

- Rename settingsSnapshot/callbackSnapshot/snap/navSnapshot → settings
- bulkReadSettings now checks "Use new settings store" flag and falls
  back to legacy reads when off, matching individual getter behavior
- Move encryption/offline guards into initPostHog (diagnostics check
  stays at call site to avoid race with async setSetting in enablePostHog)

* ENG-1617: Fix DiscourseNodeSelectPanel fallback to use first option instead of empty string

* ENG-1617: Rename snapshot variables to settings for clarity

* Fix legacy bulk settings fallback

* fix bug similar code
devin-ai-integration[bot]

This comment was marked as resolved.

sid597 added 6 commits April 17, 2026 13:39
Main landed ENG-1573 (split sync/suggestive), ENG-1267 (duplicate-alert flag),
ENG-1421 (STATIC_TOP_LEVEL_ENTRIES refactor), ENG-948 (left-sidebar context menu),
plus ~46 others. 12 files had conflicts.

Key resolutions: dropped "Suggestive mode enabled" flag and "Suggestive mode overlay"
personal setting per ENG-1573; absorbed main's schema-key consolidation; kept our
bulkReadSettings path in accessors; dropped ENG-1484 reactive subscribers for the
deleted flags (user reloads per main's UX — followup ticket to restore against
new flag names).

Net ESLint: 14 pre-existing warnings → 9 (zero introduced).
Without a prior refreshConfigTree() call, bulkReadSettings() reads an empty
discourseConfigRef.tree when the flag is OFF, returning legacy defaults
instead of user data. Populate the tree first so the initial snapshot
reflects actual settings.
Move setIsOpen to the top of the function so the optimistic UI update
lands synchronously. Add an isTogglingRef guard in PersonalSectionItem
and GlobalSection so concurrent clicks while Roam writes are in flight
are dropped instead of firing duplicate createBlock/deleteBlock on the
same Folded node.
Previous commit accidentally included an older change that set
"Query pages" default to []. Pre-migration, setQueryPages auto-seeds
this path on every plugin load, so the effective user-facing default
has always been ["discourse-graph/queries/*"]. Restore that value.
Add reifiedRelationTriples to PERSONAL_KEYS and map
"Reified relation triples" -> "use-reified-relations" in
PERSONAL_SCHEMA_PATH_TO_LEGACY_KEY. Without these entries,
readAllLegacyPersonalSettings iterates over PERSONAL_KEYS values
and omits this field, so the migration to block props fills in the
Zod default (false) instead of the user's stored preference.
BaseTextPanel: track the inner setTimeout via debounceRef so unmount
cleanup cancels it instead of letting it fire on an unmounted component.

BaseNumberPanel and BaseSelectPanel: add the same 100ms ordering gap
between the non-awaited syncToBlock write and refreshConfigTree that
BaseFlagPanel and BaseTextPanel already have, tracked via
refreshTimeoutRef for cleanup. Without the gap, refreshConfigTree can
read the tree before the async Roam write settles.
devin-ai-integration[bot]

This comment was marked as resolved.

sid597 added 5 commits April 21, 2026 01:53
- Iterate top-level keys per group (Personal, Global, Feature Flags, each
  node) and log which specific keys mismatch, with legacy vs block values
  side-by-side. Previously only group-level match/mismatch was reported.
- Skip template, specification, and index in node comparisons — their
  legacy and block-prop shapes diverge structurally (uid + empty children
  on templates; condition uids stripped on save) not semantically. Inline
  comment cites the responsible readers/writers.
- Fix getLegacyQuerySettingByParentUid returning a hardcoded "node" for
  returnNode instead of reading scratch > return > [value] from the tree.
…writer (#975)

The Datalog .q scan that resolved the top-level settings block on every
write took ~318 ms per call on real graphs. Replaced with a hash-indexed
roamAlphaAPI.pull by page title that walks :block/children — same pattern
bulkReadSettings already uses.

Behavior equivalence: the old query matched any descendant via :block/page;
the new pull traverses direct :block/children only. Safe here because
setBlockPropBasedSettings is only reached with keys[0] set to one of the
three top-level settings keys (feature flags, global, personal user-id),
all guaranteed to be direct children of the settings page.

Measured on a real graph: BaseFlagPanel click handler 420 ms → 97 ms
(−77%). The remaining 95 ms is refreshConfigTree > register translators,
the inherent dual-read cost ENG-1616 flagged for post-ENG-1470 cleanup.
…dren (#982)

Legacy tree init was calling getSubTree with parentUid, which fires
createBlock without awaiting. Subsequent createBlock calls under those
uids raced Roam's async write and failed with 'Parent entity doesn't
exist'. Replace the three fire-and-forget lookups with tree-mode
getSubTree + awaited createBlock fallback.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 53 additional findings in Devin Review.

Open in Devin Review

Comment on lines +882 to +884
globalSettings: readAllLegacyGlobalSettings() as GlobalSettings,
personalSettings: readAllLegacyPersonalSettings() as PersonalSettings,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Legacy settings path bypasses Zod validation via unsafe as cast

When the "Use new settings store" feature flag is disabled (the default), bulkReadSettings() returns readAllLegacyGlobalSettings() as GlobalSettings and readAllLegacyPersonalSettings() as PersonalSettings without running the data through Zod validation. The new-store path at lines 889-890 correctly validates via GlobalSettingsSchema.parse() / PersonalSettingsSchema.parse(), which would catch type mismatches (e.g., NaN from parseInt for Max filename length, or unexpected shapes from Relations). The legacy path silently passes through whatever the readers produce, potentially causing runtime errors in components that expect validated types.

Example: NaN propagation from legacy export reader

getUidAndIntSetting in apps/roam/src/utils/getExportSettings.ts:49 uses parseInt(...) which can return NaN. The legacy global reader at accessors.ts:423 uses ?? DEFAULT but NaN ?? 64 evaluates to NaN (not null/undefined). The as GlobalSettings cast masks this, while GlobalSettingsSchema.parse() would coerce or reject it.

Prompt for agents
The legacy fallback path in bulkReadSettings() casts readAllLegacyGlobalSettings() and readAllLegacyPersonalSettings() to GlobalSettings / PersonalSettings without Zod validation. The new-store path at lines 889-890 correctly uses .parse(). To fix, run the legacy results through the same Zod schemas: GlobalSettingsSchema.parse(readAllLegacyGlobalSettings()) and PersonalSettingsSchema.parse(readAllLegacyPersonalSettings()). This ensures both code paths produce validated, schema-conformant data and prevents malformed legacy data from propagating as unexpected types.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 142 to 151
window.clearTimeout(debounceRef.current);
debounceRef.current = window.setTimeout(() => {
if (errorRef.current) return;
setter(settingKeys, newValue);
syncToBlock?.(newValue);
debounceRef.current = window.setTimeout(() => {
if (errorRef.current) return;
refreshConfigTree();
setter(settingKeys, newValue);
}, 100);
}, DEBOUNCE_MS);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Nested debounce in BaseTextPanel can skip block-props setter for intermediate values

The handleChange function stores the inner setTimeout ID in the same debounceRef.current as the outer one. If the user types a value, the outer timeout fires (calling syncToBlock), and then the user types again within the 100ms inner window, clearTimeout(debounceRef.current) cancels only the inner timeout — meaning syncToBlock already wrote the intermediate value to the Roam block, but the block-props setter and refreshConfigTree never fire for that value. This creates a transient inconsistency between the Roam block tree and the block-props store until the next change settles. While not data-losing (the final value eventually syncs both), it can cause stale reads from the block-props store during the window.

Sequence that triggers the inconsistency
  1. User types "A" → outer timeout starts (250ms)
  2. Outer fires → syncToBlock("A") writes to Roam, inner timeout (100ms) starts, stored in debounceRef
  3. User types "AB" within 100ms → clearTimeout clears inner, new outer starts
  4. Result: Roam block has "A" written but setter/refreshConfigTree never ran for "A"
  5. Eventually "AB" settles and both stores sync
Prompt for agents
In BaseTextPanel's handleChange, the nested setTimeout pattern shares debounceRef.current for both the outer and inner timeouts, which means a new keystroke during the inner 100ms window cancels the block-props setter but not the already-executed syncToBlock. Consider using a separate ref for the inner timeout so both can be independently cancelled on cleanup and on new keystrokes. Alternatively, collapse the two timeouts into a single debounced callback that does syncToBlock, refreshConfigTree, and setter in sequence after the debounce settles.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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