Skip to content

Conversation

@saddlepaddle
Copy link
Collaborator

@saddlepaddle saddlepaddle commented Oct 20, 2025

Description

Tried to remove a lot of excessive renders from the chat. Basically, when /any/ of the messages were changing all of the content was rerendering before, which is especially bad for the code blocks / etc. that are pricey. This is almost enough to be fully stable, but there's still a few dropped frames here and there while the code edit tool streams.

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • Performance Improvements

    • Reduced UI re-renders across chat and code displays (code blocks render only when expanded; streaming updates are debounced).
    • Improved rendering performance for tool input/output and message components, resulting in snappier interactions.
  • Bug Fixes

    • More reliable streaming state display across chat, tools, and code views.
    • Fixed inconsistencies in applied/tool-call state and message rendering during streaming.

Important

Optimize chat panel performance by reducing re-renders and improving state management using memo and observer patterns.

  • Performance Improvements:
    • Use memo and observer to optimize rendering in AssistantMessage, MessageContent, ToolCallDisplay, ToolCallSimple, UserMessage, and CollapsibleCodeBlock.
    • Debounce code updates in CodeBlockComponent to reduce re-renders during streaming.
  • Bug Fixes:
    • Improve state management for chat messages and tool displays to enhance responsiveness.
  • Misc:
    • Update bun-version to 1.2.23 in ci.yml and package.json.
    • Remove @onlook/admin dependency from bun.lock and update @types/node version.

This description was created by Ellipsis for ef82d8c. You can customize this summary. It will automatically update as commits are pushed.

@supabase
Copy link

supabase bot commented Oct 20, 2025

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


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

@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

Warning

Rate limit exceeded

@saddlepaddle has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 6 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between f9c45b9 and 3930079.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml (3 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Refactors multiple chat and UI components to export memoized and/or MobX-observer-wrapped versions; introduces isStreaming/isStream and applied props; adds debounced rendering and theme-aware syntax highlighting for code blocks; optimizes conditional rendering in CollapsibleCodeBlock.

Changes

Cohort / File(s) Summary
Chat message components (memo/observer wrappers)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx, apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx, apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
Introduced internal component constants and re-exported as memo(observer(...)) or observer(...); preserved rendering logic but changed public exports to wrapped components and threaded additional props (e.g., messageId, isStream).
Tool call displays / simple tools
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx, apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx
ToolCallDisplay refactored to internal component and exported as observer(...); adds applied: boolean prop. ToolCallSimple refactored to internal component and exported as memo(...); now passes isStreaming={loading} to ToolInput/ToolOutput.
Collapsible code block
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx
Refactored to CollapsibleCodeBlockComponent and exported as memo(observer(...)); precomputes branch, only renders code and copy UI when open, and forwards isStreaming to CodeBlock.
CodeBlock (UI library)
packages/ui/src/components/ai-elements/code-block.tsx
Adds isStreaming?: boolean prop; uses debounced code state (300ms when streaming) to reduce re-renders; selects syntax style by theme; refactored to CodeBlockComponent and exported as memo(...).
Tool input/output (UI library)
packages/ui/src/components/ai-elements/tool.tsx
Introduces ToolInputComponent and ToolOutputComponent and exports ToolInput/ToolOutput as memo(...); adds optional isStreaming?: boolean prop to input/output props.
CI workflow
.github/workflows/ci.yml
Pins Bun version: replaces latest with 1.2.21 in setup-bun steps and example comment.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Chat UI
    participant Msg as Message Component<br/>(memo/observer)
    participant Store as MobX Store
    participant Code as CodeBlock<br/>(memo)

    UI->>Msg: Render message (props, messageId, isStream)
    Msg->>Store: Subscribe (observer)
    Note right of Msg: Memoized — skips re-render if props unchanged

    alt message contains code (isStream=true)
        UI->>Code: Render with isStreaming=true
        Code->>Code: Debounce updates (300ms)
        Code->>Code: Theme-aware syntax highlight
    else static code (isStream=false)
        UI->>Code: Render with isStreaming=false
        Code->>Code: Update immediately
    end

    Store-->>Msg: Observable change
    Msg->>Msg: Re-render only if needed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Wrapped in memo, I hop and stare,

Observer eyes keep state aware.
Debounced code hums calm and bright,
Streams now steady, dark or light.
A little hop for smoother sight. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Fix performance of chat panel during streaming" is directly related to the core purpose of the changeset. The raw summary shows that all changes consistently focus on performance optimization: wrapping components with memo, observer, and adding debouncing/memoization patterns to prevent excessive re-renders during streaming. The title clearly and concisely communicates this primary objective without being vague or overly broad, making it immediately understandable for someone reviewing the repository history.
Description Check ✅ Passed The PR description provides a solid explanation of the changes in the Description section, detailing how excessive re-renders were eliminated by preventing all content from re-rendering when any message changes, with specific mention of the performance impact on code blocks and streaming. However, the description is incomplete against the template: no Type of Change checkbox was marked despite this being a refactoring/optimization effort, and the Testing and Related Issues sections are left blank. The description would benefit from explicit type selection and testing documentation, though the core explanation is substantive enough to understand the PR's intent.

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

❤️ Share

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

}, [code, isStreaming]);

return (
<CodeBlockContext.Provider value={{ code }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using the debounced code value (debouncedCode) in the context provider instead of the raw code. This ensures that when users copy the code via the copy button, it matches the debounced (displayed) version during streaming.

Suggested change
<CodeBlockContext.Provider value={{ code }}>
<CodeBlockContext.Provider value={{ code: debouncedCode }}>

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
packages/ui/src/components/ai-elements/code-block.tsx (1)

134-143: Icon-only button lacks an accessible name; add aria-label (and clear timeout).

Screen readers won’t announce purpose without an accessible name. Also clear the “copied” timeout on unmount to avoid setState after unmount.

Apply this diff:

-        <Button
+        <Button
           className={cn('shrink-0', className)}
           onClick={copyToClipboard}
           size="icon"
           variant="ghost"
+          aria-label={isCopied ? 'Copied' : 'Copy code'}
+          title={isCopied ? 'Copied' : 'Copy code'}
           {...props}
         >
-            {children ?? <Icon size={14} />}
+            {children ?? <Icon size={14} aria-hidden="true" />}
         </Button>

Optional cleanup (outside this hunk):

import { useEffect, useRef } from 'react';
const timerRef = useRef<number | null>(null);
// after successful copy:
timerRef.current = window.setTimeout(() => setIsCopied(false), timeout);
// cleanup:
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx (2)

59-61: Use a button element for the trigger (keyboard a11y).

Using a div as the CollapsibleTrigger child harms focus/keyboard support. Prefer a semantic button.

- <CollapsibleTrigger asChild>
-   <div className="flex-1 flex items-center gap-2 cursor-pointer pl-3 py-2">
+ <CollapsibleTrigger asChild>
+   <button type="button" className="flex-1 flex items-center gap-2 cursor-pointer pl-3 py-2">
      ...
-   </div>
+   </button>
 </CollapsibleTrigger>

1-1: Add 'use client' directive—observer and state require client boundary.

This component uses observer (MobX), useState, and useEditorEngine hook, all of which require client execution. Per coding guidelines, apps/web/client/src/app/**/*.tsx defaults to Server Components; add 'use client' when using state/browser APIs. Per the learning guideline, observer components must be client components, and no upstream layout in the inheritance chain (app/layout.tsx, project/layout.tsx, project/[id]/layout.tsx) has a client boundary.

Add the directive at the top of the file:

+ 'use client';
+
 import { useEditorEngine } from '@/components/store/editor';
🧹 Nitpick comments (14)
packages/ui/src/components/ai-elements/tool.tsx (2)

96-110: Well-structured memoization pattern.

The refactoring correctly implements React.memo by creating an internal ToolInputComponent and exporting the memoized version. The isStreaming prop is properly typed and threaded through to CodeBlock.

However, since input is typed as ToolUIPart['input'] (likely an object), memo's default shallow comparison may not prevent re-renders if the parent component creates new object references on each render. The memoization will still help when other parent props change, which may be sufficient for streaming scenarios.

Consider adding a display name for better debugging experience:

 export const ToolInput = memo(ToolInputComponent);
+ToolInput.displayName = 'ToolInput';

112-151: Memoization pattern correctly applied with consistent prop threading.

The ToolOutput refactoring mirrors the ToolInput pattern and properly passes isStreaming to all three CodeBlock instances (lines 126, 128). The conditional rendering logic remains intact while adding the performance optimization.

Similar to ToolInput, the output prop may be a complex object, so memo's default shallow comparison might not catch all changes. However, this is acceptable for the streaming use case where the optimization targets unrelated prop changes.

Optionally add a display name for consistency with debugging tools:

 export const ToolOutput = memo(ToolOutputComponent);
+ToolOutput.displayName = 'ToolOutput';
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (2)

6-14: Consider consistent prop naming between components.

The component receives isStreaming but passes it as isStream to MessageContent. While functionally correct, this inconsistency makes the data flow harder to trace.

Apply this diff to align prop names:

-const AssistantMessageComponent = ({ message, isStreaming }: { message: ChatMessage, isStreaming: boolean }) => {
+const AssistantMessageComponent = ({ message, isStream }: { message: ChatMessage, isStream: boolean }) => {
     return (
         <div className="px-4 py-2 text-small content-start flex flex-col text-wrap gap-2">
             <MessageContent
                 messageId={message.id}
                 parts={message.parts}
                 applied={false}
-                isStream={isStreaming}
+                isStream={isStream}
             />
         </div>
     );

Or rename the MessageContent prop to match the incoming prop name.


19-19: Reconsider the memo + observer wrapper combination.

The pattern memo(observer(...)) may be redundant. MobX's observer already performs shallow prop comparison and optimizes re-renders. Wrapping with memo adds an additional equality check that typically provides minimal benefit and can complicate debugging.

Unless you have a specific performance issue that requires both wrappers, consider using only observer:

-export const AssistantMessage = memo(observer(AssistantMessageComponent));
+export const AssistantMessage = observer(AssistantMessageComponent);

If you need both, the recommended order from MobX documentation is observer(memo(...)) rather than memo(observer(...)).

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)

333-333: Reconsider the memo + observer wrapper combination.

Similar to AssistantMessage, the memo(observer(...)) pattern may be redundant. MobX's observer already optimizes re-renders through shallow prop comparison.

Consider using only observer for simplicity:

-export const UserMessage = memo(observer(UserMessageComponent));
+export const UserMessage = observer(UserMessageComponent);

If both wrappers are needed for specific performance reasons, the recommended order is observer(memo(...)).

packages/ui/src/components/ai-elements/code-block.tsx (2)

41-53: Debounce looks good; avoid redundant state sets.

Guard setDebouncedCode to skip no-op updates.

Apply this diff:

-            const timer = setTimeout(() => {
-                setDebouncedCode(code);
-            }, 300);
+            const timer = setTimeout(() => {
+                setDebouncedCode((prev) => (prev === code ? prev : code));
+            }, 300);
             return () => clearTimeout(timer);
         } else {
             // When not streaming, update immediately
-            setDebouncedCode(code);
+            setDebouncedCode((prev) => (prev === code ? prev : code));
         }

63-87: Hot path optimization: reduce Highlighter cost and prop churn.

Two incremental wins:

  • Load-time: switch to light build + dynamic import (code-split) or at least dynamic-import Prism.
  • Render-time: memoize static prop objects to avoid new identities each render.

Option A (preferred): Light build + register needed languages

// imports (outside this hunk)
import dynamic from 'next/dynamic';
const SyntaxHighlighter = dynamic(
  () => import('react-syntax-highlighter').then(m => m.PrismLight),
  { ssr: false }
);
// Register only what you use, e.g.:
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
SyntaxHighlighter.registerLanguage('tsx', tsx);

Option B: Keep Prism, but code-split it

import dynamic from 'next/dynamic';
const SyntaxHighlighter = dynamic(
  () => import('react-syntax-highlighter').then(m => m.Prism),
  { ssr: false }
);

Also memoize static props:

-                        codeTagProps={{
-                            className: 'font-mono text-xs',
-                        }}
+                        codeTagProps={codeTagProps}
...
-                        customStyle={{
-                            margin: 0,
-                            padding: '1rem',
-                            fontSize: '0.875rem',
-                            background: 'hsl(var(--background-secondary))',
-                            color: 'hsl(var(--foreground))',
-                        }}
+                        customStyle={customStyle}
...
-                        lineNumberStyle={{
-                            color: 'hsl(var(--muted-foreground))',
-                            paddingRight: '1rem',
-                            minWidth: '2.5rem',
-                        }}
+                        lineNumberStyle={lineNumberStyle}

Add (outside this hunk):

import { useMemo } from 'react';
const codeTagProps = useMemo(() => ({ className: 'font-mono text-xs' }), []);
const customStyle = useMemo(() => ({
  margin: 0, padding: '1rem', fontSize: '0.875rem',
  background: 'hsl(var(--background-secondary))',
  color: 'hsl(var(--foreground))',
}), []);
const lineNumberStyle = useMemo(() => ({
  color: 'hsl(var(--muted-foreground))', paddingRight: '1rem', minWidth: '2.5rem',
}), []);
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx (7)

20-25: Unused props (applied, messageId) and naming consistency.

applied and messageId are declared but unused; consider removing or rendering an "Applied" chip/indicator. Also consider aligning isStream → isStreaming for consistency with downstream APIs (verify impact first).

Example (if you intend to show status):

 const CollapsibleCodeBlockComponent = ({
   path,
   content,
-  isStream,
-  branchId,
+  isStream,
+  branchId,
+  applied,
 }: CollapsibleCodeBlockProps) => {
   ...

And render an "Applied" badge near the filename if true.


40-43: Guard branch lookup to avoid crashes if store is unavailable.

Make branch resolution null‑safe.

- const branch = branchId
-     ? editorEngine.branches.allBranches.find(b => b.id === branchId)
-     : editorEngine.branches.activeBranch;
+ const branch =
+   branchId
+     ? editorEngine?.branches?.allBranches?.find(b => b.id === branchId)
+     : editorEngine?.branches?.activeBranch;

78-82: Improve long-branch-name UX.

Add a title for hover tooltip so truncated names are discoverable.

- <span className="text-foreground-tertiary group-hover:text-foreground-secondary text-mini ml-0.5 flex-shrink-0 truncate max-w-24">
+ <span
+   title={branch.name}
+   className="text-foreground-tertiary group-hover:text-foreground-secondary text-mini ml-0.5 flex-shrink-0 truncate max-w-24"
+ >

98-100: Infer language from path instead of hardcoding 'jsx'.

Improves highlighting for ts/tsx/js/json/etc.

+ const inferLanguageFromPath = (p: string) => {
+   const ext = p.split('.').pop()?.toLowerCase();
+   switch (ext) {
+     case 'ts': return 'typescript';
+     case 'tsx': return 'tsx';
+     case 'js': return 'javascript';
+     case 'jsx': return 'jsx';
+     case 'json': return 'json';
+     case 'css': return 'css';
+     case 'html': return 'html';
+     case 'sh': return 'bash';
+     default: return 'plaintext';
+   }
+ };
...
- <CodeBlock code={content} language="jsx" isStreaming={isStream} className="text-xs overflow-x-auto" />
+ <CodeBlock code={content} language={inferLanguageFromPath(path)} isStreaming={isStream} className="text-xs overflow-x-auto" />

101-117: i18n the labels and harden clipboard copy (fallback + cleanup).

  • Replace hardcoded "Copy/Copied" with next‑intl. As per coding guidelines.
  • Handle clipboard API errors/unsupported contexts and clear timeout on unmount.
- import { memo, useState } from 'react';
+ import { memo, useEffect, useRef, useState } from 'react';
+ import { useTranslations } from 'next-intl';
...
   const [isOpen, setIsOpen] = useState(false);
   const [copied, setCopied] = useState(false);
+  const copyTimeoutRef = useRef<number | null>(null);
+  const t = useTranslations('chat.codeBlock');
...
- const copyToClipboard = () => {
-   navigator.clipboard.writeText(content);
-   setCopied(true);
-   setTimeout(() => setCopied(false), 2000);
- };
+ const copyToClipboard = async () => {
+   try {
+     if (navigator.clipboard?.writeText) {
+       await navigator.clipboard.writeText(content);
+     } else {
+       throw new Error('Clipboard API unavailable');
+     }
+   } catch {
+     const ta = document.createElement('textarea');
+     ta.value = content;
+     ta.style.position = 'fixed';
+     ta.style.opacity = '0';
+     document.body.appendChild(ta);
+     ta.select();
+     try { document.execCommand('copy'); } finally { document.body.removeChild(ta); }
+   }
+   setCopied(true);
+   if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
+   copyTimeoutRef.current = window.setTimeout(() => setCopied(false), 2000);
+ };
+
+ useEffect(() => () => { if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); }, []);
...
-   Copied
+   {t('copied')}
...
-   Copy
+   {t('copy')}

Define messages:

  • chat.codeBlock.copy
  • chat.codeBlock.copied

89-95: Optional: add exit animation or drop AnimatePresence.

You use AnimatePresence mode="wait" but no exit prop; add exit for a smoother collapse.

  <motion.div
    key="content"
    initial={getAnimation()}
    animate={getAnimation()}
+   exit={{ height: 0, opacity: 0 }}
    transition={{ duration: 0.2, ease: 'easeInOut' }}
    style={{ overflow: 'hidden' }}
  >

8-9: observer + memo layering is redundant.

observer already optimizes renders; wrapping with memo adds little and can confuse contributors. Prefer a single observer.

- import { memo, useState } from 'react';
+ import { useState } from 'react';
...
- export const CollapsibleCodeBlock = memo(observer(CollapsibleCodeBlockComponent));
+ export const CollapsibleCodeBlock = observer(CollapsibleCodeBlockComponent);

Also applies to: 131-131

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26b24be and f9c45b9.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (4 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx (6 hunks)
  • packages/ui/src/components/ai-elements/code-block.tsx (2 hunks)
  • packages/ui/src/components/ai-elements/tool.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Avoid hardcoded user-facing text; use next-intl messages/hooks

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Use path aliases @/* and ~/* for imports mapping to src/*

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable identities across renders
Keep the active MobX store in a useRef and clean up asynchronously with setTimeout(() => storeRef.current?.clear(), 0)
Do not use useMemo to create MobX stores
Avoid placing MobX store instances in effect dependency arrays if it causes loops; split concerns instead
observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx
  • packages/ui/src/components/ai-elements/code-block.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx
  • packages/ui/src/components/ai-elements/tool.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libs

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx
  • packages/ui/src/components/ai-elements/code-block.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx
  • packages/ui/src/components/ai-elements/tool.tsx
🧬 Code graph analysis (4)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (19-19)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx (2)
packages/ui/src/components/ai-elements/code-block.tsx (1)
  • CodeBlock (96-96)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/code-block.tsx (1)
  • CodeBlock (7-39)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx (1)
packages/ui/src/components/ai-elements/tool.tsx (4)
  • ToolInput (110-110)
  • ToolOutput (151-151)
  • ToolContent (86-94)
  • Tool (14-19)
packages/ui/src/components/ai-elements/tool.tsx (1)
packages/ui/src/components/ai-elements/code-block.tsx (1)
  • CodeBlock (96-96)
🔇 Additional comments (13)
packages/ui/src/components/ai-elements/tool.tsx (1)

6-6: LGTM! Import aligns with performance optimization goals.

The memo import is correctly added to support the memoization pattern applied to ToolInput and ToolOutput components.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx (3)

32-32: LGTM! Proper memo export pattern.

The export wrapping is correctly implemented and will help reduce unnecessary re-renders during streaming operations.


5-15: Memoization pattern approved; 'use client' boundary verified in parent hierarchy.

The restructuring to use an internal ToolCallSimpleComponent wrapped with memo follows React best practices and aligns with the PR's performance optimization goals. The 'use client' directive is properly established in parent components (chat-messages/index.tsx and others), so this child component correctly inherits the client boundary without repeating the directive.

The isStreaming prop propagation to ToolInput and ToolOutput is appropriate for streaming optimization.


25-26: Excellent propagation of streaming state to child components.

Passing isStreaming={loading} to both ToolInput and ToolOutput enables these components to optimize their rendering during streaming, which directly addresses the PR's performance objectives. Both components are properly typed to accept the optional isStreaming prop and pass it to their CodeBlock children for streaming optimization.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx (2)

74-74: Good use of observer-only wrapper.

Using only observer without memo is the appropriate pattern here. The observer wrapper provides the necessary reactivity without the redundant memoization layer seen in some other components in this PR.


1-74: No action required—architecture is correct per guidelines.

The codebase correctly implements the client boundary pattern. The single 'use client' directive is properly placed at chat-messages/index.tsx (the feature entry point), which imports UserMessage, AssistantMessage, and ErrorMessage as children. These child components in turn use MessageContent—an observer component that correctly inherits the client boundary without repeating the directive. Per coding guidelines, child observer components should not repeat 'use client'.

Likely an incorrect or invalid review comment.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (2)

46-46: Observer wrapper is appropriate for MobX store usage.

The use of observer is justified since the component calls useEditorEngine() (line 46), which likely returns a MobX store with observable properties that need reactivity tracking.


1-333: No action required—client boundary is correctly placed at feature entry per coding guidelines.

The verified component hierarchy shows:

  • index.tsx (feature entry) has 'use client' and directly imports UserMessage
  • UserMessage is an observer component that does not repeat 'use client'
  • This follows the coding guideline: "observer components must be client components; place a single client boundary at the feature entry; child observers need not repeat 'use client'"

Since index.tsx has the client boundary and directly imports UserMessage, the observer component is correctly part of the client bundle without needing its own directive.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)

1-19: Now let me check the index.tsx file to verify the import/export chain:Now let me inspect the index.tsx file and check the parent hierarchy for the proper 'use client' boundary:Based on my verification, I have sufficient information to generate the final rewritten review comment. Let me analyze what I've learned:

Verify 'use client' directive is already present at the parent boundary.

The 'use client' directive in index.tsx (confirmed at line 1) marks all its imports and child components as part of the client bundle. Since AssistantMessage is imported and wrapped with observer in the component tree, it already operates within a client context. You don't need to add the directive to every component intended for the client.

No changes required—the existing parent boundary satisfies the observer component requirement.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx (1)

16-16: LGTM! The applied prop is correctly implemented.

The new applied boolean prop is properly added to the component signature and consistently passed to all CollapsibleCodeBlock instances across the different tool branches (WriteFileTool, FuzzyEditFileTool, SearchReplaceEditTool, SearchReplaceMultiEditFileTool). This supports the streaming performance optimization mentioned in the PR objectives.

Also applies to: 21-21, 91-91, 116-116, 141-141, 166-166

packages/ui/src/components/ai-elements/code-block.tsx (2)

55-56: Confirm copy behavior uses raw code vs. displayed (debounced) code.

Context provides the live code, while the UI renders debouncedCode. Is this intentional for “copy latest even if render lags”? If you want copy to match what’s visible, provide both or switch to debouncedCode.


96-96: Memo wrapper LGTM.

Good call to export a memoized wrapper; avoid adding a custom comparator so the effect can still observe code changes.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/code-display/collapsible-code-block.tsx (1)

96-99: Nice perf win: gate CodeMirror render behind isOpen.

Avoiding the heavy editor when collapsed is the right call for streaming performance.

import { FuzzyEditFileTool, SearchReplaceEditTool, SearchReplaceMultiEditFileTool, TerminalCommandTool, TypecheckTool, WebSearchTool, WriteFileTool } from '@onlook/ai';
import type { WebSearchResult } from '@onlook/models';
import type { ToolUIPart } from 'ai';
import { observer } from 'mobx-react-lite';
Copy link

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

Verify the necessity of the observer wrapper and consider adding memoization.

The component is now wrapped with observer(), but scanning the implementation reveals no obvious access to MobX observable properties. All props appear to be plain values (strings, booleans, ToolUIPart), and the component body only accesses standard properties like toolPart.type, toolPart.state, etc.

Additionally, other components in this PR (mentioned in the AI summary) follow a memo(observer(...)) pattern for consistency and performance, but this component only uses observer().

Run the following script to verify if ToolUIPart or its accessed properties contain MobX observables:

If ToolUIPart does not contain observables, consider removing the observer() wrapper. Otherwise, for consistency with other components in this PR, apply this pattern:

-import { observer } from 'mobx-react-lite';
+import { memo } from 'react';
+import { observer } from 'mobx-react-lite';

-export const ToolCallDisplay = observer(ToolCallDisplayComponent);
+export const ToolCallDisplay = memo(observer(ToolCallDisplayComponent));

Also applies to: 12-12, 230-230


🏁 Script executed:

#!/bin/bash
# Check if ToolUIPart type contains MobX observable definitions

# Search for ToolUIPart type definition
ast-grep --pattern 'type ToolUIPart = $$$'

# Also search for interface definition
ast-grep --pattern 'interface ToolUIPart {
  $$$
}'

# Check for MobX observable usage in types
rg -n --type=ts --type=tsx 'ToolUIPart' -A 10 -B 2

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Correct search for ToolUIPart type definition using proper ripgrep syntax

# Search for ToolUIPart type definition in TypeScript files
echo "=== Searching for ToolUIPart type/interface definition ==="
rg -n 'type ToolUIPart|interface ToolUIPart' -A 15

# Search for where ToolUIPart is imported/used in the tool-call-display file
echo -e "\n=== Checking imports in tool-call-display.tsx ==="
rg -n 'ToolUIPart' apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx -B 5 -A 3

# Look for MobX observable patterns in ToolUIPart related files
echo -e "\n=== Checking for MobX observable patterns ==="
rg -n '@observable|makeObservable|observable\(' -A 3 -B 1

Length of output: 2019


🏁 Script executed:

#!/bin/bash
# View the actual component implementation to check for MobX store/observable access

echo "=== Full tool-call-display.tsx component ==="
cat apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx

echo -e "\n=== Check for store hooks or observable access patterns ==="
rg -n 'useStore|useObserver|observer\(|\.get\(\)|@observable' apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx

Length of output: 8589


Remove the unnecessary observer() wrapper—this component does not access any MobX observables.

The component only receives plain props (strings, boolean, ToolUIPart type from the ai package) and accesses standard property values without any MobX store hooks or observable patterns. Wrapping a component with observer() when it doesn't access observables adds unnecessary overhead without providing any benefit. The applied prop is passed to child components as a plain prop, not indicating MobX reactivity tracking in this component.

Remove the observer wrapper and import:

-import { observer } from 'mobx-react-lite';
 import stripAnsi from 'strip-ansi';

-export const ToolCallDisplay = observer(ToolCallDisplayComponent);
+export const ToolCallDisplay = ToolCallDisplayComponent;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx
around line 4, remove the unnecessary MobX observer usage: delete the import of
observer from 'mobx-react-lite' and remove the observer(...) wrapper around the
component export so the component is a plain React component; ensure any imports
or references to observer are removed and update the export/default export
accordingly to return the component directly.

- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
bun-version: 1.2.21
Copy link
Contributor

Choose a reason for hiding this comment

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

Bun version inconsistency: test job now pins version to 1.2.21 while typecheck job still uses latest. Consider aligning them for consistent CI builds.

@saddlepaddle saddlepaddle merged commit 59df63f into main Oct 20, 2025
3 checks passed
@Kitenite Kitenite deleted the fix-performance branch October 28, 2025 21:22
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