Skip to content

Initial page-load js (12.49 MB raw / 3,783 KB gzip) --> 2.62 MB raw (790 KB gzip)#4050

Open
jbecke wants to merge 1 commit into
mainfrom
claude/stoic-cannon-twd8bk
Open

Initial page-load js (12.49 MB raw / 3,783 KB gzip) --> 2.62 MB raw (790 KB gzip)#4050
jbecke wants to merge 1 commit into
mainfrom
claude/stoic-cannon-twd8bk

Conversation

@jbecke

@jbecke jbecke commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This PR significantly reduces the initial bundle size by deferring the loading of heavy dependencies until they're actually needed. The changes follow a consistent pattern: move SDK initialization code into separate modules that are dynamically imported, and provide lightweight synchronous accessors for code that runs before the SDKs load.

Key Changes

Observability (Datadog)

  • Extracted Datadog RUM and Logs initialization into a new impl.ts module
  • init() in index.ts now dynamically imports and initializes the SDKs asynchronously
  • Logging falls back to console until Datadog loads, maintaining the same pre-init behavior
  • Updated logger.ts, actionTracker.ts, and routingTracker.ts to use lazy accessors

LiveKit & Krisp

  • Created livekit-loader.ts with lazy loaders for livekit-client and @livekit/krisp-noise-filter
  • Provides sync accessors (getLivekit(), getKrisp()) and async loaders (loadLivekit(), loadKrisp())
  • Exports enum mirrors (LK_CONNECTION_STATE, LK_TRACK_SOURCE) so call state can be tracked without loading the SDK
  • Updated CallContext.tsx to use the loaders and enum mirrors instead of direct imports

Lexical/Markdown Editor

  • Extracted shared commands into new plugins/commands.ts to keep them out of the boot path
  • Separated wrapper context types into context/wrapperContext.ts so decorators don't pull plugin machinery
  • Updated init.ts to import node classes individually instead of from the barrel export
  • Lazy-loaded heavy components: EmojiMenu, InteractiveOnboarding, ShareModal, StaticMarkdown

PostHog Analytics

  • Created deferred PostHog facade that queues operations until the SDK loads
  • Implements only the methods actually used (capture, identify, reset, onFeatureFlags)
  • Maintains the same public interface while deferring the heavy SDK import

Other Optimizations

  • Moved PDF URL params to new block-pdf/constants.ts to avoid pulling the viewer into light consumers
  • Extracted shareButtonRefetch.ts and shareDrawer.ts to keep share modal UI out of initial bundle
  • Created automationComposerState.ts to separate state from UI components
  • Removed unused imports and consolidated related code

Implementation Details

  • All lazy-loaded modules use dynamic import() with promise caching to avoid duplicate loads
  • Synchronous accessors return null until the async load completes, allowing graceful fallbacks
  • Enum mirrors are manually maintained and asserted to their SDK types to catch breaking changes at compile time
  • Decorator components and boot-path code only import types and lightweight utilities, not implementations
  • The pattern is consistent across all SDKs: separate the initialization/heavy code, provide sync accessors, use dynamic imports where needed

https://claude.ai/code/session_01B2vhy7mk22vaiTvzEKPVhw

The entry chunk bundled nearly the whole app: every block UI (pdf,
code, canvas, chat, ...), livekit-client + the Krisp noise filter,
Datadog, PostHog, loro-crdt, the emoji datasets, and two ~700KB
generated zod schema files all loaded before first paint. Initial JS
drops from 12.49MB raw / 3,783KB gzip to 2.62MB raw / 790KB gzip.

Block components
- All block definitions now declare lazy() components (block-image and
  block-project already did); definitions stay eager for the registry
  metadata, the component trees split into route-style chunks.
- block-md's definition loads @service-sync/source (loro) inside load().

Call stack (livekit-client + @livekit/krisp-noise-filter, the two
largest dependencies)
- New channel/Call/livekit-loader.ts: lazy loaders plus compile-time-
  checked mirrors of the livekit string/protobuf enums so call state can
  be tracked without the SDK.
- CallContext creates the LivekitJsCallController (and loads livekit +
  Krisp) on first connect; device enumeration and noise suppression
  resolve the SDKs lazily; CallAudioSink/use-call use the enum mirrors.
- Root/TauriProvider/sidebar import call modules directly instead of the
  @channel/Call barrel, which kept CallOverlay/InCallPanel eager.
- callkit-drawer-theme: replaced colorjs.io with the closed-form
  OKLCH -> sRGB conversion (same math, zero dependency).

Observability / analytics
- observability package now loads the Datadog SDKs via dynamic import in
  init(); pre-init calls fall back to the console exactly as before.
- posthog-js loads lazily behind a queueing facade; capture/identify/
  reset/onFeatureFlags replay once attached, flag reads report undefined
  until loaded (same answers PostHog gives before its flag fetch).

Lexical / markdown stack
- initializeLexical() imports node classes from individual modules, not
  the @lexical-core barrel (which dragged node-list/@lexical/table and
  CustomCodeNode/prismjs).
- Decorator-shared commands moved to plugins/commands.ts; decorators no
  longer import the plugins barrel (which pulled pluginManager -> loro).
- LexicalWrapperContext split: light context in wrapperContext.ts.
- pluginManager loads the loro undo binding lazily (collab callers have
  loro loaded already, so this resolves in a microtask).
- DiffNode.handleAccept loads @lexical/markdown + transformers lazily.
- StaticMarkdown consumers in the boot path (DiffInsert, DocumentPreview,
  entity extractors, search content) lazy-load it; EmojiMenu (fuse.js +
  emoji datasets) is lazy inside MarkdownShell; DocumentCard lazy-loads
  ChannelMessageThreadCard (the whole channel Message tree).

Boot-path import graph fixes
- block-pdf URL_PARAMS moved to @block-pdf/constants; isThreadPlaceable
  moved to type/placeables — light consumers (DocumentPreview, citations,
  notification navigation, the storage client) no longer pull the entire
  PDF viewer.
- Removed the katex/pdfjs manualChunks: both are dynamic-only now, and
  the manual chunk made rollup hoist shared commonjs helpers into the
  pdfjs chunk, statically dragging it into the entry.
- componentRegistry lazy-loads Settings, ChannelCompose, EmailCompose,
  ComposeTask, NotificationsPlayground (removed from the notifications
  barrel); Layout lazy-loads IosShareSheet (iOS-gated) and
  AutomationComposer (open-gated, signal split into its own module);
  GlobalShareModal lazy-loads ShareModal; share-button refetch registry
  and drawer DOM helper split out of ShareButton.tsx.
- instructions-md query loads the editor machinery inside the query fn.

Generated zod schemas (~900KB source, not tree-shakeable)
- storage client loads saveDocumentHandlerResponse lazily (all call
  sites are post-await).
- notifications build their schema lazily with an unvalidated fallback
  until the chunk loads — the same fallback the parse-failure path
  already took.

Tests: 952 passed; the 2 websocket reconnect failures also fail on the
base commit (pre-existing flakes).

https://claude.ai/code/session_01B2vhy7mk22vaiTvzEKPVhw
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bd2710a9-9250-4f81-a268-e2987b061adb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors the codebase to improve code-splitting and reduce initial bundle size through systematic lazy-loading of components and SDKs. It converts 14 block definitions, major UI components (AutomationComposer, ShareModal, InteractiveOnboarding), and Lexical markdown editors from static imports to lazy-loaded variants wrapped in Suspense boundaries. The changes reorganize Lexical module imports to use specific entry points instead of barrel exports, centralize command definitions into a dedicated commands module, and extract context types into a dedicated wrapper context module. A new livekit-loader module provides lazy access to LiveKit and Krisp SDKs with compile-time-safe enum mirrors. External SDKs including PostHog and Datadog are converted to lazy-loaded facades that queue operations until the real implementations are available. Utility logic including PDF URL parameters, placeable types, share button refetch callbacks, and notification schema validation is extracted into dedicated modules. Additional changes remove the colorjs.io dependency by inlining color conversion math, optimize Vite chunk configuration, defer sync service imports, and update import paths throughout to reference specific modules instead of barrel exports.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

@jbecke jbecke changed the title Lazy-load heavy SDKs to reduce initial bundle size Initial page-load js (12.49 MB raw / 3,783 KB gzip) -------> 2.62 MB raw (790 KB gzip) Jun 12, 2026
@jbecke jbecke changed the title Initial page-load js (12.49 MB raw / 3,783 KB gzip) -------> 2.62 MB raw (790 KB gzip) Initial page-load js (12.49 MB raw / 3,783 KB gzip) --> 2.62 MB raw (790 KB gzip) Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 10

🧹 Nitpick comments (1)
js/app/packages/queries/storage/instructions-md.ts (1)

55-67: ⚡ Quick win

Parallelize the raw-state fetch and lazy imports to avoid a request→chunk waterfall.

At Line 55 and Line 64, these independent awaits are sequential right now. Starting both at once trims first-load latency for this query path.

♻️ Proposed refactor
-  const rawState = await syncServiceClient.getRaw({
-    documentId: id,
-  });
-
-  // Loaded lazily: this is the only boot-path consumer of the editor
-  // machinery, which would otherwise land in the initial bundle.
-  const [
-    { createLexicalWrapper },
-    { getTextContent, initializeEditorWithState },
-  ] = await Promise.all([
-    import('`@core/component/LexicalMarkdown/context/LexicalWrapperContext`'),
-    import('`@core/component/LexicalMarkdown/utils`'),
-  ]);
+  const rawStatePromise = syncServiceClient.getRaw({ documentId: id });
+  const lexicalDepsPromise = Promise.all([
+    import('`@core/component/LexicalMarkdown/context/LexicalWrapperContext`'),
+    import('`@core/component/LexicalMarkdown/utils`'),
+  ]);
+
+  const [
+    rawState,
+    [{ createLexicalWrapper }, { getTextContent, initializeEditorWithState }],
+  ] = await Promise.all([rawStatePromise, lexicalDepsPromise]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/queries/storage/instructions-md.ts` around lines 55 - 67, The
current code awaits syncServiceClient.getRaw (rawState) before starting the lazy
imports, creating a request→chunk waterfall; instead start both operations
concurrently by kicking off the raw fetch promise and the import Promise.all at
the same time, then await both promises (referencing rawState from the resolved
syncServiceClient.getRaw promise and the imported symbols createLexicalWrapper,
getTextContent, initializeEditorWithState from the Promise.all import). Ensure
you still use the same identifiers (rawState, syncServiceClient.getRaw,
createLexicalWrapper, getTextContent, initializeEditorWithState,
initializeEditorWithState) so downstream code is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@js/app/packages/app/component/Layout.tsx`:
- Around line 174-176: The IosShareSheet lazy component is rendered directly
under the outer Suspense and can suspend the whole authenticated subtree; wrap
the IosShareSheet component in a local Suspense boundary (similar to the
AutomationComposer pattern) so its loading only affects itself. Locate the Show
block using getPlatform() === 'ios' and replace the direct <IosShareSheet />
with a local <Suspense fallback={null}> (or appropriate minimal fallback) that
contains <IosShareSheet /> to isolate its suspension.

In `@js/app/packages/app/component/next-soup/utils.ts`:
- Line 12: openEntityInNewTab currently writes legacy PDF query keys
(search_page, search_raw_query, search_highlight_terms, search_snippet) when
building PDF new-tab URLs, but navigateToLocation and block-pdf expect canonical
PDF_PARAMS keys (pdf_search_page, pdf_search_raw_query,
pdf_search_highlight_terms, pdf_search_snippet). Fix openEntityInNewTab (pdf
case) to set entityUrl.searchParams using PDF_PARAMS.searchPage,
PDF_PARAMS.searchRawQuery, PDF_PARAMS.searchHighlightTerms and
PDF_PARAMS.searchSnippet instead of the hardcoded legacy strings so the created
URL matches parseLocationFromUrl and navigateToLocation.

In `@js/app/packages/channel/Call/callkit-drawer-theme.ts`:
- Around line 182-186: The green-channel coefficient in oklchToLinearSrgb is
wrong: in the returned array (the second element mapping LMS to linear sRGB)
replace the term using -0.7034186147 * s with Ottosson’s correct coefficient
-0.3413193965 * s so the matrix row becomes -1.2684380046 * l + 2.6097574011 * m
- 0.3413193965 * s; update that coefficient in the function oklchToLinearSrgb to
correct the LMS→linear-sRGB conversion.

In `@js/app/packages/core/component/LexicalMarkdown/plugins/pluginManager.ts`:
- Around line 41-44: The dynamic import in the history() block may reject and
cause an unhandled promise; wrap the import promise with error handling so
failures are caught and handled: add a .catch(...) (or try/catch around an async
import) to the import('../collaboration/undo') call and in the catcher log or
no-op (using existing logging mechanism) and ensure you do not change cleanup
when the import fails; keep references to registerLoroHistory,
loroManager.getDoc(), timeGap, cleanup and cancelled and ensure that if the
import fails you return/leave cleanup undefined and avoid calling
registerLoroHistory for editor.

In `@js/app/packages/entity/src/extractors-search/search-content.tsx`:
- Around line 46-48: The Suspense wrapper around StaticMarkdown currently has no
fallback which can cause a blank snippet during loading; update the Suspense
(wrapping StaticMarkdown in search-content.tsx) to include an appropriate
fallback prop (e.g., a lightweight skeleton/spinner or a condensed text
placeholder) so users see a stable UI while trimmedContent() and theme()
resolve; ensure the fallback visually matches the snippet size and replace the
current empty Suspense usage with Suspense fallback={...} referencing the same
StaticMarkdown component context.

In `@js/app/packages/notifications/notification-source.ts`:
- Around line 215-217: The fire-and-forget call to
loadUnifiedNotificationSchema() can produce an unhandled rejection if the
dynamic import fails; change the invocation at the top-level (the current "void
loadUnifiedNotificationSchema();") to handle errors explicitly by attaching a
.catch(...) (or awaiting inside an async init) and log or safely swallow the
error. Specifically, replace the bare call with something like: void
loadUnifiedNotificationSchema().catch(err => /* use existing logger or
console.error with a clear message referencing loadUnifiedNotificationSchema */)
so any chunk-load failures are caught and reported.

In `@js/app/packages/notifications/types.ts`:
- Around line 27-33: The dynamic-import memoization in
loadUnifiedNotificationSchema() keeps schemaPromise set on rejection and drops
the error, so reset schemaPromise and clear loadedSchema when the import/build
fails and re-throw the error to avoid a permanently failed cache; specifically,
change the Promise chain for schemaPromise (and
buildUnifiedNotificationSchema(m) usage) to include a .catch(err => {
schemaPromise = undefined; loadedSchema = null; throw err; }) so subsequent
calls can retry, and update the caller that uses void
loadUnifiedNotificationSchema() to attach a .catch(...) to avoid unhandled
rejections.

In `@js/app/packages/observability/src/index.ts`:
- Around line 5-9: Replace the loose any index signature on the User interface
and other observability payload types with unknown and narrow at consumption
sites: change "interface User { id: string; email: string; [key: string]: any }"
to use "[key: string]: unknown", update analogous signatures in logger.ts and
impl.ts to unknown, then update code that reads these dynamic properties (e.g.,
inside the logger creation functions and the implementation code around the
consumer at impl.ts line ~96) to type-check/cast or validate the expected shape
before accessing fields (use type predicates, safe property checks, or explicit
casts) so payload handling remains type-safe.

In `@js/app/packages/service-clients/service-storage/client.ts`:
- Around line 157-162: getSavedDocumentMetadataSchema currently memoizes a
failing dynamic import forever; change it to catch errors from
import('./generated/zod'), reset savedDocumentMetadataSchemaPromise to undefined
(so a transient failure doesn't get cached) and rethrow the error so callers can
handle it; then update pdfSave, simpleSave, and simpleSaveText to wrap the await
getSavedDocumentMetadataSchema() in a try/catch and convert import/schema-load
errors into the recoverable Result failure path before calling .safeParse (or
bail early), ensuring you reference savedDocumentMetadataSchemaPromise,
getSavedDocumentMetadataSchema, and the three save functions when making the
changes.

In `@js/lexical-core/nodes/DiffNode.ts`:
- Around line 113-142: Capture the node key before the lazy imports and
re-resolve the node inside editor.update to avoid operating on a
possibly-stale/captured `this` (e.g., in DiffNode.handleAccept do const nodeKey
= this.getKey(); then inside editor.update call const node =
$getNodeByKey(nodeKey) and bail if null before calling node.getChildren(),
node.clear(), $convertFromMarkdownString(..., node, ...), and
$unwrapNode(node)); also append a .catch(...) to the Promise.all(...) import
chain to handle and log import errors so you don't leave unhandled promise
rejections.

---

Nitpick comments:
In `@js/app/packages/queries/storage/instructions-md.ts`:
- Around line 55-67: The current code awaits syncServiceClient.getRaw (rawState)
before starting the lazy imports, creating a request→chunk waterfall; instead
start both operations concurrently by kicking off the raw fetch promise and the
import Promise.all at the same time, then await both promises (referencing
rawState from the resolved syncServiceClient.getRaw promise and the imported
symbols createLexicalWrapper, getTextContent, initializeEditorWithState from the
Promise.all import). Ensure you still use the same identifiers (rawState,
syncServiceClient.getRaw, createLexicalWrapper, getTextContent,
initializeEditorWithState, initializeEditorWithState) so downstream code is
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3bc22db-706c-4478-bdba-5528d964a5ff

📥 Commits

Reviewing files that changed from the base of the PR and between 3ef6e93 and f91a575.

📒 Files selected for processing (87)
  • js/app/packages/app/component/Launcher.tsx
  • js/app/packages/app/component/Layout.tsx
  • js/app/packages/app/component/Root.tsx
  • js/app/packages/app/component/app-sidebar/active-call-widget.tsx
  • js/app/packages/app/component/app-sidebar/sidebar.tsx
  • js/app/packages/app/component/global-share-modal/GlobalShareModal.tsx
  • js/app/packages/app/component/interactive-onboarding/InteractiveOnboardingModal.tsx
  • js/app/packages/app/component/next-soup/soup-view/SoupEntityActionDrawer.tsx
  • js/app/packages/app/component/next-soup/utils.ts
  • js/app/packages/app/component/split-layout/componentRegistry.tsx
  • js/app/packages/app/lib/analytics/analytics.ts
  • js/app/packages/app/vite.base.ts
  • js/app/packages/block-automation/component/AutomationComposer.tsx
  • js/app/packages/block-automation/component/automationComposerState.ts
  • js/app/packages/block-automation/component/index.ts
  • js/app/packages/block-automation/definition.ts
  • js/app/packages/block-call/definition.ts
  • js/app/packages/block-canvas/definition.ts
  • js/app/packages/block-channel/definition.ts
  • js/app/packages/block-chat/definition.ts
  • js/app/packages/block-code/definition.ts
  • js/app/packages/block-company/definition.ts
  • js/app/packages/block-contact/definition.ts
  • js/app/packages/block-email/definition.ts
  • js/app/packages/block-md/definition.ts
  • js/app/packages/block-pdf/constants.ts
  • js/app/packages/block-pdf/definition.ts
  • js/app/packages/block-pdf/signal/location.ts
  • js/app/packages/block-pdf/store/comments/freeComments.ts
  • js/app/packages/block-pdf/type/coParse.ts
  • js/app/packages/block-pdf/type/placeables.ts
  • js/app/packages/block-unknown/definition.ts
  • js/app/packages/block-video/definition.ts
  • js/app/packages/channel/Call/CallAudioSink.tsx
  • js/app/packages/channel/Call/CallContext.tsx
  • js/app/packages/channel/Call/callkit-drawer-theme.ts
  • js/app/packages/channel/Call/livekit-loader.ts
  • js/app/packages/channel/Call/use-call.ts
  • js/app/packages/core/component/DocumentPreview.tsx
  • js/app/packages/core/component/LexicalMarkdown/builder/MarkdownShell.tsx
  • js/app/packages/core/component/LexicalMarkdown/citationsUtils.ts
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/ContactMention.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/DateMention.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/DiffInsert.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentMention.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/Equation.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/GroupMention.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/HorizontalRule.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/MarkdownImage.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/MarkdownVideo.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/Snapshot.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/ThemeMention.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/UnknownMention.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/UserMention.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/dom-factory/diff-factory.tsx
  • js/app/packages/core/component/LexicalMarkdown/context/LexicalWrapperContext.tsx
  • js/app/packages/core/component/LexicalMarkdown/context/wrapperContext.ts
  • js/app/packages/core/component/LexicalMarkdown/init.ts
  • js/app/packages/core/component/LexicalMarkdown/plugins/commands.ts
  • js/app/packages/core/component/LexicalMarkdown/plugins/document-metadata/documentMetadataPlugin.ts
  • js/app/packages/core/component/LexicalMarkdown/plugins/katex/katexPlugin.ts
  • js/app/packages/core/component/LexicalMarkdown/plugins/media/mediaPlugin.ts
  • js/app/packages/core/component/LexicalMarkdown/plugins/mentions/mentionsPlugin.ts
  • js/app/packages/core/component/LexicalMarkdown/plugins/pluginManager.ts
  • js/app/packages/core/component/LexicalMarkdown/utils.ts
  • js/app/packages/core/component/TopBar/ShareButton.tsx
  • js/app/packages/core/component/TopBar/shareButtonRefetch.ts
  • js/app/packages/core/component/TopBar/shareDrawer.ts
  • js/app/packages/entity/src/extractors-search/search-content.tsx
  • js/app/packages/entity/src/extractors/entity-title.tsx
  • js/app/packages/notifications/index.ts
  • js/app/packages/notifications/notification-navigation.ts
  • js/app/packages/notifications/notification-platform.ts
  • js/app/packages/notifications/notification-source.ts
  • js/app/packages/notifications/types.ts
  • js/app/packages/observability/src/actionTracker.ts
  • js/app/packages/observability/src/impl.ts
  • js/app/packages/observability/src/index.ts
  • js/app/packages/observability/src/logger.ts
  • js/app/packages/observability/src/routingTracker.ts
  • js/app/packages/observability/src/shared.ts
  • js/app/packages/queries/storage/instructions-md.ts
  • js/app/packages/service-clients/service-storage/client.ts
  • js/app/packages/service-clients/service-storage/util/refetchResources.ts
  • js/app/packages/tauri/src/TauriProvider.tsx
  • js/lexical-core/nodes/DiffNode.ts
💤 Files with no reviewable changes (1)
  • js/app/packages/notifications/index.ts

Comment on lines +174 to +176
<Show when={getPlatform() === 'ios'}>
<IosShareSheet />
</Show>

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where IosShareSheet is mounted relative to Suspense boundaries.
rg -n -C3 "IosShareSheet|<Suspense>|</Suspense>" js/app/packages/app/component/Layout.tsx

Repository: macro-inc/macro

Length of output: 1847


🏁 Script executed:

#!/bin/bash
# Inspect surrounding Suspense boundaries and the IosShareSheet render location
nl -ba js/app/packages/app/component/Layout.tsx | sed -n '140,200p'

Repository: macro-inc/macro

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Show Layout.tsx section around the Suspense boundaries and IosShareSheet
sed -n '130,200p' js/app/packages/app/component/Layout.tsx

Repository: macro-inc/macro

Length of output: 2095


🏁 Script executed:

#!/bin/bash
# Find all Suspense usages (and whether they specify a fallback) in this file
rg -n "<Suspense" js/app/packages/app/component/Layout.tsx

Repository: macro-inc/macro

Length of output: 180


Wrap IosShareSheet in a local <Suspense> boundary.

IosShareSheet is a lazy component rendered directly under the outer <Suspense> (no local boundary), so its first unresolved load can suspend the rest of the authenticated subtree. Isolate it like the existing AutomationComposer pattern.

Proposed fix
-          <Show when={getPlatform() === 'ios'}>
-            <IosShareSheet />
-          </Show>
+          <Show when={getPlatform() === 'ios'}>
+            <Suspense>
+              <IosShareSheet />
+            </Suspense>
+          </Show>
📝 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
<Show when={getPlatform() === 'ios'}>
<IosShareSheet />
</Show>
<Show when={getPlatform() === 'ios'}>
<Suspense>
<IosShareSheet />
</Suspense>
</Show>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/app/component/Layout.tsx` around lines 174 - 176, The
IosShareSheet lazy component is rendered directly under the outer Suspense and
can suspend the whole authenticated subtree; wrap the IosShareSheet component in
a local Suspense boundary (similar to the AutomationComposer pattern) so its
loading only affects itself. Locate the Show block using getPlatform() === 'ios'
and replace the direct <IosShareSheet /> with a local <Suspense fallback={null}>
(or appropriate minimal fallback) that contains <IosShareSheet /> to isolate its
suspension.

import { URL_PARAMS as EMAIL_PARAMS } from '@block-email/constants';
import { URL_PARAMS as MD_PARAMS } from '@block-md/constants';
import { URL_PARAMS as PDF_PARAMS } from '@block-pdf/signal/location';
import { URL_PARAMS as PDF_PARAMS } from '@block-pdf/constants';

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Canonical PDF URL param definitions =="
rg -n --type=ts --type=tsx -C2 "pdf_search_page|pdf_search_raw_query|pdf_search_highlight_terms|pdf_search_snippet|URL_PARAMS\\.searchPage|URL_PARAMS\\.searchRawQuery|URL_PARAMS\\.searchHighlightTerms|URL_PARAMS\\.searchSnippet"

echo
echo "== Legacy literal key usage =="
rg -n --type=ts --type=tsx -C2 "'search_page'|\"search_page\"|'search_raw_query'|\"search_raw_query\"|'search_highlight_terms'|\"search_highlight_terms\"|'search_snippet'|\"search_snippet\""

echo
echo "== URL parsing/normalization sites for PDF params =="
rg -n --type=ts --type=tsx -C3 "parseLocationFromUrl|parseLocationFromBlockParams|goToLocationFromParams|URLSearchParams|get\\(.*search_page|get\\(.*pdf_search_page"

Repository: macro-inc/macro

Length of output: 147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Canonical PDF URL param definitions (PDF_PARAMS / URL_PARAMS members) =="
rg -n -g'*.ts' -g'*.tsx' -C2 \
  "PDF_PARAMS|URL_PARAMS\\.(searchPage|searchRawQuery|searchHighlightTerms|searchSnippet)" \
  js/app || true

echo
echo "== Legacy literal key usage (search_page / raw_query / highlight_terms / snippet) =="
rg -n -g'*.ts' -g'*.tsx' -C3 \
  "['\"]search_page['\"]|['\"]search_raw_query['\"]|['\"]search_highlight_terms['\"]|['\"]search_snippet['\"]" \
  js/app || true

echo
echo "== Usage of pdf_search_* keys =="
rg -n -g'*.ts' -g'*.tsx' -C2 \
  "pdf_search_page|pdf_search_raw_query|pdf_search_highlight_terms|pdf_search_snippet" \
  js/app || true

echo
echo "== URL parsing/normalization sites for PDF params =="
rg -n -g'*.ts' -g'*.tsx' -C3 \
  "(parseLocationFromUrl|parseLocationFromBlockParams|goToLocationFromParams|URLSearchParams|get\\(.*search_page|get\\(.*pdf_search_page))" \
  js/app || true

echo
echo "== Focus: utils.ts functions around openEntityInNewTab / navigateToLocation =="
FILE="js/app/packages/app/component/next-soup/utils.ts"
if [ -f "$FILE" ]; then
  rg -n -g'*.ts' -g'*.tsx' "openEntityInNewTab|navigateToLocation" "$FILE" || true
  echo "--- openEntityInNewTab context ---"
  rg -n -g'*.ts' -g'*.tsx' "openEntityInNewTab" "$FILE" -C12 || true
  echo "--- navigateToLocation context ---"
  rg -n -g'*.ts' -g'*.tsx' "navigateToLocation" "$FILE" -C12 || true
else
  echo "Missing file: $FILE" >&2
  exit 1
fi

Repository: macro-inc/macro

Length of output: 9124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Where legacy keys are used across the repo =="
rg -n -g'*.ts' -g'*.tsx' \
  "['\"]search_page['\"]|['\"]search_raw_query['\"]|['\"]search_highlight_terms['\"]|['\"]search_snippet['\"]" \
  js/app | head -n 200

echo
echo "== Where canonical pdf_search_* keys are referenced =="
rg -n -g'*.ts' -g'*.tsx' \
  "pdf_search_page|pdf_search_raw_query|pdf_search_highlight_terms|pdf_search_snippet" \
  js/app | head -n 200

echo
echo "== Any normalization from legacy -> canonical in block-pdf package? =="
rg -n -g'*.ts' -g'*.tsx' \
  "search_page|search_raw_query|search_highlight_terms|search_snippet|pdf_search_page|pdf_search_raw_query|pdf_search_highlight_terms|pdf_search_snippet" \
  js/app/packages/block-pdf | head -n 400

Repository: macro-inc/macro

Length of output: 1297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) next-soup/utils.ts around openEntityInNewTab pdf block
FILE1="js/app/packages/app/component/next-soup/utils.ts"
echo "=== openEntityInNewTab PDF block (utils.ts) ==="
sed -n '190,270p' "$FILE1" | cat -n

echo
echo "=== navigateToLocation PDF block (utils.ts) ==="
sed -n '430,490p' "$FILE1" | cat -n

# 2) block-pdf constants and parsing
FILE2="js/app/packages/block-pdf/constants.ts"
echo
echo "=== block-pdf/constants.ts (URL_PARAMS) ==="
sed -n '1,120p' "$FILE2" | cat -n

FILE3="js/app/packages/block-pdf/component/Document.tsx"
echo
echo "=== block-pdf/Document.tsx query param reads ==="
# Find the exact searchParams usage region
rg -n "searchParams\\[URL_PARAMS" "$FILE3" | head -n 20
sed -n '500,580p' "$FILE3" | cat -n

FILE4="js/app/packages/block-pdf/signal/location.ts"
echo
echo "=== block-pdf/signal/location.ts parseLocationParams mapping ==="
rg -n "parseLocationParams\\(|URL_PARAMS\\." "$FILE4" | head -n 50
sed -n '360,460p' "$FILE4" | cat -n

# 3) Search for any legacy -> canonical fallback in block-pdf
echo
echo "=== block-pdf legacy key fallback checks ==="
rg -n -g'*.ts' -g'*.tsx' "search_page|search_raw_query|search_highlight_terms|search_snippet" js/app/packages/block-pdf || true

# 4) Rerun URL parsing helper location search (use simpler regex, avoid capturing group mistakes)
echo
echo "=== find URL parsing helpers mentioning search_page/pdf_search_page ==="
rg -n -g'*.ts' -g'*.tsx' "pdf_search_page|search_page" js/app/packages/block-pdf | head -n 100

Repository: macro-inc/macro

Length of output: 15019


Fix PDF new-tab deep links to use canonical pdf_search_* query param keys.

openEntityInNewTab (pdf case) sets legacy keys (search_page, search_raw_query, search_highlight_terms, search_snippet), but navigateToLocation uses PDF_PARAMS.* (pdf_search_*), and block-pdf’s URL parsing (Document.tsx/parseLocationFromUrl) reads only URL_PARAMS.*. This mismatch will silently drop the target location in new-tab PDF links (no legacy fallback in block-pdf).

Update openEntityInNewTab to set entityUrl.searchParams using PDF_PARAMS.searchPage/searchRawQuery/searchHighlightTerms/searchSnippet instead of the hardcoded legacy strings. (js/app/packages/app/component/next-soup/utils.ts, openEntityInNewTab pdf case ~222-243)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/app/component/next-soup/utils.ts` at line 12,
openEntityInNewTab currently writes legacy PDF query keys (search_page,
search_raw_query, search_highlight_terms, search_snippet) when building PDF
new-tab URLs, but navigateToLocation and block-pdf expect canonical PDF_PARAMS
keys (pdf_search_page, pdf_search_raw_query, pdf_search_highlight_terms,
pdf_search_snippet). Fix openEntityInNewTab (pdf case) to set
entityUrl.searchParams using PDF_PARAMS.searchPage, PDF_PARAMS.searchRawQuery,
PDF_PARAMS.searchHighlightTerms and PDF_PARAMS.searchSnippet instead of the
hardcoded legacy strings so the created URL matches parseLocationFromUrl and
navigateToLocation.

Comment on lines +182 to +186
return [
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.7034186147 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
];

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

OKLab to linear sRGB matrix coefficients Ottosson

💡 Result:

OKLab to linear sRGB matrix coefficients (Ottosson / Oklab reference implementation), for linear-light sRGB (no gamma): linear_srgb = [r] [ 4.0767416621 -3.3077115913 0.2309699292] [l] [g] = [-1.2684380046 2.6097574011 -0.3413193965] [m] [b] [-0.0041960863 -0.7034186147 1.7076147010] [s] Where l,m,s are the LMS cube-root domain computed from Oklab as: l_ = L + 0.3963377774a + 0.2158037573b m_ = L - 0.1055613458a - 0.0638541728b s_ = L - 0.0894841775a - 1.2914855480b l = l_^3, m = m_^3, s = s_^3. These coefficients are explicitly given in Ottosson’s published reference code (and mirrored in implementations/libraries). [1][2][3]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/channel/Call/callkit-drawer-theme.ts"
if [ ! -f "$FILE" ]; then echo "Missing $FILE"; exit 1; fi
nl -ba "$FILE" | sed -n '160,210p'

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/channel/Call/callkit-drawer-theme.ts"
nl -ba "$FILE" | sed -n '160,210p'

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/channel/Call/callkit-drawer-theme.ts"
nl -ba "$FILE" | sed -n '160,210p'

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/channel/Call/callkit-drawer-theme.ts"
nl -ba "$FILE" | sed -n '160,210p'

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/channel/Call/callkit-drawer-theme.ts"
nl -ba "$FILE" | sed -n '160,210p'

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/channel/Call/callkit-drawer-theme.ts"
test -f "$FILE" && echo "Found $FILE" || { echo "Missing $FILE"; exit 1; }

echo "---- lines 160-205 ----"
sed -n '160,205p' "$FILE" | awk '{print NR+159 ":" $0}'

echo "---- occurrences of -0.7034186147 ----"
rg -n --fixed-string "-0.7034186147" "$FILE" || true

echo "---- occurrences of -0.3413193965 ----"
rg -n --fixed-string "-0.3413193965" "$FILE" || true

Repository: macro-inc/macro

Length of output: 2820


Fix OKLCH→linear sRGB matrix coefficient for green channel

oklchToLinearSrgb() uses the wrong LMS→linear-sRGB coefficient for the green channel: line 184 has -0.7034186147 * s, but Ottosson’s reference uses -0.3413193965 for that term. This will skew all green values produced by this conversion.

🐛 Proposed fix
   return [
     4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-    -1.2684380046 * l + 2.6097574011 * m - 0.7034186147 * s,
+    -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
     -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
   ];
📝 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
return [
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.7034186147 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
];
return [
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/channel/Call/callkit-drawer-theme.ts` around lines 182 - 186,
The green-channel coefficient in oklchToLinearSrgb is wrong: in the returned
array (the second element mapping LMS to linear sRGB) replace the term using
-0.7034186147 * s with Ottosson’s correct coefficient -0.3413193965 * s so the
matrix row becomes -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
update that coefficient in the function oklchToLinearSrgb to correct the
LMS→linear-sRGB conversion.

Comment on lines +41 to +44
void import('../collaboration/undo').then(({ registerLoroHistory }) => {
if (cancelled) return;
cleanup = registerLoroHistory(editor, loroManager.getDoc(), timeGap);
});

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle dynamic import rejection in history() to avoid unhandled promise failures.

Line 41 starts a lazy import with only .then(...); if the chunk fails to load, this becomes an unhandled rejection and leaves history unregistered for that editor instance.

Suggested patch
-        void import('../collaboration/undo').then(({ registerLoroHistory }) => {
-          if (cancelled) return;
-          cleanup = registerLoroHistory(editor, loroManager.getDoc(), timeGap);
-        });
+        void import('../collaboration/undo')
+          .then(({ registerLoroHistory }) => {
+            if (cancelled) return;
+            cleanup = registerLoroHistory(editor, loroManager.getDoc(), timeGap);
+          })
+          .catch((error) => {
+            if (cancelled) return;
+            console.error('Failed to load collaboration undo module', error);
+            cleanup = registerHistory(editor, createEmptyHistoryState(), timeGap);
+          });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/core/component/LexicalMarkdown/plugins/pluginManager.ts`
around lines 41 - 44, The dynamic import in the history() block may reject and
cause an unhandled promise; wrap the import promise with error handling so
failures are caught and handled: add a .catch(...) (or try/catch around an async
import) to the import('../collaboration/undo') call and in the catcher log or
no-op (using existing logging mechanism) and ensure you do not change cleanup
when the import fails; keep references to registerLoroHistory,
loroManager.getDoc(), timeGap, cleanup and cancelled and ensure that if the
import fails you return/leave cleanup undefined and avoid calling
registerLoroHistory for editor.

Comment on lines +46 to +48
<Suspense>
<StaticMarkdown markdown={trimmedContent()} theme={theme()} />
</Suspense>

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a Suspense fallback to prevent blank snippet flashes.

At Line 46, Suspense has no fallback, so the snippet can render empty while StaticMarkdown is still loading.

💡 Suggested patch
-            <Suspense>
+            <Suspense fallback={<span class="italic text-ink-disabled">Loading…</span>}>
               <StaticMarkdown markdown={trimmedContent()} theme={theme()} />
             </Suspense>
📝 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
<Suspense>
<StaticMarkdown markdown={trimmedContent()} theme={theme()} />
</Suspense>
<Suspense fallback={<span class="italic text-ink-disabled">Loading…</span>}>
<StaticMarkdown markdown={trimmedContent()} theme={theme()} />
</Suspense>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/entity/src/extractors-search/search-content.tsx` around lines
46 - 48, The Suspense wrapper around StaticMarkdown currently has no fallback
which can cause a blank snippet during loading; update the Suspense (wrapping
StaticMarkdown in search-content.tsx) to include an appropriate fallback prop
(e.g., a lightweight skeleton/spinner or a condensed text placeholder) so users
see a stable UI while trimmedContent() and theme() resolve; ensure the fallback
visually matches the snippet size and replace the current empty Suspense usage
with Suspense fallback={...} referencing the same StaticMarkdown component
context.

Comment on lines +215 to +217
// Kick off the lazy schema load so it is ready before the first
// notification arrives over the websocket.
void loadUnifiedNotificationSchema();

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify fire-and-forget preload call has explicit rejection handling
rg -n -C2 'void loadUnifiedNotificationSchema\(\)' js/app/packages/notifications/notification-source.ts
rg -n -C2 'loadUnifiedNotificationSchema\(\)\.catch' js/app/packages/notifications/notification-source.ts

Repository: macro-inc/macro

Length of output: 272


🏁 Script executed:

#!/bin/bash
# Find where loadUnifiedNotificationSchema is defined and how it handles failures.
rg -n --hidden --no-ignore -S "function loadUnifiedNotificationSchema|const loadUnifiedNotificationSchema|export (async )?function loadUnifiedNotificationSchema|loadUnifiedNotificationSchema\W*=" js/app/packages/notifications
rg -n -S "loadUnifiedNotificationSchema" js/app/packages/notifications/notification-source.ts

Repository: macro-inc/macro

Length of output: 267


🏁 Script executed:

#!/bin/bash
# Show the implementation of loadUnifiedNotificationSchema and any internal error handling.
sed -n '1,120p' js/app/packages/notifications/types.ts

Repository: macro-inc/macro

Length of output: 1994


🏁 Script executed:

#!/bin/bash
rg -n "loadUnifiedNotificationSchema\(\)" js/app/packages/notifications

Repository: macro-inc/macro

Length of output: 399


Handle preload failures to avoid unhandled Promise rejections
notification-source.ts fire-and-forgets void loadUnifiedNotificationSchema();, but loadUnifiedNotificationSchema() performs a dynamic import(...) and returns a promise with no internal error handling—so a failed chunk load can trigger unhandledrejection on startup.

Suggested fix
-  void loadUnifiedNotificationSchema();
+  void loadUnifiedNotificationSchema().catch((error) => {
+    console.warn('Failed to load notification schema', error);
+  });
📝 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
// Kick off the lazy schema load so it is ready before the first
// notification arrives over the websocket.
void loadUnifiedNotificationSchema();
// Kick off the lazy schema load so it is ready before the first
// notification arrives over the websocket.
void loadUnifiedNotificationSchema().catch((error) => {
console.warn('Failed to load notification schema', error);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/notifications/notification-source.ts` around lines 215 - 217,
The fire-and-forget call to loadUnifiedNotificationSchema() can produce an
unhandled rejection if the dynamic import fails; change the invocation at the
top-level (the current "void loadUnifiedNotificationSchema();") to handle errors
explicitly by attaching a .catch(...) (or awaiting inside an async init) and log
or safely swallow the error. Specifically, replace the bare call with something
like: void loadUnifiedNotificationSchema().catch(err => /* use existing logger
or console.error with a clear message referencing loadUnifiedNotificationSchema
*/) so any chunk-load failures are caught and reported.

Comment on lines +27 to +33
export function loadUnifiedNotificationSchema(): Promise<UnifiedNotificationSchema> {
schemaPromise ??= import('@service-notification/generated/zod').then((m) => {
loadedSchema = buildUnifiedNotificationSchema(m);
return loadedSchema;
});
return schemaPromise;
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether rejected lazy-load promises are reset for retry
rg -n -C3 'schemaPromise \?\?=' js/app/packages/notifications/types.ts
rg -n -C3 'schemaPromise\s*=\s*null' js/app/packages/notifications/types.ts
rg -n -C3 'catch\s*\(' js/app/packages/notifications/types.ts

Repository: macro-inc/macro

Length of output: 401


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="js/app/packages/notifications/types.ts"

# Show the full surrounding implementation for context
nl -ba "$FILE" | sed -n '1,120p'

# Focus around the loader and cache variables
nl -ba "$FILE" | sed -n '15,80p'

# Find any other mention of loadUnifiedNotificationSchema / schemaPromise / loadedSchema
rg -n "loadUnifiedNotificationSchema|schemaPromise|loadedSchema" "$FILE"

# Check for error handling patterns in callers (usage sites)
rg -n "loadUnifiedNotificationSchema\(" js/app/packages/notifications -S

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="js/app/packages/notifications/types.ts"

echo "=== File excerpt (1-120) ==="
awk 'NR>=1 && NR<=120 {printf "%5d:%s\n", NR, $0}' "$FILE"

echo "=== Focus excerpt (15-60) ==="
awk 'NR>=15 && NR<=60 {printf "%5d:%s\n", NR, $0}' "$FILE"

echo "=== Loader-related symbols ==="
rg -n "loadUnifiedNotificationSchema|schemaPromise|loadedSchema" "$FILE"

echo "=== Call sites (within notifications package) ==="
rg -n "loadUnifiedNotificationSchema\\(" js/app/packages/notifications -S

Repository: macro-inc/macro

Length of output: 4857


🏁 Script executed:

#!/bin/bash
set -euo pipefail

RG_TARGETS=( "js/app/packages/notifications/notification-source.ts" )

for f in "${RG_TARGETS[@]}"; do
  echo "=== $f (around loadUnifiedNotificationSchema call) ==="
  # print ~30 lines around the match
  awk '{
    if ($0 ~ /loadUnifiedNotificationSchema\(\)/) {found=NR; start=NR-20; end=NR+20}
    if (NR>=start && NR<=end) {printf "%5d:%s\n", NR, $0}
  }' "$f" | sed -n '1,120p'
done

echo "=== Where getUnifiedNotificationSchema() is used in notifications package ==="
rg -n "getUnifiedNotificationSchema\\(" js/app/packages/notifications -S

Repository: macro-inc/macro

Length of output: 1399


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== All call sites of loadUnifiedNotificationSchema() in js/app/packages/notifications ==="
rg -n "loadUnifiedNotificationSchema\\(" js/app/packages/notifications -S

echo "=== All call sites of loadUnifiedNotificationSchema() across js/app ==="
rg -n "loadUnifiedNotificationSchema\\(" js/app -S

echo "=== Inspect notification-source.ts around the load call (190-260) ==="
FILE="js/app/packages/notifications/notification-source.ts"
awk 'NR>=180 && NR<=260 {printf "%5d:%s\n", NR, $0}' "$FILE"

echo "=== Search for any catch/then handling nearby in notification-source.ts ==="
rg -n "loadUnifiedNotificationSchema\\(|catch\\(|unhandledrejection" js/app/packages/notifications/notification-source.ts -S

Repository: macro-inc/macro

Length of output: 4084


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "unhandledrejection|unhandledRejection" js/app -S
rg -n "loadUnifiedNotificationSchema\\(\\)" js/app -S

Repository: macro-inc/macro

Length of output: 41


Reset the cached unified notification schema loader promise on failure (and don’t ignore the rejection).

loadUnifiedNotificationSchema() memoizes the dynamic import promise via schemaPromise ??= ... and never clears schemaPromise on rejection, so getUnifiedNotificationSchema() stays null and websocket notifications fall back to unvalidated parsing for the rest of the session. The only caller drops the promise (void loadUnifiedNotificationSchema()), so failures can also surface as unhandled rejections.

Suggested fix
 export function loadUnifiedNotificationSchema(): Promise<UnifiedNotificationSchema> {
-  schemaPromise ??= import('`@service-notification/generated/zod`').then((m) => {
-    loadedSchema = buildUnifiedNotificationSchema(m);
-    return loadedSchema;
-  });
+  schemaPromise ??= import('`@service-notification/generated/zod`')
+    .then((m) => {
+      loadedSchema = buildUnifiedNotificationSchema(m);
+      return loadedSchema;
+    })
+    .catch((error) => {
+      schemaPromise = null;
+      throw error;
+    });
   return schemaPromise;
 }

Also handle the caller’s ignored promise (e.g., attach a .catch at void loadUnifiedNotificationSchema();) so import failures don’t become unhandled rejections.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/notifications/types.ts` around lines 27 - 33, The
dynamic-import memoization in loadUnifiedNotificationSchema() keeps
schemaPromise set on rejection and drops the error, so reset schemaPromise and
clear loadedSchema when the import/build fails and re-throw the error to avoid a
permanently failed cache; specifically, change the Promise chain for
schemaPromise (and buildUnifiedNotificationSchema(m) usage) to include a
.catch(err => { schemaPromise = undefined; loadedSchema = null; throw err; }) so
subsequent calls can retry, and update the caller that uses void
loadUnifiedNotificationSchema() to attach a .catch(...) to avoid unhandled
rejections.

Comment on lines +5 to +9
interface User {
id: string;
email: string;
[key: string]: any;
}

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify remaining `any` in observability sources
rg -nP '\bany\b' --type ts js/app/packages/observability/src

Repository: macro-inc/macro

Length of output: 261


Remove any from observability payload types

any still exists in observability sources ([key: string]: any) in:

  • js/app/packages/observability/src/index.ts:8
  • js/app/packages/observability/src/logger.ts:6
  • js/app/packages/observability/src/impl.ts:96

Replace these index signatures with unknown (and narrow where the payload is consumed).

Proposed type-safe adjustment
 interface User {
   id: string;
   email: string;
-  [key: string]: any;
+  [key: string]: unknown;
 }
📝 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
interface User {
id: string;
email: string;
[key: string]: any;
}
interface User {
id: string;
email: string;
[key: string]: unknown;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app/packages/observability/src/index.ts` around lines 5 - 9, Replace the
loose any index signature on the User interface and other observability payload
types with unknown and narrow at consumption sites: change "interface User { id:
string; email: string; [key: string]: any }" to use "[key: string]: unknown",
update analogous signatures in logger.ts and impl.ts to unknown, then update
code that reads these dynamic properties (e.g., inside the logger creation
functions and the implementation code around the consumer at impl.ts line ~96)
to type-check/cast or validate the expected shape before accessing fields (use
type predicates, safe property checks, or explicit casts) so payload handling
remains type-safe.

Source: Coding guidelines

Comment on lines +157 to +162
function getSavedDocumentMetadataSchema(): Promise<SavedDocumentMetadataSchema> {
savedDocumentMetadataSchemaPromise ??= import('./generated/zod').then(
(m) => m.saveDocumentHandlerResponse.shape.data.shape.documentMetadata
);
return savedDocumentMetadataSchemaPromise;
}

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="js/app/packages/service-clients/service-storage/client.ts"

echo "== File exists =="
ls -l "$FILE"

echo "== Declarations near getSavedDocumentMetadataSchema =="
nl -ba "$FILE" | sed -n '120,220p'

echo "== Usages around reported save callsites =="
nl -ba "$FILE" | sed -n '1420,1535p'
nl -ba "$FILE" | sed -n '1730,1860p'

echo "== All references to savedDocumentMetadataSchemaPromise =="
rg -n "savedDocumentMetadataSchemaPromise|getSavedDocumentMetadataSchema" "$FILE"

echo "== Check for try/catch around getSavedDocumentMetadataSchema callsites =="
rg -n "getSavedDocumentMetadataSchema\\(" "$FILE" -n

Repository: macro-inc/macro

Length of output: 302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="js/app/packages/service-clients/service-storage/client.ts"

echo "== Declarations near getSavedDocumentMetadataSchema =="
# show with line numbers without relying on nl
awk 'NR>=120 && NR<=220 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== All references to savedDocumentMetadataSchemaPromise =="
rg -n "savedDocumentMetadataSchemaPromise|getSavedDocumentMetadataSchema" "$FILE"

echo "== Usages around reported save callsites (areas 1400-1550 and 1700-1900) =="
awk 'NR>=1400 && NR<=1550 {printf "%d\t%s\n", NR, $0}' "$FILE"
awk 'NR>=1700 && NR<=1900 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Check for try/catch patterns near callsites =="
rg -n "try\s*\{|catch\s*\(" "$FILE"

Repository: macro-inc/macro

Length of output: 16723


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/service-clients/service-storage/client.ts"

echo "== Search pdfSave/simpleSave/simpleSaveText signatures =="
rg -n "async pdfSave|async simpleSave\\(|async simpleSaveText\\(" "$FILE"

echo "== Extract around pdfSave signature area (includes return type annotations if present) =="
awk 'NR>=1425 && NR<=1528 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Extract around simpleSaveText signature area (includes return type annotations if present) =="
awk 'NR>=1765 && NR<=1810 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Check if savedDocumentMetadataSchemaPromise is ever reset to null/undefined =="
rg -n "savedDocumentMetadataSchemaPromise\s*=\s*null|savedDocumentMetadataSchemaPromise\s*=\s*undefined|savedDocumentMetadataSchemaPromise\s*=" "$FILE"

Repository: macro-inc/macro

Length of output: 5166


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/service-clients/service-storage/client.ts"

echo "== Find pdfSave/simpleSave/simpleSaveText in StorageServiceClient type usage =="
rg -n "pdfSave\s*|simpleSave\s*\(|simpleSaveText\s*\(" "$FILE" 

echo "== Inspect around start of service client object that contains these methods =="
# find the nearest surrounding "const storageServiceClient" / "export const" / "{"
rg -n "storageServiceClient|const storage|export const|serviceClient|client\s*=" "$FILE" | head -n 20

# show a chunk earlier than pdfSave to include any return type annotations
awk 'NR>=1320 && NR<=1445 {printf "%d\t%s\n", NR, $0}' "$FILE"

# show chunk earlier than simpleSaveText to include any return type annotations
awk 'NR>=1660 && NR<=1830 {printf "%d\t%s\n", NR, $0}' "$FILE"

Repository: macro-inc/macro

Length of output: 11064


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/service-clients/service-storage/client.ts"

echo "== Around storageServiceClient declaration =="
awk 'NR>=330 && NR<=430 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Where pdfSave/simpleSave/simpleSaveText are referenced from outside (if any) =="
rg -n "storageServiceClient\.(pdfSave|simpleSave|simpleSaveText)\b" js/app/packages -S

echo "== Locate service type file for StorageServiceClient (./service) =="
DIR="$(dirname "$FILE")"
echo "DIR=$DIR"
ls -la "$DIR"

Repository: macro-inc/macro

Length of output: 4487


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/service-clients/service-storage/service.ts"

echo "== Find pdfSave/simpleSave/simpleSaveText in service.ts =="
rg -n "pdfSave|simpleSaveText|simpleSave\(" "$FILE"

echo "== Extract around the method type declarations =="
# Print a window around each match line
for line in $(rg -n "pdfSave|simpleSaveText|simpleSave\(" "$FILE" | cut -d: -f1 | sort -u); do
  start=$((line-20))
  end=$((line+60))
  echo "--- window $start-$end around line $line ---"
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"
done

echo "== Check ok/err type helpers location in this package =="
# look for definitions
rg -n "function ok\\b|function err\\b|const ok\\b|const err\\b|export const ok\\b|export const err\\b" js/app/packages/service-clients/service-storage -S

Repository: macro-inc/macro

Length of output: 6811


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILES=(
  "js/app/packages/block-pdf/signal/save.ts"
  "js/app/packages/block-md/signal/save.ts"
  "js/app/packages/block-canvas/store/canvasData.ts"
  "js/app/packages/block-code/component/CodeMirror.tsx"
)

for f in "${FILES[@]}"; do
  echo "==== $f (match windows) ===="
  rg -n "storageServiceClient\\.(pdfSave|simpleSave|simpleSaveText)\\b" "$f" || true
  # show surrounding code around first few matches
  for line in $(rg -n "storageServiceClient\\.(pdfSave|simpleSave|simpleSaveText)\\b" "$f" | cut -d: -f1 | head -n 5); do
    start=$((line-40)); end=$((line+60))
    echo "--- window $start-$end around line $line ---"
    awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$f"
  done
done

Repository: macro-inc/macro

Length of output: 11801


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/service-clients/service-storage/service.ts"

rg -n "withFetchErrors|throws:" "$FILE"

echo "== Show withFetchErrors definition (window around its match) =="
LINE=$(rg -n "withFetchErrors" "$FILE" | head -n 1 | cut -d: -f1)
START=$((LINE-60)); END=$((LINE+80))
awk -v s="$START" -v e="$END" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Show how storage client is constructed/wrapped in this file =="
rg -n "registerClient\\(|createClient\\(|createService|makeClient|service\\.client" "$FILE"

Repository: macro-inc/macro

Length of output: 5737


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/service-clients/service-storage/service.ts"

# Find the tail where it registers/exports the service
echo "== Find register/export in service.ts =="
rg -n "export const|registerClient|createService|Svc\\(" "$FILE"

echo "== Show around registerClient in service.ts (if any) =="
rg -n "registerClient\\(" "$FILE" || true
if rg -n "registerClient\\(" "$FILE" >/dev/null; then
  LINE=$(rg -n "registerClient\\(" "$FILE" | head -n 1 | cut -d: -f1)
  awk -v s=$((LINE-80)) -v e=$((LINE+80)) 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"
fi

echo "== Find assignment of implementation to service wrapper =="
rg -n "storageServiceClient|client\\.pdfSave|impl|implementation|makeClient|getClient" "$FILE" || true

echo "== Show file end for any composition/wiring =="
tail -n 80 "$FILE"

Repository: macro-inc/macro

Length of output: 3874


🏁 Script executed:

#!/bin/bash
set -euo pipefail
RGDIR="js/app/packages"
echo "== Locate withFetchErrors implementation =="
rg -n "function withFetchErrors|withFetchErrors\\s*\\(" "$RGDIR" -S

echo "== Locate core/service exports for ServiceClient/Svc =="
rg -n "export (type )?ServiceClient|class Svc|function Svc|const Svc" "$RGDIR" -S

echo "== Open likely core/service file(s) =="
fd "service.*(ts|tsx|js)" js/app/packages -a | head -n 30

Repository: macro-inc/macro

Length of output: 8655


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/core/service.ts"

# show withFetchErrors implementation and nearby helper types
awk 'NR>=260 && NR<=420 {printf "%d\t%s\n", NR, $0}' "$FILE"

# show service execution wrapper around ServiceClient call behavior (look for try/catch around handler invocation)
rg -n "try\\s*\\{|catch\\s*\\(|throw|Promise\\.resolve|invoke|fn\\(" "$FILE" | head -n 80

# show around where ServiceClient methods are implemented (search for "ServiceClient" or Proxy)
rg -n "Proxy|makeProxy|client\\b|call\\b|invoke\\b|handlers\\b|mapErr|isErr" "$FILE" | head -n 80

Repository: macro-inc/macro

Length of output: 6670


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/core/service.ts"

echo "== Svc class implementation window =="
awk 'NR>=120 && NR<=260 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Search for Proxy/call/invoke/handler in core/service.ts =="
rg -n "Proxy\\b|new Proxy\\b|invoke\\b|call\\b|handler\\b|catch\\s*\\(|try\\s*\\(|mapErr|isErr\\b|return err\\b" "$FILE"

echo "== Show around first occurrence of 'new Proxy' (if any) =="
line=$(rg -n "new Proxy" "$FILE" | head -n 1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-80)); end=$((line+120))
  awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"
else
  echo "No new Proxy found"
fi

echo "== Show around first 'catch (' occurrence =="
line=$(rg -n "catch\\s*\\(" "$FILE" | head -n 1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-80)); end=$((line+120))
  awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"
fi

Repository: macro-inc/macro

Length of output: 4920


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/core/service.ts"

echo "== Search for implementation of ServiceClient proxy/invocation =="
rg -n "new Proxy|Proxy\\s*\\(|createClient|makeClient|ServiceClient\\b|invoke\\b|handlers\\b|async \\(|fn\\(" "$FILE"

echo "== Search for any catch/try inside invocation logic =="
rg -n "catch\\s*\\(|try\\s*\\(" "$FILE"

echo "== Show later file windows around where ServiceClient types might be used (look for 'client' or 'svc' variables) =="
# show from ~420 to end
awk 'NR>=420 && NR<=620 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Tail of core/service.ts =="
tail -n 120 "$FILE"

Repository: macro-inc/macro

Length of output: 346


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/service-clients/service-storage/client.ts"

echo "== Find registerClient call in this file =="
rg -n "registerClient\\(" "$FILE"

LINE=$(rg -n "registerClient\\(" "$FILE" | head -n 1 | cut -d: -f1)
start=$((LINE-120)); end=$((LINE+60))
echo "== Show around registerClient =="
awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== Find any try/catch around pdfSave implementations =="
rg -n "try\\s*\\{|catch\\s*\\(" "$FILE" | head -n 50

Repository: macro-inc/macro

Length of output: 3679


🏁 Script executed:

#!/bin/bash
set -euo pipefail
RGDIR="js/app/packages/core"
rg -n "registerClient\\b|function registerClient|export function registerClient" "$RGDIR" -S

rg -n "createClient\\b|makeClient\\b|proxy\\b|new Proxy\\b|Proxy\\b|invoke\\b|dispatch\\b|Svc\\b.*client" "$RGDIR" -S

# Also search broader for registerClient definition in app packages
rg -n "registerClient\\b" js/app/packages -S | head -n 50

Repository: macro-inc/macro

Length of output: 6741


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="js/app/packages/core/util/mockClient.ts"
wc -l "$FILE"
sed -n '1,120p' "$FILE"

Repository: macro-inc/macro

Length of output: 518


Handle document-metadata schema load failures instead of caching a rejected promise forever

getSavedDocumentMetadataSchema() memoizes the dynamic import('./generated/zod') promise in savedDocumentMetadataSchemaPromise ??= ...; if that import ever rejects once, the rejected promise is reused for all subsequent saves.

pdfSave / simpleSave / simpleSaveText then await getSavedDocumentMetadataSchema() and immediately call .safeParse(...) with no error handling, so schema-load failures aren’t converted into a recoverable Result path and can keep breaking saves until reload.

🛠️ Suggested fix pattern
 let savedDocumentMetadataSchemaPromise: Promise<SavedDocumentMetadataSchema> | null =
   null;

 function getSavedDocumentMetadataSchema(): Promise<SavedDocumentMetadataSchema> {
-  savedDocumentMetadataSchemaPromise ??= import('./generated/zod').then(
-    (m) => m.saveDocumentHandlerResponse.shape.data.shape.documentMetadata
-  );
+  if (!savedDocumentMetadataSchemaPromise) {
+    savedDocumentMetadataSchemaPromise = import('./generated/zod')
+      .then((m) => m.saveDocumentHandlerResponse.shape.data.shape.documentMetadata)
+      .catch((error) => {
+        savedDocumentMetadataSchemaPromise = null; // allow retry
+        throw error;
+      });
+  }
   return savedDocumentMetadataSchemaPromise;
 }
-const metadata = (await getSavedDocumentMetadataSchema()).safeParse(data.documentMetadata);
+let schema: SavedDocumentMetadataSchema;
+try {
+  schema = await getSavedDocumentMetadataSchema();
+} catch {
+  return err([
+    {
+      code: 'INVALID_RESPONSE',
+      message: 'Failed to load document metadata schema',
+    },
+  ]);
+}
+const metadata = schema.safeParse(data.documentMetadata);

Apply to pdfSave (1482-1484), simpleSave (1511-1513), and simpleSaveText (1792-1794).

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @js/app/packages/service-clients/service-storage/client.ts around lines 157 -
162, getSavedDocumentMetadataSchema currently memoizes a failing dynamic import
forever; change it to catch errors from import('./generated/zod'), reset
savedDocumentMetadataSchemaPromise to undefined (so a transient failure doesn't
get cached) and rethrow the error so callers can handle it; then update pdfSave,
simpleSave, and simpleSaveText to wrap the await
getSavedDocumentMetadataSchema() in a try/catch and convert import/schema-load
errors into the recoverable Result failure path before calling .safeParse (or
bail early), ensuring you reference savedDocumentMetadataSchemaPromise,
getSavedDocumentMetadataSchema, and the three save functions when making the
changes.


</details>

<!-- fingerprinting:phantom:poseidon:hawk -->

<!-- cr-comment:v1:194080a3b455d483b8e5ed98 -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines 113 to 142
handleAccept(editor: LexicalEditor) {
editor.update(() => {
const diffInsertNode = this.getChildren().find((child) =>
$isDiffInsertNode(child)
);
if (!diffInsertNode) return;

const insertMarkdown = diffInsertNode.getMarkdown();
if (insertMarkdown === undefined) return;

this.clear();

$convertFromMarkdownString(insertMarkdown, ALL_TRANSFORMERS, this, false);

const lastChild = this.getLastChild();
$unwrapNode(this);
if (lastChild) {
lastChild.selectEnd();
}
// The markdown conversion stack loads lazily: this node class is
// registered at app boot, and importing @lexical/markdown + the
// transformer list eagerly would pull them into the initial bundle.
// handleAccept is a user action (accepting a diff), so deferring the
// update until the one-time chunk load resolves is fine.
void Promise.all([
import('@lexical/markdown'),
import('../transformers'),
]).then(([{ $convertFromMarkdownString }, { ALL_TRANSFORMERS }]) => {
editor.update(() => {
const diffInsertNode = this.getChildren().find((child) =>
$isDiffInsertNode(child)
);
if (!diffInsertNode) return;

const insertMarkdown = diffInsertNode.getMarkdown();
if (insertMarkdown === undefined) return;

this.clear();

$convertFromMarkdownString(insertMarkdown, ALL_TRANSFORMERS, this, false);

const lastChild = this.getLastChild();
$unwrapNode(this);
if (lastChild) {
lastChild.selectEnd();
}
});
});

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file
ls -la js/lexical-core/nodes/DiffNode.ts

# Show handleAccept implementation with line numbers
sed -n '1,220p' js/lexical-core/nodes/DiffNode.ts | nl -ba | sed -n '80,220p'

# Show full file if it's short, else show remainder around handleAccept end
wc -l js/lexical-core/nodes/DiffNode.ts
sed -n '220,420p' js/lexical-core/nodes/DiffNode.ts | nl -ba

# Search for handleAccept usage
rg -n "handleAccept\\(" -S js/lexical-core/nodes/DiffNode.ts js/lexical-core -S || true

# Search for similar lazy import + editor.update + catch patterns
rg -n "Promise\\.all\\(\\[\\s*import\\(" js -S | head -n 50 || true
rg -n "\\.catch\\(" js/lexical-core/nodes js/lexical-core -S | head -n 50 || true
rg -n "lazy-load markdown" -S js | head -n 50 || true

Repository: macro-inc/macro

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file
ls -la js/lexical-core/nodes/DiffNode.ts

# Show handleAccept implementation with line numbers
sed -n '1,260p' js/lexical-core/nodes/DiffNode.ts | nl -ba | sed -n '80,260p'

# Show file length and tail around if needed
wc -l js/lexical-core/nodes/DiffNode.ts
tail -n 80 js/lexical-core/nodes/DiffNode.ts | nl -ba

# Search for handleAccept usage
rg -n "handleAccept\\(" -S js/lexical-core | head -n 50 || true

# Search for similar lazy import patterns
rg -n "Promise\\.all\\(\\[\\s*import\\(" js/lexical-core -S | head -n 50 || true
rg -n "\\.catch\\(" js/lexical-core -S | head -n 50 || true

# Find other dynamic import usage without catch
rg -n "void Promise\\.all\\(" js/lexical-core -S || true
rg -n "import\\([^\\)]*\\)\\)\\.then\\(" js/lexical-core -S | head -n 50 || true

Repository: macro-inc/macro

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la js/lexical-core/nodes/DiffNode.ts
sed -n '1,260p' js/lexical-core/nodes/DiffNode.ts | nl -ba | sed -n '90,200p'
wc -l js/lexical-core/nodes/DiffNode.ts
sed -n '200,320p' js/lexical-core/nodes/DiffNode.ts | nl -ba

rg -n "handleAccept\\(" -S js/lexical-core | head -n 50 || true

rg -n "Promise\\.all\\(\\[\\s*import\\(" js/lexical-core -S | head -n 50 || true
rg -n "\\.catch\\(" js/lexical-core -S | head -n 50 || true

rg -n "import\\([^\\)]*\\)\\)\\.then\\(" js/lexical-core -S | head -n 50 || true

Repository: macro-inc/macro

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la js/lexical-core/nodes/DiffNode.ts
sed -n '1,260p' js/lexical-core/nodes/DiffNode.ts | nl -ba | sed -n '90,200p'
wc -l js/lexical-core/nodes/DiffNode.ts
sed -n '200,320p' js/lexical-core/nodes/DiffNode.ts | nl -ba

rg -n "handleAccept\\(" -S js/lexical-core | head -n 50 || true

rg -n "Promise\\.all\\(\\[\\s*import\\(" js/lexical-core -S | head -n 50 || true
rg -n "\\.catch\\(" js/lexical-core -S | head -n 50 || true

rg -n "import\\([^\\)]*\\)\\)\\.then\\(" js/lexical-core -S | head -n 50 || true

Repository: macro-inc/macro

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la js/lexical-core/nodes/DiffNode.ts
sed -n '1,260p' js/lexical-core/nodes/DiffNode.ts | nl -ba | sed -n '90,200p'
wc -l js/lexical-core/nodes/DiffNode.ts
sed -n '200,320p' js/lexical-core/nodes/DiffNode.ts | nl -ba

rg -n "handleAccept\\(" -S js/lexical-core | head -n 50 || true

rg -n "Promise\\.all\\(\\[\\s*import\\(" js/lexical-core -S | head -n 50 || true
rg -n "\\.catch\\(" js/lexical-core -S | head -n 50 || true

rg -n "import\\([^\\)]*\\)\\)\\.then\\(" js/lexical-core -S | head -n 50 || true

Repository: macro-inc/macro

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '90,170p' js/lexical-core/nodes/DiffNode.ts | nl -ba
wc -l js/lexical-core/nodes/DiffNode.ts
rg -n "handleAccept\\(" js/lexical-core/nodes/DiffNode.ts js/lexical-core -S | head -n 20 || true
rg -n "\\.catch\\(" js/lexical-core/nodes/DiffNode.ts js/lexical-core -S | head -n 20 || true

Repository: macro-inc/macro

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "FILE:"
ls -la js/lexical-core/nodes/DiffNode.ts
echo "SNIPPET:"
nl -ba js/lexical-core/nodes/DiffNode.ts | sed -n '90,170p'
echo "MATCHES:"
rg -n "handleAccept\\(" -S js/lexical-core/nodes/DiffNode.ts

Repository: macro-inc/macro

Length of output: 218


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find DiffNode.ts
fd "DiffNode.ts" -a . | head -n 20

Repository: macro-inc/macro

Length of output: 112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="js/lexical-core/nodes/DiffNode.ts"

echo "handleAccept matches:"
rg -n "handleAccept\\(" "$FILE" || true

echo
echo "Promise/all + dynamic imports in file:"
rg -n "Promise\\.all\\(|import\\(" "$FILE" || true

echo
echo "catch clauses in file:"
rg -n "\\.catch\\(" "$FILE" || true

echo
echo "Printing lines ~90-170 (no nl needed):"
sed -n '90,170p' "$FILE"

echo
echo "Printing lines ~170-260 (no nl needed):"
sed -n '170,260p' "$FILE"

echo
echo "Line-numbered excerpt of handleAccept block (Python):"
python3 - <<'PY'
import re
path="js/lexical-core/nodes/DiffNode.ts"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()

for i,l in enumerate(lines, start=1):
    if "handleAccept" in l:
        start=i-5
        end=i+120
        for j in range(max(1,start), min(len(lines), end)+1):
            print(f"{j:4d}: {lines[j-1].rstrip()}")
        break
else:
    print("No handleAccept found")
PY

Repository: macro-inc/macro

Length of output: 8414


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "handleAccept\\(" -S js | head -n 50 || true
rg -n "handleReject\\(" -S js | head -n 50 || true

# Find DiffNode.accept call sites more generally
rg -n "\\.handleAccept\\b" -S js | head -n 50 || true

# Search for patterns: dynamic imports + catch in lexical-core
rg -n "import\\([^\\)]*\\)\\s*\\)\\s*\\.catch\\(" -S js/lexical-core | head -n 50 || true
rg -n "\\.catch\\(" -S js/lexical-core | head -n 50 || true

# Search for use of $getNodeByKey in this repo
rg -n "\\$getNodeByKey\\(" -S js | head -n 50 || true

Repository: macro-inc/macro

Length of output: 650


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Call sites for handleAccept:"
rg -n "\\.handleAccept\\b" -S js | head -n 50 || true

echo
echo "Call sites for DiffNode.handleAccept:"
rg -n "handleAccept\\(" -S js | head -n 50 || true

echo
echo "Dynamic import + catch patterns (lexical-core):"
rg -n "import\\([^\\)]*\\)\\s*\\)\\s*\\.catch\\(" -S js/lexical-core | head -n 50 || true

echo
echo "Catch clauses (lexical-core, limited):"
rg -n "\\.catch\\(" -S js/lexical-core | head -n 50 || true

echo
echo "Usages of $getNodeByKey:"
rg -n "\\$getNodeByKey\\(" -S js | head -n 50 || true

Repository: macro-inc/macro

Length of output: 608


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Call sites for handleAccept:"
rg -n "\\.handleAccept\\b" -S js | head -n 50 || true

echo
echo "Call sites for handleAccept( (any):"
rg -n "handleAccept\\(" -S js | head -n 50 || true

echo
echo "Dynamic import + catch patterns (lexical-core):"
rg -n "import\\([^\\)]*\\)\\s*\\)\\s*\\.catch\\(" -S js/lexical-core | head -n 50 || true

echo
echo "Catch clauses (lexical-core, limited):"
rg -n "\\.catch\\(" -S js/lexical-core | head -n 50 || true

echo
echo "Usages of $getNodeByKey:"
rg -n "\\$getNodeByKey\\(" -S js | head -n 50 || true

Repository: macro-inc/macro

Length of output: 606


Make DiffNode.handleAccept async-safe (captured node + missing catch)

  • The lazy Promise.all(...).then(...) still operates on the captured this node inside editor.update(...); re-resolve the node inside editor.update (e.g., via key) before calling getChildren(), clear(), $convertFromMarkdownString(..., this, ...), and $unwrapNode(this).
  • Add a .catch(...) to the lazy import chain to avoid unhandled promise rejections if the dynamic imports fail.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/lexical-core/nodes/DiffNode.ts` around lines 113 - 142, Capture the node
key before the lazy imports and re-resolve the node inside editor.update to
avoid operating on a possibly-stale/captured `this` (e.g., in
DiffNode.handleAccept do const nodeKey = this.getKey(); then inside
editor.update call const node = $getNodeByKey(nodeKey) and bail if null before
calling node.getChildren(), node.clear(), $convertFromMarkdownString(..., node,
...), and $unwrapNode(node)); also append a .catch(...) to the Promise.all(...)
import chain to handle and log import errors so you don't leave unhandled
promise rejections.

@jbecke jbecke force-pushed the claude/stoic-cannon-twd8bk branch from 816e45b to f91a575 Compare June 12, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants