Skip to content

Refactor ChatView into memoized UI subcomponents#58

Merged
juliusmarminge merged 2 commits intomainfrom
codething/b1a7277c
Feb 16, 2026
Merged

Refactor ChatView into memoized UI subcomponents#58
juliusmarminge merged 2 commits intomainfrom
codething/b1a7277c

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 16, 2026

Not sure why the compiler isn't doing it's magic but this manual memoization shows significant perf wins...

Before vs after (old 00:17 trace -> new 00:49 trace):

keypress avg: 24.66ms -> 3.60ms (-85.4%)
textInput avg: 24.58ms -> 3.50ms (-85.8%)
input avg: 23.97ms -> 2.99ms (-87.5%)
worst keypress spike: 68.73ms -> 5.50ms (-92.0%)
total EventDispatch time: 6467ms -> 816ms (-87.4%)
dispatchDiscreteEvent time: 2116ms -> 245ms (-88.4%)
EventDispatch >= 50ms: 24 -> 0

Summary

  • Split ChatView into focused memoized subcomponents (ChatHeader, ThreadErrorBanner, PendingApprovalsPanel, MessagesTimeline) to reduce render churn and improve maintainability.
  • Memoized additional frequently rendered components (ChatMarkdown, ModelPicker, ReasoningEffortPicker, OpenInPicker).
  • Stabilized callbacks with useCallback (approval responses, diff toggle, work-group expand, image expand) to support memoization.
  • Reduced running-phase timer frequency from 250ms to 1000ms to lower unnecessary UI updates.
  • Preserved existing chat timeline behavior while extracting large inline rendering blocks.

Testing

  • Not run (no test execution details were provided in the commit context).
  • Manual verification recommended: send/stream messages, expand/collapse work logs, approve/decline requests, toggle Diff, and expand image previews.

Open with Devin

Summary by CodeRabbit

  • Refactor
    • Reorganized the chat interface into modular components (header, error/approvals panels, message timeline) for cleaner structure and maintainability.
  • New Features
    • Added UI pickers for model selection, reasoning effort, and "open in" options in the bottom toolbar.
  • Performance
    • Introduced memoization across components and adjusted running-phase tick interval to reduce unnecessary re-renders.

- Extract header, error, approvals, and timeline into memoized components
- Memoize `ChatMarkdown`, pickers, and handlers to reduce avoidable re-renders
- Reduce running-time ticker frequency from 250ms to 1000ms
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 16, 2026

Walkthrough

Wrapped ChatMarkdown with React.memo. Refactored ChatView by extracting inline UI into multiple memoized subcomponents (ChatHeader, ThreadErrorBanner, PendingApprovalsPanel, MessagesTimeline, ModelPicker, ReasoningEffortPicker, OpenInPicker), adjusted callback wiring and timer interval.

Changes

Cohort / File(s) Summary
ChatMarkdown Memoization
apps/web/src/components/ChatMarkdown.tsx
Added memo import and exported memo(ChatMarkdown) instead of the raw component; no rendering or prop changes.
ChatView Component Refactor
apps/web/src/components/ChatView.tsx
Split large inline UI into dedicated internal components (ChatHeader, ThreadErrorBanner, PendingApprovalsPanel, MessagesTimeline, ModelPicker, ReasoningEffortPicker, OpenInPicker), moved logic into those components, introduced useCallback for approval handling, added activeSessionId usage, changed running-phase timer from 250ms to 1000ms, and expanded imports/types.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant ChatView as ChatView
    participant Header as ChatHeader
    participant Timeline as MessagesTimeline
    participant Native as NativeApi

    User->>ChatView: interact (send message / open image / pick model)
    ChatView->>Header: render header, actions (onToggleDiff, OpenInPicker)
    Header->>ChatView: emit action callbacks
    ChatView->>Timeline: render messages, handle image expand / work-group toggle
    Timeline->>Native: request image data / interaction
    ChatView->>Native: session-scoped actions (respondToApproval, ensureSession)
    Native-->>ChatView: responses / confirmations
    ChatView-->>User: UI updates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Refactor ChatView into memoized UI subcomponents' directly and accurately summarizes the main change: extracting ChatView into memoized subcomponents for performance optimization.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/b1a7277c

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

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 16, 2026

Refactor ChatView by extracting memoized UI subcomponents and update running timer interval to 1000ms

Split ChatView into memoized components for header, error banner, pending approvals, and messages timeline; memoize ChatMarkdown; add memoized callbacks; and change running-phase nowTick updates to 1000ms in ChatView.tsx and ChatMarkdown.tsx.

📍Where to Start

Start with ChatView in ChatView.tsx, focusing on the new subcomponent props and the nowTick interval change.


Macroscope summarized b45b3ca.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 16, 2026

Greptile Summary

This PR refactors the monolithic ChatView component into focused memoized subcomponents (ChatHeader, ThreadErrorBanner, PendingApprovalsPanel, MessagesTimeline) and wraps existing helper components (ChatMarkdown, ModelPicker, ReasoningEffortPicker, OpenInPicker) with memo(). It also stabilizes event handlers with useCallback and reduces the running-phase timer from 250ms to 1000ms.

  • Subcomponent extraction: The large inline JSX blocks for header, error banner, approval panel, and message timeline are cleanly moved into separate memo-wrapped components with explicit prop interfaces.
  • Memoization gap — onRespondToApproval: The useCallback dependency on activeThread?.session (an object that changes on every provider event) causes the callback to be recreated frequently, defeating the memo on PendingApprovalsPanel during streaming. Should be narrowed to activeThread?.session?.sessionId.
  • Memoization gap — MessagesTimeline: Receives the entire activeThread object, but only uses activeThread.messages.length. Since activeThread is a new reference on every store dispatch, the memo wrapper is never effective. Passing a hasMessages boolean instead would fix this.
  • Timer interval change: Reducing the tick timer from 250ms to 1000ms is a good trade-off — elapsed durations update less frequently but remain sufficiently responsive for a streaming status display.

Confidence Score: 3/5

  • The PR is functionally safe with no behavioral regressions, but two memoization patterns are ineffective under the conditions where they matter most (streaming/active sessions).
  • Score of 3 reflects that while the refactoring is structurally sound and preserves correctness, the primary goal of reducing render churn is undermined by object-reference dependencies that defeat memo during streaming — the exact scenario the optimization targets. No runtime bugs, but the performance claims of the PR are not fully realized.
  • apps/web/src/components/ChatView.tsx — the onRespondToApproval useCallback deps and MessagesTimeline activeThread prop both defeat memo during active streaming.

Important Files Changed

Filename Overview
apps/web/src/components/ChatView.tsx Major refactoring: extracts 4 memoized subcomponents (ChatHeader, ThreadErrorBanner, PendingApprovalsPanel, MessagesTimeline), wraps existing helpers in memo, stabilizes callbacks with useCallback. Two memoization issues: activeThread?.session object reference in useCallback deps and full activeThread passed to MessagesTimeline will defeat memo during streaming.
apps/web/src/components/ChatMarkdown.tsx Simple and correct: wraps existing ChatMarkdown export with memo() for prop-based shallow comparison. No functional changes.

Flowchart

flowchart TD
    CV[ChatView] -->|"title, project, diffOpen"| CH[ChatHeader]
    CV -->|"error string"| TEB[ThreadErrorBanner]
    CV -->|"approvals, respondingIds, onRespond"| PAP[PendingApprovalsPanel]
    CV -->|"activeThread, timelineEntries, nowIso, ..."| MT[MessagesTimeline]
    MT -->|"text"| CM[ChatMarkdown]
    CV -->|"model, onModelChange"| MP[ModelPicker]
    CV -->|"effort, onEffortChange"| REP[ReasoningEffortPicker]
    CH -->|"keybindings"| OIP[OpenInPicker]
    CH -->|"api, gitCwd"| GAC[GitActionsControl]

    style CH fill:#2d6a4f,stroke:#40916c
    style TEB fill:#2d6a4f,stroke:#40916c
    style PAP fill:#e76f51,stroke:#f4a261
    style MT fill:#e76f51,stroke:#f4a261
    style CM fill:#2d6a4f,stroke:#40916c
    style MP fill:#2d6a4f,stroke:#40916c
    style REP fill:#2d6a4f,stroke:#40916c
    style OIP fill:#2d6a4f,stroke:#40916c
    style GAC fill:#264653,stroke:#2a9d8f
Loading

Last reviewed commit: bc7eae3

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/web/src/components/ChatView.tsx Outdated
setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId));
}
},
[activeThread?.id, activeThread?.session, api, dispatch],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

activeThread?.session defeats memo on PendingApprovalsPanel

activeThread?.session is a ProviderSession object that gets replaced with a new reference on every provider event (via evolveSession in the store reducer). Since this callback only uses activeThread.session.sessionId (a string), the dependency should be narrowed to activeThread?.session?.sessionId. As-is, onRespondToApproval is recreated on every incoming event, which means PendingApprovalsPanel's memo wrapper is never effective during active streaming — exactly when memoization matters most.

Suggested change
[activeThread?.id, activeThread?.session, api, dispatch],
[activeThread?.id, activeThread?.session?.sessionId, api, dispatch],

Comment thread apps/web/src/components/ChatView.tsx
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
apps/web/src/components/ChatView.tsx (2)

500-508: Timer interval increase may affect streaming UX smoothness.

The interval change from 250ms to 1000ms reduces render frequency, but elapsed time displays during streaming (used in formatMessageMeta on lines 1599-1607) will now update only once per second instead of four times. This may make the elapsed time appear to "jump" rather than count smoothly.

Consider whether this trade-off aligns with UX requirements, particularly for longer-running responses where users watch the elapsed time.

Based on learnings: "Preserve fast time-to-first-delta and smooth streaming in chat message updates."


1548-1556: Non-null assertion is safe but could be cleaner.

The image.previewUrl! assertion on line 1554 is safe because it's inside the truthy branch of image.previewUrl ?. TypeScript doesn't narrow types inside inline arrow functions, necessitating the assertion.

An alternative would be to capture the value before the callback:

♻️ Optional: Avoid non-null assertion
-                          {image.previewUrl ? (
+                          {image.previewUrl ? (() => {
+                            const previewUrl = image.previewUrl;
+                            return (
                             <img
                               src={image.previewUrl}
                               alt={image.name}
                               className="h-full max-h-[220px] w-full cursor-zoom-in object-cover"
                               onClick={() =>
-                                onImageExpand({ src: image.previewUrl!, name: image.name })
+                                onImageExpand({ src: previewUrl, name: image.name })
                               }
                             />
+                            );
+                          })() : (
-                          ) : (

- Derive `activeSessionId` and `activeThreadId` before approval callbacks
- Avoid stale thread/session captures when submitting approval decisions
- Pass `hasMessages` to `MessagesTimeline` instead of the full thread
@juliusmarminge juliusmarminge merged commit 73fa2d6 into main Feb 16, 2026
3 checks passed
@juliusmarminge
Copy link
Copy Markdown
Member Author

Desktop Dev Perf Trace

  • Command: bun dev:desktop
  • Trace: /tmp/t3code-perf-artifacts/desktop-dev-2026-02-16T09-11-15-236Z/trace.json
  • Started: 2026-02-16T09:11:16.936Z
  • Completed: 2026-02-16T09:11:24.384Z
  • Duration: 7448 ms

Interaction Run

  • Thread clicks: 8
  • Typed chars: 59
  • Model selected: GPT-5.3 Codex Spark

Input Event Metrics

Event Count Avg (ms) Max (ms) Total (ms)
keypress 0 0 0 0
textInput 0 0 0 0
input 59 1.414 2.005 83.429
keydown 59 0.084 0.518 4.928

Scheduler/Event Hotspots

  • dispatchDiscreteEvent: 89.199ms total (995 calls)
  • performWorkUntilDeadline: 14.772ms total (84 calls)
  • EventDispatch spikes >= 50ms: 0

Threshold Check

  • keypress avg <= 12ms: pass
  • keypress max <= 24ms: pass
  • long dispatch spikes <= 0: pass

Heap Counters

  • first=19.1MB, last=23.3MB, min=15MB, max=35.5MB, delta=4.2MB

Top User Timing Marks

  • 167x ​Button
  • 144x Mount
  • 45x ​FocusGuard
  • 42x Update
  • 36x ​FloatingFocusManager
  • 32x ​FloatingPortalLite
  • 28x ​MessagesTimeline
  • 26x ​CompositeList
  • 24x ​DialogRoot
  • 24x ​ToastViewport

Top Duration Events

  • RunTask: total=771.76ms, avg=0.104ms, max=106.444ms, count=7442
  • v8.callFunction: total=625.48ms, avg=0.247ms, max=27.333ms, count=2533
  • FunctionCall: total=414.53ms, avg=0.169ms, max=102.145ms, count=2455
  • TimerFire: total=303.31ms, avg=3.486ms, max=27.343ms, count=87
  • CpuProfiler::StartProfiling: total=110.38ms, avg=55.188ms, max=101.474ms, count=2
  • EventDispatch: total=102.5ms, avg=0.191ms, max=2.005ms, count=536
  • GPUTask: total=74.13ms, avg=0.332ms, max=7.947ms, count=223
  • UpdateLayoutTree: total=71.99ms, avg=0.225ms, max=3.375ms, count=320
  • V8.GC_SCAVENGER_BACKGROUND_SCAVENGE_PARALLEL: total=56.53ms, avg=0.5ms, max=1.165ms, count=113
  • Paint: total=23.15ms, avg=0.119ms, max=0.286ms, count=195

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant