Skip to content

feat(web): group live tool activity

MacroscopeApp / Macroscope - UI Consistency failed Aug 16, 2026 in 7m 22s

UI Consistency: 3 issues found

Three findings, all in apps/web/src/components/chat/MessagesTimeline.tsx (inline comments posted):

  • Order-dependent cascade conflict (line 1407) — the live-activity sweep overlay combines absolute inset-0 with the new live-activity-focus utility, which owns left/right/width. inset-0 emits the inset shorthand at the same specificity in the same utilities layer, so the overlay's -4.5rem offset can be overridden depending on emission order, misaligning the highlighted duplicate label by 4.5rem against live-activity-focus-aligned's margin-left. Fix: use inset-y-0.
  • Viewport breakpoints instead of container queries (lines 2410, 2414, 2431) — the rebuilt AgentSpawnCtaRow copies ChangedFilesCard's shell but hides its workflow name, status block and button label with md:inline/sm:flex/sm:inline. The chat column shares a flex row with the resizable inline right panel and can shrink to ~360px on a wide viewport, so these labels stay visible and crowd the primary label. ChangedFilesCard handles the same case with @container/changed-files + @[24rem]/changed-files:inline.
  • Failure state becomes color-only (line 2527) — after the "Failed" tooltip removal, aria-label="Tool call failed" sits on a generic <span>, and the row's own aria-label={displayText} (when expandable) replaces descendant text, so the destructive X is the only failure signal exposed. Fold the state into the row's accessible name or give the span role="img".

No issues found with the shared-primitive migration in AgentSpawnCtaRow (Button size="xs" variant="outline" with matching aria-label), the ChangedFilesCard header/preview restyle, or keeping the new mask/keyframe/animation utilities in index.css.

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: apps/web/src/components/chat/MessagesTimeline.tsx, apps/web/src/components/chat/MessagesTimeline.logic.ts, apps/web/src/components/chat/ChangedFilesTree.tsx, apps/web/src/index.css, plus apps/web/src/components/ui/button.tsx and the ChatView / right-panel layout shells for context.

Checks performed:

  • Verified Button size="xs" and variant="outline" exist in the primitive contract and that no call-site class string overrides the primitive's height, radius, focus ring or base colors in the new subagent card.
  • Confirmed the new raw <button> rows (LiveWorkEntryTimelineRow, the tool-group summary row, the changed-files header/preview rows) implement semantic timeline rows with aria-expanded and focus-visible rings, so they are not local Button reconstructions.
  • Confirmed the new keyframes are declared alongside existing ones in @theme, and that mask/animation behavior legitimately stays in global CSS; --live-activity-focus-width is declared on the masking element and correctly inherited by live-activity-focus-aligned, and prefers-reduced-motion is handled.
  • Traced whether the chat timeline establishes a container context (it does not) and whether its column can be much narrower than the viewport (it can: the inline right panel is drag-resizable down to a 360px sibling column, and can be maximized to collapse the chat column), which is what makes the sm:/md: variants incorrect for this card.
  • Noted but did not report: summarizeChangedFileScopes in changedFilesPresentation.ts now has only its own test as a consumer after the scope summary row was removed, and the workingStepLabel context field is no longer rendered by WorkingTimelineRow. Both are dead-plumbing cleanups rather than consistency or regression issues.

Not verifiable with the available tools: emitted production CSS ordering for the custom @utility versus core inset-0, and real-app light/dark visual evidence for the restyled rows. The cascade finding is reported as an ownership/order-dependence issue that holds regardless of the current build output.


🔍 Agent activity — 1 tool call

💭 Looking at the diff areas to identify what might be breaking the rules. The LiveWorkEntryTimelineRow uses a raw button element for a semantic row toggle, which seems acceptable. The new live-activity-focus CSS utilities with masks and animations are allowed in global styles per the guidelines. In AgentSpawnCtaRow, a raw button got replaced with a Button primitive wrapped in a div, which is an improvement, though I'm noticing the outer wrapper might have some styling implications I need to examine. Now I'm checking whether certain imports like CheckIcon, ChevronDownIcon, and Tooltip are still being used elsewhere in the file since they were removed from these components, or if they're now unused and could trigger linting issues. I also need to verify that the CSS custom properties in live-activity-focus are properly defined. Now I'm checking where CheckIcon and ChevronDownIcon are being used throughout the file. Looking at the timeline components more closely, I'm noticing that workingStepLabel appears to be passed into WorkingTimelineRow but isn't actually being used anywhere in the component — it's sitting in the context and props without being consumed. For the new row toggle buttons in LiveWorkEntryTimelineRow and WorkGroupToggleTimelineRow, they're using raw button elements with hover states and focus rings, which fits the semantic row exception. In LiveActivityRow, the duplicated highlighted content is wrapped in an aria-hidden div, which is the right approach to prevent screen readers from picking it up twice. There's a potential CSS cascade issue though: the custom live-activity-focus utility is trying to override inset-0's positioning with explicit left/right/width declarations, but depending on how Tailwind v4 orders custom utilities relative to built-ins, the inset-0 might win if it's emitted later in the cascade. The author did include right: auto intentionally to counteract this, which suggests they were aware of the risk, so it's hard to flag without seeing the actual emitted CSS. I should also note that the mask-image approach here is component-specific for a shimmer effect, which might violate the pattern of using a shared virtualized-scroll-fade contract for overflow fades. Now looking at the ChangedFilesTree changes — the "Show all N files" button was removed and summarizeChangedFileScopes is no longer imported, so I need to verify whether that function is now dead code or still being used elsewhere. There's also a question about whether useMemo became an unused import in this file. Checking the ChangedFilesCard header button, the flex-1 class was removed from the button styling while the parent container still has justify-between, which is fine. The preview row changed from a wrapping layout to a single-row layout without wrapping, and the "Show all N files" button text is gone entirely — so now the collapsed state only shows a chevron for affordance. I should verify that the chevron has proper aria-expanded attributes for accessibility. I'm looking at how the preview row handles overflow with flexbox and fixed widths — items shrink and get clipped when they exceed the container, but there's no visual indicator like the previous "+1 more" text. The test confirms this is intentional, asserting that the "+1 more" label shouldn't appear, though it does mean content gets silently cut off mid-item. The changedFileName field includes the directory prefix based on what the test expects. I should check if summarizeChangedFileScopes is still being used anywhere in the codebase, then dive into MessagesTimeline to look at the region around lines 1100-1600. I'm noticing that summarizeChangedFileScopes has become dead code—it's only used in its own test now since this PR removed its last consumer. It matches the file patterns we're checking, so it's a legitimate finding, though the rule about not demanding unrelated cleanup doesn't quite apply here since removing the consumer was part of this PR itself. Rather than flag this as a UI consistency issue (which it isn't), I'll move on to examining the WorkingTimelineRow and LiveActivity components in the next section. I'm looking at an accessibility issue where aria-label is being applied to a plain <span> element without a proper role, which means the label won't be exposed to assistive technology. This is replacing a previous pattern that also had the same problem with a span inside a TooltipTrigger. The real regression here is that the tooltip showing "Failed" was removed entirely, and now the failure state is only conveyed by an icon with no accessible text alternative. To fix this, I should either add role="img" to the SVG and put the aria-label there, or use screen-reader-only text instead. I'm seeing the same issue in PlainWorkEntryRow around line 2524. LiveActivityRow does have duplicate label text visually, but it's properly hidden with aria-hidden so there's no double announcement. Now I need to check whether PlainWorkEntryRow is rendering aria-expanded even when canExpand is false — that would be another semantic issue. Let me look at that section of the code. I'm noticing the chevron indicator and hover background styling were removed from expandable rows, leaving them with only a cursor pointer as a visual cue. This feels like a regression in interaction affordance—the guidelines emphasize preserving accessibility semantics like focus states and pointer behavior, though the hover feedback removal seems to be a deliberate design choice for a cleaner aesthetic. The more substantive accessibility issue is the aria-label placed on generic <span> elements that screen readers ignore. In WorkGroupToggleTimelineRow, the failure state relies solely on a destructive-colored X icon with no text alternative, and the previous Tooltip is gone. I should flag that the aria-label needs to move to either a role="img" span, the SVG itself, or a screen-reader-only span. PlainWorkEntryRow has a similar pattern where aria-label on the row div overrides the inner text, which is acceptable but worth noting. There's also the removal of success/neutral indicator logic that was previously tied to work entry context. I'm noticing that workingStepLabel is now dead code throughout the context — it's still passed as a prop and plumbed through but nothing renders it anymore since they switched to ThinkingActivityRow. That's a deliberate UI change, so leaving the unused prop is minor and probably not worth flagging as a violation. Now I need to examine the CSS additions more carefully to see if there are any other issues. The keyframes look good in the @theme block. Now I'm checking where the new @Utility block sits in index.css. I'm noticing the CSS utilities lack explanatory comments like the existing ones have, which is a minor issue. There's also a potential concern with how live-activity-focus declares the --live-activity-focus-width variable on itself while live-activity-focus-aligned tries to read it as an inherited value from its ancestor, though this should work since the variable is scoped to the mask element. The bigger question is the cascade order in Tailwind v4 — specifically whether inset-0 (which sets left and right) might override the width declaration in absolutely positioned elements, since both constraints together could create layout conflicts. The misalignment would be quite visible — the highlighted text would shift 4.5rem to the right of the actual label, which suggests the developer tested this and confirmed the utility order works as intended. Since I can't inspect the production CSS output, I'll trust their implementation rather than flag a potential issue. That said, there's a fragile dependency here: the custom utility needs to win against inset-0 purely through source order in the same layer. A cleaner approach would be to remove inset-0 from the JSX and instead define top and bottom positioning directly in the utility, or swap to using top-0 bottom-0 individually — that way the positioning logic lives entirely within the utility without relying on cascade order to override a conflicting class. I'm also noticing a second issue with aria-labels on generic spans used for the failure indicator. These appear in two places and seem to be a new addition in the changed lines, but I need to check whether they're actually replacing a previous accessible pattern or introducing a new accessibility concern. The problem is that aria-label on a span with no explicit role gets ignored by browsers and assistive technology since the span defaults to a generic role. The fix is straightforward: either add role="img" to the span or move the aria-label to an SVG element with role="img". This is a real but low-severity accessibility issue with a minimal fix available. Looking at the test, it does check for aria-label="Tool call failed", so adding the role would preserve that test coverage. I'm wondering if there are other substantive issues to flag. There's a dead code path in PlainWorkEntryRow's iconWrapperClass logic. The condition workEntry.tone === "tool" || showFailedIndicator is unreachable because showFailedIndicator is already handled in an earlier branch. Before this change, that condition could execute when the row wasn't destructive, but now the first branch captures showFailedIndicator cases, leaving the later check unreachable. The AgentSpawnCtaRow component properly uses the Button primitive with appropriate sizing and aria-label that matches the visible text, and the icon sizing is consistent with other parts of the codebase. The status text has been updated from a checkmark to "Completed" and uses responsive classes to hide/show elements at different breakpoints, though there's a potential layout concern with how the status and workflow name stack inside the chat timeline container. I should check whether other chat components use container queries and if the MessagesTimeline itself renders in a resizable panel, then search the file to see if sm: breakpoints are already established elsewhere. I'm noticing the file uses container queries for responsive behavior in the ChangedFilesCard, specifically hiding labels on narrow viewports with @container/changed-files and @[24rem]/changed-files:inline. The new AgentSpawnCtaRow should follow the same pattern rather than relying on viewport-level breakpoints like sm:inline, since it mirrors that card's visual design. Let me check if the chat column itself is resizable or if there's an inline preview mode that would make the MessagesTimeline narrower, which would determine whether container queries are actually necessary here. The chat column can get as narrow as 360px even in a wide viewport, so standard breakpoint-based hiding won't work for the AgentSpawnCtaRow labels. I'm noticing that ChangedFilesCard already solves this with container queries, so I should apply the same pattern here—declaring a container context on the card and using a container query breakpoint to toggle the layout inline when space is tight. The truncation and min-width constraints are already in place (labels have min-w-0 truncate, the button is shrink-0), and since flex children get blockified, the display:inline on the workflow name actually becomes display:block in the flex context, so truncation will work correctly. In the worst case with a 360px chat column, both the workflow name and status will compress, but the layout stays intact. However, this creates an inconsistency — the sibling card pattern in the same PR uses container queries instead of breakpoints (the ChangedFilesCard test explicitly removed the sm:inline assertion and replaced it with @[24rem]/changed-files:inline). This PR is using md:inline for the same "hide label when narrow" pattern, which violates the established consistency in the codebase. For the inset-0 vs custom utility conflict, I need to verify the CSS emission order — if the custom utility comes after core utilities, it'll override correctly, but if not, there's a potential misalignment. The safest recommendation is to use inset-y-0 instead to avoid the specificity conflict entirely, since both classes sit at the same layer level. I'm identifying an order-dependent conflict where a custom inset utility overrides Tailwind's inset-0, so I'll suggest using inset-y-0 instead to avoid the collision since the custom utility controls the horizontal properties. I'm also verifying that Tailwind v4's inset-0 generates the logical inset shorthand property. Now I'm checking how Tailwind v4 sorts custom utilities—they're ordered by property count, with utilities that set more properties coming first so that more specific single-property utilities can override them. So live-activity-focus with its multiple properties (mask, animation, and others) would come before the inset utility in the cascade. But wait, that means inset-0 would actually override the left and right offsets from live-activity-focus, which would break the mask positioning—the left offset would collapse to 0 instead of staying at -4.5rem, and the width constraint would become over-constrained. The highlighted text sweep would then be misaligned by 72px from the base text. I'm not entirely certain this property-count sorting is how Tailwind v4 handles custom utilities, but either way this feels fragile and order-dependent. Let me phrase the fix as a same-layer specificity conflict: inset-0 might override the utility's left and right depending on emission order, so switching to inset-y-0 keeps left, right, and width fully owned by the utility and removes that risk. Now I should verify that WorkEntryIconSvg actually supports all the icon names that toolGroupSummaryIconName is trying to use—eye, square-pen, terminal, globe, wrench, hammer. Looking at the session-logic changes, the main update is that tool.started entries are now being included in the tracking, which doesn't affect the UI consistency. For the new live work row, the button layout looks solid with the LiveActivityRow component set to fit its content width while the button itself spans full width with centered alignment. The accessibility implementation covers the expansion toggle with proper aria-expanded attributes and a descriptive label. I'm noticing a few details: the disclosure content when expanded isn't explicitly associated with the trigger, which is minor. The LiveActivityRow duplicates the label text in the DOM with the highlighted copy marked as aria-hidden, so screen readers handle it correctly and text assertions in tests will work fine. The reduced-motion handling properly disables animations and sets opacity to zero. The mask element has will-change: transform applied while the animation runs on the masked element itself, which is fine. The -webkit-mask-image uses the default position, so that's not an issue. For the color scheme, the highlighted copy uses text-foreground for both label and icon while the non-highlighted version uses text-secondary-label and text-icon-muted, which maintains good contrast. I'm identifying two findings: the absolute inset-0 positioning at line 1407 conflicts with the live-activity-focus left/right/width constraints due to specificity and order, so I should change it to inset-y-0 instead. The second issue is starting to emerge around lines 2410-2431... Actually, looking at the next part, I see there's a viewport breakpoint problem where the card uses media queries (sm:/md:) for content-width-dependent hiding, but the chat column can be around 360px on a wide viewport with a resizable right panel, so labels won't hide properly. I need to switch to container queries like ChangedFilesCard does, adding @container/agent-spawn on the wrapper and using @[24rem]/agent-spawn:... for the responsive behavior. There's also an accessibility issue at lines 1510 and 2527 where aria-label is on a generic <span> with no role, making it ignored by assistive technology — the failure state is now only conveyed by color after the Tooltip was removed. I should add role="img" to that span or use sr-only text instead. The third finding is solid, though I'm still verifying whether the span's aria-label inside a button actually contributes to the button's accessible name during name computation. does get announced via aria-describedby. So removing the tooltip means losing that announcement, which is a real accessibility regression for the failure state in PlainWorkEntryRow. I'll flag all three issues but keep the third one focused: the destructive X icon's failure label isn't exposed to screen readers when the row has an aria-label, making the failure state color-only. I could suggest either including the failure state in the row's accessible name or adding role="img" to the icon, though I want to be careful not to be too speculative about the fix. Now I need to verify the line numbers in the diff for the inline comments — they should all be on the RIGHT side of the PR diff. Line 1407 is in the added LiveActivityRow section, and lines 2410, 2414, and 2431 are within the added AgentSpawnCtaRow hunk, while 2527 is in the added PlainWorkEntryRow section. Let me double-check these against the actual diff to make sure they're accurate. I'm adding the first inline comment on line 1407 about how inset-0 and live-activity-focus both compile to utilities in the same layer, causing a specificity conflict where the sweep overlay's left offset gets overridden depending on emission order, which then misaligns the highlighted copy. For the second comment around line 2414, I need to reference the multi-line changes across the hunk (the wrapper needs a container class and three variants need updating), so I'll describe the fix without a diff since it spans multiple locations. The third comment flags an accessibility failure on line 2527.

1. modify_pr — Reviewed the changed chat timeline UI (ChangedFilesTree.tsx, MessagesTimeline.tsx, index.css) for shared-primitive usage, Tailwind/CSS ownership and preserved interaction semantics. The AgentSpawnCtaRow migration to Button and the new mask/animation utilities living in index.css both look right. Three concrete issues below: one order-dependent cascade conflict on the new live-activity overlay, one responsive-mode inconsistency in the rebuilt subagent card, and one lost failure affordance.


Agent Credits: 294 credits