Skip to content

fix(chat): improve tool group summaries and scrolling

MacroscopeApp / Macroscope - UI Consistency failed Sep 1, 2026 in 3m 55s

UI Consistency: 2 issues found

  • apps/web/src/components/chat/MessagesTimeline.tsx (WorkEntryIcon, t3-code case): the new icon is a raster <img> (apple-touch-icon.png) inside an icon slot whose color is routed through caller classes (text-icon-muted, text-destructive, highlighted text-foreground) and opacity-70/stroke-[1.8]. The raster cannot express those tones, so the T3 tool icon reads as a full-color app-icon tile among muted monochrome glyphs and does not change in failed or shimmer-highlighted states. The existing T3Wordmark SVG in apps/web/src/components/sidebar/SidebarChrome.tsx already uses fill="currentColor" and is the mark mobile uses for the same icon in this PR.
  • apps/web/src/components/chat/MessagesTimeline.tsx (PlainWorkEntryRow): the new failure XIcon is rendered after the disclosure chevron, shifting the fixed size-4 chevron slot left only on failed rows so chevrons no longer align down an expanded tool group. The mobile row in this PR places the same indicator before the chevron.

Both findings were posted as inline review comments.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Files examined in scope: apps/web/src/components/chat/MessagesTimeline.tsx, apps/web/src/components/chat/MessagesTimeline.logic.ts, apps/web/src/components/ChatView.tsx, apps/web/src/session-logic.ts, plus supporting context in apps/web/src/components/ui/scroll-area.tsx, apps/web/src/index.css, apps/web/src/components/sidebar/SidebarChrome.tsx, apps/mobile/src/features/threads/thread-work-log.tsx, and packages/client-runtime/src/work-log/presentation.ts.

Checked and found consistent:

  • ExpandedWorkGroupEntries uses the shared getVirtualizedScrollFadeClassName contract rather than a component-named mask, keeps dynamic top/bottom fade state from real scroll metrics (onScroll, onLayout, ResizeObserver on the scroll node and its content child), and the virtualized-scroll-fade utility keeps the native scrollbar lane opaque, so scrollbar visibility is preserved.
  • recycleItems is safe here because renderItem keys the child on entry.id, so per-row expanded state cannot leak across recycled containers.
  • canExpand in PlainWorkEntryRow remains equivalent to buildToolCallExpandedBody(...) !== null (MCP toolData, raw/plain command, detail, or changed files), so the new lazy body computation does not create rows that expand into nothing.
  • toolGroupConsumesUpwardNavigation in ChatView only suppresses live-follow cancellation while an inner scroller can consume the gesture; combined with overscroll-y-contain on the group this does not silently strand the outer timeline.
  • No environment-scoped actions or theme/global CSS ownership changes were introduced by this patch.

🔍 Agent activity — 12 tool calls

💭 I need to look at the relevant web files—MessagesTimeline, its logic, ChatView, and session-logic—to check things like how the scroll fade class is generated, whether LegendList forwards className/tabIndex/role props on web, the scrollbar fade geometry, and how WorkEntryIcon is used. I also want to check for an existing shared component for the T3 wordmark/logo since one implementation uses a raw img tag, and examine PlainWorkEntryRow's canExpand logic—there might be a mismatch where canExpand is true but expandedBody ends up null, causing an empty expansion when clicked. Let me trace through buildToolCallExpandedBody to verify.

1. git_tools — getVirtualizedScrollFadeClassName

2. git_tools — buildToolCallExpandedBody

💭 I'm confirming the shared virtualized scroll fade contract is used correctly here, but I want to check the global CSS to ensure the fade preserves the scrollbar lane's opacity so the track and thumb remain visible rather than fading out like the content.

3. git_tools — virtualized-scroll-fade

💭 I want to look closer at PlainWorkEntryRow, specifically the canExpand logic compared to buildToolCallExpandedBody, plus how icon colors and tones shift. Let me check MessagesTimeline.tsx around lines 2280-2620.

4. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 I'm checking whether canExpand being true always implies expandedBody is non-null, tracing through the mcp_tool_call, rawCommand, and changedFiles conditions that each push into blocks. Now I'm also noticing an ordering inconsistency with the failure X icon placement — it appears after the chevron and shares an aria-label even though the shown icon is the browser globe rather than the alert icon, and mobile seems to place it differently. I'm also questioning the T3 icon rendering using apple-touch-icon.png as a UI icon since an img tag can't adopt currentColor theming, and want to check if there's an existing shared brand mark component like T3Wordmark used elsewhere. I still need to look at how LiveActivityContent handles special tool icons and failures, and check the focus-visible ring styling on LegendList. I'm now worried LegendList's web implementation might not forward arbitrary props like tabIndex, role, or data attributes to the scroll element — if so, ChatView's data-tool-group-scroll lookup would silently fail and break wheel handling. I'm trying to find where getScrollableNode is defined or check if other code passes data-* attributes to LegendList to confirm this.

5. git_tools — getScrollableNode|data-tool-group-scroll|LegendListProps

6. git_tools — apple-touch-icon|T3Wordmark|t3-mark|T3Logo

💭 I notice web's SidebarChrome.tsx uses a local T3Wordmark SVG that mobile's comment references, but the new web icon actually uses a raster apple-touch-icon.png instead — meaning the two clients diverge, since the PNG can't inherit currentColor for tone/opacity states like the SVG can. This matters because WorkEntryIcon consumers pass color classes like text-icon-muted, text-destructive, and opacity-70 through className, but an <img> tag ignores all of those, so failed/destructive/muted tones silently won't apply in contexts like LiveActivityContent. I want to check the actual T3Wordmark implementation to confirm this consistency gap.

7. browse_code — apps/web/src/components/sidebar/SidebarChrome.tsx

8. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 I'm noticing that the t3-code icon uses a raster image instead of following the color-indirection pattern other icons use, so it won't inherit the muted/destructive/foreground text colors that callers expect to apply through wrapper classes. Since mobile already uses a currentColor SVG wordmark with proper color/accent props, the fix would be extracting that same T3Wordmark SVG for web so tone theming works consistently rather than duplicating a fixed-color brand asset. This does feel like a legitimate concrete violation worth flagging. Now looking at the X failure indicator placement — in PlainWorkEntryRow the X sits after the chevron, but mobile puts it before the chevron. This actually breaks visual alignment: since the chevron should stay in a fixed rightmost column across all rows, placing an X after it means rows with failures shift the chevron left, misaligning the disclosure affordance column-to-column across a list. I think this is worth flagging as a minor but concrete inconsistency, with the suggested fix being to move the XIcon block before the chevron span to match mobile's ordering. I'm also checking whether the failure X is even necessary given the icon wrapper already has an aria-label, and confirming the iconWrapperClass change — previously hiding the icon in expanded rows to stay compact, now always showing it, which looks intentional based on the docs. I'm now looking at accessibility duplication: failed rows get role="img" aria-label="Tool call failed" on the wrapper span, while the row itself already includes ", tool call failed" in its aria-label when expandable — this duplication seems pre-existing rather than newly introduced. I want to check whether the web LegendList actually forwards unrecognized props like tabIndex, role, aria-label, and data-tool-group-scroll down to its underlying scroll container, since if it doesn't, the keyboard focus ring styling wouldn't have any effect. If props aren't forwarded, that would break the wheel scroll chaining fix ChatView relies on via that data attribute, which is a real concern worth checking against the vendored @legendapp/list source or any patch files in the repo.

9. browse_code — patches/@[email protected]

💭 That wasn't directly relevant, but it confirms the web LegendList likely spreads rest props onto the scroll element, so style and className forward fine for ModelPicker and BranchToolbar. Now I want to check other web-scoped concerns, starting with the overscroll-y behavior. Looking at the overscroll-contain condition, it only applies when fades exist, meaning containment is only active when content is scrollable, which seems correct. But I'm now suspicious about the wheel handler that determines whether a tool group consumes upward navigation—it checks if scrollHeight exceeds clientHeight, but doesn't account for whether the group is already scrolled to the top, so it may keep intercepting upward wheel events even when there's nowhere left to scroll. I need to look at the ChatView code around where this navigation behavior is implemented to understand what "contentScrollsUp" actually does before drawing conclusions.

10. browse_code — apps/web/src/components/ChatView.tsx

💭 These handlers only cancel live-follow, and the suppression logic makes sense: scrolling over a scrollable tool group shouldn't break follow since the inner group absorbs the scroll via overscroll containment, keeping the viewport still. I'm now checking the keydown handler's use of a helper to detect when the inner focusable list should consume upward navigation. I'm second-guessing whether keyboard-triggered scrolling (ArrowUp) respects overscroll-behavior the same way wheel/touch scrolling does, since browsers might chain keyboard scroll to the ancestor once the inner list hits its top — which could leave live-follow suppressed while the timeline actually scrolls, a subtle edge case worth flagging. Shifting to the fade update logic, I check whether ResizeObserver on the scroll element and its firstElementChild correctly reacts to virtualized row height changes, and confirm the useLayoutEffect ref timing is safe since the list ref is always populated by commit time. I also note that maintainScrollAtEnd only auto-follows new entries when already at the end and not mid-expansion, which seems correctly scoped. I'm now looking at how the t3-code icon is rendered — WorkEntryIcon gets fixed sizing and opacity classes, but for this variant it swaps to an <img> with rounded corners and object-cover, using a raster PNG scaled down to 16px instead of an SVG, losing crispness and color inheritance compared to other icons. This raises a discrepancy: docs say other T3 tools keep the T3 mark, and mobile actually renders a currentColor T3 wordmark glyph while web uses this flat PNG app icon, so the two clients diverge visually for the same tool. This seems like a real finding worth flagging: the fix would be extracting the wordmark SVG from SidebarChrome into a shared currentColor component so the icon can properly inherit muted/destructive/highlight tones like every other WorkEntryIcon case, since a raster image can't respond to those color classes at all. I want to confirm this counts as a genuine violation of the shared color-indirection rule before writing it up, since WorkEntryIcon's whole contract depends on currentColor tone propagation. I'm also considering a second candidate: the XIcon placement in PlainWorkEntryRow, since LiveActivityRow duplicates its content into a base layer and an absolutely-positioned highlighted overlay for the shimmer effect, meaning any trailing XIcon inside LiveActivityContent would render in both layers simultaneously. Placing the X icon after the chevron rather than before causes the chevron column to jitter horizontally across rows in an expanded group with failed browser calls, since the X only appears conditionally. The mobile implementation in the same PR puts the failure X before the disclosure chevron, so I should fix this by reordering to match — X before chevron — and suggest a unified diff. Continuing my review, I check that formatWorkspaceRelativePath is still needed elsewhere and confirm it's still used, that canExpand now correctly includes rows with tool data even without detail, and that the expanded body render condition looks consistent. Checking the previously hidden entry icons — the change now makes icons always visible, which matches the intended docs behavior. I'm also verifying the expanded work group's scroll container height and nested scrollable rows work fine with the ChatView's scroll handling. Now I'm thinking about LegendList's item recycling with recycleItems — since rows keep local useState(expanded) state, recycled component instances could retain stale expansion state across different items, which seems like a potential bug worth flagging. Since PlainWorkEntryRow holds its own useState(expanded), a recycled row could visually retain expansion state for the wrong tool call. But I realize the renderItem output has key={item.id}, and React reconciles keyed children by unmounting and remounting when the key changes — so that key should actually force a fresh component instance despite recycling. So this isn't a real bug; ModelPickerContent probably relies on the same pattern. I'm now checking other details like the focusable scroll region's tabIndex and aria labeling, and how tool presentation names get displayed for entries with a friendly label. I'm also confirming import paths and fade class sizes look consistent between mobile and desktop, and noting that browser and web-search icons intentionally share the same globe glyph. Sharing the globe icon between "browser" and "web search" categories is probably intentional rather than a bug, since they're both preview-type actions. I want to double-check the full WorkEntryIcon switch statement to see how "browser" is actually handled there.

11. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 I'm noticing the opacity classes only produce a slight shimmer on the PNG since opacity works on images while stroke doesn't, so the color-based tone distinctions like muted versus destructive text are completely lost. I'm also flagging that using object-cover on a square icon seems off. Missing width/height attributes on the img could cause layout shift, though the size-4 classes probably prevent that, and I confirm Vite's BASE_URL always ends with a slash, so that's fine. My main finding is that the raster app icon rendered inside the currentColor icon slot breaks tone theming and diverges from the existing T3 wordmark SVG used elsewhere. The fix is to render the T3 mark as an inline SVG with fill set to currentColor instead, likely by extracting a shared component from SidebarChrome's T3Wordmark so all callers' tone classes apply correctly. I'm weighing whether to give a minimal self-contained inline suggestion versus recommending a proper shared-component extraction — the smallest fix is replacing the img tag with an inline SVG using the same path data as the existing wordmark, copied from SidebarChrome, while noting extraction as the more durable long-term solution. For the second issue, since moving the XIcon block to fix the alignment mismatch would require two separate edit hunks (removing lines further down and adding them earlier), I'm deciding a unified diff isn't clean here, so I'll just describe the needed change instead of providing an actual diff. Given it's a real cross-client layout inconsistency introduced within this PR, I'll still report it as a suggestion worth flagging. Since findings exist, and the check defaults to failure when findings are present, I'll mark the state as failure rather than "all clear." Reconsidering finding 1 once more: the colored app-icon square doesn't fit among the monochrome muted glyphs and can't express the status tones the rest of the code computes, so I'm confident flagging it — let me verify the muted-icon text class usage to be sure. I'm also checking whether the PNG icon disrupts the shimmer overlay's masking behavior — since the base layer is tinted/muted while the overlay renders at full opacity, the icon would just pulse in opacity rather than shift tone properly, which is worth noting as a minor issue. Now I'm drafting the review comment explaining that WorkEntryIcon routes its color through the caller's class. Since mobile places the same indicator before the chevron, moving this block above the chevron span keeps the chevron as the rightmost, aligned affordance. I'll confirm both comments target added lines in the diff (2584 for the XIcon line, 2221 for the img src line) and post the review with these two inline comments and a null body.

12. modify_pr — Two icon-presentation findings in the web timeline rows; both are inline.


Agent Credits: 196 credits

Token usage: 20 uncached input · 981,874 cache-read input · 161,548 cache-write input · 14,419 output
Agent Credits may also include non-token charges from external tools such as web research.