Skip to content

feat(claude): compact old threads before they burn through usage

MacroscopeApp / Macroscope - UI Consistency failed Aug 24, 2026 in 3m 53s

UI Consistency: 1 issue found

1 finding

  • apps/web/src/components/chat/ChatComposer.tsx (lines 2951-2976): the new resume-compaction notice is a hand-rolled banner (border-l-2 border-primary/60) rendered as a direct child of the composer form, bypassing the established ComposerBannerStack / ComposerBannerStackItem (Alert + AlertAction, variant tints, dismiss transition, attached glass) contract used for every other composer notice. Because it sits inside .chat-composer-glass-shell / .chat-composer-glass-host it is full-bleed with no --chat-composer-drawer-inset, so its 2px left border crosses the shell's 22px rounded edge; it also opens the drawer seam maintained by .chat-composer-drawer-slot + :has(.chat-composer-top-drawer) .chat-composer-top-drawer::before when a top drawer is visible, has no role="alert", and is not gated on isComposerCollapsedMobile like the other drawer contents. Suggested fix: emit a ComposerBannerStackItem (variant: "info", title/description, Compact/Keep actions, onDismiss) into the existing ComposerBannerStack in ChatView, or render it on the chat-composer-top-drawer surface with matching inset and mobile gating.

An inline review comment was posted on the affected range.

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 in scope reviewed

  • apps/web/src/components/chat/ChatComposer.tsx
  • apps/web/src/components/chat/ContextWindowMeter.tsx
  • apps/web/src/components/chat/ContextWindowMeter.logic.ts (+ test)
  • apps/web/src/lib/contextWindow.ts (+ test)
  • apps/web/src/components/settings/ProviderSettingsForm.test.ts

Checked and not flagged

  • ContextWindowMeter's new Compact context control uses the shared Button primitive with existing size="xs" / variant="outline" and only contextual layout classes (mt-1 w-full); no primitive geometry, focus ring, or state colors are overridden, and disabled is forwarded so the primitive's disabled:pointer-events-none / opacity behavior is preserved.
  • The optional onCompact / compactDisabled props are threaded through ComposerFooterPrimaryActions without widening the primitive contract, and the Claude-only gating is done by conditional prop spread at the call site rather than in the primitive.
  • formatContextWindowCompactionMessage gains an optional threshold argument with covering unit tests; no theme tokens, global CSS, cascade layers, variants, masks, or scroll/virtualization behavior were touched by this PR, so emitted-CSS and scroll-fade gates were not applicable.
  • useNowMinute reuse matches its documented single-timer contract already used by ChatView and Sidebar.

Methodology

Compared the new composer notice against the existing composer notice owners (ComposerBannerStack, ComposerPlanFollowUpBanner, chat-composer-top-drawer) and the related index.css surface rules (lines 789-947, 1045-1133, 1632-1668) to confirm surface, inset, seam, and theme ownership; traced the render position through ChatView (chat-composer-glass-shell / chat-composer-glass-host wrappers) to confirm the geometry interaction.


🔍 Agent activity — 20 tool calls

💭 I want to check the changed web files—ChatComposer.tsx, ContextWindowMeter.tsx, ContextWindowMeter.logic.ts, and lib/contextWindow.ts—to verify things like whether the "ghost-muted" button variant and "xs" size actually exist, whether the useNowMinute hook is defined, and whether import paths are consistent (relative vs aliased).

1. browse_code — apps/web/src/hooks/useNowMinute*

2. browse_code — apps/web/src/components/chat/ContextWindowMeter.tsx

💭 I should check if "xs" size and "ghost-muted" variant exist on the Button component. I'm also worried about the popover behavior—if it's a hover-triggered popover with pointer-events disabled for tooltip styling, a button inside it might be unclickable since the popover could close or block interaction when the cursor moves toward it. I want to check popover.tsx to see how tooltipStyle handles pointer events.

3. browse_code — apps/web/src/components/ui/button.tsx

4. browse_code — apps/web/src/components/ui/popover.tsx

💭 The popover stays open after clicking "Compact context" since it's an interactive Base UI popover, not a tooltip, though the compactDisabled state kicks in during running—minor but worth noting. Now I'm moving on to check the ChatComposer changes, starting with the import path for useNowMinute. I want to see if there's an existing shared banner/alert primitive in components/ui that this new notice div should be using instead, plus check whether ChatComposer already has similar notice patterns elsewhere (like a queued-messages banner) that follow a consistent style. The ghost-muted button variant looks fine since it's already an established pattern. Let me pull up the relevant ChatComposer sections to compare.

5. browse_code — apps/web/src/components/ui/*.tsx

6. git_tools — border-l-2|border-primary/60

💭 There's an Alert primitive in the ui folder I should check, and I want to see if ChatComposer already has similar inline notice banners I can follow the pattern of.

7. browse_code — apps/web/src/components/ui/alert.tsx

8. git_tools — data-chat-composer-|mb-2 flex items-start

💭 I want to compare how this new notice is implemented against existing patterns like the chat-composer-top-drawer, so I'm checking ChatComposer.tsx around lines 2900-3020 to see similar notice rendering logic.

9. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 I'm noticing the new resume compaction notice uses an ad-hoc banner style instead of the existing chat-composer-top-drawer pattern already used for other composer notices like pending approval or plan follow-up — that's an inconsistency worth flagging. I want to check index.css and the ComposerPlanFollowUpBanner component to confirm how the shared drawer contract is structured.

10. git_tools — chat-composer-top-drawer

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

💭 I notice the composer already has an established theme-aware surface pattern for its "top drawer" elements, but the new resume compaction notice instead uses a bare div with an ad-hoc left border treatment placed directly in the form above the drawer — inconsistent with the existing styling contract. There's also a layout concern: since showResumeCompactionNotice can be true while a pending approval drawer or shoulder tab is also showing, the notice's mb-2 combined with the drawer's negative bottom margin creates gap issues since it's positioned before the drawer rather than accounting for that margin interaction. I'm also checking whether the form sits inside the glass shell, since that affects whether this notice would render correctly when a shoulder tab is present with its top padding. Beyond that, I'm going through smaller issues: the notice's buttons default to type="button" so they won't trigger form submission, which is correct; the compact disable logic on ContextWindowMeter correctly checks running/busy/connecting states; the conditional spread for onCompactContext only applies for the claudeAgent provider; and the notice lacks an aria-live or role="alert" unlike the shared Alert component, which is a minor accessibility gap. I'm also noticing the Compact button inside the popover doesn't close the popover on click since there's no PopoverClose wrapper, so it stays open with a disabled button afterward — worth checking if other popovers in the codebase close automatically after action buttons. Separately, I want to verify the useNowMinute hook's return format since the code interpolates it directly into an ISO-like timestamp string.

12. browse_code — apps/web/src/hooks/useNowMinute.ts

13. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 The doc comment referencing "composer banner" for useNowMinute suggests there might already be an existing banner pattern I should reuse rather than building something new. I want to check for existing consumers of useNowMinute, especially something like a chat composer banner stack, to see if there's a component I should hook into for this notice.

14. git_tools — useNowMinute|chat-composer-banner-stack-cap|Banner

💭 There's already a shared ComposerBannerStack/ComposerBannerStackItem pattern used above the composer for notices with actions and dismiss, and the new resume-compaction banner should be using that instead of hand-rolling its own inside ChatComposer. I want to check ComposerBannerStack to confirm it supports actions and dismissal before flagging this.

15. browse_code — apps/web/src/components/chat/ComposerBannerStack.tsx

💭 I've confirmed the notice contract uses ComposerBannerStackItem within ComposerBannerStack, which handles variants, dismissal, animation, and theming consistently. But the new resume-compaction notice is instead hand-built inline in the composer form as a plain div with custom border styling, missing accessibility roles like alert or aria-live, lacking an icon, and using bespoke typography—placing it above the top drawer at full form width instead of following the established pattern. That's a solid first finding: this notice should reuse the ComposerBannerStack contract instead of being ad-hoc, since it visually overhangs the drawer/banner surfaces and can stack awkwardly alongside the existing banner stack and top drawer, creating three inconsistent notice treatments at once. For a second finding, I'm now examining the "Compact context" button inside the hover-triggered popover in ContextWindowMeter, checking whether Base UI's openOnHover interaction properly keeps the popup open when the pointer moves from the trigger into the popup itself. I want to verify this against other examples in the codebase of interactive elements inside openOnHover popovers, so I'll search for other usages.

16. git_tools — openOnHover

💭 I'm noticing that other openOnHover popovers in the codebase are purely informational, unlike ContextWindowMeter's popover which uses closeDelay 0 and would need to house a clickable "Compact context" action—putting an interactive button inside a hover-triggered popup like that seems risky, especially since touch devices don't have hover at all. Actually, since the popover opens on click too, keyboard users can tab to the trigger and press Enter, then tab into the popup — so it's reachable, weakening that accessibility point. The stale-state issue after clicking "Compact context" feels like a softer, optional finding. The more promising lead is checking whether formatContextWindowCompactionMessage drops the model name when autoCompactThreshold is present, so I want to look at that formatting logic for wording consistency.

17. browse_code — apps/web/src/components/chat/ContextWindowMeter.logic.ts

18. browse_code — apps/web/src/lib/contextWindow.ts

💭 I notice a minor copy inconsistency: the notice uses compact token formatting while the Claude-side dialog uses full toLocaleString formatting. I'm now checking the ChatComposer blocks to see how the notice interacts with the phase state and whether Button is properly imported. Digging deeper, I see the "Compact" button triggers onSend with "/compact" text, and compactThreadContext overwrites promptRef.current, silently discarding any draft the user had typed — that's more of a functional regression than a UI-consistency issue, so I'll keep my report focused on the two strongest violations: the ad-hoc composer notice bypassing the ComposerBannerStack/Alert contract (missing role="alert", no dismiss contract, misaligned width), and the ContextWindowMeter placing an interactive button inside a tooltip-styled hover popover that doesn't match how hover popovers are used elsewhere in the codebase. Actually, this seam relationship depends on the top drawer being the form's first element directly under the banner stack — inserting a notice above it with its own margin breaks that attached visual seam, creating an unwanted gap between the banner stack and the drawer. This confirms a real, citable regression with a visible open seam. I'm also noticing the notice doesn't check for mobile collapsed composer state like the top drawer does, so it still renders full-width above a collapsed composer, and its width doesn't match the inset drawers on either side—another visible inconsistency. Confirmed the notice's visibility condition doesn't account for mobile-collapsed state, which is a supporting detail. I'll draft one primary inline comment on the ChatComposer lines, and I'm weighing whether to add a lighter secondary comment about the ContextWindowMeter's hover popover — specifically whether putting the only interactive control behind a hover-triggered popup with a tiny gap and short close delay could break accessibility on coarse-pointer devices. Since I'm not certain how Base UI's Popover handles hover interactivity internally (whether it uses safePolygon-style hover handling), and the instructions say not to speculate without evidence, I'll drop the popover-hover point and stick to reporting only concrete, verifiable violations. The remaining details I check are minor — a redundant justify-center class and the lack of a PopoverClose after clicking the compact button — neither rising to a real issue. The one substantive finding is the ad-hoc composer notice, which counts as a consistency risk, so I'll fail the check on that basis after verifying the composer's banner stack structure in ChatView to make sure my claim about it is accurate.

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

💭 I'm tracing where the notice renders within the composer's glass shell structure, noting it sits inside the rounded glass surface above the top drawer, but need to check whether the shell's background is drawn in a way that would visually clash with the notice. I'm noticing that the notice's left border line, positioned flush at x=0 with no inset padding, would run right through the shell's rounded corner curve since there's no overflow clipping applied — the 2px primary-colored line would visibly cross over the outline rather than following the 22px radius, which is a real visual defect if the glass-host container doesn't clip its contents. I should also flag that when a top drawer is present, this breaks the assumed flush seam between the drawer and banner above it. My suggestion: surface this through the existing ComposerBannerStackItem pattern (variant "info" with title/description/actions) or at minimum route it through the existing top-drawer surface, since this is a cross-file, multi-hunk change with no inline diff suggestion possible. I'll keep this as one focused comment rather than splitting into a separate note about the mobile-collapsed case.

20. modify_pr — One consistency finding in the changed web UI: the new resume-compaction notice is a hand-rolled banner inside the composer glass shell instead of the existing composer banner/drawer surface contract. Details inline.


Agent Credits: 139 credits