Skip to content

fix(shared): preserve user ultrathink labels

MacroscopeApp / Macroscope - UI Consistency failed Aug 27, 2026 in 4m 2s

UI Consistency: 1 issue found

1 finding

  • apps/web/src/components/chat/TraitsPicker.tsx / handleSelectChange (lines 311-319): the menu's blocked-state UI (ultrathinkInBodyText, line 158) strips /^Ultrathink:\s*/i, while the newly delegated resolveReasoningTransition only owns the exact Ultrathink:\n prefix. For drafts like Ultrathink: explain the term the menu renders the effort radio items enabled with no hint, but the shared transition returns blocked, so if (!change) return silently discards the pick (menu closes via closeOnClick, nothing changes, no feedback) \u2014 unlike the keyboard path, which toasts. Pre-PR the same click stripped the prefix and applied the level. Suggested fix posted inline: derive the menu's blocked state from the same owned prefix (ideally exported from @t3tools/shared/model).

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.

Scope reviewed (include patterns): apps/web/src/components/chat/ChatComposer.tsx, apps/web/src/components/chat/TraitsPicker.tsx, apps/web/src/components/settings/KeybindingsSettings.logic.ts, apps/web/src/composer-logic.ts, apps/web/src/keybindings.ts and their tests. packages/shared/src/model.ts, packages/contracts, and docs were read as context only.

Checked and found acceptable:

  • No new native controls or locally reconstructed primitives; TraitsPicker still composes Menu/MenuRadioGroup/MenuRadioItem/ComposerControl, and the closeOnClick + disabled semantics and explanatory comments are preserved.
  • No CSS/Tailwind ownership, theme-variant, scroll/mask, or layout changes in this PR.
  • No environment-scoped actions introduced (keybinding and reasoning logic are local).
  • resolveTraitsSelectChange extracted with focused tests; mapComposerCursorAcrossLeadingPromptChange preserves caret position across the prefix add/remove, so the earlier caret-jump concern is resolved.
  • getProviderModelCapabilities is called in the new keyboard path without planModeEnabled, unlike the traits menu and composerProviderState. Plan-mode filtering only removes the agent descriptor's plan option, so the reasoning descriptor is identical today; noted but not reported as a violation.

Since the previous run only packages/shared/src/model.ts and its tests changed (commits 6019931, 5d5d7c3); previously posted ChatComposer comments were not re-posted.


🔍 Agent activity — 13 tool calls

💭 I'm looking at which files changed within the apps/web/src TypeScript and CSS scope, noting the chat composer, traits picker, keybindings settings logic, and composer logic files along with their tests. Since this is a UI consistency check, I want to verify whether previously flagged ChatComposer issues across several past commits were actually resolved. Checking each one: the reasoning shortcut fall-through bug looks fixed, the cursor jump in setPromptFromTraits was addressed with the new cursor-mapping helper, and the editor-not-bound-to-draft-prompt issue seems resolved via the new draft-prompt tracking flag. I'm now digging into whether the fourth issue, about promptRef not reflecting the draft prompt during a pending question, was fully fixed or only partially — the guard condition still compares against a promptRef that may hold the answer prompt rather than the draft during that pending state. I want to trace whether this residual issue was actually addressed in the latest commit versus just being flagged again, and separately confirm whether the missing event.repeat guard was ever added.

1. git_tools — a011c09..5d5d7c3

2. git_tools — e67074f..5d5d7c3

💭 The two most recent commits after a011c09 only touch model.ts in packages/shared, so the ChatComposer issues flagged there haven't been addressed. I want to confirm this by checking the diff between a011c09 and head.

3. git_tools — a011c09..5d5d7c3

💭 Since the only changes since that commit are in packages/shared, which is outside this check's scope, the in-scope files themselves haven't changed—so there shouldn't be new findings from those files alone. But I need to check whether the shared package change, tightening the ultrathink prefix regex to require an exact "Ultrathink:\n" match instead of the previous case-insensitive, optional-newline pattern, breaks any behavior at the in-scope call sites like ChatComposer or TraitsPicker. Looking at TraitsPicker specifically: previously it stripped a case-insensitive "Ultrathink:" prefix, but now the tightened matcher blocks lowercase variants, causing resolveTraitsSelectChange to return null silently with no user feedback. This creates an inconsistency versus ChatComposer's keyboard path, which shows a toast when blocked—so the mouse-driven select in TraitsPicker now appears to accept a click but does nothing visibly, whereas before it would strip the prefix and proceed. I'm trying to determine whether this counts as a genuine violation introduced by the changed lines in TraitsPicker.tsx, since the early-return guard there is what causes this silent swallowing behavior.

4. browse_code — apps/web/src/components/chat/TraitsPicker.tsx

💭 I'm noticing an inconsistency: TraitsPicker's ultrathinkInBodyText check uses a loose regex stripping "Ultrathink:" with optional whitespace, while the shared transition logic now requires a strict "Ultrathink:\n" prefix. This mismatch means the menu could show a different state than what actually triggers the behavior, so I need to trace through specific prompt cases to see where they diverge. For a single-line prompt like "Ultrathink: explain the term," TraitsPicker sees it as ultrathink-controlled but treats the body text as not containing ultrathink, so the menu shows it as selected with no warning. But the shared transition logic treats the whole thing as body text since the strict newline prefix doesn't match, so it blocks any attempt to switch effort levels silently — the user clicks "Medium," the menu closes, and nothing actually changes with no feedback at all. That's a real regression from combining the stricter prefix regex with TraitsPicker's outdated loose check. The smallest fix would be aligning the menu's regex with the shared prefix pattern ^Ultrathink:\n instead of the looser one — that way a draft like "Ultrathink: explain the term" would strip consistently, trigger isClaudeUltrathinkPrompt, and produce the same warning/disabled state that the shared logic already computes as blocked. I'm also checking whether the toast wording ("Remove "ultrathink" from the prompt text to change reasoning") matches the menu's warning text closely enough, and confirming that resolveTraitsSelectChange returning null on an "unchanged" status is intentional since there's nothing to update in that case. I'm also noticing another behavioral difference: previously selecting any descriptor rebuilt all selections from scratch including defaults, whereas now the reasoning path only patches the reasoning value into existing modelOptions, preserving unrelated and unknown selections instead of overwriting them. This seems like an intentional change rather than a regression. Checking the ultrathink detection logic, both the old and new paths seem to treat "ultrathink" appearing anywhere in the prompt as sufficient to trigger the label and radio selection, so behavior there looks consistent between versions. However, allowPromptInjectedEffort bothers me — TraitsPicker respects this gate, but ChatComposer's keyboard shortcut path calls resolveReasoningTransition without checking it at all. I need to verify whether any call sites actually pass allowPromptInjectedEffort={false}, since if some surface disallows prompt injection but ChatComposer still injects the "Ultrathink:" prefix via keyboard shortcut, that's a real inconsistency worth tracking down. Also, getProviderModelCapabilities takes a fourth argument, planModeEnabled, which TraitsPicker passes but ChatComposer's keyboard path appears to omit — that could produce a different set of available reasoning descriptors than what the traits menu shows, since plan mode changes which options are valid. This looks like a concrete consistency bug worth confirming against the actual function signature.

5. git_tools — getProviderModelCapabilities

6. git_tools — allowPromptInjectedEffort

💭 That settings-panel flag isn't relevant to the keyboard path I'm tracing. I'm now checking whether getProviderModelCapabilities's fourth parameter, planModeEnabled, is being passed correctly in ChatComposer's new keyboard path, since it currently seems to call it with only three args — I need to check if the composer tracks plan mode state at all.

7. browse_code — apps/web/src/providerModels.ts

8. git_tools — planModeEnabled

💭 I notice ChatComposer's keyboard path omits settings.planModeEnabled when calling getProviderModelCapabilities, unlike the traits menu and composerProviderState. But since plan-mode filtering only strips the "plan" option from the agent descriptor and leaves reasoning descriptors untouched, this omission shouldn't actually cause any divergence in reasoning behavior—though I want to double check withoutPlanAgentOption to be sure.

9. browse_code — apps/web/src/components/chat/ChatComposer.tsx

10. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 I'm noticing another gap: when blocked, the keyboard path shows a toast but the traits menu silently discards the action with no feedback, creating inconsistent behavior between the two entry points.

11. browse_code — packages/shared/src/model.ts

💭 I'm tracing through a divergence case where the prompt is "Ultrathink: explain the term" without a newline. In TraitsPicker, the regex check confirms ultrathink is present, but stripping the "Ultrathink: " prefix and testing the remaining body text "explain the term" against the same detector returns false—so the two checks disagree. That means no warning appears and items stay enabled, showing "Ultrathink" as selected in the radio. But clicking "Medium" triggers the shared resolver, which treats the whole prompt as body text (since there's no newline to match the leading prefix pattern) and sees ultrathink present there, so it blocks the change entirely—the menu just closes with nothing happening. Before this PR, the same scenario would have worked because the old logic stripped the loose "Ultrathink:" prefix and applied medium effort successfully, so this silent no-op is a real regression from the stricter shared regex. I'm also considering another case where "ultrathink:" is lowercase, which fails the case-sensitive leading pattern but still gets flagged by the body-text check, causing another block. This same class of bug appears whether users type prefixes manually or drafts persist from earlier versions using different formatting—I want to check what applyClaudePromptEffortPrefix actually generates to see if the prefix form it writes matches what the regex expects.

12. browse_code — packages/shared/src/model.ts

💭 The real bug is that TraitsPicker's ultrathink-detection logic doesn't match the shared transition's definition of the owned prefix, so the menu shows "Ultrathink" as selected even though clicking does nothing. The fix is to align that detection with how the prefix is actually stripped elsewhere. I'm double-checking a few other candidates — the identity comparisons on modelOptions in both the transition and ChatComposer paths look fine since they correctly skip updates when the reference is unchanged, and planModeEnabled being omitted from the keyboard path doesn't seem to affect reasoning descriptors either. I'm now examining the TraitsPicker's ultrathink transition logic, specifically how leaving ultrathink now updates modelOptions inline rather than returning early like it used to. With a slash command, selecting Ultrathink resolves to "unchanged" in both old and new code, so the click is silently ignored either way without user feedback — it's a pre-existing gap, not a regression. The menu still shows an enabled Ultrathink radio item that effectively does nothing in that case. I want to keep my review comment focused on the disabled/warning divergence rather than folding in this secondary issue. Now I'm checking exact line numbers for anchoring the inline comment — targeting TraitsPicker.tsx around line 319 where the if (!change) return; guard sits, since that's part of the changed diff lines. The core mismatch I want to flag: the menu's blocked-state logic still strips a looser Ultrathink: prefix pattern to decide whether to show the warning and disable the radio items, but resolveReasoningTransition now only recognizes the stricter exact prefix. That means a draft like "Ultrathink: explain the term" would show up as enabled with no warning in the UI, even though the underlying transition logic treats it differently. I think the fix is to make ultrathinkInBodyText derive from the same owned-prefix check as resolveReasoningTransition, ideally by exporting that check from the shared model package so both paths stay consistent. I'm also noting that other blocked/non-applicable statuses beyond "changed" get silently dropped without any toast feedback, which is worth calling out too—though since the fix touches a line outside the current diff hunk, I need to think about how to present it cleanly. Actually I'm wondering about that unsupported toast in ChatComposer — the copy is technically misleading for the "unsupported-prompt-injected-value" reason case, since it claims the model doesn't advertise reasoning levels when really the target level is just an unreachable injected value. But that path isn't currently reachable except for future values beyond ultrathink, so it's a low-priority nit rather than a real bug worth flagging strongly. I'm confirming that cycling on a model with reasoning options while there's an active pending progress transition still applies cleanly since it only changes modelOptions. I'll go ahead and post just this one comment, and I need to pin down the right line range in the diff — targeting lines 311 through 319 since those are all added lines in the new file, and I need to double check that GitHub's line numbering for review comments matches the new-side line numbers I'm viewing.

13. modify_pr — One consistency finding in apps/web/src/components/chat/TraitsPicker.tsx: the traits menu's blocked-state UI no longer agrees with the shared reasoning transition it now delegates to, so some clicks are silently discarded.


Agent Credits: 111 credits