Skip to content

fix(web): new thread title sits closer to the composer

MacroscopeApp / Macroscope - UI Consistency failed Aug 24, 2026 in 6m 32s

UI Consistency: 1 new finding, 1 unresolved

New finding (posted inline)

  • apps/web/src/index.css (766-772): the new .chat-draft-hero-headline and .chat-composer-stack:has(.chat-composer-shoulder-tab) rules are one-owner spacing added to global CSS. Each selector has exactly one consumer in ChatView.tsx (6784 / 6788) and .chat-composer-stack carries no declarations of its own — it is only a :has() hook. The same ancestor condition is expressible at the owner with Tailwind's group-has-[...] variant, which this codebase already uses (SidebarProviderUpdatePill.tsx, Sidebar.tsx:1272).

Still unresolved from the previous run (not re-posted)

  • apps/web/src/components/ChatView.tsx (1247-1255, 4298-4325): shoulderTabReserve() measures mainSurface.top - form.top, but the composer top drawer (approvals / pending question / plan follow-up) and ComposerTasksDrawer are siblings inside [data-chat-composer-form] that precede the div.relative holding [data-chat-composer-main-surface], so their height is counted as shoulder reserve. When a top drawer is attached, showShoulderTabs is false (no pt-7 reserve exists) and hasWideShoulderTab is false unless a task plan happens to be active, so scrollToEndClearance drops by the full drawer height and the z-30 "Scroll to end" pill overlaps the drawer. Head 82f5969 did not change this code.

Verified as correct

  • left-5.5 / right-5.5 / right-30 are valid Tailwind v4 dynamic spacing values (22px inset matching the composer's 22px corner radius and the context strip).

  • MAX_TASK_SEGMENTS suppression leaves no empty track: TaskSegments returns null before the w-10 / w-20 wrapper renders, and the new test covers both placements.

  • The :has() gate aligns exactly with the form's pt-7 reserve: ComposerStashBadge returns null at count === 0 and the tab-placement ComposerTasksBadge returns null at totalSteps <= 0, matching hasShoulderTab.

  • Because externalComposerDrawerAttached includes composerBannerItems.length > 0, shoulder tabs never coexist with the hero banner stack, so the reduced headline padding cannot be applied to the wrong gap.

  • Moving the measuring useLayoutEffect later in ChatViewContent is safe: it only reads DOM, and other layout effects in the same commit observe the pre-update composerOverlayHeight either way. contentInsetEndAdjustment still uses the full overlay height.</AC_summary><AD_details>Methodology

  • Reviewed merge base e6a109b -> head 82f5969; the only content new since the last run is commit 82f5969 (draft hero headline padding), so review focused there while confirming the earlier finding was not addressed.

  • Traced [data-chat-composer-form] / [data-chat-composer-main-surface] child order in apps/web/src/components/chat/ChatComposer.tsx (form 2874-3517: top drawer 2902-3014, ComposerTasksDrawer 3015-3025, div.relative 3026 containing tab badges then the main surface).

  • Confirmed showShoulderTabs (2437-2441) and hasShoulderTab (2442-2447) plus the conditional pt-7 on the form (2899).

  • Confirmed the layer context of the new CSS (inside @layer components, 573-1360), so a call-site utility would win the cascade.

  • Grepped for other consumers of .chat-draft-hero-headline, .chat-composer-stack, DraftHeroHeadline, and group-has-[ to establish single ownership and existing repo precedent.

Files examined: apps/web/src/components/ChatView.tsx, apps/web/src/components/chat/ChatComposer.tsx, apps/web/src/components/chat/ComposerTasksBadge.tsx, apps/web/src/components/chat/ComposerTasksBadge.test.tsx, apps/web/src/components/chat/ComposerStashBadge.tsx, apps/web/src/index.css.</AD_details>}INVALID_JSON_UNPARSEABLE,请重新输出合法 JSON。 correction: the previous message was malformed. Reissuing now.</AD_details>. Let me output valid JSON.
{

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.

...


🔍 Agent activity — 21 tool calls

💭 I want to check whether the previously flagged issue about shoulderTabReserve including drawer heights in ChatView.tsx around line 1255 was actually addressed. I'm comparing the current commit 82f5969 against the prior run's commit 0377b47 to see what changed in that area.

1. git_tools — 0377b47..82f5969

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

💭 Since the code is unchanged from the prior run and that issue was already flagged at the same location, I shouldn't duplicate the comment. I want to double check the prior finding is still valid given the head commit differs, and look at the composer structure to confirm there aren't other issues worth flagging.

3. git_tools — data-chat-composer-form|data-chat-composer-main-surface

4. git_tools — chat-composer-stack|chat-draft-hero-headline

💭 I'm looking at whether padding rules for the headline element, including one that depends on a sibling's presence via :has(), fit within the guideline that single-owner styling should live as Tailwind classes in the component itself. The sibling-conditional rule seems hard to express purely with Tailwind, so I'm weighing whether it needs to stay in CSS. I'm weighing whether this is really worth flagging as a violation, since it's a plausible but somewhat pedantic finding, and I want to check the surrounding CSS context around that block before deciding.

5. browse_code — apps/web/src/index.css

💭 I'm checking whether the shoulder tab sits inside the composer overlay by examining how the chat-composer-stack wrapper is structured around that section of the JSX.

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

💭 I'm confirming the shoulder tab sits inside the composer stack so :has() selectors will catch it, and that the draft hero headline class only ever applies in that no-messages state, while badges like stash can still coexist there. I'm also weighing whether this spacing logic belongs at the Tailwind call site instead—using a group class on the stack with group-has-*, or the newer in-* variant—versus keeping it as new global CSS. Since this is really one-owner spacing being moved into a shared stylesheet rather than genuinely shared behavior, it seems to violate the principle that ordinary single-owner presentation should stay as static utility classes at the owning component. I want to check whether ChatView actually has visibility into all shoulder-tab types before assuming the component can just compute this itself—so I'm tracing where ComposerStashBadge gets rendered to see if that state is already available upstream.

7. git_tools — ComposerStashBadge|ComposerTasksBadge

💭 Since ChatView doesn't directly know about the shoulder tab nested in ChatComposer, CSS :has() becomes a reasonable cross-component solution here, weakening the ownership argument. Tailwind could still express this using the group-has-* variant by marking the stack as a group and targeting the descendant condition. Given the cross-component complexity and the instruction not to fail for aesthetic preferences, I'm leaning toward not flagging this spacing issue. Now I want to verify whether the previously flagged shoulderTabReserve concern about drawer siblings affecting the surface-to-form gap is still valid by checking the actual ChatComposer structure — since the code is unchanged from the prior run, I shouldn't repost the finding but should confirm it's genuine before including it in the summary.

8. git_tools — data-chat-composer-form|data-chat-composer-main-surface|ComposerTasksDrawer|shoulder

💭 The grep failed because of binary detection, so I'll try searching the file differently to locate the JSX form element.

9. browse_code — In apps/web/src/components/chat/ChatComposer.tsx, show the JSX structure of the element with data-chat-composer-form="true" — list its direct children in order (including any top drawer such as approvals, pending questions, plan prompt, ComposerTasksDrawer, and the wrapper div containing data-chat-composer-main-surface="true"), and where ComposerTasksBadge (tab placement) and ComposerStashBadge are rendered relative to those. Quote the relevant JSX with line numbers.

💭 Since shoulder tabs aren't rendered when a top drawer is attached, I need to check whether shoulderTabReserve, measured as surface.top minus form.top, ends up including the drawer height plus the form's own padding-top since the bounding rect includes the border box. The regression is confirmed: when a top drawer is present and hasWideShoulderTab is false, scrollToEndClearance underestimates by drawerHeight, overlapping the pill with the drawer. I'm also noticing hasWideShoulderTab in ChatView is only a proxy based on task progress state, not actual shoulder tab visibility—it's conservative when tasks progress exists but the drawer is open, but when only the stash badge shows (progress null, showShoulderTabs true), the reserve incorrectly falls back to just the 28px padding value. I want to double-check whether the odd fractional spacing values like right-5.5, left-5.5, and right-30 are valid in whatever Tailwind version this project uses, since v4 handles arbitrary spacing multiples differently than v3. I also want to verify how the task segment cap of 10 interacts with the container's width classes—specifically whether dropping segments beyond the max leaves a leftover empty gap that a test might catch by checking for stray width classes. I'm going to open the ComposerTasksBadge component to check.

10. browse_code — apps/web/src/components/chat/ComposerTasksBadge.tsx

11. git_tools — "tailwindcss"

💭 I'm confirming the shoulder tab and stash badge positioning values are valid in Tailwind v4's dynamic spacing scale, adjusting the offsets slightly to align with the glass shell's rounded surface, and checking the conditional logic for trailing shoulder spacing. Now I'm looking at the MAX_TASK_SEGMENTS cap for tasks over 10 steps, verifying the segment widths render reasonably thin but acceptable at that threshold, and reducing padding on the chat draft hero headline styling. The gap shrinks from 2rem to 1rem when a shoulder tab is present, making room for the 28px tab element. Now I want to verify which CSS layer this rule block actually falls under, since that affects specificity and cascade order.

12. git_tools — ^@layer|^@Utility|^@variant|^}

💭 I confirm that .chat-draft-hero-headline sits in the components layer, so with pb-8 removed there's no conflict with utility classes anymore. I'm now checking if the repo has an existing convention for state-dependent spacing like this. Since the previously flagged bug is still unresolved but unchanged, I shouldn't repost that comment per the duplicate-finding guidance — though the overall conclusion should still reflect failure since the issue remains open. I'm also verifying that contentInsetEndAdjustment still correctly uses the full composer overlay height, and checking the initial value of scrollToEndClearance versus composerOverlayHeight to see if there's a timing issue before measurement completes.

13. git_tools — composerOverlayHeight|setComposerOverlayHeight|activeComposerTaskSteps|activeComposerTasksProgress

💭 I'm concerned that moving this layout effect later changes its execution order relative to other layout effects—layout effects run in declaration order, so if other effects between the old and new position read composerOverlayHeight, they might now run before that value gets measured, breaking their dependency. Actually, since React batches all layout effects in a commit before re-rendering, other effects still capture the same closure with the old height value regardless of ordering—so the reordering shouldn't cause a functional regression here, since this effect only reads the DOM rather than mutating it. The new dependency on hasWideShoulderTab just means the observer reconnects when tasks appear or disappear, which seems fine. I'm also checking the shoulderTabReserve query against the composer form selector in overlay states like the draft hero or mobile-collapsed layouts, which look okay. There's a subtle edge case though: when hasWideShoulderTab is false but a stash tab is still present, the reserve defaults to 28px (pt-7), potentially causing the pill to sit lower and overlap the stash tab region on narrow screens—though this seems like a minor issue. I confirm pt-7 is applied to the form itself when a shoulder tab exists (matching the intended 28px reserve), and when a top drawer is present instead, showShoulderTabs is false so pt-7 doesn't apply, but the drawer's height itself creates the necessary offset since it's rendered as a child before the relative container. Now I'm tracing through the case where a top drawer is open alongside a wide shoulder tab (like when the tasks drawer is open with progress present)—clearance ends up being the full height, which is correct. But when hasWideShoulderTab is false in that same drawer-open scenario, clearance becomes height minus drawer height, meaning the pill actually overlaps the drawer. That confirms the bug I flagged earlier: specifically, a pending approval panel, pending question, or plan follow-up banner rendered without an active task plan causes this real overlap regression. I also want to check whether externalComposerDrawerAttached affects this — need to determine if that drawer renders inside or outside the composer form, and then compare against the prior commit to see if this behavior actually changed or was just carried over through a rebase.

14. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/commits

💭 Since this new commit is the only change since the prior run, I should focus my review there rather than repeating earlier findings. The commit reserves fixed padding above the composer for the headline, reducing it when a shoulder tab occupies that space—but I need to verify whether the shoulder tab's absolute positioning actually places it within that band relative to its container. Working through the math, the padding reduction doesn't produce exactly equal visual gaps in both states, though it claims to restore about half. I'm now checking whether the :has() selector's matching logic aligns precisely with the actual conditions under which the shoulder tab renders and reserves space, since there could be edge cases where they diverge. I'm also weighing whether moving this spacing rule into global CSS constitutes a genuine ownership violation, since the guidance discourages preserving one-owner presentation logic in index.css when it could instead live as static Tailwind classes in the owning component — and this case is the inverse of the usual complaint, where simple local spacing got pushed into the global stylesheet rather than pulled out of it. That confirms the variant is valid syntax-wise. But I should double check whether this counts as an in-contract violation versus an aesthetic nitpick — since the CSS-ownership rule is explicitly part of what I'm checking, a newly introduced single-owner spacing rule qualifies. I also want to grep the repo to see if group-has- variants are already used elsewhere as precedent, and check the existing .chat-composer-context-strip inset value referenced in that earlier commit. The 22px inset matches, confirming that detail is consistent. Now I'm tracing whether .chat-composer-stack wraps both the docked and hero composer states (it does, same div), and looking at whether the headline's padding actually separates it from the composer banner stack or just from something else in the layout order. The tighter headline padding is meant to account for the shoulder tab spacing, but that tab sits below the banner stack, not directly under the headline — so when banner items exist (like environment warnings), that padding is actually the gap between headline and banners, not headline and tab. This creates a layout coupling issue: shrinking that gap for an unrelated reason shifts the visual spacing between headline and banners even though the total column height still adjusts correctly overall. I want to check how likely banners actually are in the draft hero state, so I'm going to look at how ComposerBannerStack computes its items.

15. git_tools — composerBannerItems

16. git_tools — group-has-[

💭 I'm realizing the shoulder tabs only appear when there's no external drawer attached, and banners count as an external drawer. So when banners are present, the shoulder-tab selector won't match anyway, which means my earlier concern about banner spacing conflicting with that padding rule doesn't actually apply. That said, adding two new global classes purely as a :has() hook for spacing does raise an ownership question, since the codebase already has precedent for using group-has-[...] variants scoped at the owning component instead. I'm leaning toward flagging this as a low-severity ownership finding rather than something that fails the check outright, since the rule is meant to catch real regression risk rather than optional style preferences. That said, the shoulder-tab clearance bug I found earlier still stands on its own and justifies a failure: when a top drawer is present but there's no active task plan, the pill's clearance calculation subtracts the drawer height from the reserved space, causing an overlap. I'm now confirming this happens specifically in cases like a pending user input panel showing without an active task plan. Since the head commit didn't touch shoulderTabReserve, I shouldn't repost that same finding — it's already flagged. Now I'm weighing whether the CSS ownership issue on those index.css lines is a real, defensible finding or just noise, since the class in question spans multiple components rather than being owned by a single one, which complicates the usual "smallest correct owner" rule. I'm inclined to raise it, but gently, since the check explicitly says not to fail over optional aesthetic preferences — so I'm looking for a stronger, more concrete issue elsewhere, like the mobile view transition padding change or a possible specificity/cascade conflict in the draft styles. I also want to check whether there's a duplicate render path — there seems to be a second copy of the hero headline structure elsewhere in the file, so I need to verify whether that branch also applies the same padding class or if it diverges.

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

💭 I'm noticing the grep results don't match what I'm seeing when viewing the file directly, which makes me suspect the grep was run against a different revision than the one I'm currently viewing—likely because I passed a specific tree ref for those searches. Since the line numbers conflict with what's shown around 6380-6470, I should re-run a targeted grep for DraftHeroHeadline to pin down all its occurrences.

18. git_tools — DraftHeroHeadline|ComposerBannerStack className

💭 That confirms only one render path exists, so those earlier line numbers must have come from a different revision reference. I want to double-check that the external composer drawer logic (which includes banner items) properly excludes shoulder tabs when a drawer is attached, so I'm looking at how that drawer flag gets passed through the ChatComposer code.

19. browse_code — In apps/web/src/components/chat/ChatComposer.tsx at ref 82f5969, quote lines 2425-2450 and show the exact definitions of showShoulderTabs and hasShoulderTab, including all conditions (external drawer attached prop name, top drawer, tasks drawer open, collapsed mobile), and quote line 2895-2901 showing the form element's className with pt-7.

💭 I notice ComposerStashBadge renders whenever showShoulderTabs is true, regardless of stashQueue.length, so I need to check whether it returns null when count is 0 to determine if the shoulder tab actually appears in the DOM.

20. browse_code — apps/web/src/components/chat/ComposerStashBadge.tsx

💭 I'm confirming the :has() selector matches exactly when the shoulder tab is present, so the pt-7 padding logic stays consistent, and checking whether the badge's negative positioning slightly overlaps the surface by 4px, which is pre-existing behavior. I'm now weighing whether this CSS :has() approach introduces any real behavioral issue. I also confirm the shoulder tab can only appear during a draft hero state via the stash queue, not task progress, matching the commit's intent. Turning back to the ownership question for index.css, I'm leaning toward treating the padding-bottom as single-owner spacing that should live in the owning TSX module rather than global CSS. Given that neither rule is genuinely reusable or behaviorally complex — the :has() logic could be replaced with the codebase's existing group-has-* Tailwind pattern — this reads as an ownership violation worth flagging. I'll draft an inline comment on index.css around lines 766-772 suggesting the fix: add a group/composer-stack class to the stack container and replace the CSS rule with a Tailwind group-has-* modifier on the headline, while also noting the corresponding cleanup needed in ChatView.tsx. I'm now working out exact diff line numbers for the added CSS block to anchor the comment correctly. So the finding is really about ownership placement rather than eliminating :has() altogether, since group-has-[...] at the owner level preserves the same selector semantics while being Tailwind-native. That's a fair, narrow ask consistent with repo precedent, so I'll post it. I'm drafting the comment now, noting that these two rules are one-owner spacing for a single call site since both classes have exactly one consumer. Since this touches two files, I need to present it as diff snippets rather than an inline suggestion, per the rule against diffs for multi-hunk cross-file changes — though I'm second-guessing whether that restriction actually applies cleanly here.

21. modify_pr — One finding on the new draft-hero headline commit: the added spacing rules are one-owner presentation that belongs at the call site rather than in index.css. The shoulderTabReserve / scrollToEndClearance issue flagged on the previous run (top-drawer height is counted as shoulder reserve, so the z-30 "Scroll to end" pill lands on top of an approval / pending-question / plan-prompt drawer whenever there is no active task plan) is unchanged in this head, so it still stands.


Agent Credits: 207 credits