Skip to content

fix: retry automatic resume dispatches

MacroscopeApp / Macroscope - UI Consistency completed Aug 28, 2026 in 4m 24s

UI Consistency: 1 minor finding

  • apps/web/src/components/chat/ThreadErrorBanner.tsx: the new action button's pending label is hard-coded to "Saving…", which is wrong for both call-site actions ("Resume when available" / "Cancel auto-resume"); suggested an action-agnostic pending word, matching the peer AlertAction pattern in LegacySidebar that keeps its label while disabled.

No other consistency or regression issues found in the changed web UI. Prior findings from earlier runs (raw threadError bypassing the dismissal mask, wall-clock formatting bypassing timestampFormat, and a permanently non-dismissible banner after cancelling auto-resume) are addressed at this head: displayedThreadError now falls back to visibleThreadError outside the pending-resume state, the schedule description goes through snoozeWakeDescription(..., timestampFormat), and dismiss is only suppressed while usageLimitResume !== null.

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 reviewed (in scope): apps/web/src/components/ChatView.tsx, apps/web/src/components/chat/ThreadErrorBanner.tsx, apps/web/src/components/chat/ThreadErrorBanner.test.tsx.

Checks performed:

  • Shared primitives: the new banner action uses Button variant="outline" size="xs" inside AlertAction, identical to the existing desktop-update alert action in LegacySidebar; the dismiss control keeps variant="ghost" size="icon-xs" with its aria-label. No raw controls or call-site overrides of primitive height/radius/focus ring were introduced.
  • Accessibility/interaction: disabled-while-pending relies on the primitive's disabled:pointer-events-none/opacity contract; coarse-pointer hit-target padding from the base variant is retained; AlertDescription's extra status line is a plain text-xs child inheriting the alert's error-foreground token.
  • Formatting/theming: the scheduled-resume string is produced by snoozeWakeDescription(nextAttemptAt, new Date(), timestampFormat), honouring the user's 12/24-hour preference and desktop locale, consistent with snooze toasts and menus.
  • Environment routing: scheduleUsageLimitResume / cancelUsageLimitResume are dispatched with the route/thread-scoped environmentId and threadId used elsewhere in ChatView, not a globally active environment.
  • Banner gating: hasTimelineTopBanner now tracks displayedThreadError, so top-banner layout offset stays in sync with what is actually rendered.

Note (not posted, low confidence / borderline): when usageLimitResume is set but session.lastError has already been cleared, the fallback strings ("T3 is resuming this thread now." / "T3 will resume this thread automatically.") render inside the error-variant Alert (role="alert", destructive styling, CircleAlertIcon). If that state is common in practice, consider threading an Alert variant through the banner so purely informational resume status is not presented as an error.


🔍 Agent activity — 13 tool calls

💭 I'm checking the current state of ChatView.tsx and ThreadErrorBanner.tsx at head 0735906 to see whether previously flagged issues were resolved and whether anything new stands out.

1. git_tools — apps/web/src/** acb599d..0735906

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

💭 I'm checking whether the earlier dismiss-button concern is actually resolved: when usageLimitResume is set, dismiss is correctly withheld, and otherwise displayedThreadError falls back to visibleThreadError with dismiss working properly, so that prior issue seems fixed now. I'm also considering a few smaller lingering questions: whether the Button component actually supports an "xs" size, whether "Saving…" is the right copy for a resume/cancel action versus other loading-state conventions, and whether the disabled button needs additional aria attributes. I want to double-check that AlertDescription properly supports block-level children like the actionDescription div, and confirm the snooze import and displayedThreadError logic still hold up. Let me actually go look at the Button and Alert component source to verify these details.

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

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

💭 I'm noticing the AlertAction container fixes its height to one line using h-lh with self-start alignment, but now it holds an outline xs Button (28px/24px) instead of the smaller icon-only button it had before, which creates a size mismatch against the text line-height of about 20px. That said, the parent already has min-height and padding overrides to accommodate the taller icon button, so this shouldn't actually be a new problem. I also want to check whether the generic "Saving…" loading label makes sense here, since it doesn't reflect the specific action being performed, and whether there's an existing convention elsewhere in the codebase for pending button labels I should follow instead. Beyond that, I'm tracing through environmentId sourcing to confirm it comes from the thread, checking whether snoozeWakeDescription produces a relative time string that could go stale since it's computed once at render, and considering edge cases like banner visibility after dismissal, and whether local (non-server) threads correctly skip the usage-limit resume action.

5. git_tools — snoozeWakeDescription

6. git_tools — Saving…|Saving...

💭 That "Saving…" label being reused for a "Resume when available"/"Cancel auto-resume" action is misleading copy, though it's a soft, minor UI-consistency issue rather than a clear violation worth flagging strongly. I want to check if other pending buttons in the codebase use a spinner or a different disabled-state pattern instead of reusing "Saving…", so I should search for similar cases. I also want to trace how the banner's error text connects to visibleThreadError/threadError, so I'll read through the ChatView code around those line ranges for full context.

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

💭 I'm checking whether earlier issues are fully resolved and spotting a new edge case: when a usage-limit resume is pending, the dismiss control gets disabled, so a previously dismissed error banner could reappear since the raw threadError is shown instead of respecting the dismissal mask. Working through the resurrection scenario, I realize resuming can only be triggered from the banner itself, so if the banner is hidden after dismissal there's no way to re-trigger a resume manually. This means the mask key (thread+error message) could permanently suppress a future legitimate occurrence of the same error message, which feels like pre-existing dismissal behavior rather than a new bug, though it's borderline. Now I'm checking the "Saving…" pending label used for the button — it's generic copy that doesn't match scheduling/canceling actions, which could be a small but legitimate inconsistency rather than just aesthetic preference, so I want to compare it against other pending-state buttons in similar banners like branch mismatch or provider status. I'm also verifying the description text styling, tooltip wrapping, accessibility on the disabled/pending button, and touch target sizing all look consistent with the rest of the codebase, and I want to grep for other Alert action button usages to confirm the size and variant choices match established patterns.

8. git_tools — AlertAction

💭 The button usage aligns with existing patterns, and the "Saving…" label diverges slightly from LegacySidebar's approach without being a real problem. I'm also checking the spacing between actionDescription and the error text within AlertDescription's flex layout, noting the small gap is fine, but there might be a layout issue worth investigating further. I'm also noting the vertical overflow of the action button within the alert's "first-line" alignment container - this already existed with the smaller icon button, so it's not a new regression. On narrow viewports where both a text button and dismiss icon render together in the usage-limit error case, the text will shrink first since it's flex-1 while actions stay fixed-width, which should keep things visually acceptable within the max-width constraints. I'm also flagging a potential inconsistency: when a resume is scheduled but there's no active thread error, the banner still renders using the destructive/error Alert styling with a CircleAlertIcon, even though the message itself is purely informational ("resume automatically"). This mixing of error-styled UI for a non-error state seems like a real design inconsistency worth noting, even if it's an edge case that mostly won't occur in practice. So this really is a genuine issue: an informational "resuming thread" message rendered inside a destructive-styled alert with role="alert", which seems like a legitimate finding worth flagging. The date-based description logic looks fine since relative wording only shifts at day boundaries, and the "Saving…" button label is too minor to call out separately—I'd rather concentrate on one or two solid findings than pad the list. Now I want to verify how environmentId is sourced in handleUsageLimitResumeAction, since it's paired with threadId and I need to confirm it reflects the thread's own environment rather than some global/active environment—this touches the environment routing rule, so I should trace where environmentId gets defined in ChatView.

9. git_tools — const environmentId|const threadId|const routeThreadKey

💭 I'm tracing whether the thread error can genuinely be null while a resume attempt is in progress, checking whether the provider runtime clears the last error once a turn starts successfully. That confirms my worry: during a normal resume attempt, the session's lastError does become null while usageLimitResume stays populated, so the banner shows a destructive red alert with "Cancel auto-resume" for what's actually a successful, ongoing resume. That's a real semantic mismatch — an error-styled component used to display informational state — and it looks reportable since it changes user-visible behavior around a shared control. The fix would be adding a variant prop to the banner component so informational resume states aren't rendered with error styling. Since that fix touches both the banner component and its caller, it's cross-file, so I should describe the change rather than provide a diff. Separately, there's a smaller issue where a "Saving…" label appears on a non-save action, inconsistent with how a peer component handles the same case by keeping its label and just disabling. That's arguably a copy nitpick more than a real bug, so I'm weighing whether it clears the bar for something worth flagging given the guidance to skip purely aesthetic preferences. Given how many comments have already been raised, I'm cautious about adding a weak one, but this does feel like an objective copy defect since "Saving…" appears for an action that never saves anything, so I'll include it briefly. I've also checked accessibility of the description element under the tooltip trigger and confirmed the pending-state description text stays visible correctly, both fine. Lastly, I'm noting that dismissal is suppressed whenever a resume is scheduled, forcing the user to cancel the resume instead of dismissing the error—likely an intentional design choice. Now I'm verifying that hasTimelineTopBanner is consistently using displayedThreadError, and checking the "T3 Code" naming convention against the banner copy "T3 is resuming this thread now" to see if other user-facing strings in the app follow the same "T3 " pattern.

10. git_tools — T3 Code|"T3

💭 The naming is mostly consistent with existing precedent, so it's a nit-level observation at most, not worth flagging as a real issue. I'll finalize my findings now. The bigger concern is the resume banner: it's rendered through the error-variant ThreadErrorBanner with alert styling even though there's no actual error, showing "T3 is resuming this thread now." styled destructively for the whole resumed turn. I need to verify whether lastError actually clears on turn.started by checking how session state gets rebuilt around the preservesErrorMetadata logic.

11. browse_code — apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts

💭 On resume, status stays "running" so lastError persists from the session, meaning the fallback resume messages only appear once there's no lingering session error. I'm trying to figure out exactly when usageLimitResume is set but threadError is null — likely only after the turn finishes and resume clears, or briefly when status flips to ready before resume completes. Actually, tracing through: after a failed turn hits the usage limit, the session could go idle and clear lastError even while a resume is still scheduled, so status becomes "ready" with lastError null but usageLimitResume still pending — that's a reachable state where the banner would show only the informational "will resume automatically" text in red styling. This raises the question of whether red styling is actually wrong here, since the thread genuinely is in an error-adjacent state, making it a borderline rather than clear-cut violation. I'm also noticing that dismissing the error entirely (visibleThreadError null) wipes out the whole banner including the "Resume when available" affordance, so users who dismiss lose the ability to schedule auto-resume for that error — a real but arguably acceptable gap since dismissal implies opting out. There's also a minor staleness issue where the scheduled-resume timestamp text doesn't update live once the scheduled time has passed. I should check whether ThreadErrorBanner is used in other routes to see if adding new required props would break those call sites.

12. git_tools — ThreadErrorBanner

💭 I check the memoized error banner component—the inline callback props aren't a new issue since that pattern already existed, and the onDismiss type change looks like a safe refinement. With prior concerns seemingly resolved, I'm weighing whether there's still enough justification for the check to fail by default. I trace through the pending-state button behavior for the usage limit action, the memoized handler's dependencies, and the focus loss that occurs when a disabled button becomes re-enabled with a different label—but that pattern already exists elsewhere in the codebase, so it isn't a new regression. I'm left questioning whether there's actually a legitimate finding worth reporting here. Since lastErrorClass doesn't persist, that concern resolves fine. I'm also considering archived threads—if scheduling is attempted there, the server would reject it via requireThreadNotArchived, surfacing a raw invariant error toast rather than a clean message, though this feels like a minor edge case. I'm still weighing whether the resume-status alert styling counts as a real violation given the guidance's emphasis on concrete regressions over optional improvements. If a session.state.changed to idle event fires after a failed turn, the error would clear and the purely-informational red banner would show — plausible but not certain from the adapters. There's actually a more clearly reachable version of this same issue: while a resume attempt is in flight (before nextAttemptAt is set), the thread is already running a new turn, yet the banner still displays the old red error text. I'm weighing whether flagging that informational-state variant is over-reach compared to the "Saving…" label bug — a Cancel button that's mislabeled "Saving…" isn't just aesthetic, it's factually wrong about what action is happening, and since it's hard-coded in a shared primitive, every future consumer inherits the bug. I'll post a comment on that one, suggesting the label either stays consistent or becomes a configurable prop for the pending state. I'm settling on suggesting a neutral pending copy like "Working…" instead of the action-agnostic label, as a self-contained one-liner. I'll keep this as the single crisp finding rather than also raising the variant concern separately, since anything not actually posted as a comment won't count — better to commit to the one clear issue. Now I'm weighing what state to assign: the guidance leans toward defaulting to failure, but a minor copy nit doesn't feel like it warrants that severity, so I'm second-guessing whether "failure" is too harsh here. Actually, the mislabeled "Saving…" text is a genuine user-visible bug worth flagging as a comment, but given the instruction not to fail checks for aesthetic nitpicks, I'm leaning toward treating this as a lightweight finding rather than an outright failure. Checking the rest of the resume/cancel logic — the environment scoping looks thread-bound and correct, and the new import for initialUsageLimitResumeAt looks like a legitimate addition. Nothing else stands out as a stronger issue, so I'll go ahead and post the one comment about the label.

13. modify_pr — One minor copy inconsistency in the new banner action; everything else (primitive reuse, size="xs"/variant="outline" in AlertAction, timestamp formatting via snoozeWakeDescription, thread-scoped environment routing for the new commands) matches existing patterns.


Agent Credits: 191 credits

Token usage: 22 uncached input · 1,096,116 cache-read input · 144,135 cache-write input · 14,424 output
Agent Credits may also include non-token charges from external tools such as web research.