Repository navigation
UI Consistency: 1 new finding, 1 unresolved
New finding
apps/web/src/components/chat/ChatComposer.tsx(lines 2586-2589): the newalt+,/alt+.reasoning-cycle handler applies prompt changes throughsetPromptFromTraits, the mouse-menu setter that collapses the cursor to the end of the prompt and callsfocusAtEnd(). On the keyboard path the textarea is normally already focused, so adding/removing theUltrathink:\nprefix jumps the caret to the end of the draft and loses the user's typing position. Suggested fix: apply the prompt with a caret-preserving update (the pattern used byremoveComposerTerminalContextFromDraft), offsetting the existing cursor by the prefix length. Inline comment posted.
Previously reported, still unresolved (not re-posted)
apps/web/src/components/chat/ChatComposer.tsx(lines 2567-2573): reasoning shortcuts are claimed viapreventDefault/stopPropagationbut then fall through the stash-specific guards. WithpendingUserInputs.length > 0 && !isComposerApprovalState, pressing a reasoning shortcut toggles the unrelated stash picker; in approval / project-selection /activePendingProgressstates it is silently swallowed with no feedback, unlike the toast paths added below. The reasoning branch should be handled before the stash-only guards, or those guards scoped tocomposer.stash.
No Tailwind/CSS ownership, theme-variant, shared-primitive, scroll/mask, or environment-routing violations were found in the changed web files.
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/chat/ChatComposer.tsx, apps/web/src/components/chat/TraitsPicker.tsx, apps/web/src/components/chat/TraitsPicker.test.ts, apps/web/src/keybindings.ts, apps/web/src/keybindings.test.ts, apps/web/src/components/settings/KeybindingsSettings.logic.ts. Context read outside scope: packages/shared/src/model.ts, apps/web/src/providerModels.ts, apps/web/src/components/ChatView.tsx, apps/web/src/components/settings/SettingsPanels.tsx, apps/web/src/components/settings/ProjectSettingsPanel.tsx.
Checks performed and cleared:
- No styling/class or CSS changes in the diff, so Tailwind ownership, theme-variant, emitted-CSS,
MenuPopupwidth defaulting, and scroll/mask rules are not engaged. TraitsPickercontinues to use the sharedMenu/MenuRadioGroup/MenuRadioItemprimitives; the extractedresolveTraitsSelectChangekeeps the non-reasoning descriptor path intact and preserves the existingultrathinkInBodyTextdisabled state and inline explanation, and it has focused unit tests for both branches.- The keyboard path calls
getProviderModelCapabilitieswithoutplanModeEnabled; verified that argument only filters the OpenCodeagentoption, so reasoning descriptors are identical to the menu's view. - Settings call sites pass
allowPromptInjectedEffort={false}withprompt=""and a no-oponPromptChange; traced that selecting a prompt-injected value there remains a no-op (same as before), so the shared transition ignoring that flag is not observable. EVENT_CODE_KEY_ALIASESadditions forComma/Periodonly add physical-position aliases and preserve later-binding precedence; covered by new tests, including terminal-focus suppression.- Reference-equality guards (
transition.modelOptions !== currentModelOptions) are safe becauseupdateReasoningSelectionreturns the same array when nothing changes.
🔍 Agent activity — 16 tool calls
💭 I want to check the diff between the prior flagged issue at ChatComposer.tsx line 2411 about stash guards and the current commit to see if that code changed in response, so I'm looking at the ChatComposer handler now.
1. git_tools — bab9ac2..940d383
2. git_tools — reasoningCycleDirectionFromCommand
💭 I want to look at the ChatComposer handler code around lines 2540-2650 to see what's there.
3. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 The unresolved stash guard issue is still there since the code didn't change, so per the rules I shouldn't repost the same comment—but I still need to fail the check since the finding remains unaddressed. Now let me check TraitsPicker.tsx for any UI consistency issues in the changed area.
4. browse_code — apps/web/src/components/chat/TraitsPicker.tsx
💭 I'm checking whether TraitsMenuContent's onSelect handling silently blocks actions without user feedback, and I'm noticing a potential inconsistency: the menu content computes capabilities with planModeEnabled factored in, while ChatComposer's keyboard path calls getProviderModelCapabilities without that flag. If plan mode affects available reasoning descriptors, this would mean the keyboard shortcut path and the picker UI could disagree on capabilities. I want to verify this by checking the actual function signature.
5. git_tools — export function getProviderModelCapabilities
💭 The planModeEnabled flag only affects the agent descriptor, not reasoning, so that's not relevant here. Now I want to check TraitsMenuContent to see if the changed onSelect logic strips out toast or UI feedback behavior, so I'll look at lines 260-360.
6. browse_code — apps/web/src/components/chat/TraitsPicker.tsx
💭 I'm checking whether the new resolveTraitsSelectChange should also account for allowPromptInjectedEffort, since the current implementation only sources caps from getProviderModelCapabilities and doesn't gate on that flag like getSelectedTraits does.
7. browse_code — apps/web/src/components/chat/TraitsPicker.tsx
💭 I'm noticing a behavior gap: the old code gated ultrathinkPromptControlled behind allowPromptInjectedEffort, but the new resolveReasoningTransition ignores that flag entirely, detecting prompt-injected ultrathink unconditionally from the descriptor and prompt text. That means on surfaces where allowPromptInjectedEffort is false, the menu could still show a selectedValue based on the persisted effort rather than respecting that restriction. I'm also comparing how non-ultrathink selections behave when ultrathink is already embedded in the prompt body — both versions treat this as a silent no-op, though there's a keyboard-path toast in ChatComposer to check. Now I'm looking at how applyClaudePromptEffortPrefix handles inserting the ultrathink prefix and whether that differs from the new prefix-stripping logic. I'm tracing a subtle divergence: the display logic and the shared transition logic use slightly different regexes to strip the "Ultrathink:" prefix, which could cause the body-text detection and menu warning behavior to disagree on prompts with embedded newlines after the prefix. Tracing the whitespace difference: selecting medium leaves a leading space in the retained prompt body versus the old code which stripped it fully—likely intentional per the tests, so not a real inconsistency to flag. The bigger question now is finding where allowPromptInjectedEffort actually evaluates to false.
8. git_tools — allowPromptInjectedEffort
💭 In the Settings panels, since prompt injected effort is disabled, I want to check what prompt gets passed there—likely an empty prompt with a no-op change handler.
9. browse_code — apps/web/src/components/settings/SettingsPanels.tsx
💭 I'm confused by the mismatch between the grep result and the file view at that line number, so I want to re-check with grep context to see what's actually going on.
10. git_tools — allowPromptInjectedEffort={false}
💭 I'm tracing through the old settings logic: since onPromptChange was a no-op there, selecting "Ultrathink" for a Claude default model never actually persisted anything—the old code would set the prompt prefix internally but the change never stuck, making the selection effectively harmless but meaningless. With the new logic, since prompt is always empty in settings and allowPromptInjectedEffort is false, there's no divergence from the old behavior—modelOptions stays the same reference so nothing updates, matching the old no-op outcome. I'm checking whether the new code introduces any regression here, and so far it looks equivalent. Looking beyond that edge case, I'm scoping the review against the shared UI consistency rules—Tailwind ownership, shared primitives, themes, scroll behavior—but this diff barely touches styling, so any real findings will be about behavioral consistency in the changed code rather than visual issues. I also recall the previous run flagged reasoning shortcuts slipping past stash guards; since that code section is unchanged, the convention against reposting duplicate comments means I shouldn't re-flag it, though I'm unsure whether the check should still fail given the issue is still present. Given the default-failure rule triggers on any finding, and this one remains unresolved, I think it's fair to fail the check while referencing the earlier comment rather than repeating it. I also want to confirm whether the head commit introduces anything new since the prior review, since the base commit hash doesn't match — likely a rebase or force-push — so I should scan again rather than assume nothing changed. I'm considering two possible issues: a toast message that could be misleading for rare reason codes like unsupported-prompt-injected-value, and a keyboard handling path in ChatComposer that appears to duplicate transition logic instead of reusing the shared select-change resolver. Checking further, though, the keyboard path and TraitsPicker menu both ultimately route through the same shared reasoning-transition resolver, so the duplication concern doesn't hold up — it's just two different UI surfaces consuming the same source of truth, and their feedback styles (inline message vs toast) are different but not inconsistent in a problematic way. I also verify that the reference equality check against currentModelOptions is safe, since the resolver returns the same object reference when nothing changes, so no unnecessary updates would occur there. I want to check whether the keyboard handler respects
allowPromptInjectedEffortandplanModeEnabled, though the latter should only affect the "agent" descriptor rather than reasoning itself, so it's likely fine. I also want to verify the default keybindings for alt+, and alt+. don't collide with existing shortcuts or cause issues on macOS, where Option+comma normally types a special character — since the handler intercepts at the window level with capture, I need to confirm it properly prevents that default behavior. Since the listener captures globally, it could unintentionally intercept alt+,/alt+. while a user is typing in unrelated text fields like a search box or rename input, blocking the "≤"/"≥" characters they might actually want to type, and it's unclear whether it stays scoped correctly when the composer isn't even mounted. I want to check whether other parts of the settings UI, like the keybinding display formatter, correctly render "," and "." symbols, and whether the EVENT_CODE_KEY_ALIASES addition for Comma/Period keys is applied consistently everywhere it's needed.
11. browse_code — apps/web/src/keybindings.ts
💭 These aliases apply regardless of altKey and are harmless on most layouts, since layoutKey would already match; on layouts where the physical key differs, this is an acceptable intentional fallback. Now I want to check what genuine UI consistency issues actually exist in the files under scope. The prior stash-guard fallthrough comment was the strongest finding, but since the code is unchanged, I shouldn't repeat it — even though the line numbers shifted from 2411 to around 2558-2573, likely due to a rebase, the underlying behavior is the same and was already flagged. I'm looking for something genuinely new instead. The toast copy with typographic quotes seems fine, and the composer model picker interaction looks unrelated. I want to verify whether the keyboard-driven reasoning cycle still works correctly when the composer is locked, like during an active thread run, and I should check what setPromptFromTraits actually does — whether it also closes menus or repositions the caret.
12. git_tools — setPromptFromTraits
💭 I notice
setPromptFromTraitsmoves the cursor to the end and refocuses, which is fine for the traits menu clicked with a mouse. But using alt+. mid-draft to trigger Ultrathink would prepend the prefix and jump the caret to the end, wiping out the user's cursor position or selection—a real regression from the new keyboard shortcut. Now I want to verify whethersetComposerCursoractually manipulates the DOM textarea's selection, so I'm checkingscheduleComposerFocusandsetComposerCursorto confirm the caret truly moves in the browser.
13. git_tools — const scheduleComposerFocus
💭 I'm noticing that using alt+. mid-draft ends up moving the caret to the end of the text since focusAtEnd() gets triggered, which is a real regression compared to the mouse path where focus already sits in the menu. This only actually happens when the prompt content changes, like when Ultrathink gets added or removed for Claude models—for Codex and similar where the prompt stays the same, this issue wouldn't surface. This does feel worth flagging under keyboard behavior and interaction semantics preservation, since it's a concrete caret-position regression. I want to double-check how focusAtEnd is actually implemented to confirm it collapses the selection to the end.
14. git_tools — focusAtEnd
💭 There's a real regression here: the keyboard shortcut reuses the mouse-menu prompt setter, which focuses at the end of the text, so mid-draft typing position gets lost when the "Ultrathink:" prefix is inserted or removed. The fix would be to restore the caret offset after mutating the prompt via keyboard, adjusting for the prefix length using the available
focusAt(cursor)handle. I'm confirming the caret behavior traces through the composer cursor state and focus scheduling to land at the end each time, and considering whether the composer is even guaranteed to be focused when the shortcut fires — if it isn't, jumping focus is fine, but if it is, the caret jump is a genuine bug worth flagging with a precise, non-duplicative comment. Now I'm pinpointing the exact lines in the diff — the prompt-setting call sits within the hunk, distinct from the earlier stash-guard comment, and I'm double-checking that the effect's dependency array correctly omitspromptsince it relies on the ref instead. I'm also noticing a possible mismatch in the toast messaging, where the "unsupported" wording may not account for other rejection reasons like a missing-choices or injected-value case. I check the keyboard-path toast versus inline menu text and confirm they're consistent, verify the traits picker still correctly no-ops on the blocked ultrathink case just as before, and note that a reference comparison in the model options change detection is harmless since the non-reasoning branch always rebuilds the array. I'm ready to post a single review comment on the caret issue and mark the check as failing, tying it back to both relevant spots. For a fix suggestion, I don't think it's self-contained enough to inline as a diff since it needs caret math plus something like a focusAt API, so I'll describe the approach instead. I'm poking around ChatComposer to see if there's already a helper like applyPromptChange or a setComposerDraftPrompt variant that preserves cursor position when updating the prompt.
15. git_tools — setComposerCursor(
💭 There's already an established pattern (removeComposerTerminalContextFromDraft) for preserving the caret during programmatic prompt mutations, so I'll suggest that as the fix in an inline comment on line 2588 without a diff. I'll leave the stash-guard concern for the summary rather than repeating it inline, then post the review.
16. modify_pr — Reviewed the web-side changes for UI/interaction consistency. One new finding on the keyboard reasoning-cycle path in ChatComposer.tsx (caret position lost when the Ultrathink prefix is added/removed). The previously reported issue — reasoning shortcuts falling through the stash-specific guards (pendingUserInputs opens the stash picker, approval/project-selection/pending-progress silently swallows the command) — still applies at ChatComposer.tsx lines 2567-2573 and is not re-posted here.
Agent Credits: 133 credits