Repository navigation
UI Consistency: 1 issue found
Finding
apps/web/src/components/ChatView.tsx(L1645-1662, L7033-7058): the new usage-limit resume actions (Resume when available / Cancel auto-resume) are environment-scoped server commands (thread.usage-limit-resume.schedule/.cancel) rendered without any server-capability gate. Every other server-backed thread action in this component is gated (threadSettlement,threadSnooze,threadPinning,threadTitleRegeneration), andExecutionEnvironmentCapabilitiesdocuments the version-skew contract as "clients hide the action instead of sending it". On an older or mismatched environment the affordance still renders and the dispatch fails into an error toast. Suggested fix: add athreadUsageLimitResumecapability and foldserverConfig?.environment.capabilities.threadUsageLimitResume === true(already thread-environment scoped at L2217) into the gate drivingshowUsageLimitResumeErrorActionandusageLimitResumeBannerItem.
An inline review comment has been posted.
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 (in scope)
apps/web/src/components/ChatView.tsxapps/web/src/components/chat/ThreadErrorBanner.tsx/.test.tsxapps/web/src/components/chat/usageLimitResumePresentation.ts/.test.ts
Context read outside scope: components/ui/alert.tsx, components/ui/button.tsx, components/chat/ComposerBannerStack.tsx, components/Sidebar.snooze.ts, packages/contracts/src/environment.ts, packages/contracts/src/orchestration.ts, apps/server/src/environment/ServerEnvironment.ts.
Checked and clean
- Shared primitives: the new banner action uses
Button size="xs" variant="outline"insideAlertActionrather than a raw control; the composer status banner is a normalComposerBannerStackItem(variant: "info",AlarmClockIcon,xsoutline action) matching the parked/woke/background-liveness peers. - Geometry/alignment:
Alert controlAlignment="first-line"sizes the action laneh-lhwith amin-h-7 sm:min-h-6first row;size="xs"ish-7 sm:h-6, identical to the pre-existingicon-xsdismiss button, so no clipping or alignment regression. Accessibility is preserved:aria-label="Dismiss error"remains on the dismiss control, and the action button keeps focus-visible/disabled semantics from the primitive. - Banner ownership: dismiss is only suppressed while a resume is actually pending and the error is a usage-limit error; when it is suppressed the presentation helper deliberately bypasses the session dismissal mask, so there is no dead dismiss button. Unrelated local errors (script failures, approval/user-input failures) keep the dismissible error banner and route resume status to the composer stack instead — covered by
usageLimitResumePresentation.test.ts. - Timestamps: the scheduled-resume label goes through
snoozeWakeDescription(..., timestampFormat), honouring the user's 12-/24-hour preference and host locale like the snooze surfaces. - Environment routing: the schedule/cancel dispatches and
initialUsageLimitResumeAt(session.retryAt)all read the route-scopedenvironmentId/activeServerThread, with no active-environment fallback. hasTimelineTopBannerwas updated to the newdisplayedThreadError, keeping timeline offset in sync with actual banner visibility.- No CSS/Tailwind ownership, theme-variant, scroll-mask, or emitted-selector changes in this PR.
🔍 Agent activity — 24 tool calls
💭 I want to check the current state of the changed web files to verify UI consistency, since earlier commits had flagged issues. I'll pull up ChatView.tsx and ThreadErrorBanner.tsx at head to review.
1. browse_code — apps/web/src/components/chat/ThreadErrorBanner.tsx
2. browse_code — apps/web/src/components/chat/usageLimitResumePresentation.ts
💭 Now I want to check how ChatView is used, so I'll look at the sections around lines 1640-1740 and 7030-7060.
3. browse_code — apps/web/src/components/ChatView.tsx
4. browse_code — apps/web/src/components/ChatView.tsx
💭 I've resolved the earlier findings and am now checking UI consistency, specifically whether the status banner item's button usage matches how other banner items implement the ComposerBannerStackItem contract. I also want to verify the AlarmClockIcon import exists and confirm the snoozeWakeDescription import path is fine, so I'll check the ComposerBannerStack component to see how other items build their actions.
5. git_tools — ComposerBannerStackItem
6. git_tools — AlarmClockIcon
💭 I'm checking the ComposerBannerStack item structure to see how other banners like the woke thread or parked thread items define their action buttons, so I can keep the variant, size, and dismissal behavior consistent.
7. browse_code — apps/web/src/components/chat/ComposerBannerStack.tsx
8. browse_code — apps/web/src/components/ChatView.tsx
💭 Let me check the banner items around lines 4770-4960 to see how actions with Button size/variant and dismissLabel are conventionally structured there.
9. browse_code — apps/web/src/components/ChatView.tsx
💭 The status banner button's behavior of disabling while pending but keeping its label seems consistent with peer patterns, though those peers do change labels—not a strong issue. I'm now checking whether the description text handles the case where nextAttemptAt is null correctly. I want to verify the timestamp handling by looking at the signature of snoozeWakeDescription to confirm it's being called correctly.
10. git_tools — snoozeWakeDescription
💭 I'm noticing a discrepancy between the git_grep result showing that flagged line at ChatView.tsx line 1650 and what the file actually shows at head commit e215f5e around lines 1643-1672 — I need to reconcile which revision each tool is actually pulling from. I'll grep explicitly against the e215f5e revision to get a definitive answer.
11. git_tools — T3 will resume this thread automatically
12. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/files?per_page=100&page=1
💭 With the head confirmed, I'm now digging into the web UI code for concrete bugs. I'm tracing through the logic for
canUseUsageLimitResume, checking how it handles a null activeServerThread (draft thread case) — the optional chaining comparison against null correctly evaluates to false there, which looks fine. Now I'm examiningthreadErrorIsUsageLimit, which depends on localServerError being null alongside the session's lastError state. Then I trace how the dismiss button gets suppressed on the error banner when a usage-limit resume is scheduled — the presentation logic bypasses normal dismissal by returning the raw thread error, which resolves a concern I'd flagged earlier. But I'm now noticing a subtler edge case: when a resume is scheduled but the active error is actually an unrelated local error rather than a usage-limit error, the banner presentation logic may still be showing something inconsistent, so I need to trace whaterrorBannerErrorresolves to in that branch. I should check the AlertAction component itself now — whether it supports multiple children with proper alignment, since it previously held just one icon button but now has an outline xs button plus an icon withcontrolAlignment="first-line". I want to verify the size="xs" Button variant exists and whether AlertAction's line-height-based container might clip or misalign that combination, so I'll look at alert.tsx directly.
13. browse_code — apps/web/src/components/ui/alert.tsx
14. browse_code — apps/web/src/components/chat/ThreadErrorBanner.test.tsx
💭 I'm noting that when controlAlignment is "first-line," the AlertAction container gets a fixed height matching one line-height, so a size-5 icon button fits neatly, but I need to check whether a text Button with size "xs" fits within that same constrained height.
15. browse_code — apps/web/src/components/ui/button.tsx
💭 I'm calculating the sizing mismatch: the xs button is 28px tall on mobile, but the alert's line-height container is only 20px, so the button will overflow the flex container by about 4px on each side even with the padding accounted for. Checking the min-h-7/min-h-6 wrapper values, I realize they're actually designed to match the icon-xs button geometry (28px mobile, 24px desktop), which lines up exactly with the xs text button dimensions -- so there's no actual regression here, the ThreadErrorBanner extension is consistent. Now I want to verify how environmentId gets resolved for the usage limit resume action in ChatView, comparing it against how other thread commands determine which environment to route to.
16. git_tools — const environmentId
17. git_tools — environmentId =
💭 I'm trying to figure out where
environmentIdis coming from in ChatViewContent—likely destructured from props or pulled from routeThreadRef—so I'll search for its usage near the function signature.
18. git_tools — ^ const { environmentId|environmentId, threadId|props.environmentId
💭 I confirm environmentId is properly route-scoped like other thread commands. Now I'm checking for UI-consistency issues around the reused usage-limit description text between the error and status banners, making sure both handle the case where nextAttemptAt is null versus a valid future date. I'm verifying the visibility conditions: showUsageLimitResumeStatus correctly requires a non-null usageLimitResume, hasTimelineTopBanner correctly reflects displayedThreadError, and dismiss behavior stays consistent whether the resume is scheduled or the thread has settled. I'm also comparing the destructive error banner's "Cancel auto-resume" action styling against peer alert actions to check for visual consistency.
19. git_tools — AlertAction
💭 I want to check how ComposerBannerStack renders item.actions and dismiss around lines 195-240, and confirm the description styling matches AlertDescription conventions. I'm also checking whether ThreadErrorBanner's
actionDescriptiontext-xs styling inside AlertDescription is consistent with the error-foreground opacity treatment used elsewhere.
20. browse_code — apps/web/src/components/chat/ComposerBannerStack.tsx
💭 I'm checking that everything looks consistent, then considering a few remaining edge cases: whether pending states need textual banner labels, the ordering of the resume banner item in the stack, and whether the "Automatic resume scheduled" description needs a live-updating minute display to stay accurate. I'm also noting that snooze-related date labels could go stale after midnight without a
nowMinuterefresh, but this seems minor and speculative rather than a concrete issue. I'm looking at whethercanUseUsageLimitResumeand the environment routing rule have gaps, and whether inline arrow function props onThreadErrorBanneralready defeat its memoization. Checking the disabled-button state duringactionPending, I notice the label stays unchanged even though prior guidance said to preserve it, so I'm skipping that. I'm also verifying thatsession.lastErrorproperly clears when a new turn starts, and thatretryAtfalling in the past correctly falls back to the +5min default. Now I'm looking atusageLimitResumefromactiveServerThread— since it's optional in the schema, older servers that don't support this command would return undefined, defaulting to null, but the UI would still show the "Resume when available" action and dispatch would fail against those servers. This looks like a missing capability gate, similar to how other features checkserverConfig?.environment.capabilitiesbefore exposing themselves. I need to check whether there's an established capabilities contract for this — if the schema includes similar flags for related features, adding one here would fit the codebase's existing gating pattern, though I should verify this falls within the specific scope of the environment-routing check I'm applying.
21. git_tools — threadSnooze
💭 I'm noticing this PR adds two new client commands for usage-limit-resume scheduling and cancellation, plus corresponding UI actions in ChatView, but unlike every other recent server-backed thread command, it lacks the established capability-flag gate that protects against version skew with older servers. I want to verify this against the documented environment-routing rules, which state that capability gating, dispatch, and labels must all read from the same environment's server config, and that actions should be suppressed rather than guessed when scope is ambiguous. This case looks like a clean violation: a new environment-scoped action introduced with no capability check at all in ChatView.tsx, which is in scope for this check. I want to pull the exact capability schema comment from environment.ts to cite precisely.
22. browse_code — packages/contracts/src/environment.ts
💭 I'm confirming the repo has a documented version-skew contract where clients treat missing new thread commands as unsupported and simply hide the action rather than send it. This PR doesn't add a capability flag anywhere, even though the thread contract comment explicitly acknowledges wire-compatibility concerns for the field—so they clearly considered backward compatibility but didn't fully implement gating for it here. This is a concrete finding at ChatView.tsx around line 1661, where the smallest fix would require gating the resume-error and schedule actions on a capability that doesn't yet exist, meaning cross-file changes into contracts and the server. Since no inline diff is feasible, I'll comment describing the smallest fix without a code suggestion. I'm double-checking whether this is truly in scope as a UI-consistency issue by verifying how serverConfig is scoped within ChatView, since it also drives things like supportsSnooze.
23. git_tools — serverConfig = |useServerConfig
💭 I'm checking that
serverConfigis scoped correctly and realize the capability check would need to referencethreadUsageLimitResumefrom the environment. SinceserverConfigis defined aftercanUseUsageLimitResumein the file, I need to note that the gate should be applied where it's actually consumed, or the definition order needs adjusting. I'm also verifying which diff line to anchor the comment on — settling on line 1662 in ChatView.tsx since it falls within the added hunk and is close to the relevant logic. I'll also note the docs file mentions the action appearing for specific tools like Codex and Claude Code, which is worth cross-referencing. I'm weighing whether to mark this check as a failure given the explicit guidance that a clear consistency or regression risk should fail it — this version-skew gap seems like a genuine, concrete divergence, though it's a moderate risk since it only triggers under specific conditions. I'm leaning toward treating it as a failure rather than neutral, since the guidance is explicit here rather than requiring me to default to success.
24. modify_pr — Reviewed the changed web UI (ChatView.tsx, chat/ThreadErrorBanner.tsx, chat/usageLimitResumePresentation.ts) against the shared component system and the repo's environment/capability conventions. The banner work itself now lines up with the primitives: ThreadErrorBanner keeps Alert/AlertAction ownership, the new action uses Button size="xs" variant="outline" (same geometry as the existing icon-xs dismiss control, so the h-lh / min-h-7 sm:min-h-6 first-line alignment contract still holds), the composer status banner matches its peer ComposerBannerStackItems, and the scheduled time is formatted through snoozeWakeDescription(..., timestampFormat) rather than a raw Intl call. One finding: the new environment-scoped resume actions are offered without a server capability gate, unlike every other server-backed thread action in this file. Details inline.
Agent Credits: 407 credits
Token usage: 38 uncached input · 3,655,024 cache-read input · 272,236 cache-write input · 13,803 output
Agent Credits may also include non-token charges from external tools such as web research.