Skip to content

Refactor ChatView into memoized UI subcomponents - #58

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

juliusmarminge merged 2 commits into
mainfrom
codething/b1a7277c

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Feb 16, 2026 •

Copy link
Copy Markdown
Member

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

coderabbitai Bot commented Feb 16, 2026 •

Copy link
Copy Markdown

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

macroscopeapp Bot commented Feb 16, 2026 •

Copy link
Copy Markdown
Contributor

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

greptile-apps Bot commented Feb 16, 2026

Copy link
Copy Markdown

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

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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

aorwall added a commit to aorwall/t3code that referenced this pull request Aug 6, 2026
…gdotgg#58)

* feat(settings): build the Secrets administration page in T3

Replace the Secrets NotInT3Yet placeholder with a real panel: a Global
and a Personal section, each listing its scope's secrets with an enable
switch, edit, and delete, plus a create/edit dialog. Reads go through the
shared moatless query helpers keyed under `secrets/{scope}`, so a write
invalidates both scopes together.

The row-derivation logic (kind labels, active-first ordering) lives in a
pure `secretRows.ts` with co-located tests; the panel only reads atoms and
renders T3's own settings primitives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(settings): build the Users administration page in T3

Replace the Users NotInT3Yet placeholder with a list and a detail page.
The list shows everyone with an account — monogram, name, admin and bot
tags — and leads to one user's page. The detail page edits name, email,
and the global role with dirty tracking and an explicit save, and shows
the read-only account identity beneath it.

There is no single-user read on the wire, so the detail page reads the
same list the index does and finds its login in it; the two share the
`users` key so a role change refetches both at once. Row derivation
(display name, monogram, humans-before-bots ordering) is a pure
`userRows.ts` with co-located tests.

The detail route uses the trailing-underscore file form so it does not
nest under the list route, and is recorded in the merge inventory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(settings): build the Moatless Loops administration pages in T3

Add the Loops list and Loop detail settings surfaces, replacing the
NotInT3Yet placeholders. The list shows each loop's state, git provenance
and source summary, and links to a detail page that edits the loop's
general fields, configuration and schedule with the workspace SaveBar
pattern, exposes lifecycle actions (pause/resume/approve-and-activate),
git override/restore, and delete. Source derivation and provenance live
in a pure `loopRows.ts` module with co-located tests.

Detail route uses the underscore form to avoid nested-route rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(settings): build the Moatless Integrations administration page in T3

Replace the NotInT3Yet placeholder on /settings/integrations with a panel
of three sections: Connections (adapter connections a loop can subscribe
to — listed, created, and opened to a detail page that removes them),
Apps (the configured adapter apps and which of each app's secrets are
set, read-only), and GitHub (registered GitHub apps, their installations
and the default installation, read-only). Naming, ordering and secret
fingerprint derivation live in a pure integrationRows.ts module with
co-located tests.

Connection detail route uses the underscore form to avoid nested-route
rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(settings): administer skills plugins and their activation in T3

Adds the Skills administration surface: a plugin catalog and, per plugin,
the skills it sources with a control for the deployment-wide default and the
viewer's own override, side by side.

The delivered badge is read straight from /plugins/effective rather than
recomputed from the two controls — off-for-everyone-plus-on-for-you is the
server's call to resolve, so a skill can read "Not set / Not set" and still be
Delivered because its plugin is on. Precedence lives in skillRows.ts behind a
test that pins the source-off-with-skill-on and unset-versus-off cases.

Detail route uses the underscore form so it does not nest under the list panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): mark moatless-admin spec in review and correct stale claims

All five administration surfaces are now built and under review as PRs
pingdotgg#51–pingdotgg#55, so the spec no longer reads "in progress". Records the merge order
the shared queries.ts forces, and the two follow-ups that land once the five
merge (delete NotInT3Yet, move this spec to .plans/completed/).

Also fixes two claims that drifted from what shipped: the acceptance criterion
still said the not-built placeholder links into the SPA, which the placeholder
decision earlier in the same doc deliberately reversed; and two seam-table
module paths pointed at apps/web/src/moatless/ rather than where the pure
modules actually landed under components/settings/moatless/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): unbreak the surfaces table split by the route-convention note

The trailing-underscore route-convention paragraph sat between two table
rows, so Markdown ended the table at it and rendered every row below —
loops through users, ten of the twelve surfaces — as literal pipes. Move the
paragraph below the last row so the table stays one block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(settings): retire the NotInT3Yet placeholder and archive the spec

Every administration surface now has a real panel, so no route renders the
placeholder. Its own comment said to delete the file when the last usage went;
this is that moment. The merge-inventory row stops describing a build where
some entries lead to a placeholder.

Neither of these could be done while the surfaces were five separate branches —
each still needed the placeholder for the other four. Combining them into one
change is what makes both possible, so they land here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(fork): list the merge inventory as entries, not tables

The inventory conflicted on every branch that touched it, and almost none of
that was disagreement. The formatter re-pads a Markdown table across all of its
rows when one cell changes width, so adding a single route path to the
administration-pages row rewrote all 60 lines of the block; two branches editing
two unrelated entries then conflicted on every line between them.

Measured on this file, adding one route path to the administration pages:
60 changed lines as a table, 1 as a list.

So the tables are gone. Every section is a list of entries: a title line, an
optional nested `Paths:` list with one path per line, and the rule. The
formatter leaves list items and prose alone — verified by running `vp fmt` over
the result and getting a byte-identical file back. The header paragraph says so,
so this does not get turned back into a table.

Content is carried over unchanged. Verified by extracting every backticked path
from both versions: 117 before, 117 after, none lost and none invented. Three
deliberate edits beside the reformat:

- "the three fork plans above" now says four; that entry has listed four plans
  since the administration spec was archived.
- "row" means a table row, and there are no tables now, so policy entries and
  the skill's vocabulary say "entry" — including the `decide, then add an entry`
  policy value the skill defines.
- `.agents/skills/fork-upstream-merge/**` gets a path-policy entry. It is
  fork-authored and was in no entry at all; upstream owns the other four skills
  under `.agents/skills/` and adds more, so a blanket `ours` there would be
  wrong. Checked against a shallow `upstream/main` fetch, which also confirmed
  the two counts this file quotes are still current: 32 of 36 `.plans/` files
  and 34 of 39 under `apps/web/src/browser/`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aorwall added a commit to aorwall/t3code that referenced this pull request Aug 6, 2026
Brings in pingdotgg#58, the Moatless administration surfaces. The only conflict
was docs/fork/upstream-merge-inventory.md, which pingdotgg#58 converted from
tables to lists so an edit stops re-padding every row. Took that shape
and re-stated this branch's four inventory changes in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
piero-dev25 added a commit to piero-dev25/devgame that referenced this pull request Aug 12, 2026
…inspect MCP tools

The generation half, productized. A harness MCP toolkit (generate_3d,
generation_status, list_generations, inspect_generation) makes a 3D
generation project-owned state an agent can drive and inspect —
mirrors the preview toolkit's structure and registers the same way.
generate_3d returns a job handle immediately; the SERVER owns the Tripo
poll loop in a fiber forked into the service's OWN scope
(acquireRelease(Scope.make())+forkIn), so a job survives the MCP
request effect returning (the async model OpenCode's 65s timeout
forces). On success it downloads the GLB, inspects it server-side
(glTF accessor parse — no Unity; verified against a committed
5,996-tri barrel fixture), stores it under a project-scoped dir, and
records a GeneratedAsset. inspect_generation returns metadata + the
provider's preview image as an MCP image content block.

Contracts are fork-owned (packages/contracts/src/generation/) — ZERO
vendor-union edits (contracts diff = one barrel-export line, matching
the 5 existing unity-* precedents). Capability grant widened to
{preview, generation} (D2 resolved); the fork-owned
GenerationCapabilityUnavailableError translates at the gate.
Credential read once from ServerSecretStore, seeded from the owner's
key file, never logged.

Deferred to increment 2 (flagged, not skipped): the fork-owned
signed-asset route (its only consumer is the OUT-of-scope panel; server.ts
is at the 20-arg route ceiling pingdotgg#58), Unity import, the Generation panel.
deriveFbx has a minimal impl for inc-2, no coverage yet.

All red-proven (grant, gate, GLB inspection non-vacuous, job lifecycle
monotonic). Server suite 2255 passed; server+contracts typecheck clean.

Co-Authored-By: Claude Fable 5 <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.

1 participant