Skip to content

fix(web): keep the working timer in the timeline

MacroscopeApp / Macroscope - UI Consistency failed Aug 31, 2026 in 6m 38s

UI Consistency: 3 issues found (2 still open)

Posted 3 inline findings on the restored composer banner/surface system.

  • apps/web/src/components/chat/ComposerBannerStack.tsx — notice icon and action/dismiss controls are bound to the title Row while the description renders as a separate row, so Compact/dismiss align to the title line instead of being centered against the title+description block (the alignment regression that caused #8693 to be reverted); banner edge padding also drops from px-3 sm:px-4 to p-1 on ComposerBanner.Root.
  • apps/web/src/components/chat/ComposerBanner.tsx — Attachment hardcodes the drawer inset (w-[calc(100%-2.75rem)]) while the PR deletes the only declaration of --chat-composer-drawer-inset, which ComposerCommandMenuLayer still reads via getComputedStyle (ChatComposer.tsx:233); that read now always falls back, leaving two unsynchronized owners of the same geometry.
  • apps/web/src/components/chat/MessagesTimeline.test.tsx — the "aligns the iconless Thinking row with the working timer" test was deleted, but the follow-up commit restored the working/thinking rows and the asserted markup still renders, so the restored row is unguarded while neighbouring row indentation changes.

Still open from the previous head (not re-posted): ComposerBanner.ToggleIcon inheriting buttonVariants' coarse-pointer hit-target pseudo-element inside Row render={<button/>}, and ComposerBanner.Body dropping the clipped CollapsiblePanel's end/bottom padding so option-button focus rings are shaved.

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.

Scope: apps/web/src/**/*.{ts,tsx,css} in the merge-base→head diff, plus directly affected consumers.

Verified as not findings:

  • Tailwind translation of the deleted index.css composer rules — before:bg-[color-mix(...)] + before:bg-[linear-gradient(...)] map to background-color / background-image and reproduce the original layered background shorthand; dark: and html[data-theme-id] arbitrary variants flatten to the same selectors as the removed @variant dark / theme blocks, and the theme outline override stays default-variant-only.
  • No orphaned references remain to the removed .chat-composer-glass-shell, .chat-composer-drawer-*, .chat-composer-top-drawer, .chat-composer-shoulder-tab, or .chat-composer-banner-stack-cap classes (only --chat-composer-drawer-inset, reported above).
  • ComposerBanner.Scroll → ScrollArea preserves keyboard scrolling and dynamic overflow fades (Base UI viewport owns focus ring and --scroll-area-overflow-* masks), so replacing the tasks list's native tabIndex={0} scroller is not a regression.
  • ComposerStashBadge/ComposerTasksBadge shoulder tabs remain semantic rows rendered through Row render={<button/>} and keep aria-expanded, aria-label, and pointer-down focus preservation.
  • The second commit's WorkingTimelineRow/WorkingTimer restoration is byte-identical to main, so only its missing test is reported.

🔍 Agent activity — 22 tool calls

💭 I need to check what happened between the prior review at 39283f4 and the current head at 9ec8bab, so I'll compare the diff to see if earlier feedback was addressed and spot anything new.

1. git_tools — 39283f4..9ec8bab

2. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}

💭 The old commit I was referencing is gone due to a force-push, so I need to check the current commits instead. The PR body mentions isWorking and activeWorkStartedAt being added to ChatComposer, but the actual diff only shows threadSyncPhase being added, suggesting the head has diverged from what I was comparing against.

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

4. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/comments

💭 Since the earlier comments are still valid at the head commit, I don't need to repeat them. What's new is the second commit adding "keep the working timer in the timeline" by juliusmarminge, so I should look at that diff specifically since the base comparison commit isn't available.

5. git_tools — 9ec8bab

💭 The head commit brings back the "working" row in the timeline, but the diff I was given doesn't show WorkingTimelineRow being added—it only shows removals in MessagesTimeline.tsx. That suggests the merge-base might have already contained that row, so I need to check what the merge-base version actually looked like.

6. git_tools — apps/web/src/components/chat/MessagesTimeline.tsx 8b817cb..9ec8bab

💭 This diff seems odd — it removes the early-return branch for summary-only rows while keeping the rest of the function structure, and I need to check if the indentation and control flow still hold together properly after that removal. I'm also tracing whether the "working" row type existed identically in the base version of MessagesTimeline.logic.ts, since the diff doesn't show it being newly added — that suggests the restoration in commit 2 matched the original exactly. But there's a removed test that verified specific spacing classes on the iconless Thinking row alongside the working timer, and I want to confirm whether that class actually still applies in the head version's WorkingTimelineRow, since the component diff doesn't show any changes there. I want to double-check whether the timeline "working" row and composer activity now duplicate, but they seem fine since composer only shows sync phase while timeline shows working. Now I want to shift focus to hunting for new UI-consistency issues in the head, particularly across ComposerBanner.tsx, ComposerSurface.tsx, ComposerActivityStatus.tsx, and ComposerServerUpdateStatus.tsx, since two issues were already flagged previously and remain unfixed. I should go look at these files directly at head rather than speculating further.

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

💭 I'm checking that the arbitrary duration value works in Tailwind v4, then looking at how the stack layout reverses the flex column order with the peek item positioned absolutely at the bottom. Sorting with toSorted should be stable so ordering concerns aren't a real issue. I'm also verifying whether translate-y utilities use the transform property or the newer translate property in Tailwind v4, since that changes how the exit transition combines with opacity. Confirming that v4 splits rotate/scale/translate into separate CSS properties rather than one shared transform, so the transition list targeting translate,opacity should behave correctly, and the duration matches the dismiss timing constant. Now I'm checking the banner's background layering for potentially conflicting before:bg-* declarations. The two bg-[...] utilities aren't actually conflicting since one resolves to background-color (the color-mix) and the other to background-image (the linear-gradient), so they stack the same way the original CSS layered a gradient tint over a color-mix base. Checking the dark variant and the fallback branch confirms the same pattern holds—each layer maps to the right background sub-property, so the compiled Tailwind output should faithfully reproduce the original layered background behavior. I'm working through how the "dark" custom variant combines with the arbitrary selector wrapper, since Tailwind's variant ordering determines whether the dark class ends up nesting inside or outside the data-theme-id selector — trying to figure out which arrangement matches the original CSS output. I should double check the project's variant definitions in index.css to confirm this, and also verify that "t3-chat" is a valid unquoted attribute value in the selector, which it is since it starts with a letter.

8. git_tools — @custom-variant

💭 I'm working through how Tailwind's variant composition order applies here—figuring out whether the arbitrary selector variant wraps around & first before the dark mode :is() selector gets layered on top, since variants apply right-to-left with the innermost one nearest to the base selector. Same logic applies with .dark on the html element, so that resolves cleanly. Now I want to verify there aren't orphan class references left over from the CSS cleanup — things like the drawer slot, shoulder tab, glass shell, context strip, or stash tab classes that got removed from index.css but might still be referenced elsewhere in the components, which would silently break styling.

9. git_tools — chat-composer-(drawer|top-drawer|shoulder|glass|banner-stack-cap|stash-tab|context-strip|tasks-tab)

💭 I found something significant: ChatComposer.tsx:233 reads a CSS variable --chat-composer-drawer-inset via getComputedStyle, but the PR deleted the CSS rule in index.css that actually defined this variable for the composer form and drawer slots. This means the runtime read now likely returns an empty string, causing broken behavior. I should look at that code to confirm.

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

💭 The fallback value of 1.375rem (22px) matches the new width calculation's 22px-per-side spacing, so removing that CSS variable definition doesn't actually break anything—it's just a dangling reference now, but the fallback preserves the correct behavior. Confirming that variable was also removed for the chat composer form selector; this looks like a real ownership issue where two places define the same geometry independently with no shared source, which is a concrete, diff-caused finding worth flagging.

11. git_tools — chat-composer-attachment-overlap|chat-composer-attached-|chat-composer-outline|chat-composer-highlight|chat-composer-glass-surface

💭 I'm tracing where the --chat-composer-outline CSS variable gets set and confirming that banners nested inside ComposerSurface.Shell properly inherit it through the component hierarchy. I'm also verifying that the theme override for attached outlines originally only applied to the default variant case, and checking whether the new implementation preserves that same scoping. Since only default banners get the theme-based outline overrides, and surface color rules apply universally, the cascade should hold correctly across variants. Now I'm turning to the ChatComposer diff to check for behavior regressions, specifically a restore handler that closes the stash menu if there's no other candidate left in the queue besides the current entry — checking whether that some() condition correctly detects when the queue is effectively empty. That closing-menu logic actually checks out, if a bit clunky. I'll move on to reviewing the banner, shoulder, and dock rendering near the top of the component for any concrete issues.

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

💭 I notice the condition controlling the stash badge shifted from checking isComposerCollapsedMobile to isMobileViewport, meaning stash now displays inline on any mobile viewport rather than just when the composer is collapsed—likely an intentional change. I want to move on to checking the Dock component next. I see it renders the stash badge whenever showInlineStashBadge is false, but that happens even when the stash queue is empty, matching the previous behavior with showShoulderTabs. I need to look at the top of ComposerStashBadge to confirm whether a count of zero still displays something like "Stash 0".

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

💭 I'm checking the Dock layout, verifying flex-1 sizing on the tasks badge versus content-width sizing on the stash and activity badges with their margin auto positioning, and moving on to check ComposerSt... Looking at ComposerStashMenu, I notice it switched from Command/CommandItem primitives to raw list rows with buttons and manual keyboard handling — worth checking against the rule preferring core UI primitives, though semantic row lists may be exempt. I also see the effect that synced highlightedId when entries changed was removed, so I want to verify how highlightedEntry is now derived and whether keyboard/accessibility behavior still works correctly by viewing the full component at head.

14. browse_code — apps/web/src/components/chat/ComposerStashMenu.tsx

💭 I'm noticing the delete button is now always visible instead of only appearing on hover, which seems like an intentional visual change even though the related test was removed. I'm also looking at how the restore button uses an absolute overlay to make the entire row clickable. The row has relative positioning so the overlay covers the whole area including the actions column, but the delete button has a higher z-index to stay clickable above it, which seems deliberately handled. I'm also checking accessibility details like aria-labels, aria-expanded, and keyboard navigation with arrow keys, which all look reasonable so far. Now I'm considering the switch from a native scrolling container to a ScrollArea component, wondering if scrollIntoView still behaves correctly since the ScrollArea should own the actual scrolling viewport. With two solid findings already identified, I want to look through the full component file once more to see if there's anything else worth flagging. I'm also checking whether unknown data attributes passed to the ScrollArea root get forwarded or silently dropped, and more importantly whether removing the previous tabIndex and focus-visible styling from the tasks list broke keyboard scrollability -- since the list region used to be directly focusable, but now it's unclear if the ScrollArea's viewport itself is focusable in the same way.

15. browse_code — apps/web/src/components/ui/scroll-area.tsx

💭 I'm reasoning that Base UI's ScrollArea Viewport becomes focusable when content overflows, which preserves keyboard scrolling, so there's no need to flag this. I also note the ScrollArea root uses full-size styling. Working through how the ComposerBanner.Scroll overrides height with an auto value while inheriting max-height, I conclude the sizing chain still resolves correctly. Given prior unresolved findings still exist, I decide the check should remain a failure state, summarizing that those earlier issues persist alongside anything newly discovered, rather than reposting the same findings. I check whether the working-timer restoration code in MessagesTimeline.tsx differs from the base branch, and it appears identical to main, so there's nothing new to flag there. I do notice the test suite dropped a regression test verifying alignment spacing for the iconless thinking row with the working timer -- since the underlying components are unchanged, that test would still pass, so removing it is a minor, if debatable, coverage loss rather than a clear violation worth flagging strongly.

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

💭 I'm noting that removing that regression test loses coverage for alignment invariants tied to the timeline row indent changes this PR touches, which feels like a low-severity concern worth flagging even though the underlying component code is unchanged. Let me double-check that the iconless class still includes the same padding so I know the test would've passed if left in place.

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

💭 I confirm the iconless case still passes with px-1, so removing that test wasn't necessary but is a minor issue. Looking at alignment, the new toggle rows use a size-6 icon container while the TurnPlan row uses a different chevron sizing with translation offsets—these are the alignment tweaks the PR description explicitly says were left for follow-up work. This traces back to the actual revert reason for #8733: dismiss controls were aligned to the title row instead of the full title-and-description block, and edge padding was reduced. Checking ComposerBannerStackAlert, the dismiss action does sit inside the title row separate from the description row below, and the root padding is indeed reduced from the previous spacing. Since the PR author openly acknowledges this unresolved regression, it's worth flagging as a concrete visual issue tied directly to why the change was reverted before.

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

💭 I've confirmed that the default centered alignment in Alert centers the icon and actions against the full title+description block, but the new ComposerBannerStackAlert only places them in the title row, so with a description present the icon and dismiss actions align to the title line instead of the whole block—exactly the regression that caused PR #8693 to be reverted in #8733, and it's still present at head. This is a concrete, unposted finding within scope of the changed lines. I also notice the padding was reduced from px-3 pt-2 sm:px-4 to p-1 on Root, shrinking the banner's edge spacing. I'm considering posting one comment covering both the alignment issue and the padding change, though a proper fix for the alignment would require restructuring rather than a trivial inline diff—maybe wrapping the icon/actions in an outer flex row spanning the block. The PR description already acknowledges this as an open todo for another contributor to address before merging, but since it's the PR's central unresolved defect and could cause the check to fail, I still think it's worth flagging explicitly. For the second candidate finding, I'm looking at whether the removed --chat-composer-drawer-inset CSS variable definition leaves a dangling reference in ChatComposer.tsx that's now effectively dead code, since the read line itself isn't part of the diff but the deletion that causes the issue is. Rather than commenting awkwardly on the deleted CSS line, I'm weighing whether to comment instead on the added Attachment line in ComposerBanner.tsx where the width calculation now hardcodes a value that happens to match the old shared inset contract by coincidence—since 2.75rem equals double the fallback of 1.375rem, current behavior still works, but the implicit connection is fragile. variable on the ancestor Shell/Host so Attachment can inherit it — that's a clean, minimal fix. I verify the max-width math still lines up (48rem minus double the inset equals the new 45.25rem literal) and confirm via a repo-wide grep that the custom property truly has no other definition, just the one dangling read. Now I'm scanning the new banner code for something potentially more serious — checking how ComposerBanner.Row's grid-column logic with the not-has selector compiles to actual CSS. Working through whether the compiled :not(:has(...)) selector logic correctly captures "no icon and no actions" — it does, since :has() with a comma-separated list matches on any, and negating that means neither. I'm now tracing whether the group-has-data-[composer-banner-surface=attached] selector actually matches against the Shell group, checking what data attribute ComposerBanner.Surface renders to confirm the group relationship works as intended. Now I'm digging into whether the command menu's attached banner surface hides the shell's glass backdrop unexpectedly — since it defaults to placement="attached", opening the command menu could trigger the same :has() selector that switches Main to its own glass, and I need to check whether the command menu is actually rendered inside the Shell's DOM tree or portaled elsewhere, since that determines whether the selector even applies. That covers the banner stack, top drawer, shoulder tabs, and tasks drawer all inside the form — a superset that's roughly equivalent. I still need to check where ComposerStashMenu renders to confirm the glass split still works when the stash drawer is open, since previously that drawer surface lived inside the command menu layer.

19. git_tools — ComposerStashMenu|ComposerCommandMenuLayer|composerDrawerLayerAnchor

💭 The grep hits a NUL byte from binary detection, so I need another way to locate where ComposerStashMenu gets used. I'll try viewing specific line ranges instead of searching, though this line of investigation may not actually matter for the findings. Let me lock in what I've found instead: the banner's dismiss/actions and icon now align to the title row rather than the title+description block, with reduced edge padding — this is the regression that caused the earlier revert, located in ComposerBannerStack.tsx around lines 245-273, which are all added lines in the diff so I can anchor a comment there. Separately, --chat-composer-drawer-inset gets deleted while still being read elsewhere, and I need to pin down the exact line in ComposerBanner.tsx's Attachment function by viewing lines 100-150.

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

💭 That selector checks out as verbose but valid, so I'm not flagging it. I'm also noticing the old max-width calculation used 48rem as the base, while the new version replaces it with a literal value close to 45.25rem instead. The deleted --chat-composer-drawer-inset variable was read imperatively via getComputedStyle, so this is a legitimate case where the check applies — the two geometry sources could drift apart even though the hardcoded fallback currently masks the issue. For a fix, I'm thinking the variable should live in ComposerSurface.Shell since it's an ancestor of the form, with Attachment deriving its width from that value, though this spans multiple files so I'd need to describe it rather than provide a diff — and I'm weighing whether to anchor the comment on the index.css deletion instead. Since commenting on the deleted lines requires an exact old-file line number and that's risky to pin down, I'll anchor on ComposerBanner.tsx line 108 where the geometry logic now lives instead. I'm also considering a third finding about the deleted Thinking-alignment test in MessagesTimeline.test.tsx — since the underlying behavior was restored in a later commit, the test arguably should be too, but I'm wary of this feeling like noise given the check's guidance not to flag purely optional preferences. Test coverage isn't aesthetic though, so I'll include it: the row it verified still exists and renders the same markup, so its removal leaves the restored working/thinking alignment unguarded. Checking the history confirms this test was deleted alongside the row in an earlier commit and never restored when the row came back, making it directly relevant. I'm pulling the exact original test code so I can quote it accurately in my comment. Since the restored activeTurnStartedAt prop matches what this test uses, the test should compile and pass — with isWorking and empty timelineEntries, the working row gets pushed with showThinking true and no visible content, which lines up. Now I need to confirm the diff's line anchoring works: for deleted lines I'd comment on the LEFT side using the original line numbers, so I'm checking the hunk header to verify line 1351 is a valid commentable position and matches the actual old content there.

21. browse_code — apps/web/src/components/chat/MessagesTimeline.test.tsx

💭 I want to verify how the old base component rendered the banner with its description and action buttons compared to the new version, so I can pin down the exact padding difference between them. The new component groups the icon, title, and actions in one row while the description sits separately below indented under the icon column, meaning actions only align with the title line rather than the full block, and padding shrinks to 4px versus the old 12/16px — that's exactly the revert reason. The smallest fix would be restructuring so the icon/actions row spans the whole content block including the description, and restoring the original edge padding on Root; I'm considering whether a minimal unified diff for ComposerBannerStackAlert can express this structural change cleanly. I'm second-guessing whether this diff fully captures the intent, since the padding fix actually lives in a separate file (ComposerBanner.Root), which makes a single clean diff harder to justify. Given that cross-file complication, I'm leaning toward not proposing a concrete diff and instead flagging the ambiguity. I'll write out the alignment fix as its own comment tied to a specific line, explaining how binding the icon and dismiss controls to the title row causes the misalignment that led to the earlier revert, and reference how Alert handled the equivalent case. Adding the conditional description span... For the second comment, I'm flagging that the drawer inset is hardcoded in Attachment, but the PR removes the --chat-composer-drawer-inset CSS variable that ComposerCommandMenuLayer still reads imperatively via getComputedStyle, which could break that consumer. Writing the suggested CSS variable fix... Actually, that inline fix doesn't fully solve the problem since it still relies on an undefined variable falling back silently. It's better to suggest declaring the inset variable where ComposerSurface.Shell already declares its other variables, keeping a single source of truth, even though that lands in a different file and line than the original anchor. For the second comment, I'll anchor it on ComposerBanner.tsx around line 108 without a diff suggestion, just describing the two-line fix needed. For the third comment on MessagesTimeline.test.tsx, I'm weighing whether restoring the deleted test assertion is worth flagging — since the commit already restored the row and the test still checks valid markup, it seems worth a brief mention, though posting on a deleted line risks a position-mapping failure, so I'll attempt it and fall back to omitting it if the review call errors out. I'm confirming this file falls within the include patterns for tsx files under apps/web, so it's fair game, and I'm deciding to keep the third comment but keep it short since removed tests represent a real discipline issue. Now I'm working out the required schema fields for the review comments — path, body, line, start_line, side, start_side are all mandatory, so I'm setting start_line equal to line and start_side equal to side, though I suspect GitHub might reject identical start_line and line values for a single-line comment. I'm resolving that by choosing genuine multi-line ranges instead: comment 1 spans lines 245-247, comment 2 spans lines 107-108, and comment 3 spans lines 1351-1364 with side LEFT. I'm verifying these ranges fall within added or removed diff regions for each file — ComposerBanner.tsx is a new file so its full 330 lines are additions, and the ComposerBannerStack.tsx range at 245-247 also falls in newly added content.

22. modify_pr — UI consistency review of the restored composer banner/surface system. Three findings below; the two findings posted on the previous head (ComposerBanner.ToggleIcon inheriting the coarse-pointer hit-target pseudo-element, and ComposerBanner.Body losing the clipped panel's end/bottom padding) are still open on this head and are not repeated here.


Agent Credits: 364 credits

Token usage: 42 uncached input · 3,170,469 cache-read input · 196,128 cache-write input · 26,041 output
Agent Credits may also include non-token charges from external tools such as web research.