Skip to content

fix(web): keep separated tool runs visible

MacroscopeApp / Macroscope - UI Consistency failed Aug 20, 2026 in 5m 43s

UI Consistency: 1 issue found

apps/web/src/components/chat/MessagesTimeline.logic.ts (lines 707-710, used at 845 and 856) — the new workEntryIsInActiveRun predicate omits the active-region guard that its sibling entryBelongsToActiveTurn applies. When isWorking is true while unsettledTurnId is null (the send-acknowledgment window this PR handles via the new latestUserMessageAt fallback), the unsettledTurnId === null || clause makes every stale inProgress / task.progress entry anywhere in the thread count as active. Since nothing force-settles toolLifecycleStatus when a turn ends, an older expanded turn's group then becomes visible again and flips from the settled work-toggle summary to the animated work-live "Running …" row, flipping back once the turn id arrives.

An inline comment with a one-line suggested fix (require a known unsettled turn) has been posted; it keeps the new keeps separated in-progress tool runs visible test passing and leaves the trailing active-run path, which goes through entryBelongsToActiveTurn / isVisibleActiveToolEntry, untouched.

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: files matching apps/web/src/**/*.{ts,tsx,css} in the merge-base..head diff — ChatView.tsx, chat/MessagesTimeline.tsx, chat/MessagesTimeline.logic.ts, session-logic.ts, index.css, and the associated tests.

Verified as addressed since earlier commits on this branch, so not re-reported:

  • isExpandedToolGroupHeader now also requires row.summary !== null, so the overflow toggle no longer collapses the spacing of rows it follows.
  • TurnFoldTimelineRow and WorkingTimelineRow now share text-sm leading-relaxed text-muted-foreground tabular-nums, so the two headers that occupy the same slot match.
  • Expanded children of a live group re-filter with workEntryIsVisibleInGroup(entry, true), so task.progress rows no longer disappear on expand.
  • Contiguous in-progress runs separated by commentary now each render a work-live row instead of vanishing.

Findings previously raised on earlier commits of this branch and still present were deliberately not duplicated: the live row's entry-scoped label vs. group-scoped failure affordance, the summary group's failure icon not being gated on collapse (unlike the overflow toggle's showHiddenFailure), the live row lacking the hover:bg-accent/20 affordance its sibling disclosures carry, the still-required but unread activeTurnInProgress prop, and the two owners of tool-category classification (toolGroupAction vs. workEntryIconName).

CSS review: the new live-activity-focus* utilities in index.css are mask + animation composition with a prefers-reduced-motion fallback, which belongs in global CSS under the stated ownership rules; no finding.


🔍 Agent activity — 1 tool call

💭 I'm checking the current PR head at 615b474 against prior review comments to see what's new, focusing on the new timeline components, the live-activity CSS utilities, and the PlainWorkEntryRow changes. The Thinking row icon alignment issue seems addressed differently than expected—they aligned it with the working timer's padding instead of reserving an icon slot, which is a deliberate but different fix. I still need to verify whether the activeTurnInProgress prop was actually removed from the props interface and call sites, and I'm noticing the work-toggle failure icon gating only applies to the overflow toggle branch, not the summary branch, which could be an inconsistency worth flagging. The aggregate failure icon in the summary branch remains ungated on collapse state, matching a previously flagged issue, so I won't repeat that unless something changed. The isExpandedToolGroupHeader logic now correctly includes a null-summary check, so that one's resolved, and the in-progress tool runs visibility issue also appears fixed with the new test and prop threading. The label/failure mismatch in LiveWorkEntryTimelineRow is still present as before, and now I'm looking at how the working row typography compares against the turn-fold styling. Since most previously flagged issues are either fixed or unchanged, I should pull up the current MessagesTimeline.tsx to look for anything genuinely new that hasn't been caught yet. This PR only has 4 commits, unlike prior runs that showed more—likely from force-pushed history on other branches. I'll focus on the latest commit, 2f6812a to 615b474, "keep separated tool runs visible," and look at its diff. I'm comparing the two work-live row producers added across commits to check they stay consistent—one uses the active-run anchor with the latest active tool entry, while the newer branch builds its id from the timeline entry group anchor and picks the last in-progress entry instead, so I need to verify these don't diverge in behavior. Checking for potential id collisions: since the group path skips entries already covered by activeWorkEntryIds, the trailing active run shouldn't overlap with the group branch's anchors, so duplicate ids seem unlikely unless tool call ids coincide. I also want to confirm the new branch renders expanded children and respects the same rendering conventions as the summary branch, which it appears to do, so now I should actually open the file to verify this directly. I'm trying to reconcile prior review findings against the current commit history — several previously flagged issues (live row label mismatch, ungated failure icon, thinking icon alignment, unused prop, duplicated icon logic) were tied to commits that no longer appear in this branch's history, suggesting a rebase happened. I need to check whether those same issues persist in the current head despite the rebase, starting with whether the unused activeTurnInProgress prop is still present and whether the summary branch's failure icon is still ungated on the expanded state. Now I'm shifting focus to what's genuinely new in the latest commit — the mid-timeline live work branch using LiveWorkEntryTimelineRow, and whether the shimmer animation makes sense there since the group is still in-progress. I also want to check the actual rendered markup for accessibility concerns, like a button wrapping another interactive element. Checking that expanded children rows for live groups correctly filter with the active-run semantics, confirming the earlier fix holds. Now I need to verify the same filtering logic applies correctly for expanded children under a settled summary group, not just live ones. I'm now comparing the two live-row producers to see if they render consistently, and I'm spotting a potential styling mismatch: the summary toggle row has hover background and padding treatment, while the live entry row's button lacks that same hover/padding styling, which could create an inconsistent feel for what's functionally the same expand/collapse control. But I recall that hover-treatment differences between live and settled tool rows were likely already flagged in earlier runs, so raising this again risks being a repeat. I should keep searching for a genuinely new, unflagged issue instead. Checking other candidates: the live row button's aria-label logic seems fine since content provides the accessible name when not failed. I'm also examining whether aria-expanded gets applied to rows that aren't actually expandable, and confirming that the duplicated aria-hidden overlay content in the live activity row doesn't double-announce failures. Now I'm looking at the new CSS utilities for live-activity-focus to see if the masking rules there introduce anything worth flagging. The positioning and reduced-motion handling for these utilities look correct, and the new search icon addition seems straightforward. I want to pull up the current PlainWorkEntryRow code to verify how aria-expanded is actually applied. I'm checking the icon slot logic in PlainWorkEntryRow for expanded tool group entries without failures—the invisible/aria-hidden handling looks fine since the span already wraps an aria-hidden svg. Now I'm shifting to look at the button logic in LiveWorkEntryTimelineRow. The live row's button lacks the hover background treatment that the other disclosure controls have, but since this was already flagged in earlier passes about live vs. settled hover treatment, I'll skip it. I'm also noticing a width mismatch: the button is full-width but its child content is only as wide as its contents, so the focus ring spans further than the visible element—worth comparing against how the toggle buttons handle this same pattern. Now I'm digging into a more substantive inconsistency: the trailing active run picks its representative entry as the last visible entry (which could be completed), while the mid-timeline live group picks the last in-progress entry specifically. That's a real semantic divergence in how these two live-row variants select which entry to display, not just a cosmetic styling difference. I'm also checking padding rules for expanded live groups and confirming they match the expanded tool group header treatment, then turning to whether the minimap or row-key logic needs to account for a live row appearing anywhere in the timeline rather than just at the end. I should check other places that consume rows by kind, like the minimap, scroll anchoring, onToggleWorkGroup, and row key extraction — searching for row.kind usage and estimatedItemSize in MessagesTimeline.tsx, plus the minimap logic that may enumerate row kinds for markers. I'm checking whether expanded child rows and collapsed work rows could end up with duplicate IDs, but tracing through the branches shows they use different id sources so there's no actual conflict there. Shifting focus now to broader UI consistency issues instead. One candidate: the live row's id is derived from the group's first entry's toolCallId, matching the trailing-run anchor, so those stay consistent. But when a live row settles into its toggle state, the row id changes format even though the groupId stays the same — meaning expansion state persists but the row id shift could cause LegendList to remount or trigger a scroll jump, since the second argument to onToggleWorkGroup likely anchors scroll position. Actually, this could be intentional for keeping separated in-progress tool runs visible, but I'm more concerned about the settled-turn edge case. If unsettledTurnId is null while isWorking is true, every stale inProgress entry anywhere in the timeline—even from old, interrupted turns—would get treated as an active live row, since workEntryIsInActiveRun lacks the index guard that entryBelongsToActiveTurn has. I want to check deriveUnsettledTurnId to see exactly when it returns null and whether stale inProgress entries from aborted turns can actually persist that way. I'm trying to pin down the strongest regression case for the newest commit, thinking through a scenario where a trailing completed run coexists with an earlier in-progress entry group, and whether groups positioned above the active turn might be mishandled visually. Actually, the real bug seems to be in the window right after sending a message, before the server creates a turn — unsettledTurnId can be null even while isWorking is true, since the previous turn is settled and no running turn ID exists yet. In that gap, workEntryIsInActiveRun would incorrectly treat any in-progress entry anywhere in the thread as part of the active run, including stale entries from long-finished or aborted turns that never got settled. This would cause historical work groups to briefly flip from a collapsed "Ran 3 commands" summary into a live shimmering "Running rg" row and then flip back once the turn is actually created — a visible glitch in scrollback. Though I'm now second-guessing how reachable this is, since older turns get folded behind "Worked for" headers and their entries are hidden unless expanded, so the flip would only be visible if the user had that fold expanded. Still a real but narrower issue. I also need to check whether this interacts with activeTurnHeaderIndex, which marks the boundary for the first entry of the unsettled turn. The cleaner bug to flag is that workEntryIsInActiveRun is missing the same turn-membership guard that entryBelongsToActiveTurn enforces, so when unsettledTurnId is null during the brief send-acknowledgment window, any stale in-progress entry anywhere in the scrollback — not just the current turn — renders as live and shimmering. The fix is to require the entry's turnId to match the active unsettled turn before treating it as live. I want to verify this is a genuine regression from the changed lines and then check the session logic to see whether stale in-progress entries actually persist across turn boundaries or get settled automatically. I don't see any global logic that settles all inProgress entries when a turn ends, so I'm searching for something like "turnEnded" or a settling mechanism. There's a prior review comment suggesting that gating inProgress on toolCallId could make an id-less tool.started row incorrectly settle as successful, which implies some persistence logic exists—I want to check deriveWorkLogEntries for anything that clears inProgress at turn end. I'm confirming stale inProgress entries persist, and checking whether workEntryIsInActiveRun can return true for them when unsettledTurnId is null but isWorking is true. I need to trace how isWorking gets computed in ChatView and whether the running turn ID can be null while still settled, looking at the new session-logic test covering the fallback to the latest user message during turn acknowledgment. In the test scenario, activeTurnId is turn-2 while latestTurn lags at turn-1, so runningTurnId derives from the active turn and correctly excludes stale entries from turn-1. But there's another window before the server assigns an active turn ID at all — during send-in-flight with session status "starting," where activeTurnId is null, latestTurn is the previous settled turn, yet isWorking is still true because sendStartedAt is set. That's precisely the gap the new latestUserMessageAt fallback is meant to handle. In that null-turn window, activeTurnHeaderIndex correctly points to the row after the last user message, and entryBelongsToActiveTurn enforces an index guard so only the trailing run counts. But workEntryIsInActiveRun has no such index guard — so any stale in-progress entry anywhere in the scrollback gets swept into the active run, causing a shimmering "Running…" line to replace a settled summary and making a previously-hidden stale entry visible again. I also need to check how folding interacts here: folds only hide entries from settled turns with a terminal assistant message, so expanded or unfoldable turns would still expose this, while collapsed entries get skipped earlier in the loop via the collapsedEntryIds check. This still feels like a real but narrow inconsistency — introduced specifically by the new predicate lacking the position/turn guard that its sibling function already has. The fix would be to pass index/turn context into the check so it only treats an entry as part of the active run when its index is at or beyond activeTurnHeaderIndex, mirroring the guard already used elsewhere. Tracing through the test case confirms gating on entryBelongsToActiveTurn keeps index 0 correctly flagged as part of the active turn, so that test still passes. I also want to check whether the hover affordance issue was already flagged elsewhere — it seems the live/summary tool row distinction was already raised in a prior comment, so I can skip re-reporting that. Checking the button wrapper for LiveActivityRow, the shimmer effect only covers the content width since the inner div is fit-sized while the button spans full width, which seems fine as-is. Looking at the new mid-timeline branch, the live rows there aren't preceded by a "working" header like the trailing active row is, but that seems irrelevant, and the createdAt handling stays consistent between branches. I'm settling on reporting one finding: the missing active-region guard in workEntryIsInActiveRun, which causes a rendering-treatment leak where an animated live row and a newly visible stale entry both show up in scrollback — this looks like pre-existing behavior that the patch surfaces rather than a new bug. Before writing that up, I want to verify exactly how the collapsedEntryIds skip interacts with the group loop's position, so I'm going to look at lines 800-900 to get the comment precise. I need to place the inline comment on a line that's actually part of the diff—likely around line 707-710 where the new helper is defined, since that whole block is added in the merge-base diff. I should also verify the PR-level diff includes these same lines before finalizing. Thinking through the fix itself, gating the filter on entryBelongsToActiveTurn would require the index, which isn't available inside the predicate. But that alone wouldn't be enough, since the earlier visibility filter also lets stale in-progress entries through, so a single-line suggestion wouldn't fully resolve the issue—I'd need to touch two separate filters or restructure the predicate to capture positional context. Instead, I realize I can simplify by dropping the unsettledTurnId === null || clause so the predicate only treats entries as active when there's a known unsettled turn matching the entry's turnId. This keeps it a clean one-line change, doesn't affect the trailing active row logic (which uses different predicates), and still passes the new test case where unsettledTurnId equals turn-1. I'm drafting this as an inline suggestion spanning the relevant two lines, now figuring out the correct diff line numbers to attach the comment to. I'll leave that second inconsistency alone since it's arguably intentional behavior rather than a bug, and raising it would dilute the focus of the review. I'll finalize with the single inline comment and mark the review as a failing check.

1. modify_pr — UI consistency review — one finding on the new mid-timeline live-run branch added in fix(web): keep separated tool runs visible. Findings already raised on earlier commits of this branch (live row label vs. group-scoped failure affordance, summary-group failure icon while expanded, live vs. settled tool-row hover treatment, unused activeTurnInProgress prop, icon-classification duplication) are not repeated.


Agent Credits: 214 credits