Skip to content

perf: weekly react best practices audit fixes - #3518

Merged
smakosh merged 10 commits into
mainfrom
claude/upbeat-johnson-77n54b
Aug 10, 2026
Merged

smakosh merged 10 commits into
mainfrom
claude/upbeat-johnson-77n54b

Conversation

@smakosh

@smakosh smakosh commented Aug 10, 2026 •

Copy link
Copy Markdown
Member

Weekly React/Next.js best-practices audit of the four Next.js apps (apps/ui, apps/code, apps/playground, apps/docs) against Vercel's react-best-practices guide. All fixes are surgical — no routing changes, no behavior rewrites, and nothing touching export const dynamic. All four apps have the React Compiler enabled, so the audit deliberately skipped manual-memoization rules and focused on waterfalls, bundle size, server-side fetching, and render purity.

Fixes

apps/ui

  • Per-Request Deduplication with React.cache() (rule 3.9) — src/lib/server-api.ts, the four preferences RSCs under settings/preferences/_components/, dashboard/[orgId]/layout.tsx, dashboard/[orgId]/[projectId]/layout.tsx, dashboard/[orgId]/[projectId]/page.tsx. The preferences page rendered four sibling RSCs that each independently fetched /projects/{id} (4 identical round-trips per page load), and /orgs + /orgs/{id}/projects were re-fetched across nested layouts. Added cache()-wrapped getProject / getOrganizations / getOrgProjects helpers and pointed all call sites at them.
  • Defer Non-Critical Third-Party Libraries (rules 2.3/2.4) — src/components/providers.tsx. The support chat widget (AI SDK + streamdown + shiki code plugin, initialized at module scope) was statically imported into the root providers and shipped on every route, including the landing and SEO pages, despite starting collapsed. Now loaded with next/dynamic + ssr: false.
  • Promise.all() for Independent Operations (rule 1.5) — dashboard/[orgId]/org/guardrails/guardrails-client.tsx. The guardrails config and rules GETs were awaited sequentially; they are independent, so they now resolve in parallel.
  • Use Passive Event Listeners (rule 4.2) — src/components/landing/navbar.tsx. The site-wide navbar scroll listener was non-passive; other listeners in the repo already pass { passive: true }.
  • Don't mutate shared module state in render (rule 7.14) — src/app/blog/page.tsx, src/app/changelog/page.tsx. Both pages called .sort() directly on the module-scope allBlogs / allChangelogs arrays shared across requests. Filtering first (which copies) and sorting the copy keeps the shared array untouched.
  • Use Lazy State Initialization (rule 5.12) — src/components/enterprise/calendly-inline.tsx. The window.Calendly probe ran on every render; now a lazy initializer.

apps/playground

  • Dependency-Based Parallelization (rule 1.3) — src/app/playground-shell.tsx. The main entry point awaited /orgs, /chat-plans/status, and the orgId-scoped projects list serially (up to 4 sequential round-trips). They are mutually independent, so they now resolve in one Promise.all. The /playground/chat-org fetch intentionally stays sequenced after the chat-plan redirect check — it provisions the chat org on demand and must not run for redirected users.
  • Promise.all() for Independent Operations (rule 1.5) — src/app/group/page.tsx. Brought /group in line with its siblings (/image, /video, /audio…), which already collapse models/providers/orgs/projects into one parallel fetch.
  • No side effects during render — src/hooks/useUser.ts. posthog.identify() ran in the render body of a hook with ~24 consumers, including chat components that re-render on every streamed token. Moved into a useEffect keyed on the user identity.
  • Conditional Module Loading (rule 2.2) — src/components/ai-elements/code-block.tsx. createHighlighter from shiki was a static top-level import even though highlighting is already async and cached; the engine + grammar registry now load lazily on first code block render.
  • Hoist static clients out of the request path (rule 3.5) — src/app/posthog.ts. A new posthog-node client (with its own queue and flush timer, never shut down) was constructed on every server request across ~11 routes. Now one memoized client per process.

apps/code

  • Avoid Barrel File Imports (rule 2.1) — next.config.ts. @llmgateway/shared/components is a 23-line export * barrel fronting ~13k lines (model directory, log cards, provider icons) with no sideEffects field, and client components import tiny symbols from it. Added it to experimental.optimizePackageImports (the playground app already uses this flag).
  • Dynamic Imports for Heavy Components (rule 2.4) — src/lib/utils/markdown.tsx: the prism-based SyntaxHighlightedPre was statically wired into the /compare/* markdown options although none of the six comparison documents contains a fenced code block; now next/dynamic. src/components/profile/ProfileView.tsx: recharts (ProfileTokensChart) was in the first-load JS of the shareable /profiles/[username] page while rendering below the fold; now next/dynamic with a size-matched placeholder.
  • Stable QueryClient identity — src/components/providers.tsx (and the same pattern in apps/ui/src/components/providers.tsx, apps/playground/src/lib/providers.tsx, apps/playground/src/components/providers.tsx). QueryClient was created with useMemo, whose cache React may discard — silently dropping the entire query cache mid-session. Switched to useState with a lazy initializer, per the React Query docs.

apps/docs

  • Dynamic Imports for Heavy Components (rule 2.4) — components/ai/search.tsx. The Ask-AI panel statically imported the whole markdown pipeline (remark + remark-gfm + remark-rehype + hast-util-to-jsx-runtime + shiki DynamicCodeBlock), putting it in the initial bundle of every docs page although it only runs once a chat message renders. Now next/dynamic.
  • Don't pull browser-only libraries into the RSC graph — app/(home)/[[...slug]]/page.tsx + components/feedback.tsx. The docs page imported browser posthog-js into a server component and called posthog.capture() inside a "use server" action — a no-op against the uninitialized browser SDK, so docs feedback was never recorded. The capture now happens client-side in the feedback component via the app's existing usePostHog() provider.
  • Put Interaction Logic in Event Handlers (rule 5.8) — components/feedback.tsx. localStorage persistence ran in an effect keyed on [previous, url], which on navigation momentarily wrote the previous page's feedback under the new page's key (and flashed "Thank you for your feedback!" on pages the user never rated). Persistence now happens directly in the submit / "Submit Again" handlers.
  • Bound module-level render cache — components/markdown.tsx. The streaming markdown renderer cached one parsed tree per intermediate token snapshot in an unbounded module-scope Map for the lifetime of the tab. Now capped with oldest-entry eviction.
  • Deterministic render output — app/(home)/[[...slug]]/page.tsx. When the GitHub last-edit lookup fails, lastUpdate fell back to new Date() (the request time), claiming every page was edited "just now". Now omitted instead.

Considered and deliberately skipped

  • Anything involving export const dynamic — intentional and required for runtime env loading; excluded from the audit per constraints.
  • docs: "uncached GitHub API call per request" — not real: fumadocs' getGithubLastEdit already fetches with cache: "force-cache", so it hits Next's data cache after the first request per path.
  • docs: ThemedImage downloading both light+dark screenshots — the suggested <picture media="(prefers-color-scheme)"> fix would break the class-based manual theme toggle; needs a different approach.
  • ui: audit-logs and routing-config pages hand-roll useEffect data fetching — the right fix is a TanStack Query (useInfiniteQuery/useQuery) rewrite of each page's data layer; too invasive for this pass.
  • ui: sidebar open-state read from localStorage post-hydration (CLS on every dashboard page) — the fix is a cookie read in the server layout passed as defaultOpen; needs prop threading through the dashboard layouts, deferred.
  • playground: MCP servers connected in serial await loops in api/chat, and getUser() serialized ahead of req.json() across API routes — real wins, but they change error-ordering semantics on the hottest route; better as a dedicated change.
  • code: CodingModelsShowcase ships the full @llmgateway/models catalogue to the client on / and /coding-models — the highest-impact bundle finding in that app, but it requires redesigning the component's server/client interface (deriving the trimmed list server-side); deferred. Note the optimizePackageImports change above does not cover this case.
  • code: hero number-ticker SSRs 0, unconditional 5s dashboard poll, theme-toggle mounted gate — all behavior-adjacent; deferred rather than risk user-visible changes in a perf pass.

Verification

  • pnpm build — all 17 tasks pass.
  • pnpm format (via lint-staged eslint --fix + prettier on every changed file).
  • No unit specs touch the changed code paths; none were modified.
  • No visual changes are intended by any of these fixes, so no screenshots — the dashboard-affecting changes (fetch dedup, parallelization, provider swaps) render identical UI.

Note: this session pushes to its designated branch, so the head branch is claude/upbeat-johnson-77n54b rather than the chore/react-bp-audit-2026-08-10 naming convention; future audits should locate this PR by title.

🤖 Generated with Claude Code

https://claude.ai/code/session_012AXwZZQXjD9sephujzkJQ9


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added a redesigned “Ask AI” control with keyboard shortcuts, Escape-to-close support, and lazy loading.
    • Improved feedback submission handling and page context capture.
  • Performance

    • Reduced initial loading for charts, code highlighting, support tools, and analytics.
    • Improved dashboard and playground data loading through concurrent requests, caching, and duplicate-request prevention.
  • Bug Fixes

    • Draft blog and changelog entries are now excluded from published views.
    • Improved guardrails error handling and project data reliability.
    • Preserved analytics identity across delayed initialization.

Weekly React/Next.js best-practices audit across the four Next.js
apps (ui, code, playground, docs), per Vercel's react-best-practices
guide: parallelize independent server fetches, dedupe per-request
fetches with React.cache, defer heavy client bundles with dynamic
imports, and fix render-purity/state-initialization issues.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AXwZZQXjD9sephujzkJQ9
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR adds typed request-scoped API helpers, parallelizes independent requests, defers selected client modules, stabilizes QueryClient and PostHog instances, and updates documentation feedback, AI search, markdown caching, and UI behavior.

Changes

Server API and dashboard data loading

Layer / File(s) Summary
Typed server API helpers
apps/ui/src/lib/server-api.ts
Adds deduplicated typed fetchers for projects, organizations, and organization projects.
Dashboard helper integration
apps/ui/src/app/dashboard/...
Replaces inline server requests with dedicated helpers while preserving validation, redirects, and rendering behavior.

Concurrent application data loading

Layer / File(s) Summary
Parallelize playground initialization
apps/playground/src/app/group/page.tsx, apps/playground/src/app/playground-shell.tsx
Starts independent organization, project, model, provider, and plan requests concurrently and reuses fetched results.
Parallelize guardrail requests
apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx
Loads configuration and custom rules concurrently and processes each response independently.

Client runtime and bundle loading

Layer / File(s) Summary
Defer code application modules
apps/code/src/components/profile/ProfileView.tsx, apps/code/src/lib/utils/markdown.tsx, apps/code/src/components/providers.tsx
Dynamically loads the profile chart and syntax highlighter, and stores the QueryClient with lazy state initialization.
Stabilize playground clients
apps/playground/src/...
Defers Shiki loading, reuses PostHog, retains QueryClient state, and queues identity updates until PostHog loads.
Defer UI client modules
apps/ui/src/components/providers.tsx, apps/ui/src/app/posthog.ts
Dynamically loads chat support and reuses the UI PostHog client.

Documentation rendering and feedback

Layer / File(s) Summary
Controlled AskAI search flow
apps/docs/app/(home)/layout.tsx, apps/docs/components/ai/*
Adds a controlled, dynamically loaded search panel with open-state and keyboard handling.
Feedback and renderer state
apps/docs/app/(home)/[[...slug]]/page.tsx, apps/docs/components/feedback.tsx, apps/docs/components/markdown.tsx
Updates feedback analytics and persistence, adds the lastUpdate fallback, and pins mounted markdown cache entries.
Bound analytics initialization
apps/docs/lib/providers.tsx
Bounds idle scheduling and reduces the timer fallback for PostHog initialization.

UI behavior adjustments

Layer / File(s) Summary
Content and browser behavior
apps/ui/src/app/blog/page.tsx, apps/ui/src/app/changelog/page.tsx, apps/ui/src/components/enterprise/calendly-inline.tsx, apps/ui/src/components/landing/navbar.tsx
Filters drafts before sorting, initializes Calendly state lazily, and registers the navbar scroll listener as passive.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's React and Next.js performance and best-practice audit fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/upbeat-johnson-77n54b

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

❤️ Share

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

@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)
apps/docs/components/markdown.tsx (1)

132-143: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use LRU ordering for the bounded cache.

Cache hits at Line 132 do not refresh insertion order. During streaming, new snapshots are added while existing messages are rendered again. After 200 entries, frequently rendered older messages can be evicted and processed again on later renders. Move cache hits to the end before eviction, or use an LRU cache, so eviction removes cold snapshots.

Proposed cache-hit update
 let result = cache.get(text);
 
-if (!result) {
+if (result) {
+	cache.delete(text);
+	cache.set(text, result);
+} else {
 	result = processor.process(text);
 	if (cache.size >= cacheLimit) {
 		const oldest = cache.keys().next().value;
🤖 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 `@apps/docs/components/markdown.tsx` around lines 132 - 143, Update the cache
lookup around result and processor.process so cache hits refresh the entry’s
insertion order before eviction. Remove and reinsert an existing text key, while
preserving the current bounded-cache behavior so the oldest key is evicted when
the limit is reached and cold snapshots are removed first.
🤖 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 `@apps/code/src/components/profile/ProfileView.tsx`:
- Around line 30-38: Remove the dynamic import in ProfileView.tsx at lines 30-38
and restore a top-level ProfileTokensChart import. Also remove the dynamic
import in markdown.tsx at lines 1-7 and restore a top-level SyntaxHighlightedPre
import; no direct change is needed elsewhere. Ensure neither file contains
next/dynamic or import() calls.

In `@apps/docs/components/ai/search.tsx`:
- Line 14: Replace the dynamic markdown import used by the next/dynamic
configuration around the Markdown component with a top-level import from
../markdown, then update the component usage to reference that static import.
Remove the now-unnecessary dynamic import so the file complies with the
repository’s TypeScript import guidelines.

In `@apps/docs/components/feedback.tsx`:
- Around line 82-83: Update the onRateAction prop contract in the feedback
component to accept only url, and call onRateAction with url alone instead of
feedback. Verify all onRateAction references in feedback.tsx and the page module
use this same single-argument contract.

In `@apps/playground/src/components/ai-elements/code-block.tsx`:
- Around line 150-157: Replace the lazy import in the code block highlighter
initialization with a top-level static Shiki import, while preserving the
existing createHighlighter configuration and highlighterPromise behavior;
alternatively, obtain an explicit exception for this file before merging.

In `@apps/ui/src/app/blog/page.tsx`:
- Line 17: Update the draft filter callback in the blog page to replace the
explicit any type with the generated content-collection entry type or an
inferred entry type. Preserve the existing !entry?.draft filtering behavior and
avoid introducing any additional any usage.

In `@apps/ui/src/app/dashboard/`[orgId]/org/guardrails/guardrails-client.tsx:
- Around line 156-163: Add before-and-after screenshots for the affected
dashboard guardrails screen in both light and dark themes where applicable, and
attach them to the pull request. No code changes are required around the
Promise.all requests.
- Around line 156-163: Replace the Promise.all coordination in the guardrails
data-loading flow with Promise.allSettled or equivalent independent request
handling so each fulfilled response is applied separately. Preserve a successful
configuration response when the rules request fails, and add a test covering
that configuration-success/rules-rejection case.

In `@apps/ui/src/components/providers.tsx`:
- Around line 20-25: Update the PR to include before-and-after screenshots for
the affected dashboard screens impacted by the global ChatSupport change,
covering both light and dark themes where applicable. Follow the repository’s
established screenshot conventions and ensure the screenshots are included with
the change.
- Line 5: Remove the next/dynamic import and the import() expression from the
provider setup, and restore a top-level ChatSupport import in the relevant
component flow. Update the ChatSupport usage to reference that static import
while preserving its existing rendering behavior.

In `@apps/ui/src/lib/server-api.ts`:
- Around line 116-125: Update getOrgProjects to pass the explicit
fetchServerData generic for the organization-project response envelope, using
the existing Project type and the { projects: Project[] } shape. Preserve the
current request path and parameters so callers receive the typed result without
unchecked assertions.

---

Nitpick comments:
In `@apps/docs/components/markdown.tsx`:
- Around line 132-143: Update the cache lookup around result and
processor.process so cache hits refresh the entry’s insertion order before
eviction. Remove and reinsert an existing text key, while preserving the current
bounded-cache behavior so the oldest key is evicted when the limit is reached
and cold snapshots are removed first.
🪄 Autofix

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 07ebfdc0-d592-4ebd-85d8-a243d78b4f01

📥 Commits

Reviewing files that changed from the base of the PR and between a8afdc7 and cb18139.

📒 Files selected for processing (29)
  • apps/code/next.config.ts
  • apps/code/src/components/profile/ProfileView.tsx
  • apps/code/src/components/providers.tsx
  • apps/code/src/lib/utils/markdown.tsx
  • apps/docs/app/(home)/[[...slug]]/page.tsx
  • apps/docs/components/ai/search.tsx
  • apps/docs/components/feedback.tsx
  • apps/docs/components/markdown.tsx
  • apps/playground/src/app/group/page.tsx
  • apps/playground/src/app/playground-shell.tsx
  • apps/playground/src/app/posthog.ts
  • apps/playground/src/components/ai-elements/code-block.tsx
  • apps/playground/src/components/providers.tsx
  • apps/playground/src/hooks/useUser.ts
  • apps/playground/src/lib/providers.tsx
  • apps/ui/src/app/blog/page.tsx
  • apps/ui/src/app/changelog/page.tsx
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/layout.tsx
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/page.tsx
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/settings/preferences/_components/archive-project.tsx
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/settings/preferences/_components/caching-settings-rsc.tsx
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/settings/preferences/_components/project-mode-settings-rsc.tsx
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/settings/preferences/_components/project-name-settings-rsc.tsx
  • apps/ui/src/app/dashboard/[orgId]/layout.tsx
  • apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx
  • apps/ui/src/components/enterprise/calendly-inline.tsx
  • apps/ui/src/components/landing/navbar.tsx
  • apps/ui/src/components/providers.tsx
  • apps/ui/src/lib/server-api.ts

Comment on lines +30 to +38
// The tokens chart pulls in recharts and renders below the fold, so keep it
// out of the profile page's initial bundle.
const ProfileTokensChart = dynamic(
() =>
import("@/components/profile/ProfileTokensChart").then(
(mod) => mod.ProfileTokensChart,
),
{ ssr: false, loading: () => <div className="h-52 w-full" /> },
);

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve the shared dynamic-import policy violation.

Both changed files use dynamic imports despite the repository rule that forbids them.

  • apps/code/src/components/profile/ProfileView.tsx#L30-L38: restore the top-level ProfileTokensChart import, or approve a documented exception.
  • apps/code/src/lib/utils/markdown.tsx#L1-L7: restore the top-level SyntaxHighlightedPre import, or approve a documented exception.

Verify the changed files with:

#!/bin/bash
set -euo pipefail

if rg -n 'next/dynamic|import\s*\(' \
  apps/code/src/components/profile/ProfileView.tsx \
  apps/code/src/lib/utils/markdown.tsx
then
	echo "Dynamic imports remain in files covered by the prohibition." >&2
	exit 1
fi

As per coding guidelines: use top-level import statements and never use dynamic imports.

📍 Affects 2 files
  • apps/code/src/components/profile/ProfileView.tsx#L30-L38 (this comment)
  • apps/code/src/lib/utils/markdown.tsx#L1-L7
🤖 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 `@apps/code/src/components/profile/ProfileView.tsx` around lines 30 - 38,
Remove the dynamic import in ProfileView.tsx at lines 30-38 and restore a
top-level ProfileTokensChart import. Also remove the dynamic import in
markdown.tsx at lines 1-7 and restore a top-level SyntaxHighlightedPre import;
no direct change is needed elsewhere. Ensure neither file contains next/dynamic
or import() calls.

Comment thread apps/docs/components/ai/search.tsx Outdated
Comment thread apps/docs/components/feedback.tsx Outdated
Comment on lines +150 to +157
// Import shiki lazily so its engine and grammar registry stay out of the
// chunk until a code block actually renders.
const highlighterPromise = import("shiki").then(({ createHighlighter }) =>
createHighlighter({
langs: [language],
themes: ["github-light", "github-dark"],
}),
);

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching code-block.tsx:"
fd -a 'code-block\.tsx$' . || true

file="$(fd 'code-block\.tsx$' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  echo
  echo "File: $file"
  wc -l "$file"
  echo
  echo "Top-level import statements and dynamic imports:"
  sed -n '1,220p' "$file" | nl -ba | sed -n '1,220p'
fi

echo
echo "Search for dynamic import pattern in ts/tsx files (excluding node_modules/dist):"
rg -n --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' '\bimport\s*\([^)]*\)' . || true

Repository: theopenco/llmgateway

Length of output: 1929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching code-block.tsx:"
fd -a 'code-block\.tsx$' . || true

file="$(fd 'code-block\.tsx$' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  echo
  echo "File: $file"
  wc -l "$file"
  echo
  echo "Top-level import statements and dynamic imports:"
  sed -n '1,220p' "$file" | nl -ba | sed -n '1,220p'
fi

echo
echo "Search for dynamic import pattern in ts/tsx files (excluding node_modules/dist):"
rg -n --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' '\bimport\s*\([^)]*\)' . || true

Repository: theopenco/llmgateway

Length of output: 1929


Resolve the dynamic-import guideline violation.

import("shiki") violates the **/*.{ts,tsx} guideline against dynamic imports. Use a top-level Shiki import instead, or get an explicit exception for this path before merge.

🤖 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 `@apps/playground/src/components/ai-elements/code-block.tsx` around lines 150 -
157, Replace the lazy import in the code block highlighter initialization with a
top-level static Shiki import, while preserving the existing createHighlighter
configuration and highlighterPromise behavior; alternatively, obtain an explicit
exception for this file before merging.

Source: Coding guidelines

Comment thread apps/ui/src/app/blog/page.tsx Outdated
Comment thread apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx Outdated

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import dynamic from "next/dynamic";

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the dynamic import or obtain an approved exception.

This TypeScript file uses next/dynamic at Line 5 and an import() expression at Line 23. Restore a top-level ChatSupport import unless maintainers approve an exception for this performance change.

Policy-compliant alternative
-import dynamic from "next/dynamic";
+import { ChatSupport } from "`@/components/chat-support`";
...
-const ChatSupport = dynamic(
-	() => import("`@/components/chat-support`").then((mod) => mod.ChatSupport),
-	{ ssr: false },
-);
#!/usr/bin/env bash
set -euo pipefail

if rg -n '"next/dynamic"|import\s*\(' apps/ui/src/components/providers.tsx; then
	echo "Dynamic import remains."
	exit 1
fi

As per coding guidelines, **/*.{ts,tsx} files must use top-level import statements and never use require or dynamic imports.

Also applies to: 20-25

🤖 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 `@apps/ui/src/components/providers.tsx` at line 5, Remove the next/dynamic
import and the import() expression from the provider setup, and restore a
top-level ChatSupport import in the relevant component flow. Update the
ChatSupport usage to reference that static import while preserving its existing
rendering behavior.

Comment on lines +20 to +25
// The support widget starts collapsed but statically pulls in the AI SDK and
// streamdown/shiki, so defer it out of the initial bundle of every route.
const ChatSupport = dynamic(
() => import("@/components/chat-support").then((mod) => mod.ChatSupport),
{ ssr: false },
);

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required dashboard screenshots.

This change modifies the global ChatSupport UI in the apps/ui provider. Add before/after screenshots for affected dashboard screens, including both themes where applicable.

As per coding guidelines, apps/{ui,code}/**/*.{ts,tsx} dashboard UI changes require before/after screenshots, including both themes where applicable. The PR objectives state that no screenshots were added.

🤖 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 `@apps/ui/src/components/providers.tsx` around lines 20 - 25, Update the PR to
include before-and-after screenshots for the affected dashboard screens impacted
by the global ChatSupport change, covering both light and dark themes where
applicable. Follow the repository’s established screenshot conventions and
ensure the screenshots are included with the change.

Source: Coding guidelines

Comment thread apps/ui/src/lib/server-api.ts Outdated

smakosh commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Addressed the CodeRabbit review in 479877d:

Fixed

  • apps/docs/components/feedback.tsx: onRateAction now takes only url, so the free-form feedback message no longer crosses the server-action boundary.
  • apps/ui/.../guardrails-client.tsx: switched to Promise.allSettled so a fulfilled config response is still applied when the rules request fails (and vice versa).
  • apps/ui/src/app/blog/page.tsx: replaced the any callbacks with the generated Blog type.
  • apps/ui/src/lib/server-api.ts: getOrgProjects now passes the explicit { projects: Project[] } generic.
  • apps/docs/components/markdown.tsx: cache hits refresh insertion order so eviction removes cold entries first (LRU).

Declined, with reasoning

  • Dynamic-import policy comments (4): the "never use dynamic imports" guideline targets require()/dynamic imports where a static import would do — not next/dynamic code-splitting, which is established practice in this repo (apps/ui/src/app/page.tsx below-the-fold sections, route-flow-editor's @xyflow/react, ProfilePassport's three.js, canvas-page-client's jspdf/html-to-image, dashboard/(main)/page.tsx and BillingClient in apps/code). Deferring these bundles is the core purpose of this perf PR, so the dynamic imports stay.
  • Screenshot requests (2): the screenshot rule covers visual changes to dashboard screens. These are load-behavior changes with intentionally zero visual delta — the guardrails page and chat widget render identically — so before/after captures would be indistinguishable.
  • Guardrails test request: there is no existing test harness for this client component; adding one is out of scope for this pass.

Generated by Claude Code

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

Caution

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

⚠️ Outside diff range comments (1)
apps/docs/components/feedback.tsx (1)

82-89: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wait for the rate action before resetting the submission pending state.

The startTransition callback returns immediately because onRateAction(url) is not awaited. This can set isPending back to false before the call completes. Await onRateAction inside the transition, guard state updates after await with startTransition, and handle failures so the form can be resubmitted.

🤖 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 `@apps/docs/components/feedback.tsx` around lines 82 - 89, Update the
submission transition around onRateAction to await the rate action before
completing, then wrap post-await updates such as localStorage persistence and
replacePrevious in startTransition. Add failure handling that restores the
form’s resubmittable state when onRateAction rejects, while preserving the
existing success result construction.
🤖 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 `@apps/ui/src/app/dashboard/`[orgId]/org/guardrails/guardrails-client.tsx:
- Around line 154-155: Update the fetch startup logic in fetchConfig to call
setError(null) alongside setIsLoading(true), clearing any stale load error
before the current request begins.
- Around line 153-157: Replace the useEffect-driven fetchConfig flow with
separate TanStack Query hooks for the guardrails configuration and rules
requests. Use each query’s data and loading state independently, and pass
canManageGuardrails as the enabled option to both queries so users without
organization management access skip both requests.
- Around line 166-185: Update the config and rules result handling in the
guardrails loading flow to inspect each fulfilled response’s error field before
applying DEFAULT_CONFIG or updating custom rules. Treat any HTTP error as a load
failure by setting the existing error state, while preserving default config
only for successful responses with no config data.
- Around line 157-187: Update fetchConfig to create a request-bound abort
controller or load token and associate it with the organizationId used for that
Promise.allSettled request. Before applying config, rules, errors, or
setIsLoading(false), verify the request is still current; ignore all completions
from earlier organization loads so they cannot overwrite active state or end the
newer loading cycle.

---

Outside diff comments:
In `@apps/docs/components/feedback.tsx`:
- Around line 82-89: Update the submission transition around onRateAction to
await the rate action before completing, then wrap post-await updates such as
localStorage persistence and replacePrevious in startTransition. Add failure
handling that restores the form’s resubmittable state when onRateAction rejects,
while preserving the existing success result construction.
🪄 Autofix

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

Review profile: CHILL

Plan: Pro Plus

Run ID: d62cbd39-2939-4810-aa7a-f5f5b32a4da2

📥 Commits

Reviewing files that changed from the base of the PR and between cb18139 and 479877d.

📒 Files selected for processing (5)
  • apps/docs/components/feedback.tsx
  • apps/docs/components/markdown.tsx
  • apps/ui/src/app/blog/page.tsx
  • apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx
  • apps/ui/src/lib/server-api.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/ui/src/app/blog/page.tsx
  • apps/ui/src/lib/server-api.ts
  • apps/docs/components/markdown.tsx

Comment on lines 153 to +157
const fetchConfig = useCallback(async () => {
try {
setIsLoading(true);
const response = await fetchClient.GET(
"/guardrails/config/{organizationId}",
{
params: { path: { organizationId } },
},
);
setIsLoading(true);
// allSettled so a fulfilled response is still applied when the other
// request fails.
const [configResult, rulesResult] = await Promise.allSettled([

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 '\b(useQuery|useQueries)\b' apps/ui/src
rg -n -C 6 'useEffect|fetchConfig|useFetchClient' \
  'apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx'

Repository: theopenco/llmgateway

Length of output: 50376


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Guardrails file relevant section:"
sed -n '120,215p' 'apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx' | cat -n

echo
echo "React Query dependency versions:"
for f in package.json apps/ui/package.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    node -e "const p=require('./$f'); console.log(JSON.stringify({dependencies:p.dependencies,devDependencies:p.devDependencies||{}},null,2))" | rg -C 2 'react-query|`@tanstack`|package-lock|pnpm|yarn' || true
  fi
done

echo
echo "Typical nearby query usage with organization params:"
rg -n -C 4 'api\.useQuery\(' 'apps/ui/src/components/sso/sso-client.tsx' 'apps/ui/src/components/custom-models/org-models-client.tsx' 'apps/ui/src/lib/dashboard-state.ts' | sed -n '1,180p'

Repository: theopenco/llmgateway

Length of output: 9235


Move the guardrails loads into TanStack Query.

This component still calls fetchConfig from a useEffect, but generated API calls here should use the query hooks. Keep the configuration and rules loads separate, and drive canManageGuardrails from the query enabled option so disabled organization users skip both requests.

🤖 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 `@apps/ui/src/app/dashboard/`[orgId]/org/guardrails/guardrails-client.tsx
around lines 153 - 157, Replace the useEffect-driven fetchConfig flow with
separate TanStack Query hooks for the guardrails configuration and rules
requests. Use each query’s data and loading state independently, and pass
canManageGuardrails as the enabled option to both queries so users without
organization management access skip both requests.

Source: Coding guidelines

Comment on lines +157 to +187
const [configResult, rulesResult] = await Promise.allSettled([
fetchClient.GET("/guardrails/config/{organizationId}", {
params: { path: { organizationId } },
}),
fetchClient.GET("/guardrails/rules/{organizationId}", {
params: { path: { organizationId } },
}),
]);

if (response.data) {
setConfig(response.data as unknown as GuardrailConfig);
if (configResult.status === "fulfilled") {
if (configResult.value.data) {
setConfig(configResult.value.data as unknown as GuardrailConfig);
} else {
// No config exists yet, use defaults
setConfig(DEFAULT_CONFIG);
}
}

const rulesResponse = await fetchClient.GET(
"/guardrails/rules/{organizationId}",
{
params: { path: { organizationId } },
},
if (rulesResult.status === "fulfilled" && rulesResult.value.data) {
setCustomRules(
(rulesResult.value.data as { rules: CustomRule[] }).rules || [],
);
}

if (rulesResponse.data) {
setCustomRules(
(rulesResponse.data as { rules: CustomRule[] }).rules || [],
);
}
} catch {
if (
configResult.status === "rejected" ||
rulesResult.status === "rejected"
) {
setError("Failed to load guardrails configuration");
} finally {
setIsLoading(false);
}
setIsLoading(false);

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 'GuardrailsClient|useParams\(\)|org/guardrails|router\.(push|replace)' \
  apps/ui/src/app/dashboard

Repository: theopenco/llmgateway

Length of output: 27196


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== guardrails-client outline =="
ast-grep outline apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx || true

echo "== guardrails-client relevant lines =="
sed -n '1,260p' apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx | cat -n

echo "== search for guardrails route usage =="
rg -n -C 4 'org/guardrails|guardrails|GuardrailsClient' apps/ui/src

Repository: theopenco/llmgateway

Length of output: 50379


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== dashboard sidebar route definitions =="
sed -n '180,230p' apps/ui/src/components/dashboard/dashboard-sidebar.tsx | cat -n

echo "== navigation hook relevant files =="
rg -n -C 3 "buildOrgUrl|organizationId|orgId|selectedOrganization" apps/ui/src/hooks apps/ui/src/lib | head -n 200

echo "== next router use in org settings/overview-ish files =="
sed -n '1,120p' apps/ui/src/app/dashboard/[orgId]/org/security-events/security-events-client.tsx | cat -n
sed -n '1,120p' apps/ui/src/app/dashboard/[orgId]/org/audit-logs/audit-logs-client.tsx | cat -n

Repository: theopenco/llmgateway

Length of output: 21223


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== locate dashboard context/organization switchers =="
rg -n -C 6 "setSelectedOrganization|selectedOrganization|useOrganization|switch|change|organizationId" apps/ui/src/hooks apps/ui/src/lib apps/ui/src/app/dashboard --glob '*.ts' --glob '*.tsx' | head -n 260

echo "== focused files likely organization switcher =="
fd -e ts -e tsx . apps/ui/src/app/apps/ui/src/hooks apps/ui/src/lib | rg 'organization|switch|context|context|navigation|settings' || true

echo "== outline organization switcher candidates =="
for f in $(rg -l "setSelectedOrganization|selectedOrganization|switch" apps/ui/src/hooks apps/ui/src/lib apps/ui/src/app/dashboard --glob '*.ts' --glob '*.tsx' | head -n 20); do
  echo "--- $f"
  ast-grep outline "$f" || true
done

Repository: theopenco/llmgateway

Length of output: 27451


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== dashboard context/state full files =="
sed -n '1,220p' apps/ui/src/lib/dashboard-context.tsx | cat -n
sed -n '1,220p' apps/ui/src/lib/dashboard-state.ts | cat -n

echo "== organization select/usages =="
rg -n -C 5 "handleOrganizationSelect|organizationSelected|useOrganization|setSelectedOrganization|selectedOrganization" apps/ui/src --glob '*.ts' --glob '*.tsx' | head -n 240

echo "== all router calls in dashboard under orgId path =="
rg -n -C 4 "router\.(push|replace|back|forward)|useRouter|router\." apps/ui/src/app/dashboard --glob '*.tsx' --glob '*.ts' | head -n 260

Repository: theopenco/llmgateway

Length of output: 45021


Use request-bound guards for guardrails fetch completions.

fetchConfig reads organizationId from useParams() inside the callback, but the pending Promise.allSettled() was created with the value captured at call time. Route changes can invoke a new fetchConfig before the first load finishes; store an abort controller/token per load and ignore responses belonging to an earlier organization, otherwise older guardrails config/rules can overwrite the active org state and setIsLoading(false) can close the newer loading state.

🤖 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 `@apps/ui/src/app/dashboard/`[orgId]/org/guardrails/guardrails-client.tsx
around lines 157 - 187, Update fetchConfig to create a request-bound abort
controller or load token and associate it with the organizationId used for that
Promise.allSettled request. Before applying config, rules, errors, or
setIsLoading(false), verify the request is still current; ignore all completions
from earlier organization loads so they cannot overwrite active state or end the
newer loading cycle.

Comment thread apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx Outdated

smakosh commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Second review round: took the stale-error reset (setError(null) at fetch start) in 2a71d2c.

Declining the remaining guardrails/feedback suggestions — each targets behavior that predates this PR and was intentionally preserved by this perf pass: the original sequential code had the same treatment of non-2xx config responses (defaults applied), the same org-switch race, no request-bound abort tokens, and the same non-awaited onRateAction transition. Changing those semantics belongs in a dedicated change, along with the TanStack Query migration for this page already noted in the PR description.


Generated by Claude Code

Correctness:
- server-api: share the in-flight promise instead of React.cache(), which
  memoized fetchServerData's null-on-error and turned one transient blip
  into a request-wide "unauthorized" render
- guardrails: treat openapi-fetch's { error } as a failure — an HTTP error
  silently installed DEFAULT_CONFIG and the next save overwrote the org's
  real configuration
- docs markdown: pin cache entries a mounted Renderer still needs so
  eviction can't flash an answered message back to the fallback
- docs page: restore the lastUpdate fallback; the unauthenticated GitHub
  lookup fails for most pages and dropped "Last updated" entirely
- docs feedback: commit the UI before persisting, and make localStorage
  best-effort so a storage failure can't swallow the confirmation
- docs providers: bound the idle deferral so ratings captured early are
  not dropped before posthog.init()
- playground: queue posthog.identify until init completes instead of
  firing once and leaving the session anonymous
- code-block: drop a rejected shiki import from the cache so one chunk
  failure doesn't permanently disable highlighting
- code: drop optimizePackageImports on the "use client" shared barrel, and
  build the posthog options inline so a discarded memo can't cancel init

Cleanup:
- ui: keep the support trigger in the server HTML (no ssr: false)
- ui: reuse one posthog-node client per process
- ui: route the remaining org/project fetches through the shared helpers
- docs: split Ask AI so the AI SDK and markdown pipeline load on open

Claude-Session: https://claude.ai/code/session_01BjEeYVaMqJDgkLgz1pohGM

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

Caution

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

⚠️ Outside diff range comments (1)
apps/playground/src/hooks/useUser.ts (1)

42-52: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Clear queued PostHog identity on logout.

identifyUser() saves a pending identity before PostHog loads. useUser.ts only queues it when data?.user is present and does not clear it when the session becomes unauthenticated, while posthog.reset() does not clear flushPendingIdentity()’s shared pending state. Add clearPendingIdentity() in apps/playground/src/lib/posthog-identity.ts, call it when data?.user is absent, and ensure the logout paths clear it as well.

🤖 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 `@apps/playground/src/hooks/useUser.ts` around lines 42 - 52, Update the
useUser effect around identifyUser so it calls clearPendingIdentity when
data?.user is absent, while preserving the existing identification flow for
authenticated users. Add the clearPendingIdentity helper in posthog-identity.ts
to remove the shared pending identity, and invoke it in every logout path
alongside posthog.reset().
🤖 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 `@apps/docs/app/`(home)/[[...slug]]/page.tsx:
- Around line 92-96: Update the lastUpdate assignment in the page data flow to
preserve undefined when getGithubLastEdit returns no time, rather than falling
back to new Date(). Keep the existing Date conversion when time is available so
the “Last updated” label reflects only GitHub edit data.

In `@apps/docs/components/ai/ask-ai.tsx`:
- Around line 13-15: Replace the dynamic AISearchPanel definition using
next/dynamic and import("./search") with a top-level static import of
AISearchPanel from "./search"; preserve the component’s existing usage and
remove the unnecessary loading configuration.

In `@apps/docs/components/markdown.tsx`:
- Around line 150-173: Update Renderer’s cache eviction flow to exclude the
current text key from evictColdEntries, preventing the newly created entry from
being removed before useEffect pins it. After the cleanup returned by the
pinning useEffect decrements or removes a pinned entry, run eviction again so
previously protected entries can be reclaimed.

---

Outside diff comments:
In `@apps/playground/src/hooks/useUser.ts`:
- Around line 42-52: Update the useUser effect around identifyUser so it calls
clearPendingIdentity when data?.user is absent, while preserving the existing
identification flow for authenticated users. Add the clearPendingIdentity helper
in posthog-identity.ts to remove the shared pending identity, and invoke it in
every logout path alongside posthog.reset().
🪄 Autofix

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

Review profile: CHILL

Plan: Pro Plus

Run ID: a4d5776c-6d60-4281-9e3a-d6329b4af94b

📥 Commits

Reviewing files that changed from the base of the PR and between 2a71d2c and 2373c5a.

📒 Files selected for processing (18)
  • apps/code/src/components/providers.tsx
  • apps/docs/app/(home)/[[...slug]]/page.tsx
  • apps/docs/app/(home)/layout.tsx
  • apps/docs/components/ai/ask-ai.tsx
  • apps/docs/components/ai/search.tsx
  • apps/docs/components/feedback.tsx
  • apps/docs/components/markdown.tsx
  • apps/docs/lib/providers.tsx
  • apps/playground/src/components/ai-elements/code-block.tsx
  • apps/playground/src/components/providers.tsx
  • apps/playground/src/hooks/useUser.ts
  • apps/playground/src/lib/posthog-identity.ts
  • apps/ui/src/app/dashboard/[orgId]/[projectId]/settings/sdk/page.tsx
  • apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx
  • apps/ui/src/app/dashboard/[orgId]/page.tsx
  • apps/ui/src/app/posthog.ts
  • apps/ui/src/components/providers.tsx
  • apps/ui/src/lib/server-api.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/playground/src/components/ai-elements/code-block.tsx
  • apps/docs/components/feedback.tsx
  • apps/ui/src/app/dashboard/[orgId]/org/guardrails/guardrails-client.tsx

Comment thread apps/docs/app/(home)/[[...slug]]/page.tsx Outdated
Comment on lines +13 to +15
const AISearchPanel = dynamic(() => import("./search"), {
loading: () => null,
});

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the disallowed dynamic import.

Line 13 invokes import("./search") through next/dynamic. The TypeScript policy prohibits dynamic imports. Use a top-level AISearchPanel import. If deferred loading is required, obtain a scoped policy exception before merge.

Proposed policy-compliant change
-import dynamic from "next/dynamic";
+import AISearchPanel from "./search";
...
-const AISearchPanel = dynamic(() => import("./search"), {
-	loading: () => null,
-});
🤖 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 `@apps/docs/components/ai/ask-ai.tsx` around lines 13 - 15, Replace the dynamic
AISearchPanel definition using next/dynamic and import("./search") with a
top-level static import of AISearchPanel from "./search"; preserve the
component’s existing usage and remove the unnecessary loading configuration.

Source: Coding guidelines

Comment thread apps/docs/components/markdown.tsx Outdated

smakosh commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Latest review round, on top of 2373c5a: took two findings in d2b9d9b — the markdown cache now shields the current entry from eviction until the pinning effect runs (and re-evicts when a pin releases), and useUser clears a queued PostHog identity when the session is unauthenticated so a pre-init logout can't attribute the anonymous session to the previous user.

Skipped two: the lastUpdate build-date fallback was deliberately reinstated in 2373c5a with a comment explaining why (unauthenticated GitHub lookups are rate-limited below the pages-per-build count, and fumadocs omits the row entirely on undefined), and the ask-ai.tsx dynamic import is the deferral mechanism itself — same rationale as the earlier dynamic-import threads.


Generated by Claude Code

Stamping the build date claimed a page had been edited when it had not,
and it moved on every deploy. fumadocs omits the line when undefined, so
"Last updated" now only ever reflects real GitHub edit data.

Claude-Session: https://claude.ai/code/session_01BjEeYVaMqJDgkLgz1pohGM
@smakosh
smakosh merged commit 31ecab5 into main Aug 10, 2026
11 checks passed
@smakosh
smakosh deleted the claude/upbeat-johnson-77n54b branch August 10, 2026 20:20
smakosh added a commit that referenced this pull request Aug 24, 2026
Weekly React/Next.js best-practices audit of the four Next.js apps
(`apps/ui`, `apps/code`, `apps/playground`, `apps/docs`) against
[Vercel's react-best-practices
guide](https://github.com/vercel-labs/agent-skills/blob/main/skills/react-best-practices/AGENTS.md).
The three previous audits (#3391, #3518, #3647) are all merged, so this
week covers code merged since then plus remaining findings, including
two items those audits explicitly deferred that turned out to have
surgical fixes. No routing changes, no user-visible behavior changes,
nothing touching `export const dynamic`.

## Fixes

### apps/ui

- **Prevent Hydration Mismatch Without Flickering (rule 6.5)** —
`lib/components/sidebar.tsx`, `app/dashboard/[orgId]/layout.tsx`. The
sidebar open state was read from localStorage in a post-mount effect
(behind a `mounted` flag), so every dashboard load rendered the default
state and then snapped to the persisted one — a layout shift on every
navigation for anyone with a collapsed sidebar. Deferred by the last two
audits as needing prop threading; it doesn't: the provider's only mount
point is the org server layout, which already reads cookies. The state
now persists in a `sidebar_state` cookie (the pattern the playground's
sidebar already uses) and the layout passes it as `defaultOpen`, so the
first paint is correct and both effects plus the `mounted` state are
gone. One-time migration cost: a previously saved localStorage value is
ignored, so a collapsed sidebar renders expanded once until the user
toggles again.
- **Per-Request Deduplication with React.cache() (rule 3.9)** —
`app/dashboard/page.tsx`. The dashboard entry page fetched `/user/me`,
`/orgs`, and `/orgs/{id}/projects` through the raw fetcher while
`dashboard/layout.tsx` fetches `/user/me` through the deduped
`getUserMe()` in the same render pass — a duplicate round-trip on every
`/dashboard` hit. All three now go through the existing `cache()`-backed
helpers, which also lets the redirect target's org layout share them.
- **Dynamic Imports for Heavy Components (rule 2.4)** —
`lib/utils/markdown.tsx`. The prism-based `SyntaxHighlightedPre`
(prism-react-renderer plus its full `themes` barrel) was statically
wired into the markdown options used by the blog, guides, changelog,
legal, use-cases, and migration routes, shipping the highlighter on
articles with zero code blocks. Now `next/dynamic`, mirroring the
identical fix `apps/code` got in #3518.
- **Cache Repeated Function Calls / Hoist Constructors (rules
7.4/7.10)** — `components/api-keys/api-keys-list.tsx`,
`api-key-limit-fields.tsx`, `api-key-ttl-fields.tsx`,
`components/master-keys/master-keys-list.tsx`,
`lib/components/number-ticker.tsx`. The API-keys table constructed 3–4
`Intl.DateTimeFormat` instances per row per render (creation date,
tooltip, expiry, period reset), the master-keys list the same, and
`NumberTicker` constructed one per spring animation frame (×3 tickers on
pages using it). All formatters are hoisted to module scope (or, for the
ticker, created once per subscription).

### apps/code

- **Minimize Serialization at RSC Boundaries / bundle size (rules 3.6,
2.x)** — new `lib/coding-models.ts`,
`components/CodingModelsShowcase.tsx`, `app/page.tsx`,
`app/coding-models/page.tsx`. The showcase — rendered on the landing
page and `/coding-models` — imported the entire `@llmgateway/models`
catalogue (~800K of source: every model with all provider mappings) into
a client component, then derived a few fields per coding model in the
browser. Flagged as the app's biggest bundle finding in #3518 and
deferred twice as "needs an interface redesign"; the redesign is small.
All derivation (DevPass coding gate, recommended/premium sets,
cheapest-provider pricing) now runs server-side and only a trimmed card
array (8 scalar fields per model) crosses the RSC boundary. The client
keeps just the tab state and copy button; rendered UI is unchanged.
- **NumberTicker per-frame `Intl.NumberFormat`** —
`components/ui/number-ticker.tsx`, same fix as the ui copy above.

### apps/playground

- **Promise.all() for Independent Operations (rule 1.5)** —
`app/realtime/page.tsx`. Realtime was the only media page still awaiting
`/orgs/{id}/projects` serially after models/providers/orgs; the siblings
(`image`, `video`, `audio`, `canvas`) all start it eagerly in the same
`Promise.all` when the URL carries an `orgId`. Realtime now does the
same, with the eager result used only when that org actually ends up
selected.
- **Defer Non-Critical Third-Party Libraries (rule 2.3)** —
`lib/stripe.ts`, `components/credits/top-up-credits-dialog.tsx`.
`useStripe()` loaded Stripe.js (~200KB, phones home on load) in an
unconditional mount effect, and the top-up dialog mounts closed on the
chat, image, video, and audio pages — so every playground visit fetched
Stripe. Ported the `enabled` gate `apps/code` got in #3647; the dialog
passes its `open` state, so Stripe.js loads only when the dialog is
actually opened.
- **Bounded module cache (rule 4.4-adjacent)** —
`components/ai-elements/code-block.tsx`. The shiki `tokensCache`
retained the full `ThemedToken[][]` of every code block ever rendered
(including one entry per streaming snapshot) for the tab's lifetime; its
sibling caches were already bounded/cleaned. Now LRU-capped at 200
entries.

### apps/docs

- **Defer Await Until Needed (rule 1.2)** — `app/api/chat/route.ts`. The
Ask-AI FlexSearch index was built eagerly at module scope: importing the
route read and indexed the processed text of all ~131 MDX pages even
when `DOCS_AI_SUPPORT_CHAT_API_KEY` is unset and the handler always 503s
— and the module-scope promise had no rejection handler until the first
tool call, so an indexing failure at boot would crash the standalone
server as an unhandled rejection. The index is now built lazily and
memoized on first search, with failed builds dropped so a transient
error doesn't stick.
- **Unnecessary `"use client"` on static components** — new
`components/tracked-link.tsx`; `components/enterprise-cta.tsx`,
`ai-tooling-cards.tsx`, `self-host-cards.tsx`. Three purely
presentational card components were client components solely to fire a
PostHog click capture — `EnterpriseCTA` renders in the TOC footer of
every docs page, so its markup shipped as client JS everywhere. A thin
`TrackedLink` client wrapper now owns the capture, and the cards (icons,
copy, layout) are server components.
- **Bounded module cache** — `components/ai/page-actions.tsx`. The "Copy
Markdown" cache stored each copied page's entire raw markdown in an
unbounded module `Map`; now capped with oldest-entry eviction, matching
the treatment `markdown.tsx` got in #3518.
- **Cache Storage API Calls (rule 7.5)** — `components/ai/search.tsx`.
The Ask-AI input wrote its draft to localStorage synchronously on every
keystroke; now debounced (300ms), flushed/cleared on submit.
- **Remove always-missing `useMemo`** — `components/ai/search.tsx`. The
context value was memoized on `[chat, open, setOpen]`, but `useChat`
returns a fresh object every render, so the memo allocated every time
and never hit; removed (React Compiler covers the rest).

## Considered and deliberately skipped

- **Anything involving `export const dynamic`** — intentional (runtime
env loading); excluded per repo policy.
- **docs: `APIPage` in `mdx-components.tsx`** pulls the fumadocs OpenAPI
playground into the client manifest of every docs page — the largest
remaining docs bundle item, but the app has a single catch-all page
route serving both prose and API reference, so splitting it needs a
route restructure. Deferred.
- **docs: static `posthog-js` import in the root provider** — the init
is already idle-deferred; deferring the import itself requires reworking
how `PostHogProvider` receives its client instance. Deferred as
behavior-sensitive.
- **ui: `posthog.identify()` inside `getUser()`** runs on every
dashboard layout render (server-side twin of the client issue fixed in
#3518/#3647) — relocating identification to the auth path is an
analytics-behavior decision, not a perf-only diff.
- **ui: security-events page hand-rolls `useEffect` fetching** (same
class as the already-deferred audit-logs/routing-config pages) — the
right fix is a `useInfiniteQuery` rewrite; too invasive for this pass.
- **ui: millisecond-precision `new Date()` in the agents-view query
key** defeats its `staleTime` — real, but truncating the window boundary
changes the queried range semantics slightly; left for a deliberate
change.
- **playground: chat route's project retrieval serialized ahead of MCP
connects** — parallelizing changes error-ordering on the hottest route;
same reasoning as #3518's deferral of that route's auth/body ordering.
- **playground: `@streamdown/mermaid` statically registered for every
assistant message** — potentially the largest chat-chunk item, but needs
bundle analysis to confirm the plugin doesn't lazy-load internally
before acting.
- **Manual memoization nits** — all four apps run the React Compiler;
only issues the compiler cannot fix (effects, module caches, per-frame
constructors, RSC boundaries) were touched.

## Verification

- `pnpm build`: 15 of 17 workspaces green, including docs, code, and
playground. `ui#build` compiles and type-checks, but static export fails
on `/compare/litellm/opengraph-image` with `SELF_SIGNED_CERT_IN_CHAIN` —
the same pre-existing build-environment artifact documented and
reproduced on clean main in #3647 (TLS-intercepting proxy breaking a
build-time `next/og` fetch), unrelated to this diff.
- `pnpm exec tsc --noEmit` in `apps/ui` passes.
- `pnpm format` clean.
- No visual changes intended: the sidebar renders in its persisted state
without the previous post-hydration snap, and the showcase/dialog/card
changes render identical UI — so no before/after screenshots.

Note: this session pushes to its designated branch, so the head branch
is `claude/upbeat-johnson-mc6akz` rather than the
`chore/react-bp-audit-2026-08-24` naming convention (same situation as
#3518); future audits should locate this PR by title.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01GQLYmwthWTnfTLViZsfYKM

---
_Generated by [Claude
Code](https://claude.ai/code/session_01GQLYmwthWTnfTLViZsfYKM)_

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Updated coding-model showcases with refreshed model details, pricing,
context limits, and recommendation badges.
  - Sidebar preferences now persist across sessions.
- Realtime views load projects for the selected workspace more reliably.
- Added consistent click tracking for documentation and promotional
links.

- **Bug Fixes**
  - Improved documentation search reliability and retry behavior.
  - Preserved AI search drafts more reliably while typing.
  - Deferred Stripe loading until the credit top-up dialog opens.

- **Performance**
- Improved code highlighting, markdown rendering, number formatting, and
dashboard data loading.
- Added bounded caching to keep documentation and code previews
responsive.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
smakosh added a commit that referenced this pull request Sep 21, 2026
Weekly React/Next.js best-practices audit of the four Next.js apps
(`apps/ui`, `apps/code`, `apps/playground`, `apps/docs`) against
[Vercel's react-best-practices
guide](https://github.com/vercel-labs/agent-skills/blob/main/skills/react-best-practices/AGENTS.md).
All prior audit rounds are merged on main, so this round covers code
merged since #4058 plus one long-standing bundle problem on the
marketing surface that turned out to be the dominant finding. No routing
or user-visible behavior changes; nothing touches `export const
dynamic`.

## Fixes

### Bundle size

- **Avoid Barrel File Imports (rule 2.1) — `packages/shared` was
un-tree-shakeable, shipping the model catalogue on every ui route.** The
root `@llmgateway/shared` barrel re-exports from
`./components/index.js`, whose graph reaches the models-directory
components and through them the full `@llmgateway/models` catalogue.
Because the package declared no `sideEffects`, bundlers had to keep
every re-export, so any client component importing the barrel for one
symbol (the root `providers.tsx` for `TimeZoneProvider`, the navbar, the
FAQ…) dragged the whole component library plus the catalogue into its
route. Declaring `"sideEffects": false` (the package has no
side-effectful modules: no bare imports, no CSS, no global mutations)
lets the bundler tree-shake the barrels. Measured on the built ui
client-reference manifests, per-route referenced client JS
(uncompressed):
- `/` 2076 KB → 977 KB, `/blog` 2002 KB → 891 KB, `/compare/open-router`
1996 KB → 888 KB — catalogue bytes on all of them 976 KB → **0 KB**
- `/models` keeps only the 137 KB of catalogue chunks it actually uses
(was 976 KB), and the dashboard project page now references 0 catalogue
bytes

This is a one-line change in `packages/shared` (allowed as strictly
required by the fix) and benefits every app that bundles the package.
- **Client bundling of the model catalogue via the footer (rules
2.1/2.4) — `apps/ui`.** The marketing `Footer` was `"use client"` and
imported `listedProviders` from `providers-catalog`, which *uses* the
whole catalogue to count active models per provider — a use tree-shaking
cannot remove, so with the fix above alone the catalogue would have
returned to all ~46 footer-bearing marketing routes. `Footer` is now a
server component (it reads `getConfig()` directly; `Newsletter` stays a
client island), and the one client consumer — the `AllModels` wrapper —
takes `footer` as a slot filled by its server pages (standard RSC
composition). The numbers above are measured with both fixes in place.
- **Defer Non-Critical Third-Party Libraries (rule 2.3) — compare-page
heroes, `apps/ui`.** `HeroCompare` (all seven `/compare/*` pages)
shipped framer-motion in the initial bundle solely for a one-shot
blur/slide hero entrance. It now uses the same `animate-hero-enter` CSS
utilities the main landing hero was already migrated to (visually
equivalent keyframes, plus the reduced-motion handling the JS variant
lacked) and becomes a server component. No other component in the
compare graph imports motion, so the library leaves those pages' first
load entirely.
- **Unnecessary client components — comparison tables, `apps/ui`.** The
seven static feature-comparison tables (`comparison*.tsx`, ~270 lines of
constant JSX each) were `"use client"` with zero hooks or handlers, so
every `/compare/*` page shipped and hydrated the whole table. The
directives are removed; the tables render as server components and only
the existing `AuthLink` leaves hydrate. All importers are server pages.
- **Conditional Module Loading (rules 2.2/2.3) — `apps/playground` zip
download.** `image-download.ts` statically imported `fflate`, putting
the zip library in the Image Studio's initial bundle although it is only
needed in the click-triggered "download all" path. It now loads via
`import()` in parallel with the image bytes, matching the repo's
existing on-demand `html-to-image`/`jspdf` pattern.

### Hydration correctness

- **Prevent Hydration Mismatch (rule 6.5) — `apps/ui` feature-page
demos.** `generateMockActivityData()` built the demo dataset for the
SSR'd errors-monitoring and performance-monitoring demos with unseeded
crypto randomness on every render, so the statically generated HTML and
the hydrating client always disagreed on every stat — a guaranteed
mismatch, and React 19 re-renders the whole subtree. The generator now
uses a seeded PRNG (mulberry32), so server and client produce identical
data; the demos are also stable across re-renders now (random-in-render
also violates the purity the React Compiler assumes).

### Interaction logic

- **Put Interaction Logic in Event Handlers (rule 5.8) —
`apps/playground` Image Studio.** The Flex service-tier toggle persisted
its cookie through a `useEffect` watching the state (including a
spurious write on every mount). The cookie write moved into the change
handler; the effect is gone.

## Considered and deliberately skipped

- **Anything involving `export const dynamic`** — intentional
runtime-env loading; excluded per repo policy (no finding touched one
this round).
- **Lounge points query serialized behind `/user/me`**
(`useLoungePoints`, `enabled: !!user` — rules 1.5/4.3): a real
one-round-trip waterfall for signed-in members, but the session cookie
is httpOnly, so the clean fix threads a server-derived signed-in hint
through a new context — auth-signal plumbing, not a surgical perf diff.
Left for a dedicated change.
- **Playground API routes awaiting `getUser()` before parsing/validating
the body** (rule 1.4): parallelizing saves single-digit milliseconds of
body-parse time, and auth-before-parse has a mild unauthenticated-DoS
rationale; not worth the churn.
- **Stale-selection resets via effects on model switch** (image
controls/page, realtime voice — rule 5.1): pre-existing, commented as
deliberate, one extra render on a rare interaction.
- **The new Image Studio code is otherwise clean** — refcounted
off-render preview decoding (`useGalleryImage`), parallel server
fetches, React-Compiler-covered render paths. Likewise the new provider
OG card routes (static lookups hoisted, `Promise.all`'d data), the
apps/code formatting unification (module-level formatters,
`next/dynamic` charts), and apps/docs (only content changed since the
last audit; the AI search panel is already lazily imported).

## Verification

- `turbo run build --filter=ui`: `✓ Compiled successfully`, and `pnpm
exec tsc --noEmit` in `apps/ui` passes. The build's static-export stage
fails in this sandbox on `/compare/litellm/opengraph-image`
(`SELF_SIGNED_CERT_IN_CHAIN` — the TLS-intercepting proxy breaks that
route's outbound fetch). This is environmental and pre-existing: the
identical failure occurs on this sandbox before any of these changes,
and was documented in the previous audit rounds.
- Full `pnpm build` for the remaining workspaces (shared, models,
playground, code, docs, admin, api, gateway) passes.
- Bundle numbers above measured by scanning `.next/static/chunks` for
catalogue markers and summing the chunks referenced by each route's
client-reference manifest, before vs. after.
- `pnpm format` clean; lint-staged (eslint + prettier) passed on every
commit; `image-download.spec.ts` (5 tests) passes.
- No visual changes intended: the compare hero plays the same entrance
via CSS, the comparison tables render identical markup server-side, the
footer renders identical markup, and the feature demos show the same
style of mock data (fixed values instead of random ones). Per repo
policy screenshots are only for dashboard UI changes; none of these
screens changed appearance.

Note: this session pushes to its designated branch
(`claude/upbeat-johnson-3b76et`) rather than the
`chore/react-bp-audit-2026-09-21` naming convention — same situation as
previous audit rounds (#3518/#3784/#3865/#3966); future audits should
locate this PR by title.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01CepaNxrPkU6gcEXT23M7WL

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CepaNxrPkU6gcEXT23M7WL)_

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added the site footer to model listings and category pages, including
text, vision, image, video, tools, embeddings, and web-search pages.
  - Service-tier preferences now persist immediately when changed.

- **Improvements**
- Updated comparison-page animations for smoother, more efficient
rendering while preserving the existing presentation.
- Image downloads continue to support ZIP creation with improved loading
behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants