Repository navigation
UI Consistency: 1 issue found
1 finding in the in-scope web files.
apps/web/src/components/ChatView.tsx(~line 5096): the new Codex/feedbackupload guard uses a ref (feedbackUploadsInFlightRef), so the in-flight upload blocks sending without any re-render. The composer's send button stays enabled and Enter is silently dropped, unlike every other send-blocking condition here (isSendBusy,sendDisabledReason), which are passed toChatComposerand drive a visible disabled/busy affordance. Suggested smallest fix: track the in-flight thread keys in state and surface asendDisabledReason, keeping the ref only as the synchronous re-entrancy guard.
The clipboard issue flagged in the prior run is resolved: the toast Copy ID action now routes through the shared writeTextToClipboard helper, which reports ClipboardApiUnavailableError through the existing error toast.
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— new Codex/feedbackinterception inonSend, local feedback pseudo-message merge intotimelineMessages, success/error toasts.apps/web/src/components/chat/MessagesTimeline.test.tsx— new rendering tests for the feedback pseudo-messages.
Checks performed
- Shared primitives: toasts go through
stackedThreadToast/toastManager; theCopy IDaction uses the toast primitive'sactionPropscontract (rendered viaToast.ActionwithbuttonVariants({ size: "xs" })), no locally reconstructed controls, no new class strings overriding primitive geometry. - Clipboard:
writeTextToClipboard(secure-context guard + structuredClipboardApiUnavailableError/ClipboardWriteError) is used, matchingSidebar,BranchToolbarBranchSelector,FileBrowserPanel, andThreadTerminalDrawer. - Timeline: verified
deriveTimelineEntriessorts bycreatedAt, so the appended local feedback pair stays chronologically ordered relative to newer server messages;turnId: nullpseudo-messages are excluded from turn folding (deriveTurnFolds) and the revert affordance (revertTurnCount), so no broken row actions are introduced. - Composer surface:
/feedbackis already advertised as a provider slash command (selectedProviderStatus.slashCommands) and rendered by the existingComposerCommandMenupath, so discovery stays consistent. - Scroll behavior: the feedback path's
scrollToEnd()matches the non-anchored branch of the normal send path. - No CSS, theme variant, or Tailwind ownership changes in this diff.
Notes
- Base UI's
Toast.Actiondoes not auto-dismiss (onlyToast.Close/toastManager.closedo), so the persistent (timeout: 0) success toast remains visible afterCopy ID; this keeps the thread ID readable and was not treated as a finding. - Mobile and server files in the diff were used as context only, per the configured include patterns.
🔍 Agent activity — 26 tool calls
💭 I'm checking the in-scope files ChatView.tsx and MessagesTimeline.test.tsx to confirm the earlier flagged clipboard issue was resolved by switching to writeTextToClipboard from useCopyToClipboard, then moving on to reviewing UI consistency around toast usage and action props in ChatView.tsx.
1. git_tools — writeTextToClipboard
2. git_tools — stackedThreadToast
💭 With that finding resolved, I'm now reviewing the new ChatView.tsx for UI consistency, checking the persistent success toast with its "Copy ID" action and confirming the clipboard write function correctly resolves false on empty values. I want to verify whether
stackedThreadToastneeds thread-scopeddatapassed in—other usages elsewhere includethreadToastData, but I'm not seeing it in ChatView's other toast calls, so I'm checking the toast helpers and how ChatView typically threads that context through.
3. browse_code — apps/web/src/components/ui/toastHelpers.ts
💭 I want to check the ChatView code around the new feedback logic, particularly how newMessageId and optimisticUserMessages are handled, plus the send-disabled gating.
4. browse_code — apps/web/src/components/ChatView.tsx
💭 I'm noting that the feedback in-flight tracking uses a ref rather than state, so the composer's UI doesn't reflect that status—more of a UX gap than a strict UI consistency issue, though the review lens does emphasize preserving interaction semantics. I'm about to check the composer draft handling next. I'm checking whether pressing send during an in-flight feedback upload silently no-ops via the guard ref, and confirming that matches the existing
sendInFlightRefpattern rather than being a new regression. Now I'm tracing throughisServerThreadandnewMessageIdusage to make sure there's no missing dependency issue, then moving to check the chat action error messaging. Now I'm turning to how the new CodexProvider's advertised slash commands (like "feedback") get surfaced in the web composer, and whether there's an existing slash-command menu UI that consumes provider status to render these commands with their hints.
5. git_tools — slashCommands
6. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 I see the /feedback provider slash command already surfaces in the composer menu, which is consistent. But I want to check whether provider slash commands are normally sent through a separate path, and whether this new client-side interception might bypass how those commands get applied when selected.
7. git_tools — provider-slash-command
💭 I'm checking whether the feedback pseudo-messages render correctly through the timeline component, particularly around how they merge with optimistic user messages and pending server messages when combining local message lists. I want to trace whether pending messages get appended after server messages rather than sorted by creation time — in the mobile version there's a test asserting ordering by createdAt, so if web just appends pending items at the end, older feedback messages could end up displayed after newer server ones, which would be a real ordering inconsistency worth flagging.
8. browse_code — apps/web/src/components/ChatView.tsx
💭 I realize the ordering logic simply concatenates server messages followed by pending ones, so older feedback pseudo-messages end up appended after newer server messages once a new message is sent — meaning the feedback bubble would visually jump to the bottom out of chronological order. Mobile explicitly guarded against this exact bug by sorting messages by createdAt, so web appears inconsistent here. I should check whether deriveTimelineEntries actually sorts by createdAt to confirm this gap.
9. git_tools — export function deriveTimelineEntries
10. browse_code — apps/web/src/session-logic.ts
11. browse_code — apps/web/src/session-logic.ts
💭 Confirming ordering is preserved since deriveTimelineEntries sorts by createdAt, and with identical timestamps the stable sort keeps user before assistant since they're pushed in that order. Now I'm checking whether these pseudo-messages actually match the ChatMessage type web expects, since codexFeedbackMessage returns an OrchestrationMessage type that might differ. Turning to UI consistency in the changed web lines: the new "Copy ID" toast action doesn't show a success confirmation like sibling copy flows do (e.g., branch name or path copy toasts). I also want to check whether clicking a toast action closes the toast by default in this Base UI-based toast component, so I'm looking at how actionProps gets rendered in the toast implementation.
12. git_tools — actionProps
💭 I'm noticing that the Toast.Action primitive only renders the children prop, not spreading the rest of actionProps like onClick—so I'm checking whether Base UI's Toast.Action automatically merges toast.actionProps from context to make onClick work at other call sites. I'm now confirming that Base UI docs describe actionProps as being spread onto the action button, so onClick does work. I'm also wondering whether clicking the action button automatically closes the toast, since that behavior would matter for the timeout:0 cases like GitActionsControl's "Open pull request" and the copy ID action—if it doesn't auto-close, the toast would stay open indefinitely with timeout 0. Now I want to check whether there's an existing pattern for confirming success after copying inside a toast action, so I'm looking at how PreviewView handles copying a recording path to see if it updates the toast afterward.
13. browse_code — apps/web/src/components/preview/PreviewView.tsx
💭 I notice PreviewView places its "copy" action in a secondary outline-variant slot with a "Copied!" confirmation, while the new ChatView "Copy ID" uses the primary filled-variant slot with no confirmation—this looks like a real inconsistency worth flagging against the pattern of reusing durable UI conventions across call sites. I also want to verify whether Base UI's Toast.Action auto-closes the toast on click, since with a zero timeout that would dismiss the toast right after copying, and I need to check if that behavior is documented or vendored in the repo.
14. browse_code — In apps/web, how does Base UI's Toast.Action behave on click — does clicking the toast action button automatically close/dismiss the toast? Look for the @base-ui/react toast implementation (vendored or node_modules or patches) and any repo code/tests/comments that indicate whether actionProps.onClick also dismisses the toast. Answer with concrete file evidence.
💭 I'm noticing the "Copy ID" action lacks any success feedback, unlike the established patterns elsewhere in the repo—success toasts or transient "Copied!" labels via useCopyToClipboard. That inconsistency seems worth flagging. Now I want to check whether thread-related toasts, like
stackedThreadToast, properly scope themselves to the active thread using something likethreadRef, so I need to look at howThreadToastDataand similar toast calls in GitActionsControl handle that scoping. Then I'm checking whether the send button reflects a busy state while feedback uploads are in flight — sincefeedbackUploadsInFlightRefis a ref rather than state, clicking send during an upload wouldn't visibly disable the button, though this matches the existing pattern used forsendInFlightRef, so it's consistent rather than a new bug. Now I'm considering whether the synthetic assistant pseudo-message rendered in MessagesTimeline could expose broken action affordances — like copy, retry, or checkpoint revert — since these are typically keyed by a real message ID, and a fake ID here might cause those actions to fail silently. The bigger concern is the user-side pseudo-message: if it supports "Edit & resend" or "Restore checkpoint" like real messages do, invoking these could hit the server with a nonexistent message ID. Optimistic user messages get replaced by real server messages almost immediately, but feedback pseudo-messages stick around for the whole session, so I want to check MessagesTimeline to see if these per-message actions are gated by streaming state, turn ID, or server presence before assuming this is safe.
15. browse_code — apps/web/src/components/chat/MessagesTimeline*
16. git_tools — role === "user"
💭 I should check UserTimelineRow for per-message actions like edit, retry, or revert, since the pseudo user message will show hover actions keyed to a message ID the server never actually saw.
17. git_tools — function UserTimelineRow
18. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx
💭 I'm confirming the revert button only appears for real messages with diff summaries, so the pseudo message correctly has none. For the assistant row, the copy button would just copy the "Feedback sent to OpenAI" text with thread ID, which seems acceptable. Now I'm weighing whether feedback pseudo-messages persisting across the session (never cleaned from
feedbackSubmissionsByThreadKey) is a real issue versus intended local-only history — it's probably fine since reload clears them. I'm also noting a potential inconsistency: web gates the feedback UI on the composer's selected provider being "codex", while mobile also checks the thread's actual session provider name, which could diverge across platforms. The more concrete finding is that clicking "Copy ID" in the success toast gives no confirmation — no "Copied!" state, no toast dismissal — unlike every other copy affordance in the app that usesuseCopyToClipboard'sisCopiedstate. That's a genuine interaction-feedback gap worth flagging, though I'm uncertain whether it counts as a "concrete violation" under the reporting criteria since those seem scoped to migrations rather than general UX gaps. I want to check the rest of the ChatView diff for other in-scope issues — beyond that, only ChatView.tsx and MessagesTimeline.test.tsx changed, and the state/ref placements and feedback submission wiring look consistent. I'm also comparing whetherscrollToEnd()afterclearDraftmatches the anchoring approach used in the normal send path. I want to comparescrollToEndagainst the send path to see if the feedback flow is missing any behavior.
19. git_tools — scrollToEnd|anchorMessageId|setPendingAnchor
💭 I'm checking the code around line 5420-5460 to see how normal send decides between anchoring the timeline to the user's message versus just scrolling to the end.
20. browse_code — apps/web/src/components/ChatView.tsx
💭 The feedback path's scrollToEnd call matches the non-anchor branch, and I'm checking the ordering of state updates—submitCodexFeedback adds the submission first, then clearDraft triggers scrollToEnd via requestAnimationFrame after render, so that looks fine. I'm also verifying the composer draft is properly reset with promptRef cleared and cursor state reset, matching the normal path. Then I'm checking that "Codex feedback thread ID" reads correctly in clipboard error messages, and it does since it's a proper noun unlike other lowercase targets like "branch name" or "plan".
21. browse_code — apps/web/src/hooks/useCopyToClipboard.ts
💭 With clipboard handled, I'm scanning for other UI-consistency issues, like the missing confirmation on "Copy ID" and whether the persistent toast's dismiss button behaves correctly. I want to check whether
chatActionErrorMessageis actually meant to handle clipboard errors or if it's scoped only to atom command errors. Turning to the composer, I'm considering how provider slash commands like/feedbackget inserted and sent—previously it likely just went through as plain text to Codex, but now the web app may intercept it, which isn't a consistency problem per se. I'm also checking whether the gating onctxSelectedProvider === "codex"matters if the composer's selected provider diverges from the thread's actual session provider, but that seems fine since it would just be treated as a normal message. I also confirm there are no CSS changes to review, and I'm now shifting to think about the pseudo-message handling. Since both pseudo messages haveturnId: null, similar to optimistic user messages, I need to check whether real assistant messages ever have a null turnId, since turn-folding logic groups rows by turnId and a null-turn assistant message could land somewhere unexpected. I want to look at howbuildTimelineRowsin MessagesTimeline.logic actually handles a null turnId for assistant messages.
22. git_tools — turnId
💭 I'm checking whether feedback can be submitted mid-turn — since
onSendbails early when busy, it likely can't, but an existing feedback pair could linger in the timeline and end up preceding a newer normal message. I'm now weighing whether the missing copy-confirmation toast is worth flagging. It's a small deviation from the app's consistentuseCopyToClipboardsuccess-feedback pattern, but since the toast still displays the Thread ID and errors are surfaced, this feels like a borderline nit rather than a real violation. I want to check whetherrouteThreadKeyandactiveThreadKeycould diverge for a server thread, since feedback messages are stored underrouteThreadKeybut the timeline renders based on the active thread — if these keys ever mismatch, feedback could silently disappear. Let me trace how both are defined.
23. git_tools — const routeThreadKey|const activeThreadKey|const isServerThread|const composerDraftTarget
💭 I'm checking whether optimistic user messages are keyed by thread, since if they aren't, keying feedback by thread is fine or even preferable. The web implementation looks reasonable, though I need to keep in mind the instruction that this check defaults to failure and should produce "All clear" only if there are truly no findings. Given the schema requires structured JSON output, I need to reconcile that with the instruction to make the response exactly "All clear" — I'll set state to success and put "All clear" in the summary and title fields. Before finalizing, I want to double check there isn't a genuine consistency issue worth flagging, like how the pseudo user message for
/feedbackgets rendered and whether it interacts properly with the display-state logic for collapsible messages. Checking a few more spots: the working spinner during upload stays static with "Sending feedback to OpenAI..." text rather than showing a spinner, which seems acceptable, and the assistant's meta info like timestamp and copy button render normally without issue. The one thing that actually worries me is whetherMessageId.makeenforces a strict pattern that would reject an ID containing a colon, like${submission.id}:feedback— if the branded type validates against something like a UUID format, this would throw at runtime, so I should look at how MessageId is actually defined.
24. git_tools — export const MessageId
💭 I'm checking whether the feedback upload busy state, tracked via a ref rather than component state, could cause disabled or loading UI to fall out of sync with actual interaction state — but since it's not tied to render, this seems like a minor internal bookkeeping choice rather than a real accessibility violation. So the real issue is that using a ref instead of state means React never re-renders, meaning the composer's disabled/busy affordance can't reflect the upload in progress. The fix would be converting that ref into actual state so it can gate the send button like other long operations do — I recall
isRevertingCheckpointfollows this same pattern, using state to disable UI during in-flight operations.
25. git_tools — isSendBusy
💭 I'm confirming that the composer's send button state relies on
isSendBusyandsendDisabledReasonprops, but the feedback in-flight state is only tracked in a ref—so while feedback is uploading, the send button stays enabled visually, yet pressing Enter or clicking Send silently does nothing because of the new guard. This is a real UI inconsistency: the blocked state isn't reflected in the control. The minimal fix would be to track the feedback in-flight status in component state and pass it throughsendDisabledReason(something like "Sending feedback") so users get visible feedback, rather than reusingbeginLocalDispatchwhich would incorrectly show a spinner/stop-button "working" state. I'll leave an inline comment near the ref check around line 5096 suggesting this state-based approach so the composer properly surfaces the disabled reason. Given the check defaults to failure unless everything is clear, and this is a genuine (if moderate) usability gap — the feedback upload is a real websocket RPC that routes through the Codex adapter and can take several seconds, during which the timeline shows a pending pseudo-message but the composer doesn't visually block input — I'll report this as a failure with one finding rather than marking things "All clear." The fix itself would need to convert the ref-based guard into actual state and thread a disabled reason through the composer, which touches multiple lines and props, so I won't attempt an inline diff suggestion — just a clear comment describing the issue and pointing at the relevant guard logic using routeThreadKey. I'll post the review with that single inline comment.
26. modify_pr — One finding on the new Codex /feedback composer path in apps/web/src/components/ChatView.tsx: the in-flight upload blocks sending but is not surfaced in the composer's send affordance. The previously flagged clipboard issue is resolved now that the action routes through writeTextToClipboard.
Agent Credits: 419 credits