Skip to content

fix(server): reject deleted resume schedules

MacroscopeApp / Macroscope - UI Consistency failed Aug 29, 2026 in 4m 6s

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), and ExecutionEnvironmentCapabilities documents 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 a threadUsageLimitResume capability and fold serverConfig?.environment.capabilities.threadUsageLimitResume === true (already thread-environment scoped at L2217) into the gate driving showUsageLimitResumeErrorAction and usageLimitResumeBannerItem.

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.tsx
  • apps/web/src/components/chat/ThreadErrorBanner.tsx / .test.tsx
  • apps/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" inside AlertAction rather than a raw control; the composer status banner is a normal ComposerBannerStackItem (variant: "info", AlarmClockIcon, xs outline action) matching the parked/woke/background-liveness peers.
  • Geometry/alignment: Alert controlAlignment="first-line" sizes the action lane h-lh with a min-h-7 sm:min-h-6 first row; size="xs" is h-7 sm:h-6, identical to the pre-existing icon-xs dismiss 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-scoped environmentId / activeServerThread, with no active-environment fallback.
  • hasTimelineTopBanner was updated to the new displayedThreadError, 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 examining threadErrorIsUsageLimit, 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 what errorBannerError resolves 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 with controlAlignment="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 environmentId is 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 actionDescription text-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 nowMinute refresh, but this seems minor and speculative rather than a concrete issue. I'm looking at whether canUseUsageLimitResume and the environment routing rule have gaps, and whether inline arrow function props on ThreadErrorBanner already defeat its memoization. Checking the disabled-button state during actionPending, I notice the label stays unchanged even though prior guidance said to preserve it, so I'm skipping that. I'm also verifying that session.lastError properly clears when a new turn starts, and that retryAt falling in the past correctly falls back to the +5min default. Now I'm looking at usageLimitResume from activeServerThread — 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 check serverConfig?.environment.capabilities before 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 serverConfig is scoped correctly and realize the capability check would need to reference threadUsageLimitResume from the environment. Since serverConfig is defined after canUseUsageLimitResume in 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.